OSDN Git Service

BugTrack/692 Show page contents in search result - search2 plugin
authorumorigu <umorigu@gmail.com>
Sat, 30 Sep 2017 06:31:10 +0000 (15:31 +0900)
committerumorigu <umorigu@gmail.com>
Sat, 30 Sep 2017 06:31:10 +0000 (15:31 +0900)
* 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)

12 files changed:
en.lng.php
ja.lng.php
lib/file.php
lib/func.php
lib/html.php
lib/link.php
plugin/search2.inc.php [new file with mode: 0644]
skin/main.js
skin/pukiwiki.css
skin/pukiwiki.skin.php
skin/search2.js [new file with mode: 0644]
skin/tdiary.css

index 4384192..470104b 100644 (file)
@@ -2,7 +2,7 @@
 // PukiWiki - Yet another WikiWikiWeb clone.
 // en.lng.php
 // Copyright
-//   2002-2016 PukiWiki Development Team
+//   2002-2017 PukiWiki Development Team
 //   2001-2002 Originally written by yu-ji
 // License: GPL v2 or (at your option) any later version
 //
@@ -53,6 +53,8 @@ $_msg_help        = 'View Text Formatting Rules';
 $_msg_week        = array('Sun','Mon','Tue','Wed','Thu','Fri','Sat');
 $_msg_content_back_to_top = '<div class="jumpmenu"><a href="#navigator">&uarr;</a></div>';
 $_msg_word        = 'These search terms have been highlighted:';
+$_msg_unsupported_webbrowser = 'This function doesn\'t support your current Web browser.';
+$_msg_use_alternative_link = 'Please go to the following link destination: $1';
 
 ///////////////////////////////////////
 // Symbols
@@ -367,6 +369,8 @@ $_btn_and       = 'AND';
 $_btn_or        = 'OR';
 $_search_pages  = 'Search for page starts from $1';
 $_search_all    = 'Search for all pages';
+$_search_searching = 'Searching...';
+$_search_detail = 'Show details';
 
 ///////////////////////////////////////
 // source.inc.php
index f2bb9fc..0704d6c 100644 (file)
@@ -2,7 +2,7 @@
 // PukiWiki - Yet another WikiWikiWeb clone.
 // ja.lng.php
 // Copyright
-//   2002-2016 PukiWiki Development Team
+//   2002-2017 PukiWiki Development Team
 //   2001-2002 Originally written by yu-ji
 // License: GPL v2 or (at your option) any later version
 //
@@ -55,6 +55,8 @@ $_msg_help           = 'テキスト整形のルールを表示する';
 $_msg_week           = array('日','月','火','水','木','金','土');
 $_msg_content_back_to_top = '<div class="jumpmenu"><a href="#navigator">&uarr;</a></div>';
 $_msg_word           = 'これらのキーワードがハイライトされています:';
+$_msg_unsupported_webbrowser = 'この機能はお使いのWebブラウザには対応していません。';
+$_msg_use_alternative_link = 'リンク先の機能をご利用ください: $1';
 
 ///////////////////////////////////////
 // Symbols
@@ -114,7 +116,7 @@ $_LANG['skin']['rename']    = '名前変更';       // Rename a page (and related)
 $_LANG['skin']['rss']       = '最終更新のRSS';    // RSS of RecentChanges
 $_LANG['skin']['rss10']     = & $_LANG['skin']['rss'];
 $_LANG['skin']['rss20']     = & $_LANG['skin']['rss'];
-$_LANG['skin']['search']    = '単語検索';
+$_LANG['skin']['search']    = '検索';
 $_LANG['skin']['top']       = 'トップ';     // Top page
 $_LANG['skin']['unfreeze']  = '凍結解除';
 $_LANG['skin']['upload']    = '添付';        // Attach a file
@@ -361,7 +363,7 @@ $_rename_messages  = array(
 
 ///////////////////////////////////////
 // search.inc.php
-$_title_search  = '単語検索';
+$_title_search  = '検索';
 $_title_result  = '$1 の検索結果';
 $_msg_searching = '全てのページから単語を検索します。大文字小文字の区別はありません。';
 $_btn_search    = '検索';
@@ -369,6 +371,8 @@ $_btn_and       = 'AND検索';
 $_btn_or        = 'OR検索';
 $_search_pages  = '$1 から始まるページを検索';
 $_search_all    = '全てのページを検索';
+$_search_searching = '検索中...';
+$_search_detail = '詳細表示';
 
 ///////////////////////////////////////
 // source.inc.php
index 597b5fc..39c3af8 100644 (file)
@@ -15,9 +15,16 @@ define('PKWK_MAXSHOW_CACHE', 'recent.dat');
 // AutoLink
 define('PKWK_AUTOLINK_REGEX_CACHE', 'autolink.dat');
 
-// Get source(wiki text) data of the page
-// Returns FALSE if error occurerd
-function get_source($page = NULL, $lock = TRUE, $join = FALSE)
+/**
+ * Get source(wiki text) data of the page
+ *
+ * @param $page page name
+ * @param $lock lock
+ * @param $join true: return string, false: return array of string
+ * @param $raw true: return file content as-is
+ * @return FALSE if error occurerd
+ */
+function get_source($page = NULL, $lock = TRUE, $join = FALSE, $raw = FALSE)
 {
        //$result = NULL;       // File is not found
        $result = $join ? '' : array();
@@ -44,6 +51,9 @@ function get_source($page = NULL, $lock = TRUE, $join = FALSE)
                        } else {
                                $result = fread($fp, $size);
                                if ($result !== FALSE) {
+                                       if ($raw) {
+                                               return $result;
+                                       }
                                        // Removing Carriage-Return
                                        $result = str_replace("\r", '', $result);
                                }
@@ -204,16 +214,54 @@ function remove_author_info($wikitext)
        return preg_replace('/^\s*#author\([^\n]*(\n|$)/m', '', $wikitext);
 }
 
-function remove_author_lines($lines)
+/**
+ * Remove author line from wikitext
+ */
+function remove_author_header($wikitext)
 {
-       $author_head = '#author(';
-       $len = strlen($author_head);
-       for ($i = 0; $i < 5; $i++) {
-               if (substr($lines[$i], 0, $len) === $author_head) {
-                       unset($lines[$i]);
+       $start = 0;
+       while (($pos = strpos($wikitext, "\n", $start)) != false) {
+               $line = substr($wikitext, $start, $pos);
+               $m = null;
+               if (preg_match('/^#author\(/', $line, $m)) {
+                       // fond #author line, Remove this line only
+                       if ($start === 0) {
+                               return substr($wikitext, $pos + 1);
+                       } else {
+                               return substr($wikitext, 0, $start - 1) .
+                                       substr($wikitext, $pos + 1);
+                       }
+               } else if (preg_match('/^#freeze(\W|$)/', $line, $m)) {
+                       // Found #freeze still in header
+               } else {
+                       // other line, #author not found
+                       return $wikitext;
+               }
+               $start = $pos + 1;
+       }
+       return $wikitext;
+}
+
+/**
+ * Get author info from wikitext
+ */
+function get_author_info($wikitext)
+{
+       $start = 0;
+       while (($pos = strpos($wikitext, "\n", $start)) != false) {
+               $line = substr($wikitext, $start, $pos);
+               $m = null;
+               if (preg_match('/^#author\(/', $line, $m)) {
+                       return $line;
+               } else if (preg_match('/^#freeze(\W|$)/', $line, $m)) {
+                       // Found #freeze still in header
+               } else {
+                       // other line, #author not found
+                       return false;
                }
+               $start = $pos + 1;
        }
-       return $lines;
+       return false;
 }
 
 function get_date_atom($timestamp)
index 716243f..9536393 100644 (file)
@@ -346,8 +346,8 @@ function do_search($word, $type = 'AND', $non_format = FALSE, $base = '')
 
                // Search for page contents
                foreach ($keys as $key) {
-                       $lines = remove_author_lines(get_source($page, TRUE, FALSE));
-                       $b_match = preg_match($key, join('', $lines));
+                       $body = get_source($page, TRUE, TRUE, TRUE);
+                       $b_match = preg_match($key, remove_author_header($body));
                        if ($b_type xor $b_match) break; // OR
                }
                if ($b_match) continue;
index 46e1d97..92972b6 100644 (file)
@@ -206,10 +206,28 @@ function get_html_scripting_data()
        if (!isset($ticket_link_sites) || !is_array($ticket_link_sites)) {
                return '';
        }
+       $is_utf8 = (bool)defined('PKWK_UTF8_ENABLE');
        // Require: PHP 5.4+
-       if (!defined('JSON_UNESCAPED_UNICODE')) {
-               return '';
-       };
+       $json_enabled = defined('JSON_UNESCAPED_UNICODE');
+       if (!$json_enabled) {
+               $empty_data = <<<EOS
+<div id="pukiwiki-site-properties" style="display:none;">
+</div>
+EOS;
+               return $empty_data;
+       }
+       // Site basic Properties
+       $props = array(
+               'is_utf8' => $is_utf8,
+               'json_enabled' => $json_enabled,
+               'base_uri_pathname' => get_base_uri(PKWK_URI_ROOT),
+               'base_uri_absolute' => get_base_uri(PKWK_URI_ABSOLUTE)
+       );
+       $props_json = htmlsc(json_encode($props, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
+       $site_props = <<<EOS
+<div data-key="site-props" data-value="$props_json"></div>
+EOS;
+       // AutoTicketLink
        $text = '';
        foreach ($ticket_link_sites as $s) {
                if (!preg_match('/^([a-zA-Z0-9]+)([\.\-][a-zA-Z0-9]+)*$/', $s['key'])) {
@@ -221,11 +239,15 @@ function get_html_scripting_data()
 EOS;
                $text .= "\n";
        }
-       $data = <<<EOS
-<div id="pukiwiki-site-properties" style="display:none;">
+       $ticketlink_data = <<<EOS
 <div class="ticketlink-def">
 $text
 </div>
+EOS;
+       $data = <<<EOS
+<div id="pukiwiki-site-properties" style="display:none;">
+$site_props
+$ticketlink_data
 </div>
 EOS;
        return $data;
index 6961069..67ccd9f 100644 (file)
@@ -266,8 +266,8 @@ function links_do_search_page($word)
                $b_match = FALSE;
                // Search for page contents
                foreach ($keys as $key) {
-                       $lines = remove_author_lines(get_source($page, TRUE, FALSE));
-                       $b_match = preg_match($key, join('', $lines));
+                       $body = get_source($page, TRUE, TRUE, TRUE);
+                       $b_match = preg_match($key, remove_author_header($body));
                        if (! $b_match) break; // OR
                }
                if ($b_match) continue;
diff --git a/plugin/search2.inc.php b/plugin/search2.inc.php
new file mode 100644 (file)
index 0000000..195267d
--- /dev/null
@@ -0,0 +1,222 @@
+<?php
+// PukiWiki - Yet another WikiWikiWeb clone.
+// search2.inc.php
+// Copyright 2017 PukiWiki Development Team
+// License: GPL v2 or (at your option) any later version
+//
+// Search2 plugin - Show detail result using JavaScript
+
+define('PLUGIN_SEARCH2_MAX_LENGTH', 80);
+define('PLUGIN_SEARCH2_MAX_BASE',   16); // #search(1,2,3,...,15,16)
+
+// Show a search box on a page
+function plugin_search2_convert()
+{
+       $args = func_get_args();
+       return plugin_search_search_form('', '', $args);
+}
+
+function plugin_search2_action()
+{
+       global $vars, $_title_search, $_title_result;
+
+       $action = isset($vars['action']) ? $vars['action'] : '';
+       $base = isset($vars['base']) ? $vars['base'] : '';
+       $bases = array();
+       if ($base !== '') {
+               $bases[] = $base;
+       }
+       if ($action === '') {
+               $q = isset($vars['q']) ? $vars['q'] : '';
+               if ($q === '') {
+                       return array('msg' => $_title_search,
+                               'body' => plugin_search2_search_form($q, '', $bases));
+               } else {
+                       $msg  = str_replace('$1', htmlsc($q), $_title_result);
+                       return array('msg' => $msg,
+                                       'body' => plugin_search2_search_form($q, '', $bases));
+               }
+       } else if ($action === 'query') {
+               $text = isset($vars['q']) ? $vars['q'] : '';
+               header('Content-Type: application/json; charset=UTF-8');
+               plugin_search2_do_search($text, $base);
+               exit;
+       }
+}
+
+function plugin_search2_do_search($query_text, $base)
+{
+       global $whatsnew, $non_list, $search_non_list;
+       global $_msg_andresult, $_msg_orresult, $_msg_notfoundresult;
+       global $search_auth;
+
+       $retval = array();
+
+       $b_type_and = true; // AND:TRUE OR:FALSE
+       $key_candidates = preg_split('/\s+/', $query_text, -1, PREG_SPLIT_NO_EMPTY);
+       for ($i = count($key_candidates) - 1; $i >= 0; $i--) {
+               if ($key_candidates[$i] === 'OR') {
+                       $b_type_and = false;
+                       unset($key_candidates[$i]);
+               }
+       }
+       $key_candidates = array_merge($key_candidates);
+       $keys = get_search_words($key_candidates);
+       foreach ($keys as $key=>$value)
+               $keys[$key] = '/' . $value . '/S';
+
+       $pages = get_existpages();
+
+       // Avoid
+       if ($base != '') {
+               $pages = preg_grep('/^' . preg_quote($base, '/') . '/S', $pages);
+       }
+       if (! $search_non_list) {
+               $pages = array_diff($pages, preg_grep('/' . $non_list . '/S', $pages));
+       }
+       natsort($pages);
+       $pages = array_flip($pages);
+       unset($pages[$whatsnew]);
+       $page_names = array_keys($pages);
+
+       $found_pages = array();
+       $readable_page_index = -1;
+       $scan_page_index = -1;
+       $last_read_page_name = null;
+       foreach ($page_names as $page) {
+               $b_match = FALSE;
+               $pagename_only = false;
+               $scan_page_index++;
+               if (! is_page_readable($page)) {
+                       if ($search_auth) {
+                               // $search_auth - 1: User can know page names that contain search text if the page is readable
+                               continue;
+                       }
+                       // $search_auth - 0: All users can know page names that conntain search text
+                       $pagename_only = true;
+               }
+               $readable_page_index++;
+               // Search for page name and contents
+               $body = get_source($page, TRUE, TRUE, TRUE);
+               $target = $page . "\n" . remove_author_header($body);
+               foreach ($keys as $key) {
+                       $b_match = preg_match($key, $target);
+                       if ($b_type_and xor $b_match) break; // OR
+               }
+               if ($b_match) {
+                       // Found!
+                       $filemtime = null;
+                       $author_info = get_author_info($body);
+                       if ($author_info === false || $pagename_only) {
+                               $updated_at = get_date_atom(filemtime(get_filename($page)));
+                       }
+                       if ($pagename_only) {
+                               // The user cannot read this page body
+                               $found_pages[] = array('name' => (string)$page,
+                                       'url' => get_page_uri($page), 'updated_at' => $updated_at,
+                                       'body' => '', 'pagename_only' => 1);
+                       } else {
+                               $found_pages[] = array('name' => (string)$page,
+                                       'url' => get_page_uri($page), 'updated_at' => $updated_at,
+                                       'body' => (string)$body);
+                       }
+               }
+               $last_read_page_name = $page;
+       }
+       $message = str_replace('$1', htmlsc($query_text), str_replace('$2', count($found_pages),
+               str_replace('$3', count($page_names), $b_type_and ? $_msg_andresult : $_msg_orresult)));
+       $search_done = (boolean)($scan_page_index + 1 === count($page_names));
+       $result_obj = array(
+               'message' => $message,
+               'q' => $query_text,
+               'read_page_count' => $readable_page_index + 1,
+               'scan_page_count' => $scan_page_index + 1,
+               'page_count' => count($page_names),
+               'last_read_page_name' => $last_read_page_name,
+               'search_done' => $search_done,
+               'results' => $found_pages);
+       $obj = $result_obj;
+       if (!defined('PKWK_UTF8_ENABLE')) {
+               if (SOURCE_ENCODING === 'EUC-JP') {
+                       mb_convert_variables('UTF-8', 'CP51932', $obj);
+               } else {
+                       mb_convert_variables('UTF-8', SOURCE_ENCODING, $obj);
+               }
+       }
+       print(json_encode($obj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
+}
+
+function plugin_search2_search_form($s_word = '', $type = '', $bases = array())
+{
+       global $_btn_search;
+       global $_search_pages, $_search_all;
+       global $_msg_andresult, $_msg_orresult, $_msg_notfoundresult;
+       global $_search_detail, $_search_searching;
+       global $_msg_unsupported_webbrowser, $_msg_use_alternative_link;
+
+       $script = get_base_uri();
+       $h_search_text = htmlsc($s_word);
+
+       $base_option = '';
+       if (!empty($bases)) {
+               $base_msg = '';
+               $_num = 0;
+               $check = ' checked';
+               foreach($bases as $base) {
+                       ++$_num;
+                       if (PLUGIN_SEARCH2_MAX_BASE < $_num) break;
+                       $s_base   = htmlsc($base);
+                       $base_str = '<strong>' . $s_base . '</strong>';
+                       $base_label = str_replace('$1', $base_str, $_search_pages);
+                       $base_msg  .=<<<EOD
+ <div>
+  <label>
+   <input type="radio" name="base" value="$s_base" $check> $base_label
+  </label>
+ </div>
+EOD;
+                       $check = '';
+               }
+               $base_msg .=<<<EOD
+<label><input type="radio" name="base" value=""> $_search_all</label>
+EOD;
+               $base_option = '<div class="small">' . $base_msg . '</div>';
+       }
+       $_search2_result_notfound = htmlsc($_msg_notfoundresult);
+       $_search2_result_found = htmlsc($_msg_andresult);
+       $_search2_search_wait_milliseconds = PLUGIN_SEARCH2_SEARCH_WAIT_MILLISECONDS;
+       $result_page_panel =<<<EOD
+<div id="_plugin_search2_search_status"></div>
+<div id="_plugin_search2_message"></div>
+<input type="checkbox" id="_plugin_search2_detail" checked><label for="_plugin_search2_detail">$_search_detail</label>
+<input type="hidden" id="_plugin_search2_msg_searching" value="$_search_searching">
+<input type="hidden" id="_plugin_search2_msg_result_notfound" value="$_search2_result_notfound">
+<input type="hidden" id="_plugin_search2_msg_result_found" value="$_search2_result_found">
+<input type="hidden" id="_search2_search_wait_milliseconds" value="$_search2_search_wait_milliseconds">
+EOD;
+       if ($h_search_text == '') {
+               $result_page_panel = '';
+       }
+
+       $plain_search_link = '<a href="' . $script . '?cmd=search' . '">' . htmlsc($_btn_search) . '</a>';
+       $alt_msg = str_replace('$1', $plain_search_link, $_msg_use_alternative_link);
+       return <<<EOD
+<noscript>
+ <p>$_msg_unsupported_webbrowser $alt_msg</p>
+</noscript>
+<p class="_plugin_search2_nosupport_message" style="display:none;">
+  $_msg_unsupported_webbrowser $alt_msg
+</p>
+<form action="$script" method="GET" class="_plugin_search2_form">
+ <div>
+  <input type="hidden" name="cmd" value="search2">
+  <input type="search"  name="q" value="$h_search_text" size="30">
+  <input type="submit" value="$_btn_search">
+ </div>
+$base_option
+</form>
+$result_page_panel
+<ul id="result-list">
+</ul>
+EOD;
+}
index 1c9537a..35079fe 100644 (file)
@@ -160,7 +160,7 @@ window.addEventListener && window.addEventListener('DOMContentLoaded', function(
       }
       var siteNodes = defRoot.querySelectorAll('.ticketlink-site');
       Array.prototype.forEach.call(siteNodes, function (e) {
-        var siteInfoText = e.dataset && e.dataset.site;
+        var siteInfoText = e.getAttribute('data-site');
         if (!siteInfoText) return;
         var info = textToSiteInfo(siteInfoText);
         if (info) {
index b006c23..7dde240 100644 (file)
@@ -641,6 +641,21 @@ tr.bugtrack_state_undef td {
        background-color: #ff3333;
 }
 
+/* search2.inc.php  */
+input#_plugin_search2_detail:checked ~ ul > div.search-result-detail {
+       display: block;
+}
+input#_plugin_search2_detail ~ ul > div.search-result-detail {
+       display: none;
+}
+.search-result-page-summary {
+       font-size: 70%;
+       color: gray;
+       overflow: hidden;
+       text-overflow: ellipsis;
+       white-space: nowrap;
+}
+
 @media print {
   a:link,
   a:visited {
index 53154bc..4ea3ab0 100644 (file)
@@ -69,6 +69,7 @@ header('Content-Type: text/html; charset=' . CONTENT_CHARSET);
  <link rel="stylesheet" type="text/css" href="<?php echo SKIN_DIR ?>pukiwiki.css" />
  <link rel="alternate" type="application/rss+xml" title="RSS" href="<?php echo $link['rss'] ?>" /><?php // RSS auto-discovery ?>
  <script type="text/javascript" src="skin/main.js" defer></script>
+ <script type="text/javascript" src="skin/search2.js" defer></script>
 
 <?php echo $head_tag ?>
 </head>
diff --git a/skin/search2.js b/skin/search2.js
new file mode 100644 (file)
index 0000000..169778b
--- /dev/null
@@ -0,0 +1,585 @@
+// PukiWiki - Yet another WikiWikiWeb clone.
+// search2.js
+// Copyright
+//   2017 PukiWiki Development Team
+// License: GPL v2 or (at your option) any later version
+//
+// PukiWiki search2 pluign - JavaScript client script
+window.addEventListener && window.addEventListener('DOMContentLoaded', function() {
+  function enableSearch2() {
+    var aroundLines = 2;
+    var maxResultLines = 20;
+    var minBlockLines = 5;
+    var minSearchWaitMilliseconds = 100;
+    var kanaMap = null;
+    function escapeHTML (s) {
+      if(typeof s !== 'string') {
+        s = '' + s;
+      }
+      return s.replace(/[&"<>]/g, function(m) {
+        return {
+          '&': '&amp;',
+          '"': '&quot;',
+          '<': '&lt;',
+          '>': '&gt;'
+        }[m];
+      });
+    }
+    function doSearch(searchText, session, startIndex) {
+      var url = './?cmd=search2&action=query';
+      var props = getSiteProps();
+      url += '&encode_hint=' + encodeURIComponent('\u3077');
+      if (searchText) {
+        url += '&q=' + encodeURIComponent(searchText);
+      }
+      if (session.base) {
+        url += '&base=' + encodeURIComponent(session.base);
+      }
+      url += '&start=' + startIndex;
+      fetch (url
+      ).then(function(response){
+        if (response.ok) {
+          return response.json();
+        } else {
+          throw new Error(response.status + ': ' +
+            + response.statusText + ' on ' + url);
+        }
+      }).then(function(obj) {
+        showResult(obj, session, searchText);
+      })['catch'](function(err){
+        console.log(err);
+        console.log('Error! Please check JavaScript console' + '\n' + JSON.stringify(err) + '|' + err);
+      });
+    }
+    function getMessageTemplate(idText, defaultText) {
+      var messageHolder = document.querySelector('#' + idText);
+      var messageTemplate = (messageHolder && messageHolder.value) || defaultText;
+      return messageTemplate;
+    }
+    function getAuthorInfo(text) {
+
+    }
+    function getPassage(now, dateText) {
+      if (! dateText) {
+        return '';
+      }
+      var units = [{u: 'm', max: 60}, {u: 'h', max: 24}, {u: 'd', max: 1}];
+      var d = new Date();
+      d.setTime(Date.parse(dateText));
+      var t = (now.getTime() - d.getTime()) / (1000 * 60); // minutes
+      var unit = units[0].u, card = units[0].max;
+      for (var i = 0; i < units.length; i++) {
+        unit = units[i].u, card = units[i].max;
+        if (t < card) break;
+        t = t / card;
+      }
+      return '(' + Math.floor(t) + unit + ')';
+    }
+    function removeSearchOperators(searchText) {
+      var sp = searchText.split(/\s+/);
+      if (sp.length <= 1) {
+        return searchText;
+      }
+      var hasOr = false;
+      for (var i = sp.length - 1; i >= 0; i--) {
+        if (sp[i] === 'OR') {
+          hasOr = true;
+          sp.splice(i, 1);
+        }
+      }
+      return sp.join(' ');
+    }
+    function showResult(obj, session, searchText) {
+      var searchRegex = textToRegex(removeSearchOperators(searchText));
+      var ul = document.querySelector('#result-list');
+      if (!ul) return;
+      ul.innerHTML = '';
+      if (! session.scan_page_count) session.scan_page_count = 0;
+      if (! session.read_page_count) session.read_page_count = 0;
+      if (! session.hit_page_count) session.hit_page_count = 0;
+      session.scan_page_count += obj.scan_page_count;
+      session.read_page_count += obj.read_page_count;
+      session.hit_page_count += obj.results.length;
+      session.page_count = obj.page_count;
+
+      var msg = obj.message;
+      var notFoundMessageTemplate = getMessageTemplate('_plugin_search2_msg_result_notfound',
+        'No page which contains $1 has been found.');
+      var foundMessageTemplate = getMessageTemplate('_plugin_search2_msg_result_found',
+        'In the page <strong>$2</strong>, <strong>$3</strong> pages that contain all the terms $1 were found.');
+      var searchTextDecorated = findAndDecorateText(searchText, searchRegex);
+      if (searchTextDecorated === null) searchTextDecorated = escapeHTML(searchText);
+      var messageTemplate = foundMessageTemplate;
+      if (session.hit_page_count === 0) {
+        messageTemplate = notFoundMessageTemplate;
+      }
+      msg = messageTemplate.replace(/\$1|\$2|\$3/g, function(m){
+        return {
+          '$1': searchTextDecorated,
+          '$2': session.hit_page_count,
+          '$3': session.read_page_count
+        }[m];
+      });
+      document.querySelector('#_plugin_search2_message').innerHTML = msg;
+
+      setSearchStatus('');
+      var results = obj.results;
+      var now = new Date();
+      results.forEach(function(val, index) {
+        var fragment = document.createDocumentFragment();
+        var li = document.createElement('li');
+        var hash = '#q=' + encodeSearchTextForHash(searchText);
+        var href = val.url + hash;
+        var decoratedName = findAndDecorateText(val.name, searchRegex);
+        if (! decoratedName) {
+          decoratedName = escapeHTML(val.name);
+        }
+        var author = getAuthorHeader(val.body);
+        var updatedAt = '';
+        if (author) {
+          updatedAt = getUpdateTimeFromAuthorInfo(author);
+        } else {
+          updatedAt = val.updated_at;
+        }
+        var liHtml = '<a href="' + escapeHTML(href) + '">' + decoratedName + '</a> ' +
+          getPassage(now, updatedAt);
+        li.innerHTML = liHtml;
+        var a = li.querySelector('a');
+        if (a && a.hash) {
+          if (a.hash !== hash) {
+            // Some browser execute encodeHTML(hash) automatically. Support them.
+            a.href = val.url + '#encq=' + encodeSearchTextForHash(searchText);
+          }
+        }
+        fragment.appendChild(li);
+        var div = document.createElement('div');
+        div.classList.add('search-result-detail');
+        var head = document.createElement('div');
+        head.classList.add('search-result-page-summary');
+        head.innerHTML = escapeHTML(getBodySummary(val.body));
+        div.appendChild(head);
+        var summary = getSummary(val.body, searchRegex);
+        for (var i = 0; i < summary.length; i++) {
+          var pre = document.createElement('pre');
+          pre.innerHTML = summary[i].lines.join('\n');
+          div.appendChild(pre);
+        }
+        fragment.appendChild(div);
+        ul.appendChild(fragment);
+      });
+    }
+    function prepareKanaMap() {
+      if (kanaMap !== null) return;
+      if (!String.prototype.normalize) {
+        kanaMap = {};
+        return;
+      }
+      var dakuten = '\uFF9E';
+      var maru = '\uFF9F';
+      var map = {};
+      for (var c = 0xFF61; c <=0xFF9F; c++) {
+        var han = String.fromCharCode(c);
+        var zen = han.normalize('NFKC');
+        map[zen] = han;
+        var hanDaku = han + dakuten;
+        var zenDaku = hanDaku.normalize('NFKC');
+        if (zenDaku.length === 1) { // +Handaku-ten OK
+            map[zenDaku] = hanDaku;
+        }
+        var hanMaru = han + maru;
+        var zenMaru = hanMaru.normalize('NFKC');
+        if (zenMaru.length === 1) { // +Maru OK
+            map[zenMaru] = hanMaru;
+        }
+      }
+      kanaMap = map;
+    }
+    function textToRegex(searchText) {
+      if (!searchText) return null;
+      var regEscape = /[\\^$.*+?()[\]{}|]/g;
+      //             1:Symbol             2:Katakana        3:Hiragana
+      var regRep = /([\\^$.*+?()[\]{}|])|([\u30a1-\u30f6])|([\u3041-\u3096])/g;
+      var s1 = searchText.replace(/^\s+|\s+$/g, '');
+      var sp = s1.split(/\s+/);
+      var rText = '';
+      prepareKanaMap();
+      for (var i = 0; i < sp.length; i++) {
+        if (rText !== '') {
+          rText += '|'
+        }
+        var s = sp[i];
+        if (s.normalize) {
+          s = s.normalize('NFKC');
+        }
+        var s2 = s.replace(regRep, function(m, m1, m2, m3){
+          if (m1) {
+            // Symbol - escape with prior backslach
+            return '\\' + m1;
+          } else if (m2) {
+            // Katakana
+            var r = '(?:' + String.fromCharCode(m2.charCodeAt(0) - 0x60) +
+              '|' + m2;
+            if (kanaMap[m2]) {
+              r += '|' + kanaMap[m2];
+            }
+            r += ')';
+            return r;
+          } else if (m3) {
+            // Hiragana
+            var katakana = String.fromCharCode(m3.charCodeAt(0) + 0x60);
+            var r = '(?:' + m3 + '|' + katakana;
+            if (kanaMap[katakana]) {
+              r += '|' + kanaMap[katakana];
+            }
+            r += ')';
+            return r;
+          }
+          return m;
+        });
+        rText += '(' + s2 + ')';
+      }
+      return new RegExp(rText, 'ig');
+    }
+    function getAuthorHeader(body) {
+      var start = 0;
+      var pos;
+      while ((pos = body.indexOf('\n', start)) >= 0) {
+        var line = body.substring(start, pos);
+        if (line.match(/^#author\(/, line)) {
+          return line;
+        } else if (line.match(/^#freeze(\W|$)/, line)) {
+          // Found #freeze still in header
+        } else {
+          // other line, #author not found
+          return null;
+        }
+        start = pos + 1;
+      }
+      return null;
+    }
+    function getUpdateTimeFromAuthorInfo(authorInfo) {
+      var m = authorInfo.match(/^#author\("([^;"]+)(;[^;"]+)?/);
+      if (m) {
+        return m[1];
+      }
+      return '';
+    }
+    function getTargetLines(body, searchRegex) {
+      var lines = body.split('\n');
+      var found = [];
+      var foundLines = [];
+      var isInAuthorHeader = true;
+      var lastFoundLineIndex = -1 - aroundLines;
+      var lastAddedLineIndex = lastFoundLineIndex;
+      var blocks = [];
+      var lineCount = 0;
+      for (var index = 0, length = lines.length; index < length; index++) {
+        var line = lines[index];
+        if (isInAuthorHeader) {
+          // '#author line is not search target'
+          if (line.match(/^#author\(/)) {
+            // Remove this line from search target
+            continue;
+          } else if (line.match(/^#freeze(\W|$)/)) {
+            // Still in header
+          } else {
+            // Already in body
+            isInAuthorHeader = false;
+          }
+        }
+        var decorated = findAndDecorateText(line, searchRegex);
+        if (decorated === null) {
+          if (index < lastFoundLineIndex + aroundLines + 1) {
+            foundLines.push('' + (index + 1) + ':\t' + escapeHTML(lines[index]));
+            lineCount++;
+            lastAddedLineIndex = index;
+          }
+        } else {
+          var startIndex = Math.max(Math.max(lastAddedLineIndex + 1, index - aroundLines), 0);
+          if (lastAddedLineIndex + 1 < startIndex) {
+            // Newly found!
+            var block = {
+              startIndex: startIndex,
+              foundLineIndex: index,
+              lines: []
+            };
+            foundLines = block.lines;
+            blocks.push(block);
+          }
+          if (lineCount >= maxResultLines) {
+            foundLines.push('...');
+            return blocks;
+          }
+          for (var i = startIndex; i < index; i++) {
+            foundLines.push('' + (i + 1) + ':\t' + escapeHTML(lines[i]));
+            lineCount++;
+          }
+          foundLines.push('' + (index + 1) + ':\t' + decorated);
+          lineCount++;
+          lastFoundLineIndex = lastAddedLineIndex = index;
+        }
+      }
+      return blocks;
+    }
+    function getSummary(bodyText, searchRegex) {
+      return getTargetLines(bodyText, searchRegex);
+    }
+    function hookSearch2(e) {
+      var form = document.querySelector('form');
+      if (form && form.q) {
+        var q = form.q;
+        if (q.value === '') {
+          q.focus();
+        }
+      }
+    }
+    function getBodySummary(body) {
+      var lines = body.split('\n');
+      var isInAuthorHeader = true;
+      var summary = [];
+      var lineCount = 0;
+      for (var index = 0, length = lines.length; index < length; index++) {
+        var line = lines[index];
+        if (isInAuthorHeader) {
+          // '#author line is not search target'
+          if (line.match(/^#author\(/)) {
+            // Remove this line from search target
+            continue;
+          } else if (line.match(/^#freeze(\W|$)/)) {
+            continue;
+            // Still in header
+          } else {
+            // Already in body
+            isInAuthorHeader = false;
+          }
+        }
+        line = line.replace(/^\s+|\s+$/g, '');
+        if (line.length === 0) continue; // Empty line
+        if (line.match(/^#\w+/)) continue; // Block-type plugin
+        if (line.match(/^\/\//)) continue; // Comment
+        if (line.substr(0, 1) === '*') {
+          line = line.replace(/\s*\[\#\w+\]$/, ''); // Remove anchor
+        }
+        summary.push(line);
+        if (summary.length >= 10) {
+          continue;
+        }
+      }
+      return summary.join(' ').substring(0, 150);
+    }
+    function removeEncodeHint() {
+      // Remove 'encode_hint' if site charset is UTF-8
+      var props = getSiteProps();
+      if (!props.is_utf8) return;
+      var forms = document.querySelectorAll('form');
+      forEach(forms, function(form){
+        if (form.cmd && form.cmd.value === 'search2') {
+          if (form.encode_hint && (typeof form.encode_hint.removeAttribute === 'function')) {
+            form.encode_hint.removeAttribute('name');
+          }
+        }
+      });
+    }
+    function kickFirstSearch() {
+      var form = document.querySelector('._plugin_search2_form');
+      var searchText = form && form.q;
+      if (!searchText) return;
+      if (searchText && searchText.value) {
+        var e = document.querySelector('#_plugin_search2_msg_searching');
+        var msg = e && e.value || 'Searching...';
+        setSearchStatus(msg);
+        var base = '';
+        forEach(form.querySelectorAll('input[name="base"]'), function(radio){
+          if (radio.checked) base = radio.value;
+        });
+        doSearch(searchText.value, {base: base}, 0);
+      }
+    }
+    function setSearchStatus(statusText) {
+      var statusObj = document.querySelector('#_plugin_search2_search_status');
+      if (statusObj) {
+        statusObj.textContent = statusText;
+      }
+    }
+    function forEach(nodeList, func) {
+      if (nodeList.forEach) {
+        nodeList.forEach(func);
+      } else {
+        for (var i = 0, n = nodeList.length; i < n; i++) {
+          func(nodeList[i], i);
+        }
+      }
+    }
+    function replaceSearchWithSearch2() {
+      forEach(document.querySelectorAll('form'), function(f){
+        if (f.action.match(/cmd=search$/)) {
+          f.addEventListener('submit', function(e) {
+            var q = e.target.word.value;
+            var base = '';
+            forEach(f.querySelectorAll('input[name="base"]'), function(radio){
+              if (radio.checked) base = radio.value;
+            });
+            var props = getSiteProps();
+            var loc = document.location;
+            var baseUri = loc.protocol + '//' + loc.host + loc.pathname;
+            if (props.base_uri_pathname) {
+              baseUri = props.base_uri_pathname;
+            }
+            var url = baseUri + '?' +
+              (props.is_utf8 ? '' : 'encode_hint=' +
+                encodeURIComponent('\u3077') + '&') +
+              'cmd=search2' +
+              '&q=' + encodeSearchText(q) +
+              (base ? '&base=' + encodeURIComponent(base) : '');
+            e.preventDefault();
+            setTimeout(function() {
+              location.href = url;
+            }, 1);
+            return false;
+          });
+          var radios = f.querySelectorAll('input[type="radio"][name="type"]');
+          forEach(radios, function(radio){
+            if (radio.value === 'AND') {
+              radio.addEventListener('click', onAndRadioClick);
+            } else if (radio.value === 'OR') {
+              radio.addEventListener('click', onOrRadioClick);
+            }
+          });
+          function onAndRadioClick(e) {
+            var sp = removeSearchOperators(f.word.value).split(/\s+/);
+            var newText = sp.join(' ');
+            if (f.word.value !== newText) {
+              f.word.value = newText;
+            }
+          }
+          function onOrRadioClick(e) {
+            var sp = removeSearchOperators(f.word.value).split(/\s+/);
+            var newText = sp.join(' OR ');
+            if (f.word.value !== newText) {
+              f.word.value = newText;
+            }
+          }
+        }
+      });
+    }
+    function encodeSearchText(q) {
+      var sp = q.split(/\s+/);
+      for (var i = 0; i < sp.length; i++) {
+        sp[i] = encodeURIComponent(sp[i]);
+      }
+      return sp.join('+');
+    }
+    function encodeSearchTextForHash(q) {
+      var sp = q.split(/\s+/);
+      return sp.join('+');
+    }
+    function findAndDecorateText(text, searchRegex) {
+      var isReplaced = false;
+      var lastIndex = 0;
+      var m;
+      var decorated = '';
+      searchRegex.lastIndex = 0;
+      while ((m = searchRegex.exec(text)) !== null) {
+        isReplaced = true;
+        var pre = text.substring(lastIndex, m.index);
+        decorated += escapeHTML(pre);
+        for (var i = 1; i < m.length; i++) {
+          if (m[i]) {
+            decorated += '<strong class="word' + (i - 1) + '">' + escapeHTML(m[i]) + '</strong>'
+          }
+        }
+        lastIndex = searchRegex.lastIndex;
+      }
+      if (isReplaced) {
+        decorated += escapeHTML(text.substr(lastIndex));
+        return decorated;
+      }
+      return null;
+    }
+    function getSearchTextInLocationHash() {
+      // TODO Cross browser
+      var hash = location.hash;
+      if (!hash) return '';
+      var q = '';
+      if (hash.substr(0, 3) === '#q=') {
+        q = hash.substr(3).replace(/\+/g, ' ');
+      } else if (hash.substr(0, 6) === '#encq=') {
+        q = decodeURIComponent(hash.substr(6).replace(/\+/g, ' '));
+      }
+      return q;
+    }
+    function colorSearchTextInBody() {
+      var searchText = getSearchTextInLocationHash();
+      if (!searchText) return;
+      var searchRegex = textToRegex(removeSearchOperators(searchText));
+      var headReText = '([\\s\\b]|^)';
+      var tailReText = '\\b';
+      var ignoreTags = ['INPUT', 'TEXTAREA', 'BUTTON',
+        'SCRIPT', 'FRAME', 'IFRAME'];
+      function colorSearchText(element, searchRegex) {
+        var decorated = findAndDecorateText(element.nodeValue, searchRegex);
+        if (decorated) {
+          var span = document.createElement('span');
+          span.innerHTML = decorated;
+          element.parentNode.replaceChild(span, element);
+        }
+      }
+      function walkElement(element) {
+        var e = element.firstChild;
+        while (e) {
+          if (e.nodeType == 3 && e.nodeValue &&
+              e.nodeValue.length >= 2 && /\S/.test(e.nodeValue)) {
+            var next = e.nextSibling;
+            colorSearchText(e, searchRegex);
+            e = next;
+          } else {
+            if (e.nodeType == 1 && ignoreTags.indexOf(e.tagName) == -1) {
+              walkElement(e);
+            }
+            e = e.nextSibling;
+          }
+        }
+      }
+      var target = document.getElementById('body');
+      walkElement(target);
+    }
+    function showNoSupportMessage() {
+      var pList = document.getElementsByClassName('_plugin_search2_nosupport_message');
+      for (var i = 0; i < pList.length; i++) {
+        var p = pList[i];
+        p.style.display = 'block';
+      }
+    }
+    function isEnabledFetchFunctions() {
+      if (window.fetch && document.querySelector && window.JSON) {
+        return true;
+      }
+      return false;
+    }
+    function isEnableServerFunctions() {
+      var props = getSiteProps();
+      if (props.json_enabled) return true;
+      return false;
+    }
+    function getSiteProps() {
+      var empty = {};
+      var propsDiv = document.getElementById('pukiwiki-site-properties');
+      if (!propsDiv) return empty;
+      var jsonE = propsDiv.querySelector('div[data-key="site-props"]');
+      if (!jsonE) return emptry;
+      var props = JSON.parse(jsonE.getAttribute('data-value'));
+      return props || empty;
+    }
+    colorSearchTextInBody();
+    if (! isEnabledFetchFunctions()) {
+      showNoSupportMessage();
+      return;
+    }
+    if (! isEnableServerFunctions()) return;
+    replaceSearchWithSearch2();
+    hookSearch2();
+    removeEncodeHint();
+    kickFirstSearch();
+  }
+  enableSearch2();
+});
index cb4c799..bbd2f09 100644 (file)
@@ -517,6 +517,21 @@ tr.bugtrack_state_undef td {
        background-color: #ff3333;
 }
 
+/* search2.inc.php  */
+input#_plugin_search2_detail:checked ~ ul > div.search-result-detail {
+       display: block;
+}
+input#_plugin_search2_detail ~ ul > div.search-result-detail {
+       display: none;
+}
+.search-result-page-summary {
+       font-size: 70%;
+       color: gray;
+       overflow: hidden;
+       text-overflow: ellipsis;
+       white-space: nowrap;
+}
+
 @media print {
   img#logo,
   div#navigator,