PageSourceSearch

https://yuanxiaosc.github.io/js/local-search.js?v=7.4.1

js yuanxiaosc.github.io collected 2026-10-03 10:13:42 UTC 10,846 bytes, 319 lines download raw bytes

1/* global CONFIG */
2
3window.addEventListener('DOMContentLoaded', () => {
4  // Popup Window
5  let isfetched = false;
6  let datas;
7  let isXml = true;
8  // Search DB path
9  let searchPath = CONFIG.path;
10  if (searchPath.length === 0) {
11    searchPath = 'search.xml';
12  } else if (/json$/i.test(searchPath)) {
13    isXml = false;
14  }
15  const path = CONFIG.root + searchPath;
16  const input = document.getElementById('search-input');
17  const resultContent = document.getElementById('search-result');
18
19  // Ref: https://github.com/ForbesLindesay/unescape-html
20  const unescapeHtml = html => {
21    return String(html)
22      .replace(/"/g, '"')
23      .replace(/'/g, '\'')
24      .replace(/:/g, ':')
25      // Replace all the other &#x; chars
26      .replace(/&#(\d+);/g, (m, p) => {
27        return String.fromCharCode(p);
28      })
29      .replace(/&lt;/g, '<')
30      .replace(/&gt;/g, '>')
31      .replace(/&amp;/g, '&');
32  };
33
34  const getIndexByWord = (word, text, caseSensitive) => {
35    let wordLen = word.length;
36    if (wordLen === 0) {
37      return [];
38    }
39    let startPosition = 0;
40    let position = [];
41    let index = [];
42    if (!caseSensitive) {
43      text = text.toLowerCase();
44      word = word.toLowerCase();
45    }
46    while ((position = text.indexOf(word, startPosition)) > -1) {
47      index.push({
48        position: position,
49        word    : word
50      });
51      startPosition = position + wordLen;
52    }
53    return index;
54  };
55
56  // Merge hits into slices
57  const mergeIntoSlice = (start, end, index, searchText) => {
58    let item = index[index.length - 1];
59    let position = item.position;
60    let word = item.word;
61    let hits = [];
62    let searchTextCountInSlice = 0;
63    while (position + word.length <= end && index.length !== 0) {
64      if (word === searchText) {
65        searchTextCountInSlice++;
66      }
67      hits.push({
68        position: position,
69        length  : word.length
70      });
71      let wordEnd = position + word.length;
72
73      // Move to next position of hit
74      index.pop();
75      while (index.length !== 0) {
76        item = index[index.length - 1];
77        position = item.position;
78        word = item.word;
79        if (wordEnd > position) {
80          index.pop();
81        } else {
82          break;
83        }
84      }
85    }
86    return {
87      hits           : hits,
88      start          : start,
89      end            : end,
90      searchTextCount: searchTextCountInSlice
91    };
92  };
93
94  // Highlight title and content
95  const highlightKeyword = (text, slice) => {
96    let result = '';
97    let prevEnd = slice.start;
98    slice.hits.forEach(hit => {
99      result += text.substring(prevEnd, hit.position);
100      let end = hit.position + hit.length;
101      result += `<b class="search-keyword">${text.substring(hit.position, end)}</b>`;
102      prevEnd = end;
103    });
104    result += text.substring(prevEnd, slice.end);
105    return result;
106  };
107
108  const inputEventFunction = () => {
109    let searchText = input.value.trim().toLowerCase();
110    let keywords = searchText.split(/[-\s]+/);
111    if (keywords.length > 1) {
112      keywords.push(searchText);
113    }
114    let resultItems = [];
115    if (searchText.length > 0) {
116      // Perform local searching
117      datas.forEach(data => {
118        // Only match articles with not empty titles
119        if (!data.title) {
120          return;
121        }
122        let searchTextCount = 0;
123        let title = data.title.trim();
124        let titleInLowerCase = title.toLowerCase();
125        let content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : '';
126        if (CONFIG.localsearch.unescape) {
127          content = unescapeHtml(content);
128        }
129        let contentInLowerCase = content.toLowerCase();
130        let articleUrl = decodeURIComponent(data.url).replace(/\/{2,}/g, '/');
131        let indexOfTitle = [];
132        let indexOfContent = [];
133        keywords.forEach(keyword => {
134          indexOfTitle = indexOfTitle.concat(getIndexByWord(keyword, titleInLowerCase, false));
135          indexOfContent = indexOfContent.concat(getIndexByWord(keyword, contentInLowerCase, false));
136        });
137
138        // Show search results
139        if (indexOfTitle.length > 0 || indexOfContent.length > 0) {
140          let hitCount = indexOfTitle.length + indexOfContent.length;
141          // Sort index by position of keyword
142          [indexOfTitle, indexOfContent].forEach(index => {
143            index.sort((itemLeft, itemRight) => {
144              if (itemRight.position !== itemLeft.position) {
145                return itemRight.position - itemLeft.position;
146              }
147              return itemLeft.word.length - itemRight.word.length;
148            });
149          });
150
151          let slicesOfTitle = [];
152          if (indexOfTitle.length !== 0) {
153            let tmp = mergeIntoSlice(0, title.length, indexOfTitle, searchText);
154            searchTextCount += tmp.searchTextCountInSlice;
155            slicesOfTitle.push(tmp);
156          }
157
158          let slicesOfContent = [];
159          while (indexOfContent.length !== 0) {
160            let item = indexOfContent[indexOfContent.length - 1];
161            let position = item.position;
162            let word = item.word;
163            // Cut out 100 characters
164            let start = position - 20;
165            let end = position + 80;
166            if (start < 0) {
167              start = 0;
168            }
169            if (end < position + word.length) {
170              end = position + word.length;
171            }
172            if (end > content.length) {
173              end = content.length;
174            }
175            let tmp = mergeIntoSlice(start, end, indexOfContent, searchText);
176            searchTextCount += tmp.searchTextCountInSlice;
177            slicesOfContent.push(tmp);
178          }
179
180          // Sort slices in content by search text's count and hits' count
181          slicesOfContent.sort((sliceLeft, sliceRight) => {
182            if (sliceLeft.searchTextCount !== sliceRight.searchTextCount) {
183              return sliceRight.searchTextCount - sliceLeft.searchTextCount;
184            } else if (sliceLeft.hits.length !== sliceRight.hits.length) {
185              return sliceRight.hits.length - sliceLeft.hits.length;
186            }
187            return sliceLeft.start - sliceRight.start;
188          });
189
190          // Select top N slices in content
191          let upperBound = parseInt(CONFIG.localsearch.top_n_per_article, 10);
192          if (upperBound >= 0) {
193            slicesOfContent = slicesOfContent.slice(0, upperBound);
194          }
195
196          let resultItem = '';
197
198          if (slicesOfTitle.length !== 0) {
199            resultItem += `<li><a href="${articleUrl}" class="search-result-title">${highlightKeyword(title, slicesOfTitle[0])}</a>`;
200          } else {
201            resultItem += `<li><a href="${articleUrl}" class="search-result-title">${title}</a>`;
202          }
203
204          slicesOfContent.forEach(slice => {
205            resultItem += `<a href="${articleUrl}"><p class="search-result">${highlightKeyword(content, slice)}...</p></a>`;
206          });
207
208          resultItem += '</li>';
209          resultItems.push({
210            item           : resultItem,
211            searchTextCount: searchTextCount,
212            hitCount       : hitCount,
213            id             : resultItems.length
214          });
215        }
216      });
217    }
218    if (keywords.length === 1 && keywords[0] === '') {
219      resultContent.innerHTML = '<div id="no-result"><i class="fa fa-search fa-5x"></i></div>';
220    } else if (resultItems.length === 0) {
221      resultContent.innerHTML = '<div id="no-result"><i class="fa fa-frown-o fa-5x"></i></div>';
222    } else {
223      resultItems.sort((resultLeft, resultRight) => {
224        if (resultLeft.searchTextCount !== resultRight.searchTextCount) {
225          return resultRight.searchTextCount - resultLeft.searchTextCount;
226        } else if (resultLeft.hitCount !== resultRight.hitCount) {
227          return resultRight.hitCount - resultLeft.hitCount;
228        }
229        return resultRight.id - resultLeft.id;
230      });
231      let searchResultList = '<ul class="search-result-list">';
232      resultItems.forEach(result => {
233        searchResultList += result.item;
234      });
235      searchResultList += '</ul>';
236      resultContent.innerHTML = searchResultList;
237      window.pjax && window.pjax.refresh(resultContent);
238    }
239  };
240
241  const fetchData = callback => {
242    fetch(path)
243      .then(response => response.text())
244      .then(res => {
245        // Get the contents from search data
246        isfetched = true;
247        datas = isXml ? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(item => {
248          return {
249            title  : item.querySelector('title').innerHTML,
250            content: item.querySelector('content').innerHTML,
251            url    : item.querySelector('url').innerHTML
252          };
253        }) : JSON.parse(res);
254
255        // Remove loading animation
256        document.querySelector('.search-pop-overlay').innerHTML = '';
257        document.body.style.overflow = '';
258
259        if (callback) {
260          callback();
261        }
262      });
263  };
264
265  if (CONFIG.localsearch.preload) {
266    fetchData();
267  }
268
269  const proceedSearch = () => {
270    document.body.style.overflow = 'hidden';
271    document.querySelector('.search-pop-overlay').style.display = 'block';
272    document.querySelector('.popup').style.display = 'block';
273    document.getElementById('search-input').focus();
274  };
275
276  // Search function
277  const searchFunc = () => {
278    document.querySelector('.search-pop-overlay').style.display = '';
279    document.querySelector('.search-pop-overlay').innerHTML = '<div class="search-loading-icon"><i class="fa fa-spinner fa-pulse fa-5x fa-fw"></i></div>';
280    fetchData(proceedSearch);
281  };
282
283  if (CONFIG.localsearch.trigger === 'auto') {
284    input.addEventListener('input', inputEventFunction);
285  } else {
286    document.querySelector('.search-icon').addEventListener('click', inputEventFunction);
287    input.addEventListener('keypress', event => {
288      if (event.keyCode === 13) {
289        inputEventFunction();
290      }
291    });
292  }
293
294  // Handle and trigger popup window
295  document.querySelector('.popup-trigger').addEventListener('click', event => {
296    event.stopPropagation();
297    if (isfetched === false) {
298      searchFunc();
299    } else {
300      proceedSearch();
301    }
302  });
303
304  // Monitor main search box
305  const onPopupClose = () => {
306    document.body.style.overflow = '';
307    document.querySelector('.search-pop-overlay').style.display = 'none';
308    document.querySelector('.popup').style.display = 'none';
309  };
310
311  document.querySelector('.search-pop-overlay').addEventListener('click', onPopupClose);
312  document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose);
313  window.addEventListener('pjax:success', onPopupClose);
314  window.addEventListener('keyup', event => {
315    if (event.which === 27) {
316      onPopupClose();
317    }
318  });
319});

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.