2015年2月24日火曜日

TiddlyWikiの検索プラグインをカスタマイズ

この投稿では TiddlyWiki classic (TWc)のプラグインのひとつ 「SimpleSearchPlugin」について書いています。

SimpleSearchPlugin

TiddlyWiki に標準で組み込まれている検索機能は、マッチした tiddler を全て表示するタイプなので検索結果によっては tiddler の洪水になってしまうことがありました。この SimpleSearchPlugin では、そんな心配はありません。

SimpleSearchPlugin は検索結果を「リスト形式」で表示してくれる便利なプラグインです。検索結果リストの tiddler を全て一括して開くこともできますが、選択した tiddler だけを開くことができるのが大きな特長です。マッチした結果リストから、どの tiddler を開くべきなのかは全てユーザーの選択に委ねられています。検索結果は 「ほぼタイトル順」 に表示されます。(「ほぼタイトル順」については後述します)

既に1年ほど、このプラグインを使っていますが、ここ最近の個人的なニーズでは検索結果リストの並び順は時系列のほうが使いやすいように感じています。特にマッチ件数が多い場合はなおさらのことです。そこで今回は検索結果リストの並び順を tiddler の変更日順に修正することにしました。



SimpleSearchPlugin の動作概要

  1. SimpleSearchPlugin は、前半に検索結果を表示する UI と css 部分、後半に標準の検索機能をオーバーライドする関数で構成されています。これによって標準の検索機能は、新しい検索機能に書き換えられます。
  2. TiddlyWiki の検索機能は、標準の組み込みマクロの search ハンドラ がユーザーからの入力を受け取った後、引数に検索文字列と検索オプションをセットして Story.search() を呼び出します。
  3. オーバーライドされた Story.prototype.search() と TiddlyWiki.prototype.search() によって、新しい検索機能で処理された検索結果リストを画面に出力します。


はじめに Story.search() から見ていきます

Story.search() は、引数に検索文字列と検索オプションを受け取り、マッチした結果のリストを画面に表示します。

Story って何?
story オブジェクトは Story クラスのインスタンスです。tiddler の表示全般を受け持つオブジェクトです。

Story.prototype.search = function(text, useCaseSensitive, useRegExp) {}

引数

  • text : (string) 検索文字列。
  • useCaseSensitive : (boolean) 検索で大文字小文字を区別。
  • useRegExp : (boolean) 正規表現で検索。

Story.search() は、検索オプションに従って予め検索文字列を加工してから store.search() に渡します。このとき、ソート項目を指定する引数 sortField は null が設定されています。

    var matches = store.search(highlightHack, null, "excludeSearch");

Story.search() の最後で、マッチした検索結果のリストを displayResults() に渡して画面に表示します。



つづいて store.search() を見ていきます

store.search() は、引数に検索文字列の正規表現オブジェクトと検索オプションを受け取り、マッチした tiddler オブジェクトの配列を返す関数で、このプラグインの主役です。

store って何?
store オブジェクトは TiddlyWiki クラスのインスタンスで、tiddler の作成や削除、保存や更新などの tiddler の操作全般を受け持つオブジェクトです。store オブジェクトの実体は HTML の非表示 DIV 要素です。

TiddlyWiki.prototype.search = function(searchRegExp, sortField, excludeTag, match) {}

引数

  • searchRegExp : (string) 検索文字列。
  • sortField : (string) ソート対象のフィールド名( tiddler オブジェクトのプロパティ)を指定。
  • excludeTag : (string) 検索結果から除外するタグ名を指定。
  • match : (boolean) 検索条件にマッチするものを含めるか、又は除外するかを指定。


対象 tiddler を抽出

store.search() は、最初に reverseLookup() を実行しています。このとき実際に渡される引数を整理すると、このようになります。

  var candidates = this.reverseLookup("tags", "excludeSearch", false);

この場合 reverseLookup() は「"tags" フィールドの値が "excludeSearch" にマッチしない」 tiddler を全て検索して、その結果を tiddler オブジェクトの配列で返します。このとき、引数 sortField を指定しない場合は、検索結果をタイトル順にソートして返されます。また、タグ名 "excludeSearch" はスペシャルタグのひとつで、検索結果から除外するタグ名として TiddlyWiki で設定されています。

言い換えると reverseLookup() は「検索除外に設定されていない tiddler 全て」を、タイトル順にソート済みの tiddler オブジェクトの配列で返します。


抽出結果を検索

次の search 部分ではオブジェクト配列の中から、「タイトル名」、「タグ名」、「本文テキスト」の各プロパティの値を検索して、各々の検索結果を収集した後で全てを結合しています。ここでは標準の検索機能に無かった「タグ名」が、新たに検索対象に加えられていることがわかります。

  var results = primary.concat(secondary).concat(tertiary);

検索結果をタイトル順にソート

if(sortField) {
  results.sort(function(a, b) {
    return a[sortField] < b[sortField] ? -1 : (a[sortField] == b[sortField] ? 0 : +1);
  });
}

最後に、検索結果を格納したオブジェクト配列を Story.search() に返す。


おおまかに、このような流れで処理が行われていることがわかります。

デバッガでコードをトレースしていて分かったのですが、どうやらタイトル順にソートするところで失敗しているようです。 と言うのも、引数の sortField は null で呼び出されているので、
if(sortField) {}if(null) {} となります。
したがってソートが実行されずにそのまま通過しているようです。その結果、「タイトル名」、「タグ名」、「本文のテキスト」のそれぞれの検索結果が存在する場合、検索結果リストは 「それぞれの、タイトル順」 で表示されることになります。

いままで「検索結果の一覧の並び順、なんか変だな?」と感じながら使っていたのですが、ようやく理由が分かった気がします。



検索結果の一覧を、変更日順に修正する

修正箇所がわかったので、ソート部分のコードを少し修正して 「tiddler の変更日順」 にしてみました。合わせて検索結果のリスト表示を修正して、変更日付を表示するようにしました。 tiddler オブジェクトのソートは store.sortTiddlers() を使っています。この関数は与えられた tiddler オブジェクトの配列を、指定した field でソートして返してくれます。


修正箇所は以下のとおりです

  1. displayResults()
    • 変更日を先頭に追加。
  2. TiddlyWiki.prototype.search()
    • 機能していないソート部分のコードをコメントアウト
    • ソート項目を指定する sortField の値を追加。
    • リストの内容を sortTiddlers() を使って並べ替え。

修正部分のコード

displayResults() の変更と追加

  displayResults: function(matches, query) {


    ......................


    if(matches.length > 0) {
      msg += "''" + config.macros.search.successMsg.format([matches.length.toString(), query]) + ":''\n";
      this.results = [];
      for(var i = 0 ; i < matches.length; i++) {
        this.results.push(matches[i].title);
  // add Date for results list
        msg += "* " + matches[i].modified.formatString("[ YYYY-0MM-0DD ]");
        msg += "  [[" + matches[i].title + "]]\n";
      }
    } else {
      msg += "''" + config.macros.search.failureMsg.format([query]) + "''"; // XXX: do not use bold here!?
    }


    ......................


    }
  },

TiddlyWiki.prototype.search() の変更と追加

  // override TiddlyWiki.search() to sort by relevance
  TiddlyWiki.prototype.search = function(searchRegExp, sortField, excludeTag, match) {


    ......................


    var results = primary.concat(secondary).concat(tertiary);

  // comment out sort function
  //  if(sortField) {
  //    results.sort(function(a, b) {
  //      return a[sortField] < b[sortField] ? -1 : (a[sortField] == b[sortField] ? 0 : +1);
  //    });
  //  }

  // add sortField, sortTiddlers
    sortField = "-modified";
    this.sortTiddlers(results,sortField);

    return results;
  };


一覧のソート項目や並び順を変更するには

sortField に "-modified" を指定すると、tiddler の変更日の新しい順にソートできます。tiddler の変更日の古い順にソートする場合は、マイナス記号の無い "modified" を指定します。文字の先頭に "-" をつけると降順になります。

他に、tiddler の作成日でソートする場合は "created" を、タイトル順なら "title" を指定できます。



修正済みのプラグイン全体のコード

https://icm7216.github.io/MyTiddlyWiki/#SimpleSearchPluginでも確認できます。



さいごに

TiddlyWiki を使い始めた頃は tiddler?, story?, store? と、良くわからない事がたくさんありましたが、ソースコードを眺めているうちに少しずつ TiddlyWiki World の謎が解けていくのが面白く感じています。TiddlyWiki の中は HTML, CSS, JavaScript で構成されているのでエディタで直接編集することも出来たり、Firefox の開発ツールで 「ごにょごにょ」 すれば、なんとか 「痒いところに手が届く」 のも気に入っている部分です。

0 件のコメント :