til-search.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. /* ============================================================================
  2. * TIL search — merges the Docusaurus TIL site's results into the WordPress
  3. * search-results page.
  4. *
  5. * The TIL site (/til/, a separate Docusaurus build) uses docusaurus-lunr-search,
  6. * which ships a prebuilt lunr 2.3.9 index (/til/lunr-index.json) + a document
  7. * store (/til/search-doc.json). This module loads both client-side and queries
  8. * them exactly the way the TIL search bar does, so the results match, then
  9. * renders a labeled "From TIL" section on the WP search page.
  10. *
  11. * SELF-CONTAINED / REMOVABLE. Depends only on the vendored global `lunr`
  12. * (js/vendor/lunr.min.js) and config in window.dwTilSearch (localized in
  13. * functions.php). Fills #dw-til-results (placeholder in templates/search.html).
  14. * To remove the feature entirely, delete:
  15. * - this file + js/vendor/lunr.min.js
  16. * - the "TIL search" enqueue block in functions.php (is_search())
  17. * - the <!-- wp:html --> #dw-til-results block in templates/search.html
  18. * - the "TIL search results" CSS block in style.css
  19. * ==========================================================================*/
  20. (function () {
  21. 'use strict';
  22. var cfg = window.dwTilSearch;
  23. if (!cfg || !cfg.query || typeof lunr === 'undefined') return;
  24. var mount = document.getElementById('dw-til-results');
  25. if (!mount) return;
  26. // docusaurus-lunr-search overrides the tokenizer separator; match it so our
  27. // client-side tokenizing (and therefore the matches) line up with the index.
  28. lunr.tokenizer.separator = /[\s\-/]+/;
  29. Promise.all([
  30. fetch(cfg.index).then(function (r) { return r.json(); }),
  31. fetch(cfg.docs).then(function (r) { return r.json(); })
  32. ]).then(function (res) {
  33. var index = lunr.Index.load(res[0]);
  34. var docs = (res[1] && res[1].searchDocs) || [];
  35. var input = String(cfg.query);
  36. // Same query the plugin's LunrSearchAdapter builds: exact terms (boosted)
  37. // plus trailing-wildcard terms for prefix matches.
  38. var results = index.query(function (query) {
  39. var tokens = lunr.tokenizer(input);
  40. query.term(tokens, { boost: 10 });
  41. query.term(tokens, { wildcard: lunr.Query.wildcard.TRAILING });
  42. });
  43. // Map hits to docs, dedupe by URL (a page can match on title + content),
  44. // keep lunr's ranking order, cap to maxHits.
  45. var seen = {}, hits = [];
  46. for (var i = 0; i < results.length && hits.length < cfg.maxHits; i++) {
  47. var doc = docs[results[i].ref];
  48. if (!doc || seen[doc.url]) continue;
  49. seen[doc.url] = 1;
  50. hits.push(doc);
  51. }
  52. render(hits, input);
  53. }).catch(function () {
  54. /* TIL index unreachable (e.g. /til not deployed) — skip silently. */
  55. });
  56. function esc(s) {
  57. return String(s).replace(/[&<>"]/g, function (c) {
  58. return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c];
  59. });
  60. }
  61. // A short excerpt centered on the first query term, with the terms highlighted.
  62. function snippet(content, input) {
  63. if (!content) return '';
  64. var tokens = lunr.tokenizer(input).map(String);
  65. var lc = content.toLowerCase();
  66. var pos = tokens.length ? lc.indexOf(tokens[0]) : -1;
  67. var start = pos < 0 ? 0 : Math.max(0, pos - 60);
  68. var slice = content.substring(start, start + 200);
  69. var out = (start > 0 ? '… ' : '') + esc(slice) +
  70. (start + 200 < content.length ? ' …' : '');
  71. tokens.forEach(function (t) {
  72. if (!t) return;
  73. var re = new RegExp('(' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'ig');
  74. out = out.replace(re, '<mark>$1</mark>');
  75. });
  76. return out;
  77. }
  78. function render(hits, input) {
  79. if (!hits.length) { mount.hidden = true; return; }
  80. var html = '<ul class="dw-til-list">';
  81. hits.forEach(function (d) {
  82. html += '<li class="dw-til-item">' +
  83. '<a class="dw-til-link" href="' + esc(d.url) + '">' + esc(d.title) + '</a>' +
  84. '<p class="dw-til-snippet">' + snippet(d.content, input) + '</p>' +
  85. '</li>';
  86. });
  87. html += '</ul>';
  88. mount.innerHTML = html;
  89. mount.hidden = false;
  90. }
  91. })();