Text_Processing / index.html
AnnLior's picture
Update index.html
3a2aaad verified
Raw
History Blame Contribute Delete
11 kB
<!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: #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="Вставьте текст… Например:&#10;4 Не делай себе кумира и никакого изображения…&#10;7 Не произноси имени Господа… (Исх. 20:1-7)."></textarea>
<div class="controls">
<label><input type="checkbox" id="handleBr" checked> Воспринимать &lt;br&gt; как перенос строки</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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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>