search.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  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. const output = render(templateDefinition, {
  75. key,
  76. title: value.item.title,
  77. hero: value.item.hero,
  78. date: value.item.date,
  79. summary: value.item.summary,
  80. link: value.item.permalink,
  81. tags: value.item.tags,
  82. categories: value.item.categories,
  83. snippet
  84. })
  85. const dom = new DOMParser().parseFromString(output, 'text/html')
  86. document.getElementById('search-results').append(dom.getElementsByClassName('post-card')[0])
  87. snippetHighlights.forEach(function (snipvalue) {
  88. const context = document.getElementById('#summary-' + key)
  89. const instance = new Mark(context)
  90. instance.mark(snipvalue)
  91. })
  92. })
  93. }
  94. function param (name) {
  95. return decodeURIComponent((location.search.split(name + '=')[1] || '').split('&')[0]).replace(/\+/g, ' ')
  96. }
  97. function render (templateString, data) {
  98. let conditionalMatches, copy
  99. const conditionalPattern = /\$\{\s*isset ([a-zA-Z]*) \s*\}(.*)\$\{\s*end\s*}/g
  100. // since loop below depends on re.lastInxdex, we use a copy to capture any manipulations whilst inside the loop
  101. copy = templateString
  102. while ((conditionalMatches = conditionalPattern.exec(templateString)) !== null) {
  103. if (data[conditionalMatches[1]]) {
  104. // valid key, remove conditionals, leave contents.
  105. copy = copy.replace(conditionalMatches[0], conditionalMatches[2])
  106. } else {
  107. // not valid, remove entire section
  108. copy = copy.replace(conditionalMatches[0], '')
  109. }
  110. }
  111. templateString = copy
  112. // now any conditionals removed we can do simple substitution
  113. let key, find, re
  114. for (key in data) {
  115. find = '\\$\\{\\s*' + key + '\\s*\\}'
  116. re = new RegExp(find, 'g')
  117. templateString = templateString.replace(re, data[key])
  118. }
  119. return templateString
  120. }
  121. })