blob: a6f206dc17c9f4387902152c5f824ac28847a645 [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
Serge Bazanski26f44da2020-09-23 18:12:11 +000022 // master is the default branch - do not make special links for that, as
23 // that makes them kinda ugly.
Serge Bazanski81262ff2021-03-06 20:44:56 +000024 if ref == flagGitwebDefaultBranch {
Serge Bazanski26f44da2020-09-23 18:12:11 +000025 ref = ""
26 }
27
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020028 parser := blackfriday.New(blackfriday.WithRenderer(r), blackfriday.WithExtensions(blackfriday.CommonExtensions))
29 ast := parser.Parse(input)
30
Serge Bazanski0a2f4132020-09-23 18:13:05 +000031 // Render table of contents (raw HTML) into bytes.
32 var tocB bytes.Buffer
33 tocB.Write([]byte(`<div class="toc">`))
34 r.RenderHeader(&tocB, ast)
35 tocB.Write([]byte(`</div>`))
36 toc := tocB.Bytes()
37
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020038 var buf bytes.Buffer
Serge Bazanski0a2f4132020-09-23 18:13:05 +000039 buf.Write([]byte(`<div class="content">`))
40 // Render Markdown with some custom behaviour.
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020041 ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
Serge Bazanski0a2f4132020-09-23 18:13:05 +000042 // Fix intra-hackdoc links to contain ?ref=
Sergiusz Bazanski4b4a33a2020-04-13 01:35:33 +020043 if ref != "" && entering && node.Type == blackfriday.Link || node.Type == blackfriday.Image {
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020044 dest := string(node.Destination)
45 u, err := url.Parse(dest)
46 if err == nil && !u.IsAbs() {
47 q := u.Query()
48 q["ref"] = []string{ref}
49 u.RawQuery = q.Encode()
50 node.Destination = []byte(u.String())
Serge Bazanski0a2f4132020-09-23 18:13:05 +000051 glog.V(10).Infof("link fix %q -> %q", dest, u.String())
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020052 }
53 }
Serge Bazanski0a2f4132020-09-23 18:13:05 +000054 // Replace [TOC] anchor with a rendered TOC.
55 if entering && node.Type == blackfriday.Text && string(node.Literal) == "[TOC]" {
56 buf.Write(toc)
57 return blackfriday.GoToNext
58 }
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020059 return r.RenderNode(&buf, node, entering)
60 })
Sergiusz Bazanski5bce7ce2020-04-11 20:16:58 +020061 buf.Write([]byte(`</div>`))
62 r.RenderFooter(&buf, ast)
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020063 return buf.Bytes()
64}
65
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020066type pathPart struct {
67 Label string
68 Path string
69}
70
Serge Bazanskid701c4e2020-08-10 17:59:59 +020071func (r *request) renderable(dirpath string) bool {
72 cfg, err := config.ForPath(r.ctx, r.source, dirpath)
73 if err != nil {
74 glog.Errorf("could not get config for path %q: %v", dirpath, err)
75 return false
76 }
77
78 for _, f := range cfg.DefaultIndex {
79 fpath := dirpath + "/" + f
80 file, err := r.source.IsFile(r.ctx, fpath)
81 if err != nil {
82 glog.Errorf("IsFile(%q): %v", fpath, err)
83 return false
84 }
85
86 if file {
87 return true
88 }
89 }
90
91 return false
92}
93
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020094func (r *request) handleFile(path string, cfg *config.Config) {
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020095 data, err := r.source.ReadFile(r.ctx, path)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020096 if err != nil {
97 glog.Errorf("ReadFile(%q): %w", err)
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020098 r.handle500()
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020099 return
100 }
101
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +0200102 // TODO(q3k): do MIME detection instead.
103 if strings.HasSuffix(path, ".md") {
104 rendered := renderMarkdown([]byte(data), r.ref)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200105
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +0200106 r.logRequest("serving markdown at %s, cfg %+v", path, cfg)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200107
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +0200108 // TODO(q3k): allow markdown files to override which template to load
109 tmpl, ok := cfg.Templates["default"]
110 if !ok {
111 glog.Errorf("No default template found for %s", path)
112 // TODO(q3k): implement fallback template
113 r.w.Write(rendered)
114 return
115 }
116
117 pathInDepot := strings.TrimPrefix(path, "//")
118 pathParts := []pathPart{
119 {Label: "//", Path: "/"},
120 }
121 parts := strings.Split(pathInDepot, "/")
122 fullPath := ""
123 for i, p := range parts {
124 label := p
125 if i != len(parts)-1 {
126 label = label + "/"
127 }
128 fullPath += "/" + p
Serge Bazanskid701c4e2020-08-10 17:59:59 +0200129 target := fullPath
130 if i != len(parts)-1 && !r.renderable("/"+fullPath) {
131 target = ""
132 }
133 pathParts = append(pathParts, pathPart{Label: label, Path: target})
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +0200134 }
135
136 vars := map[string]interface{}{
137 "Rendered": template.HTML(rendered),
138 "Title": path,
139 "Path": path,
140 "PathInDepot": pathInDepot,
141 "PathParts": pathParts,
142 "HackdocURL": flagHackdocURL,
143 "WebLinks": r.source.WebLinks(pathInDepot),
144 }
145 err = tmpl.Execute(r.w, vars)
146 if err != nil {
147 glog.Errorf("Could not execute template for %s: %v", err)
148 }
149
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200150 return
151 }
152
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +0200153 // Just serve the file.
Serge Bazanski56c262f2021-03-23 15:50:19 +0000154 var mime string
155 if strings.HasSuffix(path, ".js") {
156 // Force .js to always be the correct MIME type.
157 mime = "text/javascript"
158 } else {
159 // Otherwise, use magic to detect type.
160 mime = mimetype.Detect(data).String()
161 }
162 r.w.Header().Set("Content-Type", mime)
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +0200163 r.w.Write(data)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200164}