bukvoeb / index.html
2ch's picture
Update index.html
f95e32b verified
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Заменитель буков</title>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--border: #30363d;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--accent: #58a6ff;
--accent-hover: #79b8ff;
--accent-glow: rgba(88, 166, 255, 0.15);
--success: #3fb950;
--toggle-bg: #30363d;
--toggle-active: #58a6ff;
--radius: 12px;
--transition: 0.25s ease;
}
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
width: 100%;
max-width: 700px;
display: flex;
flex-direction: column;
gap: 20px;
}
.header {
text-align: center;
}
.header h1 {
font-size: 1.5rem;
font-weight: 600;
letter-spacing: -0.02em;
color: var(--text-primary);
}
.header p {
font-size: 0.85rem;
color: var(--text-secondary);
margin-top: 6px;
}
.controls {
display: flex;
justify-content: center;
gap: 32px;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 10px;
}
.control-label {
font-size: 0.8rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 500;
}
.toggle {
position: relative;
display: flex;
background: var(--toggle-bg);
border-radius: 8px;
padding: 3px;
gap: 2px;
}
.toggle input[type="radio"] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.toggle label {
padding: 6px 14px;
font-size: 0.8rem;
font-weight: 500;
color: var(--text-secondary);
border-radius: 6px;
cursor: pointer;
transition: all var(--transition);
user-select: none;
white-space: nowrap;
}
.toggle input[type="radio"]:checked + label {
background: var(--accent);
color: #fff;
box-shadow: 0 2px 8px rgba(88, 166, 255, 0.3);
}
.toggle label:hover {
color: var(--text-primary);
}
.editor {
position: relative;
}
#text {
width: 100%;
min-height: 280px;
height: calc(100vh - 300px);
padding: 18px 20px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.95rem;
line-height: 1.6;
color: var(--text-primary);
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
resize: vertical;
outline: none;
transition: border-color var(--transition), box-shadow var(--transition);
}
#text::placeholder {
color: var(--text-secondary);
opacity: 0.6;
}
#text:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
.editor-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
padding: 0 4px;
}
.char-count {
font-size: 0.75rem;
color: var(--text-secondary);
font-variant-numeric: tabular-nums;
}
.actions {
display: flex;
justify-content: center;
gap: 12px;
}
.btn {
padding: 12px 48px;
font-size: 0.95rem;
font-weight: 600;
color: #fff;
background: var(--accent);
border: none;
border-radius: 10px;
cursor: pointer;
transition: all var(--transition);
letter-spacing: 0.01em;
position: relative;
overflow: hidden;
}
.btn:hover {
background: var(--accent-hover);
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(88, 166, 255, 0.3);
}
.btn:active {
transform: translateY(0);
}
.btn.success {
background: var(--success);
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-secondary);
border: 1px solid var(--border);
padding: 12px 28px;
}
.btn-secondary:hover {
background: var(--toggle-bg);
color: var(--text-primary);
box-shadow: none;
transform: translateY(-1px);
}
.toast {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%) translateY(80px);
background: var(--bg-tertiary);
color: var(--text-primary);
padding: 12px 24px;
border-radius: 10px;
font-size: 0.85rem;
font-weight: 500;
border: 1px solid var(--border);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
opacity: 0;
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
z-index: 100;
}
.toast.visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.toast .icon {
margin-right: 8px;
}
@media (max-width: 480px) {
.controls {
gap: 16px;
}
.btn {
width: 100%;
}
#text {
min-height: 220px;
height: calc(100vh - 380px);
font-size: 0.9rem;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>заменитель буков</h1>
<p>заменяет символы на визуально идентичные юникод-аналоги</p>
</div>
<div class="controls">
<div class="control-group">
<span class="control-label">язык</span>
<div class="toggle">
<input type="radio" name="lang" id="lang-ru" value="ru" checked>
<label for="lang-ru">RU</label>
<input type="radio" name="lang" id="lang-en" value="en">
<label for="lang-en">EN</label>
</div>
</div>
<div class="control-group">
<span class="control-label">режим</span>
<div class="toggle">
<input type="radio" name="mode" id="mode-light" value="light">
<label for="mode-light">слабый</label>
<input type="radio" name="mode" id="mode-full" value="full" checked>
<label for="mode-full">сильный</label>
</div>
</div>
</div>
<div class="editor">
<textarea id="text" placeholder="вставь текст и нажми кнопку или Ctrl+Enter / Cmd+Enter для быстрого запуска: результат скопируется в буфер обмена автоматически"></textarea>
<div class="editor-footer">
<span class="char-count" id="charCount">0 сим.</span>
</div>
</div>
<div class="actions">
<button class="btn btn-secondary" id="revertBtn" type="button">вернуть</button>
<button class="btn" id="processBtn" type="button">сделать</button>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
(() => {
'use strict';
const maps = {
ruLight: {
'А': 'A', 'а': 'a', 'В': 'B', 'Е': 'E', 'е': 'e',
'З': '3', 'й': 'ѝ', 'К': 'K', 'Л': 'Ʌ', 'М': 'M',
'Н': 'H', 'О': 'O', 'о': 'o', 'П': 'Ⲡ', 'п': 'ⲡ',
'Р': 'P', 'р': 'p', 'С': 'C', 'с': 'c', 'Т': 'T',
'у': 'y', 'Х': 'X', 'х': 'x', 'Я': 'Я', 'я': 'ᴙ'
},
ruFull: {
'А': 'A', 'а': 'a', 'Б': 'Ƃ', 'б': 'б', 'В': 'B', 'в': 'ʙ',
'Г': 'Γ', 'г': 'ᴦ', 'Е': 'E', 'е': 'e', 'Ё': 'Ë', 'ё': 'ë',
'З': 'З', 'з': 'з', 'И': 'Ͷ', 'и': 'ᴎ', 'Й': 'Ѝ', 'й': 'ѝ',
'К': 'K', 'к': 'ĸ', 'Л': 'Ʌ', 'л': 'ᴫ', 'М': 'M', 'м': 'ᴍ',
'Н': 'Н', 'н': 'ʜ', 'О': 'O', 'о': 'o', 'П': 'Ⲡ', 'п': 'ⲡ',
'Р': 'P', 'р': 'p', 'С': 'C', 'с': 'c', 'Т': 'T', 'т': 'ⲧ',
'У': 'Ꭹ', 'у': 'y', 'Ф': 'Ⲫ', 'ф': 'ⲫ', 'Х': 'X', 'х': 'x',
'Ч': 'Ч', 'ч': 'ч', 'Ш': 'Ɯ', 'ш': 'ꟺ', 'Щ': 'Щ', 'щ': 'ɰ',
'Ъ': 'Ѣ', 'ъ': 'Ꙏ', 'Ь': 'Ⱃ', 'ь': 'ь', 'Я': 'Я', 'я': 'ᴙ'
},
enLight: {
'A': 'А', 'a': 'а', 'B': 'В', 'C': 'С', 'c': 'с',
'E': 'Е', 'e': 'е', 'g': 'ɡ', 'H': 'Н', 'I': 'І',
'i': 'і', 'J': 'Ј', 'j': 'ј', 'K': 'К', 'M': 'М',
'n': 'ո', 'O': 'О', 'o': 'о', 'P': 'Р', 'p': 'р',
'S': 'Ѕ', 's': 'ѕ', 'T': 'Т', 'U': 'Ս', 'w': 'ᴡ',
'X': 'Х', 'x': 'х', 'Y': 'У', 'y': 'у', 'Z': 'Ζ',
'z': 'ⲍ'
},
enFull: {
'A': 'А', 'a': 'а', 'B': 'В', 'b': 'ɓ', 'C': 'С', 'c': 'с',
'D': 'D', 'd': 'ԁ', 'E': 'Е', 'e': 'е', 'F': 'Ϝ', 'f': 'ƒ',
'G': 'Ꮆ', 'g': 'ɡ', 'H': 'Н', 'h': 'һ', 'I': 'І', 'i': 'і',
'J': 'Ј', 'j': 'ј', 'K': 'К', 'k': 'κ', 'L': 'Ꝉ', 'l': 'ⅼ',
'M': 'М', 'm': 'ᴍ', 'N': 'Ɲ', 'n': 'ո', 'O': 'О', 'o': 'о',
'P': 'Р', 'p': 'р', 'Q': 'Ϙ', 'q': 'ɋ', 'R': 'Ɍ', 'r': 'ꭈ',
'S': 'Ѕ', 's': 'ѕ', 'T': 'Т', 't': 'τ', 'U': 'Ս', 'u': 'υ',
'V': 'Ѵ', 'v': 'ѵ', 'W': 'Ⱳ', 'w': 'ᴡ', 'X': 'Х', 'x': 'х',
'Y': 'У', 'y': 'у', 'Z': 'Ζ', 'z': 'ⲍ'
},
};
function getSettings() {
const lang = document.querySelector('input[name="lang"]:checked').value;
const mode = document.querySelector('input[name="mode"]:checked').value;
return { lang, mode };
}
function getActiveMap({ lang, mode }) {
const key = `${lang}${mode.charAt(0).toUpperCase() + mode.slice(1)}`; // ruLight, enFull …
return maps[key] ?? {};
}
function invertMap(map) {
const inv = {};
for (const [k, v] of Object.entries(map)) {
inv[v] = k;
}
return inv;
}
async function revert() {
const settings = getSettings();
const map = invertMap(getActiveMap(settings));
let text = textEl.value;
if (!text.trim()) {
showToast('⚠️ поле пустое');
return;
}
text = replaceByMap(text, map);
textEl.value = text;
updateCharCount();
try {
await copyToClipboard(text);
showToast('<span class="icon">↩️</span>возвращено и скопировано');
} catch {
showToast('<span class="icon">↩️</span>возвращено');
}
}
function fixTypography(str) {
return str
.replace(/--/g, '—')
.replace(/ - /g, ' — ')
.replace(/(?<=\S)- /g, '— ')
.replace(/"([^"]+)"/g, '«$1»');
}
function replaceByMap(str, map) {
if (!Object.keys(map).length) return str;
const keys = Object.keys(map).sort((a, b) => b.length - a.length);
const escaped = keys.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const regex = new RegExp(escaped.join('|'), 'g');
return str.replace(regex, match => map[match] ?? match);
}
async function copyToClipboard(text) {
if (navigator.clipboard?.writeText) {
return navigator.clipboard.writeText(text);
}
const ta = Object.assign(document.createElement('textarea'), {
value: text,
style: 'position:fixed;opacity:0',
});
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
}
function showToast(message, duration = 2000) {
const toast = document.getElementById('toast');
toast.innerHTML = message;
toast.classList.add('visible');
clearTimeout(toast._timer);
toast._timer = setTimeout(() => toast.classList.remove('visible'), duration);
}
function updateCharCount() {
const len = textEl.value.length;
const word = len === 1 ? 'символ' : (len > 1 && len < 5 ? 'символа' : 'символов');
charCountEl.textContent = `${len} ${word}`;
}
async function process() {
const settings = getSettings();
const map = getActiveMap(settings);
let text = textEl.value;
if (!text.trim()) {
showToast('⚠️ поле пустое');
return;
}
if (!Object.keys(map).length) {
showToast('⚠️ карта замен в коде пуста');
return;
}
text = fixTypography(text);
text = replaceByMap(text, map);
textEl.value = text;
updateCharCount();
try {
await copyToClipboard(text);
showToast('<span class="icon">✅</span>готово и скопировано');
btn.classList.add('success');
setTimeout(() => btn.classList.remove('success'), 1200);
} catch {
showToast('<span class="icon">⚠️</span>готово, но копирование не удалось');
}
}
const textEl = document.getElementById('text');
const btn = document.getElementById('processBtn');
const revertBtn = document.getElementById('revertBtn');
revertBtn.addEventListener('click', revert);
const charCountEl = document.getElementById('charCount');
btn.addEventListener('click', process);
textEl.addEventListener('input', updateCharCount);
textEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
process();
}
});
updateCharCount();
})();
</script>
</body>
</html>