/* ============================================================================ * TIL search — merges the Docusaurus TIL site's results into the WordPress * search-results page. * * The TIL site (/til/, a separate Docusaurus build) uses docusaurus-lunr-search, * which ships a prebuilt lunr 2.3.9 index (/til/lunr-index.json) + a document * store (/til/search-doc.json). This module loads both client-side and queries * them exactly the way the TIL search bar does, so the results match, then * renders a labeled "From TIL" section on the WP search page. * * SELF-CONTAINED / REMOVABLE. Depends only on the vendored global `lunr` * (js/vendor/lunr.min.js) and config in window.dwTilSearch (localized in * functions.php). Fills #dw-til-results (placeholder in templates/search.html). * To remove the feature entirely, delete: * - this file + js/vendor/lunr.min.js * - the "TIL search" enqueue block in functions.php (is_search()) * - the #dw-til-results block in templates/search.html * - the "TIL search results" CSS block in style.css * ==========================================================================*/ (function () { 'use strict'; var cfg = window.dwTilSearch; if (!cfg || !cfg.query || typeof lunr === 'undefined') return; var mount = document.getElementById('dw-til-results'); if (!mount) return; // docusaurus-lunr-search overrides the tokenizer separator; match it so our // client-side tokenizing (and therefore the matches) line up with the index. lunr.tokenizer.separator = /[\s\-/]+/; Promise.all([ fetch(cfg.index).then(function (r) { return r.json(); }), fetch(cfg.docs).then(function (r) { return r.json(); }) ]).then(function (res) { var index = lunr.Index.load(res[0]); var docs = (res[1] && res[1].searchDocs) || []; var input = String(cfg.query); // Same query the plugin's LunrSearchAdapter builds: exact terms (boosted) // plus trailing-wildcard terms for prefix matches. var results = index.query(function (query) { var tokens = lunr.tokenizer(input); query.term(tokens, { boost: 10 }); query.term(tokens, { wildcard: lunr.Query.wildcard.TRAILING }); }); // Map hits to docs, dedupe by URL (a page can match on title + content), // keep lunr's ranking order, cap to maxHits. var seen = {}, hits = []; for (var i = 0; i < results.length && hits.length < cfg.maxHits; i++) { var doc = docs[results[i].ref]; if (!doc || seen[doc.url]) continue; seen[doc.url] = 1; hits.push(doc); } render(hits, input); }).catch(function () { /* TIL index unreachable (e.g. /til not deployed) — skip silently. */ }); function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } // A short excerpt centered on the first query term, with the terms highlighted. function snippet(content, input) { if (!content) return ''; var tokens = lunr.tokenizer(input).map(String); var lc = content.toLowerCase(); var pos = tokens.length ? lc.indexOf(tokens[0]) : -1; var start = pos < 0 ? 0 : Math.max(0, pos - 60); var slice = content.substring(start, start + 200); var out = (start > 0 ? '… ' : '') + esc(slice) + (start + 200 < content.length ? ' …' : ''); tokens.forEach(function (t) { if (!t) return; var re = new RegExp('(' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'ig'); out = out.replace(re, '$1'); }); return out; } function render(hits, input) { if (!hits.length) { mount.hidden = true; return; } var html = ''; mount.innerHTML = html; mount.hidden = false; } })();