数年前Hatena::agendaで公開したselectNodesメソッドの実装に加えて、カスタムイテレータを定義してfor each文で使えるようにしてみた。
まず_XPathNSResolverを定義。非XMLなHTMLで使う限りこれは読み飛ばし可。経緯はDOM level 3 XPathはprefixなしの名前空間を持ったノードをQNameで表現できない (agenda)。
function _XPathNSResolver(nsmap, nodeResolver){
this._nsmap = nsmap;
this._resolver = nodeResolver?
nodeResolver.ownerDocument.createNSResolver(nodeResolver) : null;
}
_XPathNSResolver.prototype.lookupNamespaceURI = function(prefix){
var v;
if (v = this._nsmap[prefix])
return v;
if (v = this._resolver)
return v.lookupNamespaceURI(prefix);
return v;
};
_XPathNSResolverの使い方。第一引数に接頭辞とURIのペアのオブジェクトリテラルを渡してインスタンスを作って、evaluateメソッドの第三引数に渡す:
/* Tipical usage of _XPathNSResolver
var de = document.documentElement;
var type = XPathResult.FIRST_ORDERED_NODE_TYPE;
var nsresolver = new _XPathNSResolver(
{xht: "http://www.w3.org/1999/xhtml"}, de);
var result = document.evaluate(
'descendant::xht:*', de, nsresolver, type, null);
*/
次。Nodeインターフェイスを拡張。まず個人的に良く使う手でスコープを作る:
(function(__proto__/* Node.prototype */){
次にselectNodesメソッドを書く:
var resolver = (
Document.prototype.xpathNSResolver = new _XPathNSResolver({}, null)
);
__proto__.selectNodes = function(expression){
var doc = this.nodeType == Node.DOCUMENT_NODE ?
this : this.ownerDocument;
var result = doc.evaluate(
expression, // XPath expression
this, // Context node
resolver, // Name space resolver
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, // Result type
null // XPathResult if reuse
);
if (result.snapshotLength == 0) return null;
return new _NodeList(result);
};
たぶん毎回_XPathNSResolverを作るのがアレだからdocumentオブジェクトのメンバにして使いまわそうとしたんだろう>自分。ここ改善の余地。
で、ここからがついさっき書き加えた部分。_NodeListコンストラクタ。selectNodesメソッドの戻り値はNodeListなので、それに似たものを自作。
function _NodeList(result){ // 「死」んでいる点に注意する
this.__index = 0;
this.__result = result;
this.length = result.snapshotLength;
}
いよいよ本題というか、_NodeListのメソッドと共にカスタムイテレータを定義。Javascript1.7は 本当にいいものだ。
(function(__proto__){
__proto__.__iterator__ = function(){
return this;
};
__proto__.item = function(index){
return this.__result.snapshotItem(index);
};
__proto__.next = function(){
var item = this.item(this.__index++);
if (!item) {
this.__index = 0;
throw StopIteration;
} else {
return item;
}
};
})(_NodeList.prototype);
_NodeList自身をイテレータにした。こうすることでfor each文を自然なPython風に使用できるという寸法。おっとスコープを閉じなきゃ。:
})(Node.prototype);
で使い方なんだけど例えば次のようなタスクを考える:
DOMでは割と小面倒な部類なんじゃないかな。北村さん(誰)に突っ込まれそうなクラス名だけど気にしない。
for each(let e in document.selectNodes("descendant::ul | descendant::ol"))
e.selectNodes("child::li").length < 4 && (e.className = "short");
終了。XPathをもっとうまく使う別解もあり。