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.go64
1 files changed, 64 insertions, 0 deletions
diff --git a/internal/norm/norm.go b/internal/norm/norm.go
index 701f765..bff2127 100644
--- a/internal/norm/norm.go
+++ b/internal/norm/norm.go
@@ -8,6 +8,7 @@ import (
"fmt"
"strings"
"unicode"
+ "unicode/utf8"
unorm "golang.org/x/text/unicode/norm"
)
@@ -70,6 +71,69 @@ func Fold(s string) string {
return b.String()
}
+// Folded is Fold's result with a way back to the text it was folded from.
+type Folded struct {
+ Text string // Fold(src)
+
+ src string
+ same bool // Text is src (ASCII)
+ start, end []int // per byte of Text, the bounds in src of the character it came from; nil when no map exists
+}
+
+// FoldMapped folds s like Fold and keeps, for every byte of the result, the
+// original character it came from, so a regex match on the folded text can
+// be read back from s with its diacritics. The map is built by folding one
+// character at a time (a run of invalid bytes counts as one, as Fold's
+// U+FFFD does); in the rare case that differs from Fold's decomposition of
+// the whole string (combining marks NFD reorders), there is no map and
+// Source returns the folded text.
+func FoldMapped(s string) Folded {
+ text := Fold(s)
+ if text == s {
+ return Folded{Text: text, src: s, same: true}
+ }
+ var b strings.Builder
+ start := make([]int, 0, len(text))
+ end := make([]int, 0, len(text))
+ for i := 0; i < len(s); {
+ j, piece := i, ""
+ if r, w := utf8.DecodeRuneInString(s[i:]); r == utf8.RuneError && w == 1 {
+ for j < len(s) {
+ if r, w := utf8.DecodeRuneInString(s[j:]); r != utf8.RuneError || w != 1 {
+ break
+ }
+ j++
+ }
+ piece = "\ufffd"
+ } else {
+ j = i + w
+ piece = Fold(s[i:j])
+ }
+ b.WriteString(piece)
+ for range len(piece) {
+ start = append(start, i)
+ end = append(end, j)
+ }
+ i = j
+ }
+ if b.String() != text {
+ return Folded{Text: text, src: s}
+ }
+ return Folded{Text: text, src: s, start: start, end: end}
+}
+
+// Source returns the original text that Text[a:b] was folded from, widened
+// to whole characters; without a map, Text[a:b] itself.
+func (f Folded) Source(a, b int) string {
+ switch {
+ case f.same || f.start == nil:
+ return f.Text[a:b]
+ case a >= b:
+ return ""
+ }
+ return f.src[f.start[a]:f.end[b-1]]
+}
+
// 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.