blob: cfd594dae37f23f9df151eb6db11337ec2dbc160 [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.
24 if ref == "master" {
25 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
31 var buf bytes.Buffer
Sergiusz Bazanski5bce7ce2020-04-11 20:16:58 +020032 buf.Write([]byte(`<div class="toc"><h1>Page Contents</h1>`))
33 r.RenderHeader(&buf, ast)
34 buf.Write([]byte(`</div><div class="content">`))
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020035 ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
Sergiusz Bazanski4b4a33a2020-04-13 01:35:33 +020036 if ref != "" && entering && node.Type == blackfriday.Link || node.Type == blackfriday.Image {
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020037 dest := string(node.Destination)
38 u, err := url.Parse(dest)
39 if err == nil && !u.IsAbs() {
40 q := u.Query()
41 q["ref"] = []string{ref}
42 u.RawQuery = q.Encode()
43 node.Destination = []byte(u.String())
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020044 glog.Infof("link fix %q -> %q", dest, u.String())
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020045 }
46 }
47 return r.RenderNode(&buf, node, entering)
48 })
Sergiusz Bazanski5bce7ce2020-04-11 20:16:58 +020049 buf.Write([]byte(`</div>`))
50 r.RenderFooter(&buf, ast)
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020051 return buf.Bytes()
52}
53
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020054type pathPart struct {
55 Label string
56 Path string
57}
58
Serge Bazanskid701c4e2020-08-10 17:59:59 +020059func (r *request) renderable(dirpath string) bool {
60 cfg, err := config.ForPath(r.ctx, r.source, dirpath)
61 if err != nil {
62 glog.Errorf("could not get config for path %q: %v", dirpath, err)
63 return false
64 }
65
66 for _, f := range cfg.DefaultIndex {
67 fpath := dirpath + "/" + f
68 file, err := r.source.IsFile(r.ctx, fpath)
69 if err != nil {
70 glog.Errorf("IsFile(%q): %v", fpath, err)
71 return false
72 }
73
74 if file {
75 return true
76 }
77 }
78
79 return false
80}
81
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020082func (r *request) handleFile(path string, cfg *config.Config) {
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020083 data, err := r.source.ReadFile(r.ctx, path)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020084 if err != nil {
85 glog.Errorf("ReadFile(%q): %w", err)
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020086 r.handle500()
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020087 return
88 }
89
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020090 // TODO(q3k): do MIME detection instead.
91 if strings.HasSuffix(path, ".md") {
92 rendered := renderMarkdown([]byte(data), r.ref)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020093
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020094 r.logRequest("serving markdown at %s, cfg %+v", path, cfg)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020095
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +020096 // TODO(q3k): allow markdown files to override which template to load
97 tmpl, ok := cfg.Templates["default"]
98 if !ok {
99 glog.Errorf("No default template found for %s", path)
100 // TODO(q3k): implement fallback template
101 r.w.Write(rendered)
102 return
103 }
104
105 pathInDepot := strings.TrimPrefix(path, "//")
106 pathParts := []pathPart{
107 {Label: "//", Path: "/"},
108 }
109 parts := strings.Split(pathInDepot, "/")
110 fullPath := ""
111 for i, p := range parts {
112 label := p
113 if i != len(parts)-1 {
114 label = label + "/"
115 }
116 fullPath += "/" + p
Serge Bazanskid701c4e2020-08-10 17:59:59 +0200117 target := fullPath
118 if i != len(parts)-1 && !r.renderable("/"+fullPath) {
119 target = ""
120 }
121 pathParts = append(pathParts, pathPart{Label: label, Path: target})
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +0200122 }
123
124 vars := map[string]interface{}{
125 "Rendered": template.HTML(rendered),
126 "Title": path,
127 "Path": path,
128 "PathInDepot": pathInDepot,
129 "PathParts": pathParts,
130 "HackdocURL": flagHackdocURL,
131 "WebLinks": r.source.WebLinks(pathInDepot),
132 }
133 err = tmpl.Execute(r.w, vars)
134 if err != nil {
135 glog.Errorf("Could not execute template for %s: %v", err)
136 }
137
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200138 return
139 }
140
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +0200141 // Just serve the file.
142 mime := mimetype.Detect(data)
143 r.w.Header().Set("Content-Type", mime.String())
144 r.w.Write(data)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200145}