// #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). function splitLines() { var html = body.innerHTML; var parts = html.split('\n'); var out = []; for (var i = 0; i < parts.length; i++) { out.push('' + parts[i] + ''); } body.innerHTML = out.join(''); } // One .gutline block per visual row. Numbers are placed at the gutter row // whose top matches their .codeline's top; filler rows pad the gaps so // alignment is driven by measured geometry, not by uniform row counts. function renumber() { var lines = body.querySelectorAll('.codeline'); 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; } // Measure each logical line's offsetTop (viewport-relative for // comparison with gutter rows rendered in the same scroll flow). // Reading all rects first avoids interleaved layout reads/writes. var tops = []; for (var i = 0; i < lines.length; i++) { tops.push(lines[i].getBoundingClientRect().top); } // Build one gutline per visual row of the tallest column (the code // body itself); each number is assigned to the visual row whose top // is closest to its line's top. var bodyTop = body.getBoundingClientRect().top; var lh = parseFloat(getComputedStyle(body).lineHeight) || 1; var totalRows = Math.max(lines.length, Math.ceil(body.getBoundingClientRect().height / lh)); 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 < tops.length; j++) { var row = Math.round((tops[j] - bodyTop) / lh); if (row < 0) row = 0; if (row > totalRows - 1) row = totalRows - 1; spans[row].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 q = 0; q < tops.length; q++) placed[q] = Math.round((tops[q] - bodyTop) / lh); requestAnimationFrame(function () { var moved = false; var tops2 = []; for (var q2 = 0; q2 < lines.length; q2++) tops2.push(lines[q2].getBoundingClientRect().top); var bodyTop2 = body.getBoundingClientRect().top; for (var q3 = 0; q3 < tops2.length; q3++) { if (Math.round((tops2[q3] - bodyTop2) / lh) !== placed[q3]) { moved = true; break; } } if (moved) renumber(); }); } splitLines(); renumber(); // Re-renumber on toggle (theme.js toggles data-wrap on ) 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); } })();