OSDN Git Service

BugTrack/2594 Fix to work with other patterns
[pukiwiki/pukiwiki.git] / skin / search2.js
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
10 window.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 })