wowchemy-search.js 6.2 KB

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