Spaces:
Running
Running
Update index.html
Browse files- index.html +49 -6
index.html
CHANGED
|
@@ -73,6 +73,11 @@
|
|
| 73 |
.block-title { font-family: -apple-system, Segoe UI, Roboto, sans-serif; font-size: 11px; color: #94a3b8; margin-bottom: 4px; }
|
| 74 |
.issue { background: #fff; border-left: 4px solid #ef4444; padding: 6px 8px; margin-bottom: 6px; border-radius: 4px; font-size: 13px; }
|
| 75 |
.issue.ok { border-color: #16a34a; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
.issue .msg { font-weight: 600; }
|
| 77 |
.issue .sugg { color: #16a34a; }
|
| 78 |
.issue .ctx { color: #64748b; font-size: 12px; margin-top: 3px; }
|
|
@@ -541,7 +546,7 @@ function renderChunkResult(idx) {
|
|
| 541 |
const end = Math.min(ch.text.length, m.offset + m.length + 25);
|
| 542 |
const ctx = ch.text.substring(start, end).replace(/\n/g, ' ');
|
| 543 |
const sugg = m.replacements.slice(0, 5).map(r => r.value).join(', ');
|
| 544 |
-
html += `<div class="issue">
|
| 545 |
<div class="msg">${i+1}. ${escapeHtml(m.message)}</div>
|
| 546 |
<div class="ctx">…${escapeHtml(ctx)}…</div>
|
| 547 |
${sugg ? `<div class="sugg">→ ${escapeHtml(sugg)}</div>` : ''}
|
|
@@ -573,7 +578,7 @@ function renderHighlighted() {
|
|
| 573 |
} else if (!ch.done) {
|
| 574 |
html += fmtSpan(ch.start, ch.end);
|
| 575 |
} else {
|
| 576 |
-
html += highlightChunk(ch);
|
| 577 |
}
|
| 578 |
html += `</div>`;
|
| 579 |
});
|
|
@@ -581,18 +586,18 @@ function renderHighlighted() {
|
|
| 581 |
highlighted.scrollTop = keepScroll;
|
| 582 |
}
|
| 583 |
|
| 584 |
-
function highlightChunk(ch) {
|
| 585 |
const text = ch.text;
|
| 586 |
const g = ch.start;
|
| 587 |
-
const matches =
|
| 588 |
let out = '';
|
| 589 |
let pos = 0;
|
| 590 |
-
for (const m of matches) {
|
| 591 |
if (m.offset < pos) continue;
|
| 592 |
out += fmtSpan(g + pos, g + m.offset);
|
| 593 |
const orig = text.substring(m.offset, m.offset + m.length);
|
| 594 |
const sugg = m.replacements.length ? m.replacements[0].value : '';
|
| 595 |
-
out += `<span class="err" title="${escapeAttr(m.message)}${sugg ? ' → ' + escapeAttr(sugg) : ''}">${fmtSpan(g + m.offset, g + m.offset + m.length)}</span>`;
|
| 596 |
if (sugg && sugg !== orig) {
|
| 597 |
out += ` <span class="fix">[→ ${escapeHtml(sugg)}]</span> `;
|
| 598 |
}
|
|
@@ -602,6 +607,44 @@ function highlightChunk(ch) {
|
|
| 602 |
return out;
|
| 603 |
}
|
| 604 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
/* ---------- Прогон всех кусков ---------- */
|
| 606 |
document.getElementById('btnRunAll').onclick = async () => {
|
| 607 |
if (running) return;
|
|
|
|
| 73 |
.block-title { font-family: -apple-system, Segoe UI, Roboto, sans-serif; font-size: 11px; color: #94a3b8; margin-bottom: 4px; }
|
| 74 |
.issue { background: #fff; border-left: 4px solid #ef4444; padding: 6px 8px; margin-bottom: 6px; border-radius: 4px; font-size: 13px; }
|
| 75 |
.issue.ok { border-color: #16a34a; }
|
| 76 |
+
.issue[data-m] { cursor: pointer; transition: background .15s; }
|
| 77 |
+
.issue[data-m]:hover { background: #f1f5f9; }
|
| 78 |
+
.issue.active { background: #fef9c3; border-left-color: #f59e0b; }
|
| 79 |
+
@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; } }
|
| 80 |
+
.err.flash { animation: flash 1.6s ease-out; }
|
| 81 |
.issue .msg { font-weight: 600; }
|
| 82 |
.issue .sugg { color: #16a34a; }
|
| 83 |
.issue .ctx { color: #64748b; font-size: 12px; margin-top: 3px; }
|
|
|
|
| 546 |
const end = Math.min(ch.text.length, m.offset + m.length + 25);
|
| 547 |
const ctx = ch.text.substring(start, end).replace(/\n/g, ' ');
|
| 548 |
const sugg = m.replacements.slice(0, 5).map(r => r.value).join(', ');
|
| 549 |
+
html += `<div class="issue" data-chunk="${idx}" data-m="${i}" title="Показать в тексте">
|
| 550 |
<div class="msg">${i+1}. ${escapeHtml(m.message)}</div>
|
| 551 |
<div class="ctx">…${escapeHtml(ctx)}…</div>
|
| 552 |
${sugg ? `<div class="sugg">→ ${escapeHtml(sugg)}</div>` : ''}
|
|
|
|
| 578 |
} else if (!ch.done) {
|
| 579 |
html += fmtSpan(ch.start, ch.end);
|
| 580 |
} else {
|
| 581 |
+
html += highlightChunk(ch, idx);
|
| 582 |
}
|
| 583 |
html += `</div>`;
|
| 584 |
});
|
|
|
|
| 586 |
highlighted.scrollTop = keepScroll;
|
| 587 |
}
|
| 588 |
|
| 589 |
+
function highlightChunk(ch, ci) {
|
| 590 |
const text = ch.text;
|
| 591 |
const g = ch.start;
|
| 592 |
+
const matches = ch.matches.map((m, i) => ({ m, i })).sort((a, b) => a.m.offset - b.m.offset);
|
| 593 |
let out = '';
|
| 594 |
let pos = 0;
|
| 595 |
+
for (const { m, i: mi } of matches) {
|
| 596 |
if (m.offset < pos) continue;
|
| 597 |
out += fmtSpan(g + pos, g + m.offset);
|
| 598 |
const orig = text.substring(m.offset, m.offset + m.length);
|
| 599 |
const sugg = m.replacements.length ? m.replacements[0].value : '';
|
| 600 |
+
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>`;
|
| 601 |
if (sugg && sugg !== orig) {
|
| 602 |
out += ` <span class="fix">[→ ${escapeHtml(sugg)}]</span> `;
|
| 603 |
}
|
|
|
|
| 607 |
return out;
|
| 608 |
}
|
| 609 |
|
| 610 |
+
/* ---------- Клик по ошибке во 2-й колонке → переход к месту в тексте ---------- */
|
| 611 |
+
results.addEventListener('click', e => {
|
| 612 |
+
const issue = e.target.closest('.issue[data-m]');
|
| 613 |
+
if (!issue) return;
|
| 614 |
+
jumpToMatch(+issue.dataset.chunk, +issue.dataset.m, issue);
|
| 615 |
+
});
|
| 616 |
+
|
| 617 |
+
function jumpToMatch(ci, mi, issueEl) {
|
| 618 |
+
const ch = chunks[ci];
|
| 619 |
+
const m = ch && ch.matches[mi];
|
| 620 |
+
if (!m) return;
|
| 621 |
+
|
| 622 |
+
results.querySelectorAll('.issue.active').forEach(x => x.classList.remove('active'));
|
| 623 |
+
if (issueEl) issueEl.classList.add('active');
|
| 624 |
+
|
| 625 |
+
syncing = true; // не даём синхронной прокрутке сдвинуть остальные колонки
|
| 626 |
+
|
| 627 |
+
// 3-я колонка: точно к нужному месту
|
| 628 |
+
const el = highlighted.querySelector(`[data-mid="${ci}-${mi}"]`);
|
| 629 |
+
if (el) {
|
| 630 |
+
const hr = highlighted.getBoundingClientRect();
|
| 631 |
+
const er = el.getBoundingClientRect();
|
| 632 |
+
highlighted.scrollTop += (er.top - hr.top) - highlighted.clientHeight / 3;
|
| 633 |
+
el.classList.remove('flash');
|
| 634 |
+
void el.offsetWidth;
|
| 635 |
+
el.classList.add('flash');
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
// 1-я колонка: по доле текста (приблизительно, но рядом с нужным местом)
|
| 639 |
+
const g = ch.start + m.offset;
|
| 640 |
+
const max = input.scrollHeight - input.clientHeight;
|
| 641 |
+
if (max > 0 && fullText.length) {
|
| 642 |
+
input.scrollTop = Math.max(0, (g / fullText.length) * input.scrollHeight - input.clientHeight / 3);
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
requestAnimationFrame(() => requestAnimationFrame(() => { syncing = false; }));
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
/* ---------- Прогон всех кусков ---------- */
|
| 649 |
document.getElementById('btnRunAll').onclick = async () => {
|
| 650 |
if (running) return;
|