Додано translit.js: транслітерація має жити поруч із фільтрами
Скелети, якими побудовано латинський шар фільтрів, мусять точно збігатися з тими, що обчислює функція на вхідних словах. Тримаємо їх в одному пакеті, щоб вони не розʼїхалися. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0411edd826
commit
2ff3c9ae4a
+8
-3
@@ -1,9 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "cyrillic-lang-data",
|
"name": "cyrillic-lang-data",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "Фільтри Блума на 8.6 млн словоформ 36 кириличних мов + таблиці літер, закінчень і триграм. Дані для ru-detector-v2.",
|
"description": "Фільтри Блума на 8.6 млн словоформ 36 кириличних мов + таблиці літер, закінчень і триграм. Дані для ru-detector-v2.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"license": "CC-BY-SA-4.0",
|
"license": "CC-BY-SA-4.0",
|
||||||
"files": ["index.js", "signals.json", "filters"]
|
"files": [
|
||||||
}
|
"index.js",
|
||||||
|
"translit.js",
|
||||||
|
"signals.json",
|
||||||
|
"filters"
|
||||||
|
]
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
// Зведення слова до «скелета» — спільного вигляду для кирилиці та латинської транслітерації.
|
||||||
|
// Мета: щоб «привет», «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] || '';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user