diff --git a/internal/ai/skills/suggest_side_quests.go b/internal/ai/skills/suggest_side_quests.go index ffcd33a..7f65d79 100644 --- a/internal/ai/skills/suggest_side_quests.go +++ b/internal/ai/skills/suggest_side_quests.go @@ -320,8 +320,9 @@ func drop(q SideQuest, regulars regularNames, known *harness.Deduper, kept []Sid // Chilis" does not, nor does "Cafe" with a combining accent after it // mention "Cafe". Punctuation at either end of the name, with any spacing // beside it, is optional, and a mention takes it in only where text spells -// it and the border still holds after it: "Easy Beef Chili!" is mentioned -// by "Easy Beef Chili." too, and by "Easy Beef Chili!x" without its "!". A +// it, spaced in any way, and the border still holds after it: "Easy Beef +// Chili!" is mentioned by "Easy Beef Chili." too, by "Easy Beef Chili !" +// with its "!", and by "Easy Beef Chili!x" without its "!". A // combining mark is part of its letter, never punctuation. A mention names // its Regular unless a mention of a different Regular covers the same text // or more, so "Your Easy Beef Chili." names "Easy Beef Chili" and not "Beef @@ -356,11 +357,11 @@ func (r regularNames) named(text string) []string { if !bordered(start, end) { continue } - if strings.HasSuffix(text[:start], lead) && bordered(start-len(lead), end) { - start -= len(lead) + if s := spelledBefore(text, start, lead); s >= 0 && bordered(s, end) { + start = s } - if strings.HasPrefix(text[end:], trail) && bordered(start, end+len(trail)) { - end += len(trail) + if e := spelledAfter(text, end, trail); e >= 0 && bordered(start, e) { + end = e } mentions = append(mentions, mention{name, start, end}) } @@ -377,6 +378,33 @@ func (r regularNames) named(text string) []string { return names } +// spelledAfter returns where outer ends when text spells it from at, +// ignoring spacing, or -1 when text does not. +func spelledAfter(text string, at int, outer string) int { + for _, c := range strings.Join(strings.Fields(outer), "") { + at = len(text) - len(strings.TrimLeftFunc(text[at:], unicode.IsSpace)) + if !strings.HasPrefix(text[at:], string(c)) { + return -1 + } + at += utf8.RuneLen(c) + } + return at +} + +// spelledBefore returns where outer starts when text spells it up to at, +// ignoring spacing, or -1 when text does not. +func spelledBefore(text string, at int, outer string) int { + punct := []rune(strings.Join(strings.Fields(outer), "")) + for i := len(punct) - 1; i >= 0; i-- { + at = len(strings.TrimRightFunc(text[:at], unicode.IsSpace)) + if !strings.HasSuffix(text[:at], string(punct[i])) { + return -1 + } + at -= utf8.RuneLen(punct[i]) + } + return at +} + // isWord reports whether c is a letter, a mark on one or a digit. func isWord(c rune) bool { return unicode.IsLetter(c) || unicode.IsMark(c) || unicode.IsDigit(c) } diff --git a/internal/ai/skills/suggest_side_quests_test.go b/internal/ai/skills/suggest_side_quests_test.go index 1b505f4..0a6076c 100644 --- a/internal/ai/skills/suggest_side_quests_test.go +++ b/internal/ai/skills/suggest_side_quests_test.go @@ -254,6 +254,10 @@ func TestSuggestSideQuestsReasonMentionRule(t *testing.T) { "spelled alike by both": {[]string{"Beef Chili!"}, "Corn chips under your Beef Chili.", "Beef Chili", none}, "punctuation then a letter": {[]string{"Beef Chili!"}, "Your Beef Chili!X is our inspiration.", "Beef Chili!", none}, "letter before punctuation": {[]string{"¡Beef Chili"}, "Your X¡Beef Chili is our inspiration.", "¡Beef Chili", none}, + "spacing before punctuation": {[]string{"Beef Chili!"}, "Your Beef Chili ! over corn chips.", "Beef Chili!", ""}, + "spacing after punctuation": {[]string{"¡Beef Chili"}, "Your ¡ Beef Chili is our inspiration.", "¡Beef Chili", ""}, + "stored spacing left off": {[]string{"Beef Chili !", "Chili!"}, "Like your Beef Chili!", "Beef Chili !", ""}, + "inside unspaced punctuation": {[]string{"Beef Chili !", "Chili!"}, "Like your Beef Chili!", "Chili!", none}, "stored symbol left off": {[]string{"Green Chili 🌶"}, "Like your Green Chili, gone green.", "Green Chili 🌶", none}, "stored accent left off": {[]string{"Chicken Cafe\u0301"}, "Like your Chicken Cafe, gone green.", "Chicken Cafe\u0301", none}, "accent written after name": {[]string{"Chicken Cafe"}, "Like your Chicken Cafe\u0301, gone green.", "Chicken Cafe", none},