117 lines
3.4 KiB
Go
117 lines
3.4 KiB
Go
package store
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"sort"
|
||
|
|
"strings"
|
||
|
|
"unicode"
|
||
|
|
)
|
||
|
|
|
||
|
|
// MentionsIn reads the people named with an @ out of a body, and returns their
|
||
|
|
// ids in the order they are written.
|
||
|
|
//
|
||
|
|
// Slået op i navnene frem for tolket ud af teksten. Et navn kan have mellemrum,
|
||
|
|
// bindestreg og punktum i sig, og et udtryk, der skulle fange dem alle, ville
|
||
|
|
// også fange sætningen bagefter: `@Sofie i morgen` ville blive til en person, der
|
||
|
|
// hedder "Sofie i morgen", og ellers til "Sofie", alt efter hvor grådigt det var
|
||
|
|
// skrevet. Her er der ikke noget at gætte — der står et navn, eller der gør ikke.
|
||
|
|
//
|
||
|
|
// Det er også dét, der gør det forsvarligt at *dele* på: en omtale giver adgang,
|
||
|
|
// og adgang må ikke afhænge af, hvor langt et regulært udtryk valgte at læse.
|
||
|
|
//
|
||
|
|
// Længste navn først, så "@Sofie Jensen" bliver Sofie Jensen og ikke Sofie med et
|
||
|
|
// efternavn hængende bagefter — også når begge findes som konti.
|
||
|
|
func MentionsIn(body string, people []Person) []string {
|
||
|
|
if body == "" || len(people) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
lower := strings.ToLower(body)
|
||
|
|
|
||
|
|
byLength := make([]Person, len(people))
|
||
|
|
copy(byLength, people)
|
||
|
|
sort.SliceStable(byLength, func(i, j int) bool {
|
||
|
|
return len(byLength[i].Name) > len(byLength[j].Name)
|
||
|
|
})
|
||
|
|
|
||
|
|
// Hvilke tegn der allerede er læst som en del af en omtale. Uden det ville
|
||
|
|
// "@Sofie Jensen" også tælle som en omtale af Sofie, hvis begge er konti — og
|
||
|
|
// noten ville blive delt med en, der ikke står i den.
|
||
|
|
claimed := make([]bool, len(body))
|
||
|
|
found := map[string]int{} // bruger -> hvor i teksten, så rækkefølgen kan bæres
|
||
|
|
|
||
|
|
for _, p := range byLength {
|
||
|
|
name := strings.ToLower(strings.TrimSpace(p.Name))
|
||
|
|
if name == "" || p.ID == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
needle := "@" + name
|
||
|
|
for at := 0; ; {
|
||
|
|
i := strings.Index(lower[at:], needle)
|
||
|
|
if i < 0 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
i += at
|
||
|
|
at = i + len(needle)
|
||
|
|
|
||
|
|
end := i + len(needle)
|
||
|
|
if overlaps(claimed, i, end) || !isMention(body, i, end) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
for j := i; j < end; j++ {
|
||
|
|
claimed[j] = true
|
||
|
|
}
|
||
|
|
if _, seen := found[p.ID]; !seen {
|
||
|
|
found[p.ID] = i
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
out := make([]string, 0, len(found))
|
||
|
|
for id := range found {
|
||
|
|
out = append(out, id)
|
||
|
|
}
|
||
|
|
sort.Slice(out, func(i, j int) bool { return found[out[i]] < found[out[j]] })
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
func overlaps(claimed []bool, from, to int) bool {
|
||
|
|
for i := from; i < to && i < len(claimed); i++ {
|
||
|
|
if claimed[i] {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// isMention er de to kanter om navnet.
|
||
|
|
//
|
||
|
|
// Foran: begyndelsen af noten eller noget, der ikke er en bogstavstavelse — så en
|
||
|
|
// e-mailadresse ikke bliver til en omtale, fordi der tilfældigvis står et navn
|
||
|
|
// efter snabel-a'et.
|
||
|
|
//
|
||
|
|
// Bagved: ikke midt i et længere ord. "@Sofies kalender" er skrevet om Sofie og
|
||
|
|
// tæller med — ejefaldets s hører til sætningen, ikke til en anden person — men
|
||
|
|
// "@Sofienberg" er et sted og ikke en, der skal have noten delt. Grænsen er sat
|
||
|
|
// ved, at et bogstav *lige* efter navnet kun må være et ejefalds-s.
|
||
|
|
func isMention(body string, from, to int) bool {
|
||
|
|
runes := []rune(body[:from])
|
||
|
|
if len(runes) > 0 {
|
||
|
|
prev := runes[len(runes)-1]
|
||
|
|
if unicode.IsLetter(prev) || unicode.IsDigit(prev) || prev == '@' {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
rest := body[to:]
|
||
|
|
if rest == "" {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
next := []rune(rest)
|
||
|
|
if next[0] == 's' || next[0] == 'S' {
|
||
|
|
next = next[1:]
|
||
|
|
if len(next) == 0 {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return !unicode.IsLetter(next[0]) && !unicode.IsDigit(next[0])
|
||
|
|
}
|