diff --git a/theme/ht_searcher.js b/theme/ht_searcher.js
index db9b55b76..3816ac368 100644
--- a/theme/ht_searcher.js
+++ b/theme/ht_searcher.js
@@ -1,92 +1,236 @@
-/* ht_searcher.js ─────────────────────────────────────────────── */
+/* ht_searcher.js ────────────────────────────────────────────────
+ Full Web-Worker search with “⏳” while loading and “🔍” after.
+ Keeps every feature of the original theme (teasers, breadcrumbs,
+ highlight link, hot-keys, arrow navigation, ESC to close, …).
+
+ Dependencies already expected by the theme:
+ • mark.js (for in-page highlights)
+ • elasticlunr.min.js (worker fetches a CDN or /elasticlunr.min.js)
+ • searchindex.js (worker fetches GitHub raw or /searchindex.js)
+*/
+
(() => {
- /* ───────────── 0. Inline Web-Worker code ────────────────────── */
- const workerCode = `
- /* Make scripts written for browsers happy inside the worker */
- self.window = self;
- self.search = self.search || {}; /* ensure object */
-
- const abs = p => location.origin + p; /* helper */
-
- /* 1 ─ elasticlunr.min.js (CDN → local) */
- try {
- importScripts('https://cdn.jsdelivr.net/npm/elasticlunr@0.9.5/elasticlunr.min.js');
- } catch (e) {
- console.error('elasticlunr CDN failed →', e);
- importScripts(abs('/elasticlunr.min.js'));
+ "use strict";
+
+ /* ───────────────── 0. Worker code (string) ───────────────── */
+ const workerCode = `
+ self.window = self; /* make 'window' exist */
+ self.search = self.search || {};
+
+ const abs = p => location.origin + p; /* helper */
+
+ /* 0.1 load elasticlunr (CDN → local fallback) */
+ try {
+ importScripts('https://cdn.jsdelivr.net/npm/elasticlunr@0.9.5/elasticlunr.min.js');
+ } catch {
+ importScripts(abs('/elasticlunr.min.js'));
+ }
+
+ /* 0.2 load searchindex.js (GitHub → local fallback) */
+ (async () => {
+ async function loadRemote () {
+ const r = await fetch(
+ 'https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/refs/heads/master/searchindex.js',
+ { mode:'cors' }
+ );
+ if (!r.ok) throw new Error('HTTP '+r.status);
+ importScripts(
+ URL.createObjectURL(
+ new Blob([await r.text()],{type:'application/javascript'})
+ )
+ );
}
-
- /* 2 ─ searchindex.js (GitHub Raw → local) */
- (async () => {
- try {
- const r = await fetch(
- 'https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/refs/heads/master/searchindex.js',
- { mode:'cors' }
- );
- if (!r.ok) throw new Error('HTTP ' + r.status);
- const blobURL = URL.createObjectURL(
- new Blob([await r.text()], { type:'application/javascript' })
- );
- importScripts(blobURL); /* MIME coercion */
- } catch (e) {
- console.error('GitHub index fetch failed →', e);
- try {
- importScripts(abs('/searchindex.js'));
- } catch (e2) {
- console.error('Local index load failed →', e2);
- throw e2; /* abort loudly */
- }
- }
-
- /* 3 ─ build index & answer queries */
- const idx = elasticlunr.Index.load(self.search.index);
-
- self.onmessage = ({ data:q }) => {
- if (!q) { postMessage([]); return; } /* empty search */
- const raw = idx.search(q, { bool:'AND', expand:true });
- const docs = raw.map(r => idx.documentStore.getDoc(r.ref));
- postMessage(docs); /* only docs cross thread */
- };
- })();
- `;
-
- const workerURL = URL.createObjectURL(
- new Blob([workerCode], { type:'application/javascript' })
- );
- const worker = new Worker(workerURL);
- URL.revokeObjectURL(workerURL); /* tidy blob */
-
- /* ───────────── 1. Tiny UI glue ─────────────────────────────── */
- const WRAP = document.getElementById('search-wrapper');
- const TOG = document.getElementById('search-toggle');
- const INP = document.getElementById('searchbar');
- const LIST = document.getElementById('searchresults');
- const HOTKEY = 83; /* “s” */
- let debounce;
-
- /* paint results */
- worker.onmessage = ({ data:docs }) => {
- LIST.innerHTML = docs.slice(0,30).map(d =>
- '
' + d.title + ''
- ).join('');
- };
-
- /* open UI */
- const open = () => { WRAP.classList.remove('hidden'); INP.focus(); };
-
- TOG.addEventListener('click', open);
- document.addEventListener('keydown', e => {
- if (!e.metaKey && !e.ctrlKey && !e.altKey && e.keyCode === HOTKEY) {
- e.preventDefault(); open();
- }
- });
-
- /* debounced keystrokes → worker */
- INP.addEventListener('input', e => {
- clearTimeout(debounce);
- debounce = setTimeout(() => {
- worker.postMessage(e.target.value.trim());
- }, 120);
- });
+ try { await loadRemote(); }
+ catch(e){ console.warn('remote index failed →',e);
+ importScripts(abs('/searchindex.js')); }
+
+ /* 0.3 build index once, keep for all queries */
+ const idx = elasticlunr.Index.load(self.search.index);
+ const DOC_URLS = self.search.doc_urls;
+ const MAX = 30;
+
+ /* ping main-thread so it can swap the icon */
+ postMessage({ready:true});
+
+ self.onmessage = ({data:q}) => {
+ if (!q) { postMessage([]); return; }
+ const res = idx.search(q,{bool:'AND',expand:true}).slice(0,MAX);
+ postMessage(res.map(r => {
+ const d = idx.documentStore.getDoc(r.ref);
+ return { /* only the fields the UI needs */
+ ref : r.ref,
+ title : d.title,
+ body : d.body,
+ breadcrumbs: d.breadcrumbs,
+ url : DOC_URLS[r.ref]
+ };
+ }));
+ };
})();
-
\ No newline at end of file
+ `;
+
+ /* ───────────────── 1. Build worker ───────────────────────── */
+ const workerURL = URL.createObjectURL(
+ new Blob([workerCode],{type:'application/javascript'})
+ );
+ const worker = new Worker(workerURL);
+ URL.revokeObjectURL(workerURL); /* tidy blob */
+
+ /* ───────────────── 2. DOM references ─────────────────────── */
+ const wrap = document.getElementById('search-wrapper');
+ const modal = document.getElementById('search-modal');
+ const bar = document.getElementById('searchbar');
+ const list = document.getElementById('searchresults');
+ const listOut= document.getElementById('searchresults-outer');
+ const header = document.getElementById('searchresults-header');
+ const icon = document.getElementById('search-toggle');
+
+ /* ───────────────── 3. Constants & state ─────────────────── */
+ const HOTKEY = 83, ESC=27, DOWN=40, UP=38, ENTER=13;
+ const URL_MARK_PARAM = 'highlight';
+ const MAX_RESULTS = 30;
+ const READY_ICON_HTML= icon.innerHTML; /* save original “🔍” */
+ icon.textContent = '⏳'; /* show hour-glass */
+ icon.setAttribute('aria-label','Loading search …');
+
+ let debounce, teaserCount = 0;
+
+ /* ───────────────── 4. Helpers (escaped, teaser, format …) ─ */
+ const escapeHTML = (() => {
+ const MAP = {'&':'&','<':'<','>':'>','"':'"',"'":'''};
+ return s => s.replace(/[&<>'"]/g,c=>MAP[c]);
+ })();
+
+ function metric(c,t){
+ return c===0 ? `No search results for '${t}'.`
+ : c===1 ? `1 search result for '${t}':`
+ : `${c} search results for '${t}':`;
+ }
+
+ /* ── teaser algorithm (unchanged from theme, just ES-ified) ── */
+ function makeTeaser(body,terms){
+ const stem = w=>elasticlunr.stemmer(w.toLowerCase());
+ const t = terms.map(stem);
+ const W_SEARCH=40,W_FIRST=8,W_NORM=2,WIN=30;
+ const wArr=[],sents=body.toLowerCase().split('. ');
+ let idx=0, val=W_FIRST, found=false;
+
+ sents.forEach(sent=>{
+ val=W_FIRST;
+ sent.split(' ').forEach(word=>{
+ if(word){
+ if(t.some(st=>stem(word).startsWith(st))){val=W_SEARCH;found=true;}
+ wArr.push([word,val,idx]); val=W_NORM;
+ }
+ idx+=word.length+1;
+ });
+ idx+=1; /* account for '. ' */
+ });
+ if(!wArr.length) return body;
+
+ const win = Math.min(wArr.length,WIN);
+ const sums=[ wArr.slice(0,win).reduce((a,[,w])=>a+w,0) ];
+ for(let i=1;i<=wArr.length-win;i++)
+ sums[i]=sums[i-1]-wArr[i-1][1]+wArr[i+win-1][1];
+
+ const best = found ? sums.lastIndexOf(Math.max(...sums)) : 0;
+ const out=[], start=wArr[best][2];
+ idx=start;
+ for(let i=best;i');
+ out.push(body.substr(pos,word.length));
+ if(w===W_SEARCH) out.push('');
+ idx=pos+word.length;
+ }
+ return out.join('');
+ }
+
+ function formatResult(doc,terms){
+ const teaser = makeTeaser(escapeHTML(doc.body),terms);
+ teaserCount++;
+ const enc = encodeURIComponent(terms.join(' ')).replace(/'/g,'%27');
+ const u = doc.url.split('#'); if(u.length===1)u.push('');
+ return ``+
+ `${doc.breadcrumbs}`+
+ `${teaser}`;
+ }
+
+ const clear = el => { while(el.firstChild) el.removeChild(el.firstChild); };
+
+ function showUI(show){
+ wrap.classList.toggle('hidden',!show);
+ icon.setAttribute('aria-expanded',show);
+ if(!show){
+ listOut.classList.add('hidden');
+ [...list.children].forEach(li=>li.classList.remove('focus'));
+ }else{
+ window.scrollTo(0,0);
+ bar.focus(); bar.select();
+ }
+ }
+
+ function blurBar(){
+ const tmp=document.createElement('input');
+ tmp.style.position='absolute'; tmp.style.opacity=0;
+ icon.appendChild(tmp); tmp.focus(); tmp.remove();
+ }
+
+ /* ───────────────── 5. Event handlers ─────────────────────── */
+ icon.addEventListener('click',()=>showUI(wrap.classList.contains('hidden')));
+
+ document.addEventListener('keydown',e=>{
+ if(e.altKey||e.ctrlKey||e.metaKey||e.shiftKey) return;
+ const isInput=/^(?:input|select|textarea)$/i.test(e.target.nodeName);
+
+ if(e.keyCode===HOTKEY && !isInput){
+ e.preventDefault(); showUI(true);
+ }else if(e.keyCode===ESC){
+ e.preventDefault(); showUI(false); blurBar();
+ }else if(e.keyCode===DOWN && document.activeElement===bar){
+ e.preventDefault();
+ const first=list.firstElementChild;
+ if(first){ blurBar(); first.classList.add('focus'); }
+ }else if((e.keyCode===DOWN||e.keyCode===UP||e.keyCode===ENTER)
+ && document.activeElement!==bar){
+ const cur=list.querySelector('li.focus'); if(!cur) return;
+ e.preventDefault();
+ if(e.keyCode===DOWN){
+ const nxt=cur.nextElementSibling;
+ if(nxt){ cur.classList.remove('focus'); nxt.classList.add('focus'); }
+ }else if(e.keyCode===UP){
+ const prv=cur.previousElementSibling;
+ cur.classList.remove('focus');
+ if(prv){ prv.classList.add('focus'); } else { bar.focus(); }
+ }else{ /* ENTER */
+ const a=cur.querySelector('a'); if(a) window.location.assign(a.href);
+ }
+ }
+ });
+
+ bar.addEventListener('input',e=>{
+ clearTimeout(debounce);
+ debounce=setTimeout(()=>worker.postMessage(e.target.value.trim()),120);
+ });
+
+ /* ───────────────── 6. Worker messages ────────────────────── */
+ worker.onmessage = ({data})=>{
+ if(data && data.ready){ /* first ping */
+ icon.innerHTML=READY_ICON_HTML; /* restore “🔍” */
+ icon.setAttribute('aria-label','Open search (S)');
+ return;
+ }
+ const docs=data;
+ const q=bar.value.trim();
+ const terms=q.split(/\s+/).filter(Boolean);
+ header.textContent=metric(docs.length,q);
+ clear(list);
+ docs.forEach(d=>{
+ const li=document.createElement('li');
+ li.innerHTML=formatResult(d,terms);
+ list.appendChild(li);
+ });
+ listOut.classList.toggle('hidden',!docs.length);
+ };
+})();