blob: eaed905c184e4434db7df59711df85621f218533 [file] [log] [blame]
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +02001package main
2
3import (
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +02004 "bytes"
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +02005 "html/template"
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +02006 "net/url"
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +02007 "strings"
8
9 "code.hackerspace.pl/hscloud/devtools/hackdoc/config"
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020010
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020011 "github.com/gabriel-vasile/mimetype"
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020012 "github.com/golang/glog"
13 "gopkg.in/russross/blackfriday.v2"
14)
15
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020016// renderMarkdown renders markdown to HTML, replacing all relative (intra-hackdoc) links with version that have ref set.
17func renderMarkdown(input []byte, ref string) []byte {
18 r := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
Sergiusz Bazanski5bce7ce2020-04-11 20:16:58 +020019 Flags: blackfriday.CommonHTMLFlags | blackfriday.TOC,
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020020 })
21
22 parser := blackfriday.New(blackfriday.WithRenderer(r), blackfriday.WithExtensions(blackfriday.CommonExtensions))
23 ast := parser.Parse(input)
24
25 var buf bytes.Buffer
Sergiusz Bazanski5bce7ce2020-04-11 20:16:58 +020026 buf.Write([]byte(`<div class="toc"><h1>Page Contents</h1>`))
27 r.RenderHeader(&buf, ast)
28 buf.Write([]byte(`</div><div class="content">`))
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020029 ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
Sergiusz Bazanski4b4a33a2020-04-13 01:35:33 +020030 if ref != "" && entering && node.Type == blackfriday.Link || node.Type == blackfriday.Image {
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020031 dest := string(node.Destination)
32 u, err := url.Parse(dest)
33 if err == nil && !u.IsAbs() {
34 q := u.Query()
35 q["ref"] = []string{ref}
36 u.RawQuery = q.Encode()
37 node.Destination = []byte(u.String())
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020038 glog.Infof("link fix %q -> %q", dest, u.String())
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020039 }
40 }
41 return r.RenderNode(&buf, node, entering)
42 })
Sergiusz Bazanski5bce7ce2020-04-11 20:16:58 +020043 buf.Write([]byte(`</div>`))
44 r.RenderFooter(&buf, ast)
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020045 return buf.Bytes()
46}
47
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020048type pathPart struct {
49 Label string
50 Path string
51}
52
Serge Bazanskid701c4e2020-08-10 17:59:59 +020053func (r *request) renderable(dirpath string) bool {
54 cfg, err := config.ForPath(r.ctx, r.source, dirpath)
55 if err != nil {
56 glog.Errorf("could not get config for path %q: %v", dirpath, err)
57 return false
58 }
59
60 for _, f := range cfg.DefaultIndex {
61 fpath := dirpath + "/" + f
62 file, err := r.source.IsFile(r.ctx, fpath)
63 if err != nil {
64 glog.Errorf("IsFile(%q): %v", fpath, err)
65 return false
66 }
67
68 if file {
69 return true
70 }
71 }
72
73 return false
74}
75
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020076func (r *request) handleFile(path string, cfg *config.Config) {
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020077 data, err := r.source.ReadFile(r.ctx, path)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020078 if err != nil {
79 glog.Errorf("ReadFile(%q): %w", err)
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020080 r.handle500()
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020081 return
82 }
83
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020084 // TODO(q3k): do MIME detection instead.
85 if strings.HasSuffix(path, ".md") {
86 rendered := renderMarkdown([]byte(data), r.ref)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020087
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020088 r.logRequest("serving markdown at %s, cfg %+v", path, cfg)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020089
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020090 // TODO(q3k): allow markdown files to override which template to load
91 tmpl, ok := cfg.Templates["default"]
92 if !ok {
93 glog.Errorf("No default template found for %s", path)
94 // TODO(q3k): implement fallback template
95 r.w.Write(rendered)
96 return
97 }
98
99 pathInDepot := strings.TrimPrefix(path, "//")
100 pathParts := []pathPart{
101 {Label: "//", Path: "/"},
102 }
103 parts := strings.Split(pathInDepot, "/")
104 fullPath := ""
105 for i, p := range parts {
106 label := p
107 if i != len(parts)-1 {
108 label = label + "/"
109 }
110 fullPath += "/" + p
Serge Bazanskid701c4e2020-08-10 17:59:59 +0200111 target := fullPath
112 if i != len(parts)-1 && !r.renderable("/"+fullPath) {
113 target = ""
114 }
115 pathParts = append(pathParts, pathPart{Label: label, Path: target})
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +0200116 }
117
118 vars := map[string]interface{}{
119 "Rendered": template.HTML(rendered),
120 "Title": path,
121 "Path": path,
122 "PathInDepot": pathInDepot,
123 "PathParts": pathParts,
124 "HackdocURL": flagHackdocURL,
125 "WebLinks": r.source.WebLinks(pathInDepot),
126 }
127 err = tmpl.Execute(r.w, vars)
128 if err != nil {
129 glog.Errorf("Could not execute template for %s: %v", err)
130 }
131
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200132 return
133 }
134
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +0200135 // Just serve the file.
136 mime := mimetype.Detect(data)
137 r.w.Header().Set("Content-Type", mime.String())
138 r.w.Write(data)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200139}