// #167: with line wrapping on, a logical line can occupy several visual // 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'); 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. 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('\n'); } function renumber() { var lines = body.querySelectorAll('.codeline'); var s = ''; if (wrapOn() && lines.length) { // 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 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(j + 1); frag.appendChild(num); 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 ) 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); } })();