Skip to content
quarry Artifact [66ee87d786]
Login

Artifact [66ee87d786]

Artifact 66ee87d78636d545622c5493e01727e01fd219101423a3ee5de5ae379975a103:


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

// TokenResolver is an OPTIONAL upgrade on Resolver: the reference
// grammar of RFC 0011 §14 — chat sequences, repo-qualified tokens and
// pasted quarry URLs. Implementations answer false for anything they
// cannot resolve, INCLUDING a cross-repository token whose target the
// reader may not see, and the token stays plain text.
//
// The link this mints carries class="ref" and data-ref/data-ref-kind
// and nothing else. The preview is armed ONCE for the whole page, by
// delegated handlers on the layout's container that read data-ref off
// whichever anchor an event reached — so a body with two hundred
// references costs two hundred short attributes, not two hundred
// copies of the same expression.
type TokenResolver interface {
	ResolveToken(t RefToken) (url string, ok bool)
}

// 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] {
		// A pasted quarry URL is already an anchor by the time the
		// resolver runs (GFM autolinks it), so the reference is marked
		// on the link the author's own text produced rather than
		// minted a second time beside it.
		if n.Data == "a" {
			annotateAnchor(n, r)
		}
		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
		token, kind      string
		hue              int
		isUser           bool
		// styled marks a reference the grammar CLAIMED, which is the
		// only kind that gets class="ref". The older wiki and hash
		// rules keep the look they have had since 0002 — the hooks are
		// added to them, the styling is not changed.
		styled bool
		mark   string // the mentioned account's identity glyph
	}
	var refs []ref
	var claimed []refSpan
	if tr, ok := r.(TokenResolver); ok {
		for _, sp := range scanRefs(s) {
			sp.tok = forSurface(sp.tok, tr)
			url, ok := tr.ResolveToken(sp.tok)
			if !ok {
				continue
			}
			claimed = append(claimed, sp)
			refs = append(refs, ref{start: sp.start, end: sp.end,
				label: s[sp.start:sp.end], url: url, styled: true,
				token: sp.tok.Canonical(), kind: sp.tok.Kind})
		}
	}
	// A claimed span is the author's whole token; the older rules must
	// not re-link the hex sitting inside one.
	overlaps := func(start, end int) bool {
		for _, c := range claimed {
			if start < c.end && c.start < end {
				return true
			}
		}
		return false
	}
	for _, m := range wikiRef.FindAllStringSubmatchIndex(s, -1) {
		name := s[m[2]:m[3]]
		if overlaps(m[0], m[1]) {
			continue
		}
		if url := r.ResolveWiki(name); url != "" {
			tok := RefToken{Kind: RefWiki, ID: name}
			refs = append(refs, ref{start: m[0], end: m[1], label: name, url: url,
				token: tok.Canonical(), kind: tok.Kind})
		}
	}
	if ur, ok := r.(UserResolver); ok {
		for _, m := range mentionRef.FindAllStringSubmatchIndex(s, -1) {
			name := s[m[4]:m[5]]
			if overlaps(m[3], m[1]) {
				continue
			}
			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).
				// The label drops the @: the mark in front already says
				// this is a person, and "@" twice over — glyph and
				// sigil — is the syntax showing through the rendering.
				// The source keeps it; the reader does not need it.
				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 overlaps(m[0], m[1]) {
			continue
		}
		if url := r.ResolveHash(tok); url != "" {
			ref0 := RefToken{Kind: RefHash, ID: strings.ToLower(tok)}
			rf := ref{start: m[0], end: m[1], label: tok, url: url,
				token: ref0.Canonical(), kind: ref0.Kind}
			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.kind == RefPerson {
			// A mention is a CHIP: the mark that says this line
			// addresses somebody. No preview hook — the person card is
			// gated on a capability most readers do not hold, so
			// arming it would offer nearly everybody an empty popover.
			a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: "at"})
		} else if rf.token != "" && !rf.isUser {
			if rf.styled {
				a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: "ref"})
			}
			a.Attr = append(a.Attr,
				html.Attribute{Key: "data-ref", Val: rf.token},
				html.Attribute{Key: "data-ref-kind", Val: rf.kind})
		}
		if rf.isUser {
			a.Attr = append(a.Attr,
				html.Attribute{Key: "class", Val: "uref at"},
				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
}

// annotateAnchor marks an EXISTING link whose href names a quarry
// artifact — the pasted-URL half of the reference grammar. Nothing is
// rewritten: the author's href and label stay exactly as written, and
// only the reference hooks are added.
func annotateAnchor(a *html.Node, r Resolver) {
	tr, ok := r.(TokenResolver)
	if !ok {
		return
	}
	var href string
	for _, at := range a.Attr {
		if at.Key == "href" {
			href = at.Val
		}
		if at.Key == "data-ref" {
			return // already marked
		}
	}
	tok, ok := ParseRef(href)
	if !ok {
		return
	}
	if _, ok := tr.ResolveToken(tok); !ok {
		return
	}
	a.Attr = append(a.Attr,
		html.Attribute{Key: "class", Val: "ref"},
		html.Attribute{Key: "data-ref", Val: tok.Canonical()},
		html.Attribute{Key: "data-ref-kind", Val: tok.Kind})
}