2 * FeedBlog SearchScript
4 * @copyright 2013 FeedBlog Project (http://sourceforge.jp/projects/feedblog/)
5 * @author Kureha Hisame (http://lunardial.sakura.ne.jp/) & Yui Naruse (http://airemix.com/)
19 // ログのリストが書かれたXMLのファイルパス
26 * XMLファイルから読み込んだファイルのバリデートモード
27 * 0 = 改行コード部分に<br/>を挿入
28 * 1 = 改行コード部分に<br/>を挿入しない
35 // fetchEntries 用のセマフォ
36 var fetchEntriesSemaphore = new Semaphore();
39 var currentSearchWords;
41 // ログのファイルリストを格納するグローバル変数です
44 // コンボボックスのオブジェクトを格納するグローバル変数です
47 // URL末尾用文字列(スクリプトを開いた瞬間のミリ秒を記録)
51 * 記事を実際に生成します。この部分を編集することでデザインを変更可能です。
52 * @param {Entry} entry 記事の情報が代入されたEntryオブジェクト
53 * @param {String} drawitem 「本文」を描画すべきパネルのDIV要素のid
54 * @param {String} renderto 「タイトル・更新日時・本文」の1日分の記事データを最終的に描画すべきパネルのDIV要素のid
55 * @param {String} closed (Ext jsパネルオプション)記事をクローズ状態で生成するか否か
57 function generatePanel(entry, drawitem, renderto, closed) {
59 if ( typeof ($("#" + renderto).feedblog_contents_plugin) == "function") {
60 $("#" + renderto).feedblog_contents_plugin({
69 var feedblogContentId = "" + renderto + "_content_div";
72 $("#" + drawitem).html(entry.content);
74 // ヘッダパネルを生成 class= .feedblog_header
75 htmlBuffer.push("<div class='feedblog_header' onclick='closePanel(\"" + feedblogContentId + "\")'><span>" + entry.title + "</span></div>" +
77 // 本体記事を作成 class= .feedblog_content
78 "<div class='feedblog_content' id='" + feedblogContentId + "'><span>" + document.getElementById(renderto).innerHTML + "</span></div>");
81 $("#" + renderto).html(htmlBuffer.join(""));
85 * システム表示画面を実際に生成します。この部分を編集することでデザインを変更可能です。
86 * @param {Entry} entry 記事の情報が代入されたEntryオブジェクト
87 * @param {String} drawitem パネルの本文を格納したDIV要素のid
88 * @param {String} renderto 「タイトル・更新日時・本文」の1日分の記事データを焼き付けるDIV要素のid
89 * @param {String} closed (Ext jsパネルオプション)記事をクローズ状態で生成するか否か
91 function generateSystemPanel(entry, drawitem, renderto, closed) {
96 var feedblogContentId = "" + renderto + "_content_div";
99 $("#" + drawitem).html(entry.content);
101 // ヘッダパネルを生成 class= .feedblog_header
102 htmlBuffer.push("<div class='feedblog_header' onclick='closePanel(\"" + feedblogContentId + "\")'><span>" + entry.title + "</span></div>" +
104 // 本体記事を作成 class= .feedblog_content
105 "<div class='feedblog_content' id='" + feedblogContentId + "'><span>" + document.getElementById(renderto).innerHTML + "</span></div>");
107 $("#" + renderto).html(htmlBuffer.join(""));
111 * 検索フォーム及び結果表示フォームを生成するメソッドです。
113 function generateForm() {
115 formBuffer.push("<form class='feedblog_searchform' onsubmit='javascript: searchDiary(); return false;'>▼ 検索語句を入力してください (語句を半角で区切るとAND、|で区切るとORで検索します)<br/>");
116 formBuffer.push("<input type='text' id='feedblog_searchword' class='feedblog_searchword'><input type='submit' id='feedblog_execsearch' value='検索'><br/>");
117 formBuffer.push("<input type='checkbox' id='feedblog_regexpOptionI' class='feedblog_regexpOptionI' checked='checked'/><label class='feedblog_searchform' for='feedblog_regexpOptionI'>大文字、小文字を区別しない</label><br/>");
118 formBuffer.push("<br/>");
119 formBuffer.push("▼ 検索対象ログ選択<br/>");
120 formBuffer.push("<div id='feedblog_logselecter'></div>");
121 formBuffer.push("<input type='checkbox' id='feedblog_allsearchcheck' class='feedblog_allsearchcheck' checked='checked'/>");
122 formBuffer.push("<label class='feedblog_searchform' for='feedblog_allsearchcheck'>すべてのログに対して検索を行う</label>");
123 formBuffer.push("<br/>");
124 formBuffer.push("</form>");
125 $("#feedblog_searchform").html(formBuffer.join(""));
127 var resultAreaBuffer = "<div class='feedblog_result_status'></div>";
129 $("#feedblog_resultwritearea").html(resultAreaBuffer);
135 function initialize() {
136 // 初期値をhiddenパラメータより読み込みます
137 mainPageUrl = $("#feedblog_mainpageurl").val();
138 searchPageUrl = $("#feedblog_searchpageurl").val();
139 latestXml = $("#feedblog_latestxml").val();
140 logXmlUrl = $("#feedblog_loglistxmlurl").val();
141 showLength = parseInt($("#feedblog_showlength").val());
142 if (isNaN(showLength)) {
145 validateMode = $("#feedblog_validatemode").val();
148 urlSuffix = +new Date();
153 if (mainPageUrl === undefined) {
154 errorBuf.push("設定値「feedblog_mainpageurl」が欠落しています。");
156 if (searchPageUrl === undefined) {
157 errorBuf.push("設定値「feedblog_searchpageurl」が欠落しています。");
159 if (latestXml === undefined) {
160 errorBuf.push("設定値「feedblog_latestxml」が欠落しています。");
162 if (logXmlUrl === undefined) {
163 errorBuf.push("設定値「feedblog_loglistxmlurl」が欠落しています。");
165 if (showLength === undefined) {
166 errorBuf.push("設定値「feedblog_showlength」が欠落しています。");
168 if (validateMode === undefined) {
169 errorBuf.push("設定値「feedblog_validatemode」が欠落しています。");
173 if ( typeof (CryptoJS.SHA1) != "function") {
174 errorBuf.push("crypt-jsモジュール(hmac-sha1.js)が読み込まれていません。");
177 errorBuf.push("crypt-jsモジュール(hmac-sha1.js)が読み込まれていません。");
181 if ($("#feedblog_searchform").length == 0) {
182 errorBuf.push("描画エリア「feedblog_searchform」が存在しません。");
184 if ($("#feedblog_resultwritearea").length == 0) {
185 errorBuf.push("描画エリア「feedblog_resultwritearea」が存在しません。");
187 if ($("#feedblog_writearea").length == 0) {
188 errorBuf.push("描画エリア「feedblog_writearea」が存在しません。");
191 // エラーがある場合は以降の処理を継続しない
192 if (errorBuf.length > 0) {
193 alert("初期設定値に誤りがあります。\n詳細:\n" + errorBuf.join("\n"));
203 $(document).ready(function() {
217 * jQueryでのパネル開閉を制御します
219 function closePanel(id) {
220 $("#" + id).slideToggle();
225 * @param {Object} obj entry 要素の DOM オブジェクト
227 function Entry(obj) {
228 this.title = $("title:first", obj).text();
229 if (this.title == "")
230 requiredElementError(obj, "title");
231 this.title = validateText(this.title);
232 this.content = $("content:first", obj).text();
233 this.content = validateText(this.content);
234 this.id = $("id:first", obj).text();
236 requiredElementError(obj, "id");
237 this.date = $("updated:first", obj).text();
239 requiredElementError(obj, "updated");
240 this.date = validateData(this.date);
241 this.category = $("category", obj);
246 * @param {Object} obj entry 要素の DOM オブジェクト
248 function SystemEntry(obj) {
249 this.title = $("title:first", obj).text();
250 this.title = validateText(this.title);
251 this.content = $("content:first", obj).text();
252 this.content = validateText(this.content);
253 this.id = $("id:first", obj).text();
254 this.date = $("updated:first", obj).text();
255 this.date = validateData(this.date);
256 this.category = $("category", obj);
261 * @param {Array} keywords 単語群
262 * @param {String} regexpType 正規表現の検索モードを示す文字列
263 * @return {boolean} bool 全て含んでいれば true、さもなくば false
265 Entry.prototype.hasKeywords = function(keywords, regexpType) {
266 // 正規表現が一致するかという判定"のみ"を行います
267 for (var i = 0; i < keywords.length; i++) {
268 // 正規表現チェック用のオブジェクトを用意します(OR条件は一時的に条件を置換)
269 var reg = new RegExp('(?:' + keywords[i] + ')(?![^<>]*>)', regexpType);
271 if (!reg.test(this.content) && !reg.test(this.title))
278 * 呼び出すとDIV:id名:feedblog_writearea上のHTMLを削除し、ロードエフェクトを表示します
280 function loadingEffect() {
281 $("#feedblog_writearea").html('<div id="feedblog_drawpanel" class="feedblog_drawpanel"><div id="feedblog_drawitem" class="feedblog_drawitem"> <\/div><\/div>');
284 var systemEntry = new SystemEntry();
285 systemEntry.title = "Now Loading .....";
286 systemEntry.content = '<br/>長時間画面が切り替わらない場合はページをリロードしてください。<br/><br/>';
287 generateSystemPanel(systemEntry, "feedblog_drawitem", "feedblog_drawpanel", false);
290 $("#feedblog_resultwritearea").html("<div class='feedblog_result_status'></div>");
296 function showErrorEffect() {
297 $("#feedblog_writearea").html('<div id="feedblog_drawpanel" class="feedblog_drawpanel"><div id="feedblog_drawitem" class="feedblog_drawitem"> <\/div><\/div>');
300 var systemEntry = new SystemEntry();
301 systemEntry.title = "エラー";
302 var errorContent = [];
303 errorContent.push('<br/>記事ファイル(XML)の取得に失敗しました。以下のような原因が考えられます。<br/><br/>');
304 errorContent.push('・設定値「feedblog_latestxml」に正しいパスが設定されていない。<br/>');
305 errorContent.push('・設定値「feedblog_loglistxmlurl」に正しいパスが設定されていない。<br/>');
306 errorContent.push('・ローカル環境で起動している(必ずサーバにアップロードして実行してください)。<br/>');
307 errorContent.push('<br/>');
308 systemEntry.content = errorContent.join("\n");
309 generateSystemPanel(systemEntry, "feedblog_drawitem", "feedblog_drawpanel", false);
312 $("#feedblog_resultwritearea").html("<div class='feedblog_result_status'></div>");
318 function notFoundErrorEffect() {
319 $("#feedblog_writearea").html('<div id="feedblog_drawpanel" class="feedblog_drawpanel"><div id="feedblog_drawitem" class="feedblog_drawitem"> <\/div><\/div>');
320 $("#feedblog_drawitem").html('<br/>検索条件に一致する記事は見つかりませんでした。<br/>或いはキャッシュが残っている可能性があります。ブラウザの「更新」ボタンを押して下さい。<br/><br/>');
323 var systemEntry = new SystemEntry();
324 systemEntry.title = "検索結果";
325 systemEntry.content = '<br/>検索条件に一致する記事は見つかりませんでした。<br/><br/>';
326 generateSystemPanel(systemEntry, "feedblog_drawitem", "feedblog_drawpanel", false);
329 $("#feedblog_resultwritearea").html("<div class='feedblog_result_status'></div>");
335 function requiredElementError(parent, name) {
336 alert(parent.ownerDocument.URL + ": 必須な要素 " + name + " が存在しないか空な " + parent.tagName + " 要素が存在します");
340 * 日付のHTML表示用バリデーション処理を行います
341 * @param {String} data RFC3339形式のdate-time文字列
343 function validateData(data) {
344 data = data.replace(/T/g, " ");
347 data = data.substring(0, 19);
353 * 記事本文のバリデーション処理を行います
354 * @param {String} contents 記事の本文が格納されている文字列
356 function validateText(contents) {
358 if (validateMode == 0) {
359 contents = contents.replace(/[\n\r]|\r\n/g, "<br />");
367 * @param {String} str エスケープを行いたい文字列
369 function xmlAttrContentEscape(str) {
370 // return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
371 return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/^[ ]+/mg, " ").replace(/^[\t]+/mg, "");
376 * @param {String} contents 記事の本文が格納されている文字列
377 * @param {String} id 記事の初公開日を示す日付文字列
379 function contentsWithid(contents, id) {
381 var hashTag = '<br/><div class="feedblog_content_footer"><a href="' + xmlAttrContentEscape(mainPageUrl) + '#' + xmlAttrContentEscape(id) + '" target="_blank">- この日の記事にリンクする -<\/a><\/div>';
382 return contents + hashTag;
387 * @param {String} word 強調したい語句
389 function emphasizeWord(word) {
390 return '<span style="background-color: red;">' + word + '</span>';
395 * @param {String} a 比較対象(1)
396 * @param {String} b 比較対象(2)
398 function compareLengthDecrease(a, b) {
401 return a > b ? -1 : a < b ? 1 : 0;
407 function Semaphore() {
417 Semaphore.prototype.init = function() {
418 while (this.xhrs.length > 0) {
419 this.xhrs.shift().abort();
421 this.id = Math.random();
427 * ログファイル選択用のコンボボックスをid名:feedblog_logselecterに生成します
429 function logXMLLoader() {
432 url : logXmlUrl + '?time=' + urlSuffix,
434 error : showErrorEffect,
435 success : function(xmlData) {
436 var separateTag = xmlData.getElementsByTagName("file");
437 logData = new Array(separateTag.length);
439 // 読み込んだ要素をStoreに格納して表示
441 boxBuffer.push("<select class='feedblog_logselecter' id='feedblog_logbox'>");
442 for (var i = 0; i < separateTag.length; i++) {
443 boxBuffer.push("<option value='" + separateTag[i].getElementsByTagName("path")[0].firstChild.nodeValue + "'>" + separateTag[i].getElementsByTagName("display")[0].firstChild.nodeValue + "</option>");
444 logData[i] = separateTag[i].getElementsByTagName("path")[0].firstChild.nodeValue;
446 boxBuffer.push("</select>");
449 $("#feedblog_logselecter").html(boxBuffer.join(""));
457 function getSearchWords() {
458 var searchWord = document.getElementById("feedblog_searchword").value;
459 if (searchWord == "")
461 var searchWords = [];
465 searchWord = xmlAttrContentEscape(searchWord);
467 searchWord = searchWord.replace(/([$()*+.?\[\\\]^{}])/g, '\\$1');
469 searchWords = searchWord.replace(/^\s+|\+$/g, '').split(/\s+/);
470 // 正規表現の選択を長い順に並び替えます(AND条件)
471 searchWords.sort(compareLengthDecrease);
473 return searchWords.length == 0 ? null : searchWords;
477 * 文章内を特定の単語で検索し、一致した部分を強調表示タグで置き換えます
478 * @param {String} searchWord 探索する単語
479 * @param {String} plainText 探索を行う文章
480 * @param {String} regexpType 正規表現の検索モードを示す文字列
482 function complexEmphasize(searchWord, plainText, regexpType) {
484 searchWord = searchWord.split('|').sort(compareLengthDecrease).join('|');
485 // タグの内側でないことを確認する正規表現を追加
486 var pattern = new RegExp('(?:' + searchWord + ')(?![^<>]*>)', regexpType);
489 var currentIndex = -1;
490 // 現在マッチしている部分文字列の開始位置
491 var currentLastIndex = -1;
492 // 現在マッチしている部分文字列の、現在の末尾
495 while ( m = pattern.exec(plainText)) {
496 if (m.index > currentLastIndex) {
497 // 新しい部分文字列へのマッチが始まったので、そこまでの文字列をバッファに書き出す
498 if (currentIndex < currentLastIndex)
499 result.push(emphasizeWord(plainText.substring(currentIndex, currentLastIndex)));
500 result.push(plainText.substring(currentLastIndex, m.index));
502 currentIndex = m.index;
505 currentLastIndex = pattern.lastIndex;
506 // 次の正規表現マッチは今マッチした文字の次の文字から
507 pattern.lastIndex = m.index + 1;
510 if (currentIndex < currentLastIndex)
511 result.push(emphasizeWord(plainText.substring(currentIndex, currentLastIndex)));
512 result.push(plainText.substring(currentLastIndex));
515 return result.join('');
519 * 検索結果を分割して表示します(2回目以降呼び出し)
520 * @param {int} showLength 一回の画面に表示する記事数
521 * @param {int} startIndex 表示を開始する記事のインデックス
523 function showEntriesRange(showLength, startIndex) {
525 var entries = loadedEntries;
527 // 表示インデックスが範囲外の場合はエラーパネルを表示して終了
528 if (startIndex < 0 || entries.length <= startIndex) {
533 var stringBuffer = [];
536 var loopLimit = (showLength + startIndex > entries.length) ? entries.length : showLength + startIndex;
537 var indexShowEntries = loopLimit + 1;
539 for (var i = startIndex; i < loopLimit; i++) {
540 stringBuffer.push('<div class="feedblog_drawpanel" id="feedblog_drawpanel');
541 stringBuffer.push(i);
542 stringBuffer.push('"><div class="feedblog_drawitem" id="feedblog_drawitem');
543 stringBuffer.push(i);
544 stringBuffer.push('"><\/div><\/div>');
546 $("#feedblog_writearea").html(stringBuffer.join(''));
548 stringBuffer.length = 0;
549 for ( i = startIndex; i < loopLimit; i++) {
550 var entry = entries[i];
551 generatePanel(entry, "feedblog_drawitem" + i, "feedblog_drawpanel" + i, false);
556 menuBuffer.push("<div class='feedblog_pager_wrapper'>");
557 menuBuffer.push("<ul class='feedblog_pager'>");
560 menuBuffer.push("<li class='feedblog_pager_blank'></li>");
563 if (startIndex - showLength >= 0) {
564 menuBuffer.push("\<li class='feedblog_pager_goback'><span class='feedblog_pager_goback' onclick='showEntriesRange(" + showLength + ", " + (startIndex - showLength) + "); return false;'>\< 前の" + showLength + "件を表示</span\></li>");
566 menuBuffer.push("\<li class='feedblog_pager_goback'>\< 前の" + showLength + "件を表示</a\></li>");
570 menuBuffer.push("<li class='feedblog_pager_center'>[ ");
571 var menuNumbers = Math.ceil(entries.length / showLength);
572 for ( i = 0; i < menuNumbers; i++) {
573 if (startIndex / showLength == i) {
574 menuBuffer.push(i + " ");
576 menuBuffer.push("<span class='feedblog_pager_center' onclick='showEntriesRange(" + showLength + ", " + (i * showLength) + "); return false;'>");
578 menuBuffer.push("</span> ");
581 menuBuffer.push("]</li>");
584 if (entries.length > startIndex + showLength) {
585 menuBuffer.push("\<li class='feedblog_pager_gonext'><span class='feedblog_pager_gonext' onclick='showEntriesRange(" + showLength + ", " + (startIndex + showLength) + "); return false;'>次の" + showLength + "件を表示 \></span\></li>");
587 menuBuffer.push("\<li class='feedblog_pager_gonext'>次の" + showLength + "件を表示 \></li>");
591 menuBuffer.push("<li class='feedblog_pager_blank'></li>");
593 menuBuffer.push("</ul></div>");
596 $("#feedblog_resultwritearea").html("<div class='feedblog_result_status'>" + entries.length + "件の記事が該当しました / " + (startIndex + 1) + "~" + loopLimit + "件目までを表示中<br/></div>" + menuBuffer.join(""));
600 * 検索時のjQuery.ajaxのcallback関数
602 function fetchEntries(xmlData) {
603 // 大文字小文字を区別するかを取得します
604 var regexpOptionI = document.getElementById("feedblog_regexpOptionI");
605 var regexpType = regexpOptionI ? "ig" : "g";
608 var entries = xmlData.getElementsByTagName("entry");
611 for (var j = 0; j < entries.length; j++) {
612 var entry = new Entry(entries[j]);
614 // 正規表現が一致した場合は、強調表現処理を行います
615 if (entry.hasKeywords(currentSearchWords, regexpType)) {
617 entry.title = complexEmphasize(currentSearchWords.join("|"), entry.title, regexpType);
618 entry.content = complexEmphasize(currentSearchWords.join("|"), entry.content, regexpType);
620 fetchEntriesSemaphore.buf.push(entry);
624 // セマフォのカウンタを減少させます (Ajaxとの同期のため)
625 fetchEntriesSemaphore.count--;
628 if (fetchEntriesSemaphore.count == 0) {
629 var entries = fetchEntriesSemaphore.buf;
631 // 一軒も検索にヒットしなかった場合は専用のパネルを表示して終了
632 if (entries.length == 0) {
633 notFoundErrorEffect();
638 entries = entries.sort(function(a, b) {
641 return a > b ? -1 : a < b ? 1 : 0;
644 loadedEntries = entries;
647 showEntriesRange(showLength, 0);
652 * 「探索」ボタンを押されたときに呼び出されるメソッドです
654 function searchDiary() {
656 document.getElementById("feedblog_writearea").innerHTML = "";
659 currentSearchWords = getSearchWords();
660 if (!currentSearchWords) {
661 alert("検索対象の単語が入力されていません");
663 $("#feedblog_resultwritearea").html("<div class='feedblog_result_status'></div>");
671 var allCheckedFlag = document.getElementById("feedblog_allsearchcheck").checked;
674 fetchEntriesSemaphore.init();
675 // 記事が全検索モードか否かをチェックします
677 if (allCheckedFlag == true) {
681 // 単独記事探索なので、選んだログのURL
682 urls = [document.getElementById("feedblog_logbox").options[document.getElementById("feedblog_logbox").selectedIndex].value];
684 fetchEntriesSemaphore.urls = urls;
685 fetchEntriesSemaphore.count = urls.length;
686 for ( i = 0; i < urls.length; i++) {
687 var xhr = new jQuery.ajax({
688 url : urls[i] + '?time=' + urlSuffix,
691 success : fetchEntries,
692 error : showErrorEffect
694 fetchEntriesSemaphore.xhrs.push(xhr);