AnnLior/Text_Processing
0
1<!DOCTYPE html>2<html lang="ru">3<head>4<meta charset="UTF-8">5<meta name="viewport" content="width=device-width, initial-scale=1.0">6<title>Обработка стихов — курсив, выделение и адрес</title>7<style>8:root {9 --bg: #f4f5f7;10 --panel: #ffffff;11 --border: #d9dce1;12 --text: #202124;13 --muted: #6b7280;14 --input: #ffffff;15 --btn: #111827;16 --btn-text: #ffffff;17 --code: #f8f9fa;18}19.dark {20 --bg: #1a1b1e;21 --panel: #252629;22 --border: #3a3b3e;23 --text: #e4e4e7;24 --muted: #9ca3af;25 --input: #1a1b1e;26 --btn: #3b82f6;27 --btn-text: #ffffff;28 --code: #1f2023;29}30* { box-sizing: border-box; }31body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; background: var(--bg); color: var(--text); transition: background .2s, color .2s; }32.container { max-width: 1200px; margin: 0 auto; padding: 24px; }33.top { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; }34h1 { margin: 0 0 6px; font-size: 26px; }35.sub { color: var(--muted); margin: 0 0 16px; line-height: 1.5; }36textarea { 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); }37.controls { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; margin: 14px 0; }38label { font-size: 13px; display: flex; align-items: center; gap: 6px; cursor: pointer; }39button { border: 0; border-radius: 8px; padding: 10px 16px; font-size: 14px; cursor: pointer; font-weight: 600; }40.primary { background: var(--btn); color: var(--btn-text); }41.ghost { background: var(--panel); color: var(--text); border: 1px solid var(--border); }42.theme { background: var(--panel); color: var(--text); border: 1px solid var(--border); }43.row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 8px; }44@media (max-width: 800px) { .row { grid-template-columns: 1fr; } }45section { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px; }46section h2 { margin: 0 0 12px; font-size: 17px; }47.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); }48.preview p { margin: 0 0 12px; }49.preview p:last-child { margin-bottom: 0; }50.preview em { font-style: italic; }51.preview mark { border-radius: 3px; padding: 0 2px; }52.codehead { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; gap: 10px; flex-wrap: wrap; }53#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); }54.stats { font-size: 13px; color: var(--muted); }55</style>56</head>57<body>58<div class="container">59 <div class="top">60 <h1>Обработка стихов</h1>61 <button class="theme" id="theme" onclick="toggleTheme()">🌙 Тёмная</button>62 </div>63 <p class="sub">64 Вставьте текст...65 </p>66 67 <textarea id="input" placeholder="Вставьте текст… Например: 4 Не делай себе кумира и никакого изображения… 7 Не произноси имени Господа… (Исх. 20:1-7)."></textarea>68 69 <div class="controls">70 <label><input type="checkbox" id="handleBr" checked> Воспринимать <br> как перенос строки</label>71 <label><input type="checkbox" id="stripTags" checked> Убирать остальные HTML-теги</label>72 <button class="primary" id="run">Обработать</button>73 <button class="ghost" id="example">Пример</button>74 <button class="ghost" id="clear">Очистить</button>75 </div>76 77 <div class="row">78 <section>79 <h2>Предпросмотр</h2>80 <div class="preview" id="preview">Текст появится здесь.</div>81 </section>82 <section>83 <h2>HTML-код</h2>84 <div class="codehead">85 <span class="stats" id="stats"></span>86 <button class="ghost" id="copy">📋 Копировать</button>87 </div>88 <textarea id="code" readonly></textarea>89 </section>90 </div>91</div>92 93<script>94const $ = id => document.getElementById(id);95 96function toggleTheme() {97 document.body.classList.toggle("dark");98 const dark = document.body.classList.contains("dark");99 try { localStorage.setItem("theme", dark ? "dark" : "light"); } catch (e) {}100 $("theme").textContent = dark ? "☀️ Светлая" : "🌙 Тёмная";101}102window.toggleTheme = toggleTheme;103try {104 if (localStorage.getItem("theme") === "dark") {105 document.body.classList.add("dark");106 $("theme").textContent = "☀️ Светлая";107 }108} catch (e) {}109 110function escapeHtml(s) {111 return String(s)112 .replace(/&/g, "&")113 .replace(/</g, "<")114 .replace(/>/g, ">")115 .replace(/"/g, """);116}117 118function decodeEntities(s) {119 const ta = document.createElement("textarea");120 ta.innerHTML = s;121 return ta.value;122}123 124/* Цвета выделения по номеру тега <a id="N" name="N"> */125const HL_COLORS = {126 1: "#ccffcc", // салатовый127 2: "#cce5ff", // голубой128 3: "#fff59a", // жёлтый129 4: "#ffd1dc", // розовый130 5: "#ffc4a3" // коралловый131};132 133/* Замена плейсхолдеров на цветные <mark> */134function applyHighlights(s) {135 let out = s.replace(/@@HL(\d+)@@/g, (m, n) => {136 const c = HL_COLORS[n];137 return c ? `<mark style="background-color:${c}">` : "";138 });139 return out.split("@@/HL@@").join("</mark>");140}141 142/* Протаскивает открытое выделение через следующие строки,143 пока не встретится закрывающий плейсхолдер @@/HL@@. */144function propagateHighlights(lines) {145 const result = [];146 let active = null;147 for (let line of lines) {148 if (!line.trim()) {149 result.push(line);150 continue;151 }152 let processed = line;153 if (active !== null && !/^@@HL\d+@@/.test(processed)) {154 processed = "@@HL" + active + "@@" + processed;155 }156 let state = active;157 const re = /@@HL(\d+)@@|@@\/HL@@/g;158 let m;159 while ((m = re.exec(processed)) !== null) {160 state = (m[0] === "@@/HL@@") ? null : Number(m[1]);161 }162 result.push(processed);163 active = state;164 }165 return result;166}167 168/* Обработка одной строки */169function processLine(line) {170 if (!line) return "";171 172 // Сначала сохраняем все плейсхолдеры выделения173 let lead = "";174 let body = line;175 const leadMatch = line.match(/^((?:@@HL\d+@@|@@\/HL@@)+)/);176 if (leadMatch) {177 lead = leadMatch[1];178 body = line.slice(lead.length);179 }180 let tail = "";181 const tailMatch = body.match(/((?:@@\/HL@@|@@HL\d+@@)+)$/);182 if (tailMatch) {183 tail = tailMatch[1];184 body = body.slice(0, body.length - tail.length);185 }186 187 // Ищем строку, начинающуюся с цифры188 const m = body.match(/^(\d+[.)]?)\s+(.*)$/);189 let out;190 191 if (!m) {192 out = escapeHtml(lead + body + tail);193 } else {194 const number = m[1];195 let rest = m[2];196 197 // Ищем скобки в конце строки (включая точку после скобок)198 const parenMatch = rest.match(/^(.*?)\s*(\([^)]*\)\.?)\s*$/);199 200 if (parenMatch) {201 const beforeParen = parenMatch[1].trim();202 const paren = parenMatch[2];203 204 if (beforeParen) {205 // Текст до скобок — курсивом, скобки — обычным текстом206 out = `${lead}<em>${escapeHtml(number)} ${escapeHtml(beforeParen)}</em> ${escapeHtml(paren)}${tail}`;207 } else {208 // Только скобки — всё обычным текстом209 out = escapeHtml(lead + number + ' ' + rest + tail);210 }211 } else {212 // Нет скобок — всё курсивом213 out = `${lead}<em>${escapeHtml(number)} ${escapeHtml(rest)}</em>${tail}`;214 }215 }216 217 // Применяем подсветку218 return applyHighlights(out);219}220 221function process() {222 let raw = $("input").value;223 raw = decodeEntities(raw);224 225 if ($("handleBr").checked) raw = raw.replace(/<br\s*\/?>/gi, "\n");226 227 // Сохраняем теги выделения228 raw = raw.replace(/<a\s+[^>]*id="?(\d+)"?[^>]*>/gi, "@@HL$1@@");229 raw = raw.replace(/<\/a>/gi, "@@/HL@@");230 231 if ($("stripTags").checked) {232 raw = raw.replace(/<[^>]+>/g, "");233 }234 235 raw = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n");236 237 raw = propagateHighlights(raw.split("\n")).join("\n");238 239 const blocks = raw.split(/\n\s*\n+/).map(b => b.trim()).filter(Boolean);240 const paragraphs = blocks.map(block => {241 const lines = block.split("\n").map(l => l.trim()).filter(Boolean);242 return lines.map(processLine).join("<br />");243 });244 245 const htmlPreview = paragraphs.map(p => `<p>${p}</p>`).join("\n");246 const htmlCode = paragraphs.join("<br />");247 248 $("preview").innerHTML = htmlPreview;249 250 // Восстанавливаем теги <a> для HTML-кода251 let codeHtml = htmlCode;252 codeHtml = codeHtml.replace(/<mark style="background-color:#[^"]*">/g, (match) => {253 const colorMap = {254 '#ccffcc': '1',255 '#cce5ff': '2',256 '#fff59a': '3',257 '#ffd1dc': '4',258 '#ffc4a3': '5'259 };260 const color = match.match(/#[^"]*/)[0];261 const id = colorMap[color] || '1';262 return `<a id="${id}" name="${id}">`;263 });264 codeHtml = codeHtml.replace(/<\/mark>/g, '</a>');265 266 $("code").value = codeHtml;267 268 const verseCount = (htmlPreview.match(/<em>/g) || []).length;269 $("stats").textContent = `Стихов: ${verseCount} · Абзацев: ${paragraphs.length}`;270}271window.process = process;272 273$("run").addEventListener("click", process);274$("input").addEventListener("keydown", e => {275 if ((e.ctrlKey || e.metaKey) && e.key === "Enter") process();276});277 278$("clear").addEventListener("click", () => {279 $("input").value = "";280 $("preview").textContent = "Текст появится здесь.";281 $("code").value = "";282 $("stats").textContent = "";283});284 285$("example").addEventListener("click", () => {286 $("input").value = ""287 process();288});289 290$("copy").addEventListener("click", async () => {291 const code = $("code").value;292 if (!code) return;293 try {294 await navigator.clipboard.writeText(code);295 $("copy").textContent = "✅ Скопировано";296 setTimeout(() => $("copy").textContent = "📋 Копировать", 1500);297 } catch (e) {298 $("code").select();299 document.execCommand("copy");300 }301});302</script>303</body>304</html>