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(`
`)) r.RenderHeader(&tocB, ast) tocB.Write([]byte(`
`)) toc := tocB.Bytes() var buf bytes.Buffer buf.Write([]byte(`
`)) // 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(`
`)) 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) }