您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Convert Latin text to Russian (custom mapping) with quick clipboard copy.
// ==UserScript== // @name TesterTV_Translit // @namespace https://greasyfork.org/ru/scripts/479202-testertv-translit-beta // @version 2025.08.31 // @license GNU GPLv3 or later // @description Convert Latin text to Russian (custom mapping) with quick clipboard copy. // @author TesterTV // @match *://*/* // @grant GM_setClipboard // ==/UserScript== (() => { // Do not run in iframes/embeds if (window.self !== window.top) return; // --------- CSS --------- const css = ` #ttv-translit-wrap { position: fixed; top: 60px; right: 10px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Arial, Helvetica, sans-serif; } #ttv-translit-btn { background: none; border: none; font-size: 22px; cursor: pointer; line-height: 1; padding: 4px 6px; filter: drop-shadow(0 1px 1px rgba(0,0,0,.3)); } #ttv-translit-panel { display: none; margin-top: 6px; background: #303236; border: 1px solid #666; border-radius: 6px; padding: 8px; box-shadow: 0 4px 16px rgba(0,0,0,.4); width: 400px; } #ttv-translit-textarea { width: 100%; height: 320px; box-sizing: border-box; background: #1f2023; color: #fff; border: 1px solid #555; border-radius: 4px; font-size: 16px; line-height: 1.4; outline: none; padding: 8px; resize: vertical; } #ttv-translit-meta { display: flex; justify-content: space-between; align-items: center; margin-top: 6px; color: #aaa; font-size: 12px; user-select: none; } #ttv-translit-copyhint { opacity: .85; } `; const style = document.createElement('style'); style.textContent = css; document.documentElement.appendChild(style); // --------- UI --------- const wrap = document.createElement('div'); wrap.id = 'ttv-translit-wrap'; const btn = document.createElement('button'); btn.id = 'ttv-translit-btn'; btn.title = 'Open translit (click). Paste Latin, get Cyrillic. Click outside to copy.'; btn.textContent = '🇹'; const panel = document.createElement('div'); panel.id = 'ttv-translit-panel'; const ta = document.createElement('textarea'); ta.id = 'ttv-translit-textarea'; ta.placeholder = 'Enter text here...'; const meta = document.createElement('div'); meta.id = 'ttv-translit-meta'; const len = document.createElement('div'); len.id = 'ttv-translit-length'; len.textContent = 'length: 0'; const hint = document.createElement('div'); hint.id = 'ttv-translit-copyhint'; hint.textContent = 'Click outside or press Esc to copy & close'; meta.appendChild(len); meta.appendChild(hint); panel.appendChild(ta); panel.appendChild(meta); wrap.appendChild(btn); wrap.appendChild(panel); document.body.appendChild(wrap); // Prevent clicks inside from closing wrap.addEventListener('click', (e) => e.stopPropagation()); // --------- Clipboard --------- function copyToClipboard(txt) { if (!txt || !txt.trim()) return; try { if (typeof GM_setClipboard === 'function') GM_setClipboard(txt); } catch {} if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(txt).catch(() => {}); } } // --------- Transliteration --------- // Base Latin→Cyrillic mapping (from your arrays, simplified) const baseMap = { a:'а', b:'б', c:'ц', d:'д', e:'е', f:'ф', g:'г', h:'х', i:'и', j:'й', k:'к', l:'л', m:'м', n:'н', o:'о', p:'п', q:'я', r:'р', s:'с', t:'т', u:'у', v:'в', w:'щ', x:'х', y:'ы', z:'з' }; const reLatin = /[a-z]/gi; const hasUpper = (s) => s !== s.toLowerCase(); // Case-aware helper const ca = (s, lowerChar) => hasUpper(s) ? lowerChar.toUpperCase() : lowerChar; // Multi-letter rules (applied after basic mapping) const pairMap = { 'зх':'ж', 'цх':'ч', 'сх':'ш', 'шх':'щ' }; const seqRules = [ // Order matters: longer tokens first { re: /##/g, fn: () => 'Ъ' }, { re: /#/g, fn: () => 'ъ' }, { re: /''/g, fn: () => 'Ь' }, { re: /'/g, fn: () => 'ь' }, { re: /(й|ы)о/gi, fn: (m) => ca(m, 'ё') }, { re: /ö/gi, fn: (m) => ca(m, 'ё') }, { re: /йе/gi, fn: (m) => ca(m, 'э') }, { re: /ä/gi, fn: (m) => ca(m, 'э') }, { re: /(й|ы)у/gi, fn: (m) => ca(m, 'ю') }, { re: /ü/gi, fn: (m) => ca(m, 'ю') }, { re: /(й|ы)а/gi, fn: (m) => ca(m, 'я') }, { re: /č/gi, fn: (m) => ca(m, 'ч') }, { re: /ž/gi, fn: (m) => ca(m, 'ж') }, { re: /š/gi, fn: (m) => ca(m, 'ш') }, { re: /твз/gi, fn: (m) => ca(m, 'ъ') }, { re: /мйз/gi, fn: (m) => ca(m, 'ь') } ]; // Build pair rules for zh/ch/sh/shh patterns (зх/цх/сх/шх after basic map) for (const [inp, out] of Object.entries(pairMap)) { seqRules.push({ re: new RegExp(inp, 'gi'), fn: (m) => ca(m, out) }); } // Final custom fix (keep last to avoid undoing earlier rules) seqRules.push({ re: /шод/gi, fn: (m) => ca(m, 'сход') }); function translit(text) { if (!text) return ''; // 1) Basic single-letter transliteration text = text.replace(reLatin, (ch) => { const lower = ch.toLowerCase(); const mapped = baseMap[lower]; if (!mapped) return ch; return ch === ch.toUpperCase() ? mapped.toUpperCase() : mapped; }); // 2) Multi-letter, diacritics, special tokens for (const rule of seqRules) { text = text.replace(rule.re, rule.fn); } return text; } // --------- Behavior --------- function openPanel() { panel.style.display = 'block'; ta.focus(); } function closePanel(copy = true) { if (panel.style.display === 'none') return; if (copy && ta.value.trim()) copyToClipboard(ta.value); ta.value = ''; len.textContent = 'length: 0'; panel.style.display = 'none'; } function togglePanel() { if (panel.style.display === 'none' || !panel.style.display) openPanel(); else closePanel(false); } btn.addEventListener('click', togglePanel); // If you prefer the original mouseover open behavior, replace the above with: // btn.addEventListener('mouseover', openPanel); document.addEventListener('click', () => closePanel(true)); document.addEventListener('keydown', (e) => { if (panel.style.display !== 'none' && e.key === 'Escape') { e.preventDefault(); closePanel(true); } }); ta.addEventListener('input', () => { const caretEnd = ta.selectionEnd; // optional: we can keep caret, but simple approach recalculates whole text const beforeLen = ta.value.length; ta.value = translit(ta.value); len.textContent = 'length: ' + ta.value.length; // Optional caret restore (best effort): const afterLen = ta.value.length; const delta = afterLen - beforeLen; try { ta.selectionStart = ta.selectionEnd = Math.max(0, caretEnd + delta); } catch {} }); // Do not close when clicking inside the panel panel.addEventListener('click', (e) => e.stopPropagation()); })();