PageSourceSearch

https://lynchrocket.github.io/js/search.js

js lynchrocket.github.io collected 2026-10-03 09:32:50 UTC 4,822 bytes, 195 lines download raw bytes

1const searchBtn = document.querySelector("#search-btn")
2const searchInput = document.querySelector("#search-input")
3const searchResult = document.querySelector("#search-result")
4const searchClearBtn = document.querySelector("#search-clear")
5
6const searchMask = document.querySelector("#search-mask")
7
8let searchStatus = 0
9
10function showSearchDialog() {
11  if (searchStatus) {
12    return
13  }
14  searchMask.style.display = 'block'
15  document.body.style.overflow = 'hidden'
16  searchInput.focus()
17  searchStatus = 1
18}
19
20function closeSearchDialog() {
21  if (!searchStatus) {
22    return
23  }
24  searchMask.style.display = 'None'
25  document.body.style.overflow = 'inherit'
26  searchInput.value = ""
27  searchStatus = 0
28}
29
30function searchInitialize(url) {
31  fetch(url).then(res => res.json()).then(res => {
32    const inputHandler = debounce(doSearch)
33
34    searchBtn.style.display = "flex"
35
36    document.addEventListener('keydown', e => {
37      if ((e.ctrlKey || e.metaKey) && e.key == "k") {
38        if (searchStatus) {
39          closeSearchDialog()
40        } else {
41          showSearchDialog()
42        }
43      }
44      if (e.key == 'Escape') {
45        closeSearchDialog()
46      }
47    })
48
49    searchClearBtn.addEventListener('click', () => {
50      searchInput.value = ""
51      clearResult()
52    })
53
54    searchBtn.addEventListener('click', () => {
55      showSearchDialog()
56    })
57
58    searchMask.addEventListener('click', e => {
59      if (e.target !== searchMask) {
60        return
61      }
62      closeSearchDialog()
63    }, true)
64
65    searchInput.addEventListener('input', inputHandler.bind(searchInput, res))
66  })
67}
68
69function clearResult() {
70  searchResult.innerHTML = ''
71}
72
73function doSearch(data) {
74  if (this.value.trim().length <= 0) {
75    return clearResult()
76  }
77
78  var keywords = this.value.trim().toLowerCase().split(/[\s\-]+/);
79
80  const result = search(data, keywords)
81
82  renderSearchResult(result, searchResult)
83}
84
85function search(data, keywords) {
86  const matchedPost = []
87
88  data.filter(post => post.content).forEach(post => {
89    let postTitle = post.title && post.title.trim()
90    postTitle = (postTitle && postTitle.length > 0) ? postTitle : "Untitled"
91
92    const postContent = post.content.trim().replace(/<[^>]+>/g, "")
93
94    const matchedContentIdices = []
95
96    keywords.forEach((keyword) => {
97      const index_title = postTitle.toLowerCase().indexOf(keyword);
98      const index_content = postContent.toLowerCase().indexOf(keyword);
99
100      if (index_title < 0 && index_content < 0) {
101        return
102      }
103
104      matchedContentIdices.push(Math.max(index_content, 0))
105    });
106
107    if (matchedContentIdices.length) {
108      matchedPost.push({
109        url: post.url,
110        content: trimeContent(matchedContentIdices, postContent, keywords),
111        title: postTitle
112      })
113    }
114  });
115
116  return matchedPost
117}
118
119function renderSearchResult(result, el) {
120  el.innerHTML = ''
121
122  if (!result || result.length <= 0) {
123    el.innerHTML = '<div class="search-result-empty">无结果</div>'
124  }
125
126  const list = document.createDocumentFragment()
127  result.forEach(res => {
128    const item = document.createElement('a')
129    item.className = 'search-result-item'
130    item.href = res.url
131
132    const title = document.createElement('DIV')
133    title.className = 'search-result__head'
134    title.innerText = res.title
135
136    const content = document.createElement('DIV')
137    content.className = 'search-result__body'
138    res.content.forEach(contentText => {
139      const contentItem = document.createElement('DIV')
140      contentItem.innerHTML = contentText
141      content.appendChild(contentItem)
142    })
143
144    item.append(title, content)
145    list.append(item)
146  })
147
148  el.appendChild(list)
149}
150
151function trimeContent(keyIndexs, content, keywords, wordLen = 20) {
152  const reg = /[\u4e00-\u9fa5]|\w+/gd
153  const splitIndex = []
154  let arr
155  while ((arr = reg.exec(content)) !== null) {
156    splitIndex.push(arr.indices[0])
157  }
158
159  return keyIndexs.map(key => {
160    let pos = binaryFind(splitIndex, key)
161    const wordStart = Math.max(0, pos - 10)
162    const wordEnd = Math.min(wordLen + wordStart, splitIndex.length - 1)
163    const start = splitIndex[wordStart][0]
164    const end = splitIndex[wordEnd][1]
165
166    keywords.forEach(keyword => {
167      content = content
168        .slice(start, end)
169        .replace(new RegExp(keyword, 'ig'), `<span class="search-keyword">${keyword}</span>`)
170    })
171
172    return content
173  })
174}
175
176function binaryFind(arr, value) {
177  let i = 0, j = arr.length - 1, mid = j
178  while (i <= j) {
179    if (value < arr[mid][0]) j = mid - 1;
180    else if (value > arr[mid][1]) i = mid + 1
181    else return mid
182    mid = Math.floor((i + j) / 2)
183  }
184  return mid
185}
186
187function debounce(fn, t = 400) {
188  let timer = null
189  return function (...args) {
190    clearTimeout(timer)
191    timer = setTimeout(() => {
192      fn.call(this, ...args)
193    }, t)
194  }
195}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.