OSDN Git Service

BugTrack/2434 Search result cache and offset paging
authorumorigu <umorigu@gmail.com>
Sat, 30 Sep 2017 20:52:20 +0000 (05:52 +0900)
committerumorigu <umorigu@gmail.com>
Sat, 30 Sep 2017 20:52:20 +0000 (05:52 +0900)
en.lng.php
ja.lng.php
lib/file.php
plugin/search2.inc.php
skin/pukiwiki.css
skin/search2.js
skin/tdiary.css

index 470104b..feb8e1e 100644 (file)
@@ -47,6 +47,8 @@ $_msg_goto        = 'Go to $1.';
 $_msg_andresult   = 'In the page <strong> $2</strong>, <strong> $3</strong> pages that contain all the terms $1 were found.';
 $_msg_orresult    = 'In the page <strong> $2</strong>, <strong> $3</strong> pages that contain at least one of the terms $1 were found.';
 $_msg_notfoundresult = 'No page which contains $1 has been found.';
+$_msg_prev_results = '&lt;&lt; Previous $1 pages';
+$_msg_more_results = 'Next $1 pages &gt;&gt;';
 $_msg_symbol      = 'Symbols';
 $_msg_other       = 'Others';
 $_msg_help        = 'View Text Formatting Rules';
@@ -55,6 +57,7 @@ $_msg_content_back_to_top = '<div class="jumpmenu"><a href="#navigator">&uarr;</
 $_msg_word        = 'These search terms have been highlighted:';
 $_msg_unsupported_webbrowser = 'This function doesn\'t support your current Web browser.';
 $_msg_use_alternative_link = 'Please go to the following link destination: $1';
+$_msg_general_error  = 'An error occurred while processing.';
 
 ///////////////////////////////////////
 // Symbols
@@ -370,6 +373,7 @@ $_btn_or        = 'OR';
 $_search_pages  = 'Search for page starts from $1';
 $_search_all    = 'Search for all pages';
 $_search_searching = 'Searching...';
+$_search_showing_result = 'Showing search results';
 $_search_detail = 'Show details';
 
 ///////////////////////////////////////
index 0704d6c..0264ab0 100644 (file)
@@ -49,6 +49,8 @@ $_msg_goto           = '$1 へ行く。';
 $_msg_andresult      = '$1 のすべてを含むページは <strong>$3</strong> ページ中、 <strong>$2</strong> ページ見つかりました。';
 $_msg_orresult       = '$1 のいずれかを含むページは <strong>$3</strong> ページ中、 <strong>$2</strong> ページ見つかりました。';
 $_msg_notfoundresult = '$1 を含むページは見つかりませんでした。';
+$_msg_prev_results   = '&lt;&lt; 前の $1 ページ';
+$_msg_more_results   = '次の $1 ページ &gt;&gt;';
 $_msg_symbol         = '記号';
 $_msg_other          = '日本語';
 $_msg_help           = 'テキスト整形のルールを表示する';
@@ -57,6 +59,7 @@ $_msg_content_back_to_top = '<div class="jumpmenu"><a href="#navigator">&uarr;</
 $_msg_word           = 'これらのキーワードがハイライトされています:';
 $_msg_unsupported_webbrowser = 'この機能はお使いのWebブラウザには対応していません。';
 $_msg_use_alternative_link = 'リンク先の機能をご利用ください: $1';
+$_msg_general_error  = '処理中にエラーが発生しました。';
 
 ///////////////////////////////////////
 // Symbols
@@ -372,6 +375,7 @@ $_btn_or        = 'OR検索';
 $_search_pages  = '$1 から始まるページを検索';
 $_search_all    = '全てのページを検索';
 $_search_searching = '検索中...';
+$_search_showing_result = '検索結果表示';
 $_search_detail = '詳細表示';
 
 ///////////////////////////////////////
index 39c3af8..fbc9dde 100644 (file)
@@ -257,11 +257,26 @@ function get_author_info($wikitext)
                        // Found #freeze still in header
                } else {
                        // other line, #author not found
-                       return false;
+                       return null;
                }
                $start = $pos + 1;
        }
-       return false;
+       return null;
+}
+
+/**
+ * Get updated datetime from author
+ */
+function get_update_datetime_from_author($author_line) {
+       $m = null;
+       if (preg_match('/^#author\(\"([^\";]+)(?:;([^\";]+))?/', $author_line, $m)) {
+               if ($m[2]) {
+                       return $m[2];
+               } else if ($m[1]) {
+                       return $m[1];
+               }
+       }
+       return null;
 }
 
 function get_date_atom($timestamp)
@@ -604,6 +619,24 @@ function put_lastmodified()
 }
 
 /**
+ * Get recent files
+ *
+ * @return Array of (file => time)
+ */
+function get_recent_files()
+{
+       $recentfile = CACHE_DIR . PKWK_MAXSHOW_CACHE;
+       $lines = file($recentfile);
+       if (!$lines) return array();
+       $files = array();
+       foreach ($lines as $line) {
+               list ($time, $file) = explode("\t", rtrim($line));
+               $files[$file] = $time;
+       }
+       return $files;
+}
+
+/**
  * Update RecentChanges page / Invalidate recent.dat
  */
 function delete_recent_changes_cache() {
index 8d71268..cbbadad 100644 (file)
@@ -6,12 +6,12 @@
 //
 // Search2 plugin - Show detail result using JavaScript
 
-define('PLUGIN_SEARCH2_MAX_LENGTH', 80);
-define('PLUGIN_SEARCH2_MAX_BASE',   16); // #search(1,2,3,...,15,16)
+define('PLUGIN_SEARCH2_MAX_BASE', 16); // #search(1,2,3,...,15,16)
 
 define('PLUGIN_SEARCH2_RESULT_RECORD_LIMIT', 500);
 define('PLUGIN_SEARCH2_RESULT_RECORD_LIMIT_START', 100);
 define('PLUGIN_SEARCH2_SEARCH_WAIT_MILLISECONDS', 1000);
+define('PLUGIN_SEARCH2_SEARCH_MAX_RESULTS', 1000);
 
 // Show a search box on a page
 function plugin_search2_convert()
@@ -22,7 +22,7 @@ function plugin_search2_convert()
 
 function plugin_search2_action()
 {
-       global $vars, $_title_search, $_title_result;
+       global $vars, $_title_search, $_title_result, $_msg_searching;
 
        $action = isset($vars['action']) ? $vars['action'] : '';
        $base = isset($vars['base']) ? $vars['base'] : '';
@@ -33,28 +33,71 @@ function plugin_search2_action()
                $bases[] = $base;
        }
        if ($action === '') {
-               $q = isset($vars['q']) ? $vars['q'] : '';
+               $q = trim(isset($vars['q']) ? $vars['q'] : '');
+               $offset_s = isset($vars['offset']) ? $vars['offset'] : '';
+               $offset = pkwk_ctype_digit($offset_s) ? intval($offset_s) : 0;
+               $prev_offset_s = isset($vars['prev_offset']) ? $vars['prev_offset'] : '';
                if ($q === '') {
                        return array('msg' => $_title_search,
-                               'body' => plugin_search2_search_form($q, '', $bases));
+                               'body' => "<br>" . $_msg_searching . "\n" .
+                               plugin_search2_search_form($q, $bases, $offset));
                } else {
                        $msg  = str_replace('$1', htmlsc($q), $_title_result);
                        return array('msg' => $msg,
-                                       'body' => plugin_search2_search_form($q, '', $bases));
+                               'body' => plugin_search2_search_form($q, $bases, $offset, $prev_offset_s));
                }
        } else if ($action === 'query') {
-               $text = isset($vars['q']) ? $vars['q'] : '';
+               $q = isset($vars['q']) ? $vars['q'] : '';
+               $search_start_time = isset($vars['search_start_time']) ?
+                       $vars['search_start_time'] : null;
+               $modified_since = (int)(isset($vars['modified_since']) ?
+                       $vars['modified_since'] : '0');
                header('Content-Type: application/json; charset=UTF-8');
-               plugin_search2_do_search($text, $base, $start_index);
+               plugin_search2_do_search($q, $base, $start_index,
+                       $search_start_time, $modified_since);
                exit;
        }
 }
 
-function plugin_search2_do_search($query_text, $base, $start_index)
+function plugin_search2_get_base_url($search_text)
+{
+       global $vars;
+       $params = array();
+       if (!defined('PKWK_UTF8_ENABLE')) {
+               $params[] = 'encode_hint=' . rawurlencode($vars['encode_hint']);
+       }
+       $params[] = 'cmd=search2';
+       if (isset($vars['encode_hint']) && $vars['encode_hint']) {
+               $params[] = 'encode_hint=' . rawurlencode($vars['encode_hint']);
+       }
+       if ($search_text) {
+               $params[] = 'q=' . plugin_search2_urlencode_searchtext($search_text);
+       }
+       if (isset($vars['base']) && $vars['base']) {
+               $params[] = 'base=' . rawurlencode($vars['base']);
+       }
+       $url = get_base_uri() . '?' . join('&', $params);
+       return $url;
+}
+
+function plugin_search2_urlencode_searchtext($search_text)
+{
+       $s2 = preg_replace('#^\s+|\s+$#', '', $search_text);
+       if (!$s2) return '';
+       $sp = preg_split('#\s+#', $s2);
+       $list = array();
+       for ($i = 0; $i < count($sp); $i++) {
+               $list[] = rawurlencode($sp[$i]);
+       }
+       return join('+', $list);
+}
+
+function plugin_search2_do_search($query_text, $base, $start_index,
+       $search_start_time, $modified_since)
 {
        global $whatsnew, $non_list, $search_non_list;
        global $_msg_andresult, $_msg_orresult, $_msg_notfoundresult;
-       global $search_auth;
+       global $search_auth, $auth_user;
 
        $result_record_limit = $start_index === 0 ?
                PLUGIN_SEARCH2_RESULT_RECORD_LIMIT_START : PLUGIN_SEARCH2_RESULT_RECORD_LIMIT;
@@ -62,7 +105,7 @@ function plugin_search2_do_search($query_text, $base, $start_index)
 
        $b_type_and = true; // AND:TRUE OR:FALSE
        $key_candidates = preg_split('/\s+/', $query_text, -1, PREG_SPLIT_NO_EMPTY);
-       for ($i = count($key_candidates) - 1; $i >= 0; $i--) {
+       for ($i = count($key_candidates) - 2; $i >= 1; $i--) {
                if ($key_candidates[$i] === 'OR') {
                        $b_type_and = false;
                        unset($key_candidates[$i]);
@@ -73,20 +116,41 @@ function plugin_search2_do_search($query_text, $base, $start_index)
        foreach ($keys as $key=>$value)
                $keys[$key] = '/' . $value . '/S';
 
-       $pages = get_existpages();
+       if ($modified_since > 0) {
+               // Recent search
+               $recent_files = get_recent_files();
+               $modified_loc = $modified_since - LOCALZONE;
+               $pages = array();
+               foreach ($recent_files as $p => $time) {
+                       if ($time >= $modified_loc) {
+                               $pages[] = $p;
+                       }
+               }
+               if ($base != '') {
+                       $pages = preg_grep('/^' . preg_quote($base, '/') . '/S', $pages);
+               }
+               $page_names = $pages;
+       } else {
+               // Normal search
+               $pages = get_existpages();
 
-       // Avoid
-       if ($base != '') {
-               $pages = preg_grep('/^' . preg_quote($base, '/') . '/S', $pages);
+               // Avoid
+               if ($base != '') {
+                       $pages = preg_grep('/^' . preg_quote($base, '/') . '/S', $pages);
+               }
+               if (! $search_non_list) {
+                       $pages = array_diff($pages, preg_grep('/' . $non_list . '/S', $pages));
+               }
+               $pages = array_flip($pages);
+               unset($pages[$whatsnew]);
+               $page_names = array_keys($pages);
        }
-       if (! $search_non_list) {
-               $pages = array_diff($pages, preg_grep('/' . $non_list . '/S', $pages));
+       natsort($page_names);
+       // Cache collabolate
+       if (is_null($search_start_time)) {
+               // Don't use client cache
+               $search_start_time = UTIME + LOCALZONE;
        }
-       natsort($pages);
-       $pages = array_flip($pages);
-       unset($pages[$whatsnew]);
-       $page_names = array_keys($pages);
-
        $found_pages = array();
        $readable_page_index = -1;
        $scan_page_index = -1;
@@ -112,28 +176,38 @@ function plugin_search2_do_search($query_text, $base, $start_index)
                if ($saved_scan_start_index === -1) {
                        $saved_scan_start_index = $scan_page_index;
                }
-               // Search for page name and contents
-               $body = get_source($page, TRUE, TRUE, TRUE);
-               $target = $page . "\n" . remove_author_header($body);
-               foreach ($keys as $key) {
-                       $b_match = preg_match($key, $target);
-                       if ($b_type_and xor $b_match) break; // OR
+               if (count($keys) > 0) {
+                       // Search for page name and contents
+                       $body = get_source($page, TRUE, TRUE, TRUE);
+                       $target = $page . "\n" . remove_author_header($body);
+                       foreach ($keys as $key) {
+                               $b_match = preg_match($key, $target);
+                               if ($b_type_and xor $b_match) break; // OR
+                       }
+               } else {
+                       // No search target. get_source($page) is meaningless.
+                       // $b_match is always false.
                }
                if ($b_match) {
                        // Found!
-                       $filemtime = null;
                        $author_info = get_author_info($body);
-                       if ($author_info === false || $pagename_only) {
-                               $updated_at = get_date_atom(filemtime(get_filename($page)));
+                       if ($author_info) {
+                               $updated_at = get_update_datetime_from_author($author_info);
+                               $updated_time = strtotime($updated_at);
+                       } else {
+                               $updated_time = filemtime(get_filename($page));
+                               $updated_at = get_date_atom($updated_time);
                        }
                        if ($pagename_only) {
                                // The user cannot read this page body
                                $found_pages[] = array('name' => (string)$page,
                                        'url' => get_page_uri($page), 'updated_at' => $updated_at,
+                                       'updated_time' => $updated_time,
                                        'body' => '', 'pagename_only' => 1);
                        } else {
                                $found_pages[] = array('name' => (string)$page,
                                        'url' => get_page_uri($page), 'updated_at' => $updated_at,
+                                       'updated_time' => $updated_time,
                                        'body' => (string)$body);
                        }
                }
@@ -157,6 +231,8 @@ function plugin_search2_do_search($query_text, $base, $start_index)
                'last_read_page_name' => $last_read_page_name,
                'next_start_index' => $readable_page_index + 1,
                'search_done' => $search_done,
+               'search_start_time' => $search_start_time,
+               'auth_user' => $auth_user,
                'results' => $found_pages);
        $obj = $result_obj;
        if (!defined('PKWK_UTF8_ENABLE')) {
@@ -169,16 +245,21 @@ function plugin_search2_do_search($query_text, $base, $start_index)
        print(json_encode($obj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
 }
 
-function plugin_search2_search_form($s_word = '', $type = '', $bases = array())
+function plugin_search2_search_form($search_text = '', $bases = array(),
+       $offset, $prev_offset_s = null)
 {
        global $_btn_search;
        global $_search_pages, $_search_all;
        global $_msg_andresult, $_msg_orresult, $_msg_notfoundresult;
-       global $_search_detail, $_search_searching;
+       global $_search_detail, $_search_searching, $_search_showing_result;
        global $_msg_unsupported_webbrowser, $_msg_use_alternative_link;
+       global $_msg_more_results, $_msg_prev_results, $_msg_general_error;
+       global $auth_user;
 
+       static $search2_form_total_count = 0;
+       $search2_form_total_count++;
        $script = get_base_uri();
-       $h_search_text = htmlsc($s_word);
+       $h_search_text = htmlsc($search_text);
 
        $base_option = '';
        if (!empty($bases)) {
@@ -210,12 +291,10 @@ EOD;
        $_search2_search_wait_milliseconds = PLUGIN_SEARCH2_SEARCH_WAIT_MILLISECONDS;
        $result_page_panel =<<<EOD
 <input type="checkbox" id="_plugin_search2_detail" checked><label for="_plugin_search2_detail">$_search_detail</label>
-<input type="hidden" id="_plugin_search2_msg_searching" value="$_search_searching">
-<input type="hidden" id="_plugin_search2_msg_result_notfound" value="$_search2_result_notfound">
-<input type="hidden" id="_plugin_search2_msg_result_found" value="$_search2_result_found">
-<input type="hidden" id="_search2_search_wait_milliseconds" value="$_search2_search_wait_milliseconds">
+<ul id="_plugin_search2_result-list">
+</ul>
 EOD;
-       if ($h_search_text == '') {
+       if ($h_search_text == '' || $search2_form_total_count > 1) {
                $result_page_panel = '';
        }
 
@@ -225,7 +304,7 @@ EOD;
 <form action="$script" method="GET" class="_plugin_search2_form">
  <div>
   <input type="hidden" name="cmd" value="search2">
-  <input type="search"  name="q" value="$h_search_text" size="30">
+  <input type="search" name="q" value="$h_search_text" data-original-q="$h_search_text" size="40">
   <input type="submit" value="$_btn_search">
  </div>
 $base_option
@@ -239,6 +318,32 @@ $form
 </div>
 EOD;
 
+       $h_auth_user = htmlsc($auth_user);
+       $h_base_url = htmlsc(plugin_search2_get_base_url($search_text));
+       $h_msg_more_results = htmlsc($_msg_more_results);
+       $h_msg_prev_results = htmlsc($_msg_prev_results);
+       $max_results = PLUGIN_SEARCH2_SEARCH_MAX_RESULTS;
+       $prev_offset = pkwk_ctype_digit($prev_offset_s) ? $prev_offset_s : '';
+       $search_props =<<<EOD
+<div style="display:none;">
+  <input type="hidden" id="_plugin_search2_auth_user" value="$h_auth_user">
+  <input type="hidden" id="_plugin_search2_base_url" value="$h_base_url">
+  <input type="hidden" id="_plugin_search2_msg_searching" value="$_search_searching">
+  <input type="hidden" id="_plugin_search2_msg_showing_result" value="$_search_showing_result">
+  <input type="hidden" id="_plugin_search2_msg_result_notfound" value="$_search2_result_notfound">
+  <input type="hidden" id="_plugin_search2_msg_result_found" value="$_search2_result_found">
+  <input type="hidden" id="_plugin_search2_msg_more_results" value="$h_msg_more_results">
+  <input type="hidden" id="_plugin_search2_msg_prev_results" value="$h_msg_prev_results">
+  <input type="hidden" id="_plugin_search2_search_wait_milliseconds" value="$_search2_search_wait_milliseconds">
+  <input type="hidden" id="_plugin_search2_max_results" value="$max_results">
+  <input type="hidden" id="_plugin_search2_offset" value="$offset">
+  <input type="hidden" id="_plugin_search2_prev_offset" value="$prev_offset">
+  <input type="hidden" id="_plugin_search2_msg_error" value="$_msg_general_error">
+</div>
+EOD;
+       if ($search2_form_total_count > 1) {
+               $search_props = '';
+       }
 
        return <<<EOD
 <noscript>
@@ -247,12 +352,11 @@ EOD;
 <p class="_plugin_search2_nosupport_message" style="display:none;">
   $_msg_unsupported_webbrowser $alt_msg
 </p>
+$search_props
 $form
 <div class="_plugin_search2_search_status"></div>
 <div class="_plugin_search2_message"></div>
 $result_page_panel
-<ul id="result-list">
-</ul>
 $second_form
 EOD;
 }
index 7dde240..e295531 100644 (file)
@@ -643,17 +643,20 @@ tr.bugtrack_state_undef td {
 
 /* search2.inc.php  */
 input#_plugin_search2_detail:checked ~ ul > div.search-result-detail {
-       display: block;
+  display: block;
 }
 input#_plugin_search2_detail ~ ul > div.search-result-detail {
-       display: none;
+  display: none;
+}
+._plugin_search2_search_status {
+  min-height: 1.5em;
 }
 .search-result-page-summary {
-       font-size: 70%;
-       color: gray;
-       overflow: hidden;
-       text-overflow: ellipsis;
-       white-space: nowrap;
+  font-size: 70%;
+  color: gray;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
 @media print {
index 8460261..f764726 100644 (file)
@@ -5,16 +5,23 @@
 // License: GPL v2 or (at your option) any later version
 //
 // PukiWiki search2 pluign - JavaScript client script
-window.addEventListener && window.addEventListener('DOMContentLoaded', function() {
+window.addEventListener && window.addEventListener('DOMContentLoaded', function() { // eslint-disable-line no-unused-expressions
+  'use strict';
   function enableSearch2() {
     var aroundLines = 2;
     var maxResultLines = 20;
-    var minBlockLines = 5;
-    var minSearchWaitMilliseconds = 100;
+    var defaultSearchWaitMilliseconds = 100;
+    var defaultMaxResults = 1000;
     var kanaMap = null;
-    function escapeHTML (s) {
-      if(typeof s !== 'string') {
-        s = '' + s;
+    var searchProps = {};
+    /**
+     * Escape HTML special charactors
+     *
+     * @param {string} s
+     */
+    function escapeHTML(s) {
+      if (typeof s !== 'string') {
+        return '' + s;
       }
       return s.replace(/[&"<>]/g, function(m) {
         return {
@@ -25,179 +32,184 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
         }[m];
       });
     }
-    function doSearch(searchText, session, startIndex) {
-      var url = './?cmd=search2&action=query';
-      var props = getSiteProps();
-      url += '&encode_hint=' + encodeURIComponent('\u3077');
-      if (searchText) {
-        url += '&q=' + encodeURIComponent(searchText);
-      }
-      if (session.base) {
-        url += '&base=' + encodeURIComponent(session.base);
+    /**
+     * @param {string} idText
+     * @param {number} defaultValue
+     * @type number
+     */
+    function getIntById(idText, defaultValue) {
+      var value = defaultValue;
+      try {
+        var element = document.getElementById(idText);
+        if (element) {
+          value = parseInt(element.value, 10);
+          if (isNaN(value)) { // eslint-disable-line no-restricted-globals
+            value = defaultValue;
+          }
+        }
+      } catch (e) {
+        value = defaultValue;
       }
-      url += '&start=' + startIndex;
-      fetch (url
-      ).then(function(response){
-        if (response.ok) {
-          return response.json();
-        } else {
-          throw new Error(response.status + ': ' +
-            + response.statusText + ' on ' + url);
+      return value;
+    }
+    /**
+     * @param {string} idText
+     * @param {string} defaultValue
+     * @type string
+     */
+    function getTextById(idText, defaultValue) {
+      var value = defaultValue;
+      try {
+        var element = document.getElementById(idText);
+        if (element.value) {
+          value = element.value;
         }
-      }).then(function(obj) {
-        showResult(obj, session, searchText);
-      })['catch'](function(err){
-        console.log(err);
-        console.log('Error! Please check JavaScript console' + '\n' + JSON.stringify(err) + '|' + err);
-      });
+      } catch (e) {
+        value = defaultValue;
+      }
+      return value;
     }
-    function getMessageTemplate(idText, defaultText) {
-      var messageHolder = document.querySelector('#' + idText);
-      var messageTemplate = (messageHolder && messageHolder.value) || defaultText;
-      return messageTemplate;
+    function prepareSearchProps() {
+      var p = {};
+      p.errorMsg = getTextById('_plugin_search2_msg_error',
+        'An error occurred while processing.');
+      p.searchingMsg = getTextById('_plugin_search2_msg_searching',
+        'Searching...');
+      p.showingResultMsg = getTextById('_plugin_search2_msg_showing_result',
+        'Showing search results');
+      p.prevOffset = getTextById('_plugin_search2_prev_offset', '');
+      var baseUrlDefault = document.location.pathname + document.location.search;
+      baseUrlDefault = baseUrlDefault.replace(/&offset=\d+/, '');
+      p.baseUrl = getTextById('_plugin_search2_base_url', baseUrlDefault);
+      p.msgPrevResultsTemplate = getTextById('_plugin_search2_msg_prev_results', 'Previous $1 pages');
+      p.msgMoreResultsTemplate = getTextById('_plugin_search2_msg_more_results', 'Next $1 pages');
+      p.user = getTextById('_plugin_search2_auth_user', '');
+      p.showingResultMsg = getTextById('_plugin_search2_msg_showing_result', 'Showing search results');
+      p.notFoundMessageTemplate = getTextById('_plugin_search2_msg_result_notfound',
+        'No page which contains $1 has been found.');
+      p.foundMessageTemplate = getTextById('_plugin_search2_msg_result_found',
+        'In the page <strong>$2</strong>, <strong>$3</strong> pages that contain all the terms $1 were found.');
+      p.maxResults = getIntById('_plugin_search2_max_results', defaultMaxResults);
+      p.searchInterval = getIntById('_plugin_search2_search_wait_milliseconds', defaultSearchWaitMilliseconds);
+      p.offset = getIntById('_plugin_search2_offset', 0);
+      searchProps = p;
     }
-    function getAuthorInfo(text) {
-
+    function getSiteProps() {
+      var empty = {};
+      var propsDiv = document.getElementById('pukiwiki-site-properties');
+      if (!propsDiv) return empty;
+      var jsonE = propsDiv.querySelector('div[data-key="site-props"]');
+      if (!jsonE) return empty;
+      var props = JSON.parse(jsonE.getAttribute('data-value'));
+      return props || empty;
     }
-    function getPassage(now, dateText) {
-      if (! dateText) {
-        return '';
-      }
-      var units = [{u: 'm', max: 60}, {u: 'h', max: 24}, {u: 'd', max: 1}];
-      var d = new Date();
-      d.setTime(Date.parse(dateText));
-      var t = (now.getTime() - d.getTime()) / (1000 * 60); // minutes
-      var unit = units[0].u, card = units[0].max;
-      for (var i = 0; i < units.length; i++) {
-        unit = units[i].u, card = units[i].max;
-        if (t < card) break;
-        t = t / card;
+    /**
+     * @param {NodeList} nodeList
+     * @param {function(Node, number): void} func
+     */
+    function forEach(nodeList, func) {
+      if (nodeList.forEach) {
+        nodeList.forEach(func);
+      } else {
+        for (var i = 0, n = nodeList.length; i < n; i++) {
+          func(nodeList[i], i);
+        }
       }
-      return '(' + Math.floor(t) + unit + ')';
     }
-    function removeSearchOperators(searchText) {
-      var sp = searchText.split(/\s+/);
-      if (sp.length <= 1) {
-        return searchText;
-      }
-      var hasOr = false;
-      for (var i = sp.length - 1; i >= 0; i--) {
-        if (sp[i] === 'OR') {
-          hasOr = true;
-          sp.splice(i, 1);
+    /**
+     * @param {string} text
+     * @param {RegExp} searchRegex
+     */
+    function findAndDecorateText(text, searchRegex) {
+      var isReplaced = false;
+      var lastIndex = 0;
+      var m;
+      var decorated = '';
+      if (!searchRegex) return null;
+      searchRegex.lastIndex = 0;
+      while ((m = searchRegex.exec(text)) !== null) {
+        if (m[0] === '') {
+          // Fail-safe
+          console.log('Invalid searchRegex ' + searchRegex);
+          return null;
+        }
+        isReplaced = true;
+        var pre = text.substring(lastIndex, m.index);
+        decorated += escapeHTML(pre);
+        for (var i = 1; i < m.length; i++) {
+          if (m[i]) {
+            decorated += '<strong class="word' + (i - 1) + '">' + escapeHTML(m[i]) + '</strong>';
+          }
         }
+        lastIndex = searchRegex.lastIndex;
       }
-      return sp.join(' ');
-    }
-    function showResult(obj, session, searchText) {
-      var searchRegex = textToRegex(removeSearchOperators(searchText));
-      var ul = document.querySelector('#result-list');
-      if (!ul) return;
-      if (obj.start_index === 0) {
-        ul.innerHTML = '';
+      if (isReplaced) {
+        decorated += escapeHTML(text.substr(lastIndex));
+        return decorated;
       }
-      if (! session.scan_page_count) session.scan_page_count = 0;
-      if (! session.read_page_count) session.read_page_count = 0;
-      if (! session.hit_page_count) session.hit_page_count = 0;
-      var prevHitPageCount = session.hit_page_count;
-      session.scan_page_count += obj.scan_page_count;
-      session.read_page_count += obj.read_page_count;
-      session.hit_page_count += obj.results.length;
-      session.page_count = obj.page_count;
-      if (prevHitPageCount === 0 && session.hit_page_count > 0) {
-        var div = document.querySelector('._plugin_search2_second_form');
-        if (div) {
-          div.style.display = 'block';
-        }
-      }
-      var msg = obj.message;
-      var notFoundMessageTemplate = getMessageTemplate('_plugin_search2_msg_result_notfound',
-        'No page which contains $1 has been found.');
-      var foundMessageTemplate = getMessageTemplate('_plugin_search2_msg_result_found',
-        'In the page <strong>$2</strong>, <strong>$3</strong> pages that contain all the terms $1 were found.');
+      return null;
+    }
+    /**
+     * @param {Object} session
+     * @param {string} searchText
+     * @param {RegExp} searchRegex
+     * @param {boolean} nowSearching
+     */
+    function getSearchResultMessage(session, searchText, searchRegex, nowSearching) {
       var searchTextDecorated = findAndDecorateText(searchText, searchRegex);
       if (searchTextDecorated === null) searchTextDecorated = escapeHTML(searchText);
-      var messageTemplate = foundMessageTemplate;
-      if (obj.search_done && session.hit_page_count === 0) {
-        messageTemplate = notFoundMessageTemplate;
+      var messageTemplate = searchProps.foundMessageTemplate;
+      if (!nowSearching && session.hitPageCount === 0) {
+        messageTemplate = searchProps.notFoundMessageTemplate;
       }
-      msg = messageTemplate.replace(/\$1|\$2|\$3/g, function(m){
+      var msg = messageTemplate.replace(/\$1|\$2|\$3/g, function(m) {
         return {
-          '$1': searchTextDecorated,
-          '$2': session.hit_page_count,
-          '$3': session.read_page_count
+          $1: searchTextDecorated,
+          $2: session.hitPageCount,
+          $3: session.readPageCount
         }[m];
       });
-      setSearchMessage(msg);
-      var progress = ' (read:' + session.read_page_count + ', scanned:' +
-        session.scan_page_count + ', all:' + session.page_count + ')';
-      var e = document.querySelector('#_plugin_search2_msg_searching');
-      var msg = e && e.value || 'Searching...';
-      setSearchStatus(msg + progress);
-      if (obj.search_done) {
-        setTimeout(function(){
-          setSearchStatus('');
-        }, 5000);
+      return msg;
+    }
+    /**
+     * @param {Object} session
+     */
+    function getSearchProgress(session) {
+      var progress = '(read:' + session.readPageCount + ', scan:' +
+        session.scanPageCount + ', all:' + session.pageCount;
+      if (session.offset) {
+        progress += ', offset: ' + session.offset;
       }
-      var results = obj.results;
-      var now = new Date();
-      results.forEach(function(val, index) {
-        var fragment = document.createDocumentFragment();
-        var li = document.createElement('li');
-        var hash = '#q=' + encodeSearchTextForHash(searchText);
-        var href = val.url + hash;
-        var decoratedName = findAndDecorateText(val.name, searchRegex);
-        if (! decoratedName) {
-          decoratedName = escapeHTML(val.name);
-        }
-        var author = getAuthorHeader(val.body);
-        var updatedAt = '';
-        if (author) {
-          updatedAt = getUpdateTimeFromAuthorInfo(author);
-        } else {
-          updatedAt = val.updated_at;
-        }
-        var liHtml = '<a href="' + escapeHTML(href) + '">' + decoratedName + '</a> ' +
-          getPassage(now, updatedAt);
-        li.innerHTML = liHtml;
-        var a = li.querySelector('a');
-        if (a && a.hash) {
-          if (a.hash !== hash) {
-            // Some browser execute encodeHTML(hash) automatically. Support them.
-            a.href = val.url + '#encq=' + encodeSearchTextForHash(searchText);
-          }
-        }
-        fragment.appendChild(li);
-        var div = document.createElement('div');
-        div.classList.add('search-result-detail');
-        var head = document.createElement('div');
-        head.classList.add('search-result-page-summary');
-        head.innerHTML = escapeHTML(getBodySummary(val.body));
-        div.appendChild(head);
-        var summary = getSummary(val.body, searchRegex);
-        for (var i = 0; i < summary.length; i++) {
-          var pre = document.createElement('pre');
-          pre.innerHTML = summary[i].lines.join('\n');
-          div.appendChild(pre);
-        }
-        fragment.appendChild(div);
-        ul.appendChild(fragment);
-      });
-      if (!obj.search_done && obj.next_start_index) {
-        var waitE = document.querySelector('#_search2_search_wait_milliseconds');
-        var interval = minSearchWaitMilliseconds;
-        try {
-          interval = parseInt(waitE.value);
-        } catch (e) {
-          interval = minSearchWaitMilliseconds;
-        }
-        if (interval < minSearchWaitMilliseconds) {
-          interval = minSearchWaitMilliseconds;
+      progress += ')';
+      return progress;
+    }
+    /**
+     * @param {Object} session
+     * @param {number} maxResults
+     */
+    function getOffsetLinks(session, maxResults) {
+      var baseUrl = searchProps.baseUrl;
+      var links = [];
+      if ('prevOffset' in session) {
+        var prevResultUrl = baseUrl;
+        if (session.prevOffset > 0) {
+          prevResultUrl += '&offset=' + session.prevOffset;
         }
-        setTimeout(function(){
-          doSearch(searchText, session, obj.next_start_index);
-        }, interval);
+        var msgPrev = searchProps.msgPrevResultsTemplate.replace(/\$1/, maxResults);
+        var prevResultHtml = '<a href="' + prevResultUrl + '">' + msgPrev + '</a>';
+        links.push(prevResultHtml);
       }
+      if ('nextOffset' in session) {
+        var nextResultUrl = baseUrl + '&offset=' + session.nextOffset +
+          '&prev_offset=' + session.offset;
+        var msgMore = searchProps.msgMoreResultsTemplate.replace(/\$1/, maxResults);
+        var moreResultHtml = '<a href="' + nextResultUrl + '">' + msgMore + '</a>';
+        links.push(moreResultHtml);
+      }
+      if (links.length > 0) {
+        return links.join(' ');
+      }
+      return '';
     }
     function prepareKanaMap() {
       if (kanaMap !== null) return;
@@ -208,102 +220,146 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
       var dakuten = '\uFF9E';
       var maru = '\uFF9F';
       var map = {};
-      for (var c = 0xFF61; c <=0xFF9F; c++) {
+      for (var c = 0xFF61; c <= 0xFF9F; c++) {
         var han = String.fromCharCode(c);
         var zen = han.normalize('NFKC');
         map[zen] = han;
         var hanDaku = han + dakuten;
         var zenDaku = hanDaku.normalize('NFKC');
         if (zenDaku.length === 1) { // +Handaku-ten OK
-            map[zenDaku] = hanDaku;
+          map[zenDaku] = hanDaku;
         }
         var hanMaru = han + maru;
         var zenMaru = hanMaru.normalize('NFKC');
         if (zenMaru.length === 1) { // +Maru OK
-            map[zenMaru] = hanMaru;
+          map[zenMaru] = hanMaru;
         }
       }
       kanaMap = map;
     }
+    /**
+     * @param {searchText} searchText
+     * @type RegExp
+     */
     function textToRegex(searchText) {
       if (!searchText) return null;
-      var regEscape = /[\\^$.*+?()[\]{}|]/g;
       //             1:Symbol             2:Katakana        3:Hiragana
       var regRep = /([\\^$.*+?()[\]{}|])|([\u30a1-\u30f6])|([\u3041-\u3096])/g;
+      var replacementFunc = function(m, m1, m2, m3) {
+        if (m1) {
+          // Symbol - escape with prior backslach
+          return '\\' + m1;
+        } else if (m2) {
+          // Katakana
+          var r = '(?:' + String.fromCharCode(m2.charCodeAt(0) - 0x60) +
+            '|' + m2;
+          if (kanaMap[m2]) {
+            r += '|' + kanaMap[m2];
+          }
+          r += ')';
+          return r;
+        } else if (m3) {
+          // Hiragana
+          var katakana = String.fromCharCode(m3.charCodeAt(0) + 0x60);
+          var r2 = '(?:' + m3 + '|' + katakana;
+          if (kanaMap[katakana]) {
+            r2 += '|' + kanaMap[katakana];
+          }
+          r2 += ')';
+          return r2;
+        }
+        return m;
+      };
       var s1 = searchText.replace(/^\s+|\s+$/g, '');
+      if (!s1) return null;
       var sp = s1.split(/\s+/);
       var rText = '';
       prepareKanaMap();
       for (var i = 0; i < sp.length; i++) {
         if (rText !== '') {
-          rText += '|'
+          rText += '|';
         }
         var s = sp[i];
         if (s.normalize) {
           s = s.normalize('NFKC');
         }
-        var s2 = s.replace(regRep, function(m, m1, m2, m3){
-          if (m1) {
-            // Symbol - escape with prior backslach
-            return '\\' + m1;
-          } else if (m2) {
-            // Katakana
-            var r = '(?:' + String.fromCharCode(m2.charCodeAt(0) - 0x60) +
-              '|' + m2;
-            if (kanaMap[m2]) {
-              r += '|' + kanaMap[m2];
-            }
-            r += ')';
-            return r;
-          } else if (m3) {
-            // Hiragana
-            var katakana = String.fromCharCode(m3.charCodeAt(0) + 0x60);
-            var r = '(?:' + m3 + '|' + katakana;
-            if (kanaMap[katakana]) {
-              r += '|' + kanaMap[katakana];
-            }
-            r += ')';
-            return r;
-          }
-          return m;
-        });
+        var s2 = s.replace(regRep, replacementFunc);
         rText += '(' + s2 + ')';
       }
       return new RegExp(rText, 'ig');
     }
-    function getAuthorHeader(body) {
-      var start = 0;
-      var pos;
-      while ((pos = body.indexOf('\n', start)) >= 0) {
-        var line = body.substring(start, pos);
-        if (line.match(/^#author\(/, line)) {
-          return line;
-        } else if (line.match(/^#freeze(\W|$)/, line)) {
-          // Found #freeze still in header
+    /**
+     * @param {string} statusText
+     */
+    function setSearchStatus(statusText) {
+      var statusList = document.querySelectorAll('._plugin_search2_search_status');
+      forEach(statusList, function(statusObj) {
+        statusObj.textContent = statusText;
+      });
+    }
+    /**
+     * @param {string} msgHTML
+     */
+    function setSearchMessage(msgHTML) {
+      var objList = document.querySelectorAll('._plugin_search2_message');
+      forEach(objList, function(obj) {
+        obj.innerHTML = msgHTML;
+      });
+    }
+    function showSecondSearchForm() {
+      // Show second search form
+      var div = document.querySelector('._plugin_search2_second_form');
+      if (div) {
+        div.style.display = 'block';
+      }
+    }
+    /**
+     * @param {Element} form
+     * @type string
+     */
+    function getSearchBase(form) {
+      var f = form || document.querySelector('._plugin_search2_form');
+      var base = '';
+      forEach(f.querySelectorAll('input[name="base"]'), function(radio) {
+        if (radio.checked) base = radio.value;
+      });
+      return base;
+    }
+    /**
+     * Decorate found block (for pre innerHTML)
+     *
+     * @param {Object} block
+     * @param {RegExp} searchRegex
+     */
+    function decorateFoundBlock(block, searchRegex) {
+      var lines = [];
+      for (var j = 0; j < block.lines.length; j++) {
+        var line = block.lines[j];
+        var decorated = findAndDecorateText(line, searchRegex);
+        if (decorated === null) {
+          lines.push('' + (block.startIndex + j + 1) + ':\t' + escapeHTML(line));
         } else {
-          // other line, #author not found
-          return null;
+          lines.push('' + (block.startIndex + j + 1) + ':\t' + decorated);
         }
-        start = pos + 1;
       }
-      return null;
-    }
-    function getUpdateTimeFromAuthorInfo(authorInfo) {
-      var m = authorInfo.match(/^#author\("([^;"]+)(;[^;"]+)?/);
-      if (m) {
-        return m[1];
+      if (block.beyondLimit) {
+        lines.push('...');
       }
-      return '';
+      return lines.join('\n');
     }
-    function getTargetLines(body, searchRegex) {
+    /**
+     * @param {string} body
+     * @param {RegExp} searchRegex
+     */
+    function getSummaryInfo(body, searchRegex) {
       var lines = body.split('\n');
-      var found = [];
       var foundLines = [];
       var isInAuthorHeader = true;
       var lastFoundLineIndex = -1 - aroundLines;
       var lastAddedLineIndex = lastFoundLineIndex;
       var blocks = [];
       var lineCount = 0;
+      var currentBlock = null;
       for (var index = 0, length = lines.length; index < length; index++) {
         var line = lines[index];
         if (isInAuthorHeader) {
@@ -318,10 +374,10 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
             isInAuthorHeader = false;
           }
         }
-        var decorated = findAndDecorateText(line, searchRegex);
-        if (decorated === null) {
+        var match = line.match(searchRegex);
+        if (!match) {
           if (index < lastFoundLineIndex + aroundLines + 1) {
-            foundLines.push('' + (index + 1) + ':\t' + escapeHTML(lines[index]));
+            foundLines.push(lines[index]);
             lineCount++;
             lastAddedLineIndex = index;
           }
@@ -334,41 +390,114 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
               foundLineIndex: index,
               lines: []
             };
+            currentBlock = block;
             foundLines = block.lines;
             blocks.push(block);
           }
           if (lineCount >= maxResultLines) {
-            foundLines.push('...');
+            currentBlock.beyondLimit = true;
             return blocks;
           }
           for (var i = startIndex; i < index; i++) {
-            foundLines.push('' + (i + 1) + ':\t' + escapeHTML(lines[i]));
+            foundLines.push(lines[i]);
             lineCount++;
           }
-          foundLines.push('' + (index + 1) + ':\t' + decorated);
+          foundLines.push(line);
           lineCount++;
           lastFoundLineIndex = lastAddedLineIndex = index;
         }
       }
       return blocks;
     }
-    function getSummary(bodyText, searchRegex) {
-      return getTargetLines(bodyText, searchRegex);
+    /**
+     * @param {Date} now
+     * @param {string} dateText
+     */
+    function getPassage(now, dateText) {
+      if (!dateText) {
+        return '';
+      }
+      var units = [{u: 'm', max: 60}, {u: 'h', max: 24}, {u: 'd', max: 1}];
+      var d = new Date();
+      d.setTime(Date.parse(dateText));
+      var t = (now.getTime() - d.getTime()) / (1000 * 60); // minutes
+      var unit = units[0].u; var card = units[0].max;
+      for (var i = 0; i < units.length; i++) {
+        unit = units[i].u; card = units[i].max;
+        if (t < card) break;
+        t = t / card;
+      }
+      return '(' + Math.floor(t) + unit + ')';
     }
-    function hookSearch2(e) {
-      var form = document.querySelector('form');
-      if (form && form.q) {
-        var q = form.q;
-        if (q.value === '') {
-          q.focus();
+    /**
+     * @param {string} searchText
+     */
+    function removeSearchOperators(searchText) {
+      var sp = searchText.split(/\s+/);
+      if (sp.length <= 1) {
+        return searchText;
+      }
+      for (var i = sp.length - 2; i >= 1; i--) {
+        if (sp[i] === 'OR') {
+          sp.splice(i, 1);
         }
       }
+      return sp.join(' ');
+    }
+    /**
+     * @param {string} pathname
+     */
+    function getSearchCacheKeyBase(pathname) {
+      return 'path.' + pathname + '.search2.';
+    }
+    /**
+     * @param {string} pathname
+     */
+    function getSearchCacheKeyDateBase(pathname) {
+      var now = new Date();
+      var dateKey = now.getFullYear() + '_0' + (now.getMonth() + 1) + '_0' + now.getDate();
+      dateKey = dateKey.replace(/_\d?(\d\d)/g, '$1');
+      return getSearchCacheKeyBase(pathname) + dateKey + '.';
+    }
+    /**
+     * @param {string} pathname
+     * @param {string} searchText
+     * @param {number} offset
+     */
+    function getSearchCacheKey(pathname, searchText, offset) {
+      return getSearchCacheKeyDateBase(pathname) + 'offset=' + offset +
+        '.' + searchText;
+    }
+    /**
+     * @param {string} pathname
+     * @param {string} searchText
+     */
+    function clearSingleCache(pathname, searchText) {
+      if (!window.localStorage) return;
+      var removeTargets = [];
+      var keyBase = getSearchCacheKeyDateBase(pathname);
+      for (var i = 0, n = localStorage.length; i < n; i++) {
+        var key = localStorage.key(i);
+        if (key.substr(0, keyBase.length) === keyBase) {
+          // Search result Cache
+          var subKey = key.substr(keyBase.length);
+          var m = subKey.match(/^offset=\d+\.(.+)$/);
+          if (m && m[1] === searchText) {
+            removeTargets.push(key);
+          }
+        }
+      }
+      removeTargets.forEach(function(target) {
+        localStorage.removeItem(target);
+      });
     }
+    /**
+     * @param {string} body
+     */
     function getBodySummary(body) {
       var lines = body.split('\n');
       var isInAuthorHeader = true;
       var summary = [];
-      var lineCount = 0;
       for (var index = 0, length = lines.length; index < length; index++) {
         var line = lines[index];
         if (isInAuthorHeader) {
@@ -389,7 +518,7 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
         if (line.match(/^#\w+/)) continue; // Block-type plugin
         if (line.match(/^\/\//)) continue; // Comment
         if (line.substr(0, 1) === '*') {
-          line = line.replace(/\s*\[\#\w+\]$/, ''); // Remove anchor
+          line = line.replace(/\s*\[#\w+\]$/, ''); // Remove anchor
         }
         summary.push(line);
         if (summary.length >= 10) {
@@ -398,12 +527,388 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
       }
       return summary.join(' ').substring(0, 150);
     }
+    /**
+     * @param {string} q searchText
+     */
+    function encodeSearchText(q) {
+      var sp = q.split(/\s+/);
+      for (var i = 0; i < sp.length; i++) {
+        sp[i] = encodeURIComponent(sp[i]);
+      }
+      return sp.join('+');
+    }
+    /**
+     * @param {string} q searchText
+     */
+    function encodeSearchTextForHash(q) {
+      var sp = q.split(/\s+/);
+      return sp.join('+');
+    }
+    function getSearchTextInLocationHash() {
+      var hash = document.location.hash;
+      if (!hash) return '';
+      var q = '';
+      if (hash.substr(0, 3) === '#q=') {
+        q = hash.substr(3).replace(/\+/g, ' ');
+      } else {
+        return '';
+      }
+      var decodedQ = decodeURIComponent(q);
+      if (q !== decodedQ) {
+        q = decodedQ + ' OR ' + q;
+      }
+      return q;
+    }
+    function colorSearchTextInBody() {
+      var searchText = getSearchTextInLocationHash();
+      if (!searchText) return;
+      var searchRegex = textToRegex(removeSearchOperators(searchText));
+      if (!searchRegex) return;
+      var ignoreTags = ['INPUT', 'TEXTAREA', 'BUTTON',
+        'SCRIPT', 'FRAME', 'IFRAME'];
+      /**
+       * @param {Element} element
+       */
+      function colorSearchText(element) {
+        var decorated = findAndDecorateText(element.nodeValue, searchRegex);
+        if (decorated) {
+          var span = document.createElement('span');
+          span.innerHTML = decorated;
+          element.parentNode.replaceChild(span, element);
+        }
+      }
+      /**
+       * @param {Element} element
+       */
+      function walkElement(element) {
+        var e = element.firstChild;
+        while (e) {
+          if (e.nodeType === 3 && e.nodeValue &&
+              e.nodeValue.length >= 2 && /\S/.test(e.nodeValue)) {
+            var next = e.nextSibling;
+            colorSearchText(e, searchRegex);
+            e = next;
+          } else {
+            if (e.nodeType === 1 && ignoreTags.indexOf(e.tagName) === -1) {
+              walkElement(e);
+            }
+            e = e.nextSibling;
+          }
+        }
+      }
+      var target = document.getElementById('body');
+      walkElement(target);
+    }
+    /**
+     * @param {Array<Object>} newResults
+     * @param {Element} ul
+     */
+    function removePastResults(newResults, ul) {
+      var removedCount = 0;
+      var nodes = ul.childNodes;
+      for (var i = nodes.length - 1; i >= 0; i--) {
+        var node = nodes[i];
+        if (node.tagName !== 'LI' && node.tagName !== 'DIV') continue;
+        var nodePagename = node.getAttribute('data-pagename');
+        var isRemoveTarget = false;
+        for (var j = 0, n = newResults.length; j < n; j++) {
+          var r = newResults[j];
+          if (r.name === nodePagename) {
+            isRemoveTarget = true;
+            break;
+          }
+        }
+        if (isRemoveTarget) {
+          if (node.tagName === 'LI') {
+            removedCount++;
+          }
+          ul.removeChild(node);
+        }
+      }
+      return removedCount;
+    }
+    /**
+     * @param {Array<Object>} results
+     * @param {string} searchText
+     * @param {RegExp} searchRegex
+     * @param {Element} parentElement
+     * @param {boolean} insertTop
+     */
+    function addSearchResult(results, searchText, searchRegex, parentElement, insertTop) {
+      var now = new Date();
+      var parentFragment = document.createDocumentFragment();
+      results.forEach(function(val) {
+        var fragment = document.createDocumentFragment();
+        var li = document.createElement('li');
+        var hash = '#q=' + encodeSearchTextForHash(searchText);
+        var href = val.url + hash;
+        var decoratedName = findAndDecorateText(val.name, searchRegex);
+        if (!decoratedName) {
+          decoratedName = escapeHTML(val.name);
+        }
+        var updatedAt = val.updatedAt;
+        var liHtml = '<a href="' + escapeHTML(href) + '">' + decoratedName + '</a> ' +
+          getPassage(now, updatedAt);
+        li.innerHTML = liHtml;
+        li.setAttribute('data-pagename', val.name);
+        fragment.appendChild(li);
+        var div = document.createElement('div');
+        div.classList.add('search-result-detail');
+        var head = document.createElement('div');
+        head.classList.add('search-result-page-summary');
+        head.innerHTML = escapeHTML(val.bodySummary);
+        div.appendChild(head);
+        var summaryInfo = val.hitSummary;
+        for (var i = 0; i < summaryInfo.length; i++) {
+          var pre = document.createElement('pre');
+          pre.innerHTML = decorateFoundBlock(summaryInfo[i], searchRegex);
+          div.appendChild(pre);
+        }
+        div.setAttribute('data-pagename', val.name);
+        fragment.appendChild(div);
+        parentFragment.appendChild(fragment);
+      });
+      if (insertTop && parentElement.firstChild) {
+        parentElement.insertBefore(parentFragment, parentElement.firstChild);
+      } else {
+        parentElement.appendChild(parentFragment);
+      }
+    }
+    /**
+     * @param {Object} obj
+     * @param {Object} session
+     * @param {string} searchText
+     * @param {number} prevTimestamp
+     */
+    function showResult(obj, session, searchText, prevTimestamp) {
+      var props = getSiteProps();
+      var searchRegex = textToRegex(removeSearchOperators(searchText));
+      var ul = document.querySelector('#_plugin_search2_result-list');
+      if (!ul) return;
+      if (obj.start_index === 0 && !prevTimestamp) {
+        ul.innerHTML = '';
+      }
+      var searchDone = obj.search_done;
+      if (!session.scanPageCount) session.scanPageCount = 0;
+      if (!session.readPageCount) session.readPageCount = 0;
+      if (!session.hitPageCount) session.hitPageCount = 0;
+      var prevHitPageCount = session.hitPageCount;
+      session.hitPageCount += obj.results.length;
+      if (!prevTimestamp) {
+        session.scanPageCount += obj.scan_page_count;
+        session.readPageCount += obj.read_page_count;
+        session.pageCount = obj.page_count;
+      }
+      session.searchStartTime = obj.search_start_time;
+      session.authUser = obj.auth_user;
+      if (prevHitPageCount === 0 && session.hitPageCount > 0) {
+        showSecondSearchForm();
+      }
+      var results = obj.results;
+      var cachedResults = [];
+      results.forEach(function(val) {
+        var cache = {};
+        cache.name = val.name;
+        cache.url = val.url;
+        cache.updatedAt = val.updated_at;
+        cache.updatedTime = val.updated_time;
+        cache.bodySummary = getBodySummary(val.body);
+        cache.hitSummary = getSummaryInfo(val.body, searchRegex);
+        cachedResults.push(cache);
+      });
+      if (prevTimestamp) {
+        var removedCount = removePastResults(cachedResults, ul);
+        session.hitPageCount -= removedCount;
+      }
+      var msg = getSearchResultMessage(session, searchText, searchRegex, !searchDone);
+      setSearchMessage(msg);
+      if (prevTimestamp) {
+        setSearchStatus(searchProps.searchingMsg);
+      } else {
+        setSearchStatus(searchProps.searchingMsg + ' ' +
+          getSearchProgress(session));
+      }
+      if (searchDone) {
+        var singlePageResult = session.offset === 0 && !session.nextOffset;
+        var progress = getSearchProgress(session);
+        setTimeout(function() {
+          if (singlePageResult) {
+            setSearchStatus('');
+          } else {
+            setSearchStatus(searchProps.showingResultMsg + ' ' + progress);
+          }
+        }, 2000);
+      }
+      if (session.results) {
+        if (prevTimestamp) {
+          var newResult = [].concat(cachedResults);
+          Array.prototype.push.apply(newResult, session.results);
+          session.results = newResult;
+        } else {
+          Array.prototype.push.apply(session.results, cachedResults);
+        }
+      } else {
+        session.results = cachedResults;
+      }
+      addSearchResult(cachedResults, searchText, searchRegex, ul, prevTimestamp);
+      var maxResults = searchProps.maxResults;
+      if (searchDone) {
+        session.searchText = searchText;
+        var prevOffset = searchProps.prevOffset;
+        if (prevOffset) {
+          session.prevOffset = parseInt(prevOffset, 10);
+        }
+        var json = JSON.stringify(session);
+        var cacheKey = getSearchCacheKey(props.base_uri_pathname, searchText, session.offset);
+        if (window.localStorage) {
+          localStorage[cacheKey] = json;
+        }
+        if ('prevOffset' in session || 'nextOffset' in session) {
+          setSearchMessage(msg + ' ' + getOffsetLinks(session, maxResults));
+        }
+      }
+      if (!searchDone && obj.next_start_index) {
+        if (session.results.length >= maxResults) {
+          // Save results
+          session.nextOffset = obj.next_start_index;
+          var prevOffset2 = searchProps.prevOffset;
+          if (prevOffset2) {
+            session.prevOffset = parseInt(prevOffset2, 10);
+          }
+          var key = getSearchCacheKey(props.base_uri_pathname, searchText, session.offset);
+          localStorage[key] = JSON.stringify(session);
+          // Stop API calling
+          setSearchMessage(msg + ' ' + getOffsetLinks(session, maxResults));
+          setSearchStatus(searchProps.showingResultMsg + ' ' +
+            getSearchProgress(session));
+        } else {
+          setTimeout(function() {
+            doSearch(searchText, // eslint-disable-line no-use-before-define
+              session, obj.next_start_index,
+              obj.search_start_time);
+          }, searchProps.searchInterval);
+        }
+      }
+    }
+    /**
+     * @param {string} searchText
+     * @param {string} base
+     * @param {number} offset
+     */
+    function showCachedResult(searchText, base, offset) {
+      var props = getSiteProps();
+      var searchRegex = textToRegex(removeSearchOperators(searchText));
+      var ul = document.querySelector('#_plugin_search2_result-list');
+      if (!ul) return null;
+      var searchCacheKey = getSearchCacheKey(props.base_uri_pathname, searchText, offset);
+      var cache1 = localStorage[searchCacheKey];
+      if (!cache1) {
+        return null;
+      }
+      var session = JSON.parse(cache1);
+      if (!session) return null;
+      if (base !== session.base) {
+        return null;
+      }
+      var user = searchProps.user;
+      if (user !== session.authUser) {
+        return null;
+      }
+      if (session.hitPageCount > 0) {
+        showSecondSearchForm();
+      }
+      var msg = getSearchResultMessage(session, searchText, searchRegex, false);
+      setSearchMessage(msg);
+      addSearchResult(session.results, searchText, searchRegex, ul);
+      var maxResults = searchProps.maxResults;
+      if ('prevOffset' in session || 'nextOffset' in session) {
+        var moreResultHtml = getOffsetLinks(session, maxResults);
+        setSearchMessage(msg + ' ' + moreResultHtml);
+        var progress = getSearchProgress(session);
+        setSearchStatus(searchProps.showingResultMsg + ' ' + progress);
+      } else {
+        setSearchStatus('');
+      }
+      return session;
+    }
+    function removeCachedResults() {
+      var props = getSiteProps();
+      if (!props || !props.base_uri_pathname) return;
+      var keyPrefix = getSearchCacheKeyDateBase(props.base_uri_pathname);
+      var keyBase = getSearchCacheKeyBase(props.base_uri_pathname);
+      var removeTargets = [];
+      for (var i = 0, n = localStorage.length; i < n; i++) {
+        var key = localStorage.key(i);
+        if (key.substr(0, keyBase.length) === keyBase) {
+          // Search result Cache
+          if (key.substr(0, keyPrefix.length) !== keyPrefix) {
+            removeTargets.push(key);
+          }
+        }
+      }
+      removeTargets.forEach(function(target) {
+        localStorage.removeItem(target);
+      });
+    }
+    /**
+     * @param {string} searchText
+     * @param {object} session
+     * @param {number} startIndex
+     * @param {number} searchStartTime
+     * @param {number} prevTimestamp
+     */
+    function doSearch(searchText, session, startIndex, searchStartTime, prevTimestamp) {
+      var url = './?cmd=search2&action=query';
+      url += '&encode_hint=' + encodeURIComponent('\u3077');
+      if (searchText) {
+        url += '&q=' + encodeURIComponent(searchText);
+      }
+      if (session.base) {
+        url += '&base=' + encodeURIComponent(session.base);
+      }
+      if (prevTimestamp) {
+        url += '&modified_since=' + prevTimestamp;
+      } else {
+        url += '&start=' + startIndex;
+        if (searchStartTime) {
+          url += '&search_start_time=' + encodeURIComponent(searchStartTime);
+        }
+        if (!('offset' in session)) {
+          session.offset = startIndex;
+        }
+      }
+      fetch(url, {credentials: 'same-origin'}
+      ).then(function(response) {
+        if (response.ok) {
+          return response.json();
+        }
+        throw new Error(response.status + ': ' +
+          response.statusText + ' on ' + url);
+      }).then(function(obj) {
+        showResult(obj, session, searchText, prevTimestamp);
+      })['catch'](function(err) { // eslint-disable-line dot-notation
+        if (window.console && console.log) {
+          console.log(err);
+          console.log('Error! Please check JavaScript console\n' + JSON.stringify(err) + '|' + err);
+        }
+        setSearchStatus(searchProps.errorMsg);
+      });
+    }
+    function hookSearch2() {
+      var form = document.querySelector('form');
+      if (form && form.q) {
+        var q = form.q;
+        if (q.value === '') {
+          q.focus();
+        }
+      }
+    }
     function removeEncodeHint() {
       // Remove 'encode_hint' if site charset is UTF-8
       var props = getSiteProps();
       if (!props.is_utf8) return;
       var forms = document.querySelectorAll('form');
-      forEach(forms, function(form){
+      forEach(forms, function(form) {
         if (form.cmd && form.cmd.value === 'search2') {
           if (form.encode_hint && (typeof form.encode_hint.removeAttribute === 'function')) {
             form.encode_hint.removeAttribute('name');
@@ -416,44 +921,44 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
       var searchText = form && form.q;
       if (!searchText) return;
       if (searchText && searchText.value) {
-        var e = document.querySelector('#_plugin_search2_msg_searching');
-        var msg = e && e.value || 'Searching...';
-        setSearchStatus(msg);
-        var base = '';
-        forEach(form.querySelectorAll('input[name="base"]'), function(radio){
-          if (radio.checked) base = radio.value;
-        });
-        doSearch(searchText.value, {base: base}, 0);
-      }
-    }
-    function setSearchStatus(statusText) {
-      var statusList = document.querySelectorAll('._plugin_search2_search_status');
-      forEach(statusList, function(statusObj){
-        statusObj.textContent = statusText;
-      });
-    }
-    function setSearchMessage(msgHTML) {
-      var objList = document.querySelectorAll('._plugin_search2_message');
-      forEach(objList, function(obj){
-        obj.innerHTML = msgHTML;
-      });
-    }
-    function forEach(nodeList, func) {
-      if (nodeList.forEach) {
-        nodeList.forEach(func);
-      } else {
-        for (var i = 0, n = nodeList.length; i < n; i++) {
-          func(nodeList[i], i);
+        var offset = searchProps.offset;
+        var base = getSearchBase(form);
+        var prevSession = showCachedResult(searchText.value, base, offset);
+        if (prevSession) {
+          // Display Cache results, then search only modified pages
+          if (!('offset' in prevSession) || prevSession.offset === 0) {
+            doSearch(searchText.value, prevSession, offset, null,
+              prevSession.searchStartTime);
+          } else {
+            // Show search results
+          }
+        } else {
+          doSearch(searchText.value, {base: base, offset: offset}, offset, null);
         }
+        removeCachedResults();
       }
     }
     function replaceSearchWithSearch2() {
-      forEach(document.querySelectorAll('form'), function(f){
+      forEach(document.querySelectorAll('form'), function(f) {
+        function onAndRadioClick() {
+          var sp = removeSearchOperators(f.word.value).split(/\s+/);
+          var newText = sp.join(' ');
+          if (f.word.value !== newText) {
+            f.word.value = newText;
+          }
+        }
+        function onOrRadioClick() {
+          var sp = removeSearchOperators(f.word.value).split(/\s+/);
+          var newText = sp.join(' OR ');
+          if (f.word.value !== newText) {
+            f.word.value = newText;
+          }
+        }
         if (f.action.match(/cmd=search$/)) {
           f.addEventListener('submit', function(e) {
             var q = e.target.word.value;
             var base = '';
-            forEach(f.querySelectorAll('input[name="base"]'), function(radio){
+            forEach(f.querySelectorAll('input[name="base"]'), function(radio) {
               if (radio.checked) base = radio.value;
             });
             var props = getSiteProps();
@@ -470,116 +975,31 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
               (base ? '&base=' + encodeURIComponent(base) : '');
             e.preventDefault();
             setTimeout(function() {
-              location.href = url;
+              window.location.href = url;
             }, 1);
             return false;
           });
           var radios = f.querySelectorAll('input[type="radio"][name="type"]');
-          forEach(radios, function(radio){
+          forEach(radios, function(radio) {
             if (radio.value === 'AND') {
               radio.addEventListener('click', onAndRadioClick);
             } else if (radio.value === 'OR') {
               radio.addEventListener('click', onOrRadioClick);
             }
           });
-          function onAndRadioClick(e) {
-            var sp = removeSearchOperators(f.word.value).split(/\s+/);
-            var newText = sp.join(' ');
-            if (f.word.value !== newText) {
-              f.word.value = newText;
-            }
-          }
-          function onOrRadioClick(e) {
-            var sp = removeSearchOperators(f.word.value).split(/\s+/);
-            var newText = sp.join(' OR ');
-            if (f.word.value !== newText) {
-              f.word.value = newText;
+        } else if (f.cmd && f.cmd.value === 'search2') {
+          f.addEventListener('submit', function() {
+            var newSearchText = f.q.value;
+            var prevSearchText = f.q.getAttribute('data-original-q');
+            if (newSearchText === prevSearchText) {
+              // Clear resultCache to search same text again
+              var props = getSiteProps();
+              clearSingleCache(props.base_uri_pathname, prevSearchText);
             }
-          }
+          });
         }
       });
     }
-    function encodeSearchText(q) {
-      var sp = q.split(/\s+/);
-      for (var i = 0; i < sp.length; i++) {
-        sp[i] = encodeURIComponent(sp[i]);
-      }
-      return sp.join('+');
-    }
-    function encodeSearchTextForHash(q) {
-      var sp = q.split(/\s+/);
-      return sp.join('+');
-    }
-    function findAndDecorateText(text, searchRegex) {
-      var isReplaced = false;
-      var lastIndex = 0;
-      var m;
-      var decorated = '';
-      searchRegex.lastIndex = 0;
-      while ((m = searchRegex.exec(text)) !== null) {
-        isReplaced = true;
-        var pre = text.substring(lastIndex, m.index);
-        decorated += escapeHTML(pre);
-        for (var i = 1; i < m.length; i++) {
-          if (m[i]) {
-            decorated += '<strong class="word' + (i - 1) + '">' + escapeHTML(m[i]) + '</strong>'
-          }
-        }
-        lastIndex = searchRegex.lastIndex;
-      }
-      if (isReplaced) {
-        decorated += escapeHTML(text.substr(lastIndex));
-        return decorated;
-      }
-      return null;
-    }
-    function getSearchTextInLocationHash() {
-      // TODO Cross browser
-      var hash = location.hash;
-      if (!hash) return '';
-      var q = '';
-      if (hash.substr(0, 3) === '#q=') {
-        q = hash.substr(3).replace(/\+/g, ' ');
-      } else if (hash.substr(0, 6) === '#encq=') {
-        q = decodeURIComponent(hash.substr(6).replace(/\+/g, ' '));
-      }
-      return q;
-    }
-    function colorSearchTextInBody() {
-      var searchText = getSearchTextInLocationHash();
-      if (!searchText) return;
-      var searchRegex = textToRegex(removeSearchOperators(searchText));
-      var headReText = '([\\s\\b]|^)';
-      var tailReText = '\\b';
-      var ignoreTags = ['INPUT', 'TEXTAREA', 'BUTTON',
-        'SCRIPT', 'FRAME', 'IFRAME'];
-      function colorSearchText(element, searchRegex) {
-        var decorated = findAndDecorateText(element.nodeValue, searchRegex);
-        if (decorated) {
-          var span = document.createElement('span');
-          span.innerHTML = decorated;
-          element.parentNode.replaceChild(span, element);
-        }
-      }
-      function walkElement(element) {
-        var e = element.firstChild;
-        while (e) {
-          if (e.nodeType == 3 && e.nodeValue &&
-              e.nodeValue.length >= 2 && /\S/.test(e.nodeValue)) {
-            var next = e.nextSibling;
-            colorSearchText(e, searchRegex);
-            e = next;
-          } else {
-            if (e.nodeType == 1 && ignoreTags.indexOf(e.tagName) == -1) {
-              walkElement(e);
-            }
-            e = e.nextSibling;
-          }
-        }
-      }
-      var target = document.getElementById('body');
-      walkElement(target);
-    }
     function showNoSupportMessage() {
       var pList = document.getElementsByClassName('_plugin_search2_nosupport_message');
       for (var i = 0; i < pList.length; i++) {
@@ -598,21 +1018,13 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
       if (props.json_enabled) return true;
       return false;
     }
-    function getSiteProps() {
-      var empty = {};
-      var propsDiv = document.getElementById('pukiwiki-site-properties');
-      if (!propsDiv) return empty;
-      var jsonE = propsDiv.querySelector('div[data-key="site-props"]');
-      if (!jsonE) return emptry;
-      var props = JSON.parse(jsonE.getAttribute('data-value'));
-      return props || empty;
-    }
+    prepareSearchProps();
     colorSearchTextInBody();
-    if (! isEnabledFetchFunctions()) {
+    if (!isEnabledFetchFunctions()) {
       showNoSupportMessage();
       return;
     }
-    if (! isEnableServerFunctions()) return;
+    if (!isEnableServerFunctions()) return;
     replaceSearchWithSearch2();
     hookSearch2();
     removeEncodeHint();
index bbd2f09..dccd400 100644 (file)
@@ -519,17 +519,20 @@ tr.bugtrack_state_undef td {
 
 /* search2.inc.php  */
 input#_plugin_search2_detail:checked ~ ul > div.search-result-detail {
-       display: block;
+  display: block;
 }
 input#_plugin_search2_detail ~ ul > div.search-result-detail {
-       display: none;
+  display: none;
+}
+._plugin_search2_search_status {
+  min-height: 1.5em;
 }
 .search-result-page-summary {
-       font-size: 70%;
-       color: gray;
-       overflow: hidden;
-       text-overflow: ellipsis;
-       white-space: nowrap;
+  font-size: 70%;
+  color: gray;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
 @media print {