git@cosmocr.at/gitweb.git @ HEAD · summary refs log
HEAD / internal / render / render.go /
internal/render/render.go · 2.8 KiB · raw
view: Source ☐ force
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// Package render converts markdown to sanitized HTML (for repo READMEs) and
// provides the matching syntax-highlighting stylesheet shared by README code
// blocks and blob views.
package render

import (
	"bytes"

	chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
	"github.com/alecthomas/chroma/v2/styles"
	"github.com/microcosm-cc/bluemonday"
	"github.com/yuin/goldmark"
	highlighting "github.com/yuin/goldmark-highlighting/v2"
	"github.com/yuin/goldmark/extension"
	"github.com/yuin/goldmark/parser"
	"github.com/yuin/goldmark/renderer"
	ghtml "github.com/yuin/goldmark/renderer/html"
	"github.com/yuin/goldmark/util"
)

// chromaStyle is the chroma style whose CSS classes the highlighter emits and
// CSS() generates. Class-based (not inline) so the page CSP can stay strict.
const chromaStyle = "github"

// Renderer turns markdown into sanitized HTML.
type Renderer struct {
	md     goldmark.Markdown
	policy *bluemonday.Policy
}

// New builds a Renderer with GitHub-flavored markdown + class-based syntax
// highlighting, and a sanitizer that permits the highlighter's markup.
func New() *Renderer {
	md := goldmark.New(
		goldmark.WithExtensions(
			extension.GFM,
			highlighting.NewHighlighting(
				highlighting.WithStyle(chromaStyle),
				highlighting.WithFormatOptions(chromahtml.WithClasses(true)),
			),
		),
		goldmark.WithParserOptions(
			parser.WithAutoHeadingID(),
			// Divert ```mermaid blocks before the highlighter turns them to code.
			parser.WithASTTransformers(util.Prioritized(mermaidTransformer{}, 100)),
		),
		goldmark.WithRendererOptions(
			ghtml.WithUnsafe(), // raw HTML is re-checked by bluemonday below
			renderer.WithNodeRenderers(util.Prioritized(mermaidHTMLRenderer{}, 100)),
		),
	)

	p := bluemonday.UGCPolicy()
	// Allow the chroma highlighter's class-tagged markup (and the mermaid
	// <pre class="mermaid"> container).
	p.AllowAttrs("class").OnElements("span", "code", "pre", "div")
	// Heading anchor ids produced by WithAutoHeadingID.
	p.AllowAttrs("id").Globally()
	// GFM task-list checkboxes.
	p.AllowAttrs("type", "checked", "disabled").OnElements("input")
	// Tables from GFM.
	p.AllowAttrs("align").OnElements("td", "th")

	return &Renderer{md: md, policy: p}
}

// ToHTML renders markdown source to sanitized HTML.
func (r *Renderer) ToHTML(src []byte) ([]byte, error) {
	var buf bytes.Buffer
	if err := r.md.Convert(src, &buf); err != nil {
		return nil, err
	}
	return r.policy.SanitizeBytes(buf.Bytes()), nil
}

// CSS returns the chroma stylesheet for the highlighter's classes. Served as a
// static stylesheet so inline styles are never needed.
func CSS() ([]byte, error) {
	style := styles.Get(chromaStyle)
	formatter := chromahtml.New(chromahtml.WithClasses(true))
	var buf bytes.Buffer
	if err := formatter.WriteCSS(&buf, style); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}