You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

181 lines
4.8KB

  1. function debounce(func, wait) {
  2. var timeout;
  3. return function () {
  4. var context = this;
  5. var args = arguments;
  6. clearTimeout(timeout);
  7. timeout = setTimeout(function () {
  8. timeout = null;
  9. func.apply(context, args);
  10. }, wait);
  11. };
  12. }
  13. // Taken from mdbook
  14. // The strategy is as follows:
  15. // First, assign a value to each word in the document:
  16. // Words that correspond to search terms (stemmer aware): 40
  17. // Normal words: 2
  18. // First word in a sentence: 8
  19. // Then use a sliding window with a constant number of words and count the
  20. // sum of the values of the words within the window. Then use the window that got the
  21. // maximum sum. If there are multiple maximas, then get the last one.
  22. // Enclose the terms in <b>.
  23. function makeTeaser(body, terms) {
  24. var TERM_WEIGHT = 40;
  25. var NORMAL_WORD_WEIGHT = 2;
  26. var FIRST_WORD_WEIGHT = 8;
  27. var TEASER_MAX_WORDS = 30;
  28. var stemmedTerms = terms.map(function (w) {
  29. return elasticlunr.stemmer(w.toLowerCase());
  30. });
  31. var termFound = false;
  32. var index = 0;
  33. var weighted = []; // contains elements of ["word", weight, index_in_document]
  34. // split in sentences, then words
  35. var sentences = body.toLowerCase().split(". ");
  36. for (var i in sentences) {
  37. var words = sentences[i].split(" ");
  38. var value = FIRST_WORD_WEIGHT;
  39. for (var j in words) {
  40. var word = words[j];
  41. if (word.length > 0) {
  42. for (var k in stemmedTerms) {
  43. if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {
  44. value = TERM_WEIGHT;
  45. termFound = true;
  46. }
  47. }
  48. weighted.push([word, value, index]);
  49. value = NORMAL_WORD_WEIGHT;
  50. }
  51. index += word.length;
  52. index += 1; // ' ' or '.' if last word in sentence
  53. }
  54. index += 1; // because we split at a two-char boundary '. '
  55. }
  56. if (weighted.length === 0) {
  57. return body;
  58. }
  59. var windowWeights = [];
  60. var windowSize = Math.min(weighted.length, TEASER_MAX_WORDS);
  61. // We add a window with all the weights first
  62. var curSum = 0;
  63. for (var i = 0; i < windowSize; i++) {
  64. curSum += weighted[i][1];
  65. }
  66. windowWeights.push(curSum);
  67. for (var i = 0; i < weighted.length - windowSize; i++) {
  68. curSum -= weighted[i][1];
  69. curSum += weighted[i + windowSize][1];
  70. windowWeights.push(curSum);
  71. }
  72. // If we didn't find the term, just pick the first window
  73. var maxSumIndex = 0;
  74. if (termFound) {
  75. var maxFound = 0;
  76. // backwards
  77. for (var i = windowWeights.length - 1; i >= 0; i--) {
  78. if (windowWeights[i] > maxFound) {
  79. maxFound = windowWeights[i];
  80. maxSumIndex = i;
  81. }
  82. }
  83. }
  84. var teaser = [];
  85. var startIndex = weighted[maxSumIndex][2];
  86. for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) {
  87. var word = weighted[i];
  88. if (startIndex < word[2]) {
  89. // missing text from index to start of `word`
  90. teaser.push(body.substring(startIndex, word[2]));
  91. startIndex = word[2];
  92. }
  93. // add <em/> around search terms
  94. if (word[1] === TERM_WEIGHT) {
  95. teaser.push("<b>");
  96. }
  97. startIndex = word[2] + word[0].length;
  98. teaser.push(body.substring(word[2], startIndex));
  99. if (word[1] === TERM_WEIGHT) {
  100. teaser.push("</b>");
  101. }
  102. }
  103. teaser.push("…");
  104. return teaser.join("");
  105. }
  106. function formatSearchResultItem(item, terms) {
  107. return '<div class="search-results__item">'
  108. + `<a href="${item.ref}">${item.doc.title}</a>`
  109. + `<div>${makeTeaser(item.doc.body, terms)}</div>`
  110. + '</div>';
  111. }
  112. function initSearch() {
  113. var $searchInput = document.getElementById("search");
  114. var $searchResults = document.querySelector(".search-results");
  115. var $searchResultsItems = document.querySelector(".search-results__items");
  116. var MAX_ITEMS = 10;
  117. var options = {
  118. bool: "AND",
  119. fields: {
  120. title: {boost: 2},
  121. body: {boost: 1},
  122. }
  123. };
  124. var currentTerm = "";
  125. var index = elasticlunr.Index.load(window.searchIndex);
  126. $searchInput.addEventListener("keyup", debounce(function() {
  127. var term = $searchInput.value.trim();
  128. if (term === currentTerm || !index) {
  129. return;
  130. }
  131. $searchResults.style.display = term === "" ? "none" : "block";
  132. $searchResultsItems.innerHTML = "";
  133. if (term === "") {
  134. return;
  135. }
  136. var results = index.search(term, options);
  137. if (results.length === 0) {
  138. $searchResults.style.display = "none";
  139. return;
  140. }
  141. currentTerm = term;
  142. for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) {
  143. var item = document.createElement("li");
  144. item.innerHTML = formatSearchResultItem(results[i], term.split(" "));
  145. $searchResultsItems.appendChild(item);
  146. }
  147. }, 150));
  148. }
  149. if (document.readyState === "complete" ||
  150. (document.readyState !== "loading" && !document.documentElement.doScroll)
  151. ) {
  152. initSearch();
  153. } else {
  154. document.addEventListener("DOMContentLoaded", initSearch);
  155. }