Files
palette/internal/web/static/paste-lines.js
T
fen 71c53b65f3
CI / test (pull_request) Successful in 47s
CI / docker (pull_request) Skipped
Fix #167 r2: measure wrapped rows from rendered line geometry
Round-1 accounting rounded each .codeline span's own height to rows, but
in pre-wrap the newline text nodes between blocks each render as their own
anonymous row, so per-span height undercounts and gutter numbers drift
below their line's first visual row. Renumber now derives rows per line
from the rendered gap between consecutive line tops (wrapped rows plus the
newline row); the last line falls back to its own box height. Wrap-off
path unchanged.
2026-09-10 11:54:07 -05:00

99 lines
3.9 KiB
JavaScript

// #167: with line wrapping on, a logical line can occupy several visual
// lines; the gutter must show one number per logical line, aligned with its
// FIRST visual row, and blank rows below it so later numbers don't drift.
//
// Row accounting must be based on actual rendered geometry, not per-span
// height: in a pre-wrap container the newline text nodes BETWEEN the
// .codeline blocks each render as their own anonymous-block row, so a
// logical line's visual footprint is (next line's top - this line's top),
// not this line's own box height. Measuring the gap between consecutive
// line tops captures both the wrapped rows and the newline row.
(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.
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('\n');
}
function renumber() {
var lines = body.querySelectorAll('.codeline');
var s = '';
if (wrapOn() && lines.length) {
// Rows per logical line = rendered vertical gap to the next line's
// first row (wrapped rows + the pre-wrap newline row between blocks),
// divided by line-height. The last line has no following newline, so
// its own box height is correct. The code column width must not
// change while measuring (the gutter is flex-shrink:0 so its own row
// count never affects it), and renumber must be idempotent to avoid a
// ResizeObserver feedback loop: only width changes re-trigger it.
var lh = parseFloat(getComputedStyle(body).lineHeight) || 1;
var tops = [];
for (var i = 0; i < lines.length; i++) {
tops.push(lines[i].getBoundingClientRect().top);
}
gutter.textContent = '';
var frag = document.createDocumentFragment();
for (var j = 0; j < lines.length; j++) {
var span;
if (j < lines.length - 1) {
span = tops[j + 1] - tops[j];
} else {
span = lines[j].getBoundingClientRect().height;
}
var rows = Math.max(1, Math.round(span / lh));
var num = document.createElement('span');
num.className = 'gutline';
num.textContent = String(j + 1);
frag.appendChild(num);
for (var r = 1; r < rows; r++) {
var blank = document.createElement('span');
blank.className = 'gutline';
blank.textContent = '\u00a0';
frag.appendChild(blank);
}
}
gutter.appendChild(frag);
} else {
for (var k = 1; k <= lines.length; k++) s += k + '\n';
gutter.textContent = lines.length ? s.slice(0, -1) : '1';
}
}
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);
}
})();