PageSourceSearch

https://blog-7sirb.ondigitalocean.app/js/insight.js

js blog-7sirb.ondigitalocean.app collected 2026-10-03 14:54:00 UTC 10,393 bytes, 354 lines download raw bytes

1/**
2 * Insight search plugin
3 * @author PPOffice { @link https://github.com/ppoffice }
4 */
5// eslint-disable-next-line no-unused-vars
6function loadInsight(config, translation) {
7  const $main = $('.searchbox');
8  const $input = $main.find('.searchbox-input');
9  const $container = $main.find('.searchbox-body');
10
11  function section(title) {
12    return $('<section>').addClass('searchbox-result-section').append($('<header>').text(title));
13  }
14
15  function merge(ranges) {
16    let last;
17    const result = [];
18
19    ranges.forEach((r) => {
20      if (!last || r[0] > last[1]) {
21        result.push((last = r));
22      } else if (r[1] > last[1]) {
23        last[1] = r[1];
24      }
25    });
26
27    return result;
28  }
29
30  function findAndHighlight(text, matches, maxlen) {
31    if (!Array.isArray(matches) || !matches.length || !text) {
32      return maxlen ? text.slice(0, maxlen) : text;
33    }
34    const testText = text.toLowerCase();
35    const indices = matches
36      .map((match) => {
37        const index = testText.indexOf(match.toLowerCase());
38        if (!match || index === -1) {
39          return null;
40        }
41        return [index, index + match.length];
42      })
43      .filter((match) => {
44        return match !== null;
45      })
46      .sort((a, b) => {
47        return a[0] - b[0] || a[1] - b[1];
48      });
49
50    if (!indices.length) {
51      return text;
52    }
53
54    let result = '';
55    let last = 0;
56    const ranges = merge(indices);
57    const sumRange = [ranges[0][0], ranges[ranges.length - 1][1]];
58    if (maxlen && maxlen < sumRange[1]) {
59      last = sumRange[0];
60    }
61
62    for (let i = 0; i < ranges.length; i++) {
63      const range = ranges[i];
64      result += text.slice(last, Math.min(range[0], sumRange[0] + maxlen));
65      if (maxlen && range[0] >= sumRange[0] + maxlen) {
66        break;
67      }
68      result += '<em>' + text.slice(range[0], range[1]) + '</em>';
69      last = range[1];
70      if (i === ranges.length - 1) {
71        if (maxlen) {
72          result += text.slice(range[1], Math.min(text.length, sumRange[0] + maxlen + 1));
73        } else {
74          result += text.slice(range[1]);
75        }
76      }
77    }
78
79    return result;
80  }
81
82  function searchItem(icon, title, slug, preview, url) {
83    title = title != null && title !== '' ? title : translation.untitled;
84    const subtitle = slug
85      ? '<span class="searchbox-result-title-secondary">(' + slug + ')</span>'
86      : '';
87
88    return `<a class="searchbox-result-item" href="${url}">
89            <span class="searchbox-result-icon">
90                <i class="fa fa-${icon}" />
91            </span>
92            <span class="searchbox-result-content">
93                <span class="searchbox-result-title">
94                    ${title}
95                    ${subtitle}
96                </span>
97                ${preview ? '<span class="searchbox-result-preview">' + preview + '</span>' : ''}
98            </span>
99        </a>`;
100  }
101
102  function sectionFactory(keywords, type, array) {
103    let $searchItems;
104    if (array.length === 0) return null;
105    const sectionTitle = translation[type.toLowerCase()];
106    switch (type) {
107      case 'POSTS':
108      case 'PAGES':
109        $searchItems = array.map((item) => {
110          const title = findAndHighlight(item.title, keywords);
111          const text = findAndHighlight(item.text, keywords, 100);
112          return searchItem('file', title, null, text, item.link);
113        });
114        break;
115      case 'CATEGORIES':
116      case 'TAGS':
117        $searchItems = array.map((item) => {
118          const name = findAndHighlight(item.name, keywords);
119          const slug = findAndHighlight(item.slug, keywords);
120          return searchItem(type === 'CATEGORIES' ? 'folder' : 'tag', name, slug, null, item.link);
121        });
122        break;
123      default:
124        return null;
125    }
126    return section(sectionTitle).append($searchItems);
127  }
128
129  function parseKeywords(keywords) {
130    return keywords
131      .split(' ')
132      .filter((keyword) => {
133        return !!keyword;
134      })
135      .map((keyword) => {
136        return keyword.toLowerCase();
137      });
138  }
139
140  /**
141   * Judge if a given post/page/category/tag contains all of the keywords.
142   * @param Object            obj     Object to be weighted
143   * @param Array<String>     fields  Object's fields to find matches
144   */
145  function filter(keywords, obj, fields) {
146    const keywordArray = parseKeywords(keywords);
147    const containKeywords = keywordArray.filter((keyword) => {
148      const containFields = fields.filter((field) => {
149        if (!Object.prototype.hasOwnProperty.call(obj, field)) {
150          return false;
151        }
152        if (obj[field].toLowerCase().indexOf(keyword) > -1) {
153          return true;
154        }
155        return false;
156      });
157      if (containFields.length > 0) {
158        return true;
159      }
160      return false;
161    });
162    return containKeywords.length === keywordArray.length;
163  }
164
165  function filterFactory(keywords) {
166    return {
167      post: function (obj) {
168        return filter(keywords, obj, ['title', 'text']);
169      },
170      page: function (obj) {
171        return filter(keywords, obj, ['title', 'text']);
172      },
173      category: function (obj) {
174        return filter(keywords, obj, ['name', 'slug']);
175      },
176      tag: function (obj) {
177        return filter(keywords, obj, ['name', 'slug']);
178      },
179    };
180  }
181
182  /**
183   * Calculate the weight of a matched post/page/category/tag.
184   * @param Object            obj     Object to be weighted
185   * @param Array<String>     fields  Object's fields to find matches
186   * @param Array<Integer>    weights Weight of every field
187   */
188  function weight(keywords, obj, fields, weights) {
189    let value = 0;
190    parseKeywords(keywords).forEach((keyword) => {
191      const pattern = new RegExp(keyword, 'img'); // Global, Multi-line, Case-insensitive
192      fields.forEach((field, index) => {
193        if (Object.prototype.hasOwnProperty.call(obj, field)) {
194          const matches = obj[field].match(pattern);
195          value += matches ? matches.length * weights[index] : 0;
196        }
197      });
198    });
199    return value;
200  }
201
202  function weightFactory(keywords) {
203    return {
204      post: function (obj) {
205        return weight(keywords, obj, ['title', 'text'], [3, 1]);
206      },
207      page: function (obj) {
208        return weight(keywords, obj, ['title', 'text'], [3, 1]);
209      },
210      category: function (obj) {
211        return weight(keywords, obj, ['name', 'slug'], [1, 1]);
212      },
213      tag: function (obj) {
214        return weight(keywords, obj, ['name', 'slug'], [1, 1]);
215      },
216    };
217  }
218
219  function search(json, keywords) {
220    const weights = weightFactory(keywords);
221    const filters = filterFactory(keywords);
222    const posts = json.posts;
223    const pages = json.pages;
224    const tags = json.tags;
225    const categories = json.categories;
226    return {
227      posts: posts
228        .filter(filters.post)
229        .sort((a, b) => {
230          return weights.post(b) - weights.post(a);
231        })
232        .slice(0, 5),
233      pages: pages
234        .filter(filters.page)
235        .sort((a, b) => {
236          return weights.page(b) - weights.page(a);
237        })
238        .slice(0, 5),
239      categories: categories
240        .filter(filters.category)
241        .sort((a, b) => {
242          return weights.category(b) - weights.category(a);
243        })
244        .slice(0, 5),
245      tags: tags
246        .filter(filters.tag)
247        .sort((a, b) => {
248          return weights.tag(b) - weights.tag(a);
249        })
250        .slice(0, 5),
251    };
252  }
253
254  function searchResultToDOM(keywords, searchResult) {
255    $container.empty();
256    for (const key in searchResult) {
257      $container.append(
258        sectionFactory(parseKeywords(keywords), key.toUpperCase(), searchResult[key]),
259      );
260    }
261  }
262
263  function scrollTo($item) {
264    if ($item.length === 0) return;
265    const wrapperHeight = $container[0].clientHeight;
266    const itemTop = $item.position().top - $container.scrollTop();
267    const itemBottom = $item[0].clientHeight + $item.position().top;
268    if (itemBottom > wrapperHeight + $container.scrollTop()) {
269      $container.scrollTop(itemBottom - $container[0].clientHeight);
270    }
271    if (itemTop < 0) {
272      $container.scrollTop($item.position().top);
273    }
274  }
275
276  function selectItemByDiff(value) {
277    const $items = $.makeArray($container.find('.searchbox-result-item'));
278    let prevPosition = -1;
279    $items.forEach((item, index) => {
280      if ($(item).hasClass('active')) {
281        prevPosition = index;
282      }
283    });
284    const nextPosition = ($items.length + prevPosition + value) % $items.length;
285    $($items[prevPosition]).removeClass('active');
286    $($items[nextPosition]).addClass('active');
287    scrollTo($($items[nextPosition]));
288  }
289
290  function gotoLink($item) {
291    if ($item && $item.length) {
292      location.href = $item.attr('href');
293    }
294  }
295
296  $.getJSON(config.contentUrl, (json) => {
297    if (location.hash.trim() === '#insight-search') {
298      $main.addClass('show');
299    }
300    $input.on('input', function () {
301      const keywords = $(this).val();
302      searchResultToDOM(keywords, search(json, keywords));
303    });
304    $input.trigger('input');
305  });
306
307  let touch = false;
308  $(document)
309    .on('click focus', '.navbar-main .search', () => {
310      $main.addClass('show');
311      $main.find('.searchbox-input').focus();
312    })
313    .on('click touchend', '.searchbox-result-item', function (e) {
314      if (e.type !== 'click' && !touch) {
315        return;
316      }
317      gotoLink($(this));
318      touch = false;
319    })
320    .on('click touchend', '.searchbox-close', (e) => {
321      if (e.type !== 'click' && !touch) {
322        return;
323      }
324      $('.navbar-main').css('pointer-events', 'none');
325      setTimeout(() => {
326        $('.navbar-main').css('pointer-events', 'auto');
327      }, 400);
328      $main.removeClass('show');
329      touch = false;
330    })
331    .on('keydown', (e) => {
332      if (!$main.hasClass('show')) return;
333      switch (e.keyCode) {
334        case 27: // ESC
335          $main.removeClass('show');
336          break;
337        case 38: // UP
338          selectItemByDiff(-1);
339          break;
340        case 40: // DOWN
341          selectItemByDiff(1);
342          break;
343        case 13: // ENTER
344          gotoLink($container.find('.searchbox-result-item.active').eq(0));
345          break;
346      }
347    })
348    .on('touchstart', (e) => {
349      touch = true;
350    })
351    .on('touchmove', (e) => {
352      touch = false;
353    });
354}

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.