diff --git a/package.json b/package.json index 55dce7d..5befa28 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,14 @@ { "name": "cyrillic-lang-data", - "version": "1.0.0", + "version": "1.1.0", "description": "Фільтри Блума на 8.6 млн словоформ 36 кириличних мов + таблиці літер, закінчень і триграм. Дані для ru-detector-v2.", "type": "module", "main": "index.js", "license": "CC-BY-SA-4.0", - "files": ["index.js", "signals.json", "filters"] -} + "files": [ + "index.js", + "translit.js", + "signals.json", + "filters" + ] +} \ No newline at end of file diff --git a/translit.js b/translit.js new file mode 100644 index 0000000..6d8825e --- /dev/null +++ b/translit.js @@ -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] || ''; +}