1(function () { 2 var overlay = document.getElementById('search-overlay'); 3 var container = document.getElementById('search-container'); 4 if (!overlay || !container) return; 5 6 var instance = null; 7 var suggestions = []; 8 var dropdown = null; 9 var activeIdx = -1; 10 var input = null; 11 var suppressHide = false; 12 13 // Build suggestions array from datalist, then remove it 14 var datalist = document.getElementById('search-suggestions'); 15 if (datalist) { 16 var opts = datalist.querySelectorAll('option'); 17 for (var i = 0; i < opts.length; i++) { 18 if (opts[i].value) suggestions.push(opts[i].value); 19 } 20 datalist.remove(); 21 } 22 23 var pageLang = document.documentElement.dataset.lang || document.documentElement.lang || 'en'; 24 var isLatinQuery = function (q) { return /^[\x20-\x7E]+$/.test(q); }; 25 var isLatinTerm = function (t) { return /^[\x20-\x7E]/.test(t); }; 26 27 // Detect whether Fuse.js should be used (zh/ja pages with Fuse loaded) 28 var useFuse = typeof Fuse !== 'undefined' && (pageLang === 'zh' || pageLang === 'ja'); 29 30 // Fuse.js state 31 var fuseIndex = null; // Fuse instance for full-text (searches subsections) 32 var fuseFlat = null; // flattened subsection array (source of truth for exact scan) 33 var fuseSuggestions = null; // Fuse instance for autocomplete terms 34 var fuseLoading = false; 35 var fuseLoaded = false; 36 var fuseSearchInput = null; 37 var fuseResultsEl = null; 38 var fuseMessageEl = null; 39 var fuseDebounceTimer = null; 40 var enFlat = null; // en edition flattened subsections (lazy-loaded) 41 var enFailed = false; 42 var enToken = 0; // stale-input guard for async en fallback 43 var enPromise = null; // cached in-flight en-index fetch promise 44 45 var SEARCH_SUBMIT_SVG = '\u2728'; 46 47 function getSearchInput() { 48 return ( 49 fuseSearchInput || 50 input || 51 (container && container.querySelector('.pagefind-ui__search-input')) 52 ); 53 } 54 55 function createSearchSubmitButton() { 56 var btn = document.createElement('button'); 57 btn.type = 'button'; 58 btn.className = 'plurality-search__submit'; 59 btn.setAttribute('aria-label', (t && t.search_label) || 'Search'); 60 btn.innerHTML = SEARCH_SUBMIT_SVG; 61 btn.addEventListener('click', function () { 62 submitSearch(); 63 }); 64 return btn; 65 } 66 67 function submitSearch() { 68 var inp = getSearchInput(); 69 if (!inp) return; 70 var q = inp.value.trim(); 71 if (dropdown) dropdown.hidden = true; 72 activeIdx = -1; 73 inp.removeAttribute('aria-activedescendant'); 74 if (!q) { 75 if (useFuse && fuseResultsEl && fuseMessageEl) { 76 fuseResultsEl.innerHTML = ''; 77 fuseMessageEl.textContent = ''; 78 } 79 return; 80 } 81 if (window.PluralityBookAsk && typeof window.PluralityBookAsk.runAsk === 'function') { 82 window.PluralityBookAsk.runAsk(q).then(function () { 83 window.dispatchEvent( 84 new CustomEvent('plurality-search-after-ask', { detail: { query: q } }), 85 ); 86 }); 87 } else { 88 runKeywordSearchAfterAsk(q); 89 } 90 } 91 92 function wrapSearchFormRow(form) { 93 if (!form || form.querySelector('.plurality-search__row')) return; 94 var inp = form.querySelector('.pagefind-ui__search-input, input[type="search"], input[type="text"]'); 95 if (!inp) return; 96 var row = document.createElement('div'); 97 row.className = 'plurality-search__row'; 98 var wrap = document.createElement('div'); 99 wrap.className = 'plurality-search__input-wrap'; 100 form.insertBefore(row, inp); 101 wrap.appendChild(inp); 102 row.appendChild(wrap); 103 row.appendChild(createSearchSubmitButton()); 104 var clearBtn = form.querySelector('.pagefind-ui__search-clear'); 105 if (clearBtn) clearBtn.remove(); 106 } 107 108 109 var uiTranslations = { 110 en: { 111 placeholder: "Search the book\u2026", 112 zero_results: "No results for [SEARCH_TERM]", 113 exact_summary: "[EXACT] exact · [NEAR] near matches for [SEARCH_TERM]", 114 exact_only: "[EXACT] exact matches for [SEARCH_TERM]", 115 exact_badge: "Exact", 116 en_fallback: "[COUNT] exact matches in the English edition" 117 }, 118 zh: { 119 placeholder: "\u641c\u5c0b\u672c\u66f8\u2026", 120 zero_results: "\u627e\u4e0d\u5230 [SEARCH_TERM] \u7684\u76f8\u95dc\u7d50\u679c", 121 many_results: "\u627e\u5230 [COUNT] \u500b [SEARCH_TERM] \u7684\u76f8\u95dc\u7d50\u679c", 122 one_result: "\u627e\u5230 [COUNT] \u500b [SEARCH_TERM] \u7684\u76f8\u95dc\u7d50\u679c", 123 search_label: "\u641c\u5c0b", 124 clear_search: "\u6e05\u9664", 125 load_more: "\u8f09\u5165\u66f4\u591a", 126 searching: "\u641c\u5c0b [SEARCH_TERM] \u4e2d\u2026", 127 exact_summary: "ã[SEARCH_TERM]ãå®å ¨ç¬¦å [EXACT] çã»ç¸è¿çµæ [NEAR] ç", 128 exact_only: "ã[SEARCH_TERM]ãå®å ¨ç¬¦å [EXACT] ç", 129 exact_badge: "å®å ¨ç¬¦å", 130 en_fallback: "è±æçå®å ¨ç¬¦å [COUNT] ç" 131 }, 132 ja: { 133 placeholder: "\u672c\u3092\u691c\u7d22\u2026",
134 zero_results: "[SEARCH_TERM] ã«è¿ãæ¤ç´¢çµæã¯ããã¾ãã", 135 many_results: "[SEARCH_TERM] ã«è¿ãæ¤ç´¢çµæ [COUNT] ä»¶", 136 one_result: "[SEARCH_TERM] ã«è¿ãæ¤ç´¢çµæ [COUNT] ä»¶", 137 search_label: "\u691c\u7d22", 138 clear_search: "\u30af\u30ea\u30a2", 139 load_more: "\u3082\u3063\u3068\u898b\u308b", 140 searching: "[SEARCH_TERM] \u3092\u691c\u7d22\u4e2d\u2026", 141 exact_summary: "ã[SEARCH_TERM]ãå®å ¨ä¸è´ [EXACT] ä»¶ã»è¿ãçµæ [NEAR] ä»¶", 142 exact_only: "ã[SEARCH_TERM]ãå®å ¨ä¸è´ [EXACT] ä»¶", 143 exact_badge: "å®å ¨ä¸è´", 144 en_fallback: "è±èªçã§ã®å®å ¨ä¸è´ [COUNT] ä»¶" 145 }, 146 de: { 147 placeholder: "Buch durchsuchen\u2026", 148 zero_results: "Keine Ergebnisse f\u00fcr [SEARCH_TERM]", 149 many_results: "[COUNT] Ergebnisse f\u00fcr [SEARCH_TERM]", 150 one_result: "[COUNT] Ergebnis f\u00fcr [SEARCH_TERM]", 151 search_label: "Suche", 152 clear_search: "L\u00f6schen", 153 load_more: "Mehr laden", 154 searching: "Suche nach [SEARCH_TERM]\u2026" 155 }, 156 th: { 157 placeholder: "\u0e04\u0e49\u0e19\u0e2b\u0e32\u0e43\u0e19\u0e2b\u0e19\u0e31\u0e07\u0e2a\u0e37\u0e2d\u2026", 158 zero_results: "\u0e44\u0e21\u0e48\u0e1e\u0e1a\u0e1c\u0e25\u0e25\u0e31\u0e1e\u0e18\u0e4c\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a [SEARCH_TERM]", 159 many_results: "\u0e1e\u0e1a [COUNT] \u0e1c\u0e25\u0e25\u0e31\u0e1e\u0e18\u0e4c\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a [SEARCH_TERM]", 160 one_result: "\u0e1e\u0e1a [COUNT] \u0e1c\u0e25\u0e25\u0e31\u0e1e\u0e18\u0e4c\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a [SEARCH_TERM]", 161 search_label: "\u0e04\u0e49\u0e19\u0e2b\u0e32", 162 clear_search: "\u0e25\u0e49\u0e32\u0e07", 163 load_more: "\u0e42\u0e2b\u0e25\u0e14\u0e40\u0e1e\u0e34\u0e48\u0e21\u0e40\u0e15\u0e34\u0e21", 164 searching: "\u0e01\u0e33\u0e25\u0e31\u0e07\u0e04\u0e49\u0e19\u0e2b\u0e32 [SEARCH_TERM]\u2026" 165 }, 166 el: { 167 placeholder: "\u0391\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 \u03c3\u03c4\u03bf \u03b2\u03b9\u03b2\u03bb\u03af\u03bf\u2026", 168 zero_results: "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03b1\u03c0\u03bf\u03c4\u03b5\u03bb\u03ad\u03c3\u03bc\u03b1\u03c4\u03b1 \u03b3\u03b9\u03b1 [SEARCH_TERM]", 169 many_results: "\u0392\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd [COUNT] \u03b1\u03c0\u03bf\u03c4\u03b5\u03bb\u03ad\u03c3\u03bc\u03b1\u03c4\u03b1 \u03b3\u03b9\u03b1 [SEARCH_TERM]", 170 one_result: "\u0392\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 [COUNT] \u03b1\u03c0\u03bf\u03c4\u03ad\u03bb\u03b5\u03c3\u03bc\u03b1 \u03b3\u03b9\u03b1 [SEARCH_TERM]", 171 search_label: "\u0391\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7", 172 clear_search: "\u039a\u03b1\u03b8\u03b1\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2", 173 load_more: "\u03a0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1", 174 searching: "\u0391\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 [SEARCH_TERM]\u2026" 175 } 176 }; 177 178 var t = uiTranslations[pageLang] || uiTranslations.en; 179 180 // ââ Matching helpers (used for non-Fuse autocomplete) ââ 181 182 function matchTerm(term, query) { 183 var q = query.toLowerCase(); 184 var tl = term.toLowerCase(); 185 if (!isLatinQuery(q)) { 186 return tl.indexOf(q) !== -1; 187 } 188 var words = tl.split(/[\s/(-]+/); 189 for (var i = 0; i < words.length; i++) { 190 if (words[i].indexOf(q) === 0) return true; 191 } 192 return false; 193 } 194 195 function sortMatches(matches) { 196 if (pageLang === 'en') return matches; 197 return matches.sort(function (a, b) { 198 var aLatin = isLatinTerm(a); 199 var bLatin = isLatinTerm(b); 200 if (aLatin === bLatin) return 0; 201 return aLatin ? 1 : -1; 202 }); 203 } 204 205 // ââ Autocomplete dropdown ââ 206 207 function positionDropdown() { 208 if (!dropdown || !input) return; 209 var rect = input.getBoundingClientRect(); 210 var overlayRect = overlay.getBoundingClientRect(); 211 dropdown.style.top = (rect.bottom - overlayRect.top + 4) + 'px'; 212 dropdown.style.left = (rect.left - overlayRect.left) + 'px'; 213 dropdown.style.width = rect.width + 'px'; 214 } 215 216 function showSuggestions(query) { 217 if (!dropdown) return; 218 activeIdx = -1; 219 dropdown.innerHTML = ''; 220 221 if (!query || query.length < 1) { 222 dropdown.hidden = true; 223 if (input) input.removeAttribute('aria-activedescendant'); 224 return; 225 } 226 227 var matches; 228 if (useFuse && fuseSuggestions) { 229 var fuseResults = fuseSuggestions.search(query, { limit: 20 }); 230 var raw = []; 231 for (var i = 0; i < fuseResults.length; i++) { 232 raw.push(fuseResults[i].item); 233 } 234 matches = sortMatches(raw).slice(0, 8); 235 } else { 236 var raw = []; 237 for (var i = 0; i < suggestions.length && raw.length < 20; i++) {
238 if (matchTerm(suggestions[i], query)) raw.push(suggestions[i]); 239 } 240 matches = sortMatches(raw).slice(0, 8); 241 } 242 243 if (matches.length === 0) { 244 dropdown.hidden = true; 245 if (input) input.removeAttribute('aria-activedescendant'); 246 return; 247 } 248 249 for (var j = 0; j < matches.length; j++) { 250 var li = document.createElement('li'); 251 li.className = 'search-suggest__item'; 252 li.setAttribute('role', 'option'); 253 li.id = 'suggest-' + j; 254 li.textContent = matches[j]; 255 li.addEventListener('mousedown', (function (val) { 256 return function (e) { 257 e.preventDefault(); 258 e.stopPropagation(); 259 suppressHide = true; 260 selectSuggestion(val); 261 }; 262 })(matches[j])); 263 dropdown.appendChild(li); 264 } 265 266 positionDropdown(); 267 dropdown.hidden = false; 268 } 269 270 function selectSuggestion(value) { 271 if (!input) return; 272 Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(input, value); 273 input.dispatchEvent(new Event('input', { bubbles: true })); 274 dropdown.hidden = true; 275 activeIdx = -1; 276 input.removeAttribute('aria-activedescendant'); 277 input.focus(); 278 suppressHide = false;
279 } 280 281 function handleKeydown(e) { 282 if (!dropdown || dropdown.hidden) return; 283 var items = dropdown.querySelectorAll('.search-suggest__item'); 284 if (!items.length) return; 285 286 if (e.key === 'ArrowDown') { 287 e.preventDefault(); 288 activeIdx = Math.min(activeIdx + 1, items.length - 1); 289 updateActive(items); 290 } else if (e.key === 'ArrowUp') { 291 e.preventDefault(); 292 activeIdx = Math.max(activeIdx - 1, -1); 293 updateActive(items); 294 } else if (e.key === 'Enter' && activeIdx >= 0) { 295 e.preventDefault(); 296 selectSuggestion(items[activeIdx].textContent); 297 } else if (e.key === 'Enter') { 298 e.preventDefault(); 299 submitSearch(); 300 } 301 } 302 303 function updateActive(items) { 304 for (var i = 0; i < items.length; i++) { 305 items[i].classList.toggle('search-suggest__item--active', i === activeIdx); 306 } 307 if (input) { 308 if (activeIdx >= 0) { 309 input.setAttribute('aria-activedescendant', 'suggest-' + activeIdx); 310 items[activeIdx].scrollIntoView({ block: 'nearest' }); 311 } else { 312 input.removeAttribute('aria-activedescendant'); 313 } 314 } 315 } 316 317 function initDropdown() { 318 dropdown = document.createElement('ul'); 319 dropdown.className = 'search-suggest'; 320 dropdown.setAttribute('role', 'listbox'); 321 dropdown.id = 'search-suggest-list'; 322 dropdown.hidden = true; 323 overlay.appendChild(dropdown); 324 } 325 326 // ââ Fuse.js helpers ââ 327 328 function escapeHtml(str) { 329 return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); 330 } 331 332 // Strip CJK punctuation for normalized matching 333 // Handles ã»ãï¼ãï¼ï¼ï¼ï¼ããããï¼ï¼ããâ¦âï½ etc. 334 var cjkPunct = /[ã»\u30FBãï¼ãï¼ï¼ï¼ï¼ããããï¼ï¼ããããâ¦âï½Â·\u00B7]/g; 335 336 // Build a position map: for each char in the normalized string, 337 // store its original index in the source string 338 function buildNormMap(str) { 339 var norm = ''; 340 var map = []; 341 for (var i = 0; i < str.length; i++) { 342 if (!cjkPunct.test(str[i])) { 343 map.push(i); 344 norm += str[i]; 345 } 346 cjkPunct.lastIndex = 0; // reset regex state 347 } 348 return { norm: norm, map: map }; 349 } 350 351 function normalizeForExact(s) { 352 var out = String(s || '').toLowerCase().replace(cjkPunct, ''); 353 cjkPunct.lastIndex = 0; 354 return out; 355 } 356 357 function scanExact(flatArr, query) { 358 var nq = normalizeForExact(query); 359 if (!nq) return []; 360 var headingHits = []; 361 var contentHits = []; 362 for (var i = 0; i < flatArr.length; i++) { 363 var item = flatArr[i]; 364 if (!item.heading) continue; // matches render-skip for intro/endorsement blobs 365 if (normalizeForExact(item.heading).indexOf(nq) !== -1 || normalizeForExact(item.chapterTitle).indexOf(nq) !== -1) { 366 headingHits.push(i); 367 } else if (normalizeForExact(item.content).indexOf(nq) !== -1) { 368 contentHits.push(i); 369 } 370 } 371 return headingHits.concat(contentHits); 372 } 373 374 function buildExcerpt(content, query) { 375 if (!content) return ''; 376 var ctxChars = 40; 377 378 // 1. Try exact substring match first 379 var lowerContent = content.toLowerCase(); 380 var lowerQuery = query.toLowerCase(); 381 var idx = lowerContent.indexOf(lowerQuery); 382 383 if (idx !== -1) { 384 var start = Math.max(0, idx - ctxChars); 385 var end = Math.min(content.length, idx + query.length + ctxChars); 386 return (start > 0 ? '\u2026' : '') + 387 escapeHtml(content.substring(start, idx)) + 388 '<mark>' + escapeHtml(content.substring(idx, idx + query.length)) + '</mark>' + 389 escapeHtml(content.substring(idx + query.length, end)) + 390 (end < content.length ? '\u2026' : ''); 391 } 392 393 // 2. Try normalized match (strip CJK punctuation from both sides) 394 var info = buildNormMap(lowerContent); 395 var normQuery = lowerQuery.replace(cjkPunct, ''); 396 cjkPunct.lastIndex = 0; 397 var normIdx = info.norm.indexOf(normQuery); 398 399 if (normIdx !== -1 && normQuery.length > 0) { 400 // Map back to original content positions 401 var origStart = info.map[normIdx]; 402 var origEnd = info.map[normIdx + normQuery.length - 1] + 1; 403 var winStart = Math.max(0, origStart - ctxChars); 404 var winEnd = Math.min(content.length, origEnd + ctxChars); 405 return (winStart > 0 ? '\u2026' : '') + 406 escapeHtml(content.substring(winStart, origStart)) + 407 '<mark>' + escapeHtml(content.substring(origStart, origEnd)) + '</mark>' + 408 escapeHtml(content.substring(origEnd, winEnd)) + 409 (winEnd < content.length ? '\u2026' : ''); 410 } 411 412 // 3. Fallback: show beginning of content 413 var slice = content.substring(0, 100); 414 return escapeHtml(slice) + (content.length > 100 ? '\u2026' : ''); 415 } 416 417 function formatMessage(template, term, counts) { 418 var count = (counts && counts.count) || 0; 419 var exact = (counts && counts.exact) || 0; 420 var near = (counts && counts.near) || 0; 421 return template 422 .replace('[COUNT]', count) 423 .replace('[EXACT]', exact) 424 .replace('[NEAR]', near) 425 .replace('[SEARCH_TERM]', term); 426 } 427
428 var pendingKeywordAfterAsk = null; 429 430 function runKeywordSearchAfterAsk(q) { 431 if (!q) return; 432 if (useFuse) { 433 if (fuseSearchInput) { 434 Object.getOwnPropertyDescriptor( 435 HTMLInputElement.prototype, 436 'value', 437 ).set.call(fuseSearchInput, q); 438 } 439 if (fuseIndex && fuseResultsEl) { 440 renderFuseResults(q); 441 } else { 442 pendingKeywordAfterAsk = q; 443 } 444 return; 445 } 446 var pfInput = container.querySelector('input'); 447 if (pfInput) { 448 Object.getOwnPropertyDescriptor( 449 HTMLInputElement.prototype, 450 'value', 451 ).set.call(pfInput, q); 452 pfInput.dispatchEvent(new Event('input', { bubbles: true })); 453 } 454 } 455 456 window.addEventListener('plurality-search-after-ask', function (ev) { 457 var q = ev.detail && ev.detail.query; 458 runKeywordSearchAfterAsk(q); 459 }); 460 // Render one chapter group's HTML (reused by renderFuseResults and en fallback) 461 function renderGroupHtml(ch, query) { 462 var html = ''; 463 html += '<li class="pagefind-ui__result">'; 464 html += '<div class="pagefind-ui__result-inner">'; 465 466 // Main chapter title 467 html += '<p class="pagefind-ui__result-title">'; 468 html += '<a class="pagefind-ui__result-link" href="' + escapeHtml(ch.url) + '">'; 469 html += escapeHtml(ch.title); 470 html += '</a>'; 471 html += '</p>'; 472 473 // Sub-results (subsection anchors) 474 for (var s = 0; s < ch.subResults.length; s++) { 475 var sub = ch.subResults[s]; 476 if (!sub.heading) continue;
476 // skip intro sections without headings 477 478 var subUrl = ch.url + '#' + sub.anchor; 479 var subExcerpt = buildExcerpt(sub.content, query); 480 481 html += '<div class="pagefind-ui__result-nested">'; 482 html += '<p class="pagefind-ui__result-title">'; 483 html += '<a class="pagefind-ui__result-link" href="' + escapeHtml(subUrl) + '">'; 484 html += escapeHtml(sub.heading); 485 html += '</a>'; 486 if (sub.exact) { 487 html += '<span class="plurality-search__badge">' + escapeHtml(t.exact_badge) + '</span>'; 488 } 489 html += '</p>'; 490 html += '<p class="pagefind-ui__result-excerpt">' + subExcerpt + '</p>'; 491 html += '</div>'; 492 } 493 494 // Section tag (matches PageFind's metadata tags) 495 html += '<ul class="pagefind-ui__result-tags">'; 496 html += '<li class="pagefind-ui__result-tag">Section: ' + escapeHtml(ch.section) + '</li>'; 497 html += '</ul>'; 498 499 html += '</div>'; 500 html += '</li>'; 501 return html; 502 } 503 504 // Search the flat subsection index: exact substring hits first, then Fuse fuzzy results 505 function renderFuseResults(query) { 506 if (!fuseIndex || !fuseResultsEl || !fuseMessageEl) return; 507 enToken++; // invalidate any pending en-fallback callback 508 509 if (!query || !query.trim()) { 510 fuseMessageEl.textContent = ''; 511 fuseResultsEl.innerHTML = ''; 512 return; 513 } 514 515 var exactIdx = scanExact(fuseFlat, query); 516 var exactSet = {}; 517 for (var e = 0; e < exactIdx.length; e++) exactSet[exactIdx[e]] = true; 518 519 var fuzzyResults = fuseIndex.search(query, { limit: 40 }); 520 var fuzzy = []; 521 for (var f = 0; f < fuzzyResults.length; f++) { 522 if (exactSet[fuzzyResults[f].refIndex]) continue; 523 fuzzy.push(fuzzyResults[f]); 524 } 525 526 // Build one ordered list: exact items first, then fuzzy 527 var ordered = []; 528 for (var ei = 0; ei < exactIdx.length; ei++) { 529 ordered.push({ item: fuseFlat[exactIdx[ei]], exact: true }); 530 } 531 for (var fi = 0; fi < fuzzy.length; fi++) { 532 ordered.push({ item: fuzzy[fi].item, exact: false }); 533 } 534 535 // Group results by chapter URL (first-seen group order) 536 var grouped = []; 537 var chapterMap = {}; 538 for (var i = 0; i < ordered.length; i++) { 539 var item = ordered[i].item; 540 var key = item.chapterUrl; 541 if (!chapterMap[key]) { 542 chapterMap[key] = { 543 title: item.chapterTitle, 544 section: item.chapterSection, 545 url: item.chapterUrl, 546 subResults: [], 547 }; 548 grouped.push(chapterMap[key]); 549 } 550 // Max 3 sub-results per chapter (matches PageFind behavior) 551 if (chapterMap[key].subResults.length < 3) { 552 chapterMap[key].subResults.push({ 553 heading: item.heading, 554 anchor: item.anchor, 555 content: item.content, 556 exact: ordered[i].exact, 557 }); 558 } 559 } 560 561 // Limit to 10 chapter-level results 562 grouped = grouped.slice(0, 10); 563 564 // Counts (subsection-level, pre-cap) 565 var EXACT = exactIdx.length; 566 var NEAR = fuzzy.length; 567 var count = grouped.length; 568 569 var msgTemplate; 570 if (EXACT > 0 && NEAR > 0) { 571 msgTemplate = t.exact_summary || uiTranslations.en.exact_summary; 572 } else if (EXACT > 0 && NEAR === 0) { 573 msgTemplate = t.exact_only || uiTranslations.en.exact_only; 574 } else if (count === 0) { 575 msgTemplate = t.zero_results || 'No results for [SEARCH_TERM]'; 576 } else if (count === 1) { 577 msgTemplate = t.one_result || t.many_results || '[COUNT] results for [SEARCH_TERM]'; 578 } else { 579 msgTemplate = t.many_results || '[COUNT] results for [SEARCH_TERM]'; 580 } 581 fuseMessageEl.textContent = formatMessage(msgTemplate, query, { count: count, exact: EXACT, near: NEAR }); 582 583 // Render results 584 var html = ''; 585 for (var g = 0; g < grouped.length; g++) { 586 html += renderGroupHtml(grouped[g], query); 587 } 588 fuseResultsEl.innerHTML = html; 589 590 // English exact-fallback tier: Latin queries with zero local exact hits 591 if (exactIdx.length === 0 && isLatinQuery(query) && query.length >= 3 && pageLang !== 'en') { 592 var token = ++enToken; 593 ensureEnIndex().then(function (flatEn) { 594 if (token !== enToken || !flatEn) return; 595 var enHits = scanExact(flatEn, query); 596 if (!enHits.length) return; 597 var enGrouped = []; 598 var enChapterMap = {}; 599 for (var ei = 0; ei < enHits.length; ei++) { 600 var enItem = flatEn[enHits[ei]]; 601 var enKey = enItem.chapterUrl; 602 if (!enChapterMap[enKey]) { 603 enChapterMap[enKey] = { 604 title: enItem.chapterTitle, 605 section: enItem.chapterSection, 606 url: enItem.chapterUrl, 607 subResults: [], 608 }; 609 enGrouped.push(enChapterMap[enKey]); 610 } 611 if (enChapterMap[enKey].subResults.length < 3) { 612 enChapterMap[enKey].subResults.push({ 613 heading: enItem.heading, 614 anchor: enItem.anchor, 615 content: enItem.content, 616 exact: true, 617 }); 618 } 619 } 620 enGrouped = enGrouped.slice(0, 5); 621 var enHtml = '<li class="plurality-search__en-note">' + 622 escapeHtml(formatMessage(t.en_fallback || uiTranslations.en.en_fallback, query, { count: enHits.length })) + 623 '</li>'; 624 for (var eg = 0; eg < enGrouped.length; eg++) { 625 enHtml += renderGroupHtml(enGrouped[eg], query); 626 } 627 fuseResultsEl.innerHTML = fuseResultsEl.innerHTML + enHtml; 628 }); 629 } 630 } 631
632 function flattenChapters(data) { 633 var flat = []; 634 for (var c = 0; c < data.length; c++) { 635 var ch = data[c]; 636 var subs = ch.subsections || []; 637 for (var s = 0; s < subs.length; s++) { 638 flat.push({ 639 chapterTitle: ch.title, 640 chapterSection: ch.section, 641 chapterUrl: ch.url, 642 heading: subs[s].heading, 643 anchor: subs[s].anchor, 644 content: subs[s].content, 645 }); 646 } 647 } 648 return flat; 649 } 650 651 function ensureEnIndex() { 652 if (enFlat) return Promise.resolve(enFlat); 653 if (enFailed) return Promise.resolve(null); 654 if (enPromise) return enPromise; 655 enPromise = fetch('/search-index.json') 656 .then(function (res) { 657 if (!res.ok) throw new Error('HTTP ' + res.status); 658 return res.json(); 659 }) 660 .then(function (data) { 661 enFlat = flattenChapters(data); 662 return enFlat; 663 }) 664 .catch(function () { 665 enFailed = true; 666 return null; 667 }); 668 return enPromise; 669 } 670 671 function loadFuseIndex() { 672 if (fuseLoaded || fuseLoading) return; 673 fuseLoading = true; 674 if (fuseMessageEl) { 675 fuseMessageEl.textContent = formatMessage(t.searching || 'Loading\u2026', '', null); 676 } 677 fetch('/' + pageLang + '/search-index.json') 678 .then(function (res) { 679 if (!res.ok) throw new Error('HTTP ' + res.status); 680 return res.json(); 681 }) 682 .then(function (data) { 683 fuseFlat = flattenChapters(data); 684 685 fuseIndex = new Fuse(fuseFlat, { 686 keys: [ 687 { name: 'heading', weight: 2 }, 688 { name: 'content', weight: 1 }, 689 { name: 'chapterTitle', weight: 1.5 }, 690 ], 691 includeScore: true, 692 includeMatches: true, 693 ignoreLocation: true, 694 threshold: 0.3, 695 minMatchCharLength: 1, 696 findAllMatches: true, 697 }); 698 fuseLoaded = true; 699 fuseLoading = false; 700 if (fuseMessageEl) fuseMessageEl.textContent = ''; 701 if (pendingKeywordAfterAsk) { 702 var pending = pendingKeywordAfterAsk; 703 pendingKeywordAfterAsk = null; 704 runKeywordSearchAfterAsk(pending); 705 } else if (fuseSearchInput && fuseSearchInput.value.trim()) { 706 renderFuseResults(fuseSearchInput.value.trim()); 707 } 708 }) 709 .catch(function () { 710 fuseLoading = false; 711 useFuse = false; 712 initPagefind(); 713 }); 714 } 715 716 function buildFuseUI() { 717 container.innerHTML = ''; 718 719 var wrapper = document.createElement('div'); 720 wrapper.className = 'pagefind-ui pagefind-ui--fuse'; 721 722 var form = document.createElement('form'); 723 form.className = 'pagefind-ui__form'; 724 form.setAttribute('role', 'search'); 725 form.addEventListener('submit', function (e) { e.preventDefault(); submitSearch(); }); 726 727 fuseSearchInput = document.createElement('input'); 728 fuseSearchInput.className = 'pagefind-ui__search-input'; 729 fuseSearchInput.type = 'search'; 730 fuseSearchInput.setAttribute('enterkeyhint', 'search'); 731 fuseSearchInput.placeholder = t.placeholder || 'Search\u2026'; 732 733 form.appendChild(fuseSearchInput); 734 wrapSearchFormRow(form); 735 736 var drawer = document.createElement('div'); 737 drawer.className = 'pagefind-ui__drawer'; 738 739 fuseMessageEl = document.createElement('div'); 740 fuseMessageEl.className = 'pagefind-ui__message'; 741 742 fuseResultsEl = document.createElement('ol'); 743 fuseResultsEl.className = 'pagefind-ui__results'; 744 745 drawer.appendChild(fuseMessageEl); 746 drawer.appendChild(fuseResultsEl); 747 748 wrapper.appendChild(form); 749 wrapper.appendChild(drawer); 750 container.appendChild(wrapper); 751 752 // Input handler with debounce for full-text, immediate for autocomplete 753 fuseSearchInput.addEventListener('input', function () { 754 var val = fuseSearchInput.value; 755 showSuggestions(val); 756 clearTimeout(fuseDebounceTimer); 757 fuseDebounceTimer = setTimeout(function () { 758 renderFuseResults(val.trim()); 759 }, 200); 760 }); 761 762 input = fuseSearchInput; 763 764 fuseSearchInput.addEventListener('keydown', handleKeydown); 765 fuseSearchInput.addEventListener('focus', function () { 766 if (fuseSearchInput.value.length >= 1) showSuggestions(fuseSearchInput.value); 767 }); 768 fuseSearchInput.addEventListener('blur', function () { 769 if (suppressHide) return; 770 setTimeout(function () { 771 if (dropdown) dropdown.hidden = true; 772 }, 200); 773 }); 774 } 775 776 function initPagefind() { 777 if (instance) return; 778 var opts = { 779 element: container, 780 showSubResults: true, 781 showImages: false, 782 translations: t 783 }; 784 // Pre-filter to current language so the filter panel doesn't show 785 opts.mergeFilter = {}; 786 opts.mergeFilter.lang = [pageLang]; 787 instance = new PagefindUI(opts); 788 } 789 790 function initFuseSuggestions() { 791 if (fuseSuggestions || !suggestions.length) return; 792 fuseSuggestions = new Fuse(suggestions, { 793 includeScore: true, 794 ignoreLocation: true, 795 threshold: 0.3, 796 minMatchCharLength: 1, 797 }); 798 } 799 800 // ââ Open / Close ââ 801 802 function open() { 803 overlay.classList.add('active'); 804 overlay.setAttribute('aria-hidden', 'false'); 805 document.body.style.overflow = 'hidden'; 806 807 if (useFuse) { 808 if (!fuseSearchInput) { 809 buildFuseUI(); 810 initFuseSuggestions(); 811 if (!dropdown && suggestions.length) { 812 initDropdown(); 813 } 814 if (input && suggestions.length) { 815 input.setAttribute('role', 'combobox'); 816 input.setAttribute('aria-autocomplete', 'list'); 817 input.setAttribute('aria-controls', 'search-suggest-list'); 818 input.setAttribute('autocomplete', 'off'); 819 } 820 loadFuseIndex(); 821 } 822 setTimeout(function () { 823 if (fuseSearchInput) fuseSearchInput.focus(); 824 }, 50); 825 } else { 826 if (!instance) { 827 initPagefind(); 828 } 829 setTimeout(function () { 830 input = container.querySelector('input'); 831 if (input && suggestions.length && !dropdown) { 832 initDropdown(); 833 834 input.setAttribute('role', 'combobox'); 835 input.setAttribute('aria-autocomplete', 'list'); 836 input.setAttribute('aria-controls', 'search-suggest-list'); 837 input.setAttribute('autocomplete', 'off'); 838 839 input.addEventListener('input', function () { 840 showSuggestions(input.value); 841 }); 842 input.addEventListener('keydown', handleKeydown); 843 input.addEventListener('focus', function () { 844 if (input.value.length >= 1) showSuggestions(input.value); 845 }); 846 input.addEventListener('blur', function () { 847 if (suppressHide) return; 848 setTimeout(function () { 849 if (dropdown) dropdown.hidden = true; 850 }, 200); 851 }); 852 } 853 var pfForm = container.querySelector('.pagefind-ui__form'); 854 if (pfForm) wrapSearchFormRow(pfForm); 855 input = getSearchInput(); 856 if (input) input.focus(); 857 }, 100); 858 } 859 } 860 861 function close() { 862 overlay.classList.remove('active'); 863 overlay.setAttribute('aria-hidden', 'true'); 864 document.body.style.overflow = ''; 865 if (dropdown) dropdown.hidden = true; 866 } 867 868 document.querySelectorAll('.search-toggle').forEach(function (btn) { 869 btn.addEventListener('click', open); 870 }); 871 872 overlay.addEventListener('click', function (e) { 873 if (e.target === overlay) close(); 874 }); 875 876 window.PluralitySearch = { submit: submitSearch }; 877 878 document.addEventListener('keydown', function (e) { 879 if ((e.metaKey || e.ctrlKey) && e.key === 'k') { 880 e.preventDefault(); 881 if (overlay.classList.contains('active')) close(); else open(); 882 } 883 if (e.key === 'Escape' && overlay.classList.contains('active')) { 884 close(); 885 } 886 }); 887})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.