Files
cyrillic-lang-data/translit.js
T
full_droperandClaude Opus 5 2ff3c9ae4a Додано translit.js: транслітерація має жити поруч із фільтрами
Скелети, якими побудовано латинський шар фільтрів, мусять точно збігатися
з тими, що обчислює функція на вхідних словах. Тримаємо їх в одному пакеті,
щоб вони не розʼїхалися.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 12:32:08 +03:00

103 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Зведення слова до «скелета» — спільного вигляду для кирилиці та латинської транслітерації.
// Мета: щоб «привет», «privet», «privyet», «pryvjet» дали однаковий ключ.
// Використовується і при збиранні фільтрів, і в самій функції на вхідних словах.
// Латинські літери, якими підміняють кириличні, щоб обійти детектор (гомогліфи).
const HOMOGLYPHS = {
a: 'а', b: 'ь', c: 'с', e: 'е', h: 'н', i: 'і', j: 'ј', k: 'к', m: 'м',
o: 'о', p: 'р', s: 'ѕ', t: 'т', x: 'х', y: 'у', A: 'А', B: 'В', C: 'С',
E: 'Е', H: 'Н', I: 'І', K: 'К', M: 'М', O: 'О', P: 'Р', T: 'Т', X: 'Х', Y: 'У'
};
// Кирилиця → латинський скелет. Масив = неоднозначний запис, дає кілька варіантів.
const CYR = {
а: 'a', б: 'b', в: 'v', г: ['g', 'h'], ґ: 'g', д: 'd', е: 'e', є: 'ie', ё: ['e', 'o'],
ж: 'Z', з: 'z', и: 'i', і: 'i', ї: 'i', й: 'i', к: 'k', л: 'l', м: 'm', н: 'n',
о: 'o', п: 'p', р: 'r', с: 's', т: 't', у: 'u', ў: 'u', ф: 'f', х: 'h', ц: 'c',
ч: 'C', ш: 'S', щ: ['S', 'Sc'], ъ: '', ы: 'i', ь: '', э: 'e', ю: 'iu', я: 'ia',
// сербська, македонська
ђ: 'dj', ј: 'i', љ: 'l', њ: 'n', ћ: 'c', џ: 'dZ', ѓ: 'g', ќ: 'k', ѕ: 'z',
// тюркські, кавказькі, монгольська
ә: 'a', ғ: 'g', қ: 'k', ң: 'n', ө: 'o', ұ: 'u', ү: 'u', һ: 'h', ӣ: 'i', ӯ: 'u',
ҳ: 'h', ҷ: 'C', ҹ: 'C', җ: 'Z', ӑ: 'a', ӗ: 'e', ҫ: 's', ӳ: 'u', ӧ: 'o', ӱ: 'u',
ӹ: 'i', ӏ: '', ҕ: 'g', ҥ: 'n', ө: 'o', ӊ: 'n', ԥ: 'p', ҍ: '', ҩ: 'o'
};
// Цифри замість літер — теж спосіб обійти детектор: «4то», «3ачем».
const LEET = { 4: 'ch', 3: 'z', 0: 'o', 1: 'i', 6: 'b', 5: 's', 9: 'ya' };
// Латиниця → той самий скелет. Порядок важливий: спершу диграфи.
const LAT_RULES = [
[/shch|shtch|sch|szcz/g, 'S'],
[/zh|jh/g, 'Z'],
[/ch|tsch|cz/g, 'C'],
[/sh|sz/g, 'S'],
[/kh/g, 'h'],
[/ts|tc|tz/g, 'c'],
[/ya|ja/g, 'ia'],
[/yu|ju/g, 'iu'],
[/ye|je/g, 'e'], // є і е зводимо до одного
[/yo|jo/g, 'o'], // ё найчастіше пишуть як o
[/yi|ji|yy/g, 'i'],
[/gh/g, 'g'],
[/ck/g, 'k'],
[/[wq]/g, (m) => (m === 'w' ? 'v' : 'k')],
[/x/g, 'h'],
[/[yj]/g, 'i']
];
const hasCyr = (s) => /[Ѐ-ӿ]/.test(s);
// Прибирає подвоєння та службові знаки, зводить «ie» до «e» (є ≡ е).
function tidy(s) {
return s
.replace(/[^a-zA-Z]/g, '')
.replace(/(.)\1+/g, '$1')
.replace(/ie/g, 'e')
.replace(/(.)\1+/g, '$1');
}
// Кирилиця → масив варіантів скелета (обмежуємо, щоб не розростався).
function fromCyrillic(word) {
let variants = [''];
for (const ch of word) {
const map = CYR[ch];
if (map === undefined) continue; // цифри, знаки, латинські вкраплення
const options = Array.isArray(map) ? map : [map];
const next = [];
for (const v of variants) for (const o of options) next.push(v + o);
variants = next.length > 8 ? next.slice(0, 8) : next;
}
return variants;
}
function fromLatin(word) {
const base = word.toLowerCase().replace(/[0-9]/g, (d) => LEET[d] ?? '');
// «j» читають і як «й» (ja = я), і як «ж» (jizn = жизнь) — беремо обидва прочитання.
const branches = base.includes('j') ? [base, base.replace(/j/g, 'Z')] : [base];
return branches.map((b) => {
let s = b;
for (const [re, to] of LAT_RULES) s = s.replace(re, to);
return s;
});
}
// Якщо слово змішане і кирилиці в ньому більше — латинські гомогліфи вважаємо підміною.
export function deHomoglyph(word) {
const cyr = (word.match(/[Ѐ-ӿ]/g) || []).length;
const lat = (word.match(/[a-zA-Z]/g) || []).length;
if (cyr === 0 || lat === 0 || cyr < lat) return word;
return word.replace(/[a-zA-Z]/g, (c) => HOMOGLYPHS[c] || c);
}
// Головна функція: слово → масив скелетів (зазвичай один).
export function skeletons(word) {
const w = deHomoglyph(word.toLowerCase().replace(/[̀-ͯ҃-҉]/g, ''));
const raw = hasCyr(w) ? fromCyrillic(w) : fromLatin(w);
return [...new Set(raw.map(tidy).filter((s) => s.length > 1))];
}
export function skeleton(word) {
return skeletons(word)[0] || '';
}