From 64737582a5df51bb8f635dad2a7e40de71ac17d8 Mon Sep 17 00:00:00 2001 From: interlope Date: Wed, 3 Jun 2026 00:27:05 +0200 Subject: [PATCH] feat: add line numbers to code blocks, enhance search overlay functionality, and improve navbar search visibility --- css/style.css | 58 ++++++++++++++++++++++++-- js/blowdit.js | 65 +++++++++++++++++++++++++----- js/highlightjs-line-numbers.min.js | 1 + metadata.json | 2 +- php/navbar.php | 5 ++- 5 files changed, 114 insertions(+), 17 deletions(-) create mode 100644 js/highlightjs-line-numbers.min.js diff --git a/css/style.css b/css/style.css index 2317b46..67910b8 100644 --- a/css/style.css +++ b/css/style.css @@ -1862,6 +1862,22 @@ pre code.hljs { background: transparent !important; } +/* Line numbers (highlightjs-line-numbers plugin builds a table inside ) */ +.hljs-ln { border-collapse: collapse; } +.hljs-ln td { padding: 0; border: 0; vertical-align: top; } +.hljs-ln-numbers { + width: 1%; + white-space: nowrap; + text-align: right; + padding-right: 0.9em !important; + color: var(--text-muted); + border-right: 1px solid var(--border-color); + user-select: none; + -webkit-user-select: none; +} +.hljs-ln-code { padding-left: 0.9em !important; } +.hljs-ln-n::before { content: attr(data-line-number); } + /* ======================================== ANCHOR OFFSET — clear the fixed navbar on ToC / hash jumps Without this, jumping to a heading lands it at y=0, hidden behind the @@ -2277,11 +2293,21 @@ button:focus:not(:focus-visible) { box-shadow: var(--shadow-lg); } -.search-input { - width: 100%; - padding: 1rem 1.2rem; - border: none; +.search-field { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0 1.1rem; border-bottom: 1px solid var(--border-color); +} + +.search-icon { flex: 0 0 auto; color: var(--text-muted); } + +.search-input { + flex: 1; + min-width: 0; + padding: 0.95rem 0; + border: none; background: transparent; color: var(--text-primary); font-family: var(--font-sans); @@ -2291,6 +2317,30 @@ button:focus:not(:focus-visible) { .search-input::placeholder { color: var(--text-muted); } +.search-hint { + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.5rem 1.1rem; + border-top: 1px solid var(--border-subtle); + font-size: var(--font-size-xs); + color: var(--text-muted); +} + +.search-hint kbd { + padding: 0.05em 0.4em; + font-family: var(--font-sans); + font-size: 0.95em; + color: var(--text-secondary); + background: var(--bg-subtle); + border: 1px solid var(--border-color); + border-radius: 4px; +} + +/* Navbar search button — hidden until JS reveals it (no on-page search in view) */ +.nav-search { display: none; } +.nav-search.is-visible { display: block; } + .search-results { list-style: none; margin: 0; diff --git a/js/blowdit.js b/js/blowdit.js index 3f0be52..b2b6c3f 100644 --- a/js/blowdit.js +++ b/js/blowdit.js @@ -694,6 +694,9 @@ pre.parentNode.classList.contains('code-block')) return; var code = pre.querySelector('code'); + // Capture now — before highlighting/line-numbers rewrite the DOM (the + // line-numbers table would otherwise mangle textContent on copy). + var sourceText = (code ? code.textContent : pre.textContent) || ''; var wrap = document.createElement('div'); wrap.className = 'code-block'; @@ -720,8 +723,7 @@ var resetTimer = null; btn.addEventListener('click', function () { - var text = (code ? code.textContent : pre.textContent) || ''; - copyText(text).then(function () { + copyText(sourceText).then(function () { btn.classList.add('is-copied'); btn.innerHTML = SVG_CHECK + '' + T.copied + ''; btn.setAttribute('aria-label', T.copied); @@ -799,6 +801,17 @@ Array.prototype.forEach.call(codes, function (code) { try { window.hljs.highlightElement(code); } catch (e) {} }); + // Line numbers (self-hosted plugin). Skips single-line blocks by default. + var ln = document.createElement('script'); + ln.src = BASE + 'js/highlightjs-line-numbers.min.js'; + ln.onload = function () { + if (!window.hljs || !window.hljs.lineNumbersBlock) return; + Array.prototype.forEach.call(codes, function (code) { + if (code.closest && code.closest('.tab-group')) return; // keep tab copy clean + try { window.hljs.lineNumbersBlock(code, { singleLine: false }); } catch (e) {} + }); + }; + document.head.appendChild(ln); }; document.head.appendChild(s); })(); @@ -1179,13 +1192,20 @@ var el = document.getElementById('blowdit-search-index'); if (el) { try { data = JSON.parse(el.textContent) || []; } catch (e) { data = []; } } + var ICON = ''; + var overlay = document.createElement('div'); overlay.className = 'search-overlay'; overlay.innerHTML = ''; document.body.appendChild(overlay); @@ -1193,13 +1213,12 @@ var results = overlay.querySelector('.search-results'); var empty = overlay.querySelector('.search-empty'); var active = -1; - var current = []; function open() { overlay.classList.add('is-open'); document.body.style.overflow = 'hidden'; input.value = ''; - render([]); + update(); setTimeout(function () { input.focus(); }, 30); } function close() { @@ -1208,13 +1227,12 @@ } function isOpen() { return overlay.classList.contains('is-open'); } - function score(item, q) { + function matches(item, q) { var hay = (item.t + ' ' + (item.c || '') + ' ' + (item.g || []).join(' ') + ' ' + (item.d || '')).toLowerCase(); return hay.indexOf(q) !== -1; } function render(list) { - current = list; active = list.length ? 0 : -1; results.innerHTML = ''; empty.hidden = !(input.value.trim() && list.length === 0); @@ -1231,14 +1249,14 @@ function update() { var q = input.value.trim().toLowerCase(); - if (!q) { render([]); return; } - render(data.filter(function (item) { return score(item, q); }).slice(0, 8)); + if (!q) { render(data.slice(0, 6)); return; } // recent posts as suggestions + render(data.filter(function (item) { return matches(item, q); }).slice(0, 8)); } function move(delta) { var items = results.querySelectorAll('.search-result'); if (!items.length) return; - items[active] && items[active].classList.remove('is-active'); + if (items[active]) items[active].classList.remove('is-active'); active = (active + delta + items.length) % items.length; items[active].classList.add('is-active'); items[active].scrollIntoView({ block: 'nearest' }); @@ -1257,10 +1275,37 @@ }); overlay.addEventListener('click', function (e) { if (e.target === overlay) close(); }); + // Navbar button(s) open the overlay. Array.prototype.forEach.call(document.querySelectorAll('.js-search-open'), function (b) { b.addEventListener('click', function (e) { e.preventDefault(); open(); }); }); + // Reproduce search on the existing on-page boxes (hero + sidebar): focusing + // or clicking them opens the overlay instead of the plain server search. + Array.prototype.forEach.call(document.querySelectorAll('.home-search input, .plugin-search input, .plugin-search .form-control'), function (inp) { + ['focus', 'click', 'mousedown'].forEach(function (ev) { + inp.addEventListener(ev, function (e) { e.preventDefault(); inp.blur(); open(); }); + }); + }); + + // Navbar loupe: show only when no on-page search box is visible (avoids a + // duplicate). Appears once the hero/sidebar search scrolls out of view. + var navItems = document.querySelectorAll('.nav-search'); + var pageSearch = document.querySelector('.home-search, .sidebar-container .plugin-search'); + function setNav(v) { + Array.prototype.forEach.call(navItems, function (n) { n.classList.toggle('is-visible', v); }); + } + if (navItems.length) { + if (!pageSearch || !('IntersectionObserver' in window)) { + setNav(true); // nothing on-page to duplicate -> always available + } else { + setNav(false); + new IntersectionObserver(function (entries) { + setNav(!entries[0].isIntersecting); + }, { rootMargin: '-64px 0px 0px 0px' }).observe(pageSearch); + } + } + // Expose for the keyboard-shortcuts handler. window.BLOWDIT_SEARCH = { open: open, close: close, isOpen: isOpen }; })(); diff --git a/js/highlightjs-line-numbers.min.js b/js/highlightjs-line-numbers.min.js new file mode 100644 index 0000000..a5f9f20 --- /dev/null +++ b/js/highlightjs-line-numbers.min.js @@ -0,0 +1 @@ +!function(r,o){"use strict";var e,i="hljs-ln",l="hljs-ln-line",h="hljs-ln-code",s="hljs-ln-numbers",c="hljs-ln-n",m="data-line-number",a=/\r\n|\r|\n/g;function u(e){for(var n=e.toString(),t=e.anchorNode;"TD"!==t.nodeName;)t=t.parentNode;for(var r=e.focusNode;"TD"!==r.nodeName;)r=r.parentNode;var o=parseInt(t.dataset.lineNumber),a=parseInt(r.dataset.lineNumber);if(o==a)return n;var i,l=t.textContent,s=r.textContent;for(a
{6}',[l,s,c,m,h,o+n.startFrom,0{1}',[i,r])}return e}(e.innerHTML,o)}function v(e){var n=e.className;if(/hljs-/.test(n)){for(var t=g(e.innerHTML),r=0,o="";r{1}\n',[n,0 [!NOTE]/[!TIP]/[!WARNING]/...), code-block header bar with language label + copy button, visual breadcrumbs, custom 404 page, image figure/figcaption from the markdown title, a mobile table-of-contents drawer, and third-party resource hints. v1.6.0: self-hosted Inter (no Google Fonts), client-side search overlay (\"/\" key + navbar button), keyboard shortcuts (/ , g h, t, Esc), a tabbed-code copy button, series navigation (series-* tag), and content typography (keycaps, task-list checkboxes, , external-link icon, footnote/definition-list styling)." + "notes": "A Bludit theme inspired by the Blowfish Hugo theme. Five-theme swatch picker (Light/Dark/Nord/Dracula/Catppuccin), sticky ToC sidebar on articles, image lightbox with zoom, image carousel (```carousel fence), tabbed code blocks (```tabs fence), smart sidebar per page type, and clean responsive typography. v1.3.0: SEO (Open Graph, Twitter Card, JSON-LD, theme-color, cover preload), per-theme syntax highlighting (self-hosted highlight.js), copy-to-clipboard on code blocks, numbered pagination, inline-SVG icons (dropped the Bootstrap Icons webfont), removed jQuery/Bootstrap JS, deferred cached JS bundle, reading progress bar, back-to-top, randomised sidebar selection, lazy-loaded images, accessibility (skip link, ARIA, :focus-visible) and a print stylesheet. v1.4.0: canonical URL, WebSite + BreadcrumbList + author sameAs JSON-LD, og:image:alt / twitter:site+creator, cover-image aspect-ratio (CLS), heading anchor links, prev/next article nav, related posts, share buttons (X/LinkedIn/copy-link), mailto social icon, PWA web app manifest, 'updated on' date, and outbound-link hardening (noopener noreferrer). v1.5.0: callout/admonition boxes (> [!NOTE]/[!TIP]/[!WARNING]/...), code-block header bar with language label + copy button, visual breadcrumbs, custom 404 page, image figure/figcaption from the markdown title, a mobile table-of-contents drawer, and third-party resource hints. v1.6.0: self-hosted Inter (no Google Fonts), client-side search overlay (\"/\" key + navbar button), keyboard shortcuts (/ , g h, t, Esc), a tabbed-code copy button, series navigation (series-* tag), and content typography (keycaps, task-list checkboxes, , external-link icon, footnote/definition-list styling). Code blocks gained line numbers; the on-page search boxes (hero + sidebar) now open the same instant overlay, and the navbar search icon only appears once they scroll out of view." } diff --git a/php/navbar.php b/php/navbar.php index 5c1c315..ead0ff4 100644 --- a/php/navbar.php +++ b/php/navbar.php @@ -59,8 +59,9 @@ - -