summaryrefslogtreecommitdiff
path: root/internal/norm/norm.go
blob: e7933ec643d2e031c18026bdf7e143fb926dc6b6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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
}