Download index.html from 2ch/bukvoeb: direct link, hf CLI and curl.
- Browser
- Download file 15.3 kB
-
https://huggingface.co/spaces/2ch/bukvoeb/resolve/main/index.html
- Command line
-
hf download hf://spaces/2ch/bukvoeb/index.html
-
curl -L -o index.html https://huggingface.co/spaces/2ch/bukvoeb/resolve/main/index.html
15.3 kB
| <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> |