diff options
Diffstat (limited to 'internal/norm/norm.go')
| -rw-r--r-- | internal/norm/norm.go | 96 |
1 files changed, 96 insertions, 0 deletions
diff --git a/internal/norm/norm.go b/internal/norm/norm.go new file mode 100644 index 0000000..e7933ec --- /dev/null +++ b/internal/norm/norm.go @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +// Package norm puts text into the form krino compares it in: optionally +// without diacritics, optionally lower case, with white space collapsed. +package norm + +import ( + "strings" + "unicode" + + unorm "golang.org/x/text/unicode/norm" +) + +// special holds the letters that do not decompose under Unicode NFD, so +// Fold maps them explicitly. +var special = map[rune]string{ + 'ł': "l", 'Ł': "L", + 'ø': "o", 'Ø': "O", + 'đ': "d", 'Đ': "D", + 'ħ': "h", 'Ħ': "H", + 'ß': "ss", + 'æ': "ae", 'Æ': "AE", + 'œ': "oe", 'Œ': "OE", + 'ı': "i", +} + +// Fold strips diacritics: Unicode NFD, drop combining marks (category Mn), +// then map the letters that do not decompose. ASCII input is returned +// unchanged without allocating. +func Fold(s string) string { + ascii := true + for i := 0; i < len(s); i++ { + if s[i] >= 0x80 { + ascii = false + break + } + } + if ascii { + return s + } + + var b strings.Builder + b.Grow(len(s)) + for _, r := range unorm.NFD.String(s) { + if unicode.Is(unicode.Mn, r) { + continue + } + if rep, ok := special[r]; ok { + b.WriteString(rep) + continue + } + b.WriteRune(r) + } + return b.String() +} + +// Text puts s into the form content and keywords are compared in: Fold if +// fold, strings.ToLower if ignoreCase, then every run of Unicode white +// space becomes one ASCII space and both ends are trimmed. +func Text(s string, ignoreCase, fold bool) string { + if fold { + s = Fold(s) + } + if ignoreCase { + s = strings.ToLower(s) + } + + var b strings.Builder + b.Grow(len(s)) + inSpace := false + started := false + for _, r := range s { + if unicode.IsSpace(r) { + if started { + inSpace = true + } + continue + } + if inSpace { + b.WriteByte(' ') + inSpace = false + } + b.WriteRune(r) + started = true + } + return b.String() +} + +// Name puts a file name into the form it is matched against: Fold(s) if +// fold, else s. Case is handled by the regex flag, not here. +func Name(s string, fold bool) string { + if fold { + return Fold(s) + } + return s +} |
