Subversion Repositories JSX

Compare Revisions

Last modification

Ignore whitespace Rev 521 → Rev 522

/trunk/dom/xpath.js
7,9 → 7,9
* @section Copyright & Disclaimer
*
* @author (C) 2008‒2012 Thomas Lahn <js@PointedEars.de>
*
*
* @partof PointedEars' JavaScript Extensions (JSX)
*
*
* JSX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
67,446 → 67,339
/**
* @namespace
*/
jsx.dom.xpath = {
version: "0.1.$Revision$ ($Date$)",
copyright: "Copyright \xA9 2008-2011",
author: "Thomas Lahn",
email: "js@PointedEars.de",
path: "http://PointedEars.de/scripts/",
jsx.dom.xpath = (/** @constructor */ function () {
/* Imports */
var _isMethod = jsx.object.isMethod;
 
/* Private variables */
 
/**
* Returns the result of evaluating an XPath expression against a context node
*
* Exception thrown if an unsupported XPath implementation is encountered
*
* @function
* @param expression : String
* XPath expression. You may use a namespace prefix starting with "_"
* to indicate a default namespace of the context node (that XPath
* usually cannot resolve directly). You then need to provide a custom
* namespace resolver, like one created with
* {@link jsx.dom.xpath#createCustomNSResolver}, to return the namespace
* URI for those prefixes. This workaround is necessary because MSXML
* XPath for JScript does not require or support a custom namespace
* resolver for elements in the default namespace, and those virtual
* prefixes must be trimmed from the expression by this method beforehand.
* @param contextNode : Node
* Context node
* @param namespaceResolver : Function
* Namespace resolver. If not provided, a default namespace
* resolver will be created using {@link jsx.dom.xpath#createDefaultNSResolver}.
* Is ignored if MSXML is used, as MSXML for JScript supports only
* the default namespace resolver. See the section
* {@ref "Namespace Resolvers"} for details.
* @param resultType : Number
* Expected XPath result type. Should be one of the XPathResult.* constants.
* Is ignored if MSXML is used, as MSXML only supports the equivalent of
* {@link XPathResult#ORDERED_NODE_ITERATOR_TYPE}.
* @param oResult : XPathResult
* Optional XPathResult. For backwards compatibility only.
* @return Result of the XPath expression,
* or <code>null</code> on recoverable error
* @type Null|number|string|boolean|Array
* @throws DOMException on unrecoverable XPath error
*/
evaluate:
(function () {
var isMethod = jsx.object.isMethod;
var _XPathResult = XPathResult;
var createDefaultNSResolver = null;
return function (expression, contextNode, namespaceResolver,
resultType, oResult) {
var result = null;
if (!contextNode)
{
contextNode = document;
var documentNode = contextNode;
}
else
{
documentNode = contextNode.ownerDocument || contextNode;
}
var _InvalidImplementationError = (function () {
jsx.Error.call(this);
}).extend(jsx.Error, {
name: "jsx.dom.xpath.InvalidImplementationError"
});
 
if (isMethod(documentNode, "evaluate"))
{
var nsResolver = namespaceResolver
|| (
createDefaultNSResolver
|| (createDefaultNSResolver = jsx.dom.xpath.createDefaultNSResolver)
)(contextNode, documentNode);
var iResultType = resultType || 0;
var objResult = oResult || null;
/* Make DOM exception recoverable in WebCore */
jsx.tryThis(
function () {
result = documentNode.evaluate(expression, contextNode, nsResolver,
iResultType, objResult);
},
function (e) {
jsx.rethrowThis(e);
}
);
}
else if (isMethod(contextNode, "selectNodes"))
{
result = contextNode.selectNodes(expression.replace(/_\w*:/g, ""));
resultType = _XPathResult.ORDERED_NODE_ITERATOR_TYPE;
}
if (result)
{
var found = [];
switch (typeof result.resultType != "undefined"
? result.resultType
: resultType)
{
case _XPathResult.NUMBER_TYPE:
if (typeof result.numberValue != "undefined")
{
result = result.numberValue;
}
break;
case _XPathResult.STRING_TYPE:
if (typeof result.stringValue != "undefined")
{
result = result.stringValue;
}
break;
case _XPathResult.BOOLEAN_TYPE:
if (typeof result.booleanValue != "undefined")
{
result = result.booleanValue;
}
break;
case _XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
case _XPathResult.ORDERED_NODE_ITERATOR_TYPE:
if (isMethod(result, "iterateNext"))
{
/* DOM Level 3 XPath: NodeIterator */
var res;
while ((res = result.iterateNext()))
{
found.push(res);
}
result = found;
}
else if (typeof result.length != "undefined")
{
/* MSXML DOM: NodeList */
for (var i = 0, len = result.length; i < len; ++i)
{
found.push(result[i]);
}
result = found;
}
break;
case _XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
case _XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:
if (typeof result.snapshotLength != "undefined"
&& isMethod(result, "snapshotItem"))
{
for (var i = 0, len = result.snapshotLength; i < len; i++)
{
found.push(result.snapshotItem(i));
}
result = found;
}
break;
case _XPathResult.ANY_UNORDERED_NODE_TYPE:
case _XPathResult.FIRST_ORDERED_NODE_TYPE:
if (typeof result.singleNodeValue != "undefined")
{
result = result.singleNodeValue;
}
break;
default:
jsx.throwThis("jsx.dom.xpath.InvalidImplementationError");
result = null;
}
}
return result;
};
}()),
/**
* @section Namespace Resolvers
*
* To facilitate matching of elements and attributes in namespaces
* with XPath, a namespace resolver is required. A namespace resolver
* is a function that returns the namespace URI for a given namespace
* prefix found in an XPath expression.
*
* JSX:xpath.js provides three ways to create a namespace resolver:
* <ul>
* <li>{@link jsx.dom.xpath#createDefaultNSResolver} creates a built-in
* namespace resolver (if available) that can resolve elements
* and attributes in the document in the null namespace (in no
* namespace) or in namespaces with prefixes (declared with
* <code>xmlns:<var>prefix</var>="<var>URI</var>"</code>.
* It cannot resolve elements and attributes in a default
* namespace (declared with <code>xmlns="<var>URI</var>"</code>).
* </li>
* <li>{@link jsx.dom.xpath#createCustomNSResolver} creates a custom
* namespace resolver that can resolve only namespace prefixes
* that you specify. You can use this kind of resolver
* to select elements and attributes in known default namespaces,
* by giving those prefixes starting with <code>_</code>, and
* namespaces with other prefixes.</li>
* <li>{@link jsx.dom.xpath#createFullNSResolver} gives you the
* best of both worlds. It creates a customizable namespace
* resolver that scans the document starting from a given context
* node (the document element node by default) for the first
* default namespace declaracation and for declarations of
* namespaces with a prefix.
* </ul>
*/
/**
* Creates and returns a default namespace resolver, as created and
* returned by <code>Document::createNSResolver()</code>, if available,
* or <code>null</code>.
*
*
* Note that despite its name a default namespace resolver cannot
* resolve elements in a default namespace. Use
* {@link jsx.dom.xpath#createCustomNSResolver()} to create such a resolver
* {@link jsx.dom.xpath.createCustomNSResolver()} to create such a resolver
* instead.
*
* @param contextNode : Node
*
* @param {Node} contextNode
* Context node for which the default namespace resolver should be
* created.
* @param documentNode : Document
* @param {Document} documentNode
* Document node, required for calling the <code>createNSResolver()</code>
* method. If not specified, <code>document</code> is used.
* @return NSResolver|null
*/
createDefaultNSResolver: function (contextNode, documentNode) {
function _createDefaultNSResolver (contextNode, documentNode)
{
if (!documentNode)
{
documentNode = document;
}
return (jsx.object.isMethod(documentNode, "createNSResolver")
 
return (_isMethod(documentNode, "createNSResolver")
? documentNode.createNSResolver(
(contextNode.ownerDocument == null
? contextNode.documentElement
: contextNode.ownerDocument.documentElement))
: null);
},
/**
* Collects and returns the namespaces of a node and its descendants.
*
* Collects the namespace prefixes and URIs of <var>contextNode</var> and
* its descendant element nodes. Duplicate prefixes with different namespace
* URI are ignored and must be resolved manually using the <var>namespaces</var>
* argument of {@link jsx.dom.xpath#createFullNSResolver()}.
*
* @param namespaces : Object
* Contains the collected namespace declarations. Existing properties
* are preserved.
* @param contextNode : Document|Element
* The parent node from where to start searching for namespace
* declarations. If it is a Document node, the document element node
* is used instead. The default is the document element node.
* @return {Object}
* The resulting value of <var>namespaces</var> if successful,
* <code>null</code> otherwise.
* @throws jsx.dom.xpath.InvalidNodeError if a value has been specified
* for <var>contextNode</var> that is not a reference to a Document
* node or an Element node.
* @see jsx.dom.xpath#createFullNSResolver()
*/
collectNamespaces: function jsx_xpath_collectNamespaces (namespaces, contextNode) {
if (!namespaces)
{
jsx.warn("`namespaces' is not convertible to an Object; argument is not modified.");
namespaces = {};
}
}
 
/* Scan entire document by default */
if (!contextNode)
{
contextNode = document.documentElement;
}
/* if DOCUMENT_NODE, use root element */
if (contextNode.nodeType == 9)
{
contextNode = contextNode.documentElement;
}
if (!contextNode || contextNode.nodeType != 1)
{
jsx.throwThis("jsx.dom.xpath.InvalidNodeError", contextNode);
return null;
}
for (var i = 0, attribs = contextNode.attributes, len = attribs && attribs.length;
i < len;
++i)
{
var attr = attribs[i];
var attrName = attr.name;
var matches;
if ((matches = String(attrName).match(/^xmlns($|:(.+))/)))
return {
/**
* @memberOf jsx.dom.xpath
*/
version: "0.1.$Revision$ ($Date$)",
copyright: "Copyright \xA9 2008-2011",
author: "Thomas Lahn",
email: "js@PointedEars.de",
path: "http://PointedEars.de/scripts/",
 
/**
* Returns the result of evaluating an XPath expression against a context node
*
* @param {String} expression
* XPath expression. You may use a namespace prefix starting with "_"
* to indicate a default namespace of the context node (that XPath
* usually cannot resolve directly). You then need to provide a custom
* namespace resolver, like one created with
* {@link #createCustomNSResolver}, to return the namespace
* URI for those prefixes. This workaround is necessary because MSXML
* XPath for JScript does not require or support a custom namespace
* resolver for elements in the default namespace, and those virtual
* prefixes must be trimmed from the expression by this method beforehand.
* @param {Node} contextNode
* Context node
* @param {Function} namespaceResolver
* Namespace resolver. If not provided, a default namespace
* resolver will be created using {@link #createDefaultNSResolver}.
* Is ignored if MSXML is used, as MSXML for JScript supports only
* the default namespace resolver. See the section
* {@ref "Namespace Resolvers"} for details.
* @param {Number} resultType
* Expected XPath result type. Should be one of the XPathResult.* constants.
* Is ignored if MSXML is used, as MSXML only supports the equivalent of
* {@link XPathResult#ORDERED_NODE_ITERATOR_TYPE}.
* @param {XPathResult} oResult
* Optional XPathResult. For backwards compatibility only.
* @return {Null|number|string|boolean|Array}
* Result of the XPath expression,
* or <code>null</code> on recoverable error
* @throws DOMException on unrecoverable XPath error
*/
evaluate: function (expression, contextNode, namespaceResolver,
resultType, oResult) {
var result = null;
 
if (!contextNode)
{
var originalPrefix = matches[2];
var prefix = originalPrefix || "_";
if (typeof namespaces[prefix] == "undefined")
{
var attrValue = attr.value;
/*
* Default NS declaration with empty value means _no_ namespace,
* see <http://www.w3.org/TR/REC-xml-names/#defaulting>
*/
if (!originalPrefix && attrValue == "")
{
attrValue = null;
}
namespaces[prefix] = attrValue;
}
contextNode = document;
var documentNode = contextNode;
}
}
else
{
documentNode = contextNode.ownerDocument || contextNode;
}
 
for (var i = 0, childNodes = contextNode.childNodes, len = childNodes && childNodes.length;
i < len;
++i)
{
var childNode = childNodes[i];
/* If ELEMENT_NODE, recurse */
if (childNode.nodeType == 1)
if (_isMethod(documentNode, "evaluate"))
{
var nsResolver = namespaceResolver
|| _createDefaultNSResolver(contextNode, documentNode);
var iResultType = resultType || 0;
var objResult = oResult || null;
 
/* Make DOM exception recoverable in WebCore */
jsx.tryThis(
function() {
jsx_xpath_collectNamespaces(namespaces, childNode);
function () {
result = documentNode.evaluate(expression, contextNode, nsResolver,
iResultType, objResult);
},
function (e) {
if (e.name === "jsx.dom.xpath.InvalidArgumentError")
if (e.name == "SyntaxError")
{
jsx.throwThis(e);
jsx.throwThis("SyntaxError", e.message
+ "\nSee http://www.w3.org/TR/xpath for details.");
}
});
}
);
}
}
return namespaces;
},
/**
* Creates and returns a custom namespace resolver.
*
* This method exists primarily to facilitate the matching of elements
* that are in the default namespace as XPath defines <tt>QNames</tt>
* without prefix to match only elements in the null namespace. Use it
* instead of {@link jsx.dom.xpath#createFullNSResolver} if you only want
* to match elements in custom default namespaces.
*
* See also the description of the <var>expression</var> argument of
* {@link jsx.dom.xpath#evaluate} for details.
*
* @param namespaces : Object
* Namespace declarations. The property names are the namespace
* prefixes, the property values are the namespace URIs.
* @return {Function} A namespace resolver that can resolve the
* declared namespaces. A selector in an undeclared namespace is
* considered to be in the null namespace.
*/
createCustomNSResolver: function (namespaces) {
return function (prefix) {
return namespaces[prefix] || null;
};
},
/**
* Creates and returns a customizable full namespace resolver.
*
* Creates and returns a customizable namespace resolver that
* considers the namespaces already declared in the document first.
*
* Like {@link jsx.dom.xpath#createCustomNSResolver}, this method exists
* primarily to facilitate the matching of elements that are in a
* default namespace. Use it instead of
* {@link jsx.dom.xpath#createCustomNSResolver} if you need to match elements
* in the context node's default namespace, or in one of the namespaces
* with prefix declared in the context node or its descendant elements.
*
* See also the description of the <var>expression</var> argument of
* {@link jsx.dom.xpath#evaluate} for details.
*
* @param namespaces : Object
* Namespace declarations. The property names are the namespace
* prefixes, the property values are the namespace URIs.
* @param contextNode : Document|Element
* The parent node from where to start searching for namespace
* declarations. If it is a Document node, the document element node
* is used instead. The default is the document element node.
* @return {Function} A namespace resolver that can resolve the
* declared namespaces. A selector in an undeclared namespace is
* considered to be in the null namespace.
* @throws jsx.dom.xpath#InvalidNodeError if a value has been specified
* for <var>contextNode</var> that is not a reference to a Document
* node or an Element node.
* @see jsx.dom.xpath#collectNamespaces(string, Element|Document)
* @function
*/
createFullNSResolver: (function () {
var collectNamespaces = null;
return function (namespaces, contextNode) {
if (contextNode)
else if (_isMethod(contextNode, "selectNodes"))
{
if (!namespaces)
result = contextNode.selectNodes(expression.replace(/_\w*:/g, ""));
resultType = XPathResult.ORDERED_NODE_ITERATOR_TYPE;
}
 
if (result)
{
var found = [];
 
switch (typeof result.resultType != "undefined"
? result.resultType
: resultType)
{
namespaces = {};
case XPathResult.NUMBER_TYPE:
if (typeof result.numberValue != "undefined")
{
result = result.numberValue;
}
break;
 
case XPathResult.STRING_TYPE:
if (typeof result.stringValue != "undefined")
{
result = result.stringValue;
}
break;
 
case XPathResult.BOOLEAN_TYPE:
if (typeof result.booleanValue != "undefined")
{
result = result.booleanValue;
}
break;
 
case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
case XPathResult.ORDERED_NODE_ITERATOR_TYPE:
if (_isMethod(result, "iterateNext"))
{
/* DOM Level 3 XPath: NodeIterator */
var res;
while ((res = result.iterateNext()))
{
found.push(res);
}
 
result = found;
}
else if (typeof result.length != "undefined")
{
/* MSXML DOM: NodeList */
for (var i = 0, len = result.length; i < len; ++i)
{
found.push(result[i]);
}
 
result = found;
}
break;
 
case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:
if (typeof result.snapshotLength != "undefined"
&& _isMethod(result, "snapshotItem"))
{
for (var i = 0, len = result.snapshotLength; i < len; i++)
{
found.push(result.snapshotItem(i));
}
 
result = found;
}
break;
 
case XPathResult.ANY_UNORDERED_NODE_TYPE:
case XPathResult.FIRST_ORDERED_NODE_TYPE:
if (typeof result.singleNodeValue != "undefined")
{
result = result.singleNodeValue;
}
break;
 
default:
jsx.throwThis(_InvalidImplementationError);
result = null;
}
(
collectNamespaces || (collectNamespaces = jsx.dom.xpath.collectNamespaces)
)(namespaces, contextNode);
}
 
return result;
},
 
/**
* @section Namespace Resolvers
*
* To facilitate matching of elements and attributes in namespaces
* with XPath, a namespace resolver is required. A namespace resolver
* is a function that returns the namespace URI for a given namespace
* prefix found in an XPath expression.
*
* JSX:xpath.js provides three ways to create a namespace resolver:
* <ul>
* <li>{@link #createDefaultNSResolver} creates a built-in
* namespace resolver (if available) that can resolve elements
* and attributes in the document in the null namespace (in no
* namespace) or in namespaces with prefixes (declared with
* <code>xmlns:<var>prefix</var>="<var>URI</var>"</code>.
* It cannot resolve elements and attributes in a default
* namespace (declared with <code>xmlns="<var>URI</var>"</code>).
* </li>
* <li>{@link #createCustomNSResolver} creates a custom
* namespace resolver that can resolve only namespace prefixes
* that you specify. You can use this kind of resolver
* to select elements and attributes in known default namespaces,
* by giving those prefixes starting with <code>_</code>, and
* namespaces with other prefixes.</li>
* <li>{@link #createFullNSResolver} gives you the
* best of both worlds. It creates a customizable namespace
* resolver that scans the document starting from a given context
* node (the document element node by default) for the first
* default namespace declaracation and for declarations of
* namespaces with a prefix.
* </ul>
*/
 
createDefaultNSResolver: _createDefaultNSResolver,
 
/**
* Creates and returns a custom namespace resolver.
*
* This method exists primarily to facilitate the matching of elements
* that are in the default namespace as XPath defines <tt>QNames</tt>
* without prefix to match only elements in the null namespace. Use it
* instead of {@link #createFullNSResolver} if you only want
* to match elements in custom default namespaces.
*
* See also the description of the <var>expression</var> argument of
* {@link #evaluate} for details.
*
* @param {Object} namespaces
* Namespace declarations. The property names are the namespace
* prefixes, the property values are the namespace URIs.
* @return {Function} A namespace resolver that can resolve the
* declared namespaces. A selector in an undeclared namespace is
* considered to be in the null namespace.
*/
createCustomNSResolver: function (namespaces) {
return function (prefix) {
return namespaces[prefix] || null;
};
};
}()),
/**
* Exception thrown if an unsupported XPath implementation is encountered
*
* @function
*/
InvalidImplementationError: (function () {
jsx.Error.call(this);
}).extend(jsx.Error, {
name: "jsx.dom.xpath.InvalidImplementationError"
}),
/**
* Exception thrown if an invalid Node reference is passed
*
* @function
*/
InvalidNodeError: (function (contextNode) {
jsx.Error.call(this, contextNode);
}).extend(jsx.Error, {
name: "jsx.dom.xpath.InvalidNodeError"
})
};
},
 
/**
* Creates and returns a customizable full namespace resolver.
*
* Creates and returns a customizable namespace resolver that
* considers the namespaces already declared in the document first.
*
* Like {@link #createCustomNSResolver}, this method exists
* primarily to facilitate the matching of elements that are in a
* default namespace. Use it instead of
* {@link #createCustomNSResolver} if you need to match elements
* in the context node's default namespace, or in one of the namespaces
* with prefix declared in the context node or its descendant elements.
*
* See also the description of the <var>expression</var> argument of
* {@link #evaluate} for details.
*
* @param {Object} namespaces
* Namespace declarations. The property names are the namespace
* prefixes, the property values are the namespace URIs.
* @param {Document|Element} contextNode
* The parent node from where to start searching for namespace
* declarations. If it is a Document node, the document element node
* is used instead. The default is the document element node.
* @return {Function} A namespace resolver that can resolve the
* declared namespaces. A selector in an undeclared namespace is
* considered to be in the null namespace.
* @throws jsx.dom.InvalidNodeError if a value has been specified
* for <var>contextNode</var> that is not a reference to a Document
* node or an Element node.
* @see jsx.dom.collectNamespaces(string, Element|Document)
* @function
*/
createFullNSResolver: (function () {
var _collectNamespaces = null;
 
return function (namespaces, contextNode) {
if (contextNode)
{
if (!namespaces)
{
namespaces = {};
}
 
(
_collectNamespaces || (_collectNamespaces = jsx.dom.collectNamespaces)
)(namespaces, contextNode);
}
 
return function (prefix) {
return namespaces[prefix] || null;
};
};
}()),
 
InvalidImplementationError: _InvalidImplementationError
};
}());