X-0032 DOM3 XPathをノードの検索に活用する

DOM3 XPathを使うと何が便利?

MozillaはXPath関係の機能をDOM3 XPathに則って実装しています。これを活用すれば、getElementsByTagName()などのメソッドと条件分岐を駆使してノードを収集しなくても、複雑な条件でノードを検索することができます。

以下に一例を示しましょう。

  • <menuitem label="リンクをタブで開く" hidden="true"/>
  • <menuitem label="リンクをウィンドウで開く" hidden="true"/>
  • <menuseparator>
  • <menu label="選択範囲を送る"> <menupopup>
    • <menuitem label="Google検索"/>
    • <menuitem label="Yahoo!検索"/>
    • <menuseparator>
    • <menuitem label="英辞郎" hidden="true"/>
    • <menuitem label="Excite翻訳" hidden="true"/>
    </menupopup> </menu>
  • <menuseparator>
  • <menuitem label="切り取り"/>
  • <menuitem label="コピー"/>
  • <menuitem label="貼り付け"/>
  • <menuseparator>
  • <menuitem label="削除"/>
  • <menuseparator>
  • <menuitem label="すべて選択" hidden="true"/>
  • <menuseparator>
  • <menuitem label="プロパティ"/>

こういう内容のメニューがあったとします。このうち、いくつかのアイテムはhidden="true"となっているので、実際に表示されるのは、残りの強調した項目だけです。

この時、「メニューの最初や最後にあるmenuseparator」「二つ以上連続したmenuseparator」だけを効率よく非表示にする方法はないものでしょうか――という風なケースで役に立つのが、DOM3 XPathなのです。

DOM3 XPathとはどういうもの?

DOM3 XPathの仕様はW3Cでワーキングドラフトや勧告候補にもなっていますが、まだ正式な勧告にはなっていません。そのため、細かい部分が変更される可能性もあります。最新の仕様はW3C DOMのページから見ることができます。2005年5月現在でこの仕様を実装しているのは、Mozilla(FirefoxやThunderbird、XULRunnerなども含む)のみです。

DOM3 XPathは、XPathの式で指定された条件を満たすノードを取得するためのインターフェースを提供します。使い方の解説はHawk's W3 Laboratoryの「DOMとXPathの連携」が詳しいので、そちらを参照してください。一つのXPath式で様々な条件を指定し、それを元にDOM3 XPathでノードを取得すれば、何度もループを使ったりしなくても、効率的にノードを処理することができます。

実際に使うには?

Hawk's W3 Laboratoryの「DOMとXPathの連携」の解説に従って、僕は以下のような関数を定義して使っています。この関数は、XPath式の文字列とコンテキストノードを渡すと、それを元にXPath式の表すノードを検索して、配列として返すというものです。


function getNodesFromXPath(aXPath, aContextNode) 
{
  const XULNS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
  const XHTMLNS = 'http://www.w3.org/1999/xhtml';
  const XLinkNS = 'http://www.w3.org/1999/xlink';

  // 引数の型チェック。
  if (aXPath) {
    aXPath = String(aXPath);
  }
  else {
    throw 'ERROR: blank XPath expression';
  }
  if (aContextNode) {
    try {
      if (!(gContextNode instanceof Node))
        throw '';
    }
    catch(e) {
      throw 'ERROR: invalid context node';
    }
  }

  const xmlDoc  = aContextNode ? aContextNode.ownerDocument : document ;
  const context = aContextNode || xmlDoc.documentElement;
  const type    = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE;
  const resolver = {
    lookupNamespaceURI : function(aPrefix)
    {
      switch (aPrefix)
      {
        case 'xul':
          return XULNS;
        case 'html':
        case 'xhtml':
          return XHTMLNS;
        case 'xlink':
          return XLinkNS;
        default:
          return '';
      }
    }
  };

  try {
    var expression = xmlDoc.createExpression(aXPath, resolver);
    var result = expression.evaluate(context, type, null);
  }
  catch(e) {
    return {
      snapshotLength : 0,
      snapshotItem : function()
      {
        return null;
      }
    };
  }

  return result;
}

または、以下のようにも書けます。


function getNodesFromXPath(aXPath, aContextNode) 
{
  const XULNS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
  const XHTMLNS = 'http://www.w3.org/1999/xhtml';
  const XLinkNS = 'http://www.w3.org/1999/xlink';

  // 引数の型チェック。
  if (aXPath) {
    aXPath = String(aXPath);
  }
  else {
    throw 'ERROR: blank XPath expression';
  }
  if (aContextNode) {
    try {
      if (!(gContextNode instanceof Node))
        throw '';
    }
    catch(e) {
      throw 'ERROR: invalid context node';
    }
  }

  const xmlDoc   = aContextNode ? aContextNode.ownerDocument : document ;
  const context  = aContextNode || xmlDoc.documentElement;
  const type     = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE;
  const resolver = {
    lookupNamespaceURI : function(aPrefix)
    {
      switch (aPrefix)
      {
        case 'xul':
          return XULNS;
        case 'html':
        case 'xhtml':
          return XHTMLNS;
        case 'xlink':
          return XLinkNS;
        default:
          return '';
      }
    }
  };

  try {
    var result = xmlDoc.evaluate(aXPath, context, resolver, type, null);
  }
  catch(e) {
    return {
      snapshotLength : 0,
      snapshotItem : function()
      {
        return null;
      }
    };
  }

  return result;
}

XPathNSResolverを得るのにXMLDocument.createNSResolver()を使っていませんが、これは、名前空間と接頭辞の関係を保持しているノードによって、その対応が変わってしまう恐れがあるためです。

これはどういう事かというと、例えば「child::html:p」という式を渡した場合に、<html:html xmlns:html="http://www.w3.org/1999/xhtml"/> という風な要素ノードを元にcreateNSResolverで生成したXPathNSResolverと、<html:original xmlns:html="http://piro.srakura.ne.jp/namespace"/> という風な要素ノードを元にcreateNSResolverで生成したXPathNSResolverとでは、「html」という接頭辞に対して返す名前空間URIが変わってしまうということです。

これに対して、前述のコードの例のように、自分でXPathNSResolverに相当するオブジェクトを用意しておけば、このXPathNSResolverは、「xul」と書けば必ずXULの名前空間URIを、「html」を書けば必ずXHTMLの名前空間URIを返してくれます。これによって、安心して「決め打ち」でXPath式を記述することができます。

これを利用した具体例については、次のページあるいは他のTipsで紹介したいと思います。