feat: GitHub-style line linking (?L=5 or ?L=5-12)

This commit is contained in:
ComputerTech 2026-04-06 19:46:16 +01:00
parent be6b102d16
commit f93d232769
2 changed files with 139 additions and 0 deletions

View File

@ -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; }

View File

@ -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 <code> 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());
}