render.go
3200 bytes
1package pages
2
3import (
4 "bytes"
5 "html/template"
6 "net/http"
7
8 "github.com/alecthomas/chroma/v2"
9 chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
10 "github.com/alecthomas/chroma/v2/lexers"
11 "github.com/alecthomas/chroma/v2/styles"
12 "github.com/microcosm-cc/bluemonday"
13 "github.com/yuin/goldmark"
14 "github.com/yuin/goldmark/ast"
15 "github.com/yuin/goldmark/text"
16)
17
18// SetContentSafetyHeaders applies response headers that are safe for pages and assets.
19func SetContentSafetyHeaders(w http.ResponseWriter) {
20 w.Header().Set("X-Content-Type-Options", "nosniff")
21 w.Header().Set("Referrer-Policy", "same-origin")
22}
23
24// SetSecurityHeaders applies the common security policy for dynamic page responses.
25func SetSecurityHeaders(w http.ResponseWriter) {
26 SetContentSafetyHeaders(w)
27 w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'")
28}
29
30// RenderMarkdown renders Markdown with raw HTML disabled and sanitizes the result.
31func RenderMarkdown(source []byte) (template.HTML, error) {
32 var rendered bytes.Buffer
33 if err := goldmark.Convert(source, &rendered); err != nil {
34 return "", err
35 }
36 return template.HTML(bluemonday.UGCPolicy().SanitizeBytes(rendered.Bytes())), nil // #nosec G203 -- sanitizer output only
37}
38
39// RenderRepositoryMarkdown renders Markdown and rewrites safe relative links through page routes.
40func RenderRepositoryMarkdown(repository, refName, sourcePath string, source []byte) (template.HTML, error) {
41 markdown := goldmark.New()
42 document := markdown.Parser().Parse(text.NewReader(source))
43 if err := ast.Walk(document, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
44 if !entering {
45 return ast.WalkContinue, nil
46 }
47 switch node := node.(type) {
48 case *ast.Link:
49 node.Destination = []byte(safeRelativeMarkdownURL(repository, refName, sourcePath, string(node.Destination), false))
50 case *ast.Image:
51 node.Destination = []byte(safeRelativeMarkdownURL(repository, refName, sourcePath, string(node.Destination), true))
52 }
53 return ast.WalkContinue, nil
54 }); err != nil {
55 return "", err
56 }
57 var rendered bytes.Buffer
58 if err := markdown.Renderer().Render(&rendered, source, document); err != nil {
59 return "", err
60 }
61 return template.HTML(bluemonday.UGCPolicy().SanitizeBytes(rendered.Bytes())), nil // #nosec G203 -- sanitizer output only
62}
63
64// RenderSource highlights bounded source using Chroma's escaping HTML formatter.
65func RenderSource(filename string, source []byte) (template.HTML, error) {
66 lexer := lexers.Match(filename)
67 if lexer == nil {
68 lexer = lexers.Analyse(string(source))
69 }
70 if lexer == nil {
71 lexer = lexers.Fallback
72 }
73 iterator, err := lexer.Tokenise(nil, string(source))
74 if err != nil {
75 return "", err
76 }
77
78 var rendered bytes.Buffer
79 formatter := chromahtml.New(chromahtml.WithClasses(true), chromahtml.WithLineNumbers(true))
80 if err := formatter.Format(&rendered, styles.Fallback, iterator); err != nil {
81 return "", err
82 }
83 return template.HTML(rendered.String()), nil // #nosec G203 -- Chroma escapes source tokens
84}
85
86// Compile-time checks keep renderer dependencies explicit.
87var _ chroma.Formatter = chromahtml.New()