// Зведення слова до «скелета» — спільного вигляду для кирилиці та латинської транслітерації. // Мета: щоб «привет», «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] || ''; }