diff --git a/internal/web/static/paste-lines.js b/internal/web/static/paste-lines.js index 0eac3d3..98eda6a 100644 --- a/internal/web/static/paste-lines.js +++ b/internal/web/static/paste-lines.js @@ -1,7 +1,13 @@ // #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. +// 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'); @@ -28,21 +34,32 @@ 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. + // 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 i = 0; i < lines.length; i++) { + 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(i + 1); + num.textContent = String(j + 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';