はてなブックマーク Firefox 拡張
実装の舞台裏

nanto_vi, 2009-11-08

自己紹介

概略

  1. コーディング規約
  2. スクリプトのモジュール化
  3. データベースと O/R マッパ
  4. サイドバーとカスタムツリービュー
  5. ブックマーク追加ダイアログと XBL
  6. FUEL のメモリリーク
  7. おすすめタグと本文抽出ライブラリ

コーディング規約

function FooBar() { ... }

extend(FooBar.prototype, {
    method: function FB_method() {
        let x = ...
        ...
    },
    get property FB_get_property() { ... },
    set property FB_set_property(value) { ... },
});

スクリプトのモジュール化

モジュール化の必要性

スクリプトを読み込む手段

mozIJSSubScriptLoader
コンテキストを指定することで、変数のスコープをファイル単位に制限可能
読み込むたびに評価される
JavaScript モジュール
コンテキストはモジュールファイルごとに独立
評価は一度のみ
Firefox 3 以降
JavaScript XPCOM コンポーネント
コンテキストはコンポーネントファイルごとに独立
評価は一度のみ
インターフェースを定義すれば C++ からも呼び出し可能

ローダの作成

bookmark-xul/
    chrome/
        content/
            autoloader.js
            common/
                02-utils.js
                20-database.js
                ...
            sidebar.xul
            sidebar/
                10-TagTreeView.js
                10-BookmarkTreeView.js
                ...
    resource/
        modules/
            00-utils.jsm
            10-event.jsm
            ...
// JavaScript モジュール

Components.utils.import("resource://hatenabookmark/modules/00-utils.jsm");
loadPrecedingModules();

const EXPORTED_SYMBOLS = ['MyModule'];

var MyModule = { ... };
// JavaScript ファイル

const EXPORT = ['hello', 'world'];

function hello() { ... }
var world = ...;
function mysterious() { ... }
<?xml version="1.0" encoding="utf-8"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
    <script type="application/javascript" src="autoloader.js"/>
    <script type="application/javascript"><![CDATA[
        hBookmark.MyModule;
        hBookmark.hello();
    ]]></script>

    ...
</window>

データベースと O/R マッパ

let Bookmark = Model.Entity({
    name : 'bookmarks',
    fields : {
        id           : 'INTEGER PRIMARY KEY',
        url          : 'TEXT UNIQUE NOT NULL',
        title        : 'TEXT',
        comment      : 'TEXT',
        ...
    }
});

extend(Bookmark, {
    findByTags: function(tags, limit, ascending, offset) { ... },
    search: function(str, limit, ascending, offset) { ... },
    searchByTitle: function(str, limit, ascending, offset) { ... },
    searchByUrl: function(str, limit, ascending, offset) { ... },
    searchByComment: function(str, limit, ascending, offset) { ... },
    ...
});

addAround(Bookmark, 'searchBy*', function(proceed, args, target) {
    target.db.setPragma('case_sensitive_like', 0);
    try {
        return proceed(args);
    } finally {
        target.db.setPragma('case_sensitive_like', 1);
    }
});

addAround(Bookmark.prototype, 'save', function(proceed, args, target) {
    target.search = [target.title, target.comment, target.url].join("\n").toLowerCase(); // SQLite での検索用
    proceed(args);
    target.updateTags();
    Prefs.bookmark.set('everBookmarked', true);
});

Model.Bookmark = Bookmark;
Model.MODELS.push("Bookmark");

サイドバーとカスタムツリービュー

[図 1: サイドバーのタグ一覧とブックマーク一覧]

[図 2: 関連するタグを持つ行 (+/- が先頭に付く) と持たない行]

ブックマーク追加ダイアログと XBL

[図 3: ポップアップウィンドウを使ったブックマーク追加ダイアログ]

XML Bindng Language

<?xml version="1.0" encoding="utf-8"?>
<bindings xmlns="http://www.mozilla.org/xbl"
          xmlns:xbl="http://www.mozilla.org/xbl"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
    <binding id="hello">
        <content>
            <xul:label value="Hello, XBL world!"/>
        </content>
        <implementation>
            <method name="sayHello">
                <parameter name="target"/>
                <body><![CDATA[
                    alert('Hello, ' + target + ' world!');
                ]]></body>
            </method>
        </implementation>
    </binding>
</bindings>
.hello {
    -moz-binding: url("hello.xml#hello");
}

ブックマークの追加

[図 4: ブラウザタブ下部のブックマーク追加パネル、試作段階のもの]

FUEL のメモリリーク

おすすめタグと本文抽出ライブラリ

[図 5: 他のユーザーのつけたタグの中からのおすすめタグと、自分の付けたタグの中からのおすすめタグ]

var start = d.evaluate(
    'descendant::comment()[contains(., "google_ad_section_start") and not(contains(., "ignore"))]',
    d.body, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
).singleNodeValue;