Skip to content
quarry Artifact [2b03aed793]
Login

Artifact [2b03aed793]

Artifact 2b03aed79336d7becba88fe3f7ac0efa1b32fe42d0be79d564fe16b4297bacee:


// 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"
	"regexp"

	"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`)
)

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 string
	}
	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{m[0], m[1], name, url})
		}
	}
	for _, m := range hashRef.FindAllStringIndex(s, -1) {
		tok := s[m[0]:m[1]]
		if url := r.ResolveHash(tok); url != "" {
			refs = append(refs, ref{m[0], m[1], tok, url})
		}
	}
	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}}}
		a.AppendChild(&html.Node{Type: html.TextNode, Data: rf.label})
		insert(a)
		pos = rf.end
	}
	if pos < len(s) {
		insert(&html.Node{Type: html.TextNode, Data: s[pos:]})
	}
	parent.RemoveChild(text)
}