Parent directory

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()