Text_Processing / index.html
AnnLior's picture
Update index.html
87ad720 verified
Raw History Blame Contribute Delete
26.5 kB
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Проверка русского текста (LanguageTool)</title>
<style>
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
font-family: -apple-system, Segoe UI, Roboto, sans-serif;
margin: 0; padding: 12px; background: #f4f6f8;
height: 100vh; height: 100dvh;
display: flex; flex-direction: column; overflow: hidden;
}
h1 { font-size: 18px; margin: 0 0 8px; flex: none; }
.toolbar { margin-bottom: 10px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; flex: none; }
button { padding: 8px 12px; font-size: 14px; cursor: pointer; border: none; border-radius: 6px; background: #2563eb; color: #fff; }
button:disabled { background: #94a3b8; cursor: not-allowed; }
button.pause { background: #f59e0b; }
button.reset { background: #64748b; }
/* ===== Три колонки на всю высоту экрана ===== */
.cols {
flex: 1; min-height: 0;
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-rows: minmax(0, 1fr);
gap: 10px;
}
@media (max-width: 900px) {
.cols { grid-template-columns: minmax(0, 1fr); grid-template-rows: repeat(3, minmax(0, 1fr)); }
}
.col {
background: #fff; border-radius: 10px; padding: 10px;
display: flex; flex-direction: column; min-height: 0; min-width: 0;
box-shadow: 0 1px 3px rgba(0,0,0,.08);
}
.col h2 { flex: none; font-size: 13px; margin: 0 0 8px; color: #334155; text-transform: uppercase; letter-spacing: .5px; }
/* ===== Единый стиль текста для 1-й и 3-й колонок ===== */
.txt {
font-family: Georgia, "Times New Roman", serif;
font-size: 16px; line-height: 1.6; color: #0f172a;
font-weight: 400; font-style: normal;
white-space: pre-wrap; word-wrap: break-word; overflow-wrap: anywhere;
}
.txt b, .txt strong { font-weight: 700; }
.txt i, .txt em { font-style: italic; }
.pane {
flex: 1; min-height: 0; overflow-y: auto;
padding: 10px 12px; border: 1px solid #cbd5e1; border-radius: 8px; background: #fafafa;
}
#input { outline: none; background: #fff; }
#input:focus { border-color: #2563eb; box-shadow: 0 0 0 2px rgba(37,99,235,.15); }
#input:empty::before { content: attr(data-placeholder); color: #94a3b8; pointer-events: none; }
#input div { min-height: 1.6em; }
/* Результаты проверки (2-я колонка) */
#results { font-size: 14px; line-height: 1.5; font-family: -apple-system, Segoe UI, Roboto, sans-serif; white-space: normal; }
.chunk-buttons { flex: none; margin-top: 10px; display: flex; gap: 6px; flex-wrap: wrap; max-height: 84px; overflow-y: auto; }
.chunk-buttons button { background: #0ea5e9; font-size: 13px; padding: 5px 9px; }
.chunk-buttons button.done { background: #16a34a; }
.chunk-buttons button.err { background: #dc2626; }
.chunk-info { flex: none; font-size: 12px; color: #64748b; margin-top: 6px; }
.status { font-size: 13px; color: #475569; margin-left: 8px; }
.err { background: #fee2e2; border-bottom: 2px solid #ef4444; cursor: help; }
.fix { background: #dcfce7; color: #166534; font-weight: 600; font-family: -apple-system, Segoe UI, Roboto, sans-serif; font-size: 13px; }
.block { margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px dashed #e2e8f0; }
.block:last-child { border-bottom: none; }
.block-title { font-family: -apple-system, Segoe UI, Roboto, sans-serif; font-size: 11px; color: #94a3b8; margin-bottom: 4px; }
.issue { background: #fff; border-left: 4px solid #ef4444; padding: 6px 8px; margin-bottom: 6px; border-radius: 4px; font-size: 13px; }
.issue.ok { border-color: #16a34a; }
.issue[data-m] { cursor: pointer; transition: background .15s; }
.issue[data-m]:hover { background: #f1f5f9; }
.issue.active { background: #fef9c3; border-left-color: #f59e0b; }
@keyframes flash { 0%,100% { box-shadow: 0 0 0 0 rgba(245,158,11,0); } 30% { box-shadow: 0 0 0 4px rgba(245,158,11,.7); background: #fde68a; } }
.err.flash { animation: flash 1.6s ease-out; }
.issue .msg { font-weight: 600; }
.issue .sugg { color: #16a34a; }
.issue .ctx { color: #64748b; font-size: 12px; margin-top: 3px; }
.issue pre { white-space: pre-wrap; word-wrap: break-word; font-size: 12px; margin: 4px 0 0; color: #7f1d1d; }
</style>
</head>
<body>
<h1>Проверка русского текста — LanguageTool</h1>
<div class="toolbar">
<button id="btnSplit">Разбить на куски ≤ 20000 знаков</button>
<button id="btnRunAll" disabled>Проверить все куски</button>
<button id="btnPause" class="pause" disabled>Пауза</button>
<button id="btnReset" class="reset">Сброс</button>
<span class="status" id="status"></span>
</div>
<div class="cols">
<div class="col">
<h2>1. Ваш текст</h2>
<div id="input" class="pane txt" contenteditable="true" spellcheck="false"
data-placeholder="Вставьте сюда большой русский текст…"></div>
<div class="chunk-info" id="chunkInfo"></div>
<div class="chunk-buttons" id="chunkButtons"></div>
</div>
<div class="col">
<h2>2. Результаты проверки</h2>
<div class="pane" id="results">Здесь появятся ошибки по каждому куску…</div>
</div>
<div class="col">
<h2>3. Текст с подсветкой</h2>
<div class="pane txt" id="highlighted">Здесь будет сводный текст: красным — проблемные места, зелёным — предложенные исправления.</div>
</div>
</div>
<script>
const API = 'https://api.languagetool.org/v2/check';
const LIMIT = 20000;
/* ================== СТОП-ЛИСТ СЛОВ ================== */
const STOP_WORDS = [
'Иезавель',
'Иезавели',
'Иезавелью',
'Уззия',
'Уззии',
'Ругияш',
'Осия',
'Потифар',
// добавьте свои — каждое с новой строки в кавычках и с запятой
];
const STOP_SET = new Set(STOP_WORDS.map(w => w.toLowerCase().trim()));
/* ================== ИГНОР-ЛИСТ ПРАВИЛ ================== */
const IGNORED_RULES = new Set([
'Many_PNN', // Загруженность предложения местоимениями
]);
/* ================== ФИЛЬТРЫ ================== */
function isInsideStopWord(text, offset, length) {
if (STOP_SET.size === 0) return false;
const target = text.substring(offset, offset + length).toLowerCase().trim();
if (STOP_SET.has(target)) return true;
const isLetter = c => /[а-яёa-z]/i.test(c);
const before = text[offset - 1] || ' ';
const after = text[offset + length] || ' ';
if (isLetter(before) || isLetter(after)) {
let s = offset, e = offset + length;
while (s > 0 && isLetter(text[s - 1])) s--;
while (e < text.length && isLetter(text[e])) e++;
const wholeWord = text.substring(s, e).toLowerCase().trim();
if (STOP_SET.has(wholeWord)) return true;
}
return false;
}
function isSillySuggestion(orig, sugg) {
if (!sugg) return false;
const o = orig.toLowerCase(), s = sugg.toLowerCase();
if (o === s) return false;
if (s.length > o.length * 2 || o.length > s.length * 2) return true;
const common = [...new Set(o)].filter(c => s.includes(c)).length;
const minLen = Math.min(o.length, s.length);
if (minLen > 3 && common < minLen / 2) return true;
return false;
}
function filterMatches(matches, text) {
return matches
.filter(m => !IGNORED_RULES.has(m.rule?.id))
.filter(m => !isInsideStopWord(text, m.offset, m.length))
.map(m => {
const orig = text.substring(m.offset, m.offset + m.length);
const good = m.replacements.filter(r => !isSillySuggestion(orig, r.value));
return { ...m, replacements: good };
})
.filter(m => {
if (m.replacements.length > 0) return true;
if (m.rule?.issueType === 'misspelling') return true;
return false;
});
}
/* ==================================================== */
const input = document.getElementById('input');
const results = document.getElementById('results');
const highlighted = document.getElementById('highlighted');
const chunkButtons = document.getElementById('chunkButtons');
const chunkInfo = document.getElementById('chunkInfo');
const status = document.getElementById('status');
let chunks = [];
let running = false;
let paused = false;
// Снимок текста и форматирования на момент разбивки
let fullText = '';
let fullFmt = []; // для каждого символа: бит 1 = жирный, бит 2 = курсив
/* ================== ВСТАВКА: чистим всё, кроме жирного и курсива ================== */
function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
const BLOCK_TAGS = new Set(['P','DIV','LI','UL','OL','TR','BLOCKQUOTE','TABLE','SECTION','ARTICLE','PRE','HR','H1','H2','H3','H4','H5','H6']);
const SKIP_TAGS = new Set(['SCRIPT','STYLE','HEAD','META','TITLE','LINK']);
function sanitizeHtml(html) {
const doc = new DOMParser().parseFromString(html, 'text/html');
const runs = [];
const atLineStart = () => runs.length === 0 || runs[runs.length - 1].t === '\n';
const endLine = () => { if (!atLineStart()) runs.push({ t: '\n' }); };
function walk(n, b, i) {
if (n.nodeType === 3) {
let s = n.nodeValue.replace(/[\s\u00a0]+/g, ' ');
if (atLineStart()) s = s.replace(/^ /, '');
if (!s) return;
runs.push({ t: s, b, i });
return;
}
if (n.nodeType !== 1) return;
const tag = n.tagName;
if (SKIP_TAGS.has(tag)) return;
if (tag === 'BR') { runs.push({ t: '\n' }); return; }
let nb = b, ni = i;
if (tag === 'B' || tag === 'STRONG' || /^H[1-6]$/.test(tag)) nb = true;
if (tag === 'I' || tag === 'EM' || tag === 'CITE') ni = true;
const st = n.style || {};
// inline-стиль главнее тега (Google Docs оборачивает всё в <b style="font-weight:normal">)
if (st.fontWeight) nb = (st.fontWeight === 'bold' || st.fontWeight === 'bolder' || parseInt(st.fontWeight) >= 600);
if (st.fontStyle) ni = (st.fontStyle === 'italic' || st.fontStyle === 'oblique');
const blk = BLOCK_TAGS.has(tag);
if (blk) endLine();
n.childNodes.forEach(c => walk(c, nb, ni));
if (blk) endLine();
}
walk(doc.body, false, false);
// собираем строки
const lines = [[]];
for (const r of runs) {
if (r.t === '\n') lines.push([]); else lines[lines.length - 1].push(r);
}
while (lines.length && lines[lines.length - 1].length === 0) lines.pop();
return lines.map(line => {
if (!line.length) return '<div><br></div>';
line[line.length - 1] = { ...line[line.length - 1], t: line[line.length - 1].t.replace(/ $/, '') };
return '<div>' + line.map(r => {
let s = esc(r.t);
if (r.b) s = '<b>' + s + '</b>';
if (r.i) s = '<i>' + s + '</i>';
return s;
}).join('') + '</div>';
}).join('');
}
function plainToHtml(text) {
return text.replace(/\r/g, '').split('\n')
.map(l => l ? '<div>' + esc(l) + '</div>' : '<div><br></div>').join('');
}
// Вставка HTML в позицию курсора без execCommand (он в ряде браузеров молча ничего не делает)
function insertHtmlAtCursor(html) {
input.focus();
const sel = window.getSelection();
let range;
if (sel.rangeCount && input.contains(sel.getRangeAt(0).commonAncestorContainer)) {
range = sel.getRangeAt(0);
} else {
range = document.createRange();
range.selectNodeContents(input);
range.collapse(false);
}
range.deleteContents();
const tpl = document.createElement('template');
tpl.innerHTML = html;
const frag = tpl.content;
const last = frag.lastChild;
range.insertNode(frag);
if (last) {
const r = document.createRange();
r.setStartAfter(last);
r.collapse(true);
sel.removeAllRanges();
sel.addRange(r);
}
}
function insertFromData(dt) {
const html = dt.getData('text/html');
const text = dt.getData('text/plain');
let clean = '';
if (html) {
try { clean = sanitizeHtml(html); }
catch (err) { console.error('Ошибка очистки HTML, вставляю как простой текст', err); }
}
if (!clean) clean = plainToHtml(text);
insertHtmlAtCursor(clean);
}
input.addEventListener('paste', e => {
e.preventDefault();
try {
insertFromData(e.clipboardData);
} catch (err) {
console.error('Ошибка вставки', err);
try { insertHtmlAtCursor(plainToHtml(e.clipboardData.getData('text/plain'))); } catch (_) {}
}
});
input.addEventListener('drop', e => {
e.preventDefault();
try { insertFromData(e.dataTransfer); } catch (err) { console.error(err); }
});
/* ================== Извлечение текста + форматирования из редактора ================== */
function extractFromEditor(root) {
let text = '';
const fmt = [];
const styleCache = new Map();
function flagsOf(el) {
if (styleCache.has(el)) return styleCache.get(el);
const cs = getComputedStyle(el);
const f = (parseInt(cs.fontWeight) >= 600 ? 1 : 0) | ((cs.fontStyle === 'italic' || cs.fontStyle === 'oblique') ? 2 : 0);
styleCache.set(el, f);
return f;
}
function push(s, f) {
text += s;
for (let k = 0; k < s.length; k++) fmt.push(f);
}
const endsNL = () => text.length === 0 || text[text.length - 1] === '\n';
function walk(n) {
if (n.nodeType === 3) {
const s = n.nodeValue.replace(/\u00a0/g, ' ');
if (s) push(s, flagsOf(n.parentElement));
return;
}
if (n.nodeType !== 1) return;
if (n.tagName === 'BR') { push('\n', 0); return; }
const blk = BLOCK_TAGS.has(n.tagName);
if (blk && !endsNL()) push('\n', 0);
n.childNodes.forEach(walk);
if (blk && !endsNL()) push('\n', 0);
}
root.childNodes.forEach(walk);
// убираем хвостовые переводы строк
while (text.endsWith('\n')) { text = text.slice(0, -1); fmt.pop(); }
return { text, fmt };
}
/* Текст [from,to) из снимка, с жирным/курсивом, в HTML */
function fmtSpan(from, to) {
let out = '';
let i = from;
while (i < to) {
const f = fullFmt[i];
let j = i + 1;
while (j < to && fullFmt[j] === f) j++;
let s = esc(fullText.substring(i, j));
if (f & 1) s = '<b>' + s + '</b>';
if (f & 2) s = '<i>' + s + '</i>';
out += s;
i = j;
}
return out;
}
/* ================== СИНХРОННАЯ ПРОКРУТКА ================== */
const scrollables = [input, results, highlighted];
let syncing = false;
function syncScroll(source) {
if (syncing) return;
syncing = true;
const srcMax = source.scrollHeight - source.clientHeight;
const ratio = srcMax > 0 ? source.scrollTop / srcMax : 0;
scrollables.forEach(el => {
if (el === source) return;
const max = el.scrollHeight - el.clientHeight;
if (max > 0) el.scrollTop = ratio * max;
});
requestAnimationFrame(() => { syncing = false; });
}
scrollables.forEach(el => {
el.addEventListener('scroll', () => syncScroll(el), { passive: true });
});
/* ---------- Разбивка на куски ≤ LIMIT по абзацам ---------- */
function splitText(text, limit) {
const parts = [];
const paragraphs = text.split(/(\n)/);
let buf = '';
let cursor = 0;
let bufStart = 0;
function flush() {
if (buf.length > 0) {
parts.push({ text: buf, start: bufStart, end: bufStart + buf.length, done: false, matches: [], error: null });
buf = '';
}
}
for (const p of paragraphs) {
if (p.length > limit) {
flush();
const pieces = hardSplit(p, limit);
for (const piece of pieces) {
parts.push({ text: piece, start: cursor, end: cursor + piece.length, done: false, matches: [], error: null });
cursor += piece.length;
}
bufStart = cursor;
continue;
}
if ((buf + p).length > limit) {
flush();
bufStart = cursor;
}
if (buf.length === 0) bufStart = cursor;
buf += p;
cursor += p.length;
}
flush();
return parts;
}
function hardSplit(s, limit) {
const out = [];
let i = 0;
while (i < s.length) {
let end = Math.min(i + limit, s.length);
if (end < s.length) {
const slice = s.substring(i, end);
const lastDot = Math.max(slice.lastIndexOf('. '), slice.lastIndexOf('! '), slice.lastIndexOf('? '), slice.lastIndexOf('\n'));
if (lastDot > limit * 0.5) end = i + lastDot + 1;
}
out.push(s.substring(i, end));
i = end;
}
return out;
}
document.getElementById('btnSplit').onclick = () => {
const ex = extractFromEditor(input);
if (!ex.text.trim()) { alert('Введите текст'); return; }
fullText = ex.text;
fullFmt = ex.fmt;
chunks = splitText(fullText, LIMIT);
chunks.forEach(ch => { ch.text = fullText.substring(ch.start, ch.end); }); // гарантия совпадения смещений
chunkInfo.textContent = `Всего знаков: ${fullText.length}. Кусков: ${chunks.length} (по ≤ ${LIMIT}).`;
chunkButtons.innerHTML = '';
chunks.forEach((ch, i) => {
const b = document.createElement('button');
b.textContent = `Кусок ${i+1} (${ch.text.length} зн.)`;
b.dataset.idx = i;
b.onclick = () => runOne(i);
chunkButtons.appendChild(b);
});
document.getElementById('btnRunAll').disabled = false;
results.textContent = '';
renderHighlighted();
status.textContent = '';
};
/* ---------- Запрос к LanguageTool ---------- */
async function checkChunk(text) {
const body = new URLSearchParams({ text: text, language: 'ru-RU' });
let res;
try {
res = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
});
} catch (netErr) {
throw new Error('Сеть/CORS: ' + netErr.message +
' — попробуйте открыть страницу через http://localhost, а не file://');
}
if (!res.ok) {
let detail = '';
try { detail = (await res.text()).substring(0, 300); } catch(_) {}
let hint = '';
if (res.status === 429) hint = ' — превышен лимит 20 запросов/мин, подождите минуту и повторите';
if (res.status === 413) hint = ' — кусок слишком большой, уменьшите LIMIT в коде';
throw new Error('HTTP ' + res.status + hint + (detail ? ' | ' + detail : ''));
}
const data = await res.json();
return filterMatches(data.matches || [], text);
}
/* ---------- Проверка одного куска ---------- */
async function runOne(idx) {
const ch = chunks[idx];
if (!ch || ch.done) return false;
status.textContent = `Проверяем кусок ${idx+1}…`;
try {
ch.matches = await checkChunk(ch.text);
ch.done = true;
ch.error = null;
markButton(idx, 'done');
renderChunkResult(idx);
renderHighlighted();
status.textContent = `Кусок ${idx+1} готов (ошибок: ${ch.matches.length}).`;
return true;
} catch (e) {
console.error('Ошибка на куске', idx, e);
ch.error = e.message;
ch.done = false;
markButton(idx, 'err');
renderChunkError(idx, e.message);
status.textContent = `Ошибка на куске ${idx+1}: ${e.message}`;
return false;
}
}
function markButton(idx, cls) {
const b = chunkButtons.querySelector(`button[data-idx="${idx}"]`);
if (!b) return;
b.classList.remove('done', 'err');
if (cls) b.classList.add(cls);
}
function renderChunkResult(idx) {
const ch = chunks[idx];
const div = document.createElement('div');
div.className = 'block';
let html = `<div class="block-title">Кусок ${idx+1} (знаки ${ch.start}–${ch.end})</div>`;
if (ch.matches.length === 0) {
html += `<div class="issue ok">✅ Ошибок не найдено</div>`;
} else {
ch.matches.forEach((m, i) => {
const start = Math.max(0, m.offset - 25);
const end = Math.min(ch.text.length, m.offset + m.length + 25);
const ctx = ch.text.substring(start, end).replace(/\n/g, ' ');
const sugg = m.replacements.slice(0, 5).map(r => r.value).join(', ');
html += `<div class="issue" data-chunk="${idx}" data-m="${i}" title="Показать в тексте">
<div class="msg">${i+1}. ${escapeHtml(m.message)}</div>
<div class="ctx">…${escapeHtml(ctx)}…</div>
${sugg ? `<div class="sugg">→ ${escapeHtml(sugg)}</div>` : ''}
</div>`;
});
}
div.innerHTML = html;
results.appendChild(div);
results.scrollTop = results.scrollHeight;
}
function renderChunkError(idx, msg) {
const div = document.createElement('div');
div.className = 'block';
div.innerHTML = `<div class="block-title">Кусок ${idx+1}</div>
<div class="issue">❌ ${escapeHtml(msg)}<pre>Проверьте вкладку Console (F12) для деталей</pre></div>`;
results.appendChild(div);
results.scrollTop = results.scrollHeight;
}
/* ---------- Подсветка в 3-й колонке (с сохранением жирного/курсива) ---------- */
function renderHighlighted() {
const keepScroll = highlighted.scrollTop;
let html = '';
chunks.forEach((ch, idx) => {
html += `<div class="block"><div class="block-title">Кусок ${idx+1}</div>`;
if (ch.error) {
html += `<div class="issue">❌ ${escapeHtml(ch.error)}</div>` + fmtSpan(ch.start, ch.end);
} else if (!ch.done) {
html += fmtSpan(ch.start, ch.end);
} else {
html += highlightChunk(ch, idx);
}
html += `</div>`;
});
highlighted.innerHTML = html;
highlighted.scrollTop = keepScroll;
}
function highlightChunk(ch, ci) {
const text = ch.text;
const g = ch.start;
const matches = ch.matches.map((m, i) => ({ m, i })).sort((a, b) => a.m.offset - b.m.offset);
let out = '';
let pos = 0;
for (const { m, i: mi } of matches) {
if (m.offset < pos) continue;
out += fmtSpan(g + pos, g + m.offset);
const orig = text.substring(m.offset, m.offset + m.length);
const sugg = m.replacements.length ? m.replacements[0].value : '';
out += `<span class="err" data-mid="${ci}-${mi}" title="${escapeAttr(m.message)}${sugg ? ' → ' + escapeAttr(sugg) : ''}">${fmtSpan(g + m.offset, g + m.offset + m.length)}</span>`;
if (sugg && sugg !== orig) {
out += ` <span class="fix">[→ ${escapeHtml(sugg)}]</span> `;
}
pos = m.offset + m.length;
}
out += fmtSpan(g + pos, g + text.length);
return out;
}
/* ---------- Клик по ошибке во 2-й колонке → переход к месту в тексте ---------- */
results.addEventListener('click', e => {
const issue = e.target.closest('.issue[data-m]');
if (!issue) return;
jumpToMatch(+issue.dataset.chunk, +issue.dataset.m, issue);
});
function jumpToMatch(ci, mi, issueEl) {
const ch = chunks[ci];
const m = ch && ch.matches[mi];
if (!m) return;
results.querySelectorAll('.issue.active').forEach(x => x.classList.remove('active'));
if (issueEl) issueEl.classList.add('active');
syncing = true; // не даём синхронной прокрутке сдвинуть остальные колонки
// 3-я колонка: точно к нужному месту
const el = highlighted.querySelector(`[data-mid="${ci}-${mi}"]`);
if (el) {
const hr = highlighted.getBoundingClientRect();
const er = el.getBoundingClientRect();
highlighted.scrollTop += (er.top - hr.top) - highlighted.clientHeight / 3;
el.classList.remove('flash');
void el.offsetWidth;
el.classList.add('flash');
}
// 1-я колонка: по доле текста (приблизительно, но рядом с нужным местом)
const g = ch.start + m.offset;
const max = input.scrollHeight - input.clientHeight;
if (max > 0 && fullText.length) {
input.scrollTop = Math.max(0, (g / fullText.length) * input.scrollHeight - input.clientHeight / 3);
}
requestAnimationFrame(() => requestAnimationFrame(() => { syncing = false; }));
}
/* ---------- Прогон всех кусков ---------- */
document.getElementById('btnRunAll').onclick = async () => {
if (running) return;
running = true;
paused = false;
document.getElementById('btnPause').disabled = false;
document.getElementById('btnRunAll').disabled = true;
let okCount = 0, errCount = 0;
for (let i = 0; i < chunks.length; i++) {
if (paused) {
status.textContent = `Пауза (готово: ${okCount}, ошибок: ${errCount}). Нажмите «Проверить все куски» для продолжения.`;
running = false;
document.getElementById('btnRunAll').disabled = false;
return;
}
if (chunks[i].done) { okCount++; continue; }
const ok = await runOne(i);
if (ok) okCount++; else errCount++;
if (i < chunks.length - 1) await sleep(1200);
}
if (errCount === 0) {
status.textContent = `Все куски проверены ✅ (${okCount})`;
} else {
status.textContent = `Готово: успешно ${okCount}, с ошибкой ${errCount}. Исправьте причину и повторите.`;
}
running = false;
document.getElementById('btnPause').disabled = true;
document.getElementById('btnRunAll').disabled = false;
};
document.getElementById('btnPause').onclick = () => { paused = true; };
document.getElementById('btnReset').onclick = () => {
chunks = [];
fullText = ''; fullFmt = [];
running = false; paused = false;
input.innerHTML = '';
results.textContent = '';
highlighted.textContent = '';
chunkButtons.innerHTML = '';
chunkInfo.textContent = '';
status.textContent = '';
document.getElementById('btnRunAll').disabled = true;
document.getElementById('btnPause').disabled = true;
};
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function escapeHtml(s) { return esc(s); }
function escapeAttr(s) { return esc(s).replace(/"/g,'&quot;'); }
</script>
</body>
</html>