search.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import Fuse from 'fuse.js'
  2. import Mark from 'mark.js'
  3. window.addEventListener('DOMContentLoaded', () => {
  4. const summaryInclude = 60
  5. const fuseOptions = {
  6. shouldSort: true,
  7. includeMatches: true,
  8. threshold: 0.0,
  9. tokenize: true,
  10. location: 0,
  11. distance: 100,
  12. maxPatternLength: 32,
  13. minMatchCharLength: 1,
  14. keys: [
  15. { name: 'title', weight: 0.8 },
  16. { name: 'hero', weight: 0.7 },
  17. { name: 'summary', weight: 0.6 },
  18. { name: 'date', weight: 0.5 },
  19. { name: 'contents', weight: 0.5 },
  20. { name: 'tags', weight: 0.3 },
  21. { name: 'categories', weight: 0.3 }
  22. ]
  23. }
  24. const searchQuery = param('keyword')
  25. if (searchQuery) {
  26. document.getElementById('search-box').value = searchQuery
  27. executeSearch(searchQuery)
  28. } else {
  29. const node = document.createElement('p')
  30. node.textContent = 'Please enter a word or phrase above'
  31. document.getElementById('search-results')?.append(node)
  32. }
  33. function executeSearch (searchQuery) {
  34. const url = window.location.href.split('/search/')[0] + '/index.json'
  35. fetch(url).then(response => response.json()).then(function (data) {
  36. const pages = data
  37. const fuse = new Fuse(pages, fuseOptions)
  38. const results = fuse.search(searchQuery)
  39. document.getElementById('search-box').value = searchQuery
  40. if (results.length > 0) {
  41. populateResults(results)
  42. } else {
  43. const node = document.createElement('p')
  44. node.textContent = 'No matches found'
  45. document.getElementById('search-results')?.append(node)
  46. }
  47. })
  48. }
  49. function populateResults (results) {
  50. results.forEach(function (value, key) {
  51. const contents = value.item.contents
  52. let snippet = ''
  53. const snippetHighlights = []
  54. if (fuseOptions.tokenize) {
  55. snippetHighlights.push(searchQuery)
  56. } else {
  57. value.matches.forEach(function (mvalue) {
  58. if (mvalue.key === 'tags' || mvalue.key === 'categories') {
  59. snippetHighlights.push(mvalue.value)
  60. } else if (mvalue.key === 'contents') {
  61. const start = mvalue.indices[0][0] - summaryInclude > 0 ? mvalue.indices[0][0] - summaryInclude : 0
  62. const end = mvalue.indices[0][1] + summaryInclude < contents.length ? mvalue.indices[0][1] + summaryInclude : contents.length
  63. snippet += contents.substring(start, end)
  64. snippetHighlights.push(mvalue.value.substring(mvalue.indices[0][0], mvalue.indices[0][1] - mvalue.indices[0][0] + 1))
  65. }
  66. })
  67. }
  68. if (snippet.length < 1) {
  69. snippet += contents.substring(0, summaryInclude * 2)
  70. }
  71. // pull template from hugo template definition
  72. const templateDefinition = document.getElementById('search-result-template').innerHTML
  73. // replace values
  74. function adaptTags() {
  75. const tags = value.item.tags;
  76. let string = '';
  77. if (tags) tags.forEach((t) => {string += '<li class="rounded"><a href="/tags/' + t.toLowerCase() + '/" class="btn btn-sm btn-info">' + t + "</a></li>"});
  78. return string;
  79. }
  80. const output = render(templateDefinition, {
  81. key,
  82. title: value.item.title,
  83. hero: value.item.hero,
  84. date: value.item.date,
  85. summary: value.item.summary,
  86. link: value.item.permalink,
  87. tags: adaptTags(),
  88. categories: value.item.categories,
  89. snippet
  90. })
  91. const dom = new DOMParser().parseFromString(output, 'text/html')
  92. document.getElementById('search-results').append(dom.getElementsByClassName('post-card')[0])
  93. snippetHighlights.forEach(function (snipvalue) {
  94. const context = document.getElementById('#summary-' + key)
  95. const instance = new Mark(context)
  96. instance.mark(snipvalue)
  97. })
  98. })
  99. }
  100. function param (name) {
  101. return decodeURIComponent((location.search.split(name + '=')[1] || '').split('&')[0]).replace(/\+/g, ' ')
  102. }
  103. function render (templateString, data) {
  104. let conditionalMatches, copy
  105. const conditionalPattern = /\$\{\s*isset ([a-zA-Z]*) \s*\}(.*)\$\{\s*end\s*}/g
  106. // since loop below depends on re.lastInxdex, we use a copy to capture any manipulations whilst inside the loop
  107. copy = templateString
  108. while ((conditionalMatches = conditionalPattern.exec(templateString)) !== null) {
  109. if (data[conditionalMatches[1]]) {
  110. // valid key, remove conditionals, leave contents.
  111. copy = copy.replace(conditionalMatches[0], conditionalMatches[2])
  112. } else {
  113. // not valid, remove entire section
  114. copy = copy.replace(conditionalMatches[0], '')
  115. }
  116. }
  117. templateString = copy
  118. // now any conditionals removed we can do simple substitution
  119. let key, find, re
  120. for (key in data) {
  121. find = '\\$\\{\\s*' + key + '\\s*\\}'
  122. re = new RegExp(find, 'g')
  123. templateString = templateString.replace(re, data[key])
  124. }
  125. return templateString
  126. }
  127. })