153 lines
4.6 KiB
Go
153 lines
4.6 KiB
Go
// Package search — поиск по каталогу через Meilisearch (docs/library-and-storage.md#поиск).
|
||
package search
|
||
|
||
import (
|
||
"strings"
|
||
"unicode"
|
||
)
|
||
|
||
// Established spellings cannot always be derived letter by letter. Keep these
|
||
// as whole-word alternatives, retaining the mechanical transliteration too.
|
||
var pronunciationWords = map[string]string{"boulevard": "бульвар"}
|
||
|
||
func TranslitVariants(s string) []string {
|
||
out := []string{}
|
||
if mechanical := Translit(s); mechanical != "" {
|
||
out = append(out, mechanical)
|
||
}
|
||
words := strings.Fields(strings.ToLower(s))
|
||
changed := false
|
||
for i, word := range words {
|
||
if spoken, ok := pronunciationWords[word]; ok {
|
||
words[i], changed = spoken, true
|
||
} else if translated := Translit(word); translated != "" {
|
||
words[i] = translated
|
||
}
|
||
}
|
||
if changed {
|
||
out = append(out, strings.Join(words, " "))
|
||
}
|
||
return out
|
||
}
|
||
|
||
func searchKey(s string) string {
|
||
return strings.Join(strings.FieldsFunc(strings.ToLower(s), func(r rune) bool {
|
||
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
|
||
}), " ")
|
||
}
|
||
|
||
// Кириллица → латиница (упрощённая ГОСТ-подобная схема: «Кино» → «kino», «Щедрин» → «shchedrin»).
|
||
var cyrToLat = map[rune]string{
|
||
'а': "a", 'б': "b", 'в': "v", 'г': "g", 'д': "d", 'е': "e", 'ё': "e", 'ж': "zh", 'з': "z", 'и': "i",
|
||
'й': "y", 'к': "k", 'л': "l", 'м': "m", 'н': "n", 'о': "o", 'п': "p", 'р': "r", 'с': "s", 'т': "t",
|
||
'у': "u", 'ф': "f", 'х': "kh", 'ц': "ts", 'ч': "ch", 'ш': "sh", 'щ': "shch", 'ъ': "", 'ы': "y", 'ь': "",
|
||
'э': "e", 'ю': "yu", 'я': "ya", 'і': "i", 'ї': "yi", 'є': "ye", 'ґ': "g",
|
||
}
|
||
|
||
// Латиница → кириллица: сначала длинные сочетания.
|
||
var latToCyr = []struct{ lat, cyr string }{
|
||
{"shch", "щ"}, {"zh", "ж"}, {"kh", "х"}, {"ts", "ц"}, {"ch", "ч"}, {"sh", "ш"}, {"yu", "ю"}, {"ya", "я"},
|
||
{"yo", "ё"}, {"a", "а"}, {"b", "б"}, {"v", "в"}, {"g", "г"}, {"d", "д"}, {"e", "е"}, {"z", "з"},
|
||
{"i", "и"}, {"y", "й"}, {"k", "к"}, {"l", "л"}, {"m", "м"}, {"n", "н"}, {"o", "о"}, {"p", "п"},
|
||
{"r", "р"}, {"s", "с"}, {"t", "т"}, {"u", "у"}, {"f", "ф"}, {"h", "х"}, {"c", "к"}, {"w", "в"},
|
||
{"x", "кс"}, {"q", "к"}, {"j", "дж"},
|
||
}
|
||
|
||
// Translit — вариант строки в другой письменности для поля *_translit: кириллица → латиница,
|
||
// латиница → кириллица. Смешанные строки переводятся посимвольно. Пустая строка — перевод не нужен.
|
||
func Translit(s string) string {
|
||
s = strings.ToLower(s)
|
||
hasCyr, hasLat := false, false
|
||
for _, r := range s {
|
||
switch {
|
||
case unicode.Is(unicode.Cyrillic, r):
|
||
hasCyr = true
|
||
case r < unicode.MaxASCII && unicode.IsLetter(r):
|
||
hasLat = true
|
||
}
|
||
}
|
||
var b strings.Builder
|
||
switch {
|
||
case hasCyr:
|
||
for _, r := range s {
|
||
if v, ok := cyrToLat[r]; ok {
|
||
b.WriteString(v)
|
||
} else {
|
||
b.WriteRune(r)
|
||
}
|
||
}
|
||
case hasLat:
|
||
for i := 0; i < len(s); {
|
||
matched := false
|
||
for _, p := range latToCyr {
|
||
if strings.HasPrefix(s[i:], p.lat) {
|
||
b.WriteString(p.cyr)
|
||
i += len(p.lat)
|
||
matched = true
|
||
break
|
||
}
|
||
}
|
||
if !matched {
|
||
b.WriteByte(s[i])
|
||
i++
|
||
}
|
||
}
|
||
default:
|
||
return ""
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
// Раскладки ЙЦУКЕН ↔ QWERTY: та же клавиша.
|
||
const (
|
||
qwerty = "`qwertyuiop[]asdfghjkl;'zxcvbnm,./~QWERTYUIOP{}ASDFGHJKL:\"ZXCVBNM<>?"
|
||
jcuken = "ёйцукенгшщзхъфывапролджэячсмитьбю.ЁЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,"
|
||
)
|
||
|
||
var (
|
||
toRu = map[rune]rune{}
|
||
toEn = map[rune]rune{}
|
||
)
|
||
|
||
func init() {
|
||
q, j := []rune(qwerty), []rune(jcuken)
|
||
for i := range q {
|
||
toRu[q[i]] = j[i]
|
||
toEn[j[i]] = q[i]
|
||
}
|
||
}
|
||
|
||
// SwitchLayout — запрос, набранный не в той раскладке: «ghbdtn» → «привет», «руддщ» → «hello».
|
||
// Направление выбирается по большинству букв. Пустая строка — менять нечего.
|
||
func SwitchLayout(s string) string {
|
||
lat, cyr := 0, 0
|
||
for _, r := range s {
|
||
switch {
|
||
case unicode.Is(unicode.Cyrillic, r):
|
||
cyr++
|
||
case r < unicode.MaxASCII && unicode.IsLetter(r):
|
||
lat++
|
||
}
|
||
}
|
||
if lat == 0 && cyr == 0 {
|
||
return ""
|
||
}
|
||
m := toRu
|
||
if cyr > lat {
|
||
m = toEn
|
||
}
|
||
var b strings.Builder
|
||
for _, r := range s {
|
||
if v, ok := m[r]; ok {
|
||
b.WriteRune(v)
|
||
} else {
|
||
b.WriteRune(r)
|
||
}
|
||
}
|
||
out := b.String()
|
||
if out == s {
|
||
return ""
|
||
}
|
||
return out
|
||
}
|