Skip to content
quarry Artifact [0751667050]
Login

Artifact [0751667050]

Artifact 07516670505da5c21e1ee25fdde8c2c0dfa7252a2591536b0feb845c2f33e2eb:


// refs.go is the reference-token grammar (RFC 0011 ยง14): one syntax
// that chat, forum posts, tickets and landings all speak, parsed here
// so the four surfaces cannot drift. Parsing is pure text work โ€”
// whether a token names anything real is the Resolver's question, and
// an unresolvable token stays plain text.
package markup

import (
	"regexp"
	"strings"
)

// Reference kinds. "hash" is deliberately unresolved: a bare artifact
// name may be a check-in, a ticket or a forum post, and only the index
// knows which. The resolver answers that, not the grammar.
const (
	RefChat    = "chat"
	RefHash    = "hash"
	RefPerson  = "person"
	RefTicket  = "ticket"
	RefCheckin = "checkin"
	RefLanding = "landing"
	RefForum   = "forum"
	RefWiki    = "wiki"
)

// RefToken is one parsed reference. Repo is empty for a token that
// names something in the reader's current repository; a non-empty Repo
// is a cross-repository reference, which resolves only when the reader
// may read that repository.
type RefToken struct {
	Repo string
	Kind string
	ID   string
}

// Canonical is the token's one spelling โ€” what rides in data-ref, what
// a backlink row stores, and what an API client is handed. Two spellings
// of the same reference (a pasted URL and a typed `#412`) canonicalize
// to the same string, so the two never look like two references.
func (t RefToken) Canonical() string {
	prefix := ""
	if t.Repo != "" {
		prefix = t.Repo + ":"
	}
	switch t.Kind {
	case RefChat:
		return prefix + "#" + t.ID
	case RefHash:
		return prefix + "[" + t.ID + "]"
	case RefPerson:
		return "@" + t.ID
	default:
		return prefix + t.Kind + "/" + t.ID
	}
}

const repoPat = `[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}`

var (
	// A chat sequence, optionally repo-qualified. Fossil spells a
	// message id `#N` in /chat-query; quarry makes it a link.
	seqTok = regexp.MustCompile(`(?:^|[^\w/#:-])((?:` + repoPat + `:)?#[1-9][0-9]{0,18})\b`)
	// An artifact name, bracketed or repo-qualified. A BARE hash with
	// no repo and no brackets is left to the older hashRef rule, which
	// has resolved check-ins since 0002 and whose rendering differs.
	hashTok  = regexp.MustCompile(`(?:^|[^\w/:-])((?:` + repoPat + `:)?\[[0-9a-fA-F]{6,64}\])`)
	qhashTok = regexp.MustCompile(`(?:^|[^\w/:-])(` + repoPat + `:[0-9a-fA-F]{10,64})\b`)
	// A pasted quarry URL, absolute or site-relative.
	urlTok = regexp.MustCompile(`(?:^|[^\w])((?:https?://[^\s/]+)?/r/` + repoPat + `/(?:chat|ticket|landing|forum|checkin|wiki)/[^\s<>"'()\[\]]+)`)
	// A mention. Shares its shape with mentionRef, which owns rendering.
	personTok = regexp.MustCompile(`^@([a-zA-Z0-9][a-zA-Z0-9._-]{0,63})$`)

	oneSeq  = regexp.MustCompile(`^(?:(` + repoPat + `):)?#([1-9][0-9]{0,18})$`)
	oneHash = regexp.MustCompile(`^(?:(` + repoPat + `):)?\[([0-9a-fA-F]{6,64})\]$`)
	oneBare = regexp.MustCompile(`^(?:(` + repoPat + `):)?([0-9a-fA-F]{10,64})$`)
	oneURL  = regexp.MustCompile(`^(?:https?://[^\s/]+)?/r/(` + repoPat + `)/(chat|ticket|landing|forum|checkin|wiki)/([^\s?#]+)`)
)

// ParseRef reads one whole token. It is the grammar's only entry point:
// scanRefs finds candidates in prose and hands each one here, and the
// ops layer parses a `ref=` query parameter the same way, so a token
// typed into a URL bar and a token typed into a message mean the same.
func ParseRef(s string) (RefToken, bool) {
	s = strings.TrimSpace(s)
	if s == "" {
		return RefToken{}, false
	}
	if m := oneURL.FindStringSubmatch(s); m != nil {
		kind, id := m[2], strings.Trim(m[3], "/")
		if id == "" {
			return RefToken{}, false
		}
		if kind == RefChat && !isSeq(id) {
			return RefToken{}, false
		}
		return RefToken{Repo: m[1], Kind: kind, ID: id}, true
	}
	if m := oneSeq.FindStringSubmatch(s); m != nil {
		return RefToken{Repo: m[1], Kind: RefChat, ID: m[2]}, true
	}
	if m := oneHash.FindStringSubmatch(s); m != nil {
		return RefToken{Repo: m[1], Kind: RefHash, ID: strings.ToLower(m[2])}, true
	}
	if m := oneBare.FindStringSubmatch(s); m != nil {
		return RefToken{Repo: m[1], Kind: RefHash, ID: strings.ToLower(m[2])}, true
	}
	if m := personTok.FindStringSubmatch(s); m != nil {
		return RefToken{Kind: RefPerson, ID: m[1]}, true
	}
	return RefToken{}, false
}

func isSeq(s string) bool {
	if s == "" || s[0] == '0' {
		return false
	}
	for _, c := range s {
		if c < '0' || c > '9' {
			return false
		}
	}
	return true
}

// refSpan is one token found in prose, at its exact byte span.
type refSpan struct {
	start, end int
	tok        RefToken
}

// scanRefs finds every reference token in one run of text, in order and
// without overlaps: a longer URL match wins over a shorter hash match
// inside it, because the URL is what the author wrote.
func scanRefs(s string) []refSpan {
	var out []refSpan
	claimed := func(start, end int) bool {
		for _, r := range out {
			if start < r.end && r.start < end {
				return true
			}
		}
		return false
	}
	// URLs first: they contain the other shapes.
	for _, re := range []*regexp.Regexp{urlTok, hashTok, qhashTok, seqTok} {
		for _, m := range re.FindAllStringSubmatchIndex(s, -1) {
			start, end := m[2], m[3]
			if claimed(start, end) {
				continue
			}
			tok, ok := ParseRef(s[start:end])
			if !ok {
				continue
			}
			out = append(out, refSpan{start: start, end: end, tok: tok})
		}
	}
	for i := 1; i < len(out); i++ {
		for j := i; j > 0 && out[j-1].start > out[j].start; j-- {
			out[j-1], out[j] = out[j], out[j-1]
		}
	}
	return out
}

// RefsIn lists the resolvable references in a body, canonicalized, in
// order and without duplicates. The ops layer uses it to hand API
// clients the tokens a message carries, and the backlink writer uses it
// to record what a chat message pointed at โ€” both from the SAME grammar
// the renderer links, so a reference a reader can click is a reference
// the store knows about.
func RefsIn(text string, tr TokenResolver) []RefToken {
	var out []RefToken
	seen := map[string]bool{}
	for _, sp := range scanRefs(text) {
		if _, ok := tr.ResolveToken(sp.tok); !ok {
			continue
		}
		c := sp.tok.Canonical()
		if seen[c] {
			continue
		}
		seen[c] = true
		out = append(out, sp.tok)
	}
	return out
}