Fix #167 r2: measure wrapped rows from rendered line geometry
CI / test (pull_request) Successful in 47s
CI / docker (pull_request) Skipped

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.
This commit is contained in:
fen
2026-09-10 11:54:07 -05:00
parent 1a8da8cd7a
commit 71c53b65f3
+29 -12
View File
@@ -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';