// 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.
//
// ADJACENCY is the safety, not a short verb list. The verb must be
// immediately followed by the reference, with nothing but spaces or a
// colon between, which 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. Given that rule, refusing
// "fixed ae12f3" buys no safety and costs a reader the link they
// plainly asked for — a landing message written in the past tense is
// the common case, not the exception.
//
// So: the three tenses of the three verbs, which is GitHub's set and
// the one people already have in their fingers.
var closingPhrase = regexp.MustCompile(
`(?i)\b(?:close[sd]?|closing|fix(?:e[sd])?|fixing|resolve[sd]?|resolving)\b:?[ \t]+(\S+)`)
// addressingPhrase is the same shape for the phrases that LINK without
// settling.
//
// Wider than the closing set on purpose, and safe to be: a false
// positive here links a ticket a reader can see is linked, where a
// false positive on a close shuts somebody's ticket. So the words
// people actually write when a landing moves a ticket without ending
// it all count, in whatever tense.
//
// "re", "see" and "for" are deliberately absent. Each precedes a
// reference in ordinary prose far too often to read as a declaration —
// "see 2058808c3504 for the numbers", "waited for 3600000" — and the
// adjacency rule cannot help, because there the reference IS the next
// token.
var addressingPhrase = regexp.MustCompile(
`(?i)\b(?:address(?:e[sd])?|addressing|` +
`refs?|references?|referenced|referencing|` +
`part of|relates? to|related to|relating to|` +
`works? on|working on|toward|towards|` +
`progress(?:es)? on|contributes? to)\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 bare "#12" reads as a chat line elsewhere, but a chat line
// is never a thing to close or address: after one of these
// verbs it is the ticket numbered 12, as "Closes #12" is on a
// pull request.
if tok.Kind == RefChat {
tok.Kind = RefTicket
}
// A person is never a thing to close, nor a landing (!12): a
// landing settles by its own accept or reject. Both parse as
// tokens, so they are refused here rather than left for the
// caller to notice.
if tok.Kind == RefPerson || tok.Kind == RefLanding {
continue
}
c := tok.Canonical()
if seen[c] {
continue
}
seen[c] = true
out = append(out, tok)
}
return out
}