Files
palette/internal/web/static/paste-lines.js
T
fen 1f5e671a43
CI / test (pull_request) Successful in 43s
CI / docker (pull_request) Skipped
Fix #194 r2: align line-number gutter in both wrap modes
QA FAIL of PR #179 (verified on palette-dev 2026-09-10) found two defects:

1) Row accounting: splitLines() joined .codeline blocks with '\n' text
   nodes. Since dee062d made .codeline display:block, each interleaved
   newline text node rendered as its own extra visual row in the code
   column under pre-wrap, but renumber() counted only .codeline span
   heights, so the gutter had one fewer row per line break and numbers
   drifted off alignment on wrapped lines. Fix: join('') so blocks are
   adjacent with no phantom newline rows; blank source lines become
   empty blocks given a line box via .codeline:empty::before (ZWJ) in
   app.css so they still count as one row.

2) Inline gutter spans: .gutline spans were appended with no separators
   and .code .gutter .gutline { display: block } did not exist on the
   deployed build, so all numbers landed on one row. That rule is
   present in origin/dev (merged via #183); kept unchanged and verified
   by rendering in a local build.

Wrap OFF path unchanged: gutter still gets '1\n2\n3\n4' text and the
horizontal-scroll gutter behavior is untouched (no CSS changes to
scrolling; only a :empty line-box rule). CSP unchanged: no inline
styles/handlers, static JS only.
2026-09-10 12:07:42 -05:00

88 lines
3.6 KiB
JavaScript

// #167: with line wrapping on, a logical line can occupy several visual
// lines; the gutter must show one number per VISUAL line. 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
// blocks must be adjacent with NO newline text between them (#194):
// .codeline is display:block, and under pre-wrap each interleaved '\n'
// text node renders as an extra phantom row in the code column that
// renumber() does not count, drifting the gutter off alignment on
// wrapped lines. Blank lines become empty blocks; .codeline:empty keeps
// them one row tall (see app.css).
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('');
}
function renumber() {
var lines = body.querySelectorAll('.codeline');
var s = '';
if (wrapOn() && lines.length) {
// Each logical line block occupies rows = height / line-height when
// wrapped; its number sits on the first row and the remaining rows get
// blank gutter lines so numbers stay aligned with line starts.
// 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.
var lh = parseFloat(getComputedStyle(body).lineHeight) || 1;
gutter.textContent = '';
var frag = document.createDocumentFragment();
for (var i = 0; i < lines.length; i++) {
var num = document.createElement('span');
num.className = 'gutline';
num.textContent = String(i + 1);
frag.appendChild(num);
var rows = Math.max(1, Math.round(lines[i].getBoundingClientRect().height / lh));
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);
}
})();