1 // PukiWiki - Yet another WikiWikiWeb clone.
4 // 2017 PukiWiki Development Team
5 // License: GPL v2 or (at your option) any later version
7 // PukiWiki search2 pluign - JavaScript client script
8 window.addEventListener && window.addEventListener('DOMContentLoaded', function() { // eslint-disable-line no-unused-expressions
10 function enableSearch2() {
12 var maxResultLines = 20;
13 var defaultSearchWaitMilliseconds = 100;
14 var defaultMaxResults = 1000;
18 * Escape HTML special charactors
22 function escapeHTML(s) {
23 if (typeof s !== 'string') {
26 return s.replace(/[&"<>]/g, function(m) {
36 * @param {string} idText
37 * @param {number} defaultValue
40 function getIntById(idText, defaultValue) {
41 var value = defaultValue;
43 var element = document.getElementById(idText);
45 value = parseInt(element.value, 10);
46 if (isNaN(value)) { // eslint-disable-line no-restricted-globals
56 * @param {string} idText
57 * @param {string} defaultValue
60 function getTextById(idText, defaultValue) {
61 var value = defaultValue;
63 var element = document.getElementById(idText);
65 value = element.value;
72 function prepareSearchProps() {
74 p.errorMsg = getTextById('_plugin_search2_msg_error',
75 'An error occurred while processing.');
76 p.searchingMsg = getTextById('_plugin_search2_msg_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);
97 function getSiteProps() {
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;
105 * @param {NodeList} nodeList
106 * @param {function(Node, number): void} func
108 function forEach(nodeList, func) {
109 if (nodeList.forEach) {
110 nodeList.forEach(func);
112 for (var i = 0, n = nodeList.length; i < n; i++) {
113 func(nodeList[i], i);
118 * @param {string} text
119 * @param {RegExp} searchRegex
121 function findAndDecorateText(text, searchRegex) {
122 var isReplaced = false;
126 if (!searchRegex) return null;
127 searchRegex.lastIndex = 0;
128 while ((m = searchRegex.exec(text)) !== null) {
131 console.log('Invalid searchRegex ' + searchRegex);
135 var pre = text.substring(lastIndex, m.index);
136 decorated += escapeHTML(pre);
137 for (var i = 1; i < m.length; i++) {
139 decorated += '<strong class="word' + (i - 1) + '">' + escapeHTML(m[i]) + '</strong>';
142 lastIndex = searchRegex.lastIndex;
145 decorated += escapeHTML(text.substr(lastIndex));
151 * @param {Object} session
152 * @param {string} searchText
153 * @param {RegExp} searchRegex
154 * @param {boolean} nowSearching
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;
163 var msg = messageTemplate.replace(/\$1|\$2|\$3/g, function(m) {
165 $1: searchTextDecorated,
166 $2: session.hitPageCount,
167 $3: session.readPageCount
173 * @param {Object} session
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;
185 * @param {Object} session
186 * @param {number} maxResults
188 function getOffsetLinks(session, maxResults) {
189 var baseUrl = searchProps.baseUrl;
191 if ('prevOffset' in session) {
192 var prevResultUrl = baseUrl;
193 if (session.prevOffset > 0) {
194 prevResultUrl += '&offset=' + session.prevOffset;
196 var msgPrev = searchProps.msgPrevResultsTemplate.replace(/\$1/, maxResults);
197 var prevResultHtml = '<a href="' + prevResultUrl + '">' + msgPrev + '</a>';
198 links.push(prevResultHtml);
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);
207 if (links.length > 0) {
208 return links.join(' ');
212 function prepareKanaMap() {
213 if (kanaMap !== null) return;
214 if (!String.prototype.normalize) {
218 var dakuten = '\uFF9E';
221 for (var c = 0xFF61; c <= 0xFF9F; c++) {
222 var han = String.fromCharCode(c);
223 var zen = han.normalize('NFKC');
225 var hanDaku = han + dakuten;
226 var zenDaku = hanDaku.normalize('NFKC');
227 if (zenDaku.length === 1) { // +Handaku-ten OK
228 map[zenDaku] = hanDaku;
230 var hanMaru = han + maru;
231 var zenMaru = hanMaru.normalize('NFKC');
232 if (zenMaru.length === 1) { // +Maru OK
233 map[zenMaru] = hanMaru;
239 * @param {searchText} searchText
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) {
248 // Symbol - escape with prior backslach
252 var r = '(?:' + String.fromCharCode(m2.charCodeAt(0) - 0x60) +
255 r += '|' + kanaMap[m2];
261 var katakana = String.fromCharCode(m3.charCodeAt(0) + 0x60);
262 var r2 = '(?:' + m3 + '|' + katakana;
263 if (kanaMap[katakana]) {
264 r2 += '|' + kanaMap[katakana];
271 var s1 = searchText.replace(/^\s+|\s+$/g, '');
272 if (!s1) return null;
273 var sp = s1.split(/\s+/);
276 for (var i = 0; i < sp.length; i++) {
282 s = s.normalize('NFKC');
284 var s2 = s.replace(regRep, replacementFunc);
285 rText += '(' + s2 + ')';
287 return new RegExp(rText, 'ig');
290 * @param {string} statusText
292 function setSearchStatus(statusText) {
293 var statusList = document.querySelectorAll('._plugin_search2_search_status');
294 forEach(statusList, function(statusObj) {
295 statusObj.textContent = statusText;
299 * @param {string} msgHTML
301 function setSearchMessage(msgHTML) {
302 var objList = document.querySelectorAll('._plugin_search2_message');
303 forEach(objList, function(obj) {
304 obj.innerHTML = msgHTML;
307 function showSecondSearchForm() {
308 // Show second search form
309 var div = document.querySelector('._plugin_search2_second_form');
311 div.style.display = 'block';
315 * @param {Element} form
318 function getSearchBase(form) {
319 var f = form || document.querySelector('._plugin_search2_form');
321 forEach(f.querySelectorAll('input[name="base"]'), function(radio) {
322 if (radio.checked) base = radio.value;
327 * Decorate found block (for pre innerHTML)
329 * @param {Object} block
330 * @param {RegExp} searchRegex
332 function decorateFoundBlock(block, searchRegex) {
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));
340 lines.push('' + (block.startIndex + j + 1) + ':\t' + decorated);
343 if (block.beyondLimit) {
346 return lines.join('\n');
349 * @param {string} body
350 * @param {RegExp} searchRegex
352 function getSummaryInfo(body, searchRegex) {
353 var lines = body.split('\n');
355 var isInAuthorHeader = true;
356 var lastFoundLineIndex = -1 - aroundLines;
357 var lastAddedLineIndex = lastFoundLineIndex;
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
368 } else if (line.match(/^#freeze(\W|$)/)) {
372 isInAuthorHeader = false;
375 var match = line.match(searchRegex);
377 if (index < lastFoundLineIndex + aroundLines + 1) {
378 foundLines.push(lines[index]);
380 lastAddedLineIndex = index;
383 var startIndex = Math.max(Math.max(lastAddedLineIndex + 1, index - aroundLines), 0);
384 if (lastAddedLineIndex + 1 < startIndex) {
387 startIndex: startIndex,
388 foundLineIndex: index,
391 currentBlock = block;
392 foundLines = block.lines;
395 if (lineCount >= maxResultLines) {
396 currentBlock.beyondLimit = true;
399 for (var i = startIndex; i < index; i++) {
400 foundLines.push(lines[i]);
403 foundLines.push(line);
405 lastFoundLineIndex = lastAddedLineIndex = index;
412 * @param {string} dateText
414 function getPassage(now, dateText) {
418 var units = [{u: 'm', max: 60}, {u: 'h', max: 24}, {u: 'd', max: 1}];
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;
428 return '(' + Math.floor(t) + unit + ')';
431 * @param {string} searchText
433 function removeSearchOperators(searchText) {
434 var sp = searchText.split(/\s+/);
435 if (sp.length <= 1) {
438 for (var i = sp.length - 2; i >= 1; i--) {
439 if (sp[i] === 'OR') {
446 * @param {string} pathname
448 function getSearchCacheKeyBase(pathname) {
449 return 'path.' + pathname + '.search2.';
452 * @param {string} pathname
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 + '.';
461 * @param {string} pathname
462 * @param {string} searchText
463 * @param {number} offset
465 function getSearchCacheKey(pathname, searchText, offset) {
466 return getSearchCacheKeyDateBase(pathname) + 'offset=' + offset +
470 * @param {string} pathname
471 * @param {string} searchText
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);
488 removeTargets.forEach(function(target) {
489 localStorage.removeItem(target);
493 * @param {string} body
495 function getBodySummary(body) {
496 var lines = body.split('\n');
497 var isInAuthorHeader = true;
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
506 } else if (line.match(/^#freeze(\W|$)/)) {
511 isInAuthorHeader = false;
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
522 if (summary.length >= 10) {
526 return summary.join(' ').substring(0, 150);
529 * @param {string} q searchText
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]);
539 * @param {string} q searchText
541 function encodeSearchTextForHash(q) {
542 var sp = q.split(/\s+/);
545 function getSearchTextInLocationHash() {
546 var hash = document.location.hash;
547 if (!hash) return '';
549 if (hash.substr(0, 3) === '#q=') {
550 q = hash.substr(3).replace(/\+/g, ' ');
554 var decodedQ = decodeURIComponent(q);
555 if (q !== decodedQ) {
556 q = decodedQ + ' OR ' + q;
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'];
568 * @param {Element} element
570 function colorSearchText(element) {
571 var decorated = findAndDecorateText(element.nodeValue, searchRegex);
573 var span = document.createElement('span');
574 span.innerHTML = decorated;
575 element.parentNode.replaceChild(span, element);
579 * @param {Element} element
581 function walkElement(element) {
582 var e = element.firstChild;
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);
590 if (e.nodeType === 1 && ignoreTags.indexOf(e.tagName) === -1) {
597 var target = document.getElementById('body');
601 * @param {Array<Object>} newResults
602 * @param {Element} ul
604 function removePastResults(newResults, ul) {
605 var removedCount = 0;
606 var nodes = ul.childNodes;
607 for (var i = nodes.length - 1; i >= 0; 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;
619 if (isRemoveTarget) {
620 if (node.tagName === 'LI') {
623 ul.removeChild(node);
629 * @param {Array<Object>} results
630 * @param {string} searchText
631 * @param {RegExp} searchRegex
632 * @param {Element} parentElement
633 * @param {boolean} insertTop
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);
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);
665 div.setAttribute('data-pagename', val.name);
666 fragment.appendChild(div);
667 parentFragment.appendChild(fragment);
669 if (insertTop && parentElement.firstChild) {
670 parentElement.insertBefore(parentFragment, parentElement.firstChild);
672 parentElement.appendChild(parentFragment);
676 * @param {Object} obj
677 * @param {Object} session
678 * @param {string} searchText
679 * @param {number} prevTimestamp
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');
686 if (obj.start_index === 0 && !prevTimestamp) {
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;
700 session.searchStartTime = obj.search_start_time;
701 session.authUser = obj.auth_user;
702 if (prevHitPageCount === 0 && session.hitPageCount > 0) {
703 showSecondSearchForm();
705 var results = obj.results;
706 var cachedResults = [];
707 results.forEach(function(val) {
709 cache.name = val.name;
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);
718 var removedCount = removePastResults(cachedResults, ul);
719 session.hitPageCount -= removedCount;
721 var msg = getSearchResultMessage(session, searchText, searchRegex, !searchDone);
722 setSearchMessage(msg);
724 setSearchStatus(searchProps.searchingMsg);
726 setSearchStatus(searchProps.searchingMsg + ' ' +
727 getSearchProgress(session));
730 var singlePageResult = session.offset === 0 && !session.nextOffset;
731 var progress = getSearchProgress(session);
732 setTimeout(function() {
733 if (singlePageResult) {
736 setSearchStatus(searchProps.showingResultMsg + ' ' + progress);
740 if (session.results) {
742 var newResult = [].concat(cachedResults);
743 Array.prototype.push.apply(newResult, session.results);
744 session.results = newResult;
746 Array.prototype.push.apply(session.results, cachedResults);
749 session.results = cachedResults;
751 addSearchResult(cachedResults, searchText, searchRegex, ul, prevTimestamp);
752 var maxResults = searchProps.maxResults;
754 session.searchText = searchText;
755 var prevOffset = searchProps.prevOffset;
757 session.prevOffset = parseInt(prevOffset, 10);
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;
764 if ('prevOffset' in session || 'nextOffset' in session) {
765 setSearchMessage(msg + ' ' + getOffsetLinks(session, maxResults));
768 if (!searchDone && obj.next_start_index) {
769 if (session.results.length >= maxResults) {
771 session.nextOffset = obj.next_start_index;
772 var prevOffset2 = searchProps.prevOffset;
774 session.prevOffset = parseInt(prevOffset2, 10);
776 var key = getSearchCacheKey(props.base_uri_pathname, searchText, session.offset);
777 localStorage[key] = JSON.stringify(session);
779 setSearchMessage(msg + ' ' + getOffsetLinks(session, maxResults));
780 setSearchStatus(searchProps.showingResultMsg + ' ' +
781 getSearchProgress(session));
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);
792 * @param {string} searchText
793 * @param {string} base
794 * @param {number} offset
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];
806 var session = JSON.parse(cache1);
807 if (!session) return null;
808 if (base !== session.base) {
811 var user = searchProps.user;
812 if (user !== session.authUser) {
815 if (session.hitPageCount > 0) {
816 showSecondSearchForm();
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);
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);
847 removeTargets.forEach(function(target) {
848 localStorage.removeItem(target);
852 * @param {string} searchText
853 * @param {object} session
854 * @param {number} startIndex
855 * @param {number} searchStartTime
856 * @param {number} prevTimestamp
858 function doSearch(searchText, session, startIndex, searchStartTime, prevTimestamp) {
859 var url = './?cmd=search2&action=query';
860 url += '&encode_hint=' + encodeURIComponent('\u3077');
862 url += '&q=' + encodeURIComponent(searchText);
865 url += '&base=' + encodeURIComponent(session.base);
868 url += '&modified_since=' + prevTimestamp;
870 url += '&start=' + startIndex;
871 if (searchStartTime) {
872 url += '&search_start_time=' + encodeURIComponent(searchStartTime);
874 if (!('offset' in session)) {
875 session.offset = startIndex;
878 fetch(url, {credentials: 'same-origin'}
879 ).then(function(response) {
881 return response.json();
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) {
890 console.log('Error! Please check JavaScript console\n' + JSON.stringify(err) + '|' + err);
892 setSearchStatus(searchProps.errorMsg);
895 function hookSearch2() {
896 var form = document.querySelector('form');
897 if (form && form.q) {
899 if (q.value === '') {
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');
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);
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);
931 // Show search results
934 doSearch(searchText.value, {base: base, offset: offset}, offset, null);
936 removeCachedResults();
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;
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;
955 if (f.action.match(/cmd=search$/)) {
956 f.addEventListener('submit', function(e) {
957 var q = e.target.word.value;
959 forEach(f.querySelectorAll('input[name="base"]'), function(radio) {
960 if (radio.checked) base = radio.value;
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;
968 var url = baseUri + '?' +
969 (props.is_utf8 ? '' : 'encode_hint=' +
970 encodeURIComponent('\u3077') + '&') +
972 '&q=' + encodeSearchText(q) +
973 (base ? '&base=' + encodeURIComponent(base) : '');
975 setTimeout(function() {
976 window.location.href = url;
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);
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);
1001 function showNoSupportMessage() {
1002 var pList = document.getElementsByClassName('_plugin_search2_nosupport_message');
1003 for (var i = 0; i < pList.length; i++) {
1005 p.style.display = 'block';
1008 function isEnabledFetchFunctions() {
1009 if (window.fetch && document.querySelector && window.JSON) {
1014 function isEnableServerFunctions() {
1015 var props = getSiteProps();
1016 if (props.json_enabled) return true;
1019 prepareSearchProps();
1020 colorSearchTextInBody();
1021 if (!isEnabledFetchFunctions()) {
1022 showNoSupportMessage();
1025 if (!isEnableServerFunctions()) return;
1026 replaceSearchWithSearch2();