Skip to content
quarry Artifact [c793d5b698]
Login

Artifact [c793d5b698]

Artifact c793d5b6980d1756eada7e9be6899ff7140732f0f4d3c7186c2786618437fb88:


// closing.go reads the two sentences a landing may say about tickets:
// that accepting it finishes them, and that it works on them without
// finishing them.
//
// Both are the same grammar and the same link. The difference is only
// what the accept path does with each: a closes settles the ticket, an
// addresses settles nothing. The second verb exists because most work
// is the second case — a landing that moves a ticket forward without
// ending it had no way to say so, so it either said nothing (and the
// ticket showed no sign the work existed) or said "closes" (and the
// ticket was wrongly shut).
//
// The grammar is deliberately the SAME one the renderer links with —
// ParseRef, not a second pattern — so a reference that closes a ticket
// is a reference a reader can click, and there is no way to write one
// that acts without also appearing. A closing phrase quarry acts on
// must be a phrase quarry shows.
package markup

import (
	"regexp"
	"strings"
)

// closingPhrase is a closing verb and the token it governs.
//
// The verb must be immediately followed by the reference, with nothing
// but spaces or a colon between. That is what separates a statement of
// intent from prose that happens to contain both words: "closes ae12f3"
// acts, and "this closes the gap that ae12f3 describes" does not,
// because in the second the token is four words away.
//
// Kept to three verbs, all unambiguous in the imperative. GitHub's
// longer list (close/closed/closing, fix/fixed/fixes/fixing, resolve/…)
// buys little here and widens the surface for an accident, and an
// accident closes somebody's ticket.
var closingPhrase = regexp.MustCompile(`(?i)\b(?:closes|fixes|resolves)\b:?[ \t]+(\S+)`)

// addressingPhrase is the same shape for the verb that LINKS without
// settling. One verb, not a family: "addresses" is the word, and every
// synonym added here is another way to write a reference that behaves
// differently from how it reads.
var addressingPhrase = regexp.MustCompile(`(?i)\baddresses\b:?[ \t]+(\S+)`)

// ClosingRefs lists the references a message declares it closes, in
// order and without duplicates.
//
// Parsing only: it says what the text CLAIMS, never what exists. The
// caller resolves each token and decides which are tickets it may act
// on — a landing cannot close something by naming it.
func ClosingRefs(text string) []RefToken { return refsAfter(closingPhrase, text) }

// AddressingRefs lists the references a message declares it addresses:
// work on that ticket that does not finish it.
//
// Parsed by the same rules and refused for the same reasons as a close.
// The caller is expected to LINK these and act on none of them.
func AddressingRefs(text string) []RefToken { return refsAfter(addressingPhrase, text) }

// refsAfter is the shared reading: one verb pattern, the token it
// governs, in order and without duplicates.
func refsAfter(pat *regexp.Regexp, text string) []RefToken {
	var out []RefToken
	seen := map[string]bool{}
	for _, m := range pat.FindAllStringSubmatch(text, -1) {
		// The token as written wins. Only if that is not a reference is
		// trailing punctuation stripped and the shorter form tried —
		// the other order eats the closing bracket of "[ae12f3]",
		// which is the canonical spelling of a bare hash and so the
		// one form that must never fail to parse.
		tok, ok := ParseRef(m[1])
		if !ok {
			tok, ok = ParseRef(strings.TrimRight(m[1], ".,;:!?)]}"))
		}
		if !ok {
			continue
		}
		// A person is never a thing to close, and neither is a chat
		// line. Both parse as tokens, so both are refused here rather
		// than left for the caller to notice.
		if tok.Kind == RefPerson || tok.Kind == RefChat {
			continue
		}
		c := tok.Canonical()
		if seen[c] {
			continue
		}
		seen[c] = true
		out = append(out, tok)
	}
	return out
}