orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1package main
2
3// README rendering and file highlighting. The bluemonday pass is not optional:
4// goldmark passes raw HTML through, and this renders READMEs from mirrored
5// repositories nobody here reviewed.
6
7import (
8 "bytes"
9 "fmt"
10 "html/template"
11 "path"
12 "strings"
13 "unicode/utf8"
14
15 "github.com/alecthomas/chroma/v2"
16 chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
17 "github.com/alecthomas/chroma/v2/lexers"
18 "github.com/alecthomas/chroma/v2/styles"
19 "github.com/microcosm-cc/bluemonday"
20 "github.com/yuin/goldmark"
21 "github.com/yuin/goldmark/extension"
22 goldmarkhtml "github.com/yuin/goldmark/renderer/html"
23)
24
25// maxReadmeSize caps what is rendered on a repository page.
26const maxReadmeSize = 1 << 20
27
28// maxHighlightSize caps syntax highlighting; past it the file is shown as plain
29// text, since chroma on a minified bundle is seconds of CPU.
30const maxHighlightSize = 1 << 20
31
32var (
33 markdown = goldmark.New(
34 goldmark.WithExtensions(
35 extension.GFM,
36 extension.Footnote,
37 extension.Typographer,
38 ),
39 goldmark.WithRendererOptions(
40 // Unsafe is on because escaping raw HTML breaks the badges and
41 // anchors most READMEs use; sanitizer below is what makes it safe.
42 goldmarkhtml.WithUnsafe(),
43 ),
44 )
45
46 // UGCPolicy allows README formatting and strips script, style, iframe,
47 // object, form and every event handler attribute.
48 sanitizer = newSanitizer()
49)
50
51func newSanitizer() *bluemonday.Policy {
52 p := bluemonday.UGCPolicy()
53 // Sizing attributes, so badges do not render at full resolution.
54 p.AllowAttrs("width", "height", "align").OnElements("img")
55 p.AllowAttrs("id").OnElements("h1", "h2", "h3", "h4", "h5", "h6")
56 // chroma's spans carry their colours on class attributes.
57 p.AllowAttrs("class").OnElements("code", "pre", "span", "div", "table", "td", "th", "tr")
58 p.AllowAttrs("align").OnElements("td", "th", "p", "div")
59 // Task lists render as disabled checkboxes.
60 p.AllowAttrs("type", "checked", "disabled").OnElements("input")
61 p.AllowElements("input")
62 return p
63}
64
65// RenderMarkdown converts a README to sanitised HTML.
66func RenderMarkdown(src []byte) (template.HTML, error) {
67 if len(src) > maxReadmeSize {
68 src = src[:maxReadmeSize]
69 }
70 var buf bytes.Buffer
71 if err := markdown.Convert(src, &buf); err != nil {
72 return "", fmt.Errorf("render markdown: %w", err)
73 }
74 return template.HTML(sanitizer.SanitizeBytes(buf.Bytes())), nil
75}
76
77// readmeNames is looked at in order. The plain and .txt forms are here because
78// some mirrored repositories are older than the .md convention.
79var readmeNames = []string{
80 "README.md", "readme.md", "README.markdown",
81 "README.rst", "README.txt", "README", "readme",
82}
83
84// IsMarkdown decides whether a README gets goldmark or a <pre>.
85func IsMarkdown(name string) bool {
86 switch strings.ToLower(path.Ext(name)) {
87 case ".md", ".markdown", ".mdown":
88 return true
89 }
90 return false
91}
92
93// chromaStyle uses the site's own tokens, and keeps literals clear of the green
94// and red the diff view reserves for meaning.
95var chromaStyle = chroma.MustNewStyle("repos", chroma.StyleEntries{
96 chroma.Background: "#e4dfe4 bg:#151317",
97 chroma.Comment: "italic #8b8291",
98 chroma.CommentPreproc: "#a99bf5",
99 chroma.Keyword: "#a99bf5",
100 chroma.KeywordType: "#e6c88a",
101 chroma.KeywordConstant: "#e6c88a",
102 chroma.KeywordDeclaration: "#a99bf5",
103 chroma.Operator: "#b9b1c6",
104 chroma.Punctuation: "#9a92a5",
105 chroma.Name: "#e4dfe4",
106 chroma.NameBuiltin: "#c9b8f0",
107 chroma.NameClass: "#e6c88a",
108 chroma.NameFunction: "#c9b8f0",
109 chroma.NameNamespace: "#e6c88a",
110 chroma.NameAttribute: "#e6c88a",
111 chroma.NameTag: "#d79ec0",
112 chroma.NameDecorator: "#d79ec0",
113 chroma.NameVariable: "#e4dfe4",
114 chroma.NameConstant: "#e0a07a",
115 chroma.LiteralString: "#8fbf9f",
116 chroma.LiteralStringEscape: "#e0a07a",
117 chroma.LiteralNumber: "#e0a07a",
118 chroma.GenericDeleted: "#e08a8a",
119 chroma.GenericInserted: "#8fbf9f",
120 chroma.GenericHeading: "bold #e4dfe4",
121 chroma.GenericSubheading: "bold #c9b8f0",
122 chroma.GenericEmph: "italic",
123 chroma.GenericStrong: "bold",
124 chroma.Error: "#e08a8a",
125})
126
127// Highlight renders one file as HTML. Only the basename reaches the lexer
128// matcher, never a request-supplied path that something might open.
129func Highlight(name string, src []byte) (template.HTML, bool) {
130 if len(src) > maxHighlightSize || !utf8.Valid(src) {
131 return "", false
132 }
133
134 lexer := lexers.Match(path.Base(name))
135 if lexer == nil {
136 lexer = lexers.Analyse(string(src))
137 }
138 if lexer == nil {
139 lexer = lexers.Fallback
140 }
141 lexer = chroma.Coalesce(lexer)
142
143 iterator, err := lexer.Tokenise(nil, string(src))
144 if err != nil {
145 return "", false
146 }
147
148 formatter := chromahtml.New(
149 chromahtml.WithClasses(false),
150 chromahtml.WithLineNumbers(true),
151 chromahtml.WithLinkableLineNumbers(true, "L"),
152 chromahtml.TabWidth(4),
153 )
154
155 style := chromaStyle
156 if style == nil {
157 style = styles.Fallback
158 }
159
160 var buf bytes.Buffer
161 if err := formatter.Format(&buf, style, iterator); err != nil {
162 return "", false
163 }
164 return template.HTML(buf.String()), true
165}
166
167// IsBinary decides whether to offer a file for download. The NUL test is what git
168// itself uses.
169func IsBinary(src []byte) bool {
170 limit := min(len(src), 8000)
171 return bytes.IndexByte(src[:limit], 0) >= 0
172}
173
174// languageOf is chroma's lexer name, used only as a label on a blob page.
175func languageOf(name string) string {
176 if l := lexers.Match(path.Base(name)); l != nil {
177 return l.Config().Name
178 }
179 return ""
180}