feat: GitHub-style line linking (?L=5 or ?L=5-12)
This commit is contained in:
parent
be6b102d16
commit
f93d232769
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue