diff --git a/internal/web/static/app.css b/internal/web/static/app.css index 2e0b0ee..f1e5e2c 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -291,6 +291,8 @@ html[data-wrap] .float { overflow-x: hidden; } /* gutter/code share line metrics; the editor gutter keeps its own padding (#50) */ .code .gutter { padding-top: 0; padding-bottom: 0; } .codebody { padding: 0 18px; white-space: pre; } +/* #167: each logical line is its own block so offsetTop identifies its first visual row */ +.codeline { display: block; } /* syntax highlight tokens (#1) */ .tok-kw { color: #c792ea; } .tok-str { color: #a5e075; } diff --git a/internal/web/static/paste-lines.js b/internal/web/static/paste-lines.js new file mode 100644 index 0000000..0eac3d3 --- /dev/null +++ b/internal/web/static/paste-lines.js @@ -0,0 +1,81 @@ +// #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'); + }; + + // 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) { + // 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 < lines.length; 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 <= 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); + } +})(); diff --git a/internal/web/templates/paste.html b/internal/web/templates/paste.html index fde0251..090cf83 100644 --- a/internal/web/templates/paste.html +++ b/internal/web/templates/paste.html @@ -55,9 +55,10 @@ {{end}}
-
{{.Gutter}}
{{.ContentHTML}}
+
{{.Gutter}}
{{.ContentHTML}}
+ {{template "foot" .}}