blob: 1557af1b2b51e31d46dff4f8a6426e754d0cf0f3 [file] [log] [blame]
package main
import (
"bytes"
"html/template"
"net/url"
"strings"
"code.hackerspace.pl/hscloud/devtools/hackdoc/config"
"github.com/gabriel-vasile/mimetype"
"github.com/golang/glog"
"github.com/russross/blackfriday/v2"
)
// renderMarkdown renders markdown to HTML, replacing all relative (intra-hackdoc) links with version that have ref set.
func renderMarkdown(input []byte, ref string) []byte {
r := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
Flags: blackfriday.CommonHTMLFlags | blackfriday.TOC,
})
// master is the default branch - do not make special links for that, as
// that makes them kinda ugly.
if ref == flagGitwebDefaultBranch {
ref = ""
}
parser := blackfriday.New(blackfriday.WithRenderer(r), blackfriday.WithExtensions(blackfriday.CommonExtensions))
ast := parser.Parse(input)
// Render table of contents (raw HTML) into bytes.
var tocB bytes.Buffer
tocB.Write([]byte(`<div class="toc">`))
r.RenderHeader(&tocB, ast)
tocB.Write([]byte(`</div>`))
toc := tocB.Bytes()
var buf bytes.Buffer
buf.Write([]byte(`<div class="content">`))
// Render Markdown with some custom behaviour.
ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
// Fix intra-hackdoc links to contain ?ref=
if ref != "" && entering && node.Type == blackfriday.Link || node.Type == blackfriday.Image {
dest := string(node.Destination)
u, err := url.Parse(dest)
if err == nil && !u.IsAbs() {
q := u.Query()
q["ref"] = []string{ref}
u.RawQuery = q.Encode()
node.Destination = []byte(u.String())
glog.V(10).Infof("link fix %q -> %q", dest, u.String())
}
}
// Replace [TOC] anchor with a rendered TOC.
if entering && node.Type == blackfriday.Text && string(node.Literal) == "[TOC]" {
buf.Write(toc)
return blackfriday.GoToNext
}
return r.RenderNode(&buf, node, entering)
})
buf.Write([]byte(`</div>`))
r.RenderFooter(&buf, ast)
return buf.Bytes()
}
type pathPart struct {
Label string
Path string
}
func (r *request) renderable(dirpath string) bool {
cfg, err := config.ForPath(r.ctx, r.source, dirpath)
if err != nil {
glog.Errorf("could not get config for path %q: %v", dirpath, err)
return false
}
for _, f := range cfg.DefaultIndex {
fpath := dirpath + "/" + f
file, err := r.source.IsFile(r.ctx, fpath)
if err != nil {
glog.Errorf("IsFile(%q): %v", fpath, err)
return false
}
if file {
return true
}
}
return false
}
func (r *request) handleFile(path string, cfg *config.Config) {
data, err := r.source.ReadFile(r.ctx, path)
if err != nil {
glog.Errorf("ReadFile(%q): %w", err)
r.handle500()
return
}
// TODO(q3k): do MIME detection instead.
if strings.HasSuffix(path, ".md") {
rendered := renderMarkdown([]byte(data), r.ref)
r.logRequest("serving markdown at %s, cfg %+v", path, cfg)
// TODO(q3k): allow markdown files to override which template to load
tmpl, ok := cfg.Templates["default"]
if !ok {
glog.Errorf("No default template found for %s", path)
// TODO(q3k): implement fallback template
r.w.Write(rendered)
return
}
pathInDepot := strings.TrimPrefix(path, "//")
pathParts := []pathPart{
{Label: "//", Path: "/"},
}
parts := strings.Split(pathInDepot, "/")
fullPath := ""
for i, p := range parts {
label := p
if i != len(parts)-1 {
label = label + "/"
}
fullPath += "/" + p
target := fullPath
if i != len(parts)-1 && !r.renderable("/"+fullPath) {
target = ""
}
pathParts = append(pathParts, pathPart{Label: label, Path: target})
}
vars := map[string]interface{}{
"Rendered": template.HTML(rendered),
"Title": path,
"Path": path,
"PathInDepot": pathInDepot,
"PathParts": pathParts,
"HackdocURL": flagHackdocURL,
"WebLinks": r.source.WebLinks(pathInDepot),
}
err = tmpl.Execute(r.w, vars)
if err != nil {
glog.Errorf("Could not execute template for %s: %v", err)
}
return
}
// Just serve the file.
var mime string
if strings.HasSuffix(path, ".js") {
// Force .js to always be the correct MIME type.
mime = "text/javascript"
} else {
// Otherwise, use magic to detect type.
mime = mimetype.Detect(data).String()
}
r.w.Header().Set("Content-Type", mime)
r.w.Write(data)
}