Artifact 0d3a856fa8cf3edbbb78452567049a4b666f234878b9ccbbde7f253a20f9ec9c:
- File
internal/markup/resolver.go
— part of check-in
[0ba22dea9e]
at
2026-08-23 22:31:40
on branch trunk
— markdown: mentions carry the identity mark, quotes are visible
An @mention now renders the same glyph as the person's byline, through the resolver seam so markup still knows nothing about accounts. The identicon moved to internal/idn — web and api each hashed the name separately and agreed by luck. Blockquotes had no rule at all. (user: dmestas, size: 7820) [annotate] [blame] [check-ins using]
// resolver.go — zit reference resolution (0002 §2 rework: git-ref hooks // replaced with zit resolution). Runs AFTER sanitization, over text // nodes only — the links it inserts are quarry-built, and code spans, // pre blocks, and existing links are left alone. package markup import ( "bytes" "fmt" "regexp" "strings" "golang.org/x/net/html" "golang.org/x/net/html/atom" ) var ( wikiRef = regexp.MustCompile(`\[\[([^\[\]|]{1,100})\]\]`) hashRef = regexp.MustCompile(`\b[0-9a-f]{10,64}\b`) ) // UserResolver is an OPTIONAL upgrade on Resolver: @name mentions that // resolve to a known person render as a link in that person's hue — // the same identity color the rest of the forge speaks. type UserResolver interface { // ResolveUser answers ("", 0, false) for names it does not know; // an unresolved mention stays plain text, never a dead link. ResolveUser(name string) (url string, hue int, ok bool) } var mentionRef = regexp.MustCompile(`(^|[^a-zA-Z0-9._-])@([a-zA-Z0-9][a-zA-Z0-9._-]{0,63})`) // HashDescriber is an OPTIONAL upgrade on Resolver: a one-line human // description for a resolvable hash (a check-in's comment). Described // links carry it as a hover title and a visible excerpt. type HashDescriber interface { DescribeHash(prefix string) string } // excerpt clips s at n runes on a rune boundary, marking the cut. func excerpt(s string, n int) string { runes := []rune(s) if len(runes) <= n { return s } return string(runes[:n]) + "…" } func resolveRefs(in []byte, r Resolver) []byte { body := &html.Node{Type: html.ElementNode, Data: "body", DataAtom: atom.Body} nodes, err := html.ParseFragment(bytes.NewReader(in), body) if err != nil { return in } var out bytes.Buffer for _, n := range nodes { walkResolve(n, r) if err := html.Render(&out, n); err != nil { return in } } return out.Bytes() } var skipTags = map[string]bool{"a": true, "code": true, "pre": true} func walkResolve(n *html.Node, r Resolver) { if n.Type == html.ElementNode && skipTags[n.Data] { return } for c := n.FirstChild; c != nil; c = c.NextSibling { walkResolve(c, r) } // Collect text children first: splicing replaces nodes mid-walk. var texts []*html.Node for c := n.FirstChild; c != nil; c = c.NextSibling { if c.Type == html.TextNode { texts = append(texts, c) } } for _, t := range texts { splice(n, t, r) } } // splice replaces one text node with text/link/text/... nodes for each // resolvable reference found in it. func splice(parent, text *html.Node, r Resolver) { s := text.Data type ref struct { start, end int label, url, desc string hue int isUser bool mark string // the mentioned account's identity glyph } var refs []ref for _, m := range wikiRef.FindAllStringSubmatchIndex(s, -1) { name := s[m[2]:m[3]] if url := r.ResolveWiki(name); url != "" { refs = append(refs, ref{start: m[0], end: m[1], label: name, url: url}) } } if ur, ok := r.(UserResolver); ok { for _, m := range mentionRef.FindAllStringSubmatchIndex(s, -1) { name := s[m[4]:m[5]] if url, hue, ok := ur.ResolveUser(name); ok { // The @ belongs to the link; the leading boundary char // stays as text (start index m[3] ends the boundary). refs = append(refs, ref{start: m[3], end: m[1], label: "@" + name, url: url, hue: hue, isUser: true, mark: r.UserMark(name)}) } } } hd, _ := r.(HashDescriber) for _, m := range hashRef.FindAllStringIndex(s, -1) { tok := s[m[0]:m[1]] if url := r.ResolveHash(tok); url != "" { rf := ref{start: m[0], end: m[1], label: tok, url: url} if hd != nil { rf.desc = hd.DescribeHash(tok) } refs = append(refs, rf) } } if len(refs) == 0 { return } // Wiki and hash matches cannot overlap ([[..]] never lexes as a hex // word), so a simple position sort orders the splice. for i := 1; i < len(refs); i++ { for j := i; j > 0 && refs[j-1].start > refs[j].start; j-- { refs[j-1], refs[j] = refs[j], refs[j-1] } } pos := 0 insert := func(nn *html.Node) { parent.InsertBefore(nn, text) } for _, rf := range refs { if rf.start > pos { insert(&html.Node{Type: html.TextNode, Data: s[pos:rf.start]}) } a := &html.Node{Type: html.ElementNode, Data: "a", DataAtom: atom.A, Attr: []html.Attribute{{Key: "href", Val: rf.url}}} if rf.isUser { a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: "uref"}, html.Attribute{Key: "style", Val: fmt.Sprintf("--uh: %d", rf.hue)}) // The identity mark rides INSIDE the link, parsed into // nodes rather than pasted as text — a text node would // render as visible angle brackets. // // Resolution runs AFTER sanitizing (see the file header), so // nothing re-inspects this. That is safe here and only here // because the mark is quarry's own drawing of a name, not // anything an author wrote: the document policy admits no // <svg> at all, and this is not a hole in it but a glyph // added on the far side of it, the same way the pikchr // diagrams are restored after their own narrow policy. if rf.mark != "" { if frag, err := html.ParseFragment(strings.NewReader(rf.mark), a); err == nil { for _, n := range frag { a.AppendChild(n) } } } } a.AppendChild(&html.Node{Type: html.TextNode, Data: rf.label}) // A described reference (a check-in's comment) renders GitHub's // way: the message rides the link — full first line as the // hover title, an excerpt visible beside the hash. if rf.desc != "" { a.Attr = append(a.Attr, html.Attribute{Key: "title", Val: rf.desc}) } insert(a) if rf.desc != "" { span := &html.Node{Type: html.ElementNode, Data: "span", DataAtom: atom.Span, Attr: []html.Attribute{{Key: "class", Val: "cref"}}} span.AppendChild(&html.Node{Type: html.TextNode, Data: " (" + excerpt(rf.desc, 60) + ")"}) insert(span) } pos = rf.end } if pos < len(s) { insert(&html.Node{Type: html.TextNode, Data: s[pos:]}) } parent.RemoveChild(text) } // WikiRef is one page reference found in a body. type WikiRef struct { Name string // Explicit marks the [[Name]] construct, which names a page whether // or not it exists — a reader wrote it meaning "there should be a // page here". Fossil's [Name|label] form is not explicit: fossil // links it only when the page already exists and otherwise leaves // it as text, so an unresolvable one is a sentence, not a request. Explicit bool } // WikiRefs lists the page references in a body, in order and without // duplicates. It reads the SAME two constructs the renderer resolves, // because a structure page built from a second, nearly-identical rule // would eventually disagree with the links on the page it describes. // // External targets (anything with a scheme or a slash) are not page // names and are left out. func WikiRefs(body []byte, fossilWiki bool) []WikiRef { s := string(body) var out []WikiRef seen := map[string]bool{} keep := func(name string, explicit bool) { name = strings.TrimSpace(name) if name == "" || seen[name] { return } if strings.Contains(name, "/") || strings.Contains(name, ":") { return // a URL, not a page } seen[name] = true out = append(out, WikiRef{Name: name, Explicit: explicit}) } for _, m := range wikiRef.FindAllStringSubmatch(s, -1) { keep(m[1], true) } if fossilWiki { // Fossil's [Target|label] pattern also matches the inner half of // a [[Name]], which would report "[Name" as a page. Blank the // double-bracket spans first so each construct is read once. masked := wikiRef.ReplaceAllStringFunc(s, func(m string) string { return strings.Repeat(" ", len(m)) }) for _, m := range fossilLink.FindAllStringSubmatch(masked, -1) { keep(m[1], false) } } return out }