Commit e1056df refactored renumber() to place gutter numbers by measured
.codeline geometry but left the one-frame verification pass reading the
removed 'tops' array — a ReferenceError at line 80 that aborts renumber()
before any number is assigned, leaving the wrap-ON gutter with blank filler
rows. Read the placement from the already-computed rowIdx array instead.
Verified locally (CDP bounding-box probes, 6-line paste incl. one long
unbroken token that wraps mid-word): gutter number top vs .codeline top
diff <= 0.1px for every line at 1400x900 and 375x812, wrap ON and OFF.
Wrap OFF keeps the plain '1..N' text gutter with no .gutline spans.
No console or CSP violations.
115 lines
4.8 KiB
JavaScript
115 lines
4.8 KiB
JavaScript
// #167: with line wrapping on, a logical line can occupy several visual
|
|
// lines; the gutter must show one number per VISUAL line, and each number
|
|
// must sit on the visual row where its logical line STARTS. Per-line spans
|
|
// give each logical line its own box so offsetTop order stays correct even
|
|
// when highlighting spans cross no line boundaries.
|
|
(function () {
|
|
var body = document.getElementById('codebody');
|
|
var gutter = document.getElementById('gutter');
|
|
var code = document.getElementById('code');
|
|
if (!body || !gutter || !code) return;
|
|
|
|
var wrapOn = function () {
|
|
return document.documentElement.hasAttribute('data-wrap');
|
|
};
|
|
|
|
// Wrap each logical line (split on newline; spans never contain newlines
|
|
// because HighlightCode highlights per line) in a .codeline block. The
|
|
// spans are display:block, so they are joined with '' — a '\n' join leaves
|
|
// newline text nodes between blocks that pre-wrap renders as an extra line
|
|
// box per line, which would shift every following number down one row (#167).
|
|
function splitLines() {
|
|
var html = body.innerHTML;
|
|
var parts = html.split('\n');
|
|
var out = [];
|
|
for (var i = 0; i < parts.length; i++) {
|
|
out.push('<span class="codeline">' + parts[i] + '</span>');
|
|
}
|
|
body.innerHTML = out.join('');
|
|
}
|
|
|
|
// One .gutline block per visual row. Numbers are placed at the gutter row
|
|
// whose top matches their .codeline's top; filler rows pad the gaps so
|
|
// alignment is driven by measured geometry, not by uniform row counts.
|
|
function renumber() {
|
|
var lines = body.querySelectorAll('.codeline');
|
|
if (!wrapOn() || !lines.length) {
|
|
// wrap OFF: one number per logical line (pre-existing behavior,
|
|
// including the gutter scrolling with horizontal scroll).
|
|
var s = '';
|
|
for (var k = 1; k <= lines.length; k++) s += k + '\n';
|
|
gutter.textContent = lines.length ? s.slice(0, -1) : '1';
|
|
return;
|
|
}
|
|
|
|
// Measure each logical line's geometry. Reading all rects first avoids
|
|
// interleaved layout reads/writes. A line's number goes on the visual row
|
|
// whose top matches the line's top; the gutter is sized to cover the
|
|
// deepest row any line STARTS on (a trailing empty logical line renders
|
|
// with zero height but still needs its number row).
|
|
var bodyTop = body.getBoundingClientRect().top;
|
|
var lh = parseFloat(getComputedStyle(body).lineHeight) || 1;
|
|
var rowIdx = [];
|
|
var totalRows = 1;
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var row = Math.round((lines[i].getBoundingClientRect().top - bodyTop) / lh);
|
|
if (row < 0) row = 0;
|
|
rowIdx.push(row);
|
|
if (row + 1 > totalRows) totalRows = row + 1;
|
|
}
|
|
if (totalRows < lines.length) totalRows = lines.length;
|
|
gutter.textContent = '';
|
|
var frag = document.createDocumentFragment();
|
|
var spans = [];
|
|
for (var r = 0; r < totalRows; r++) {
|
|
var cell = document.createElement('span');
|
|
cell.className = 'gutline';
|
|
cell.textContent = '\u00a0';
|
|
spans.push(cell);
|
|
frag.appendChild(cell);
|
|
}
|
|
gutter.appendChild(frag);
|
|
for (var j = 0; j < rowIdx.length; j++) {
|
|
spans[rowIdx[j]].textContent = String(j + 1);
|
|
}
|
|
// A wrapped line whose last visual row is only partially filled can
|
|
// settle a hair under N * line-height after the gutter is rebuilt; a
|
|
// gutter pass that changes the code column width reflows it. Verify the
|
|
// placement one frame later and re-run if any line's row moved (#167).
|
|
var placed = {};
|
|
for (var q = 0; q < rowIdx.length; q++) placed[q] = rowIdx[q];
|
|
requestAnimationFrame(function () {
|
|
var moved = false;
|
|
var tops2 = [];
|
|
for (var q2 = 0; q2 < lines.length; q2++) tops2.push(lines[q2].getBoundingClientRect().top);
|
|
var bodyTop2 = body.getBoundingClientRect().top;
|
|
for (var q3 = 0; q3 < tops2.length; q3++) {
|
|
if (Math.round((tops2[q3] - bodyTop2) / lh) !== placed[q3]) { moved = true; break; }
|
|
}
|
|
if (moved) renumber();
|
|
});
|
|
}
|
|
|
|
splitLines();
|
|
renumber();
|
|
|
|
// Re-renumber on toggle (theme.js toggles data-wrap on <html>) and on any
|
|
// size change (paste of lots of text, window resize, zoom). Only width
|
|
// changes of the code column affect wrapping, so observe width only —
|
|
// height changes caused by our own renumbering must not re-trigger.
|
|
var mo = new MutationObserver(renumber);
|
|
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-wrap'] });
|
|
var lastW = body.getBoundingClientRect().width;
|
|
if (window.ResizeObserver) {
|
|
var ro = new ResizeObserver(function () {
|
|
var w = body.getBoundingClientRect().width;
|
|
if (Math.abs(w - lastW) < 0.5) return;
|
|
lastW = w;
|
|
renumber();
|
|
});
|
|
ro.observe(body);
|
|
} else {
|
|
window.addEventListener('resize', renumber);
|
|
}
|
|
})();
|