OSDN Git Service

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