| 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) |
| } |