// 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 }