• R/O
  • HTTP
  • SSH
  • HTTPS

コミット

タグ
未設定

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

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

コミットメタ情報

リビジョン15e6dda2512403bb7738c34bccd906842d0b6a14 (tree)
日時2017-09-30 15:31:10
作者umorigu <umorigu@gmai...>
コミッターumorigu

ログメッセージ

BugTrack/692 Show page contents in search result - search2 plugin

* Show page contents by client-side JavaScript
* Add new "search2" plugin with "skin/search2.js" JavaScript
* Toggle switch to show details or not
* Supoort both UTF-8 and EUC-JP encodings
* OR Search with "OR"-combined keywords (ex: "A OR B")
* Always show passage
* Color search texts in details view
* Color search texts in each text-found page
* Web browser requirement: HTML5 Fetch API (You can use Polyfill library)
* Server requirement: PHP5.4+ (can handle JSON)

変更サマリ

差分

--- a/en.lng.php
+++ b/en.lng.php
@@ -2,7 +2,7 @@
22 // PukiWiki - Yet another WikiWikiWeb clone.
33 // en.lng.php
44 // Copyright
5-// 2002-2016 PukiWiki Development Team
5+// 2002-2017 PukiWiki Development Team
66 // 2001-2002 Originally written by yu-ji
77 // License: GPL v2 or (at your option) any later version
88 //
@@ -53,6 +53,8 @@ $_msg_help = 'View Text Formatting Rules';
5353 $_msg_week = array('Sun','Mon','Tue','Wed','Thu','Fri','Sat');
5454 $_msg_content_back_to_top = '<div class="jumpmenu"><a href="#navigator">&uarr;</a></div>';
5555 $_msg_word = 'These search terms have been highlighted:';
56+$_msg_unsupported_webbrowser = 'This function doesn\'t support your current Web browser.';
57+$_msg_use_alternative_link = 'Please go to the following link destination: $1';
5658
5759 ///////////////////////////////////////
5860 // Symbols
@@ -367,6 +369,8 @@ $_btn_and = 'AND';
367369 $_btn_or = 'OR';
368370 $_search_pages = 'Search for page starts from $1';
369371 $_search_all = 'Search for all pages';
372+$_search_searching = 'Searching...';
373+$_search_detail = 'Show details';
370374
371375 ///////////////////////////////////////
372376 // source.inc.php
--- a/ja.lng.php
+++ b/ja.lng.php
@@ -2,7 +2,7 @@
22 // PukiWiki - Yet another WikiWikiWeb clone.
33 // ja.lng.php
44 // Copyright
5-// 2002-2016 PukiWiki Development Team
5+// 2002-2017 PukiWiki Development Team
66 // 2001-2002 Originally written by yu-ji
77 // License: GPL v2 or (at your option) any later version
88 //
@@ -55,6 +55,8 @@ $_msg_help = 'テキスト整形のルールを表示する';
5555 $_msg_week = array('日','月','火','水','木','金','土');
5656 $_msg_content_back_to_top = '<div class="jumpmenu"><a href="#navigator">&uarr;</a></div>';
5757 $_msg_word = 'これらのキーワードがハイライトされています:';
58+$_msg_unsupported_webbrowser = 'この機能はお使いのWebブラウザには対応していません。';
59+$_msg_use_alternative_link = 'リンク先の機能をご利用ください: $1';
5860
5961 ///////////////////////////////////////
6062 // Symbols
@@ -114,7 +116,7 @@ $_LANG['skin']['rename'] = '名前変更'; // Rename a page (and related)
114116 $_LANG['skin']['rss'] = '最終更新のRSS'; // RSS of RecentChanges
115117 $_LANG['skin']['rss10'] = & $_LANG['skin']['rss'];
116118 $_LANG['skin']['rss20'] = & $_LANG['skin']['rss'];
117-$_LANG['skin']['search'] = '単語検索';
119+$_LANG['skin']['search'] = '検索';
118120 $_LANG['skin']['top'] = 'トップ'; // Top page
119121 $_LANG['skin']['unfreeze'] = '凍結解除';
120122 $_LANG['skin']['upload'] = '添付'; // Attach a file
@@ -361,7 +363,7 @@ $_rename_messages = array(
361363
362364 ///////////////////////////////////////
363365 // search.inc.php
364-$_title_search = '単語検索';
366+$_title_search = '検索';
365367 $_title_result = '$1 の検索結果';
366368 $_msg_searching = '全てのページから単語を検索します。大文字小文字の区別はありません。';
367369 $_btn_search = '検索';
@@ -369,6 +371,8 @@ $_btn_and = 'AND検索';
369371 $_btn_or = 'OR検索';
370372 $_search_pages = '$1 から始まるページを検索';
371373 $_search_all = '全てのページを検索';
374+$_search_searching = '検索中...';
375+$_search_detail = '詳細表示';
372376
373377 ///////////////////////////////////////
374378 // source.inc.php
--- a/lib/file.php
+++ b/lib/file.php
@@ -15,9 +15,16 @@ define('PKWK_MAXSHOW_CACHE', 'recent.dat');
1515 // AutoLink
1616 define('PKWK_AUTOLINK_REGEX_CACHE', 'autolink.dat');
1717
18-// Get source(wiki text) data of the page
19-// Returns FALSE if error occurerd
20-function get_source($page = NULL, $lock = TRUE, $join = FALSE)
18+/**
19+ * Get source(wiki text) data of the page
20+ *
21+ * @param $page page name
22+ * @param $lock lock
23+ * @param $join true: return string, false: return array of string
24+ * @param $raw true: return file content as-is
25+ * @return FALSE if error occurerd
26+ */
27+function get_source($page = NULL, $lock = TRUE, $join = FALSE, $raw = FALSE)
2128 {
2229 //$result = NULL; // File is not found
2330 $result = $join ? '' : array();
@@ -44,6 +51,9 @@ function get_source($page = NULL, $lock = TRUE, $join = FALSE)
4451 } else {
4552 $result = fread($fp, $size);
4653 if ($result !== FALSE) {
54+ if ($raw) {
55+ return $result;
56+ }
4757 // Removing Carriage-Return
4858 $result = str_replace("\r", '', $result);
4959 }
@@ -204,16 +214,54 @@ function remove_author_info($wikitext)
204214 return preg_replace('/^\s*#author\([^\n]*(\n|$)/m', '', $wikitext);
205215 }
206216
207-function remove_author_lines($lines)
217+/**
218+ * Remove author line from wikitext
219+ */
220+function remove_author_header($wikitext)
208221 {
209- $author_head = '#author(';
210- $len = strlen($author_head);
211- for ($i = 0; $i < 5; $i++) {
212- if (substr($lines[$i], 0, $len) === $author_head) {
213- unset($lines[$i]);
222+ $start = 0;
223+ while (($pos = strpos($wikitext, "\n", $start)) != false) {
224+ $line = substr($wikitext, $start, $pos);
225+ $m = null;
226+ if (preg_match('/^#author\(/', $line, $m)) {
227+ // fond #author line, Remove this line only
228+ if ($start === 0) {
229+ return substr($wikitext, $pos + 1);
230+ } else {
231+ return substr($wikitext, 0, $start - 1) .
232+ substr($wikitext, $pos + 1);
233+ }
234+ } else if (preg_match('/^#freeze(\W|$)/', $line, $m)) {
235+ // Found #freeze still in header
236+ } else {
237+ // other line, #author not found
238+ return $wikitext;
239+ }
240+ $start = $pos + 1;
241+ }
242+ return $wikitext;
243+}
244+
245+/**
246+ * Get author info from wikitext
247+ */
248+function get_author_info($wikitext)
249+{
250+ $start = 0;
251+ while (($pos = strpos($wikitext, "\n", $start)) != false) {
252+ $line = substr($wikitext, $start, $pos);
253+ $m = null;
254+ if (preg_match('/^#author\(/', $line, $m)) {
255+ return $line;
256+ } else if (preg_match('/^#freeze(\W|$)/', $line, $m)) {
257+ // Found #freeze still in header
258+ } else {
259+ // other line, #author not found
260+ return false;
214261 }
262+ $start = $pos + 1;
215263 }
216- return $lines;
264+ return false;
217265 }
218266
219267 function get_date_atom($timestamp)
--- a/lib/func.php
+++ b/lib/func.php
@@ -346,8 +346,8 @@ function do_search($word, $type = 'AND', $non_format = FALSE, $base = '')
346346
347347 // Search for page contents
348348 foreach ($keys as $key) {
349- $lines = remove_author_lines(get_source($page, TRUE, FALSE));
350- $b_match = preg_match($key, join('', $lines));
349+ $body = get_source($page, TRUE, TRUE, TRUE);
350+ $b_match = preg_match($key, remove_author_header($body));
351351 if ($b_type xor $b_match) break; // OR
352352 }
353353 if ($b_match) continue;
--- a/lib/html.php
+++ b/lib/html.php
@@ -206,10 +206,28 @@ function get_html_scripting_data()
206206 if (!isset($ticket_link_sites) || !is_array($ticket_link_sites)) {
207207 return '';
208208 }
209+ $is_utf8 = (bool)defined('PKWK_UTF8_ENABLE');
209210 // Require: PHP 5.4+
210- if (!defined('JSON_UNESCAPED_UNICODE')) {
211- return '';
212- };
211+ $json_enabled = defined('JSON_UNESCAPED_UNICODE');
212+ if (!$json_enabled) {
213+ $empty_data = <<<EOS
214+<div id="pukiwiki-site-properties" style="display:none;">
215+</div>
216+EOS;
217+ return $empty_data;
218+ }
219+ // Site basic Properties
220+ $props = array(
221+ 'is_utf8' => $is_utf8,
222+ 'json_enabled' => $json_enabled,
223+ 'base_uri_pathname' => get_base_uri(PKWK_URI_ROOT),
224+ 'base_uri_absolute' => get_base_uri(PKWK_URI_ABSOLUTE)
225+ );
226+ $props_json = htmlsc(json_encode($props, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
227+ $site_props = <<<EOS
228+<div data-key="site-props" data-value="$props_json"></div>
229+EOS;
230+ // AutoTicketLink
213231 $text = '';
214232 foreach ($ticket_link_sites as $s) {
215233 if (!preg_match('/^([a-zA-Z0-9]+)([\.\-][a-zA-Z0-9]+)*$/', $s['key'])) {
@@ -221,11 +239,15 @@ function get_html_scripting_data()
221239 EOS;
222240 $text .= "\n";
223241 }
224- $data = <<<EOS
225-<div id="pukiwiki-site-properties" style="display:none;">
242+ $ticketlink_data = <<<EOS
226243 <div class="ticketlink-def">
227244 $text
228245 </div>
246+EOS;
247+ $data = <<<EOS
248+<div id="pukiwiki-site-properties" style="display:none;">
249+$site_props
250+$ticketlink_data
229251 </div>
230252 EOS;
231253 return $data;
--- a/lib/link.php
+++ b/lib/link.php
@@ -266,8 +266,8 @@ function links_do_search_page($word)
266266 $b_match = FALSE;
267267 // Search for page contents
268268 foreach ($keys as $key) {
269- $lines = remove_author_lines(get_source($page, TRUE, FALSE));
270- $b_match = preg_match($key, join('', $lines));
269+ $body = get_source($page, TRUE, TRUE, TRUE);
270+ $b_match = preg_match($key, remove_author_header($body));
271271 if (! $b_match) break; // OR
272272 }
273273 if ($b_match) continue;
--- /dev/null
+++ b/plugin/search2.inc.php
@@ -0,0 +1,222 @@
1+<?php
2+// PukiWiki - Yet another WikiWikiWeb clone.
3+// search2.inc.php
4+// Copyright 2017 PukiWiki Development Team
5+// License: GPL v2 or (at your option) any later version
6+//
7+// Search2 plugin - Show detail result using JavaScript
8+
9+define('PLUGIN_SEARCH2_MAX_LENGTH', 80);
10+define('PLUGIN_SEARCH2_MAX_BASE', 16); // #search(1,2,3,...,15,16)
11+
12+// Show a search box on a page
13+function plugin_search2_convert()
14+{
15+ $args = func_get_args();
16+ return plugin_search_search_form('', '', $args);
17+}
18+
19+function plugin_search2_action()
20+{
21+ global $vars, $_title_search, $_title_result;
22+
23+ $action = isset($vars['action']) ? $vars['action'] : '';
24+ $base = isset($vars['base']) ? $vars['base'] : '';
25+ $bases = array();
26+ if ($base !== '') {
27+ $bases[] = $base;
28+ }
29+ if ($action === '') {
30+ $q = isset($vars['q']) ? $vars['q'] : '';
31+ if ($q === '') {
32+ return array('msg' => $_title_search,
33+ 'body' => plugin_search2_search_form($q, '', $bases));
34+ } else {
35+ $msg = str_replace('$1', htmlsc($q), $_title_result);
36+ return array('msg' => $msg,
37+ 'body' => plugin_search2_search_form($q, '', $bases));
38+ }
39+ } else if ($action === 'query') {
40+ $text = isset($vars['q']) ? $vars['q'] : '';
41+ header('Content-Type: application/json; charset=UTF-8');
42+ plugin_search2_do_search($text, $base);
43+ exit;
44+ }
45+}
46+
47+function plugin_search2_do_search($query_text, $base)
48+{
49+ global $whatsnew, $non_list, $search_non_list;
50+ global $_msg_andresult, $_msg_orresult, $_msg_notfoundresult;
51+ global $search_auth;
52+
53+ $retval = array();
54+
55+ $b_type_and = true; // AND:TRUE OR:FALSE
56+ $key_candidates = preg_split('/\s+/', $query_text, -1, PREG_SPLIT_NO_EMPTY);
57+ for ($i = count($key_candidates) - 1; $i >= 0; $i--) {
58+ if ($key_candidates[$i] === 'OR') {
59+ $b_type_and = false;
60+ unset($key_candidates[$i]);
61+ }
62+ }
63+ $key_candidates = array_merge($key_candidates);
64+ $keys = get_search_words($key_candidates);
65+ foreach ($keys as $key=>$value)
66+ $keys[$key] = '/' . $value . '/S';
67+
68+ $pages = get_existpages();
69+
70+ // Avoid
71+ if ($base != '') {
72+ $pages = preg_grep('/^' . preg_quote($base, '/') . '/S', $pages);
73+ }
74+ if (! $search_non_list) {
75+ $pages = array_diff($pages, preg_grep('/' . $non_list . '/S', $pages));
76+ }
77+ natsort($pages);
78+ $pages = array_flip($pages);
79+ unset($pages[$whatsnew]);
80+ $page_names = array_keys($pages);
81+
82+ $found_pages = array();
83+ $readable_page_index = -1;
84+ $scan_page_index = -1;
85+ $last_read_page_name = null;
86+ foreach ($page_names as $page) {
87+ $b_match = FALSE;
88+ $pagename_only = false;
89+ $scan_page_index++;
90+ if (! is_page_readable($page)) {
91+ if ($search_auth) {
92+ // $search_auth - 1: User can know page names that contain search text if the page is readable
93+ continue;
94+ }
95+ // $search_auth - 0: All users can know page names that conntain search text
96+ $pagename_only = true;
97+ }
98+ $readable_page_index++;
99+ // Search for page name and contents
100+ $body = get_source($page, TRUE, TRUE, TRUE);
101+ $target = $page . "\n" . remove_author_header($body);
102+ foreach ($keys as $key) {
103+ $b_match = preg_match($key, $target);
104+ if ($b_type_and xor $b_match) break; // OR
105+ }
106+ if ($b_match) {
107+ // Found!
108+ $filemtime = null;
109+ $author_info = get_author_info($body);
110+ if ($author_info === false || $pagename_only) {
111+ $updated_at = get_date_atom(filemtime(get_filename($page)));
112+ }
113+ if ($pagename_only) {
114+ // The user cannot read this page body
115+ $found_pages[] = array('name' => (string)$page,
116+ 'url' => get_page_uri($page), 'updated_at' => $updated_at,
117+ 'body' => '', 'pagename_only' => 1);
118+ } else {
119+ $found_pages[] = array('name' => (string)$page,
120+ 'url' => get_page_uri($page), 'updated_at' => $updated_at,
121+ 'body' => (string)$body);
122+ }
123+ }
124+ $last_read_page_name = $page;
125+ }
126+ $message = str_replace('$1', htmlsc($query_text), str_replace('$2', count($found_pages),
127+ str_replace('$3', count($page_names), $b_type_and ? $_msg_andresult : $_msg_orresult)));
128+ $search_done = (boolean)($scan_page_index + 1 === count($page_names));
129+ $result_obj = array(
130+ 'message' => $message,
131+ 'q' => $query_text,
132+ 'read_page_count' => $readable_page_index + 1,
133+ 'scan_page_count' => $scan_page_index + 1,
134+ 'page_count' => count($page_names),
135+ 'last_read_page_name' => $last_read_page_name,
136+ 'search_done' => $search_done,
137+ 'results' => $found_pages);
138+ $obj = $result_obj;
139+ if (!defined('PKWK_UTF8_ENABLE')) {
140+ if (SOURCE_ENCODING === 'EUC-JP') {
141+ mb_convert_variables('UTF-8', 'CP51932', $obj);
142+ } else {
143+ mb_convert_variables('UTF-8', SOURCE_ENCODING, $obj);
144+ }
145+ }
146+ print(json_encode($obj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
147+}
148+
149+function plugin_search2_search_form($s_word = '', $type = '', $bases = array())
150+{
151+ global $_btn_search;
152+ global $_search_pages, $_search_all;
153+ global $_msg_andresult, $_msg_orresult, $_msg_notfoundresult;
154+ global $_search_detail, $_search_searching;
155+ global $_msg_unsupported_webbrowser, $_msg_use_alternative_link;
156+
157+ $script = get_base_uri();
158+ $h_search_text = htmlsc($s_word);
159+
160+ $base_option = '';
161+ if (!empty($bases)) {
162+ $base_msg = '';
163+ $_num = 0;
164+ $check = ' checked';
165+ foreach($bases as $base) {
166+ ++$_num;
167+ if (PLUGIN_SEARCH2_MAX_BASE < $_num) break;
168+ $s_base = htmlsc($base);
169+ $base_str = '<strong>' . $s_base . '</strong>';
170+ $base_label = str_replace('$1', $base_str, $_search_pages);
171+ $base_msg .=<<<EOD
172+ <div>
173+ <label>
174+ <input type="radio" name="base" value="$s_base" $check> $base_label
175+ </label>
176+ </div>
177+EOD;
178+ $check = '';
179+ }
180+ $base_msg .=<<<EOD
181+<label><input type="radio" name="base" value=""> $_search_all</label>
182+EOD;
183+ $base_option = '<div class="small">' . $base_msg . '</div>';
184+ }
185+ $_search2_result_notfound = htmlsc($_msg_notfoundresult);
186+ $_search2_result_found = htmlsc($_msg_andresult);
187+ $_search2_search_wait_milliseconds = PLUGIN_SEARCH2_SEARCH_WAIT_MILLISECONDS;
188+ $result_page_panel =<<<EOD
189+<div id="_plugin_search2_search_status"></div>
190+<div id="_plugin_search2_message"></div>
191+<input type="checkbox" id="_plugin_search2_detail" checked><label for="_plugin_search2_detail">$_search_detail</label>
192+<input type="hidden" id="_plugin_search2_msg_searching" value="$_search_searching">
193+<input type="hidden" id="_plugin_search2_msg_result_notfound" value="$_search2_result_notfound">
194+<input type="hidden" id="_plugin_search2_msg_result_found" value="$_search2_result_found">
195+<input type="hidden" id="_search2_search_wait_milliseconds" value="$_search2_search_wait_milliseconds">
196+EOD;
197+ if ($h_search_text == '') {
198+ $result_page_panel = '';
199+ }
200+
201+ $plain_search_link = '<a href="' . $script . '?cmd=search' . '">' . htmlsc($_btn_search) . '</a>';
202+ $alt_msg = str_replace('$1', $plain_search_link, $_msg_use_alternative_link);
203+ return <<<EOD
204+<noscript>
205+ <p>$_msg_unsupported_webbrowser $alt_msg</p>
206+</noscript>
207+<p class="_plugin_search2_nosupport_message" style="display:none;">
208+ $_msg_unsupported_webbrowser $alt_msg
209+</p>
210+<form action="$script" method="GET" class="_plugin_search2_form">
211+ <div>
212+ <input type="hidden" name="cmd" value="search2">
213+ <input type="search" name="q" value="$h_search_text" size="30">
214+ <input type="submit" value="$_btn_search">
215+ </div>
216+$base_option
217+</form>
218+$result_page_panel
219+<ul id="result-list">
220+</ul>
221+EOD;
222+}
--- a/skin/main.js
+++ b/skin/main.js
@@ -160,7 +160,7 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
160160 }
161161 var siteNodes = defRoot.querySelectorAll('.ticketlink-site');
162162 Array.prototype.forEach.call(siteNodes, function (e) {
163- var siteInfoText = e.dataset && e.dataset.site;
163+ var siteInfoText = e.getAttribute('data-site');
164164 if (!siteInfoText) return;
165165 var info = textToSiteInfo(siteInfoText);
166166 if (info) {
--- a/skin/pukiwiki.css
+++ b/skin/pukiwiki.css
@@ -641,6 +641,21 @@ tr.bugtrack_state_undef td {
641641 background-color: #ff3333;
642642 }
643643
644+/* search2.inc.php */
645+input#_plugin_search2_detail:checked ~ ul > div.search-result-detail {
646+ display: block;
647+}
648+input#_plugin_search2_detail ~ ul > div.search-result-detail {
649+ display: none;
650+}
651+.search-result-page-summary {
652+ font-size: 70%;
653+ color: gray;
654+ overflow: hidden;
655+ text-overflow: ellipsis;
656+ white-space: nowrap;
657+}
658+
644659 @media print {
645660 a:link,
646661 a:visited {
--- a/skin/pukiwiki.skin.php
+++ b/skin/pukiwiki.skin.php
@@ -69,6 +69,7 @@ header('Content-Type: text/html; charset=' . CONTENT_CHARSET);
6969 <link rel="stylesheet" type="text/css" href="<?php echo SKIN_DIR ?>pukiwiki.css" />
7070 <link rel="alternate" type="application/rss+xml" title="RSS" href="<?php echo $link['rss'] ?>" /><?php // RSS auto-discovery ?>
7171 <script type="text/javascript" src="skin/main.js" defer></script>
72+ <script type="text/javascript" src="skin/search2.js" defer></script>
7273
7374 <?php echo $head_tag ?>
7475 </head>
--- /dev/null
+++ b/skin/search2.js
@@ -0,0 +1,585 @@
1+// PukiWiki - Yet another WikiWikiWeb clone.
2+// search2.js
3+// Copyright
4+// 2017 PukiWiki Development Team
5+// License: GPL v2 or (at your option) any later version
6+//
7+// PukiWiki search2 pluign - JavaScript client script
8+window.addEventListener && window.addEventListener('DOMContentLoaded', function() {
9+ function enableSearch2() {
10+ var aroundLines = 2;
11+ var maxResultLines = 20;
12+ var minBlockLines = 5;
13+ var minSearchWaitMilliseconds = 100;
14+ var kanaMap = null;
15+ function escapeHTML (s) {
16+ if(typeof s !== 'string') {
17+ s = '' + s;
18+ }
19+ return s.replace(/[&"<>]/g, function(m) {
20+ return {
21+ '&': '&amp;',
22+ '"': '&quot;',
23+ '<': '&lt;',
24+ '>': '&gt;'
25+ }[m];
26+ });
27+ }
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);
37+ }
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);
46+ }
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+ });
53+ }
54+ function getMessageTemplate(idText, defaultText) {
55+ var messageHolder = document.querySelector('#' + idText);
56+ var messageTemplate = (messageHolder && messageHolder.value) || defaultText;
57+ return messageTemplate;
58+ }
59+ function getAuthorInfo(text) {
60+
61+ }
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;
75+ }
76+ return '(' + Math.floor(t) + unit + ')';
77+ }
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);
88+ }
89+ }
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+ ul.innerHTML = '';
97+ if (! session.scan_page_count) session.scan_page_count = 0;
98+ if (! session.read_page_count) session.read_page_count = 0;
99+ if (! session.hit_page_count) session.hit_page_count = 0;
100+ session.scan_page_count += obj.scan_page_count;
101+ session.read_page_count += obj.read_page_count;
102+ session.hit_page_count += obj.results.length;
103+ session.page_count = obj.page_count;
104+
105+ var msg = obj.message;
106+ var notFoundMessageTemplate = getMessageTemplate('_plugin_search2_msg_result_notfound',
107+ 'No page which contains $1 has been found.');
108+ var foundMessageTemplate = getMessageTemplate('_plugin_search2_msg_result_found',
109+ 'In the page <strong>$2</strong>, <strong>$3</strong> pages that contain all the terms $1 were found.');
110+ var searchTextDecorated = findAndDecorateText(searchText, searchRegex);
111+ if (searchTextDecorated === null) searchTextDecorated = escapeHTML(searchText);
112+ var messageTemplate = foundMessageTemplate;
113+ if (session.hit_page_count === 0) {
114+ messageTemplate = notFoundMessageTemplate;
115+ }
116+ msg = messageTemplate.replace(/\$1|\$2|\$3/g, function(m){
117+ return {
118+ '$1': searchTextDecorated,
119+ '$2': session.hit_page_count,
120+ '$3': session.read_page_count
121+ }[m];
122+ });
123+ document.querySelector('#_plugin_search2_message').innerHTML = msg;
124+
125+ setSearchStatus('');
126+ var results = obj.results;
127+ var now = new Date();
128+ results.forEach(function(val, index) {
129+ var fragment = document.createDocumentFragment();
130+ var li = document.createElement('li');
131+ var hash = '#q=' + encodeSearchTextForHash(searchText);
132+ var href = val.url + hash;
133+ var decoratedName = findAndDecorateText(val.name, searchRegex);
134+ if (! decoratedName) {
135+ decoratedName = escapeHTML(val.name);
136+ }
137+ var author = getAuthorHeader(val.body);
138+ var updatedAt = '';
139+ if (author) {
140+ updatedAt = getUpdateTimeFromAuthorInfo(author);
141+ } else {
142+ updatedAt = val.updated_at;
143+ }
144+ var liHtml = '<a href="' + escapeHTML(href) + '">' + decoratedName + '</a> ' +
145+ getPassage(now, updatedAt);
146+ li.innerHTML = liHtml;
147+ var a = li.querySelector('a');
148+ if (a && a.hash) {
149+ if (a.hash !== hash) {
150+ // Some browser execute encodeHTML(hash) automatically. Support them.
151+ a.href = val.url + '#encq=' + encodeSearchTextForHash(searchText);
152+ }
153+ }
154+ fragment.appendChild(li);
155+ var div = document.createElement('div');
156+ div.classList.add('search-result-detail');
157+ var head = document.createElement('div');
158+ head.classList.add('search-result-page-summary');
159+ head.innerHTML = escapeHTML(getBodySummary(val.body));
160+ div.appendChild(head);
161+ var summary = getSummary(val.body, searchRegex);
162+ for (var i = 0; i < summary.length; i++) {
163+ var pre = document.createElement('pre');
164+ pre.innerHTML = summary[i].lines.join('\n');
165+ div.appendChild(pre);
166+ }
167+ fragment.appendChild(div);
168+ ul.appendChild(fragment);
169+ });
170+ }
171+ function prepareKanaMap() {
172+ if (kanaMap !== null) return;
173+ if (!String.prototype.normalize) {
174+ kanaMap = {};
175+ return;
176+ }
177+ var dakuten = '\uFF9E';
178+ var maru = '\uFF9F';
179+ var map = {};
180+ for (var c = 0xFF61; c <=0xFF9F; c++) {
181+ var han = String.fromCharCode(c);
182+ var zen = han.normalize('NFKC');
183+ map[zen] = han;
184+ var hanDaku = han + dakuten;
185+ var zenDaku = hanDaku.normalize('NFKC');
186+ if (zenDaku.length === 1) { // +Handaku-ten OK
187+ map[zenDaku] = hanDaku;
188+ }
189+ var hanMaru = han + maru;
190+ var zenMaru = hanMaru.normalize('NFKC');
191+ if (zenMaru.length === 1) { // +Maru OK
192+ map[zenMaru] = hanMaru;
193+ }
194+ }
195+ kanaMap = map;
196+ }
197+ function textToRegex(searchText) {
198+ if (!searchText) return null;
199+ var regEscape = /[\\^$.*+?()[\]{}|]/g;
200+ // 1:Symbol 2:Katakana 3:Hiragana
201+ var regRep = /([\\^$.*+?()[\]{}|])|([\u30a1-\u30f6])|([\u3041-\u3096])/g;
202+ var s1 = searchText.replace(/^\s+|\s+$/g, '');
203+ var sp = s1.split(/\s+/);
204+ var rText = '';
205+ prepareKanaMap();
206+ for (var i = 0; i < sp.length; i++) {
207+ if (rText !== '') {
208+ rText += '|'
209+ }
210+ var s = sp[i];
211+ if (s.normalize) {
212+ s = s.normalize('NFKC');
213+ }
214+ var s2 = s.replace(regRep, function(m, m1, m2, m3){
215+ if (m1) {
216+ // Symbol - escape with prior backslach
217+ return '\\' + m1;
218+ } else if (m2) {
219+ // Katakana
220+ var r = '(?:' + String.fromCharCode(m2.charCodeAt(0) - 0x60) +
221+ '|' + m2;
222+ if (kanaMap[m2]) {
223+ r += '|' + kanaMap[m2];
224+ }
225+ r += ')';
226+ return r;
227+ } else if (m3) {
228+ // Hiragana
229+ var katakana = String.fromCharCode(m3.charCodeAt(0) + 0x60);
230+ var r = '(?:' + m3 + '|' + katakana;
231+ if (kanaMap[katakana]) {
232+ r += '|' + kanaMap[katakana];
233+ }
234+ r += ')';
235+ return r;
236+ }
237+ return m;
238+ });
239+ rText += '(' + s2 + ')';
240+ }
241+ return new RegExp(rText, 'ig');
242+ }
243+ function getAuthorHeader(body) {
244+ var start = 0;
245+ var pos;
246+ while ((pos = body.indexOf('\n', start)) >= 0) {
247+ var line = body.substring(start, pos);
248+ if (line.match(/^#author\(/, line)) {
249+ return line;
250+ } else if (line.match(/^#freeze(\W|$)/, line)) {
251+ // Found #freeze still in header
252+ } else {
253+ // other line, #author not found
254+ return null;
255+ }
256+ start = pos + 1;
257+ }
258+ return null;
259+ }
260+ function getUpdateTimeFromAuthorInfo(authorInfo) {
261+ var m = authorInfo.match(/^#author\("([^;"]+)(;[^;"]+)?/);
262+ if (m) {
263+ return m[1];
264+ }
265+ return '';
266+ }
267+ function getTargetLines(body, searchRegex) {
268+ var lines = body.split('\n');
269+ var found = [];
270+ var foundLines = [];
271+ var isInAuthorHeader = true;
272+ var lastFoundLineIndex = -1 - aroundLines;
273+ var lastAddedLineIndex = lastFoundLineIndex;
274+ var blocks = [];
275+ var lineCount = 0;
276+ for (var index = 0, length = lines.length; index < length; index++) {
277+ var line = lines[index];
278+ if (isInAuthorHeader) {
279+ // '#author line is not search target'
280+ if (line.match(/^#author\(/)) {
281+ // Remove this line from search target
282+ continue;
283+ } else if (line.match(/^#freeze(\W|$)/)) {
284+ // Still in header
285+ } else {
286+ // Already in body
287+ isInAuthorHeader = false;
288+ }
289+ }
290+ var decorated = findAndDecorateText(line, searchRegex);
291+ if (decorated === null) {
292+ if (index < lastFoundLineIndex + aroundLines + 1) {
293+ foundLines.push('' + (index + 1) + ':\t' + escapeHTML(lines[index]));
294+ lineCount++;
295+ lastAddedLineIndex = index;
296+ }
297+ } else {
298+ var startIndex = Math.max(Math.max(lastAddedLineIndex + 1, index - aroundLines), 0);
299+ if (lastAddedLineIndex + 1 < startIndex) {
300+ // Newly found!
301+ var block = {
302+ startIndex: startIndex,
303+ foundLineIndex: index,
304+ lines: []
305+ };
306+ foundLines = block.lines;
307+ blocks.push(block);
308+ }
309+ if (lineCount >= maxResultLines) {
310+ foundLines.push('...');
311+ return blocks;
312+ }
313+ for (var i = startIndex; i < index; i++) {
314+ foundLines.push('' + (i + 1) + ':\t' + escapeHTML(lines[i]));
315+ lineCount++;
316+ }
317+ foundLines.push('' + (index + 1) + ':\t' + decorated);
318+ lineCount++;
319+ lastFoundLineIndex = lastAddedLineIndex = index;
320+ }
321+ }
322+ return blocks;
323+ }
324+ function getSummary(bodyText, searchRegex) {
325+ return getTargetLines(bodyText, searchRegex);
326+ }
327+ function hookSearch2(e) {
328+ var form = document.querySelector('form');
329+ if (form && form.q) {
330+ var q = form.q;
331+ if (q.value === '') {
332+ q.focus();
333+ }
334+ }
335+ }
336+ function getBodySummary(body) {
337+ var lines = body.split('\n');
338+ var isInAuthorHeader = true;
339+ var summary = [];
340+ var lineCount = 0;
341+ for (var index = 0, length = lines.length; index < length; index++) {
342+ var line = lines[index];
343+ if (isInAuthorHeader) {
344+ // '#author line is not search target'
345+ if (line.match(/^#author\(/)) {
346+ // Remove this line from search target
347+ continue;
348+ } else if (line.match(/^#freeze(\W|$)/)) {
349+ continue;
350+ // Still in header
351+ } else {
352+ // Already in body
353+ isInAuthorHeader = false;
354+ }
355+ }
356+ line = line.replace(/^\s+|\s+$/g, '');
357+ if (line.length === 0) continue; // Empty line
358+ if (line.match(/^#\w+/)) continue; // Block-type plugin
359+ if (line.match(/^\/\//)) continue; // Comment
360+ if (line.substr(0, 1) === '*') {
361+ line = line.replace(/\s*\[\#\w+\]$/, ''); // Remove anchor
362+ }
363+ summary.push(line);
364+ if (summary.length >= 10) {
365+ continue;
366+ }
367+ }
368+ return summary.join(' ').substring(0, 150);
369+ }
370+ function removeEncodeHint() {
371+ // Remove 'encode_hint' if site charset is UTF-8
372+ var props = getSiteProps();
373+ if (!props.is_utf8) return;
374+ var forms = document.querySelectorAll('form');
375+ forEach(forms, function(form){
376+ if (form.cmd && form.cmd.value === 'search2') {
377+ if (form.encode_hint && (typeof form.encode_hint.removeAttribute === 'function')) {
378+ form.encode_hint.removeAttribute('name');
379+ }
380+ }
381+ });
382+ }
383+ function kickFirstSearch() {
384+ var form = document.querySelector('._plugin_search2_form');
385+ var searchText = form && form.q;
386+ if (!searchText) return;
387+ if (searchText && searchText.value) {
388+ var e = document.querySelector('#_plugin_search2_msg_searching');
389+ var msg = e && e.value || 'Searching...';
390+ setSearchStatus(msg);
391+ var base = '';
392+ forEach(form.querySelectorAll('input[name="base"]'), function(radio){
393+ if (radio.checked) base = radio.value;
394+ });
395+ doSearch(searchText.value, {base: base}, 0);
396+ }
397+ }
398+ function setSearchStatus(statusText) {
399+ var statusObj = document.querySelector('#_plugin_search2_search_status');
400+ if (statusObj) {
401+ statusObj.textContent = statusText;
402+ }
403+ }
404+ function forEach(nodeList, func) {
405+ if (nodeList.forEach) {
406+ nodeList.forEach(func);
407+ } else {
408+ for (var i = 0, n = nodeList.length; i < n; i++) {
409+ func(nodeList[i], i);
410+ }
411+ }
412+ }
413+ function replaceSearchWithSearch2() {
414+ forEach(document.querySelectorAll('form'), function(f){
415+ if (f.action.match(/cmd=search$/)) {
416+ f.addEventListener('submit', function(e) {
417+ var q = e.target.word.value;
418+ var base = '';
419+ forEach(f.querySelectorAll('input[name="base"]'), function(radio){
420+ if (radio.checked) base = radio.value;
421+ });
422+ var props = getSiteProps();
423+ var loc = document.location;
424+ var baseUri = loc.protocol + '//' + loc.host + loc.pathname;
425+ if (props.base_uri_pathname) {
426+ baseUri = props.base_uri_pathname;
427+ }
428+ var url = baseUri + '?' +
429+ (props.is_utf8 ? '' : 'encode_hint=' +
430+ encodeURIComponent('\u3077') + '&') +
431+ 'cmd=search2' +
432+ '&q=' + encodeSearchText(q) +
433+ (base ? '&base=' + encodeURIComponent(base) : '');
434+ e.preventDefault();
435+ setTimeout(function() {
436+ location.href = url;
437+ }, 1);
438+ return false;
439+ });
440+ var radios = f.querySelectorAll('input[type="radio"][name="type"]');
441+ forEach(radios, function(radio){
442+ if (radio.value === 'AND') {
443+ radio.addEventListener('click', onAndRadioClick);
444+ } else if (radio.value === 'OR') {
445+ radio.addEventListener('click', onOrRadioClick);
446+ }
447+ });
448+ function onAndRadioClick(e) {
449+ var sp = removeSearchOperators(f.word.value).split(/\s+/);
450+ var newText = sp.join(' ');
451+ if (f.word.value !== newText) {
452+ f.word.value = newText;
453+ }
454+ }
455+ function onOrRadioClick(e) {
456+ var sp = removeSearchOperators(f.word.value).split(/\s+/);
457+ var newText = sp.join(' OR ');
458+ if (f.word.value !== newText) {
459+ f.word.value = newText;
460+ }
461+ }
462+ }
463+ });
464+ }
465+ function encodeSearchText(q) {
466+ var sp = q.split(/\s+/);
467+ for (var i = 0; i < sp.length; i++) {
468+ sp[i] = encodeURIComponent(sp[i]);
469+ }
470+ return sp.join('+');
471+ }
472+ function encodeSearchTextForHash(q) {
473+ var sp = q.split(/\s+/);
474+ return sp.join('+');
475+ }
476+ function findAndDecorateText(text, searchRegex) {
477+ var isReplaced = false;
478+ var lastIndex = 0;
479+ var m;
480+ var decorated = '';
481+ searchRegex.lastIndex = 0;
482+ while ((m = searchRegex.exec(text)) !== null) {
483+ isReplaced = true;
484+ var pre = text.substring(lastIndex, m.index);
485+ decorated += escapeHTML(pre);
486+ for (var i = 1; i < m.length; i++) {
487+ if (m[i]) {
488+ decorated += '<strong class="word' + (i - 1) + '">' + escapeHTML(m[i]) + '</strong>'
489+ }
490+ }
491+ lastIndex = searchRegex.lastIndex;
492+ }
493+ if (isReplaced) {
494+ decorated += escapeHTML(text.substr(lastIndex));
495+ return decorated;
496+ }
497+ return null;
498+ }
499+ function getSearchTextInLocationHash() {
500+ // TODO Cross browser
501+ var hash = location.hash;
502+ if (!hash) return '';
503+ var q = '';
504+ if (hash.substr(0, 3) === '#q=') {
505+ q = hash.substr(3).replace(/\+/g, ' ');
506+ } else if (hash.substr(0, 6) === '#encq=') {
507+ q = decodeURIComponent(hash.substr(6).replace(/\+/g, ' '));
508+ }
509+ return q;
510+ }
511+ function colorSearchTextInBody() {
512+ var searchText = getSearchTextInLocationHash();
513+ if (!searchText) return;
514+ var searchRegex = textToRegex(removeSearchOperators(searchText));
515+ var headReText = '([\\s\\b]|^)';
516+ var tailReText = '\\b';
517+ var ignoreTags = ['INPUT', 'TEXTAREA', 'BUTTON',
518+ 'SCRIPT', 'FRAME', 'IFRAME'];
519+ function colorSearchText(element, searchRegex) {
520+ var decorated = findAndDecorateText(element.nodeValue, searchRegex);
521+ if (decorated) {
522+ var span = document.createElement('span');
523+ span.innerHTML = decorated;
524+ element.parentNode.replaceChild(span, element);
525+ }
526+ }
527+ function walkElement(element) {
528+ var e = element.firstChild;
529+ while (e) {
530+ if (e.nodeType == 3 && e.nodeValue &&
531+ e.nodeValue.length >= 2 && /\S/.test(e.nodeValue)) {
532+ var next = e.nextSibling;
533+ colorSearchText(e, searchRegex);
534+ e = next;
535+ } else {
536+ if (e.nodeType == 1 && ignoreTags.indexOf(e.tagName) == -1) {
537+ walkElement(e);
538+ }
539+ e = e.nextSibling;
540+ }
541+ }
542+ }
543+ var target = document.getElementById('body');
544+ walkElement(target);
545+ }
546+ function showNoSupportMessage() {
547+ var pList = document.getElementsByClassName('_plugin_search2_nosupport_message');
548+ for (var i = 0; i < pList.length; i++) {
549+ var p = pList[i];
550+ p.style.display = 'block';
551+ }
552+ }
553+ function isEnabledFetchFunctions() {
554+ if (window.fetch && document.querySelector && window.JSON) {
555+ return true;
556+ }
557+ return false;
558+ }
559+ function isEnableServerFunctions() {
560+ var props = getSiteProps();
561+ if (props.json_enabled) return true;
562+ return false;
563+ }
564+ function getSiteProps() {
565+ var empty = {};
566+ var propsDiv = document.getElementById('pukiwiki-site-properties');
567+ if (!propsDiv) return empty;
568+ var jsonE = propsDiv.querySelector('div[data-key="site-props"]');
569+ if (!jsonE) return emptry;
570+ var props = JSON.parse(jsonE.getAttribute('data-value'));
571+ return props || empty;
572+ }
573+ colorSearchTextInBody();
574+ if (! isEnabledFetchFunctions()) {
575+ showNoSupportMessage();
576+ return;
577+ }
578+ if (! isEnableServerFunctions()) return;
579+ replaceSearchWithSearch2();
580+ hookSearch2();
581+ removeEncodeHint();
582+ kickFirstSearch();
583+ }
584+ enableSearch2();
585+});
--- a/skin/tdiary.css
+++ b/skin/tdiary.css
@@ -517,6 +517,21 @@ tr.bugtrack_state_undef td {
517517 background-color: #ff3333;
518518 }
519519
520+/* search2.inc.php */
521+input#_plugin_search2_detail:checked ~ ul > div.search-result-detail {
522+ display: block;
523+}
524+input#_plugin_search2_detail ~ ul > div.search-result-detail {
525+ display: none;
526+}
527+.search-result-page-summary {
528+ font-size: 70%;
529+ color: gray;
530+ overflow: hidden;
531+ text-overflow: ellipsis;
532+ white-space: nowrap;
533+}
534+
520535 @media print {
521536 img#logo,
522537 div#navigator,