Artifact 40a7fca003bed2b61c8a600472c5cd33e75e49c999aa48aec9d8f15b7770ff08:
- File
internal/markup/markup.go
— part of check-in
[5ba3499b8a]
at
2026-08-15 11:20:48
on branch trunk
— markup: goldmark + cleanroom sanitizer + resolver seam (0002 §2)
Two-lineage conformance corpus in third_party (ledger rows, inputs only); parse-oracle fuzz target; [[wiki]]/hash resolution post-sanitize. (user: dmestas, size: 4097) [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" "github.com/microcosm-cc/bluemonday" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/renderer/html" ) // 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 hook: language classes only, nothing else. p.AllowAttrs("class").Matching(regexp.MustCompile(`^language-[a-zA-Z0-9_+#.-]+$`)).OnElements("code") // 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), 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) }