Artifact fb89da2a7c0226a05944cd8bc0721769d5a6e579d74e4e5ab9748bed736ee950:
- File
internal/markup/markup.go
— part of check-in
[689d0692d7]
at
2026-08-15 12:16:01
on branch trunk
— highlight: chroma wrapper, fenced-code + file-view highlighting
Cleanroom (their wrapper is settings-coupled); classes only, both themes in one stylesheet; sanitizer learns chroma token classes. (user: dmestas, size: 4701) [annotate] [blame] [check-ins using]
// Package markup renders wiki/ticket/chat text to safe HTML (0002 §2): // goldmark for markdown, a CLEANROOM bluemonday policy for // sanitization — written fresh, copied from neither the Gitea nor the // Forgejo lineage — and one Resolver seam for zit reference resolution // (artifact hashes, [[wiki]] links) so the markdown assembly stays free // of index knowledge. // // The policy's conformance gate is the two-lineage vector corpus under // third_party/sanitizer-corpus (inputs are data, recorded in the // provenance ledger); markup_test runs it, and FuzzSanitize hammers the // policy beyond it. package markup import ( "bytes" "regexp" chromahtml "github.com/alecthomas/chroma/v2/formatters/html" "github.com/microcosm-cc/bluemonday" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/renderer/html" highlighting "github.com/yuin/goldmark-highlighting/v2" ) // Resolver turns zit references found in text into links. Empty return // means "not resolvable — leave as plain text". Implementations sit on // the index; markup never touches it directly. type Resolver interface { // ResolveHash resolves a hex token (artifact-name shaped, 10–64 // chars) to a URL — a check-in page, a ticket page, whatever the // index says the artifact is. ResolveHash(prefix string) string // ResolveWiki resolves a [[Wiki Name]] reference to a URL. ResolveWiki(name string) string } // sanitize is the cleanroom policy. Everything here is written from the // question "what does markdown rendered by goldmark plus quarry's own // features actually need", not from either lineage's list. var sanitize = func() *bluemonday.Policy { p := bluemonday.NewPolicy() // Block structure. p.AllowElements("p", "br", "hr", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "pre", "ul", "ol", "li", "dl", "dt", "dd", "table", "thead", "tbody", "tr", "th", "td", "details", "summary", "picture", "source") // Inline structure. p.AllowElements("em", "strong", "b", "i", "s", "del", "ins", "u", "code", "kbd", "sub", "sup", "mark", "q", "cite", "abbr") // Links: safe schemes plus relative URLs; rel=nofollow is forced on // every absolute link. p.AllowAttrs("href").OnElements("a") p.AllowStandardURLs() p.RequireNoFollowOnLinks(true) // Images. p.AllowAttrs("src", "alt", "title", "width", "height").OnElements("img") p.AllowAttrs("loading").Matching(regexp.MustCompile(`^lazy$`)).OnElements("img") p.AllowAttrs("media", "srcset").OnElements("source") // Syntax-highlight hooks: language classes on code, chroma's short // token classes on span, and the chroma wrapper class on pre — // nothing framework-shaped gets through (the class-injection attacks // in the corpus). p.AllowAttrs("class").Matching(regexp.MustCompile(`^language-[a-zA-Z0-9_+#.-]+$`)).OnElements("code") p.AllowAttrs("class").Matching(regexp.MustCompile(`^[a-z][a-z0-9]{0,3}$`)).OnElements("span") p.AllowAttrs("class").Matching(regexp.MustCompile(`^chroma$`)).OnElements("pre") // Task lists. p.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") p.AllowAttrs("checked", "disabled").Matching(regexp.MustCompile(`^$|^checked$|^disabled$`)).OnElements("input") p.AllowAttrs("class").Matching(regexp.MustCompile(`^(checked|unchecked|indeterminate)$`)).OnElements("li") // Emoji spans and the two color properties, on non-code elements. p.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("span") p.AllowAttrs("aria-label", "data-alias").OnElements("span") p.AllowStyles("color", "background-color").OnElements("span", "p", "th", "td", "div") // Table alignment emitted by the GFM extension. p.AllowAttrs("align").Matching(regexp.MustCompile(`^(left|center|right)$`)).OnElements("th", "td") return p }() var md = goldmark.New( goldmark.WithExtensions( extension.GFM, extension.Strikethrough, extension.TaskList, highlighting.NewHighlighting( highlighting.WithStyle("github"), highlighting.WithFormatOptions(chromahtml.WithClasses(true)), ), ), goldmark.WithRendererOptions(html.WithUnsafe()), // sanitizer is the gate, not the renderer ) // Render converts markdown to sanitized HTML. r may be nil (no // reference resolution). func Render(src []byte, r Resolver) ([]byte, error) { var buf bytes.Buffer if err := md.Convert(src, &buf); err != nil { return nil, err } out := sanitize.SanitizeBytes(buf.Bytes()) if r != nil { out = resolveRefs(out, r) } return out, nil } // Sanitize applies the policy alone — the surface the conformance // corpus and the fuzz target exercise directly. func Sanitize(in string) string { return sanitize.Sanitize(in) }