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