wowchemy-search.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. /*************************************************
  2. * Wowchemy
  3. * https://github.com/wowchemy/wowchemy-hugo-modules
  4. *
  5. * In-built Fuse based search algorithm.
  6. **************************************************/
  7. /* ---------------------------------------------------------------------------
  8. * Configuration.
  9. * --------------------------------------------------------------------------- */
  10. // Configure Fuse.
  11. let fuseOptions = {
  12. shouldSort: true,
  13. includeMatches: true,
  14. tokenize: true,
  15. threshold: search_config.threshold, // Set to ~0.3 for parsing diacritics and CJK languages.
  16. location: 0,
  17. distance: 100,
  18. maxPatternLength: 32,
  19. minMatchCharLength: search_config.minLength, // Set to 1 for parsing CJK languages.
  20. keys: [
  21. {name:'title', weight:0.99}, /* 1.0 doesn't work o_O */
  22. {name:'summary', weight:0.6},
  23. {name:'authors', weight:0.5},
  24. {name:'content', weight:0.2},
  25. {name:'tags', weight:0.5},
  26. {name:'categories', weight:0.5}
  27. ]
  28. };
  29. // Configure summary.
  30. let summaryLength = 60;
  31. /* ---------------------------------------------------------------------------
  32. * Functions.
  33. * --------------------------------------------------------------------------- */
  34. // Get query from URI.
  35. function getSearchQuery(name) {
  36. return decodeURIComponent((location.search.split(name + '=')[1] || '').split('&')[0]).replace(/\+/g, ' ');
  37. }
  38. // Set query in URI without reloading the page.
  39. function updateURL(url) {
  40. if (history.replaceState) {
  41. window.history.replaceState({path:url}, '', url);
  42. }
  43. }
  44. // Pre-process new search query.
  45. function initSearch(force, fuse) {
  46. let query = $("#search-query").val();
  47. // If query deleted, clear results.
  48. if ( query.length < 1) {
  49. $('#search-hits').empty();
  50. }
  51. // Check for timer event (enter key not pressed) and query less than minimum length required.
  52. if (!force && query.length < fuseOptions.minMatchCharLength)
  53. return;
  54. // Do search.
  55. $('#search-hits').empty();
  56. searchAcademic(query, fuse);
  57. let newURL = window.location.protocol + "//" + window.location.host + window.location.pathname + '?q=' + encodeURIComponent(query) + window.location.hash;
  58. updateURL(newURL);
  59. }
  60. // Perform search.
  61. function searchAcademic(query, fuse) {
  62. let results = fuse.search(query);
  63. // console.log({"results": results});
  64. if (results.length > 0) {
  65. $('#search-hits').append('<h3 class="mt-0">' + results.length + ' ' + i18n.results + '</h3>');
  66. parseResults(query, results);
  67. } else {
  68. $('#search-hits').append('<div class="search-no-results">' + i18n.no_results + '</div>');
  69. }
  70. }
  71. // Parse search results.
  72. function parseResults(query, results) {
  73. $.each( results, function(key, value) {
  74. let content_key = value.item.section;
  75. let content = "";
  76. let snippet = "";
  77. let snippetHighlights = [];
  78. // Show abstract in results for content types where the abstract is often the primary content.
  79. if (["publication", "talk"].includes(content_key)) {
  80. content = value.item.summary;
  81. } else {
  82. content = value.item.content;
  83. }
  84. if ( fuseOptions.tokenize ) {
  85. snippetHighlights.push(query);
  86. } else {
  87. $.each( value.matches, function(matchKey, matchValue) {
  88. if (matchValue.key == "content") {
  89. let start = (matchValue.indices[0][0]-summaryLength>0) ? matchValue.indices[0][0]-summaryLength : 0;
  90. let end = (matchValue.indices[0][1]+summaryLength<content.length) ? matchValue.indices[0][1]+summaryLength : content.length;
  91. snippet += content.substring(start, end);
  92. snippetHighlights.push(matchValue.value.substring(matchValue.indices[0][0], matchValue.indices[0][1]-matchValue.indices[0][0]+1));
  93. }
  94. });
  95. }
  96. if (snippet.length < 1) {
  97. snippet += value.item.summary; // Alternative fallback: `content.substring(0, summaryLength*2);`
  98. }
  99. // Load template.
  100. let template = $('#search-hit-fuse-template').html();
  101. // Localize content types.
  102. if (content_key in content_type) {
  103. content_key = content_type[content_key];
  104. }
  105. // Parse template.
  106. let templateData = {
  107. key: key,
  108. title: value.item.title,
  109. type: content_key,
  110. relpermalink: value.item.relpermalink,
  111. snippet: snippet
  112. };
  113. let output = render(template, templateData);
  114. $('#search-hits').append(output);
  115. // Highlight search terms in result.
  116. $.each( snippetHighlights, function(hlKey, hlValue){
  117. $("#summary-"+key).mark(hlValue);
  118. });
  119. });
  120. }
  121. function render(template, data) {
  122. // Replace placeholders with their values.
  123. let key, find, re;
  124. for (key in data) {
  125. find = '\\{\\{\\s*' + key + '\\s*\\}\\}'; // Expect placeholder in the form `{{x}}`.
  126. re = new RegExp(find, 'g');
  127. template = template.replace(re, data[key]);
  128. }
  129. return template;
  130. }
  131. /* ---------------------------------------------------------------------------
  132. * Initialize.
  133. * --------------------------------------------------------------------------- */
  134. // If Academic's in-built search is enabled and Fuse loaded, then initialize it.
  135. if (typeof Fuse === 'function') {
  136. // Wait for Fuse to initialize.
  137. $.getJSON(search_config.indexURI, function (search_index) {
  138. let fuse = new Fuse(search_index, fuseOptions);
  139. // On page load, check for search query in URL.
  140. if (query = getSearchQuery('q')) {
  141. $("body").addClass('searching');
  142. $('.search-results').css({opacity: 0, visibility: "visible"}).animate({opacity: 1},200);
  143. $("#search-query").val(query);
  144. $("#search-query").focus();
  145. initSearch(true, fuse);
  146. }
  147. // On search box key up, process query.
  148. $('#search-query').keyup(function (e) {
  149. clearTimeout($.data(this, 'searchTimer')); // Ensure only one timer runs!
  150. if (e.keyCode == 13) {
  151. initSearch(true, fuse);
  152. } else {
  153. $(this).data('searchTimer', setTimeout(function () {
  154. initSearch(false, fuse);
  155. }, 250));
  156. }
  157. });
  158. });
  159. }