repos
/ orchard main

orchard

mirror

Every 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

5.5 KB · 181 lines · Go Raw History
  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}