diff --git a/static/css/style.css b/static/css/style.css index 09fb248..2f3ee6d 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -285,6 +285,41 @@ pre[class*="language-"], code[class*="language-"] { background: transparent !important; } +/* ── Line highlight (paste view) ───────────────────────────────────────── */ +.view-full pre { position: relative; } +.view-full code { position: relative; z-index: 1; } + +/* Overlay bar behind code text */ +.line-hl { + position: absolute; + left: 0; + right: 0; + background: rgba(37, 99, 235, 0.1); + border-left: 3px solid var(--primary); + pointer-events: none; + z-index: 0; +} +[data-theme="dark"] .line-hl { + background: rgba(59, 130, 246, 0.15); +} + +/* Gutter span for highlighted lines */ +.line-numbers-rows > span.hl-active::before { + color: var(--primary) !important; + font-weight: 700; +} +/* Gutter spans are clickable when line numbers are shown */ +.line-numbers-rows { + pointer-events: auto !important; +} +.line-numbers-rows > span { + cursor: pointer; + pointer-events: auto; +} +.line-numbers-rows > span:hover::before { + color: var(--text-sub) !important; +} + /* ── Responsive ────────────────────────────────────────────────────────── */ @media (max-width: 600px) { .nav-input { width: 90px; } diff --git a/static/js/paste_view.js b/static/js/paste_view.js index 4e518d1..acf1290 100644 --- a/static/js/paste_view.js +++ b/static/js/paste_view.js @@ -56,6 +56,7 @@ document.addEventListener('DOMContentLoaded', async () => { renderPaste(_decryptedPaste); initPasteActions(); + initLineHighlight(); // register Prism hook before initLineNumbers triggers Prism initLineNumbers(); initDeletion(); }); @@ -208,3 +209,106 @@ function initDeletion() { }); } } + +// ── Line highlight / linking ────────────────────────────────────────────────── + +let _hlAnchor = null; // anchor line for shift-click range +let _hlStart = null; +let _hlEnd = null; +let _hlScrolled = false; + +function initLineHighlight() { + // Parse ?L=5 or ?L=5-12 from the URL + const lParam = new URLSearchParams(window.location.search).get('L'); + if (lParam) { + const m = lParam.match(/^(\d+)(?:-(\d+))?$/); + if (m) { + _hlStart = parseInt(m[1], 10); + _hlEnd = m[2] ? parseInt(m[2], 10) : _hlStart; + _hlAnchor = _hlStart; + } + } + + // After every Prism highlight, wire gutter clicks and redraw highlight bars. + // initLineNumbers() calls Prism, which fires this hook. + Prism.hooks.add('complete', function (env) { + if (env.element && env.element.id === 'codeBlock') { + _wireLineClicks(); + if (_hlStart !== null) { + _drawHighlight(_hlStart, _hlEnd); + if (!_hlScrolled) { + _scrollToLine(_hlStart); + _hlScrolled = true; + } + } + } + }); +} + +function _wireLineClicks() { + const viewPre = document.getElementById('viewPre'); + if (!viewPre) return; + viewPre.querySelectorAll('.line-numbers-rows > span').forEach((span, i) => { + if (span.dataset.lineWired) return; + span.dataset.lineWired = '1'; + const lineNum = i + 1; + span.title = `Line ${lineNum}`; + span.addEventListener('click', (e) => { + if (e.shiftKey && _hlAnchor !== null) { + const a = Math.min(_hlAnchor, lineNum); + const b = Math.max(_hlAnchor, lineNum); + _hlStart = a; _hlEnd = b; + _drawHighlight(a, b); + _updateLineUrl(a, b); + } else { + _hlAnchor = lineNum; + _hlStart = lineNum; + _hlEnd = lineNum; + _drawHighlight(lineNum, lineNum); + _updateLineUrl(lineNum, lineNum); + } + }); + }); +} + +function _drawHighlight(start, end) { + const viewPre = document.getElementById('viewPre'); + const code = document.getElementById('codeBlock'); + if (!viewPre || !code) return; + + // Remove old bars and gutter classes + viewPre.querySelectorAll('.line-hl').forEach(el => el.remove()); + viewPre.querySelectorAll('.line-numbers-rows > span').forEach( + (s, i) => s.classList.toggle('hl-active', i + 1 >= start && i + 1 <= end) + ); + + const lineH = parseFloat(getComputedStyle(code).lineHeight) || 22.4; + const padTop = parseFloat(getComputedStyle(viewPre).paddingTop) || 16; + + const frag = document.createDocumentFragment(); + for (let i = start; i <= end; i++) { + const bar = document.createElement('div'); + bar.className = 'line-hl'; + bar.style.top = (padTop + (i - 1) * lineH) + 'px'; + bar.style.height = lineH + 'px'; + frag.appendChild(bar); + } + // Insert before so bars sit below code text in z-order + viewPre.insertBefore(frag, viewPre.firstChild); +} + +function _scrollToLine(lineNum) { + const code = document.getElementById('codeBlock'); + const viewPre = document.getElementById('viewPre'); + const scroller = document.querySelector('.view-full'); + if (!code || !viewPre || !scroller) return; + const lineH = parseFloat(getComputedStyle(code).lineHeight) || 22.4; + const padTop = parseFloat(getComputedStyle(viewPre).paddingTop) || 16; + scroller.scrollTop = Math.max(0, padTop + (lineNum - 1) * lineH - 80); +} + +function _updateLineUrl(start, end) { + const url = new URL(window.location.href); + url.searchParams.set('L', start === end ? String(start) : `${start}-${end}`); + history.replaceState(null, '', url.toString()); +}