The paste gutter was pinned to a fixed 3ch width. With box-sizing: border-box that leaves only ~19px of content after the 10px+10px side padding, so 2+ digit line numbers overflow right into the code text (owner-visible from line 10, worst at 100+). paste-lines.js now sets the gutter width to calc(Nch + 20px), where N is the digit count of the highest line number, via CSSOM (CSP forbids inline style attributes). Numbers were already right-aligned; the column now matches the width of the biggest number. The width is only written when it changes, so the resize-observer/renumber loop keeps a stable fixed point.
141 lines
6.2 KiB
JavaScript
141 lines
6.2 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,
|
|
// #205). Whitespace-only text nodes are also stripped defensively below: any
|
|
// that reach the DOM (older cached HTML, other templates) render as phantom
|
|
// rows under white-space: pre and drift the gutter.
|
|
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('');
|
|
// #205: strip whitespace-only text nodes between the .codeline blocks.
|
|
var ws = [];
|
|
for (var n = body.firstChild; n; n = n.nextSibling) {
|
|
if (n.nodeType === 3 && !/\S/.test(n.nodeValue)) ws.push(n);
|
|
}
|
|
for (var w = 0; w < ws.length; w++) body.removeChild(ws[w]);
|
|
}
|
|
|
|
// One .gutline block per visual row. The gutter must reproduce the code
|
|
// column's REAL rendered rows: each number goes on the visual row whose top
|
|
// matches its .codeline's top (a line wrapping to N rows gets its number on
|
|
// the FIRST of those rows), and filler rows pad the gaps. Geometry is
|
|
// measured, not derived from span counts or heights.
|
|
function renumber() {
|
|
var lines = body.querySelectorAll('.codeline');
|
|
// #257: size the gutter column to the widest line number so numbers in
|
|
// the 100s+ fit their own column instead of bleeding into the code text.
|
|
// The gutter is box-sizing: border-box, so the column width must be the
|
|
// digits PLUS the 10px left + 10px right padding; at the CSS default 3ch
|
|
// the padding alone leaves only ~19px of content, and any 2+ digit
|
|
// number overflows into the code. Numbers are right-aligned, and the
|
|
// width below fits the widest number exactly. Set via CSSOM (CSP
|
|
// forbids inline style attributes). Only touch the width when it
|
|
// changes: the resize observer below re-runs renumber() when the gutter
|
|
// width reflows the code column, and rewriting the same value would
|
|
// ping-pong the fixed point forever.
|
|
var digits = String(lines.length || 1).length;
|
|
var w = 'calc(' + digits + 'ch + 20px)';
|
|
if (gutter.style.width !== w) {
|
|
gutter.style.minWidth = w;
|
|
gutter.style.width = w;
|
|
}
|
|
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;
|
|
}
|
|
|
|
// Read all rects first: batching layout reads before the writes below
|
|
// avoids interleaved read/write reflows.
|
|
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 p = 0; p < rowIdx.length; p++) placed.push(rowIdx[p]);
|
|
requestAnimationFrame(function () {
|
|
var moved = false;
|
|
var bodyTop2 = body.getBoundingClientRect().top;
|
|
var lh2 = parseFloat(getComputedStyle(body).lineHeight) || lh;
|
|
var tops2 = [];
|
|
for (var q2 = 0; q2 < lines.length; q2++) tops2.push(lines[q2].getBoundingClientRect().top);
|
|
for (var q3 = 0; q3 < tops2.length; q3++) {
|
|
if (Math.round((tops2[q3] - bodyTop2) / lh2) !== 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);
|
|
}
|
|
})();
|