• R/O
  • HTTP
  • SSH
  • HTTPS

コミット

タグ
未設定

よく使われているワード(クリックで追加)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

コミットメタ情報

リビジョン1309e51ee63316d75c3a4bc879012fec19875034 (tree)
日時2017-10-01 05:52:20
作者umorigu <umorigu@gmai...>
コミッターumorigu

ログメッセージ

BugTrack/2434 Search result cache and offset paging

変更サマリ

差分

--- a/en.lng.php
+++ b/en.lng.php
@@ -47,6 +47,8 @@ $_msg_goto = 'Go to $1.';
4747 $_msg_andresult = 'In the page <strong> $2</strong>, <strong> $3</strong> pages that contain all the terms $1 were found.';
4848 $_msg_orresult = 'In the page <strong> $2</strong>, <strong> $3</strong> pages that contain at least one of the terms $1 were found.';
4949 $_msg_notfoundresult = 'No page which contains $1 has been found.';
50+$_msg_prev_results = '&lt;&lt; Previous $1 pages';
51+$_msg_more_results = 'Next $1 pages &gt;&gt;';
5052 $_msg_symbol = 'Symbols';
5153 $_msg_other = 'Others';
5254 $_msg_help = 'View Text Formatting Rules';
@@ -55,6 +57,7 @@ $_msg_content_back_to_top = '<div class="jumpmenu"><a href="#navigator">&uarr;</
5557 $_msg_word = 'These search terms have been highlighted:';
5658 $_msg_unsupported_webbrowser = 'This function doesn\'t support your current Web browser.';
5759 $_msg_use_alternative_link = 'Please go to the following link destination: $1';
60+$_msg_general_error = 'An error occurred while processing.';
5861
5962 ///////////////////////////////////////
6063 // Symbols
@@ -370,6 +373,7 @@ $_btn_or = 'OR';
370373 $_search_pages = 'Search for page starts from $1';
371374 $_search_all = 'Search for all pages';
372375 $_search_searching = 'Searching...';
376+$_search_showing_result = 'Showing search results';
373377 $_search_detail = 'Show details';
374378
375379 ///////////////////////////////////////
--- a/ja.lng.php
+++ b/ja.lng.php
@@ -49,6 +49,8 @@ $_msg_goto = '$1 へ行く。';
4949 $_msg_andresult = '$1 のすべてを含むページは <strong>$3</strong> ページ中、 <strong>$2</strong> ページ見つかりました。';
5050 $_msg_orresult = '$1 のいずれかを含むページは <strong>$3</strong> ページ中、 <strong>$2</strong> ページ見つかりました。';
5151 $_msg_notfoundresult = '$1 を含むページは見つかりませんでした。';
52+$_msg_prev_results = '&lt;&lt; 前の $1 ページ';
53+$_msg_more_results = '次の $1 ページ &gt;&gt;';
5254 $_msg_symbol = '記号';
5355 $_msg_other = '日本語';
5456 $_msg_help = 'テキスト整形のルールを表示する';
@@ -57,6 +59,7 @@ $_msg_content_back_to_top = '<div class="jumpmenu"><a href="#navigator">&uarr;</
5759 $_msg_word = 'これらのキーワードがハイライトされています:';
5860 $_msg_unsupported_webbrowser = 'この機能はお使いのWebブラウザには対応していません。';
5961 $_msg_use_alternative_link = 'リンク先の機能をご利用ください: $1';
62+$_msg_general_error = '処理中にエラーが発生しました。';
6063
6164 ///////////////////////////////////////
6265 // Symbols
@@ -372,6 +375,7 @@ $_btn_or = 'OR検索';
372375 $_search_pages = '$1 から始まるページを検索';
373376 $_search_all = '全てのページを検索';
374377 $_search_searching = '検索中...';
378+$_search_showing_result = '検索結果表示';
375379 $_search_detail = '詳細表示';
376380
377381 ///////////////////////////////////////
--- a/lib/file.php
+++ b/lib/file.php
@@ -257,11 +257,26 @@ function get_author_info($wikitext)
257257 // Found #freeze still in header
258258 } else {
259259 // other line, #author not found
260- return false;
260+ return null;
261261 }
262262 $start = $pos + 1;
263263 }
264- return false;
264+ return null;
265+}
266+
267+/**
268+ * Get updated datetime from author
269+ */
270+function get_update_datetime_from_author($author_line) {
271+ $m = null;
272+ if (preg_match('/^#author\(\"([^\";]+)(?:;([^\";]+))?/', $author_line, $m)) {
273+ if ($m[2]) {
274+ return $m[2];
275+ } else if ($m[1]) {
276+ return $m[1];
277+ }
278+ }
279+ return null;
265280 }
266281
267282 function get_date_atom($timestamp)
@@ -604,6 +619,24 @@ function put_lastmodified()
604619 }
605620
606621 /**
622+ * Get recent files
623+ *
624+ * @return Array of (file => time)
625+ */
626+function get_recent_files()
627+{
628+ $recentfile = CACHE_DIR . PKWK_MAXSHOW_CACHE;
629+ $lines = file($recentfile);
630+ if (!$lines) return array();
631+ $files = array();
632+ foreach ($lines as $line) {
633+ list ($time, $file) = explode("\t", rtrim($line));
634+ $files[$file] = $time;
635+ }
636+ return $files;
637+}
638+
639+/**
607640 * Update RecentChanges page / Invalidate recent.dat
608641 */
609642 function delete_recent_changes_cache() {
--- a/plugin/search2.inc.php
+++ b/plugin/search2.inc.php
@@ -6,12 +6,12 @@
66 //
77 // Search2 plugin - Show detail result using JavaScript
88
9-define('PLUGIN_SEARCH2_MAX_LENGTH', 80);
10-define('PLUGIN_SEARCH2_MAX_BASE', 16); // #search(1,2,3,...,15,16)
9+define('PLUGIN_SEARCH2_MAX_BASE', 16); // #search(1,2,3,...,15,16)
1110
1211 define('PLUGIN_SEARCH2_RESULT_RECORD_LIMIT', 500);
1312 define('PLUGIN_SEARCH2_RESULT_RECORD_LIMIT_START', 100);
1413 define('PLUGIN_SEARCH2_SEARCH_WAIT_MILLISECONDS', 1000);
14+define('PLUGIN_SEARCH2_SEARCH_MAX_RESULTS', 1000);
1515
1616 // Show a search box on a page
1717 function plugin_search2_convert()
@@ -22,7 +22,7 @@ function plugin_search2_convert()
2222
2323 function plugin_search2_action()
2424 {
25- global $vars, $_title_search, $_title_result;
25+ global $vars, $_title_search, $_title_result, $_msg_searching;
2626
2727 $action = isset($vars['action']) ? $vars['action'] : '';
2828 $base = isset($vars['base']) ? $vars['base'] : '';
@@ -33,28 +33,71 @@ function plugin_search2_action()
3333 $bases[] = $base;
3434 }
3535 if ($action === '') {
36- $q = isset($vars['q']) ? $vars['q'] : '';
36+ $q = trim(isset($vars['q']) ? $vars['q'] : '');
37+ $offset_s = isset($vars['offset']) ? $vars['offset'] : '';
38+ $offset = pkwk_ctype_digit($offset_s) ? intval($offset_s) : 0;
39+ $prev_offset_s = isset($vars['prev_offset']) ? $vars['prev_offset'] : '';
3740 if ($q === '') {
3841 return array('msg' => $_title_search,
39- 'body' => plugin_search2_search_form($q, '', $bases));
42+ 'body' => "<br>" . $_msg_searching . "\n" .
43+ plugin_search2_search_form($q, $bases, $offset));
4044 } else {
4145 $msg = str_replace('$1', htmlsc($q), $_title_result);
4246 return array('msg' => $msg,
43- 'body' => plugin_search2_search_form($q, '', $bases));
47+ 'body' => plugin_search2_search_form($q, $bases, $offset, $prev_offset_s));
4448 }
4549 } else if ($action === 'query') {
46- $text = isset($vars['q']) ? $vars['q'] : '';
50+ $q = isset($vars['q']) ? $vars['q'] : '';
51+ $search_start_time = isset($vars['search_start_time']) ?
52+ $vars['search_start_time'] : null;
53+ $modified_since = (int)(isset($vars['modified_since']) ?
54+ $vars['modified_since'] : '0');
4755 header('Content-Type: application/json; charset=UTF-8');
48- plugin_search2_do_search($text, $base, $start_index);
56+ plugin_search2_do_search($q, $base, $start_index,
57+ $search_start_time, $modified_since);
4958 exit;
5059 }
5160 }
5261
53-function plugin_search2_do_search($query_text, $base, $start_index)
62+function plugin_search2_get_base_url($search_text)
63+{
64+ global $vars;
65+ $params = array();
66+ if (!defined('PKWK_UTF8_ENABLE')) {
67+ $params[] = 'encode_hint=' . rawurlencode($vars['encode_hint']);
68+ }
69+ $params[] = 'cmd=search2';
70+ if (isset($vars['encode_hint']) && $vars['encode_hint']) {
71+ $params[] = 'encode_hint=' . rawurlencode($vars['encode_hint']);
72+ }
73+ if ($search_text) {
74+ $params[] = 'q=' . plugin_search2_urlencode_searchtext($search_text);
75+ }
76+ if (isset($vars['base']) && $vars['base']) {
77+ $params[] = 'base=' . rawurlencode($vars['base']);
78+ }
79+ $url = get_base_uri() . '?' . join('&', $params);
80+ return $url;
81+}
82+
83+function plugin_search2_urlencode_searchtext($search_text)
84+{
85+ $s2 = preg_replace('#^\s+|\s+$#', '', $search_text);
86+ if (!$s2) return '';
87+ $sp = preg_split('#\s+#', $s2);
88+ $list = array();
89+ for ($i = 0; $i < count($sp); $i++) {
90+ $list[] = rawurlencode($sp[$i]);
91+ }
92+ return join('+', $list);
93+}
94+
95+function plugin_search2_do_search($query_text, $base, $start_index,
96+ $search_start_time, $modified_since)
5497 {
5598 global $whatsnew, $non_list, $search_non_list;
5699 global $_msg_andresult, $_msg_orresult, $_msg_notfoundresult;
57- global $search_auth;
100+ global $search_auth, $auth_user;
58101
59102 $result_record_limit = $start_index === 0 ?
60103 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)
62105
63106 $b_type_and = true; // AND:TRUE OR:FALSE
64107 $key_candidates = preg_split('/\s+/', $query_text, -1, PREG_SPLIT_NO_EMPTY);
65- for ($i = count($key_candidates) - 1; $i >= 0; $i--) {
108+ for ($i = count($key_candidates) - 2; $i >= 1; $i--) {
66109 if ($key_candidates[$i] === 'OR') {
67110 $b_type_and = false;
68111 unset($key_candidates[$i]);
@@ -73,20 +116,41 @@ function plugin_search2_do_search($query_text, $base, $start_index)
73116 foreach ($keys as $key=>$value)
74117 $keys[$key] = '/' . $value . '/S';
75118
76- $pages = get_existpages();
119+ if ($modified_since > 0) {
120+ // Recent search
121+ $recent_files = get_recent_files();
122+ $modified_loc = $modified_since - LOCALZONE;
123+ $pages = array();
124+ foreach ($recent_files as $p => $time) {
125+ if ($time >= $modified_loc) {
126+ $pages[] = $p;
127+ }
128+ }
129+ if ($base != '') {
130+ $pages = preg_grep('/^' . preg_quote($base, '/') . '/S', $pages);
131+ }
132+ $page_names = $pages;
133+ } else {
134+ // Normal search
135+ $pages = get_existpages();
77136
78- // Avoid
79- if ($base != '') {
80- $pages = preg_grep('/^' . preg_quote($base, '/') . '/S', $pages);
137+ // Avoid
138+ if ($base != '') {
139+ $pages = preg_grep('/^' . preg_quote($base, '/') . '/S', $pages);
140+ }
141+ if (! $search_non_list) {
142+ $pages = array_diff($pages, preg_grep('/' . $non_list . '/S', $pages));
143+ }
144+ $pages = array_flip($pages);
145+ unset($pages[$whatsnew]);
146+ $page_names = array_keys($pages);
81147 }
82- if (! $search_non_list) {
83- $pages = array_diff($pages, preg_grep('/' . $non_list . '/S', $pages));
148+ natsort($page_names);
149+ // Cache collabolate
150+ if (is_null($search_start_time)) {
151+ // Don't use client cache
152+ $search_start_time = UTIME + LOCALZONE;
84153 }
85- natsort($pages);
86- $pages = array_flip($pages);
87- unset($pages[$whatsnew]);
88- $page_names = array_keys($pages);
89-
90154 $found_pages = array();
91155 $readable_page_index = -1;
92156 $scan_page_index = -1;
@@ -112,28 +176,38 @@ function plugin_search2_do_search($query_text, $base, $start_index)
112176 if ($saved_scan_start_index === -1) {
113177 $saved_scan_start_index = $scan_page_index;
114178 }
115- // Search for page name and contents
116- $body = get_source($page, TRUE, TRUE, TRUE);
117- $target = $page . "\n" . remove_author_header($body);
118- foreach ($keys as $key) {
119- $b_match = preg_match($key, $target);
120- if ($b_type_and xor $b_match) break; // OR
179+ if (count($keys) > 0) {
180+ // Search for page name and contents
181+ $body = get_source($page, TRUE, TRUE, TRUE);
182+ $target = $page . "\n" . remove_author_header($body);
183+ foreach ($keys as $key) {
184+ $b_match = preg_match($key, $target);
185+ if ($b_type_and xor $b_match) break; // OR
186+ }
187+ } else {
188+ // No search target. get_source($page) is meaningless.
189+ // $b_match is always false.
121190 }
122191 if ($b_match) {
123192 // Found!
124- $filemtime = null;
125193 $author_info = get_author_info($body);
126- if ($author_info === false || $pagename_only) {
127- $updated_at = get_date_atom(filemtime(get_filename($page)));
194+ if ($author_info) {
195+ $updated_at = get_update_datetime_from_author($author_info);
196+ $updated_time = strtotime($updated_at);
197+ } else {
198+ $updated_time = filemtime(get_filename($page));
199+ $updated_at = get_date_atom($updated_time);
128200 }
129201 if ($pagename_only) {
130202 // The user cannot read this page body
131203 $found_pages[] = array('name' => (string)$page,
132204 'url' => get_page_uri($page), 'updated_at' => $updated_at,
205+ 'updated_time' => $updated_time,
133206 'body' => '', 'pagename_only' => 1);
134207 } else {
135208 $found_pages[] = array('name' => (string)$page,
136209 'url' => get_page_uri($page), 'updated_at' => $updated_at,
210+ 'updated_time' => $updated_time,
137211 'body' => (string)$body);
138212 }
139213 }
@@ -157,6 +231,8 @@ function plugin_search2_do_search($query_text, $base, $start_index)
157231 'last_read_page_name' => $last_read_page_name,
158232 'next_start_index' => $readable_page_index + 1,
159233 'search_done' => $search_done,
234+ 'search_start_time' => $search_start_time,
235+ 'auth_user' => $auth_user,
160236 'results' => $found_pages);
161237 $obj = $result_obj;
162238 if (!defined('PKWK_UTF8_ENABLE')) {
@@ -169,16 +245,21 @@ function plugin_search2_do_search($query_text, $base, $start_index)
169245 print(json_encode($obj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
170246 }
171247
172-function plugin_search2_search_form($s_word = '', $type = '', $bases = array())
248+function plugin_search2_search_form($search_text = '', $bases = array(),
249+ $offset, $prev_offset_s = null)
173250 {
174251 global $_btn_search;
175252 global $_search_pages, $_search_all;
176253 global $_msg_andresult, $_msg_orresult, $_msg_notfoundresult;
177- global $_search_detail, $_search_searching;
254+ global $_search_detail, $_search_searching, $_search_showing_result;
178255 global $_msg_unsupported_webbrowser, $_msg_use_alternative_link;
256+ global $_msg_more_results, $_msg_prev_results, $_msg_general_error;
257+ global $auth_user;
179258
259+ static $search2_form_total_count = 0;
260+ $search2_form_total_count++;
180261 $script = get_base_uri();
181- $h_search_text = htmlsc($s_word);
262+ $h_search_text = htmlsc($search_text);
182263
183264 $base_option = '';
184265 if (!empty($bases)) {
@@ -210,12 +291,10 @@ EOD;
210291 $_search2_search_wait_milliseconds = PLUGIN_SEARCH2_SEARCH_WAIT_MILLISECONDS;
211292 $result_page_panel =<<<EOD
212293 <input type="checkbox" id="_plugin_search2_detail" checked><label for="_plugin_search2_detail">$_search_detail</label>
213-<input type="hidden" id="_plugin_search2_msg_searching" value="$_search_searching">
214-<input type="hidden" id="_plugin_search2_msg_result_notfound" value="$_search2_result_notfound">
215-<input type="hidden" id="_plugin_search2_msg_result_found" value="$_search2_result_found">
216-<input type="hidden" id="_search2_search_wait_milliseconds" value="$_search2_search_wait_milliseconds">
294+<ul id="_plugin_search2_result-list">
295+</ul>
217296 EOD;
218- if ($h_search_text == '') {
297+ if ($h_search_text == '' || $search2_form_total_count > 1) {
219298 $result_page_panel = '';
220299 }
221300
@@ -225,7 +304,7 @@ EOD;
225304 <form action="$script" method="GET" class="_plugin_search2_form">
226305 <div>
227306 <input type="hidden" name="cmd" value="search2">
228- <input type="search" name="q" value="$h_search_text" size="30">
307+ <input type="search" name="q" value="$h_search_text" data-original-q="$h_search_text" size="40">
229308 <input type="submit" value="$_btn_search">
230309 </div>
231310 $base_option
@@ -239,6 +318,32 @@ $form
239318 </div>
240319 EOD;
241320
321+ $h_auth_user = htmlsc($auth_user);
322+ $h_base_url = htmlsc(plugin_search2_get_base_url($search_text));
323+ $h_msg_more_results = htmlsc($_msg_more_results);
324+ $h_msg_prev_results = htmlsc($_msg_prev_results);
325+ $max_results = PLUGIN_SEARCH2_SEARCH_MAX_RESULTS;
326+ $prev_offset = pkwk_ctype_digit($prev_offset_s) ? $prev_offset_s : '';
327+ $search_props =<<<EOD
328+<div style="display:none;">
329+ <input type="hidden" id="_plugin_search2_auth_user" value="$h_auth_user">
330+ <input type="hidden" id="_plugin_search2_base_url" value="$h_base_url">
331+ <input type="hidden" id="_plugin_search2_msg_searching" value="$_search_searching">
332+ <input type="hidden" id="_plugin_search2_msg_showing_result" value="$_search_showing_result">
333+ <input type="hidden" id="_plugin_search2_msg_result_notfound" value="$_search2_result_notfound">
334+ <input type="hidden" id="_plugin_search2_msg_result_found" value="$_search2_result_found">
335+ <input type="hidden" id="_plugin_search2_msg_more_results" value="$h_msg_more_results">
336+ <input type="hidden" id="_plugin_search2_msg_prev_results" value="$h_msg_prev_results">
337+ <input type="hidden" id="_plugin_search2_search_wait_milliseconds" value="$_search2_search_wait_milliseconds">
338+ <input type="hidden" id="_plugin_search2_max_results" value="$max_results">
339+ <input type="hidden" id="_plugin_search2_offset" value="$offset">
340+ <input type="hidden" id="_plugin_search2_prev_offset" value="$prev_offset">
341+ <input type="hidden" id="_plugin_search2_msg_error" value="$_msg_general_error">
342+</div>
343+EOD;
344+ if ($search2_form_total_count > 1) {
345+ $search_props = '';
346+ }
242347
243348 return <<<EOD
244349 <noscript>
@@ -247,12 +352,11 @@ EOD;
247352 <p class="_plugin_search2_nosupport_message" style="display:none;">
248353 $_msg_unsupported_webbrowser $alt_msg
249354 </p>
355+$search_props
250356 $form
251357 <div class="_plugin_search2_search_status"></div>
252358 <div class="_plugin_search2_message"></div>
253359 $result_page_panel
254-<ul id="result-list">
255-</ul>
256360 $second_form
257361 EOD;
258362 }
--- a/skin/pukiwiki.css
+++ b/skin/pukiwiki.css
@@ -643,17 +643,20 @@ tr.bugtrack_state_undef td {
643643
644644 /* search2.inc.php */
645645 input#_plugin_search2_detail:checked ~ ul > div.search-result-detail {
646- display: block;
646+ display: block;
647647 }
648648 input#_plugin_search2_detail ~ ul > div.search-result-detail {
649- display: none;
649+ display: none;
650+}
651+._plugin_search2_search_status {
652+ min-height: 1.5em;
650653 }
651654 .search-result-page-summary {
652- font-size: 70%;
653- color: gray;
654- overflow: hidden;
655- text-overflow: ellipsis;
656- white-space: nowrap;
655+ font-size: 70%;
656+ color: gray;
657+ overflow: hidden;
658+ text-overflow: ellipsis;
659+ white-space: nowrap;
657660 }
658661
659662 @media print {
--- a/skin/search2.js
+++ b/skin/search2.js
@@ -5,16 +5,23 @@
55 // License: GPL v2 or (at your option) any later version
66 //
77 // PukiWiki search2 pluign - JavaScript client script
8-window.addEventListener && window.addEventListener('DOMContentLoaded', function() {
8+window.addEventListener && window.addEventListener('DOMContentLoaded', function() { // eslint-disable-line no-unused-expressions
9+ 'use strict';
910 function enableSearch2() {
1011 var aroundLines = 2;
1112 var maxResultLines = 20;
12- var minBlockLines = 5;
13- var minSearchWaitMilliseconds = 100;
13+ var defaultSearchWaitMilliseconds = 100;
14+ var defaultMaxResults = 1000;
1415 var kanaMap = null;
15- function escapeHTML (s) {
16- if(typeof s !== 'string') {
17- s = '' + s;
16+ var searchProps = {};
17+ /**
18+ * Escape HTML special charactors
19+ *
20+ * @param {string} s
21+ */
22+ function escapeHTML(s) {
23+ if (typeof s !== 'string') {
24+ return '' + s;
1825 }
1926 return s.replace(/[&"<>]/g, function(m) {
2027 return {
@@ -25,179 +32,184 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
2532 }[m];
2633 });
2734 }
28- function doSearch(searchText, session, startIndex) {
29- var url = './?cmd=search2&action=query';
30- var props = getSiteProps();
31- url += '&encode_hint=' + encodeURIComponent('\u3077');
32- if (searchText) {
33- url += '&q=' + encodeURIComponent(searchText);
34- }
35- if (session.base) {
36- url += '&base=' + encodeURIComponent(session.base);
35+ /**
36+ * @param {string} idText
37+ * @param {number} defaultValue
38+ * @type number
39+ */
40+ function getIntById(idText, defaultValue) {
41+ var value = defaultValue;
42+ try {
43+ var element = document.getElementById(idText);
44+ if (element) {
45+ value = parseInt(element.value, 10);
46+ if (isNaN(value)) { // eslint-disable-line no-restricted-globals
47+ value = defaultValue;
48+ }
49+ }
50+ } catch (e) {
51+ value = defaultValue;
3752 }
38- url += '&start=' + startIndex;
39- fetch (url
40- ).then(function(response){
41- if (response.ok) {
42- return response.json();
43- } else {
44- throw new Error(response.status + ': ' +
45- + response.statusText + ' on ' + url);
53+ return value;
54+ }
55+ /**
56+ * @param {string} idText
57+ * @param {string} defaultValue
58+ * @type string
59+ */
60+ function getTextById(idText, defaultValue) {
61+ var value = defaultValue;
62+ try {
63+ var element = document.getElementById(idText);
64+ if (element.value) {
65+ value = element.value;
4666 }
47- }).then(function(obj) {
48- showResult(obj, session, searchText);
49- })['catch'](function(err){
50- console.log(err);
51- console.log('Error! Please check JavaScript console' + '\n' + JSON.stringify(err) + '|' + err);
52- });
67+ } catch (e) {
68+ value = defaultValue;
69+ }
70+ return value;
5371 }
54- function getMessageTemplate(idText, defaultText) {
55- var messageHolder = document.querySelector('#' + idText);
56- var messageTemplate = (messageHolder && messageHolder.value) || defaultText;
57- return messageTemplate;
72+ function prepareSearchProps() {
73+ var p = {};
74+ p.errorMsg = getTextById('_plugin_search2_msg_error',
75+ 'An error occurred while processing.');
76+ p.searchingMsg = getTextById('_plugin_search2_msg_searching',
77+ 'Searching...');
78+ p.showingResultMsg = getTextById('_plugin_search2_msg_showing_result',
79+ 'Showing search results');
80+ p.prevOffset = getTextById('_plugin_search2_prev_offset', '');
81+ var baseUrlDefault = document.location.pathname + document.location.search;
82+ baseUrlDefault = baseUrlDefault.replace(/&offset=\d+/, '');
83+ p.baseUrl = getTextById('_plugin_search2_base_url', baseUrlDefault);
84+ p.msgPrevResultsTemplate = getTextById('_plugin_search2_msg_prev_results', 'Previous $1 pages');
85+ p.msgMoreResultsTemplate = getTextById('_plugin_search2_msg_more_results', 'Next $1 pages');
86+ p.user = getTextById('_plugin_search2_auth_user', '');
87+ p.showingResultMsg = getTextById('_plugin_search2_msg_showing_result', 'Showing search results');
88+ p.notFoundMessageTemplate = getTextById('_plugin_search2_msg_result_notfound',
89+ 'No page which contains $1 has been found.');
90+ p.foundMessageTemplate = getTextById('_plugin_search2_msg_result_found',
91+ 'In the page <strong>$2</strong>, <strong>$3</strong> pages that contain all the terms $1 were found.');
92+ p.maxResults = getIntById('_plugin_search2_max_results', defaultMaxResults);
93+ p.searchInterval = getIntById('_plugin_search2_search_wait_milliseconds', defaultSearchWaitMilliseconds);
94+ p.offset = getIntById('_plugin_search2_offset', 0);
95+ searchProps = p;
5896 }
59- function getAuthorInfo(text) {
60-
97+ function getSiteProps() {
98+ var empty = {};
99+ var propsDiv = document.getElementById('pukiwiki-site-properties');
100+ if (!propsDiv) return empty;
101+ var jsonE = propsDiv.querySelector('div[data-key="site-props"]');
102+ if (!jsonE) return empty;
103+ var props = JSON.parse(jsonE.getAttribute('data-value'));
104+ return props || empty;
61105 }
62- function getPassage(now, dateText) {
63- if (! dateText) {
64- return '';
65- }
66- var units = [{u: 'm', max: 60}, {u: 'h', max: 24}, {u: 'd', max: 1}];
67- var d = new Date();
68- d.setTime(Date.parse(dateText));
69- var t = (now.getTime() - d.getTime()) / (1000 * 60); // minutes
70- var unit = units[0].u, card = units[0].max;
71- for (var i = 0; i < units.length; i++) {
72- unit = units[i].u, card = units[i].max;
73- if (t < card) break;
74- t = t / card;
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+ }
75117 }
76- return '(' + Math.floor(t) + unit + ')';
77118 }
78- function removeSearchOperators(searchText) {
79- var sp = searchText.split(/\s+/);
80- if (sp.length <= 1) {
81- return searchText;
82- }
83- var hasOr = false;
84- for (var i = sp.length - 1; i >= 0; i--) {
85- if (sp[i] === 'OR') {
86- hasOr = true;
87- sp.splice(i, 1);
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+ }
88143 }
144+ lastIndex = searchRegex.lastIndex;
89145 }
90- return sp.join(' ');
91- }
92- function showResult(obj, session, searchText) {
93- var searchRegex = textToRegex(removeSearchOperators(searchText));
94- var ul = document.querySelector('#result-list');
95- if (!ul) return;
96- if (obj.start_index === 0) {
97- ul.innerHTML = '';
146+ if (isReplaced) {
147+ decorated += escapeHTML(text.substr(lastIndex));
148+ return decorated;
98149 }
99- if (! session.scan_page_count) session.scan_page_count = 0;
100- if (! session.read_page_count) session.read_page_count = 0;
101- if (! session.hit_page_count) session.hit_page_count = 0;
102- var prevHitPageCount = session.hit_page_count;
103- session.scan_page_count += obj.scan_page_count;
104- session.read_page_count += obj.read_page_count;
105- session.hit_page_count += obj.results.length;
106- session.page_count = obj.page_count;
107- if (prevHitPageCount === 0 && session.hit_page_count > 0) {
108- var div = document.querySelector('._plugin_search2_second_form');
109- if (div) {
110- div.style.display = 'block';
111- }
112- }
113- var msg = obj.message;
114- var notFoundMessageTemplate = getMessageTemplate('_plugin_search2_msg_result_notfound',
115- 'No page which contains $1 has been found.');
116- var foundMessageTemplate = getMessageTemplate('_plugin_search2_msg_result_found',
117- 'In the page <strong>$2</strong>, <strong>$3</strong> pages that contain all the terms $1 were found.');
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) {
118159 var searchTextDecorated = findAndDecorateText(searchText, searchRegex);
119160 if (searchTextDecorated === null) searchTextDecorated = escapeHTML(searchText);
120- var messageTemplate = foundMessageTemplate;
121- if (obj.search_done && session.hit_page_count === 0) {
122- messageTemplate = notFoundMessageTemplate;
161+ var messageTemplate = searchProps.foundMessageTemplate;
162+ if (!nowSearching && session.hitPageCount === 0) {
163+ messageTemplate = searchProps.notFoundMessageTemplate;
123164 }
124- msg = messageTemplate.replace(/\$1|\$2|\$3/g, function(m){
165+ var msg = messageTemplate.replace(/\$1|\$2|\$3/g, function(m) {
125166 return {
126- '$1': searchTextDecorated,
127- '$2': session.hit_page_count,
128- '$3': session.read_page_count
167+ $1: searchTextDecorated,
168+ $2: session.hitPageCount,
169+ $3: session.readPageCount
129170 }[m];
130171 });
131- setSearchMessage(msg);
132- var progress = ' (read:' + session.read_page_count + ', scanned:' +
133- session.scan_page_count + ', all:' + session.page_count + ')';
134- var e = document.querySelector('#_plugin_search2_msg_searching');
135- var msg = e && e.value || 'Searching...';
136- setSearchStatus(msg + progress);
137- if (obj.search_done) {
138- setTimeout(function(){
139- setSearchStatus('');
140- }, 5000);
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;
141182 }
142- var results = obj.results;
143- var now = new Date();
144- results.forEach(function(val, index) {
145- var fragment = document.createDocumentFragment();
146- var li = document.createElement('li');
147- var hash = '#q=' + encodeSearchTextForHash(searchText);
148- var href = val.url + hash;
149- var decoratedName = findAndDecorateText(val.name, searchRegex);
150- if (! decoratedName) {
151- decoratedName = escapeHTML(val.name);
152- }
153- var author = getAuthorHeader(val.body);
154- var updatedAt = '';
155- if (author) {
156- updatedAt = getUpdateTimeFromAuthorInfo(author);
157- } else {
158- updatedAt = val.updated_at;
159- }
160- var liHtml = '<a href="' + escapeHTML(href) + '">' + decoratedName + '</a> ' +
161- getPassage(now, updatedAt);
162- li.innerHTML = liHtml;
163- var a = li.querySelector('a');
164- if (a && a.hash) {
165- if (a.hash !== hash) {
166- // Some browser execute encodeHTML(hash) automatically. Support them.
167- a.href = val.url + '#encq=' + encodeSearchTextForHash(searchText);
168- }
169- }
170- fragment.appendChild(li);
171- var div = document.createElement('div');
172- div.classList.add('search-result-detail');
173- var head = document.createElement('div');
174- head.classList.add('search-result-page-summary');
175- head.innerHTML = escapeHTML(getBodySummary(val.body));
176- div.appendChild(head);
177- var summary = getSummary(val.body, searchRegex);
178- for (var i = 0; i < summary.length; i++) {
179- var pre = document.createElement('pre');
180- pre.innerHTML = summary[i].lines.join('\n');
181- div.appendChild(pre);
182- }
183- fragment.appendChild(div);
184- ul.appendChild(fragment);
185- });
186- if (!obj.search_done && obj.next_start_index) {
187- var waitE = document.querySelector('#_search2_search_wait_milliseconds');
188- var interval = minSearchWaitMilliseconds;
189- try {
190- interval = parseInt(waitE.value);
191- } catch (e) {
192- interval = minSearchWaitMilliseconds;
193- }
194- if (interval < minSearchWaitMilliseconds) {
195- interval = minSearchWaitMilliseconds;
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;
196197 }
197- setTimeout(function(){
198- doSearch(searchText, session, obj.next_start_index);
199- }, interval);
198+ var msgPrev = searchProps.msgPrevResultsTemplate.replace(/\$1/, maxResults);
199+ var prevResultHtml = '<a href="' + prevResultUrl + '">' + msgPrev + '</a>';
200+ links.push(prevResultHtml);
200201 }
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 '';
201213 }
202214 function prepareKanaMap() {
203215 if (kanaMap !== null) return;
@@ -208,102 +220,146 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
208220 var dakuten = '\uFF9E';
209221 var maru = '\uFF9F';
210222 var map = {};
211- for (var c = 0xFF61; c <=0xFF9F; c++) {
223+ for (var c = 0xFF61; c <= 0xFF9F; c++) {
212224 var han = String.fromCharCode(c);
213225 var zen = han.normalize('NFKC');
214226 map[zen] = han;
215227 var hanDaku = han + dakuten;
216228 var zenDaku = hanDaku.normalize('NFKC');
217229 if (zenDaku.length === 1) { // +Handaku-ten OK
218- map[zenDaku] = hanDaku;
230+ map[zenDaku] = hanDaku;
219231 }
220232 var hanMaru = han + maru;
221233 var zenMaru = hanMaru.normalize('NFKC');
222234 if (zenMaru.length === 1) { // +Maru OK
223- map[zenMaru] = hanMaru;
235+ map[zenMaru] = hanMaru;
224236 }
225237 }
226238 kanaMap = map;
227239 }
240+ /**
241+ * @param {searchText} searchText
242+ * @type RegExp
243+ */
228244 function textToRegex(searchText) {
229245 if (!searchText) return null;
230- var regEscape = /[\\^$.*+?()[\]{}|]/g;
231246 // 1:Symbol 2:Katakana 3:Hiragana
232247 var regRep = /([\\^$.*+?()[\]{}|])|([\u30a1-\u30f6])|([\u3041-\u3096])/g;
248+ var replacementFunc = function(m, m1, m2, m3) {
249+ if (m1) {
250+ // Symbol - escape with prior backslach
251+ return '\\' + m1;
252+ } else if (m2) {
253+ // Katakana
254+ var r = '(?:' + String.fromCharCode(m2.charCodeAt(0) - 0x60) +
255+ '|' + m2;
256+ if (kanaMap[m2]) {
257+ r += '|' + kanaMap[m2];
258+ }
259+ r += ')';
260+ return r;
261+ } else if (m3) {
262+ // Hiragana
263+ var katakana = String.fromCharCode(m3.charCodeAt(0) + 0x60);
264+ var r2 = '(?:' + m3 + '|' + katakana;
265+ if (kanaMap[katakana]) {
266+ r2 += '|' + kanaMap[katakana];
267+ }
268+ r2 += ')';
269+ return r2;
270+ }
271+ return m;
272+ };
233273 var s1 = searchText.replace(/^\s+|\s+$/g, '');
274+ if (!s1) return null;
234275 var sp = s1.split(/\s+/);
235276 var rText = '';
236277 prepareKanaMap();
237278 for (var i = 0; i < sp.length; i++) {
238279 if (rText !== '') {
239- rText += '|'
280+ rText += '|';
240281 }
241282 var s = sp[i];
242283 if (s.normalize) {
243284 s = s.normalize('NFKC');
244285 }
245- var s2 = s.replace(regRep, function(m, m1, m2, m3){
246- if (m1) {
247- // Symbol - escape with prior backslach
248- return '\\' + m1;
249- } else if (m2) {
250- // Katakana
251- var r = '(?:' + String.fromCharCode(m2.charCodeAt(0) - 0x60) +
252- '|' + m2;
253- if (kanaMap[m2]) {
254- r += '|' + kanaMap[m2];
255- }
256- r += ')';
257- return r;
258- } else if (m3) {
259- // Hiragana
260- var katakana = String.fromCharCode(m3.charCodeAt(0) + 0x60);
261- var r = '(?:' + m3 + '|' + katakana;
262- if (kanaMap[katakana]) {
263- r += '|' + kanaMap[katakana];
264- }
265- r += ')';
266- return r;
267- }
268- return m;
269- });
286+ var s2 = s.replace(regRep, replacementFunc);
270287 rText += '(' + s2 + ')';
271288 }
272289 return new RegExp(rText, 'ig');
273290 }
274- function getAuthorHeader(body) {
275- var start = 0;
276- var pos;
277- while ((pos = body.indexOf('\n', start)) >= 0) {
278- var line = body.substring(start, pos);
279- if (line.match(/^#author\(/, line)) {
280- return line;
281- } else if (line.match(/^#freeze(\W|$)/, line)) {
282- // Found #freeze still in header
291+ /**
292+ * @param {string} statusText
293+ */
294+ function setSearchStatus(statusText) {
295+ var statusList = document.querySelectorAll('._plugin_search2_search_status');
296+ forEach(statusList, function(statusObj) {
297+ statusObj.textContent = statusText;
298+ });
299+ }
300+ /**
301+ * @param {string} msgHTML
302+ */
303+ function setSearchMessage(msgHTML) {
304+ var objList = document.querySelectorAll('._plugin_search2_message');
305+ forEach(objList, function(obj) {
306+ obj.innerHTML = msgHTML;
307+ });
308+ }
309+ function showSecondSearchForm() {
310+ // Show second search form
311+ var div = document.querySelector('._plugin_search2_second_form');
312+ if (div) {
313+ div.style.display = 'block';
314+ }
315+ }
316+ /**
317+ * @param {Element} form
318+ * @type string
319+ */
320+ function getSearchBase(form) {
321+ var f = form || document.querySelector('._plugin_search2_form');
322+ var base = '';
323+ forEach(f.querySelectorAll('input[name="base"]'), function(radio) {
324+ if (radio.checked) base = radio.value;
325+ });
326+ return base;
327+ }
328+ /**
329+ * Decorate found block (for pre innerHTML)
330+ *
331+ * @param {Object} block
332+ * @param {RegExp} searchRegex
333+ */
334+ function decorateFoundBlock(block, searchRegex) {
335+ var lines = [];
336+ for (var j = 0; j < block.lines.length; j++) {
337+ var line = block.lines[j];
338+ var decorated = findAndDecorateText(line, searchRegex);
339+ if (decorated === null) {
340+ lines.push('' + (block.startIndex + j + 1) + ':\t' + escapeHTML(line));
283341 } else {
284- // other line, #author not found
285- return null;
342+ lines.push('' + (block.startIndex + j + 1) + ':\t' + decorated);
286343 }
287- start = pos + 1;
288344 }
289- return null;
290- }
291- function getUpdateTimeFromAuthorInfo(authorInfo) {
292- var m = authorInfo.match(/^#author\("([^;"]+)(;[^;"]+)?/);
293- if (m) {
294- return m[1];
345+ if (block.beyondLimit) {
346+ lines.push('...');
295347 }
296- return '';
348+ return lines.join('\n');
297349 }
298- function getTargetLines(body, searchRegex) {
350+ /**
351+ * @param {string} body
352+ * @param {RegExp} searchRegex
353+ */
354+ function getSummaryInfo(body, searchRegex) {
299355 var lines = body.split('\n');
300- var found = [];
301356 var foundLines = [];
302357 var isInAuthorHeader = true;
303358 var lastFoundLineIndex = -1 - aroundLines;
304359 var lastAddedLineIndex = lastFoundLineIndex;
305360 var blocks = [];
306361 var lineCount = 0;
362+ var currentBlock = null;
307363 for (var index = 0, length = lines.length; index < length; index++) {
308364 var line = lines[index];
309365 if (isInAuthorHeader) {
@@ -318,10 +374,10 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
318374 isInAuthorHeader = false;
319375 }
320376 }
321- var decorated = findAndDecorateText(line, searchRegex);
322- if (decorated === null) {
377+ var match = line.match(searchRegex);
378+ if (!match) {
323379 if (index < lastFoundLineIndex + aroundLines + 1) {
324- foundLines.push('' + (index + 1) + ':\t' + escapeHTML(lines[index]));
380+ foundLines.push(lines[index]);
325381 lineCount++;
326382 lastAddedLineIndex = index;
327383 }
@@ -334,41 +390,114 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
334390 foundLineIndex: index,
335391 lines: []
336392 };
393+ currentBlock = block;
337394 foundLines = block.lines;
338395 blocks.push(block);
339396 }
340397 if (lineCount >= maxResultLines) {
341- foundLines.push('...');
398+ currentBlock.beyondLimit = true;
342399 return blocks;
343400 }
344401 for (var i = startIndex; i < index; i++) {
345- foundLines.push('' + (i + 1) + ':\t' + escapeHTML(lines[i]));
402+ foundLines.push(lines[i]);
346403 lineCount++;
347404 }
348- foundLines.push('' + (index + 1) + ':\t' + decorated);
405+ foundLines.push(line);
349406 lineCount++;
350407 lastFoundLineIndex = lastAddedLineIndex = index;
351408 }
352409 }
353410 return blocks;
354411 }
355- function getSummary(bodyText, searchRegex) {
356- return getTargetLines(bodyText, searchRegex);
412+ /**
413+ * @param {Date} now
414+ * @param {string} dateText
415+ */
416+ function getPassage(now, dateText) {
417+ if (!dateText) {
418+ return '';
419+ }
420+ var units = [{u: 'm', max: 60}, {u: 'h', max: 24}, {u: 'd', max: 1}];
421+ var d = new Date();
422+ d.setTime(Date.parse(dateText));
423+ var t = (now.getTime() - d.getTime()) / (1000 * 60); // minutes
424+ var unit = units[0].u; var card = units[0].max;
425+ for (var i = 0; i < units.length; i++) {
426+ unit = units[i].u; card = units[i].max;
427+ if (t < card) break;
428+ t = t / card;
429+ }
430+ return '(' + Math.floor(t) + unit + ')';
357431 }
358- function hookSearch2(e) {
359- var form = document.querySelector('form');
360- if (form && form.q) {
361- var q = form.q;
362- if (q.value === '') {
363- q.focus();
432+ /**
433+ * @param {string} searchText
434+ */
435+ function removeSearchOperators(searchText) {
436+ var sp = searchText.split(/\s+/);
437+ if (sp.length <= 1) {
438+ return searchText;
439+ }
440+ for (var i = sp.length - 2; i >= 1; i--) {
441+ if (sp[i] === 'OR') {
442+ sp.splice(i, 1);
364443 }
365444 }
445+ return sp.join(' ');
446+ }
447+ /**
448+ * @param {string} pathname
449+ */
450+ function getSearchCacheKeyBase(pathname) {
451+ return 'path.' + pathname + '.search2.';
452+ }
453+ /**
454+ * @param {string} pathname
455+ */
456+ function getSearchCacheKeyDateBase(pathname) {
457+ var now = new Date();
458+ var dateKey = now.getFullYear() + '_0' + (now.getMonth() + 1) + '_0' + now.getDate();
459+ dateKey = dateKey.replace(/_\d?(\d\d)/g, '$1');
460+ return getSearchCacheKeyBase(pathname) + dateKey + '.';
461+ }
462+ /**
463+ * @param {string} pathname
464+ * @param {string} searchText
465+ * @param {number} offset
466+ */
467+ function getSearchCacheKey(pathname, searchText, offset) {
468+ return getSearchCacheKeyDateBase(pathname) + 'offset=' + offset +
469+ '.' + searchText;
470+ }
471+ /**
472+ * @param {string} pathname
473+ * @param {string} searchText
474+ */
475+ function clearSingleCache(pathname, searchText) {
476+ if (!window.localStorage) return;
477+ var removeTargets = [];
478+ var keyBase = getSearchCacheKeyDateBase(pathname);
479+ for (var i = 0, n = localStorage.length; i < n; i++) {
480+ var key = localStorage.key(i);
481+ if (key.substr(0, keyBase.length) === keyBase) {
482+ // Search result Cache
483+ var subKey = key.substr(keyBase.length);
484+ var m = subKey.match(/^offset=\d+\.(.+)$/);
485+ if (m && m[1] === searchText) {
486+ removeTargets.push(key);
487+ }
488+ }
489+ }
490+ removeTargets.forEach(function(target) {
491+ localStorage.removeItem(target);
492+ });
366493 }
494+ /**
495+ * @param {string} body
496+ */
367497 function getBodySummary(body) {
368498 var lines = body.split('\n');
369499 var isInAuthorHeader = true;
370500 var summary = [];
371- var lineCount = 0;
372501 for (var index = 0, length = lines.length; index < length; index++) {
373502 var line = lines[index];
374503 if (isInAuthorHeader) {
@@ -389,7 +518,7 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
389518 if (line.match(/^#\w+/)) continue; // Block-type plugin
390519 if (line.match(/^\/\//)) continue; // Comment
391520 if (line.substr(0, 1) === '*') {
392- line = line.replace(/\s*\[\#\w+\]$/, ''); // Remove anchor
521+ line = line.replace(/\s*\[#\w+\]$/, ''); // Remove anchor
393522 }
394523 summary.push(line);
395524 if (summary.length >= 10) {
@@ -398,12 +527,388 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
398527 }
399528 return summary.join(' ').substring(0, 150);
400529 }
530+ /**
531+ * @param {string} q searchText
532+ */
533+ function encodeSearchText(q) {
534+ var sp = q.split(/\s+/);
535+ for (var i = 0; i < sp.length; i++) {
536+ sp[i] = encodeURIComponent(sp[i]);
537+ }
538+ return sp.join('+');
539+ }
540+ /**
541+ * @param {string} q searchText
542+ */
543+ function encodeSearchTextForHash(q) {
544+ var sp = q.split(/\s+/);
545+ return sp.join('+');
546+ }
547+ function getSearchTextInLocationHash() {
548+ var hash = document.location.hash;
549+ if (!hash) return '';
550+ var q = '';
551+ if (hash.substr(0, 3) === '#q=') {
552+ q = hash.substr(3).replace(/\+/g, ' ');
553+ } else {
554+ return '';
555+ }
556+ var decodedQ = decodeURIComponent(q);
557+ if (q !== decodedQ) {
558+ q = decodedQ + ' OR ' + q;
559+ }
560+ return q;
561+ }
562+ function colorSearchTextInBody() {
563+ var searchText = getSearchTextInLocationHash();
564+ if (!searchText) return;
565+ var searchRegex = textToRegex(removeSearchOperators(searchText));
566+ if (!searchRegex) return;
567+ var ignoreTags = ['INPUT', 'TEXTAREA', 'BUTTON',
568+ 'SCRIPT', 'FRAME', 'IFRAME'];
569+ /**
570+ * @param {Element} element
571+ */
572+ function colorSearchText(element) {
573+ var decorated = findAndDecorateText(element.nodeValue, searchRegex);
574+ if (decorated) {
575+ var span = document.createElement('span');
576+ span.innerHTML = decorated;
577+ element.parentNode.replaceChild(span, element);
578+ }
579+ }
580+ /**
581+ * @param {Element} element
582+ */
583+ function walkElement(element) {
584+ var e = element.firstChild;
585+ while (e) {
586+ if (e.nodeType === 3 && e.nodeValue &&
587+ e.nodeValue.length >= 2 && /\S/.test(e.nodeValue)) {
588+ var next = e.nextSibling;
589+ colorSearchText(e, searchRegex);
590+ e = next;
591+ } else {
592+ if (e.nodeType === 1 && ignoreTags.indexOf(e.tagName) === -1) {
593+ walkElement(e);
594+ }
595+ e = e.nextSibling;
596+ }
597+ }
598+ }
599+ var target = document.getElementById('body');
600+ walkElement(target);
601+ }
602+ /**
603+ * @param {Array<Object>} newResults
604+ * @param {Element} ul
605+ */
606+ function removePastResults(newResults, ul) {
607+ var removedCount = 0;
608+ var nodes = ul.childNodes;
609+ for (var i = nodes.length - 1; i >= 0; i--) {
610+ var node = nodes[i];
611+ if (node.tagName !== 'LI' && node.tagName !== 'DIV') continue;
612+ var nodePagename = node.getAttribute('data-pagename');
613+ var isRemoveTarget = false;
614+ for (var j = 0, n = newResults.length; j < n; j++) {
615+ var r = newResults[j];
616+ if (r.name === nodePagename) {
617+ isRemoveTarget = true;
618+ break;
619+ }
620+ }
621+ if (isRemoveTarget) {
622+ if (node.tagName === 'LI') {
623+ removedCount++;
624+ }
625+ ul.removeChild(node);
626+ }
627+ }
628+ return removedCount;
629+ }
630+ /**
631+ * @param {Array<Object>} results
632+ * @param {string} searchText
633+ * @param {RegExp} searchRegex
634+ * @param {Element} parentElement
635+ * @param {boolean} insertTop
636+ */
637+ function addSearchResult(results, searchText, searchRegex, parentElement, insertTop) {
638+ var now = new Date();
639+ var parentFragment = document.createDocumentFragment();
640+ results.forEach(function(val) {
641+ var fragment = document.createDocumentFragment();
642+ var li = document.createElement('li');
643+ var hash = '#q=' + encodeSearchTextForHash(searchText);
644+ var href = val.url + hash;
645+ var decoratedName = findAndDecorateText(val.name, searchRegex);
646+ if (!decoratedName) {
647+ decoratedName = escapeHTML(val.name);
648+ }
649+ var updatedAt = val.updatedAt;
650+ var liHtml = '<a href="' + escapeHTML(href) + '">' + decoratedName + '</a> ' +
651+ getPassage(now, updatedAt);
652+ li.innerHTML = liHtml;
653+ li.setAttribute('data-pagename', val.name);
654+ fragment.appendChild(li);
655+ var div = document.createElement('div');
656+ div.classList.add('search-result-detail');
657+ var head = document.createElement('div');
658+ head.classList.add('search-result-page-summary');
659+ head.innerHTML = escapeHTML(val.bodySummary);
660+ div.appendChild(head);
661+ var summaryInfo = val.hitSummary;
662+ for (var i = 0; i < summaryInfo.length; i++) {
663+ var pre = document.createElement('pre');
664+ pre.innerHTML = decorateFoundBlock(summaryInfo[i], searchRegex);
665+ div.appendChild(pre);
666+ }
667+ div.setAttribute('data-pagename', val.name);
668+ fragment.appendChild(div);
669+ parentFragment.appendChild(fragment);
670+ });
671+ if (insertTop && parentElement.firstChild) {
672+ parentElement.insertBefore(parentFragment, parentElement.firstChild);
673+ } else {
674+ parentElement.appendChild(parentFragment);
675+ }
676+ }
677+ /**
678+ * @param {Object} obj
679+ * @param {Object} session
680+ * @param {string} searchText
681+ * @param {number} prevTimestamp
682+ */
683+ function showResult(obj, session, searchText, prevTimestamp) {
684+ var props = getSiteProps();
685+ var searchRegex = textToRegex(removeSearchOperators(searchText));
686+ var ul = document.querySelector('#_plugin_search2_result-list');
687+ if (!ul) return;
688+ if (obj.start_index === 0 && !prevTimestamp) {
689+ ul.innerHTML = '';
690+ }
691+ var searchDone = obj.search_done;
692+ if (!session.scanPageCount) session.scanPageCount = 0;
693+ if (!session.readPageCount) session.readPageCount = 0;
694+ if (!session.hitPageCount) session.hitPageCount = 0;
695+ var prevHitPageCount = session.hitPageCount;
696+ session.hitPageCount += obj.results.length;
697+ if (!prevTimestamp) {
698+ session.scanPageCount += obj.scan_page_count;
699+ session.readPageCount += obj.read_page_count;
700+ session.pageCount = obj.page_count;
701+ }
702+ session.searchStartTime = obj.search_start_time;
703+ session.authUser = obj.auth_user;
704+ if (prevHitPageCount === 0 && session.hitPageCount > 0) {
705+ showSecondSearchForm();
706+ }
707+ var results = obj.results;
708+ var cachedResults = [];
709+ results.forEach(function(val) {
710+ var cache = {};
711+ cache.name = val.name;
712+ cache.url = val.url;
713+ cache.updatedAt = val.updated_at;
714+ cache.updatedTime = val.updated_time;
715+ cache.bodySummary = getBodySummary(val.body);
716+ cache.hitSummary = getSummaryInfo(val.body, searchRegex);
717+ cachedResults.push(cache);
718+ });
719+ if (prevTimestamp) {
720+ var removedCount = removePastResults(cachedResults, ul);
721+ session.hitPageCount -= removedCount;
722+ }
723+ var msg = getSearchResultMessage(session, searchText, searchRegex, !searchDone);
724+ setSearchMessage(msg);
725+ if (prevTimestamp) {
726+ setSearchStatus(searchProps.searchingMsg);
727+ } else {
728+ setSearchStatus(searchProps.searchingMsg + ' ' +
729+ getSearchProgress(session));
730+ }
731+ if (searchDone) {
732+ var singlePageResult = session.offset === 0 && !session.nextOffset;
733+ var progress = getSearchProgress(session);
734+ setTimeout(function() {
735+ if (singlePageResult) {
736+ setSearchStatus('');
737+ } else {
738+ setSearchStatus(searchProps.showingResultMsg + ' ' + progress);
739+ }
740+ }, 2000);
741+ }
742+ if (session.results) {
743+ if (prevTimestamp) {
744+ var newResult = [].concat(cachedResults);
745+ Array.prototype.push.apply(newResult, session.results);
746+ session.results = newResult;
747+ } else {
748+ Array.prototype.push.apply(session.results, cachedResults);
749+ }
750+ } else {
751+ session.results = cachedResults;
752+ }
753+ addSearchResult(cachedResults, searchText, searchRegex, ul, prevTimestamp);
754+ var maxResults = searchProps.maxResults;
755+ if (searchDone) {
756+ session.searchText = searchText;
757+ var prevOffset = searchProps.prevOffset;
758+ if (prevOffset) {
759+ session.prevOffset = parseInt(prevOffset, 10);
760+ }
761+ var json = JSON.stringify(session);
762+ var cacheKey = getSearchCacheKey(props.base_uri_pathname, searchText, session.offset);
763+ if (window.localStorage) {
764+ localStorage[cacheKey] = json;
765+ }
766+ if ('prevOffset' in session || 'nextOffset' in session) {
767+ setSearchMessage(msg + ' ' + getOffsetLinks(session, maxResults));
768+ }
769+ }
770+ if (!searchDone && obj.next_start_index) {
771+ if (session.results.length >= maxResults) {
772+ // Save results
773+ session.nextOffset = obj.next_start_index;
774+ var prevOffset2 = searchProps.prevOffset;
775+ if (prevOffset2) {
776+ session.prevOffset = parseInt(prevOffset2, 10);
777+ }
778+ var key = getSearchCacheKey(props.base_uri_pathname, searchText, session.offset);
779+ localStorage[key] = JSON.stringify(session);
780+ // Stop API calling
781+ setSearchMessage(msg + ' ' + getOffsetLinks(session, maxResults));
782+ setSearchStatus(searchProps.showingResultMsg + ' ' +
783+ getSearchProgress(session));
784+ } else {
785+ setTimeout(function() {
786+ doSearch(searchText, // eslint-disable-line no-use-before-define
787+ session, obj.next_start_index,
788+ obj.search_start_time);
789+ }, searchProps.searchInterval);
790+ }
791+ }
792+ }
793+ /**
794+ * @param {string} searchText
795+ * @param {string} base
796+ * @param {number} offset
797+ */
798+ function showCachedResult(searchText, base, offset) {
799+ var props = getSiteProps();
800+ var searchRegex = textToRegex(removeSearchOperators(searchText));
801+ var ul = document.querySelector('#_plugin_search2_result-list');
802+ if (!ul) return null;
803+ var searchCacheKey = getSearchCacheKey(props.base_uri_pathname, searchText, offset);
804+ var cache1 = localStorage[searchCacheKey];
805+ if (!cache1) {
806+ return null;
807+ }
808+ var session = JSON.parse(cache1);
809+ if (!session) return null;
810+ if (base !== session.base) {
811+ return null;
812+ }
813+ var user = searchProps.user;
814+ if (user !== session.authUser) {
815+ return null;
816+ }
817+ if (session.hitPageCount > 0) {
818+ showSecondSearchForm();
819+ }
820+ var msg = getSearchResultMessage(session, searchText, searchRegex, false);
821+ setSearchMessage(msg);
822+ addSearchResult(session.results, searchText, searchRegex, ul);
823+ var maxResults = searchProps.maxResults;
824+ if ('prevOffset' in session || 'nextOffset' in session) {
825+ var moreResultHtml = getOffsetLinks(session, maxResults);
826+ setSearchMessage(msg + ' ' + moreResultHtml);
827+ var progress = getSearchProgress(session);
828+ setSearchStatus(searchProps.showingResultMsg + ' ' + progress);
829+ } else {
830+ setSearchStatus('');
831+ }
832+ return session;
833+ }
834+ function removeCachedResults() {
835+ var props = getSiteProps();
836+ if (!props || !props.base_uri_pathname) return;
837+ var keyPrefix = getSearchCacheKeyDateBase(props.base_uri_pathname);
838+ var keyBase = getSearchCacheKeyBase(props.base_uri_pathname);
839+ var removeTargets = [];
840+ for (var i = 0, n = localStorage.length; i < n; i++) {
841+ var key = localStorage.key(i);
842+ if (key.substr(0, keyBase.length) === keyBase) {
843+ // Search result Cache
844+ if (key.substr(0, keyPrefix.length) !== keyPrefix) {
845+ removeTargets.push(key);
846+ }
847+ }
848+ }
849+ removeTargets.forEach(function(target) {
850+ localStorage.removeItem(target);
851+ });
852+ }
853+ /**
854+ * @param {string} searchText
855+ * @param {object} session
856+ * @param {number} startIndex
857+ * @param {number} searchStartTime
858+ * @param {number} prevTimestamp
859+ */
860+ function doSearch(searchText, session, startIndex, searchStartTime, prevTimestamp) {
861+ var url = './?cmd=search2&action=query';
862+ url += '&encode_hint=' + encodeURIComponent('\u3077');
863+ if (searchText) {
864+ url += '&q=' + encodeURIComponent(searchText);
865+ }
866+ if (session.base) {
867+ url += '&base=' + encodeURIComponent(session.base);
868+ }
869+ if (prevTimestamp) {
870+ url += '&modified_since=' + prevTimestamp;
871+ } else {
872+ url += '&start=' + startIndex;
873+ if (searchStartTime) {
874+ url += '&search_start_time=' + encodeURIComponent(searchStartTime);
875+ }
876+ if (!('offset' in session)) {
877+ session.offset = startIndex;
878+ }
879+ }
880+ fetch(url, {credentials: 'same-origin'}
881+ ).then(function(response) {
882+ if (response.ok) {
883+ return response.json();
884+ }
885+ throw new Error(response.status + ': ' +
886+ response.statusText + ' on ' + url);
887+ }).then(function(obj) {
888+ showResult(obj, session, searchText, prevTimestamp);
889+ })['catch'](function(err) { // eslint-disable-line dot-notation
890+ if (window.console && console.log) {
891+ console.log(err);
892+ console.log('Error! Please check JavaScript console\n' + JSON.stringify(err) + '|' + err);
893+ }
894+ setSearchStatus(searchProps.errorMsg);
895+ });
896+ }
897+ function hookSearch2() {
898+ var form = document.querySelector('form');
899+ if (form && form.q) {
900+ var q = form.q;
901+ if (q.value === '') {
902+ q.focus();
903+ }
904+ }
905+ }
401906 function removeEncodeHint() {
402907 // Remove 'encode_hint' if site charset is UTF-8
403908 var props = getSiteProps();
404909 if (!props.is_utf8) return;
405910 var forms = document.querySelectorAll('form');
406- forEach(forms, function(form){
911+ forEach(forms, function(form) {
407912 if (form.cmd && form.cmd.value === 'search2') {
408913 if (form.encode_hint && (typeof form.encode_hint.removeAttribute === 'function')) {
409914 form.encode_hint.removeAttribute('name');
@@ -416,44 +921,44 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
416921 var searchText = form && form.q;
417922 if (!searchText) return;
418923 if (searchText && searchText.value) {
419- var e = document.querySelector('#_plugin_search2_msg_searching');
420- var msg = e && e.value || 'Searching...';
421- setSearchStatus(msg);
422- var base = '';
423- forEach(form.querySelectorAll('input[name="base"]'), function(radio){
424- if (radio.checked) base = radio.value;
425- });
426- doSearch(searchText.value, {base: base}, 0);
427- }
428- }
429- function setSearchStatus(statusText) {
430- var statusList = document.querySelectorAll('._plugin_search2_search_status');
431- forEach(statusList, function(statusObj){
432- statusObj.textContent = statusText;
433- });
434- }
435- function setSearchMessage(msgHTML) {
436- var objList = document.querySelectorAll('._plugin_search2_message');
437- forEach(objList, function(obj){
438- obj.innerHTML = msgHTML;
439- });
440- }
441- function forEach(nodeList, func) {
442- if (nodeList.forEach) {
443- nodeList.forEach(func);
444- } else {
445- for (var i = 0, n = nodeList.length; i < n; i++) {
446- func(nodeList[i], i);
924+ var offset = searchProps.offset;
925+ var base = getSearchBase(form);
926+ var prevSession = showCachedResult(searchText.value, base, offset);
927+ if (prevSession) {
928+ // Display Cache results, then search only modified pages
929+ if (!('offset' in prevSession) || prevSession.offset === 0) {
930+ doSearch(searchText.value, prevSession, offset, null,
931+ prevSession.searchStartTime);
932+ } else {
933+ // Show search results
934+ }
935+ } else {
936+ doSearch(searchText.value, {base: base, offset: offset}, offset, null);
447937 }
938+ removeCachedResults();
448939 }
449940 }
450941 function replaceSearchWithSearch2() {
451- forEach(document.querySelectorAll('form'), function(f){
942+ forEach(document.querySelectorAll('form'), function(f) {
943+ function onAndRadioClick() {
944+ var sp = removeSearchOperators(f.word.value).split(/\s+/);
945+ var newText = sp.join(' ');
946+ if (f.word.value !== newText) {
947+ f.word.value = newText;
948+ }
949+ }
950+ function onOrRadioClick() {
951+ var sp = removeSearchOperators(f.word.value).split(/\s+/);
952+ var newText = sp.join(' OR ');
953+ if (f.word.value !== newText) {
954+ f.word.value = newText;
955+ }
956+ }
452957 if (f.action.match(/cmd=search$/)) {
453958 f.addEventListener('submit', function(e) {
454959 var q = e.target.word.value;
455960 var base = '';
456- forEach(f.querySelectorAll('input[name="base"]'), function(radio){
961+ forEach(f.querySelectorAll('input[name="base"]'), function(radio) {
457962 if (radio.checked) base = radio.value;
458963 });
459964 var props = getSiteProps();
@@ -470,116 +975,31 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
470975 (base ? '&base=' + encodeURIComponent(base) : '');
471976 e.preventDefault();
472977 setTimeout(function() {
473- location.href = url;
978+ window.location.href = url;
474979 }, 1);
475980 return false;
476981 });
477982 var radios = f.querySelectorAll('input[type="radio"][name="type"]');
478- forEach(radios, function(radio){
983+ forEach(radios, function(radio) {
479984 if (radio.value === 'AND') {
480985 radio.addEventListener('click', onAndRadioClick);
481986 } else if (radio.value === 'OR') {
482987 radio.addEventListener('click', onOrRadioClick);
483988 }
484989 });
485- function onAndRadioClick(e) {
486- var sp = removeSearchOperators(f.word.value).split(/\s+/);
487- var newText = sp.join(' ');
488- if (f.word.value !== newText) {
489- f.word.value = newText;
490- }
491- }
492- function onOrRadioClick(e) {
493- var sp = removeSearchOperators(f.word.value).split(/\s+/);
494- var newText = sp.join(' OR ');
495- if (f.word.value !== newText) {
496- f.word.value = newText;
990+ } else if (f.cmd && f.cmd.value === 'search2') {
991+ f.addEventListener('submit', function() {
992+ var newSearchText = f.q.value;
993+ var prevSearchText = f.q.getAttribute('data-original-q');
994+ if (newSearchText === prevSearchText) {
995+ // Clear resultCache to search same text again
996+ var props = getSiteProps();
997+ clearSingleCache(props.base_uri_pathname, prevSearchText);
497998 }
498- }
999+ });
4991000 }
5001001 });
5011002 }
502- function encodeSearchText(q) {
503- var sp = q.split(/\s+/);
504- for (var i = 0; i < sp.length; i++) {
505- sp[i] = encodeURIComponent(sp[i]);
506- }
507- return sp.join('+');
508- }
509- function encodeSearchTextForHash(q) {
510- var sp = q.split(/\s+/);
511- return sp.join('+');
512- }
513- function findAndDecorateText(text, searchRegex) {
514- var isReplaced = false;
515- var lastIndex = 0;
516- var m;
517- var decorated = '';
518- searchRegex.lastIndex = 0;
519- while ((m = searchRegex.exec(text)) !== null) {
520- isReplaced = true;
521- var pre = text.substring(lastIndex, m.index);
522- decorated += escapeHTML(pre);
523- for (var i = 1; i < m.length; i++) {
524- if (m[i]) {
525- decorated += '<strong class="word' + (i - 1) + '">' + escapeHTML(m[i]) + '</strong>'
526- }
527- }
528- lastIndex = searchRegex.lastIndex;
529- }
530- if (isReplaced) {
531- decorated += escapeHTML(text.substr(lastIndex));
532- return decorated;
533- }
534- return null;
535- }
536- function getSearchTextInLocationHash() {
537- // TODO Cross browser
538- var hash = location.hash;
539- if (!hash) return '';
540- var q = '';
541- if (hash.substr(0, 3) === '#q=') {
542- q = hash.substr(3).replace(/\+/g, ' ');
543- } else if (hash.substr(0, 6) === '#encq=') {
544- q = decodeURIComponent(hash.substr(6).replace(/\+/g, ' '));
545- }
546- return q;
547- }
548- function colorSearchTextInBody() {
549- var searchText = getSearchTextInLocationHash();
550- if (!searchText) return;
551- var searchRegex = textToRegex(removeSearchOperators(searchText));
552- var headReText = '([\\s\\b]|^)';
553- var tailReText = '\\b';
554- var ignoreTags = ['INPUT', 'TEXTAREA', 'BUTTON',
555- 'SCRIPT', 'FRAME', 'IFRAME'];
556- function colorSearchText(element, searchRegex) {
557- var decorated = findAndDecorateText(element.nodeValue, searchRegex);
558- if (decorated) {
559- var span = document.createElement('span');
560- span.innerHTML = decorated;
561- element.parentNode.replaceChild(span, element);
562- }
563- }
564- function walkElement(element) {
565- var e = element.firstChild;
566- while (e) {
567- if (e.nodeType == 3 && e.nodeValue &&
568- e.nodeValue.length >= 2 && /\S/.test(e.nodeValue)) {
569- var next = e.nextSibling;
570- colorSearchText(e, searchRegex);
571- e = next;
572- } else {
573- if (e.nodeType == 1 && ignoreTags.indexOf(e.tagName) == -1) {
574- walkElement(e);
575- }
576- e = e.nextSibling;
577- }
578- }
579- }
580- var target = document.getElementById('body');
581- walkElement(target);
582- }
5831003 function showNoSupportMessage() {
5841004 var pList = document.getElementsByClassName('_plugin_search2_nosupport_message');
5851005 for (var i = 0; i < pList.length; i++) {
@@ -598,21 +1018,13 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
5981018 if (props.json_enabled) return true;
5991019 return false;
6001020 }
601- function getSiteProps() {
602- var empty = {};
603- var propsDiv = document.getElementById('pukiwiki-site-properties');
604- if (!propsDiv) return empty;
605- var jsonE = propsDiv.querySelector('div[data-key="site-props"]');
606- if (!jsonE) return emptry;
607- var props = JSON.parse(jsonE.getAttribute('data-value'));
608- return props || empty;
609- }
1021+ prepareSearchProps();
6101022 colorSearchTextInBody();
611- if (! isEnabledFetchFunctions()) {
1023+ if (!isEnabledFetchFunctions()) {
6121024 showNoSupportMessage();
6131025 return;
6141026 }
615- if (! isEnableServerFunctions()) return;
1027+ if (!isEnableServerFunctions()) return;
6161028 replaceSearchWithSearch2();
6171029 hookSearch2();
6181030 removeEncodeHint();
--- a/skin/tdiary.css
+++ b/skin/tdiary.css
@@ -519,17 +519,20 @@ tr.bugtrack_state_undef td {
519519
520520 /* search2.inc.php */
521521 input#_plugin_search2_detail:checked ~ ul > div.search-result-detail {
522- display: block;
522+ display: block;
523523 }
524524 input#_plugin_search2_detail ~ ul > div.search-result-detail {
525- display: none;
525+ display: none;
526+}
527+._plugin_search2_search_status {
528+ min-height: 1.5em;
526529 }
527530 .search-result-page-summary {
528- font-size: 70%;
529- color: gray;
530- overflow: hidden;
531- text-overflow: ellipsis;
532- white-space: nowrap;
531+ font-size: 70%;
532+ color: gray;
533+ overflow: hidden;
534+ text-overflow: ellipsis;
535+ white-space: nowrap;
533536 }
534537
535538 @media print {