Spaces:
Running
Running
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Обработка стихов — курсив, выделение и адрес</title> | |
| <style> | |
| :root { | |
| --bg: #f4f5f7; | |
| --panel: #ffffff; | |
| --border: #d9dce1; | |
| --text: #202124; | |
| --muted: #6b7280; | |
| --input: #ffffff; | |
| --btn: #111827; | |
| --btn-text: #ffffff; | |
| --code: #f8f9fa; | |
| } | |
| .dark { | |
| --bg: #1a1b1e; | |
| --panel: #252629; | |
| --border: #3a3b3e; | |
| --text: #e4e4e7; | |
| --muted: #9ca3af; | |
| --input: #1a1b1e; | |
| --btn: #3b82f6; | |
| --btn-text: #ffffff; | |
| --code: #1f2023; | |
| } | |
| * { box-sizing: border-box; } | |
| body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; background: var(--bg); color: var(--text); transition: background .2s, color .2s; } | |
| .container { max-width: 1200px; margin: 0 auto; padding: 24px; } | |
| .top { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; } | |
| h1 { margin: 0 0 6px; font-size: 26px; } | |
| .sub { color: var(--muted); margin: 0 0 16px; line-height: 1.5; } | |
| textarea { width: 100%; min-height: 300px; resize: vertical; border: 1px solid var(--border); border-radius: 10px; padding: 12px; font: 14px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: var(--input); color: var(--text); } | |
| .controls { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; margin: 14px 0; } | |
| label { font-size: 13px; display: flex; align-items: center; gap: 6px; cursor: pointer; } | |
| button { border: 0; border-radius: 8px; padding: 10px 16px; font-size: 14px; cursor: pointer; font-weight: 600; } | |
| .primary { background: var(--btn); color: var(--btn-text); } | |
| .ghost { background: var(--panel); color: var(--text); border: 1px solid var(--border); } | |
| .theme { background: var(--panel); color: var(--text); border: 1px solid var(--border); } | |
| .row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 8px; } | |
| @media (max-width: 800px) { .row { grid-template-columns: 1fr; } } | |
| section { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px; } | |
| section h2 { margin: 0 0 12px; font-size: 17px; } | |
| .preview { min-height: 260px; max-height: 560px; overflow: auto; border: 1px solid var(--border); border-radius: 10px; padding: 14px; line-height: 1.75; font-size: 15px; background: var(--input); color: var(--text); } | |
| .preview p { margin: 0 0 12px; } | |
| .preview p:last-child { margin-bottom: 0; } | |
| .preview em { font-style: italic; } | |
| .preview mark { border-radius: 3px; padding: 0 2px; } | |
| .codehead { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; gap: 10px; flex-wrap: wrap; } | |
| #code { width: 100%; min-height: 260px; max-height: 560px; resize: vertical; font: 12.5px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: var(--code); border: 1px solid var(--border); border-radius: 10px; padding: 12px; color: var(--text); } | |
| .stats { font-size: 13px; color: var(--muted); } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="top"> | |
| <h1>Обработка стихов</h1> | |
| <button class="theme" id="theme" onclick="toggleTheme()">🌙 Тёмная</button> | |
| </div> | |
| <p class="sub"> | |
| Вставьте текст... | |
| </p> | |
| <textarea id="input" placeholder="Вставьте текст… Например: 4 Не делай себе кумира и никакого изображения… 7 Не произноси имени Господа… (Исх. 20:1-7)."></textarea> | |
| <div class="controls"> | |
| <label><input type="checkbox" id="handleBr" checked> Воспринимать <br> как перенос строки</label> | |
| <label><input type="checkbox" id="stripTags" checked> Убирать остальные HTML-теги</label> | |
| <button class="primary" id="run">Обработать</button> | |
| <button class="ghost" id="example">Пример</button> | |
| <button class="ghost" id="clear">Очистить</button> | |
| </div> | |
| <div class="row"> | |
| <section> | |
| <h2>Предпросмотр</h2> | |
| <div class="preview" id="preview">Текст появится здесь.</div> | |
| </section> | |
| <section> | |
| <h2>HTML-код</h2> | |
| <div class="codehead"> | |
| <span class="stats" id="stats"></span> | |
| <button class="ghost" id="copy">📋 Копировать</button> | |
| </div> | |
| <textarea id="code" readonly></textarea> | |
| </section> | |
| </div> | |
| </div> | |
| <script> | |
| const $ = id => document.getElementById(id); | |
| function toggleTheme() { | |
| document.body.classList.toggle("dark"); | |
| const dark = document.body.classList.contains("dark"); | |
| try { localStorage.setItem("theme", dark ? "dark" : "light"); } catch (e) {} | |
| $("theme").textContent = dark ? "☀️ Светлая" : "🌙 Тёмная"; | |
| } | |
| window.toggleTheme = toggleTheme; | |
| try { | |
| if (localStorage.getItem("theme") === "dark") { | |
| document.body.classList.add("dark"); | |
| $("theme").textContent = "☀️ Светлая"; | |
| } | |
| } catch (e) {} | |
| function escapeHtml(s) { | |
| return String(s) | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """); | |
| } | |
| function decodeEntities(s) { | |
| const ta = document.createElement("textarea"); | |
| ta.innerHTML = s; | |
| return ta.value; | |
| } | |
| /* Цвета выделения по номеру тега <a id="N" name="N"> */ | |
| const HL_COLORS = { | |
| 1: "#ccffcc", // салатовый | |
| 2: "#cce5ff", // голубой | |
| 3: "#fff59a", // жёлтый | |
| 4: "#ffd1dc", // розовый | |
| 5: "#ffc4a3" // коралловый | |
| }; | |
| /* Замена плейсхолдеров на цветные <mark> */ | |
| function applyHighlights(s) { | |
| let out = s.replace(/@@HL(\d+)@@/g, (m, n) => { | |
| const c = HL_COLORS[n]; | |
| return c ? `<mark style="background-color:${c}">` : ""; | |
| }); | |
| return out.split("@@/HL@@").join("</mark>"); | |
| } | |
| /* Протаскивает открытое выделение через следующие строки, | |
| пока не встретится закрывающий плейсхолдер @@/HL@@. */ | |
| function propagateHighlights(lines) { | |
| const result = []; | |
| let active = null; | |
| for (let line of lines) { | |
| if (!line.trim()) { | |
| result.push(line); | |
| continue; | |
| } | |
| let processed = line; | |
| if (active !== null && !/^@@HL\d+@@/.test(processed)) { | |
| processed = "@@HL" + active + "@@" + processed; | |
| } | |
| let state = active; | |
| const re = /@@HL(\d+)@@|@@\/HL@@/g; | |
| let m; | |
| while ((m = re.exec(processed)) !== null) { | |
| state = (m[0] === "@@/HL@@") ? null : Number(m[1]); | |
| } | |
| result.push(processed); | |
| active = state; | |
| } | |
| return result; | |
| } | |
| /* Обработка одной строки */ | |
| function processLine(line) { | |
| if (!line) return ""; | |
| // Сначала сохраняем все плейсхолдеры выделения | |
| let lead = ""; | |
| let body = line; | |
| const leadMatch = line.match(/^((?:@@HL\d+@@|@@\/HL@@)+)/); | |
| if (leadMatch) { | |
| lead = leadMatch[1]; | |
| body = line.slice(lead.length); | |
| } | |
| let tail = ""; | |
| const tailMatch = body.match(/((?:@@\/HL@@|@@HL\d+@@)+)$/); | |
| if (tailMatch) { | |
| tail = tailMatch[1]; | |
| body = body.slice(0, body.length - tail.length); | |
| } | |
| // Ищем строку, начинающуюся с цифры | |
| const m = body.match(/^(\d+[.)]?)\s+(.*)$/); | |
| let out; | |
| if (!m) { | |
| out = escapeHtml(lead + body + tail); | |
| } else { | |
| const number = m[1]; | |
| let rest = m[2]; | |
| // Ищем скобки в конце строки (включая точку после скобок) | |
| const parenMatch = rest.match(/^(.*?)\s*(\([^)]*\)\.?)\s*$/); | |
| if (parenMatch) { | |
| const beforeParen = parenMatch[1].trim(); | |
| const paren = parenMatch[2]; | |
| if (beforeParen) { | |
| // Текст до скобок — курсивом, скобки — обычным текстом | |
| out = `${lead}<em>${escapeHtml(number)} ${escapeHtml(beforeParen)}</em> ${escapeHtml(paren)}${tail}`; | |
| } else { | |
| // Только скобки — всё обычным текстом | |
| out = escapeHtml(lead + number + ' ' + rest + tail); | |
| } | |
| } else { | |
| // Нет скобок — всё курсивом | |
| out = `${lead}<em>${escapeHtml(number)} ${escapeHtml(rest)}</em>${tail}`; | |
| } | |
| } | |
| // Применяем подсветку | |
| return applyHighlights(out); | |
| } | |
| function process() { | |
| let raw = $("input").value; | |
| raw = decodeEntities(raw); | |
| if ($("handleBr").checked) raw = raw.replace(/<br\s*\/?>/gi, "\n"); | |
| // Сохраняем теги выделения | |
| raw = raw.replace(/<a\s+[^>]*id="?(\d+)"?[^>]*>/gi, "@@HL$1@@"); | |
| raw = raw.replace(/<\/a>/gi, "@@/HL@@"); | |
| if ($("stripTags").checked) { | |
| raw = raw.replace(/<[^>]+>/g, ""); | |
| } | |
| raw = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); | |
| raw = propagateHighlights(raw.split("\n")).join("\n"); | |
| const blocks = raw.split(/\n\s*\n+/).map(b => b.trim()).filter(Boolean); | |
| const paragraphs = blocks.map(block => { | |
| const lines = block.split("\n").map(l => l.trim()).filter(Boolean); | |
| return lines.map(processLine).join("<br />"); | |
| }); | |
| const htmlPreview = paragraphs.map(p => `<p>${p}</p>`).join("\n"); | |
| const htmlCode = paragraphs.join("<br />"); | |
| $("preview").innerHTML = htmlPreview; | |
| // Восстанавливаем теги <a> для HTML-кода | |
| let codeHtml = htmlCode; | |
| codeHtml = codeHtml.replace(/<mark style="background-color:#[^"]*">/g, (match) => { | |
| const colorMap = { | |
| '#ccffcc': '1', | |
| '#cce5ff': '2', | |
| '#fff59a': '3', | |
| '#ffd1dc': '4', | |
| '#ffc4a3': '5' | |
| }; | |
| const color = match.match(/#[^"]*/)[0]; | |
| const id = colorMap[color] || '1'; | |
| return `<a id="${id}" name="${id}">`; | |
| }); | |
| codeHtml = codeHtml.replace(/<\/mark>/g, '</a>'); | |
| $("code").value = codeHtml; | |
| const verseCount = (htmlPreview.match(/<em>/g) || []).length; | |
| $("stats").textContent = `Стихов: ${verseCount} · Абзацев: ${paragraphs.length}`; | |
| } | |
| window.process = process; | |
| $("run").addEventListener("click", process); | |
| $("input").addEventListener("keydown", e => { | |
| if ((e.ctrlKey || e.metaKey) && e.key === "Enter") process(); | |
| }); | |
| $("clear").addEventListener("click", () => { | |
| $("input").value = ""; | |
| $("preview").textContent = "Текст появится здесь."; | |
| $("code").value = ""; | |
| $("stats").textContent = ""; | |
| }); | |
| $("example").addEventListener("click", () => { | |
| $("input").value = "" | |
| process(); | |
| }); | |
| $("copy").addEventListener("click", async () => { | |
| const code = $("code").value; | |
| if (!code) return; | |
| try { | |
| await navigator.clipboard.writeText(code); | |
| $("copy").textContent = "✅ Скопировано"; | |
| setTimeout(() => $("copy").textContent = "📋 Копировать", 1500); | |
| } catch (e) { | |
| $("code").select(); | |
| document.execCommand("copy"); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |