リビジョン | 1309e51ee63316d75c3a4bc879012fec19875034 (tree) |
---|---|
日時 | 2017-10-01 05:52:20 |
作者 | umorigu <umorigu@gmai...> |
コミッター | umorigu |
BugTrack/2434 Search result cache and offset paging
@@ -47,6 +47,8 @@ $_msg_goto = 'Go to $1.'; | ||
47 | 47 | $_msg_andresult = 'In the page <strong> $2</strong>, <strong> $3</strong> pages that contain all the terms $1 were found.'; |
48 | 48 | $_msg_orresult = 'In the page <strong> $2</strong>, <strong> $3</strong> pages that contain at least one of the terms $1 were found.'; |
49 | 49 | $_msg_notfoundresult = 'No page which contains $1 has been found.'; |
50 | +$_msg_prev_results = '<< Previous $1 pages'; | |
51 | +$_msg_more_results = 'Next $1 pages >>'; | |
50 | 52 | $_msg_symbol = 'Symbols'; |
51 | 53 | $_msg_other = 'Others'; |
52 | 54 | $_msg_help = 'View Text Formatting Rules'; |
@@ -55,6 +57,7 @@ $_msg_content_back_to_top = '<div class="jumpmenu"><a href="#navigator">↑</ | ||
55 | 57 | $_msg_word = 'These search terms have been highlighted:'; |
56 | 58 | $_msg_unsupported_webbrowser = 'This function doesn\'t support your current Web browser.'; |
57 | 59 | $_msg_use_alternative_link = 'Please go to the following link destination: $1'; |
60 | +$_msg_general_error = 'An error occurred while processing.'; | |
58 | 61 | |
59 | 62 | /////////////////////////////////////// |
60 | 63 | // Symbols |
@@ -370,6 +373,7 @@ $_btn_or = 'OR'; | ||
370 | 373 | $_search_pages = 'Search for page starts from $1'; |
371 | 374 | $_search_all = 'Search for all pages'; |
372 | 375 | $_search_searching = 'Searching...'; |
376 | +$_search_showing_result = 'Showing search results'; | |
373 | 377 | $_search_detail = 'Show details'; |
374 | 378 | |
375 | 379 | /////////////////////////////////////// |
@@ -49,6 +49,8 @@ $_msg_goto = '$1 へ行く。'; | ||
49 | 49 | $_msg_andresult = '$1 のすべてを含むページは <strong>$3</strong> ページ中、 <strong>$2</strong> ページ見つかりました。'; |
50 | 50 | $_msg_orresult = '$1 のいずれかを含むページは <strong>$3</strong> ページ中、 <strong>$2</strong> ページ見つかりました。'; |
51 | 51 | $_msg_notfoundresult = '$1 を含むページは見つかりませんでした。'; |
52 | +$_msg_prev_results = '<< 前の $1 ページ'; | |
53 | +$_msg_more_results = '次の $1 ページ >>'; | |
52 | 54 | $_msg_symbol = '記号'; |
53 | 55 | $_msg_other = '日本語'; |
54 | 56 | $_msg_help = 'テキスト整形のルールを表示する'; |
@@ -57,6 +59,7 @@ $_msg_content_back_to_top = '<div class="jumpmenu"><a href="#navigator">↑</ | ||
57 | 59 | $_msg_word = 'これらのキーワードがハイライトされています:'; |
58 | 60 | $_msg_unsupported_webbrowser = 'この機能はお使いのWebブラウザには対応していません。'; |
59 | 61 | $_msg_use_alternative_link = 'リンク先の機能をご利用ください: $1'; |
62 | +$_msg_general_error = '処理中にエラーが発生しました。'; | |
60 | 63 | |
61 | 64 | /////////////////////////////////////// |
62 | 65 | // Symbols |
@@ -372,6 +375,7 @@ $_btn_or = 'OR検索'; | ||
372 | 375 | $_search_pages = '$1 から始まるページを検索'; |
373 | 376 | $_search_all = '全てのページを検索'; |
374 | 377 | $_search_searching = '検索中...'; |
378 | +$_search_showing_result = '検索結果表示'; | |
375 | 379 | $_search_detail = '詳細表示'; |
376 | 380 | |
377 | 381 | /////////////////////////////////////// |
@@ -257,11 +257,26 @@ function get_author_info($wikitext) | ||
257 | 257 | // Found #freeze still in header |
258 | 258 | } else { |
259 | 259 | // other line, #author not found |
260 | - return false; | |
260 | + return null; | |
261 | 261 | } |
262 | 262 | $start = $pos + 1; |
263 | 263 | } |
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; | |
265 | 280 | } |
266 | 281 | |
267 | 282 | function get_date_atom($timestamp) |
@@ -604,6 +619,24 @@ function put_lastmodified() | ||
604 | 619 | } |
605 | 620 | |
606 | 621 | /** |
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 | +/** | |
607 | 640 | * Update RecentChanges page / Invalidate recent.dat |
608 | 641 | */ |
609 | 642 | function delete_recent_changes_cache() { |
@@ -6,12 +6,12 @@ | ||
6 | 6 | // |
7 | 7 | // Search2 plugin - Show detail result using JavaScript |
8 | 8 | |
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) | |
11 | 10 | |
12 | 11 | define('PLUGIN_SEARCH2_RESULT_RECORD_LIMIT', 500); |
13 | 12 | define('PLUGIN_SEARCH2_RESULT_RECORD_LIMIT_START', 100); |
14 | 13 | define('PLUGIN_SEARCH2_SEARCH_WAIT_MILLISECONDS', 1000); |
14 | +define('PLUGIN_SEARCH2_SEARCH_MAX_RESULTS', 1000); | |
15 | 15 | |
16 | 16 | // Show a search box on a page |
17 | 17 | function plugin_search2_convert() |
@@ -22,7 +22,7 @@ function plugin_search2_convert() | ||
22 | 22 | |
23 | 23 | function plugin_search2_action() |
24 | 24 | { |
25 | - global $vars, $_title_search, $_title_result; | |
25 | + global $vars, $_title_search, $_title_result, $_msg_searching; | |
26 | 26 | |
27 | 27 | $action = isset($vars['action']) ? $vars['action'] : ''; |
28 | 28 | $base = isset($vars['base']) ? $vars['base'] : ''; |
@@ -33,28 +33,71 @@ function plugin_search2_action() | ||
33 | 33 | $bases[] = $base; |
34 | 34 | } |
35 | 35 | 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'] : ''; | |
37 | 40 | if ($q === '') { |
38 | 41 | 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)); | |
40 | 44 | } else { |
41 | 45 | $msg = str_replace('$1', htmlsc($q), $_title_result); |
42 | 46 | return array('msg' => $msg, |
43 | - 'body' => plugin_search2_search_form($q, '', $bases)); | |
47 | + 'body' => plugin_search2_search_form($q, $bases, $offset, $prev_offset_s)); | |
44 | 48 | } |
45 | 49 | } 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'); | |
47 | 55 | 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); | |
49 | 58 | exit; |
50 | 59 | } |
51 | 60 | } |
52 | 61 | |
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) | |
54 | 97 | { |
55 | 98 | global $whatsnew, $non_list, $search_non_list; |
56 | 99 | global $_msg_andresult, $_msg_orresult, $_msg_notfoundresult; |
57 | - global $search_auth; | |
100 | + global $search_auth, $auth_user; | |
58 | 101 | |
59 | 102 | $result_record_limit = $start_index === 0 ? |
60 | 103 | 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) | ||
62 | 105 | |
63 | 106 | $b_type_and = true; // AND:TRUE OR:FALSE |
64 | 107 | $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--) { | |
66 | 109 | if ($key_candidates[$i] === 'OR') { |
67 | 110 | $b_type_and = false; |
68 | 111 | unset($key_candidates[$i]); |
@@ -73,20 +116,41 @@ function plugin_search2_do_search($query_text, $base, $start_index) | ||
73 | 116 | foreach ($keys as $key=>$value) |
74 | 117 | $keys[$key] = '/' . $value . '/S'; |
75 | 118 | |
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(); | |
77 | 136 | |
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); | |
81 | 147 | } |
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; | |
84 | 153 | } |
85 | - natsort($pages); | |
86 | - $pages = array_flip($pages); | |
87 | - unset($pages[$whatsnew]); | |
88 | - $page_names = array_keys($pages); | |
89 | - | |
90 | 154 | $found_pages = array(); |
91 | 155 | $readable_page_index = -1; |
92 | 156 | $scan_page_index = -1; |
@@ -112,28 +176,38 @@ function plugin_search2_do_search($query_text, $base, $start_index) | ||
112 | 176 | if ($saved_scan_start_index === -1) { |
113 | 177 | $saved_scan_start_index = $scan_page_index; |
114 | 178 | } |
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. | |
121 | 190 | } |
122 | 191 | if ($b_match) { |
123 | 192 | // Found! |
124 | - $filemtime = null; | |
125 | 193 | $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); | |
128 | 200 | } |
129 | 201 | if ($pagename_only) { |
130 | 202 | // The user cannot read this page body |
131 | 203 | $found_pages[] = array('name' => (string)$page, |
132 | 204 | 'url' => get_page_uri($page), 'updated_at' => $updated_at, |
205 | + 'updated_time' => $updated_time, | |
133 | 206 | 'body' => '', 'pagename_only' => 1); |
134 | 207 | } else { |
135 | 208 | $found_pages[] = array('name' => (string)$page, |
136 | 209 | 'url' => get_page_uri($page), 'updated_at' => $updated_at, |
210 | + 'updated_time' => $updated_time, | |
137 | 211 | 'body' => (string)$body); |
138 | 212 | } |
139 | 213 | } |
@@ -157,6 +231,8 @@ function plugin_search2_do_search($query_text, $base, $start_index) | ||
157 | 231 | 'last_read_page_name' => $last_read_page_name, |
158 | 232 | 'next_start_index' => $readable_page_index + 1, |
159 | 233 | 'search_done' => $search_done, |
234 | + 'search_start_time' => $search_start_time, | |
235 | + 'auth_user' => $auth_user, | |
160 | 236 | 'results' => $found_pages); |
161 | 237 | $obj = $result_obj; |
162 | 238 | if (!defined('PKWK_UTF8_ENABLE')) { |
@@ -169,16 +245,21 @@ function plugin_search2_do_search($query_text, $base, $start_index) | ||
169 | 245 | print(json_encode($obj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); |
170 | 246 | } |
171 | 247 | |
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) | |
173 | 250 | { |
174 | 251 | global $_btn_search; |
175 | 252 | global $_search_pages, $_search_all; |
176 | 253 | global $_msg_andresult, $_msg_orresult, $_msg_notfoundresult; |
177 | - global $_search_detail, $_search_searching; | |
254 | + global $_search_detail, $_search_searching, $_search_showing_result; | |
178 | 255 | global $_msg_unsupported_webbrowser, $_msg_use_alternative_link; |
256 | + global $_msg_more_results, $_msg_prev_results, $_msg_general_error; | |
257 | + global $auth_user; | |
179 | 258 | |
259 | + static $search2_form_total_count = 0; | |
260 | + $search2_form_total_count++; | |
180 | 261 | $script = get_base_uri(); |
181 | - $h_search_text = htmlsc($s_word); | |
262 | + $h_search_text = htmlsc($search_text); | |
182 | 263 | |
183 | 264 | $base_option = ''; |
184 | 265 | if (!empty($bases)) { |
@@ -210,12 +291,10 @@ EOD; | ||
210 | 291 | $_search2_search_wait_milliseconds = PLUGIN_SEARCH2_SEARCH_WAIT_MILLISECONDS; |
211 | 292 | $result_page_panel =<<<EOD |
212 | 293 | <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> | |
217 | 296 | EOD; |
218 | - if ($h_search_text == '') { | |
297 | + if ($h_search_text == '' || $search2_form_total_count > 1) { | |
219 | 298 | $result_page_panel = ''; |
220 | 299 | } |
221 | 300 |
@@ -225,7 +304,7 @@ EOD; | ||
225 | 304 | <form action="$script" method="GET" class="_plugin_search2_form"> |
226 | 305 | <div> |
227 | 306 | <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"> | |
229 | 308 | <input type="submit" value="$_btn_search"> |
230 | 309 | </div> |
231 | 310 | $base_option |
@@ -239,6 +318,32 @@ $form | ||
239 | 318 | </div> |
240 | 319 | EOD; |
241 | 320 | |
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 | + } | |
242 | 347 | |
243 | 348 | return <<<EOD |
244 | 349 | <noscript> |
@@ -247,12 +352,11 @@ EOD; | ||
247 | 352 | <p class="_plugin_search2_nosupport_message" style="display:none;"> |
248 | 353 | $_msg_unsupported_webbrowser $alt_msg |
249 | 354 | </p> |
355 | +$search_props | |
250 | 356 | $form |
251 | 357 | <div class="_plugin_search2_search_status"></div> |
252 | 358 | <div class="_plugin_search2_message"></div> |
253 | 359 | $result_page_panel |
254 | -<ul id="result-list"> | |
255 | -</ul> | |
256 | 360 | $second_form |
257 | 361 | EOD; |
258 | 362 | } |
@@ -643,17 +643,20 @@ tr.bugtrack_state_undef td { | ||
643 | 643 | |
644 | 644 | /* search2.inc.php */ |
645 | 645 | input#_plugin_search2_detail:checked ~ ul > div.search-result-detail { |
646 | - display: block; | |
646 | + display: block; | |
647 | 647 | } |
648 | 648 | 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; | |
650 | 653 | } |
651 | 654 | .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; | |
657 | 660 | } |
658 | 661 | |
659 | 662 | @media print { |
@@ -5,16 +5,23 @@ | ||
5 | 5 | // License: GPL v2 or (at your option) any later version |
6 | 6 | // |
7 | 7 | // 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'; | |
9 | 10 | function enableSearch2() { |
10 | 11 | var aroundLines = 2; |
11 | 12 | var maxResultLines = 20; |
12 | - var minBlockLines = 5; | |
13 | - var minSearchWaitMilliseconds = 100; | |
13 | + var defaultSearchWaitMilliseconds = 100; | |
14 | + var defaultMaxResults = 1000; | |
14 | 15 | 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; | |
18 | 25 | } |
19 | 26 | return s.replace(/[&"<>]/g, function(m) { |
20 | 27 | return { |
@@ -25,179 +32,184 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function( | ||
25 | 32 | }[m]; |
26 | 33 | }); |
27 | 34 | } |
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; | |
37 | 52 | } |
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; | |
46 | 66 | } |
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; | |
53 | 71 | } |
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; | |
58 | 96 | } |
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; | |
61 | 105 | } |
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 | + } | |
75 | 117 | } |
76 | - return '(' + Math.floor(t) + unit + ')'; | |
77 | 118 | } |
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 | + } | |
88 | 143 | } |
144 | + lastIndex = searchRegex.lastIndex; | |
89 | 145 | } |
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; | |
98 | 149 | } |
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) { | |
118 | 159 | var searchTextDecorated = findAndDecorateText(searchText, searchRegex); |
119 | 160 | 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; | |
123 | 164 | } |
124 | - msg = messageTemplate.replace(/\$1|\$2|\$3/g, function(m){ | |
165 | + var msg = messageTemplate.replace(/\$1|\$2|\$3/g, function(m) { | |
125 | 166 | 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 | |
129 | 170 | }[m]; |
130 | 171 | }); |
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; | |
141 | 182 | } |
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; | |
196 | 197 | } |
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); | |
200 | 201 | } |
202 | + if ('nextOffset' in session) { | |
203 | + var nextResultUrl = baseUrl + '&offset=' + session.nextOffset + | |
204 | + '&prev_offset=' + session.offset; | |
205 | + var msgMore = searchProps.msgMoreResultsTemplate.replace(/\$1/, maxResults); | |
206 | + var moreResultHtml = '<a href="' + nextResultUrl + '">' + msgMore + '</a>'; | |
207 | + links.push(moreResultHtml); | |
208 | + } | |
209 | + if (links.length > 0) { | |
210 | + return links.join(' '); | |
211 | + } | |
212 | + return ''; | |
201 | 213 | } |
202 | 214 | function prepareKanaMap() { |
203 | 215 | if (kanaMap !== null) return; |
@@ -208,102 +220,146 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function( | ||
208 | 220 | var dakuten = '\uFF9E'; |
209 | 221 | var maru = '\uFF9F'; |
210 | 222 | var map = {}; |
211 | - for (var c = 0xFF61; c <=0xFF9F; c++) { | |
223 | + for (var c = 0xFF61; c <= 0xFF9F; c++) { | |
212 | 224 | var han = String.fromCharCode(c); |
213 | 225 | var zen = han.normalize('NFKC'); |
214 | 226 | map[zen] = han; |
215 | 227 | var hanDaku = han + dakuten; |
216 | 228 | var zenDaku = hanDaku.normalize('NFKC'); |
217 | 229 | if (zenDaku.length === 1) { // +Handaku-ten OK |
218 | - map[zenDaku] = hanDaku; | |
230 | + map[zenDaku] = hanDaku; | |
219 | 231 | } |
220 | 232 | var hanMaru = han + maru; |
221 | 233 | var zenMaru = hanMaru.normalize('NFKC'); |
222 | 234 | if (zenMaru.length === 1) { // +Maru OK |
223 | - map[zenMaru] = hanMaru; | |
235 | + map[zenMaru] = hanMaru; | |
224 | 236 | } |
225 | 237 | } |
226 | 238 | kanaMap = map; |
227 | 239 | } |
240 | + /** | |
241 | + * @param {searchText} searchText | |
242 | + * @type RegExp | |
243 | + */ | |
228 | 244 | function textToRegex(searchText) { |
229 | 245 | if (!searchText) return null; |
230 | - var regEscape = /[\\^$.*+?()[\]{}|]/g; | |
231 | 246 | // 1:Symbol 2:Katakana 3:Hiragana |
232 | 247 | 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 | + }; | |
233 | 273 | var s1 = searchText.replace(/^\s+|\s+$/g, ''); |
274 | + if (!s1) return null; | |
234 | 275 | var sp = s1.split(/\s+/); |
235 | 276 | var rText = ''; |
236 | 277 | prepareKanaMap(); |
237 | 278 | for (var i = 0; i < sp.length; i++) { |
238 | 279 | if (rText !== '') { |
239 | - rText += '|' | |
280 | + rText += '|'; | |
240 | 281 | } |
241 | 282 | var s = sp[i]; |
242 | 283 | if (s.normalize) { |
243 | 284 | s = s.normalize('NFKC'); |
244 | 285 | } |
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); | |
270 | 287 | rText += '(' + s2 + ')'; |
271 | 288 | } |
272 | 289 | return new RegExp(rText, 'ig'); |
273 | 290 | } |
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)); | |
283 | 341 | } else { |
284 | - // other line, #author not found | |
285 | - return null; | |
342 | + lines.push('' + (block.startIndex + j + 1) + ':\t' + decorated); | |
286 | 343 | } |
287 | - start = pos + 1; | |
288 | 344 | } |
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('...'); | |
295 | 347 | } |
296 | - return ''; | |
348 | + return lines.join('\n'); | |
297 | 349 | } |
298 | - function getTargetLines(body, searchRegex) { | |
350 | + /** | |
351 | + * @param {string} body | |
352 | + * @param {RegExp} searchRegex | |
353 | + */ | |
354 | + function getSummaryInfo(body, searchRegex) { | |
299 | 355 | var lines = body.split('\n'); |
300 | - var found = []; | |
301 | 356 | var foundLines = []; |
302 | 357 | var isInAuthorHeader = true; |
303 | 358 | var lastFoundLineIndex = -1 - aroundLines; |
304 | 359 | var lastAddedLineIndex = lastFoundLineIndex; |
305 | 360 | var blocks = []; |
306 | 361 | var lineCount = 0; |
362 | + var currentBlock = null; | |
307 | 363 | for (var index = 0, length = lines.length; index < length; index++) { |
308 | 364 | var line = lines[index]; |
309 | 365 | if (isInAuthorHeader) { |
@@ -318,10 +374,10 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function( | ||
318 | 374 | isInAuthorHeader = false; |
319 | 375 | } |
320 | 376 | } |
321 | - var decorated = findAndDecorateText(line, searchRegex); | |
322 | - if (decorated === null) { | |
377 | + var match = line.match(searchRegex); | |
378 | + if (!match) { | |
323 | 379 | if (index < lastFoundLineIndex + aroundLines + 1) { |
324 | - foundLines.push('' + (index + 1) + ':\t' + escapeHTML(lines[index])); | |
380 | + foundLines.push(lines[index]); | |
325 | 381 | lineCount++; |
326 | 382 | lastAddedLineIndex = index; |
327 | 383 | } |
@@ -334,41 +390,114 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function( | ||
334 | 390 | foundLineIndex: index, |
335 | 391 | lines: [] |
336 | 392 | }; |
393 | + currentBlock = block; | |
337 | 394 | foundLines = block.lines; |
338 | 395 | blocks.push(block); |
339 | 396 | } |
340 | 397 | if (lineCount >= maxResultLines) { |
341 | - foundLines.push('...'); | |
398 | + currentBlock.beyondLimit = true; | |
342 | 399 | return blocks; |
343 | 400 | } |
344 | 401 | for (var i = startIndex; i < index; i++) { |
345 | - foundLines.push('' + (i + 1) + ':\t' + escapeHTML(lines[i])); | |
402 | + foundLines.push(lines[i]); | |
346 | 403 | lineCount++; |
347 | 404 | } |
348 | - foundLines.push('' + (index + 1) + ':\t' + decorated); | |
405 | + foundLines.push(line); | |
349 | 406 | lineCount++; |
350 | 407 | lastFoundLineIndex = lastAddedLineIndex = index; |
351 | 408 | } |
352 | 409 | } |
353 | 410 | return blocks; |
354 | 411 | } |
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 + ')'; | |
357 | 431 | } |
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); | |
364 | 443 | } |
365 | 444 | } |
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 | + }); | |
366 | 493 | } |
494 | + /** | |
495 | + * @param {string} body | |
496 | + */ | |
367 | 497 | function getBodySummary(body) { |
368 | 498 | var lines = body.split('\n'); |
369 | 499 | var isInAuthorHeader = true; |
370 | 500 | var summary = []; |
371 | - var lineCount = 0; | |
372 | 501 | for (var index = 0, length = lines.length; index < length; index++) { |
373 | 502 | var line = lines[index]; |
374 | 503 | if (isInAuthorHeader) { |
@@ -389,7 +518,7 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function( | ||
389 | 518 | if (line.match(/^#\w+/)) continue; // Block-type plugin |
390 | 519 | if (line.match(/^\/\//)) continue; // Comment |
391 | 520 | if (line.substr(0, 1) === '*') { |
392 | - line = line.replace(/\s*\[\#\w+\]$/, ''); // Remove anchor | |
521 | + line = line.replace(/\s*\[#\w+\]$/, ''); // Remove anchor | |
393 | 522 | } |
394 | 523 | summary.push(line); |
395 | 524 | if (summary.length >= 10) { |
@@ -398,12 +527,388 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function( | ||
398 | 527 | } |
399 | 528 | return summary.join(' ').substring(0, 150); |
400 | 529 | } |
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 | + } | |
401 | 906 | function removeEncodeHint() { |
402 | 907 | // Remove 'encode_hint' if site charset is UTF-8 |
403 | 908 | var props = getSiteProps(); |
404 | 909 | if (!props.is_utf8) return; |
405 | 910 | var forms = document.querySelectorAll('form'); |
406 | - forEach(forms, function(form){ | |
911 | + forEach(forms, function(form) { | |
407 | 912 | if (form.cmd && form.cmd.value === 'search2') { |
408 | 913 | if (form.encode_hint && (typeof form.encode_hint.removeAttribute === 'function')) { |
409 | 914 | form.encode_hint.removeAttribute('name'); |
@@ -416,44 +921,44 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function( | ||
416 | 921 | var searchText = form && form.q; |
417 | 922 | if (!searchText) return; |
418 | 923 | 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); | |
447 | 937 | } |
938 | + removeCachedResults(); | |
448 | 939 | } |
449 | 940 | } |
450 | 941 | 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 | + } | |
452 | 957 | if (f.action.match(/cmd=search$/)) { |
453 | 958 | f.addEventListener('submit', function(e) { |
454 | 959 | var q = e.target.word.value; |
455 | 960 | var base = ''; |
456 | - forEach(f.querySelectorAll('input[name="base"]'), function(radio){ | |
961 | + forEach(f.querySelectorAll('input[name="base"]'), function(radio) { | |
457 | 962 | if (radio.checked) base = radio.value; |
458 | 963 | }); |
459 | 964 | var props = getSiteProps(); |
@@ -470,116 +975,31 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function( | ||
470 | 975 | (base ? '&base=' + encodeURIComponent(base) : ''); |
471 | 976 | e.preventDefault(); |
472 | 977 | setTimeout(function() { |
473 | - location.href = url; | |
978 | + window.location.href = url; | |
474 | 979 | }, 1); |
475 | 980 | return false; |
476 | 981 | }); |
477 | 982 | var radios = f.querySelectorAll('input[type="radio"][name="type"]'); |
478 | - forEach(radios, function(radio){ | |
983 | + forEach(radios, function(radio) { | |
479 | 984 | if (radio.value === 'AND') { |
480 | 985 | radio.addEventListener('click', onAndRadioClick); |
481 | 986 | } else if (radio.value === 'OR') { |
482 | 987 | radio.addEventListener('click', onOrRadioClick); |
483 | 988 | } |
484 | 989 | }); |
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); | |
497 | 998 | } |
498 | - } | |
999 | + }); | |
499 | 1000 | } |
500 | 1001 | }); |
501 | 1002 | } |
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 | - } | |
583 | 1003 | function showNoSupportMessage() { |
584 | 1004 | var pList = document.getElementsByClassName('_plugin_search2_nosupport_message'); |
585 | 1005 | for (var i = 0; i < pList.length; i++) { |
@@ -598,21 +1018,13 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function( | ||
598 | 1018 | if (props.json_enabled) return true; |
599 | 1019 | return false; |
600 | 1020 | } |
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(); | |
610 | 1022 | colorSearchTextInBody(); |
611 | - if (! isEnabledFetchFunctions()) { | |
1023 | + if (!isEnabledFetchFunctions()) { | |
612 | 1024 | showNoSupportMessage(); |
613 | 1025 | return; |
614 | 1026 | } |
615 | - if (! isEnableServerFunctions()) return; | |
1027 | + if (!isEnableServerFunctions()) return; | |
616 | 1028 | replaceSearchWithSearch2(); |
617 | 1029 | hookSearch2(); |
618 | 1030 | removeEncodeHint(); |
@@ -519,17 +519,20 @@ tr.bugtrack_state_undef td { | ||
519 | 519 | |
520 | 520 | /* search2.inc.php */ |
521 | 521 | input#_plugin_search2_detail:checked ~ ul > div.search-result-detail { |
522 | - display: block; | |
522 | + display: block; | |
523 | 523 | } |
524 | 524 | 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; | |
526 | 529 | } |
527 | 530 | .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; | |
533 | 536 | } |
534 | 537 | |
535 | 538 | @media print { |