Files

68 lines
2.0 KiB
Go

// Package casefold makes a case-insensitive search able to quote what it found.
//
// The obvious way to search text without regard to case is to lowercase both
// sides and take strings.Index. The offset that comes back addresses the folded
// copy, not the original, and the two do not line up: strings.ToLower maps rune
// by rune, and a rune can change byte length doing it (U+0130 is two bytes and
// folds to one, U+212A is three). Past the first such rune, slicing the original
// at a folded offset lands mid-rune. It quotes bytes the writer never typed, or
// cuts one in half into invalid UTF-8.
//
// Index hands back the map that fixes it, so a match found in the fold can be
// quoted from the source exactly as it was written.
package casefold
import (
"strings"
"unicode"
)
// Index folds s to lower case and returns the folded text together with, for
// every byte offset in it, the offset it came from in s. The map has one entry
// past the end, so the end of a match at the end of the text maps too.
//
// A nil map means the two are byte-for-byte aligned and offsets need no
// translation, which is the case for ASCII text and so for almost every call.
func Index(s string) (string, []int) {
if isASCII(s) {
return strings.ToLower(s), nil
}
var b strings.Builder
b.Grow(len(s))
offsets := make([]int, 0, len(s)+1)
for i, r := range s {
before := b.Len()
b.WriteRune(unicode.ToLower(r))
for n := b.Len() - before; n > 0; n-- {
offsets = append(offsets, i)
}
}
offsets = append(offsets, len(s))
return b.String(), offsets
}
// Origin translates a byte offset in the folded copy back to the original. It
// returns -1 for an offset the map does not cover, which a caller must treat as
// "cannot quote this" rather than as position zero.
func Origin(offsets []int, at int) int {
if offsets == nil {
if at < 0 {
return -1
}
return at
}
if at < 0 || at >= len(offsets) {
return -1
}
return offsets[at]
}
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= 0x80 {
return false
}
}
return true
}