1 // PukiWiki - Yet another WikiWikiWeb clone.
4 // 2017-2020 PukiWiki Development Team
5 // License: GPL v2 or (at your option) any later version
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 () {
12 function enableSearch2 () {
14 var maxResultLines = 20
15 var defaultSearchWaitMilliseconds = 100
16 var defaultMaxResults = 1000
20 * Escape HTML special charactors
24 function escapeHTML (s) {
25 if (typeof s !== 'string') {
28 return s.replace(/[&"<>]/g, function (m) {
38 * @param {string} idText
39 * @param {number} defaultValue
42 function getIntById (idText, defaultValue) {
43 var value = defaultValue
45 var element = document.getElementById(idText)
47 value = parseInt(element.value, 10)
48 if (isNaN(value)) { // eslint-disable-line no-restricted-globals
58 * @param {string} idText
59 * @param {string} defaultValue
62 function getTextById (idText, defaultValue) {
63 var value = defaultValue
65 var element = document.getElementById(idText)
74 function prepareSearchProps () {
76 p.errorMsg = getTextById('_plugin_search2_msg_error',
77 'An error occurred while processing.')
78 p.searchingMsg = getTextById('_plugin_search2_msg_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)
99 function getSiteProps () {
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
107 * @param {NodeList} nodeList
108 * @param {function(Node, number): void} func
110 function forEach (nodeList, func) {
111 if (nodeList.forEach) {
112 nodeList.forEach(func)
114 for (var i = 0, n = nodeList.length; i < n; i++) {
120 * @param {string} text
121 * @param {RegExp} searchRegex
123 function findAndDecorateText (text, searchRegex) {
124 var isReplaced = false
128 if (!searchRegex) return null
129 searchRegex.lastIndex = 0
130 while ((m = searchRegex.exec(text)) !== null) {
133 console.log('Invalid searchRegex ' + searchRegex)
137 var pre = text.substring(lastIndex, m.index)
138 decorated += escapeHTML(pre)
139 for (var i = 1; i < m.length; i++) {
141 decorated += '<strong class="word' + (i - 1) + '">' + escapeHTML(m[i]) + '</strong>'
144 lastIndex = searchRegex.lastIndex
147 decorated += escapeHTML(text.substr(lastIndex))
153 * @param {Object} session
154 * @param {string} searchText
155 * @param {RegExp} searchRegex
156 * @param {boolean} nowSearching
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
165 var msg = messageTemplate.replace(/\$1|\$2|\$3/g, function (m) {
167 $1: searchTextDecorated,
168 $2: session.hitPageCount,
169 $3: session.readPageCount
175 * @param {Object} session
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
187 * @param {Object} session
188 * @param {number} maxResults
190 function getOffsetLinks (session, maxResults) {
191 var baseUrl = searchProps.baseUrl
193 if ('prevOffset' in session) {
194 var prevResultUrl = baseUrl
195 if (session.prevOffset > 0) {
196 prevResultUrl += '&offset=' + session.prevOffset
198 var msgPrev = searchProps.msgPrevResultsTemplate.replace(/\$1/, maxResults)
199 var prevResultHtml = '<a href="' + prevResultUrl + '">' + msgPrev + '</a>'
200 links.push(prevResultHtml)
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)
209 if (links.length > 0) {
210 return links.join(' ')
214 function prepareKanaMap () {
215 if (kanaMap !== null) return
216 if (!String.prototype.normalize) {
220 var dakuten = '\uFF9E'
223 for (var c = 0xFF61; c <= 0xFF9F; c++) {
224 var han = String.fromCharCode(c)
225 var zen = han.normalize('NFKC')
227 var hanDaku = han + dakuten
228 var zenDaku = hanDaku.normalize('NFKC')
229 if (zenDaku.length === 1) { // +Handaku-ten OK
230 map[zenDaku] = hanDaku
232 var hanMaru = han + maru
233 var zenMaru = hanMaru.normalize('NFKC')
234 if (zenMaru.length === 1) { // +Maru OK
235 map[zenMaru] = hanMaru
241 * Hankaku to Zenkaku.
243 * @param {String} hankakuChar
246 function toZenkaku (hankakuChar) {
247 if (hankakuChar.length !== 1) {
250 var zenkakuChar = String.fromCharCode(hankakuChar.charCodeAt(0) + 0xfee0)
251 if (!String.prototype.normalize) {
254 if (zenkakuChar.normalize('NFKC') === hankakuChar) {
260 * @param {searchText} searchText
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) {
270 return '[' + m1 + toZenkaku(m1) + ']'
273 var r = '(?:' + String.fromCharCode(m2.charCodeAt(0) - 0x60) +
276 r += '|' + kanaMap[m2]
282 var katakana = String.fromCharCode(m3.charCodeAt(0) + 0x60)
283 var r2 = '(?:' + m3 + '|' + katakana
284 if (kanaMap[katakana]) {
285 r2 += '|' + kanaMap[katakana]
292 return '[' + m4 + kanaMap[m4] + ']'
297 return '[' + '\\' + m5 + toZenkaku(m5) + ']'
301 var s1 = searchText.replace(/^\s+|\s+$/g, '')
303 var sp = s1.split(/\s+/)
306 for (var i = 0; i < sp.length; i++) {
312 s = s.normalize('NFKC')
314 var s2 = s.replace(regRep, replacementFunc)
315 rText += '(' + s2 + ')'
317 return new RegExp(rText, 'ig')
320 * @param {string} statusText
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')
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)
340 textObj1.textContent = statusText
346 textObj2.textContent = ' ' + statusText2
348 textObj2.textContent = ''
354 * @param {string} msgHTML
356 function setSearchMessage (msgHTML) {
357 var objList = document.querySelectorAll('._plugin_search2_message')
358 forEach(objList, function (obj) {
359 obj.innerHTML = msgHTML
362 function showSecondSearchForm () {
363 // Show second search form
364 var div = document.querySelector('._plugin_search2_second_form')
366 div.style.display = 'block'
370 * @param {Element} form
373 function getSearchBase (form) {
374 var f = form || document.querySelector('._plugin_search2_form')
376 forEach(f.querySelectorAll('input[name="base"]'), function (radio) {
377 if (radio.checked) base = radio.value
382 * Decorate found block (for pre innerHTML)
384 * @param {Object} block
385 * @param {RegExp} searchRegex
387 function decorateFoundBlock (block, searchRegex) {
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))
395 lines.push('' + (block.startIndex + j + 1) + ':\t' + decorated)
398 if (block.beyondLimit) {
401 return lines.join('\n')
404 * @param {string} body
405 * @param {RegExp} searchRegex
407 function getSummaryInfo (body, searchRegex) {
408 var lines = body.split('\n')
410 var isInAuthorHeader = true
411 var lastFoundLineIndex = -1 - aroundLines
412 var lastAddedLineIndex = lastFoundLineIndex
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
423 } else if (line.match(/^#freeze(\W|$)/)) {
427 isInAuthorHeader = false
430 var match = line.match(searchRegex)
432 if (index < lastFoundLineIndex + aroundLines + 1) {
433 foundLines.push(lines[index])
435 lastAddedLineIndex = index
438 var startIndex = Math.max(Math.max(lastAddedLineIndex + 1, index - aroundLines), 0)
439 if (lastAddedLineIndex + 1 < startIndex) {
442 startIndex: startIndex,
443 foundLineIndex: index,
447 foundLines = block.lines
450 if (lineCount >= maxResultLines) {
451 currentBlock.beyondLimit = true
454 for (var i = startIndex; i < index; i++) {
455 foundLines.push(lines[i])
458 foundLines.push(line)
460 lastFoundLineIndex = lastAddedLineIndex = index
467 * @param {string} dateText
469 function getPassage (now, dateText) {
473 var units = [{ u: 'm', max: 60 }, { u: 'h', max: 24 }, { u: 'd', max: 1 }]
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
483 return '(' + Math.floor(t) + unit + ')'
486 * @param {string} searchText
488 function removeSearchOperators (searchText) {
489 var sp = searchText.split(/\s+/)
490 if (sp.length <= 1) {
493 for (var i = sp.length - 2; i >= 1; i--) {
494 if (sp[i] === 'OR') {
501 * @param {string} pathname
503 function getSearchCacheKeyBase (pathname) {
504 return 'path.' + pathname + '.search2.'
507 * @param {string} pathname
509 function getSearchCacheKeyDateBase (pathname) {
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 + '.'
516 * @param {string} pathname
517 * @param {string} searchText
518 * @param {number} offset
520 function getSearchCacheKey (pathname, searchText, offset) {
521 return getSearchCacheKeyDateBase(pathname) + 'offset=' + offset +
525 * @param {string} pathname
526 * @param {string} searchText
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)
543 removeTargets.forEach(function (target) {
544 localStorage.removeItem(target)
548 * @param {string} body
550 function getBodySummary (body) {
551 var lines = body.split('\n')
552 var isInAuthorHeader = true
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
561 } else if (line.match(/^#freeze(\W|$)/)) {
566 isInAuthorHeader = false
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
577 if (summary.length >= 10) {
581 return summary.join(' ').substring(0, 150)
584 * @param {string} q searchText
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])
594 * @param {string} q searchText
596 function encodeSearchTextForHash (q) {
597 var sp = q.split(/\s+/)
600 function getSearchTextInLocationHash () {
601 var hash = document.location.hash
604 if (hash.substr(0, 3) === '#q=') {
605 q = hash.substr(3).replace(/\+/g, ' ')
609 var decodedQ = decodeURIComponent(q)
610 if (q !== decodedQ) {
611 q = decodedQ + ' OR ' + q
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']
623 * @param {Element} element
625 function colorSearchText (element) {
626 var decorated = findAndDecorateText(element.nodeValue, searchRegex)
628 var span = document.createElement('span')
629 span.innerHTML = decorated
630 element.parentNode.replaceChild(span, element)
634 * @param {Element} element
636 function walkElement (element) {
637 var e = element.firstChild
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)
645 if (e.nodeType === 1 && ignoreTags.indexOf(e.tagName) === -1) {
652 var target = document.getElementById('body')
656 * @param {Array<Object>} newResults
657 * @param {Element} ul
659 function removePastResults (newResults, ul) {
661 var nodes = ul.childNodes
662 for (var i = nodes.length - 1; i >= 0; 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
674 if (isRemoveTarget) {
675 if (node.tagName === 'LI') {
684 * @param {Array<Object>} results
685 * @param {string} searchText
686 * @param {RegExp} searchRegex
687 * @param {Element} parentUlElement
688 * @param {boolean} insertTop
690 function addSearchResult (results, searchText, searchRegex, parentUlElement, insertTop) {
691 var props = getSiteProps()
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)
702 var updatedAt = val.updatedAt
704 if (props.show_passage) {
705 passageHtml = ' ' + getPassage(now, updatedAt)
707 var liHtml = '<a href="' + escapeHTML(href) + '">' +
708 decoratedName + '</a>' + passageHtml
709 li.innerHTML = liHtml
710 li.setAttribute('data-pagename', val.name)
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)
724 div.setAttribute('data-pagename', val.name)
725 // Add li to ul (parentUlElement)
727 parentFragment.appendChild(li)
729 if (insertTop && parentUlElement.firstChild) {
730 parentUlElement.insertBefore(parentFragment, parentUlElement.firstChild)
732 parentUlElement.appendChild(parentFragment)
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)
750 removeTargets.push(key)
754 removeTargets.forEach(function (target) {
755 localStorage.removeItem(target)
758 function removeCachedResults () {
759 removeCachedResultsBase(true)
761 function removeAllCachedResults () {
762 removeCachedResultsBase(false)
765 * @param {Object} obj
766 * @param {Object} session
767 * @param {string} searchText
768 * @param {number} prevTimestamp
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')
775 if (obj.start_index === 0 && !prevTimestamp) {
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
789 session.searchStartTime = obj.search_start_time
790 session.authUser = obj.auth_user
791 if (prevHitPageCount === 0 && session.hitPageCount > 0) {
792 showSecondSearchForm()
794 var results = obj.results
795 var cachedResults = []
796 results.forEach(function (val) {
798 cache.name = val.name
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)
807 var removedCount = removePastResults(cachedResults, ul)
808 session.hitPageCount -= removedCount
810 var msg = getSearchResultMessage(session, searchText, searchRegex, !searchDone)
811 setSearchMessage(msg)
813 setSearchStatus(searchProps.searchingMsg)
815 setSearchStatus(searchProps.searchingMsg,
816 getSearchProgress(session))
819 var singlePageResult = session.offset === 0 && !session.nextOffset
820 var progress = getSearchProgress(session)
821 setTimeout(function () {
822 if (singlePageResult) {
825 setSearchStatus(searchProps.showingResultMsg, progress)
829 if (session.results) {
831 var newResult = [].concat(cachedResults)
832 Array.prototype.push.apply(newResult, session.results)
833 session.results = newResult
835 Array.prototype.push.apply(session.results, cachedResults)
838 session.results = cachedResults
840 addSearchResult(cachedResults, searchText, searchRegex, ul, prevTimestamp)
841 var maxResults = searchProps.maxResults
843 session.searchText = searchText
844 var prevOffset = searchProps.prevOffset
846 session.prevOffset = parseInt(prevOffset, 10)
848 var json = JSON.stringify(session)
849 var cacheKey = getSearchCacheKey(props.base_uri_pathname, searchText, session.offset)
850 if (window.localStorage) {
852 localStorage[cacheKey] = json
854 // QuotaExceededError "exceeded the quota."
856 removeAllCachedResults()
859 if ('prevOffset' in session || 'nextOffset' in session) {
860 setSearchMessage(msg + ' ' + getOffsetLinks(session, maxResults))
863 if (!searchDone && obj.next_start_index) {
864 if (session.results.length >= maxResults) {
866 session.nextOffset = obj.next_start_index
867 var prevOffset2 = searchProps.prevOffset
869 session.prevOffset = parseInt(prevOffset2, 10)
871 var key = getSearchCacheKey(props.base_uri_pathname, searchText, session.offset)
872 localStorage[key] = JSON.stringify(session)
874 setSearchMessage(msg + ' ' + getOffsetLinks(session, maxResults))
875 setSearchStatus(searchProps.showingResultMsg,
876 getSearchProgress(session))
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)
887 * @param {string} searchText
888 * @param {string} base
889 * @param {number} offset
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')
896 var searchCacheKey = getSearchCacheKey(props.base_uri_pathname, searchText, offset)
897 var cache1 = localStorage[searchCacheKey]
901 var session = JSON.parse(cache1)
902 if (!session) return null
903 if (base !== session.base) {
906 var user = searchProps.user
907 if (user !== session.authUser) {
910 if (session.hitPageCount > 0) {
911 showSecondSearchForm()
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)
928 * @param {string} searchText
929 * @param {object} session
930 * @param {number} startIndex
931 * @param {number} searchStartTime
932 * @param {number} prevTimestamp
934 function doSearch (searchText, session, startIndex, searchStartTime, prevTimestamp) {
935 var props = getSiteProps()
937 if (props.base_uri_pathname) {
938 baseUrl = props.base_uri_pathname
940 var url = baseUrl + '?cmd=search2&action=query'
941 url += '&encode_hint=' + encodeURIComponent('\u3077')
943 url += '&q=' + encodeURIComponent(searchText)
946 url += '&base=' + encodeURIComponent(session.base)
949 url += '&modified_since=' + prevTimestamp
951 url += '&start=' + startIndex
952 if (searchStartTime) {
953 url += '&search_start_time=' + encodeURIComponent(searchStartTime)
955 if (!('offset' in session)) {
956 session.offset = startIndex
959 fetch(url, { credentials: 'same-origin' }
960 ).then(function (response) {
962 return response.json()
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) {
971 console.log('Error! Please check JavaScript console\n' + JSON.stringify(err) + '|' + err)
973 setSearchStatus(searchProps.errorMsg)
976 function hookSearch2 () {
977 var form = document.querySelector('form')
978 if (form && form.q) {
980 if (q.value === '') {
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')
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)
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)
1012 // Show search results
1015 doSearch(searchText.value, { base: base, offset: offset }, offset, null)
1017 removeCachedResults()
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
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
1036 if (f.action.match(/cmd=search$/)) {
1037 f.addEventListener('submit', function (e) {
1038 var q = e.target.word.value
1040 forEach(f.querySelectorAll('input[name="base"]'), function (radio) {
1041 if (radio.checked) base = radio.value
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
1049 var url = baseUri + '?' +
1050 (props.is_utf8 ? '' : 'encode_hint=' +
1051 encodeURIComponent('\u3077') + '&') +
1053 '&q=' + encodeSearchText(q) +
1054 (base ? '&base=' + encodeURIComponent(base) : '')
1056 setTimeout(function () {
1057 window.location.href = url
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)
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)
1082 function showNoSupportMessage () {
1083 var pList = document.getElementsByClassName('_plugin_search2_nosupport_message')
1084 for (var i = 0; i < pList.length; i++) {
1086 p.style.display = 'block'
1089 function isEnabledFetchFunctions () {
1090 if (window.fetch && document.querySelector && window.JSON) {
1095 function isEnableServerFunctions () {
1096 var props = getSiteProps()
1097 if (props.json_enabled) return true
1100 prepareSearchProps()
1101 colorSearchTextInBody()
1102 if (!isEnabledFetchFunctions()) {
1103 showNoSupportMessage()
1106 if (!isEnableServerFunctions()) return
1107 replaceSearchWithSearch2()