Skip to content
quarry Artifact [40a7fca003]
Login

Artifact [40a7fca003]

Artifact 40a7fca003bed2b61c8a600472c5cd33e75e49c999aa48aec9d8f15b7770ff08:


// 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)
}