2007年 11月 19日

XPathNSResolver のクロスブラウザとか

application/xhtml+xml の場合 (というより XML の場合)、XPath では namespace を持っているノードに対して prefix が必須 *1。すなわち HTML で //body と書いていたやつは //h:body とする必要があり、さらに h を http://www.w3.org/1999/xhtml (XHTML の NS URI) に関連付ける必要がある。

DOM 3 XPath では prefix の解決に XPathNSResolver というのを使っていて、これで prefix を関連づけてやる。

var context = document;
var resolver = function (prefix) {
    if (prefix == "h") return "http://www.w3.org/1999/xhtml";
    return null; # unknown
};
var expr = document.createExpression("//h:body", resolver);

で、これだと text/html のとき (全てのノードは名前空間が空) h:body はなにも選択しなくなってしまって悲しいので条件分岐する。

var context = document;
var resolver = function (prefix) {
    if (prefix == "h") return (document.documentElement.namespaceURI ? "http://www.w3.org/1999/xhtml" : "");
    return null; # unknown
};
var expr = document.createExpression("//h:body", resolver);

document.documentElement.namespaceURI でドキュメントが XML として扱われているかを判別してよさげなのをかえす。


基本的にはこれで十分だけど、XML として処理されるとき、dc: や rdf: とかもちゃんと選択できると嬉しい。毎回 resolver に条件を追加するのもいいけど、dc とか rdf とか、普通 prefix を変えたりしないので、こうする。

var context = document;
var resolver = function (prefix) {
    var c = document.createNSResolver(context).lookupNamespaceURI(prefix);
    if (c) return c;
    if (prefix == "h") return (document.documentElement.namespaceURI ? "http://www.w3.org/1999/xhtml" : "");
   return null; # unknown
};
var expr = document.createExpression("//h:body", resolver);

createNSResolver は Node をあたえると、そのノードが持っている名前 prefix と URI の対応をつかってリゾルバを作る。下位のノードは上位のノードの prefix/URI マッピングも持っている (というかスコープにある prefix は全部とってくる) ので、できるだけ下位のノードをわたす。

だいたい汎用的に使える resolver になった。ただ、xhtml の接頭辞に h を使うかはいまいち同意がとれないので、lookupNamespaceURI でみつからないなら全部 xhtml として扱うことにする。

var context = document;
var resolver = function (prefix) {
    var c = document.createNSResolver(context).lookupNamespaceURI(prefix);
    if (c) return c;
    return (document.documentElement.namespaceURI ? "http://www.w3.org/1999/xhtml" : "");
};
var expr = document.createExpression("//h:body", resolver);

割と嬉しい resolver になった。


ちなみに、いろいろ調べているうちに Opera の非互換を踏んでしまった。Opera (9.24/9.5) では resolver が "" (空文字列) をかえすと、null を返したときと同じように NAMESPACE_ERR を投げてしまう (Safari や Gecko では名前空間が空ということにしてくれる)。つまり Opera だと全適用のユーザスクリプトで XPath つかうときはページが XML か HTML かで式自体を変えないといけないのでめんどい。とかいうのを IRC でごちゃごちゃ言ってたら Kuruma さんに報告してもらった。ありがとう!

あと XML の仕様では「名前空間に関連づいていないノードの名前空間」の名前が定義されていないみたい。no value (Namespace in XML) とかだったり null namespace (XSLT) だったりする。

drry さんが document を createNSResolver にわたすと名前空間解決できないことがあるよっていっていたので (よくわかってなかたw)、さらに追試してみたところ Opera と Fx2 では document をわたしたとき documentElement に解決を委譲しないらしいことがわかった (GranParadiso では修正済らしい https://bugzilla.mozilla.org/show_bug.cgi?id=307465 あと Safari では問題ない)

     case DOCUMENT_NODE: 
          return documentElement.lookupNamespaceURI(prefix) 
http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/namespaces-algorithms.html

この部分が正しく実装されてない。Opera--

*1: 名前空間を持っているノードは prefix なしでは絶対に選択できない