aboutsummaryrefslogtreecommitdiff
path: root/internal/norm/norm.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/norm/norm.go')
-rw-r--r--internal/norm/norm.go96
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
+}