JavaScript で XTF の実装2005年09月02日 05時31分

この間は JavaScript で XPCOM コンポーネントを作ったので、今回はその発展として XTF の実装に挑戦。XTF (eXtensible Tag Framework) っていうのは XPCOM を用いて新しい XML 要素を実装できるというもので、IE でいうところの Element Behavior (日本語での要約記事) に似てる。Gecko 1.8 (Firefox 1.5) からのサポートなので現時点で試すためには nightly ビルドを使う必要あり。

といってもどんな要素を作ったものかうまく思い浮かばないのでとりあえずカレンダーを作ってみることにする。<calendar xmlns="http://www.ne.jp/asahi/nanto/moon/ns/calendar" /> とすることで今月のカレンダーが表示されるというもの。

ファクトリの作成

XTF ではまず特定の名前空間に対応する XPCOM コンポーネントをひとつ作り、要素の生成はそのコンポーネントが受け持つという形になる。デザインパターンでいうところの Factory Method ってところか。で、そのコンポーネントが実装しなくてはいけないのが nsIXTFElementFactory インターフェース

interface nsIXTFElementFactory : nsISupports
{
  nsIXTFElement createElement(in AString tagName);
};

メンバは createElement メソッドのみ。というわけでサクッと実装。

function nntCalendarFactory() {}

nntCalendarFactory.prototype = {
  createElement: function (tagName)
  {
    switch (tagName) {
      case "calendar":
        return new nntCalendarElement();

      default:
        throw Components.results.NS_ERROR_FAILURE;
    }
  },

  QueryInterface: function (iid)
  {
    if (iid.equals(Components.interfaces.nsIXTFElementFactory) ||
        iid.equals(Components.interfaces.nsISupports))
      return this;

    Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE;
    return null;
  }
};

nntCalendarElement っていうのが実際の要素に当たるオブジェクト。その解説は後回しにしてとりあえずこれを登録してみる。

const XMLNS_NNT_CALENDAR = "http://www.ne.jp/asahi/nanto/moon/ns/calendar";

var nntCalendarFactoryModule = {
  _description: "XTF Calendar Factory",
  _classId:     Components.ID("{1abb1e29-c7a7-45c7-8670-ba8919f0ae6d}"),
  _contractId:  "@mozilla.org/xtf/element-factory;1?namespace="
                  + XMLNS_NNT_CALENDAR,

  ...

  registerSelf: function (compMgr, fileSpec, location, type)
  {
    ...
    compMgr.registerFactoryLocation(this._classId,
                                    this._description,
                                    this._contractId,
                                    fileSpec,
                                    location,
                                    type);
  },

  ...
};

function NSGetModule(compMgr, fileSpec)
{
  return nntCalendarFactoryModule;
}

"@mozilla.org/xtf/element-factory;1?namespace=NamespaceURI" という Contract ID で Factory を登録することによって、<elementName xmlns="NamespaceURI"> という要素が現れたときに Factory.createElement("elementName") が呼び出されるようになるらしい。CID (Class ID?) は適宜生成

要素オブジェクトの作成

次は実際に要素を表すオブジェクトの作成。これは nsIXTFElement インターフェースを実装しなくてはいけない。

function nntCalendarElement() {}

nntCalendarElement.prototype = {
  // nsIXTFElement の実装
  elementType: Components.interfaces.nsIXTFElement.ELEMENT_TYPE_XML_VISUAL,
  isAttributeHandler: false,
  getScriptingInterfaces: function (count)
  {
    var interfaces = [Components.interfaces.nsIDOMElement,
                      Components.interfaces.nsIDOM3Node,
                      Components.interfaces.nsIDOMEventTarget];

    count.value = interfaces.length;
    return interfaces;
  },

  // nsIXTFVisual (nsIXTFXMLVisual の派生元) の実装
  visualContent: null,
  insertionPoint: null,
  applyDocumentStyleSheets: true,

  // nsIXTFXMLVisual の実装
  onCreated: function (/* nsIXTFXMLVisualWrapper */ wrapper)
  {
    ...

    var builder = Components.classes["@mozilla.org/xtf/xml-contentbuilder;1"]
                            .createInstance(
                              Components.interfaces.nsIXMLContentBuilder);
    builder.setElementNamespace("http://www.w3.org/1999/xhtml");

    builder.beginElement("table");
      ...
    builder.endElement(); // table

    this.visualContent = builder.root;
  },

  // nsISupports の実装
  QueryInterface: function (iid)
  {
    if (iid.equals(Components.interfaces.nsIXTFXMLVisual) ||
        iid.equals(Components.interfaces.nsIXTFVisual) ||
        iid.equals(Components.interfaces.nsIXTFElement) ||
        iid.equals(Components.interfaces.nsISupports))
      return this;

    Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE;
    return null;
  }
};

まずは elementType にそれがどういう種類の要素か指定する。指定できる値は以下のとおり。指定した値によって追加のインターフェースを実装する必要あり。

elementType プロパティに指定できる値
説明 インターフェース
ELEMENT_TYPE_GENERIC_ELEMENT 表示には反映されない要素を表すみたい。HTML での link 要素とか script 要素とかみたいなものか。 nsIXTFGenericElement
ELEMENT_TYPE_SVG_VISUAL SVG を使って表示する要素を表すみたい。 nsIXTFSVGVisual
ELEMENT_TYPE_XML_VISUAL 何らかの内容を表示する一般的な要素。とりあえず SVG を使わないのならこれを指定しておけばいいのか? nsIXTFXMLVisual
ELEMENT_TYPE_XUL_VISUAL XUL 要素と同等の要素? nsIXTFXULVisual
ELEMENT_TYPE_BINDABLE XBL によるバインディングが可能な要素? nsIXTFBindableElement

isAttributeHandler はとりあえず false にしておけばよさそう。true にすると nsIXTFAttributeHandler インターフェースも実装しなくてはいけないから面倒くさそう。

getScriptingInterfacesnsIDOMElement とかを返しておかないと自動的に nsIDOMElement だと認識されない。つまり document.getElementsByTagName("calendar")[0].nodeType とかやっても undefined になる。

void getScriptingInterfaces(out unsigned long count,
                            [array, size_is(count), retval] out nsIIDPtr array);

IDL を見ると out だなんて使われているのでどう実装したらいいものか悩んだが、FAQ によると out の引数にはオブジェクトが渡されるからその value プロパティに値をセットすればいいそうな。配列のほうは JavaScript の配列を自動的に変換してくれる。 retval がついているのは返り値として指定すればいいということみたい。

ここでは elementTypeELEMENT_TYPE_XML_VISUAL を指定していたので nsIXTFXMLVisual インターフェースも実装しなくてはいけない。が、その前にその派生元である nsIXTFVisual インターフェースを実装。まずは visualContent 、ここには実際に表示に使われる要素を指定する。今回は onCreated() の呼び出しの中でセットするが、それ以降は指定される要素自体を変更してはいけない。要素の属性や子要素の変更ならできる。

insertionPoint には visualContent に指定した要素か、その子孫要素を指定する。appendChild とかの実際の適用先がここに指定された要素になる。null を指定しておけば子要素を挿入しても表示されないらしい。

applyDocumentStyleSheets は多分読んで字のごとく。製作者スタイルシートを適用するかどうかだと思う。

そして nsIXTFXMLVisual インターフェースの実装。といっても onCreated メソッドしかない。要素の作成時に呼び出される。今回はここでカレンダーを表す表を作成、visualContent にセットしている。

要素の作成には XMLContentBuilder を使用。最初に名前空間 URI をセットしてから beginElement(tagName)endElement()attrib(name, value)textNode(text) などを使って内容を構築していく。

サンプル

サンプルも作成。getScriptingInterfacesnsIDOMEventTarget を返しておけば、addEventListener でイベントハンドラを登録することもできる。スクリーンショットあり。

パッケージング

そもそも Firefox 1.5 にしか対応していないのでパッケージングも非常に楽。1.0 のことなど考えず chrome.manifest が使えたりする。XPIファイル及びファイル構成は以下のとおり。XPI ファイルをインストールすることで calendar 要素が使用できるようになる。

参考文献

日本語文字と欧文文字の間のスペース2005年09月02日 14時15分

私は以前は日本語文字と欧文文字の間にスペースを入れていなかった。(メールなどテキストでしか表せない媒体ではスペースを入れることもあったが。) つまり「今日 Firefox を使った」ではなく「今日Firefoxを使った」と表記していたのだ。

なぜか。その理由は CSS 3 に text-autospace プロパティが存在したことによる。CSS で日本語文字と欧文文字 (表意文字と表音文字) の間の間隔を制御できるというのなら、それはスタイルシートによって制御すべき事柄であり、見た目の調整のためにスペースという物理的な文字を入れるべきではないのではと思ったからだ。実際 MS WordOOo Writer といったワープロソフトではこちらがスペースを入れなくても適宜日本語文字と欧文文字の間隔を調整してくれる。(もちろんこの設定を切ることもできる。)

しかし、周りを見てみると日本語文字と欧文文字の間にスペースを入れているところは多いし、text-autospace も CSS 3 Text Effect Module 20050627 WD から外れたし、IE の text-autospace の先行実装もあまり見栄えがよくなるとは思えないし、何よりもスペースを入れたほうが実際問題読みやすい。なのでこれからは日本語文字と欧文文字の間にスペースを入れていくことにする。

複数のFirefoxをバッチで同時起動2005年09月02日 16時52分

複数の Firefox を同時起動する話。私は Windows で 1.0 をメインに使っていて、複数の Firefox を同時起動するときというのは nightly をテスト目的に使うときだけなので、以下のようなバッチファイルを使っている。

@echo off
if not exist profile (
  mkdir profile
  echo // Default Settings> profile\user.js
  echo user_pref("browser.cache.disk.capacity", 5000^);>> profile\user.js
  echo user_pref("browser.link.open_newwindow", 3^);>> profile\user.js
  echo user_pref("browser.shell.checkDefaultBrowser", false^);>> profile\user.js
  echo user_pref("browser.startup.homepage", "http://www.google.co.jp/firefox"^);>> profile\user.js
  echo user_pref("browser.tabs.autoHide", false^);>> profile\user.js
  echo user_pref("dom.event.contextmenu.enabled", false^);>> profile\user.js
  echo user_pref("intl.accept_languages", "ja,en-us,en"^);>> profile\user.js
  echo.>> profile\user.js
  echo // Settings for DOM Inspector>> profile\user.js
  echo user_pref("inspector.dom.showWhitespaceNodes", false^);>> profile\user.js
  echo.>> profile\user.js
  echo // Settings for XUL Development>> profile\user.js
  echo user_pref("nglayout.debug.disable_xul_cache", true^);>> profile\user.js
  echo user_pref("javascript.options.strict", true^);>> profile\user.js
  echo user_pref("javascript.options.showInConsole", true^);>> profile\user.js
  echo user_pref("browser.dom.window.dump.enabled", true^);>> profile\user.js
)
set MOZ_NO_REMOTE=1
start firefox.exe -console -profile "profile"
set MOZ_NO_REMOTE=0

これを解凍した (nightly は ZIP 版を使っているので) Firefox のディレクトリ内におき、それを実行するだけ。if の中で echo を連続するよりも別に user.js を作っておいてそれを copy したほうがスマートだろうが、ファイルひとつで収まるのが手軽なのでこうしている。プロファイル名ではなくプロファイルディレクトリを直接指定しているのでほかのプロファイルマネージャに影響を及ぼすこともない。テスト目的なのでブックマークなどを引き継ぐ必要はないが、初期設定だけ済ませておきたいというのに便利だ。

私がこの MOZ_NO_REMOTE のセットにバッチを使う方法を見たのはどこか海外の人のブログだったと思うが、朝顔日記その意味が詳しく解説されている。MOZ_NO_REMOTE とは DDE を制御するものだそうな。

それにしても -console をつけると JavaScript コンソールに "Error: Warning: unrecognized command line flag -console" というエラーが出るのはいかがなものか。つけないとコンソールが出てこず、デバッグ用のメッセージを確認できないし。