PageSourceSearch

https://dinosquadsuriku.com/skin/search2.js

js dinosquadsuriku.com collected 2026-09-24 09:55:04 UTC 37,855 bytes, 1,113 lines download raw bytes

1// PukiWiki - Yet another WikiWikiWeb clone.
2// search2.js
3// Copyright
4//   2017-2020 PukiWiki Development Team
5// License: GPL v2 or (at your option) any later version
6//
7// PukiWiki search2 pluign - JavaScript client script
8/* eslint-env browser */
9// eslint-disable-next-line no-unused-expressions
10window.addEventListener && window.addEventListener('DOMContentLoaded', function () {
11  'use strict'
12  function enableSearch2 () {
13    var aroundLines = 2
14    var maxResultLines = 20
15    var defaultSearchWaitMilliseconds = 100
16    var defaultMaxResults = 1000
17    var kanaMap = null
18    var searchProps = {}
19    /**
20     * Escape HTML special charactors
21     *
22     * @param {string} s
23     */
24    function escapeHTML (s) {
25      if (typeof s !== 'string') {
26        return '' + s
27      }
28      return s.replace(/[&"<>]/g, function (m) {
29        return {
30          '&': '&amp;',
31          '"': '&quot;',
32          '<': '&lt;',
33          '>': '&gt;'
34        }[m]
35      })
36    }
37    /**
38     * @param {string} idText
39     * @param {number} defaultValue
40     * @type number
41     */
42    function getIntById (idText, defaultValue) {
43      var value = defaultValue
44      try {
45        var element = document.getElementById(idText)
46        if (element) {
47          value = parseInt(element.value, 10)
48          if (isNaN(value)) { // eslint-disable-line no-restricted-globals
49            value = defaultValue
50          }
51        }
52      } catch (e) {
53        value = defaultValue
54      }
55      return value
56    }
57    /**
58     * @param {string} idText
59     * @param {string} defaultValue
60     * @type string
61     */
62    function getTextById (idText, defaultValue) {
63      var value = defaultValue
64      try {
65        var element = document.getElementById(idText)
66        if (element.value) {
67          value = element.value
68        }
69      } catch (e) {
70        value = defaultValue
71      }
72      return value
73    }
74    function prepareSearchProps () {
75      var p = {}
76      p.errorMsg = getTextById('_plugin_search2_msg_error',
77        'An error occurred while processing.')
78      p.searchingMsg = getTextById('_plugin_search2_msg_searching',
79        'Searching...')
80      p.showingResultMsg = getTextById('_plugin_search2_msg_showing_result',
81        'Showing search results')
82      p.prevOffset = getTextById('_plugin_search2_prev_offset', '')
83      var baseUrlDefault = document.location.pathname + document.location.search
84      baseUrlDefault = baseUrlDefault.replace(/&offset=\d+/, '')
85      p.baseUrl = getTextById('_plugin_search2_base_url', baseUrlDefault)
86      p.msgPrevResultsTemplate = getTextById('_plugin_search2_msg_prev_results', 'Previous $1 pages')
87      p.msgMoreResultsTemplate = getTextById('_plugin_search2_msg_more_results', 'Next $1 pages')
88      p.user = getTextById('_plugin_search2_auth_user', '')
89      p.showingResultMsg = getTextById('_plugin_search2_msg_showing_result', 'Showing search results')
90      p.notFoundMessageTemplate = getTextById('_plugin_search2_msg_result_notfound',
91        'No page which contains $1 has been found.')
92      p.foundMessageTemplate = getTextById('_plugin_search2_msg_result_found',
93        'In the page <strong>$2</strong>, <strong>$3</strong> pages that contain all the terms $1 were found.')
94      p.maxResults = getIntById('_plugin_search2_max_results', defaultMaxResults)
95      p.searchInterval = getIntById('_plugin_search2_search_wait_milliseconds', defaultSearchWaitMilliseconds)
96      p.offset = getIntById('_plugin_search2_offset', 0)
97      searchProps = p
98    }
99    function getSiteProps () {
100      var empty = {}
101      var propsE = document.querySelector('#pukiwiki-site-properties .site-props')
102      if (!propsE) return empty
103      var props = JSON.parse(propsE.value)
104      return props || empty
105    }
106    /**
107     * @param {NodeList} nodeList
108     * @param {function(Node, number): void} func
109     */
110    function forEach (nodeList, func) {
111      if (nodeList.forEach) {
112        nodeList.forEach(func)
113      } else {
114        for (var i = 0, n = nodeList.length; i < n; i++) {
115          func(nodeList[i], i)
116        }
117      }
118    }
119    /**
120     * @param {string} text
121     * @param {RegExp} searchRegex
122     */
123    function findAndDecorateText (text, searchRegex) {
124      var isReplaced = false
125      var lastIndex = 0
126      var m
127      var decorated = ''
128      if (!searchRegex) return null
129      searchRegex.lastIndex = 0
130      while ((m = searchRegex.exec(text)) !== null) {
131        if (m[0] === '') {
132          // Fail-safe
133          console.log('Invalid searchRegex ' + searchRegex)
134          return null
135        }
136        isReplaced = true
137        var pre = text.substring(lastIndex, m.index)
138        decorated += escapeHTML(pre)
139        for (var i = 1; i < m.length; i++) {
140          if (m[i]) {
141            decorated += '<strong class="word' + (i - 1) + '">' + escapeHTML(m[i]) + '</strong>'
142          }
143        }
144        lastIndex = searchRegex.lastIndex
145      }
146      if (isReplaced) {
147        decorated += escapeHTML(text.substr(lastIndex))
148        return decorated
149      }
150      return null
151    }
152    /**
153     * @param {Object} session
154     * @param {string} searchText
155     * @param {RegExp} searchRegex
156     * @param {boolean} nowSearching
157     */
158    function getSearchResultMessage (session, searchText, searchRegex, nowSearching) {
159      var searchTextDecorated = findAndDecorateText(searchText, searchRegex)
160      if (searchTextDecorated === null) searchTextDecorated = escapeHTML(searchText)
161      var messageTemplate = searchProps.foundMessageTemplate
162      if (!nowSearching && session.hitPageCount === 0) {
163        messageTemplate = searchProps.notFoundMessageTemplate
164      }
165      var msg = messageTemplate.replace(/\$1|\$2|\$3/g, function (m) {
166        return {
167          $1: searchTextDecorated,
168          $2: session.hitPageCount,
169          $3: session.readPageCount
170        }[m]
171      })
172      return msg
173    }
174    /**
175     * @param {Object} session
176     */
177    function getSearchProgress (session) {
178      var progress = '(read:' + session.readPageCount + ', scan:' +
179        session.scanPageCount + ', all:' + session.pageCount
180      if (session.offset) {
181        progress += ', offset: ' + session.offset
182      }
183      progress += ')'
184      return progress
185    }
186    /**
187     * @param {Object} session
188     * @param {number} maxResults
189     */
190    function getOffsetLinks (session, maxResults) {
191      var baseUrl = searchProps.baseUrl
192      var links = []
193      if ('prevOffset' in session) {
194        var prevResultUrl = baseUrl
195        if (session.prevOffset > 0) {
196          prevResultUrl += '&offset=' + session.prevOffset
197        }
198        var msgPrev = searchProps.msgPrevResultsTemplate.replace(/\$1/, maxResults)
199        var prevResultHtml = '<a href="' + prevResultUrl + '">' + msgPrev + '</a>'
200        links.push(prevResultHtml)
201      }
202      if ('nextOffset' in session) {
203        var nextResultUrl = baseUrl + '&offset=' + session.nextOffset +
204          '&prev_offset=' + session.offset
205        var msgMore = searchProps.msgMoreResultsTemplate.replace(/\$1/, maxResults)
206        var moreResultHtml = '<a href="' + nextResultUrl + '">' + msgMore + '</a>'
207        links.push(moreResultHtml)
208      }
209      if (links.length > 0) {
210        return links.join(' ')
211      }
212      return ''
213    }
214    function prepareKanaMap () {
215      if (kanaMap !== null) return
216      if (!String.prototype.normalize) {
217        kanaMap = {}
218        return
219      }
220      var dakuten = '\uFF9E'
221      var maru = '\uFF9F'
222      var map = {}
223      for (var c = 0xFF61; c <= 0xFF9F; c++) {
224        var han = String.fromCharCode(c)
225        var zen = han.normalize('NFKC')
226        map[zen] = han
227        var hanDaku = han + dakuten
228        var zenDaku = hanDaku.normalize('NFKC')
229        if (zenDaku.length === 1) { // +Handaku-ten OK
230          map[zenDaku] = hanDaku
231        }
232        var hanMaru = han + maru
233        var zenMaru = hanMaru.normalize('NFKC')
234        if (zenMaru.length === 1) { // +Maru OK
235          map[zenMaru] = hanMaru
236        }
237      }
238      kanaMap = map
239    }
240    /**
241     * Hankaku to Zenkaku.
242     *
243     * @param {String} hankakuChar
244     * @returns {String}
245     */
246    function toZenkaku (hankakuChar) {
247      if (hankakuChar.length !== 1) {
248        return hankakuChar
249      }
250      var zenkakuChar = String.fromCharCode(hankakuChar.charCodeAt(0) + 0xfee0)
251      if (!String.prototype.normalize) {
252        return hankakuChar
253      }
254      if (zenkakuChar.normalize('NFKC') === hankakuChar) {
255        return zenkakuChar
256      }
257      return hankakuChar
258    }
259    /**
260     * @param {searchText} searchText
261     * @type RegExp
262     */
263    function textToRegex (searchText) {
264      if (!searchText) return null
265      //            1: Alphabet   2:Katakana        3:Hiragana        4:Wa kigo                                5:Other symbols
266      var regRep = /([a-zA-Z0-9])|([\u30a1-\u30f6])|([\u3041-\u3096])|([\u30fb\u30fc\u300c\u300d\u3001\u3002])|([\u0021-\u007e])/ig
267      var replacementFunc = function (m, m1, m2, m3, m4, m5) {
268        if (m1) {
269          // [a-zA-Z0-9]
270          return '[' + m1 + toZenkaku(m1) + ']'
271        } else if (m2) {
272          // Katakana
273          var r = '(?:' + String.fromCharCode(m2.charCodeAt(0) - 0x60) +
274            '|' + m2
275          if (kanaMap[m2]) {
276            r += '|' + kanaMap[m2]
277          }
278          r += ')'
279          return r
280        } else if (m3) {
281          // Hiragana
282          var katakana = String.fromCharCode(m3.charCodeAt(0) + 0x60)
283          var r2 = '(?:' + m3 + '|' + katakana
284          if (kanaMap[katakana]) {
285            r2 += '|' + kanaMap[katakana]
286          }
287          r2 += ')'
288          return r2
289        } else if (m4) {
290          // Wa kigo
291          if (kanaMap[m4]) {
292            return '[' + m4 + kanaMap[m4] + ']'
293          }
294          return m4
295        } else if (m5) {
296          // Other symbols
297          return '[' + '\\' + m5 + toZenkaku(m5) + ']'
298        }
299        return m
300      }
301      var s1 = searchText.replace(/^\s+|\s+$/g, '')
302      if (!s1) return null
303      var sp = s1.split(/\s+/)
304      var rText = ''
305      prepareKanaMap()
306      for (var i = 0; i < sp.length; i++) {
307        if (rText !== '') {
308          rText += '|'
309        }
310        var s = sp[i]
311        if (s.normalize) {
312          s = s.normalize('NFKC')
313        }
314        var s2 = s.replace(regRep, replacementFunc)
315        rText += '(' + s2 + ')'
316      }
317      return new RegExp(rText, 'ig')
318    }
319    /**
320     * @param {string} statusText
321     */
322    function setSearchStatus (statusText, statusText2) {
323      var statusList = document.querySelectorAll('._plugin_search2_search_status')
324      forEach(statusList, function (statusObj) {
325        var textObj1 = statusObj.querySelector('._plugin_search2_search_status_text1')
326        var textObj2 = statusObj.querySelector('._plugin_search2_search_status_text2')
327        if (textObj1) {
328          var prevText = textObj1.getAttribute('data-text')
329          if (prevText !== statusText) {
330            textObj1.setAttribute('data-text', statusText)
331            if (statusText.substr(statusText.length - 3) === '...') {
332              var firstHalf = statusText.substr(0, statusText.length - 3)
333              textObj1.textContent = firstHalf
334              var span = document.createElement('span')
335              span.innerHTML = '<span class="plugin-search2-progress plugin-search2-progress1">.</span>' +
336                '<span class="plugin-search2-progress plugin-search2-progress2">.</span>' +
337                '<span class="plugin-search2-progress plugin-search2-progress3">.</span>'
338              textObj1.appendChild(span)
339            } else {
340              textObj1.textContent = statusText
341            }
342          }
343        }
344        if (textObj2) {
345          if (statusText2) {
346            textObj2.textContent = ' ' + statusText2
347          } else {
348            textObj2.textContent = ''
349          }
350        }
351      })
352    }
353    /**
354     * @param {string} msgHTML
355     */
356    function setSearchMessage (msgHTML) {
357      var objList = document.querySelectorAll('._plugin_search2_message')
358      forEach(objList, function (obj) {
359        obj.innerHTML = msgHTML
360      })
361    }
362    function showSecondSearchForm () {
363      // Show second search form
364      var div = document.querySelector('._plugin_search2_second_form')
365      if (div) {
366        div.style.display = 'block'
367      }
368    }
369    /**
370     * @param {Element} form
371     * @type string
372     */
373    function getSearchBase (form) {
374      var f = form || document.querySelector('._plugin_search2_form')
375      var base = ''
376      forEach(f.querySelectorAll('input[name="base"]'), function (radio) {
377        if (radio.checked) base = radio.value
378      })
379      return base
380    }
381    /**
382     * Decorate found block (for pre innerHTML)
383     *
384     * @param {Object} block
385     * @param {RegExp} searchRegex
386     */
387    function decorateFoundBlock (block, searchRegex) {
388      var lines = []
389      for (var j = 0; j < block.lines.length; j++) {
390        var line = block.lines[j]
391        var decorated = findAndDecorateText(line, searchRegex)
392        if (decorated === null) {
393          lines.push('' + (block.startIndex + j + 1) + ':\t' + escapeHTML(line))
394        } else {
395          lines.push('' + (block.startIndex + j + 1) + ':\t' + decorated)
396        }
397      }
398      if (block.beyondLimit) {
399        lines.push('...')
400      }
401      return lines.join('\n')
402    }
403    /**
404     * @param {string} body
405     * @param {RegExp} searchRegex
406     */
407    function getSummaryInfo (body, searchRegex) {
408      var lines = body.split('\n')
409      var foundLines = []
410      var isInAuthorHeader = true
411      var lastFoundLineIndex = -1 - aroundLines
412      var lastAddedLineIndex = lastFoundLineIndex
413      var blocks = []
414      var lineCount = 0
415      var currentBlock = null
416      for (var index = 0, length = lines.length; index < length; index++) {
417        var line = lines[index]
418        if (isInAuthorHeader) {
419          // '#author line is not search target'
420          if (line.match(/^#author\(/)) {
421            // Remove this line from search target
422            continue
423          } else if (line.match(/^#freeze(\W|$)/)) {
424            // Still in header
425          } else {
426            // Already in body
427            isInAuthorHeader = false
428          }
429        }
430        var match = line.match(searchRegex)
431        if (!match) {
432          if (index < lastFoundLineIndex + aroundLines + 1) {
433            foundLines.push(lines[index])
434            lineCount++
435            lastAddedLineIndex = index
436          }
437        } else {
438          var startIndex = Math.max(Math.max(lastAddedLineIndex + 1, index - aroundLines), 0)
439          if (lastAddedLineIndex + 1 < startIndex) {
440            // Newly found!
441            var block = {
442              startIndex: startIndex,
443              foundLineIndex: index,
444              lines: []
445            }
446            currentBlock = block
447            foundLines = block.lines
448            blocks.push(block)
449          }
450          if (lineCount >= maxResultLines) {
451            currentBlock.beyondLimit = true
452            return blocks
453          }
454          for (var i = startIndex; i < index; i++) {
455            foundLines.push(lines[i])
456            lineCount++
457          }
458          foundLines.push(line)
459          lineCount++
460          lastFoundLineIndex = lastAddedLineIndex = index
461        }
462      }
463      return blocks
464    }
465    /**
466     * @param {Date} now
467     * @param {string} dateText
468     */
469    function getPassage (now, dateText) {
470      if (!dateText) {
471        return ''
472      }
473      var units = [{ u: 'm', max: 60 }, { u: 'h', max: 24 }, { u: 'd', max: 1 }]
474      var d = new Date()
475      d.setTime(Date.parse(dateText))
476      var t = (now.getTime() - d.getTime()) / (1000 * 60) // minutes
477      var unit = units[0].u; var card = units[0].max
478      for (var i = 0; i < units.length; i++) {
479        unit = units[i].u; card = units[i].max
480        if (t < card) break
481        t = t / card
482      }
483      return '(' + Math.floor(t) + unit + ')'
484    }
485    /**
486     * @param {string} searchText
487     */
488    function removeSearchOperators (searchText) {
489      var sp = searchText.split(/\s+/)
490      if (sp.length <= 1) {
491        return searchText
492      }
493      for (var i = sp.length - 2; i >= 1; i--) {
494        if (sp[i] === 'OR') {
495          sp.splice(i, 1)
496        }
497      }
498      return sp.join(' ')
499    }
500    /**
501     * @param {string} pathname
502     */
503    function getSearchCacheKeyBase (pathname) {
504      return 'path.' + pathname + '.search2.'
505    }
506    /**
507     * @param {string} pathname
508     */
509    function getSearchCacheKeyDateBase (pathname) {
510      var now = new Date()
511      var dateKey = now.getFullYear() + '_0' + (now.getMonth() + 1) + '_0' + now.getDate()
512      dateKey = dateKey.replace(/_\d?(\d\d)/g, '$1')
513      return getSearchCacheKeyBase(pathname) + dateKey + '.'
514    }
515    /**
516     * @param {string} pathname
517     * @param {string} searchText
518     * @param {number} offset
519     */
520    function getSearchCacheKey (pathname, searchText, offset) {
521      return getSearchCacheKeyDateBase(pathname) + 'offset=' + offset +
522        '.' + searchText
523    }
524    /**
525     * @param {string} pathname
526     * @param {string} searchText
527     */
528    function clearSingleCache (pathname, searchText) {
529      if (!window.localStorage) return
530      var removeTargets = []
531      var keyBase = getSearchCacheKeyDateBase(pathname)
532      for (var i = 0, n = localStorage.length; i < n; i++) {
533        var key = localStorage.key(i)
534        if (key.substr(0, keyBase.length) === keyBase) {
535          // Search result Cache
536          var subKey = key.substr(keyBase.length)
537          var m = subKey.match(/^offset=\d+\.(.+)$/)
538          if (m && m[1] === searchText) {
539            removeTargets.push(key)
540          }
541        }
542      }
543      removeTargets.forEach(function (target) {
544        localStorage.removeItem(target)
545      })
546    }
547    /**
548     * @param {string} body
549     */
550    function getBodySummary (body) {
551      var lines = body.split('\n')
552      var isInAuthorHeader = true
553      var summary = []
554      for (var index = 0, length = lines.length; index < length; index++) {
555        var line = lines[index]
556        if (isInAuthorHeader) {
557          // '#author line is not search target'
558          if (line.match(/^#author\(/)) {
559            // Remove this line from search target
560            continue
561          } else if (line.match(/^#freeze(\W|$)/)) {
562            continue
563            // Still in header
564          } else {
565            // Already in body
566            isInAuthorHeader = false
567          }
568        }
569        line = line.replace(/^\s+|\s+$/g, '')
570        if (line.length === 0) continue // Empty line
571        if (line.match(/^#\w+/)) continue // Block-type plugin
572        if (line.match(/^\/\//)) continue // Comment
573        if (line.substr(0, 1) === '*') {
574          line = line.replace(/\s*\[#\w+\]$/, '') // Remove anchor
575        }
576        summary.push(line)
577        if (summary.length >= 10) {
578          continue
579        }
580      }
581      return summary.join(' ').substring(0, 150)
582    }
583    /**
584     * @param {string} q searchText
585     */
586    function encodeSearchText (q) {
587      var sp = q.split(/\s+/)
588      for (var i = 0; i < sp.length; i++) {
589        sp[i] = encodeURIComponent(sp[i])
590      }
591      return sp.join('+')
592    }
593    /**
594     * @param {string} q searchText
595     */
596    function encodeSearchTextForHash (q) {
597      var sp = q.split(/\s+/)
598      return sp.join('+')
599    }
600    function getSearchTextInLocationHash () {
601      var hash = document.location.hash
602      if (!hash) return ''
603      var q = ''
604      if (hash.substr(0, 3) === '#q=') {
605        q = hash.substr(3).replace(/\+/g, ' ')
606      } else {
607        return ''
608      }
609      var decodedQ = decodeURIComponent(q)
610      if (q !== decodedQ) {
611        q = decodedQ + ' OR ' + q
612      }
613      return q
614    }
615    function colorSearchTextInBody () {
616      var searchText = getSearchTextInLocationHash()
617      if (!searchText) return
618      var searchRegex = textToRegex(removeSearchOperators(searchText))
619      if (!searchRegex) return
620      var ignoreTags = ['INPUT', 'TEXTAREA', 'BUTTON',
621        'SCRIPT', 'FRAME', 'IFRAME']
622      /**
623       * @param {Element} element
624       */
625      function colorSearchText (element) {
626        var decorated = findAndDecorateText(element.nodeValue, searchRegex)
627        if (decorated) {
628          var span = document.createElement('span')
629          span.innerHTML = decorated
630          element.parentNode.replaceChild(span, element)
631        }
632      }
633      /**
634       * @param {Element} element
635       */
636      function walkElement (element) {
637        var e = element.firstChild
638        while (e) {
639          if (e.nodeType === 3 && e.nodeValue &&
640              e.nodeValue.length >= 2 && /\S/.test(e.nodeValue)) {
641            var next = e.nextSibling
642            colorSearchText(e, searchRegex)
643            e = next
644          } else {
645            if (e.nodeType === 1 && ignoreTags.indexOf(e.tagName) === -1) {
646              walkElement(e)
647            }
648            e = e.nextSibling
649          }
650        }
651      }
652      var target = document.getElementById('body')
653      walkElement(target)
654    }
655    /**
656     * @param {Array<Object>} newResults
657     * @param {Element} ul
658     */
659    function removePastResults (newResults, ul) {
660      var removedCount = 0
661      var nodes = ul.childNodes
662      for (var i = nodes.length - 1; i >= 0; i--) {
663        var node = nodes[i]
664        if (node.tagName !== 'LI' && node.tagName !== 'DIV') continue
665        var nodePagename = node.getAttribute('data-pagename')
666        var isRemoveTarget = false
667        for (var j = 0, n = newResults.length; j < n; j++) {
668          var r = newResults[j]
669          if (r.name === nodePagename) {
670            isRemoveTarget = true
671            break
672          }
673        }
674        if (isRemoveTarget) {
675          if (node.tagName === 'LI') {
676            removedCount++
677          }
678          ul.removeChild(node)
679        }
680      }
681      return removedCount
682    }
683    /**
684     * @param {Array<Object>} results
685     * @param {string} searchText
686     * @param {RegExp} searchRegex
687     * @param {Element} parentUlElement
688     * @param {boolean} insertTop
689     */
690    function addSearchResult (results, searchText, searchRegex, parentUlElement, insertTop) {
691      var props = getSiteProps()
692      var now = new Date()
693      var parentFragment = document.createDocumentFragment()
694      results.forEach(function (val) {
695        var li = document.createElement('li')
696        var hash = '#q=' + encodeSearchTextForHash(searchText)
697        var href = val.url + hash
698        var decoratedName = findAndDecorateText(val.name, searchRegex)
699        if (!decoratedName) {
700          decoratedName = escapeHTML(val.name)
701        }
702        var updatedAt = val.updatedAt
703        var passageHtml = ''
704        if (props.show_passage) {
705          passageHtml = ' ' + getPassage(now, updatedAt)
706        }
707        var liHtml = '<a href="' + escapeHTML(href) + '">' +
708          decoratedName + '</a>' + passageHtml
709        li.innerHTML = liHtml
710        li.setAttribute('data-pagename', val.name)
711        // Page detail div
712        var div = document.createElement('div')
713        div.classList.add('search-result-detail')
714        var head = document.createElement('div')
715        head.classList.add('search-result-page-summary')
716        head.innerHTML = escapeHTML(val.bodySummary)
717        div.appendChild(head)
718        var summaryInfo = val.hitSummary
719        for (var i = 0; i < summaryInfo.length; i++) {
720          var pre = document.createElement('pre')
721          pre.innerHTML = decorateFoundBlock(summaryInfo[i], searchRegex)
722          div.appendChild(pre)
723        }
724        div.setAttribute('data-pagename', val.name)
725        // Add li to ul (parentUlElement)
726        li.appendChild(div)
727        parentFragment.appendChild(li)
728      })
729      if (insertTop && parentUlElement.firstChild) {
730        parentUlElement.insertBefore(parentFragment, parentUlElement.firstChild)
731      } else {
732        parentUlElement.appendChild(parentFragment)
733      }
734    }
735    function removeCachedResultsBase (keepTodayCache) {
736      var props = getSiteProps()
737      if (!props || !props.base_uri_pathname) return
738      var keyPrefix = getSearchCacheKeyDateBase(props.base_uri_pathname)
739      var keyBase = getSearchCacheKeyBase(props.base_uri_pathname)
740      var removeTargets = []
741      for (var i = 0, n = localStorage.length; i < n; i++) {
742        var key = localStorage.key(i)
743        if (key.substr(0, keyBase.length) === keyBase) {
744          // Search result Cache
745          if (keepTodayCache) {
746            if (key.substr(0, keyPrefix.length) !== keyPrefix) {
747              removeTargets.push(key)
748            }
749          } else {
750            removeTargets.push(key)
751          }
752        }
753      }
754      removeTargets.forEach(function (target) {
755        localStorage.removeItem(target)
756      })
757    }
758    function removeCachedResults () {
759      removeCachedResultsBase(true)
760    }
761    function removeAllCachedResults () {
762      removeCachedResultsBase(false)
763    }
764    /**
765     * @param {Object} obj
766     * @param {Object} session
767     * @param {string} searchText
768     * @param {number} prevTimestamp
769     */
770    function showResult (obj, session, searchText, prevTimestamp) {
771      var props = getSiteProps()
772      var searchRegex = textToRegex(removeSearchOperators(searchText))
773      var ul = document.querySelector('#_plugin_search2_result-list')
774      if (!ul) return
775      if (obj.start_index === 0 && !prevTimestamp) {
776        ul.innerHTML = ''
777      }
778      var searchDone = obj.search_done
779      if (!session.scanPageCount) session.scanPageCount = 0
780      if (!session.readPageCount) session.readPageCount = 0
781      if (!session.hitPageCount) session.hitPageCount = 0
782      var prevHitPageCount = session.hitPageCount
783      session.hitPageCount += obj.results.length
784      if (!prevTimestamp) {
785        session.scanPageCount += obj.scan_page_count
786        session.readPageCount += obj.read_page_count
787        session.pageCount = obj.page_count
788      }
789      session.searchStartTime = obj.search_start_time
790      session.authUser = obj.auth_user
791      if (prevHitPageCount === 0 && session.hitPageCount > 0) {
792        showSecondSearchForm()
793      }
794      var results = obj.results
795      var cachedResults = []
796      results.forEach(function (val) {
797        var cache = {}
798        cache.name = val.name
799        cache.url = val.url
800        cache.updatedAt = val.updated_at
801        cache.updatedTime = val.updated_time
802        cache.bodySummary = getBodySummary(val.body)
803        cache.hitSummary = getSummaryInfo(val.body, searchRegex)
804        cachedResults.push(cache)
805      })
806      if (prevTimestamp) {
807        var removedCount = removePastResults(cachedResults, ul)
808        session.hitPageCount -= removedCount
809      }
810      var msg = getSearchResultMessage(session, searchText, searchRegex, !searchDone)
811      setSearchMessage(msg)
812      if (prevTimestamp) {
813        setSearchStatus(searchProps.searchingMsg)
814      } else {
815        setSearchStatus(searchProps.searchingMsg,
816          getSearchProgress(session))
817      }
818      if (searchDone) {
819        var singlePageResult = session.offset === 0 && !session.nextOffset
820        var progress = getSearchProgress(session)
821        setTimeout(function () {
822          if (singlePageResult) {
823            setSearchStatus('')
824          } else {
825            setSearchStatus(searchProps.showingResultMsg, progress)
826          }
827        }, 2000)
828      }
829      if (session.results) {
830        if (prevTimestamp) {
831          var newResult = [].concat(cachedResults)
832          Array.prototype.push.apply(newResult, session.results)
833          session.results = newResult
834        } else {
835          Array.prototype.push.apply(session.results, cachedResults)
836        }
837      } else {
838        session.results = cachedResults
839      }
840      addSearchResult(cachedResults, searchText, searchRegex, ul, prevTimestamp)
841      var maxResults = searchProps.maxResults
842      if (searchDone) {
843        session.searchText = searchText
844        var prevOffset = searchProps.prevOffset
845        if (prevOffset) {
846          session.prevOffset = parseInt(prevOffset, 10)
847        }
848        var json = JSON.stringify(session)
849        var cacheKey = getSearchCacheKey(props.base_uri_pathname, searchText, session.offset)
850        if (window.localStorage) {
851          try {
852            localStorage[cacheKey] = json
853          } catch (e) {
854            // QuotaExceededError "exceeded the quota."
855            console.log(e)
856            removeAllCachedResults()
857          }
858        }
859        if ('prevOffset' in session || 'nextOffset' in session) {
860          setSearchMessage(msg + ' ' + getOffsetLinks(session, maxResults))
861        }
862      }
863      if (!searchDone && obj.next_start_index) {
864        if (session.results.length >= maxResults) {
865          // Save results
866          session.nextOffset = obj.next_start_index
867          var prevOffset2 = searchProps.prevOffset
868          if (prevOffset2) {
869            session.prevOffset = parseInt(prevOffset2, 10)
870          }
871          var key = getSearchCacheKey(props.base_uri_pathname, searchText, session.offset)
872          localStorage[key] = JSON.stringify(session)
873          // Stop API calling
874          setSearchMessage(msg + ' ' + getOffsetLinks(session, maxResults))
875          setSearchStatus(searchProps.showingResultMsg,
876            getSearchProgress(session))
877        } else {
878          setTimeout(function () {
879            doSearch(searchText, // eslint-disable-line no-use-before-define
880              session, obj.next_start_index,
881              obj.search_start_time)
882          }, searchProps.searchInterval)
883        }
884      }
885    }
886    /**
887     * @param {string} searchText
888     * @param {string} base
889     * @param {number} offset
890     */
891    function showCachedResult (searchText, base, offset) {
892      var props = getSiteProps()
893      var searchRegex = textToRegex(removeSearchOperators(searchText))
894      var ul = document.querySelector('#_plugin_search2_result-list')
895      if (!ul) return null
896      var searchCacheKey = getSearchCacheKey(props.base_uri_pathname, searchText, offset)
897      var cache1 = localStorage[searchCacheKey]
898      if (!cache1) {
899        return null
900      }
901      var session = JSON.parse(cache1)
902      if (!session) return null
903      if (base !== session.base) {
904        return null
905      }
906      var user = searchProps.user
907      if (user !== session.authUser) {
908        return null
909      }
910      if (session.hitPageCount > 0) {
911        showSecondSearchForm()
912      }
913      var msg = getSearchResultMessage(session, searchText, searchRegex, false)
914      setSearchMessage(msg)
915      addSearchResult(session.results, searchText, searchRegex, ul)
916      var maxResults = searchProps.maxResults
917      if ('prevOffset' in session || 'nextOffset' in session) {
918        var moreResultHtml = getOffsetLinks(session, maxResults)
919        setSearchMessage(msg + ' ' + moreResultHtml)
920        var progress = getSearchProgress(session)
921        setSearchStatus(searchProps.showingResultMsg, progress)
922      } else {
923        setSearchStatus('')
924      }
925      return session
926    }
927    /**
928     * @param {string} searchText
929     * @param {object} session
930     * @param {number} startIndex
931     * @param {number} searchStartTime
932     * @param {number} prevTimestamp
933     */
934    function doSearch (searchText, session, startIndex, searchStartTime, prevTimestamp) {
935      var props = getSiteProps()
936      var baseUrl = './'
937      if (props.base_uri_pathname) {
938        baseUrl = props.base_uri_pathname
939      }
940      var url = baseUrl + '?cmd=search2&action=query'
941      url += '&encode_hint=' + encodeURIComponent('\u3077')
942      if (searchText) {
943        url += '&q=' + encodeURIComponent(searchText)
944      }
945      if (session.base) {
946        url += '&base=' + encodeURIComponent(session.base)
947      }
948      if (prevTimestamp) {
949        url += '&modified_since=' + prevTimestamp
950      } else {
951        url += '&start=' + startIndex
952        if (searchStartTime) {
953          url += '&search_start_time=' + encodeURIComponent(searchStartTime)
954        }
955        if (!('offset' in session)) {
956          session.offset = startIndex
957        }
958      }
959      fetch(url, { credentials: 'same-origin' }
960      ).then(function (response) {
961        if (response.ok) {
962          return response.json()
963        }
964        throw new Error(response.status + ': ' +
965          response.statusText + ' on ' + url)
966      }).then(function (obj) {
967        showResult(obj, session, searchText, prevTimestamp)
968      })['catch'](function (err) { // eslint-disable-line dot-notation
969        if (window.console && console.log) {
970          console.log(err)
971          console.log('Error! Please check JavaScript console\n' + JSON.stringify(err) + '|' + err)
972        }
973        setSearchStatus(searchProps.errorMsg)
974      })
975    }
976    function hookSearch2 () {
977      var form = document.querySelector('form')
978      if (form && form.q) {
979        var q = form.q
980        if (q.value === '') {
981          q.focus()
982        }
983      }
984    }
985    function removeEncodeHint () {
986      // Remove 'encode_hint' if site charset is UTF-8
987      var props = getSiteProps()
988      if (!props.is_utf8) return
989      var forms = document.querySelectorAll('form')
990      forEach(forms, function (form) {
991        if (form.cmd && form.cmd.value === 'search2') {
992          if (form.encode_hint && (typeof form.encode_hint.removeAttribute === 'function')) {
993            form.encode_hint.removeAttribute('name')
994          }
995        }
996      })
997    }
998    function kickFirstSearch () {
999      var form = document.querySelector('._plugin_search2_form')
1000      var searchText = form && form.q
1001      if (!searchText) return
1002      if (searchText && searchText.value) {
1003        var offset = searchProps.offset
1004        var base = getSearchBase(form)
1005        var prevSession = showCachedResult(searchText.value, base, offset)
1006        if (prevSession) {
1007          // Display Cache results, then search only modified pages
1008          if (!('offset' in prevSession) || prevSession.offset === 0) {
1009            doSearch(searchText.value, prevSession, offset, null,
1010              prevSession.searchStartTime)
1011          } else {
1012            // Show search results
1013          }
1014        } else {
1015          doSearch(searchText.value, { base: base, offset: offset }, offset, null)
1016        }
1017        removeCachedResults()
1018      }
1019    }
1020    function replaceSearchWithSearch2 () {
1021      forEach(document.querySelectorAll('form'), function (f) {
1022        function onAndRadioClick () {
1023          var sp = removeSearchOperators(f.word.value).split(/\s+/)
1024          var newText = sp.join(' ')
1025          if (f.word.value !== newText) {
1026            f.word.value = newText
1027          }
1028        }
1029        function onOrRadioClick () {
1030          var sp = removeSearchOperators(f.word.value).split(/\s+/)
1031          var newText = sp.join(' OR ')
1032          if (f.word.value !== newText) {
1033            f.word.value = newText
1034          }
1035        }
1036        if (f.action.match(/cmd=search$/)) {
1037          f.addEventListener('submit', function (e) {
1038            var q = e.target.word.value
1039            var base = ''
1040            forEach(f.querySelectorAll('input[name="base"]'), function (radio) {
1041              if (radio.checked) base = radio.value
1042            })
1043            var props = getSiteProps()
1044            var loc = document.location
1045            var baseUri = loc.protocol + '//' + loc.host + loc.pathname
1046            if (props.base_uri_pathname) {
1047              baseUri = props.base_uri_pathname
1048            }
1049            var url = baseUri + '?' +
1050              (props.is_utf8 ? '' : 'encode_hint=' +
1051                encodeURIComponent('\u3077') + '&') +
1052              'cmd=search2' +
1053              '&q=' + encodeSearchText(q) +
1054              (base ? '&base=' + encodeURIComponent(base) : '')
1055            e.preventDefault()
1056            setTimeout(function () {
1057              window.location.href = url
1058            }, 1)
1059            return false
1060          })
1061          var radios = f.querySelectorAll('input[type="radio"][name="type"]')
1062          forEach(radios, function (radio) {
1063            if (radio.value === 'AND') {
1064              radio.addEventListener('click', onAndRadioClick)
1065            } else if (radio.value === 'OR') {
1066              radio.addEventListener('click', onOrRadioClick)
1067            }
1068          })
1069        } else if (f.cmd && f.cmd.value === 'search2') {
1070          f.addEventListener('submit', function () {
1071            var newSearchText = f.q.value
1072            var prevSearchText = f.q.getAttribute('data-original-q')
1073            if (newSearchText === prevSearchText) {
1074              // Clear resultCache to search same text again
1075              var props = getSiteProps()
1076              clearSingleCache(props.base_uri_pathname, prevSearchText)
1077            }
1078          })
1079        }
1080      })
1081    }
1082    function showNoSupportMessage () {
1083      var pList = document.getElementsByClassName('_plugin_search2_nosupport_message')
1084      for (var i = 0; i < pList.length; i++) {
1085        var p = pList[i]
1086        p.style.display = 'block'
1087      }
1088    }
1089    function isEnabledFetchFunctions () {
1090      if (window.fetch && document.querySelector && window.JSON) {
1091        return true
1092      }
1093      return false
1094    }
1095    function isEnableServerFunctions () {
1096      var props = getSiteProps()
1097      if (props.json_enabled) return true
1098      return false
1099    }
1100    prepareSearchProps()
1101    colorSearchTextInBody()
1102    if (!isEnabledFetchFunctions()) {
1103      showNoSupportMessage()
1104      return
1105    }
1106    if (!isEnableServerFunctions()) return
1107    replaceSearchWithSearch2()
1108    hookSearch2()
1109    removeEncodeHint()
1110    kickFirstSearch()
1111  }
1112  enableSearch2()
1113})

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.