// closing.go reads the one sentence a landing may say about tickets:
// that accepting it finishes them.
//
// 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+)`)
// 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 {
var out []RefToken
seen := map[string]bool{}
for _, m := range closingPhrase.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
}