// #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. (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'); }; // #194: join with NO separator. .codeline is display:block, so a '\n' // text node between spans is an extra line box in the white-space:pre // container — that doubled the visual line spacing and desynced the // gutter in BOTH modes. 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(''); } function renumber() { var lines = body.querySelectorAll('.codeline'); // #194: a paste ending in '\n' produces one trailing empty .codeline; // number only real lines (like every editor) so the count matches. var count = lines.length; if (count > 1 && lines[count - 1].textContent === '') count--; var s = ''; if (wrapOn() && count) { // 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. var lh = parseFloat(getComputedStyle(body).lineHeight) || 1; gutter.textContent = ''; var frag = document.createDocumentFragment(); for (var i = 0; i < count; i++) { var num = document.createElement('span'); num.className = 'gutline'; num.textContent = String(i + 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'; blank.textContent = '\u00a0'; frag.appendChild(blank); } } gutter.appendChild(frag); } else { for (var k = 1; k <= count; k++) s += k + '\n'; gutter.textContent = count ? 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); } })();