// #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. The gutter must reproduce the code // column's REAL rendered rows: each number goes on the visual row whose top // matches its .codeline's top (a line wrapping to N rows gets its number on // the FIRST of those rows), and filler rows pad the gaps. Geometry is // measured, not derived from span counts or heights. 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; } // Read all rects first: batching layout reads before the writes below // avoids interleaved read/write reflows. var bodyTop = body.getBoundingClientRect().top; var lh = parseFloat(getComputedStyle(body).lineHeight) || 1; var rowIdx = []; var totalRows = 1; for (var i = 0; i < lines.length; i++) { var row = Math.round((lines[i].getBoundingClientRect().top - bodyTop) / lh); if (row < 0) row = 0; rowIdx.push(row); if (row + 1 > totalRows) totalRows = row + 1; } if (totalRows < lines.length) totalRows = lines.length; 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 < rowIdx.length; j++) { spans[rowIdx[j]].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 p = 0; p < rowIdx.length; p++) placed.push(rowIdx[p]); requestAnimationFrame(function () { var moved = false; var bodyTop2 = body.getBoundingClientRect().top; var lh2 = parseFloat(getComputedStyle(body).lineHeight) || lh; var tops2 = []; for (var q2 = 0; q2 < lines.length; q2++) tops2.push(lines[q2].getBoundingClientRect().top); for (var q3 = 0; q3 < tops2.length; q3++) { if (Math.round((tops2[q3] - bodyTop2) / lh2) !== 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); } })();