diff options
Diffstat (limited to 'internal/norm/norm.go')
| -rw-r--r-- | internal/norm/norm.go | 64 |
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. |
