├── .gitignore ├── .npmignore ├── .prettierrc ├── CHANGELOG.md ├── LICENCE.md ├── README.md ├── docs ├── XPathEvaluator.md ├── XPathResult.md ├── function resolvers.md ├── namespace resolvers.md ├── parsed expressions.md ├── variable resolvers.md └── xpath methods.md ├── package.json ├── src ├── api.ts ├── avl-tree.ts ├── consts.ts ├── function-call.ts ├── function-resolver.ts ├── functions.ts ├── index.ts ├── location-path.ts ├── namespace-resolver.ts ├── node-test.ts ├── node-x-path-ns-resolver.ts ├── operations │ ├── and-operation.ts │ ├── bar-operation.ts │ ├── binary-operation.ts │ ├── div-operation.ts │ ├── equals-operation.ts │ ├── greater-than-operation.ts │ ├── greater-than-or-equal-operation.ts │ ├── less-than-operation.ts │ ├── less-than-or-equal-operation.ts │ ├── minus-operation.ts │ ├── mod-operation.ts │ ├── multiply-operation.ts │ ├── not-equals-operation.ts │ ├── or-operation.ts │ ├── plus-operation.ts │ ├── unary-minus-operation.ts │ └── unary-operation.ts ├── parse-api.ts ├── path-expr.ts ├── step.ts ├── utils │ ├── character.ts │ ├── types.ts │ └── xml.ts ├── variable-reference.ts ├── variable-resolver.ts ├── xpath-evaluator.ts ├── xpath-exception.ts ├── xpath-expression-impl.ts ├── xpath-namespace.ts ├── xpath-ns-resolver-wrapper.ts ├── xpath-parser.ts ├── xpath-result-impl.ts ├── xpath-types.ts └── xpath.ts ├── test ├── jsdom.test.ts ├── tests.ts └── xmldom.test.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | /docs/_build/ 4 | lib/ 5 | dist/ 6 | types/ 7 | 8 | # Logs 9 | *.log 10 | 11 | # Coverage 12 | /.nyc_output 13 | /coverage 14 | 15 | # IDE specific 16 | /.vscode/ 17 | /.project/ 18 | /.settings/ 19 | 20 | /.DS_Store 21 | src/.DS_Store 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | /docs/_build/ 4 | 5 | # Logs 6 | *.log 7 | 8 | # Coverage 9 | /.nyc_output 10 | /coverage 11 | 12 | # IDE specific 13 | /.vscode/ 14 | /.project/ 15 | /.settings/ 16 | 17 | /.DS_Store 18 | src/.DS_Store 19 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "arrowParens": "always" 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Revision 23: May 24, 2019 2 | Switched to XMLDOM-TS as test implementation 3 | 4 | Revision 22: December 17, 2018 5 | Change default namespace handling for Xpath evaluator 6 | 7 | Revision 21: December 10, 2018 8 | Completely rewritten to TypeScript 9 | One monolith file divided into classes 10 | Test rewritten to Mocha + Chai 11 | Changed XPathResult.stringValue and other similar methods to getters for comformance with https://www.w3.org/TR/DOM-Level-3-XPath/xpath.html#XPathResult 12 | Add preliminary DOM4 support (JSdom implementation) 13 | 14 | Revision 20: April 26, 2011 15 | Fixed a typo resulting in FIRST_ORDERED_NODE_TYPE results being wrong, 16 | thanks to . 17 | 18 | Revision 19: November 29, 2005 19 | Nodesets now store their nodes in a height balanced tree, increasing 20 | performance for the common case of selecting nodes in document order, 21 | thanks to S 閎 astien Cramatte . 22 | AVL tree code adapted from Raimund Neumann . 23 | 24 | Revision 18: October 27, 2005 25 | DOM 3 XPath support. Caveats: - namespace prefixes aren't resolved in XPathEvaluator.createExpression, 26 | but in XPathExpression.evaluate. - XPathResult.invalidIteratorState is not implemented. 27 | 28 | Revision 17: October 25, 2005 29 | Some core XPath function fixes and a patch to avoid crashing certain 30 | versions of MSXML in PathExpr.prototype.getOwnerElement, thanks to 31 | S 閎 astien Cramatte . 32 | 33 | Revision 16: September 22, 2005 34 | Workarounds for some IE 5.5 deficiencies. 35 | Fixed problem with prefix node tests on attribute nodes. 36 | 37 | Revision 15: May 21, 2005 38 | Fixed problem with QName node tests on elements with an xmlns="...". 39 | 40 | Revision 14: May 19, 2005 41 | Fixed QName node tests on attribute node regression. 42 | 43 | Revision 13: May 3, 2005 44 | Node tests are case insensitive now if working in an HTML DOM. 45 | 46 | Revision 12: April 26, 2005 47 | Updated licence. Slight code changes to enable use of Dean 48 | Edwards' script compression, http://dean.edwards.name/packer/ . 49 | 50 | Revision 11: April 23, 2005 51 | Fixed bug with 'and' and 'or' operators, fix thanks to 52 | Sandy McArthur . 53 | 54 | Revision 10: April 15, 2005 55 | Added support for a virtual root node, supposedly helpful for 56 | implementing XForms. Fixed problem with QName node tests and 57 | the parent axis. 58 | 59 | Revision 9: March 17, 2005 60 | Namespace resolver tweaked so using the document node as the context 61 | for namespace lookups is equivalent to using the document element. 62 | 63 | Revision 8: February 13, 2005 64 | Handle implicit declaration of 'xmlns' namespace prefix. 65 | Fixed bug when comparing nodesets. 66 | Instance data can now be associated with a FunctionResolver, and 67 | workaround for MSXML not supporting 'localName' and 'getElementById', 68 | thanks to Grant Gongaware. 69 | Fix a few problems when the context node is the root node. 70 | 71 | Revision 7: February 11, 2005 72 | Default namespace resolver fix from Grant Gongaware 73 | . 74 | 75 | Revision 6: February 10, 2005 76 | Fixed bug in 'number' function. 77 | 78 | Revision 5: February 9, 2005 79 | Fixed bug where text nodes not getting converted to string values. 80 | 81 | Revision 4: January 21, 2005 82 | Bug in 'name' function, fix thanks to Bill Edney. 83 | Fixed incorrect processing of namespace nodes. 84 | Fixed NamespaceResolver to resolve 'xml' namespace. 85 | Implemented union '|' operator. 86 | 87 | Revision 3: January 14, 2005 88 | Fixed bug with nodeset comparisons, bug lexing < and >. 89 | 90 | Revision 2: October 26, 2004 91 | QName node test namespace handling fixed. Few other bug fixes. 92 | 93 | Revision 1: August 13, 2004 94 | Bug fixes from William J. Edney . 95 | Added minimal licence. 96 | 97 | Initial version: June 14, 2004 98 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2018 Cameron McCormack and other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XPath library 2 | 3 | DOM 3 and 4 XPath 1.0 implemention for browser and Node.js environment with support for custom **Function**, **Variable** and **Namespace** mapping. 4 | 5 | ## Requirements 6 | 7 | - [Node v11.x or greater](https://nodejs.org/en/download/) 8 | 9 | ## Release Notes 10 | 11 | See [CHANGELOG.md](CHANGELOG.md) 12 | 13 | ## Usage 14 | 15 | Install with [npm](http://github.com/isaacs/npm): 16 | 17 | ``` 18 | npm install xpath-ts 19 | ``` 20 | 21 | This library is xml engine agnostic but I recommend to use [xmldom-ts](https://github.com/backslash47/xmldom), [xmldom](https://github.com/jindw/xmldom) or [jsdom](https://github.com/jsdom/jsdom) 22 | 23 | ``` 24 | npm install xmldom-ts 25 | ``` 26 | or 27 | 28 | ``` 29 | npm install xmldom 30 | ``` 31 | 32 | 33 | or 34 | 35 | ``` 36 | npm install jsdom 37 | ``` 38 | 39 | ## API Documentation 40 | 41 | Can be found [here](https://github.com/backslash47/backslash47/blob/master/docs/xpath%20methods.md). See below for example usage. 42 | 43 | ## Your first xpath: 44 | 45 | ```typescript 46 | import { DOMParserImpl as dom } from 'xmldom-ts'; 47 | import * as xpath from 'xpath-ts'; 48 | 49 | const xml = 'Harry Potter'; 50 | const doc = new dom().parseFromString(xml); 51 | const nodes = xpath.select('//title', doc); 52 | 53 | console.log(nodes[0].localName + ': ' + nodes[0].firstChild.data); 54 | console.log('Node: ' + nodes[0].toString()); 55 | ``` 56 | 57 | ➡ 58 | 59 | ``` 60 | title: Harry Potter 61 | Node: Harry Potter 62 | ``` 63 | 64 | ### Alternatively 65 | 66 | Using the same interface you have on modern browsers ([MDN]) 67 | 68 | ```typescript 69 | import { DOMParserImpl as dom } from 'xmldom-ts'; 70 | import * as xpath from 'xpath-ts'; 71 | 72 | const xml = "Harry Potter"; 73 | const doc = new dom().parseFromString(xml); 74 | 75 | xpath.installDOM3XPathSupport(doc); 76 | 77 | const result = doc.evaluate( 78 | '/book/title', // xpathExpression 79 | doc, // contextNode 80 | null, // namespaceResolver 81 | xpath.XPathResult.ANY_TYPE, // resultType 82 | null // result 83 | ); 84 | 85 | let node = result.iterateNext(); 86 | while (node) { 87 | console.log(node.localName + ': ' + node.firstChild.data); 88 | console.log('Node: ' + node.toString()); 89 | 90 | node = result.iterateNext(); 91 | } 92 | ``` 93 | 94 | ➡ 95 | 96 | ``` 97 | title: Harry Potter 98 | Node: Harry Potter 99 | ``` 100 | 101 | ## Evaluate string values directly: 102 | 103 | ```typescript 104 | import { DOMParserImpl as dom } from 'xmldom-ts'; 105 | import * as xpath from 'xpath-ts'; 106 | 107 | const xml = 'Harry Potter'; 108 | const doc = new dom().parseFromString(xml); 109 | const title = xpath.select('string(//title)', doc); 110 | 111 | console.log(title); 112 | ``` 113 | 114 | ➡ 115 | 116 | ``` 117 | Harry Potter 118 | ``` 119 | 120 | ## Namespaces 121 | 122 | ```typescript 123 | import { DOMParserImpl as dom } from 'xmldom-ts'; 124 | import * as xpath from 'xpath-ts'; 125 | 126 | const xml = "Harry Potter"; 127 | const doc = new dom().parseFromString(xml); 128 | const node = xpath.select("//*[local-name(.)='title' and namespace-uri(.)='myns']", doc)[0]; 129 | 130 | console.log(node.namespaceURI); 131 | ``` 132 | 133 | ➡ 134 | 135 | ``` 136 | myns 137 | ``` 138 | 139 | ## Namespaces with easy mappings 140 | 141 | ```typescript 142 | import { DOMParserImpl as dom } from 'xmldom-ts'; 143 | import * as xpath from 'xpath-ts'; 144 | 145 | const xml = "Harry Potter"; 146 | const select = xpath.useNamespaces({ bookml: 'http://example.com/book' }); 147 | 148 | console.log(select('//bookml:title/text()', doc)[0].nodeValue); 149 | ``` 150 | 151 | ➡ 152 | 153 | ``` 154 | Harry Potter 155 | ``` 156 | 157 | ## Default namespace with mapping 158 | 159 | ```typescript 160 | import { DOMParserImpl as dom } from 'xmldom-ts'; 161 | import * as xpath from 'xpath-ts'; 162 | 163 | const xml = "Harry Potter"; 164 | const select = xpath.useNamespaces({ bookml: 'http://example.com/book' }); 165 | 166 | console.log(select('//bookml:title/text()', doc)[0].nodeValue); 167 | ``` 168 | 169 | ➡ 170 | 171 | ``` 172 | Harry Potter 173 | ``` 174 | 175 | ## Attributes 176 | 177 | ```typescript 178 | import { DOMParserImpl as dom } from 'xmldom-ts'; 179 | import * as xpath from 'xpath-ts'; 180 | 181 | const xml = "Harry Potter"; 182 | const doc = new dom().parseFromString(xml); 183 | const author = xpath.select1('/book/@author', doc).value; 184 | 185 | console.log(author); 186 | ``` 187 | 188 | ➡ 189 | 190 | ``` 191 | J. K. Rowling 192 | ``` 193 | 194 | [mdn]: https://developer.mozilla.org/en/docs/Web/API/Document/evaluate 195 | 196 | ## Developing and Testing 197 | 198 | #### Download 199 | 200 | ``` 201 | git clone 'https://github.com/backslash47/xpath-ts' 202 | cd xpath-ts 203 | ``` 204 | 205 | #### Install 206 | 207 | ``` 208 | npm install 209 | ``` 210 | 211 | #### Build 212 | 213 | ``` 214 | npm run build 215 | ``` 216 | 217 | You will get the transpiled code under '/dist/lib' and typings under '/dist/types'. 218 | 219 | #### Test 220 | 221 | Run standard tests with Mocha + Chai testing framework 222 | 223 | ``` 224 | npm test 225 | ``` 226 | 227 | ## Authors 228 | 229 | - **Cameron McCormack** - _Initial work_ - [blog](http://mcc.id.au/xpathjs) 230 | - **Yaron Naveh** - [blog](http://webservices20.blogspot.com/) 231 | - **goto100** 232 | - **Jimmy Rishe** 233 | - **Thomas Weinert** 234 | - **Matus Zamborsky** - _TypeScript rewrite_ - [Backslash47](https://github.com/backslash47) 235 | - **Others** - [others](https://github.com/goto100/xpath/graphs/contributors) 236 | 237 | ## Licence 238 | 239 | This project is licensed under the MIT License - see the [LICENCE.md](LICENCE.md) file for details. 240 | -------------------------------------------------------------------------------- /docs/XPathEvaluator.md: -------------------------------------------------------------------------------- 1 | # `XPathEvaluator` 2 | 3 | The `xpath.parse()` method returns an `XPathEvaluator`, which contains the following methods. 4 | 5 | Each of these methods takes an optional `options` object, which can contain any of the following properties. See the links for each item for further details: 6 | 7 | - `namespaces` - a [namespace resolver](namespace%20resolvers.md) 8 | 9 | - `variables` - a [variable resolver](variable%20resolvers.md) 10 | 11 | - `functions` - a [function resolver](function%20resolvers.md) 12 | 13 | - `node` - the context node for evaluating the expression 14 | 15 | Example usage: 16 | 17 | ```typescript 18 | const evaluator = xpath.parse('/characters/character[@greeting = $greeting]'); 19 | const character = evaluator.select1({ 20 | node: myCharacterDoc, 21 | variables: { 22 | greeting: "Hello, I'm Harry, Harry Potter." 23 | } 24 | }); 25 | ``` 26 | 27 | ## `XPathEvaluator` methods 28 | 29 | `evaluate([options])` 30 | 31 | Evaluates the XPath expression and returns the result. The resulting type is determined based on the type of the expression, using the same criteria as [`xpath.select`](xpath%20methods.md). 32 | 33 | `evaluateNumber([options])` 34 | 35 | Evaluates the XPath expression and returns the result as a number. 36 | 37 | `evaluateString([options])` 38 | 39 | Evaluates the XPath expression and returns the result as a string. 40 | 41 | `evaluateBoolean([options])` 42 | 43 | Evaluates the XPath expression and returns the result as a boolean value. 44 | 45 | `evaluateNodeSet([options])` 46 | 47 | Evaluates the XPath expression and returns the result as an XNodeSet. See the [documentation page](#) for details on this interface. 48 | 49 | This is only valid for expressions that evaluate to a node set. 50 | 51 | `select([options])` 52 | 53 | Evaluates the XPath expression and returns an array of the resulting nodes, in document order. 54 | 55 | This is only valid for expressions that evaluate to a node set. 56 | 57 | `select1([options])` 58 | 59 | Evaluates the XPath expression and the first node in the resulting node set, in document order. Returns `undefined` if the resulting node set is empty. 60 | 61 | This is only valid for expressions that evaluate to a node set. 62 | -------------------------------------------------------------------------------- /docs/XPathResult.md: -------------------------------------------------------------------------------- 1 | # XPathResult interface 2 | 3 | Represents the result of an XPath expression. This interface is used for the parameters passed into custom functions 4 | used in [function resolvers](function resolvers.md) and can represent a number, a string, a boolean value, or a node set. 5 | 6 | ## Methods and Getters 7 | 8 | ```typescript 9 | booleanValue: boolean; 10 | ``` 11 | 12 | Returns the boolean value of the result in accordance with the XPath 1.0 spec. 13 | 14 | ```typescript 15 | numberValue: number; 16 | ``` 17 | 18 | Returns the numeric value of the result in accordance with the XPath 1.0 spec. 19 | 20 | ```typescript 21 | stringValue: string; 22 | ``` 23 | 24 | Returns the string value of the result in accordance with the XPath 1.0 spec. 25 | 26 | ## Methods and properties that are only present on `XPathResult`s representing node sets 27 | 28 | ```typescript 29 | toArray(): Node[] 30 | ``` 31 | 32 | Returns an array of the nodes in the node set, in document order. 33 | 34 | ```typescript 35 | first(): Node 36 | ``` 37 | 38 | Returns the first node in the node set, in document order. 39 | 40 | ```typescript 41 | size: number; 42 | ``` 43 | 44 | Returns the number of nodes in this node set 45 | -------------------------------------------------------------------------------- /docs/function resolvers.md: -------------------------------------------------------------------------------- 1 | # Function Resolvers 2 | 3 | The methods on the [XPathEvaluator](XPathEvaluator.md) type can optionally take a function resolver to resolve 4 | function references in the XPath expression being evaluated. 5 | 6 | There are three ways to specify a function resolver and you can use any one of them depending on which is 7 | most suited to your particular situation. 8 | 9 | Note that if your functions are in a namespace (e.g. `fn:myFunction()`), you must use the second or third 10 | type as the plain object implementation does not support namespaces. 11 | 12 | ## Function implementations 13 | 14 | Custom XPath functions are implemented as JavaScript functions taking one or more arguments. 15 | 16 | The first argument passed in is a context object containing a number of properties relating to the execution context, 17 | the most important being `contextNode`, the context in which the function is being evaluated. 18 | 19 | The remaining arguments are the arguments passed into the XPath function, as instances of the `XPathResult` interface. 20 | Please see [the documentation on that interface](XPathResult.md) for details. 21 | 22 | As the return value, you can return a string, number, boolean, single node, or array of nodes. 23 | 24 | ## Function Resolver Type 1: Plain object 25 | 26 | A plain object with function names as the keys and function implementations as the values. 27 | 28 | Example usage: 29 | 30 | ```typescript 31 | const evaluator = xpath.parse('squareRoot(10)'); 32 | const aboutPi = evaluator.evaluateNumber({ 33 | functions: { 34 | squareRoot: (c: XPathContext, value: Expression) => { 35 | return Math.sqrt(value.numberValue); 36 | } 37 | } 38 | }); 39 | ``` 40 | 41 | ## Function Resolver Type 2: Function 42 | 43 | A function that takes a function name as its first parameter and an optional namespace URI as its second parameter 44 | and returns a function based on the name and namespace. 45 | 46 | Example usage: 47 | 48 | ```typescript 49 | const evaluator = xpath.parse('math:squareRoot(10)'); 50 | const aboutPi = evaluator.evaluateNumber({ 51 | functions(name: string, namespace: string) { 52 | if (name === 'squareRoot' && namespace === 'http://sample.org/math/') { 53 | return function(c: XPathContext, value: Expression) { 54 | return Math.sqrt(value.numberValue); 55 | }; 56 | } 57 | }, 58 | namespaces: { 59 | math: 'http://sample.org/math/' 60 | } 61 | }); 62 | ``` 63 | 64 | ## Function Resolver Type 3: Object with `getFunction` method 65 | 66 | An object with a method named `getFunction` that works in the same way as the function-based function resolver 67 | described above. 68 | 69 | Example usage: 70 | 71 | ```typescript 72 | const evaluator = xpath.parse('math:squareRoot(10)'); 73 | const aboutPi = evaluator.evaluateNumber({ 74 | functions: { 75 | getFunction(name: string, namespace: string) { 76 | if (name === 'squareRoot' && namespace === 'http://sample.org/math/') { 77 | return (c: XPathContext, value: Expression) => { 78 | return Math.sqrt(value.numberValue); 79 | }; 80 | } 81 | } 82 | }, 83 | namespaces: { 84 | math: 'http://sample.org/math/' 85 | } 86 | }); 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/namespace resolvers.md: -------------------------------------------------------------------------------- 1 | # Namespace Resolvers 2 | 3 | The methods on the [XPathEvaluator](XPathEvaluator.md) type can optionally take a namespace resolver to resolve 4 | namespace references in the XPath expression being evaluated. 5 | 6 | There are three ways to specify a namespace resolver and you can use any one of them depending on which is 7 | most suited to your particular situation. 8 | 9 | ## Namespace Resolver Type 1: Plain object 10 | 11 | A plain object with namespace prefixes as the keys and namespace URIs as the values: 12 | 13 | Example usage: 14 | 15 | ```typescript 16 | const evaluator = xpath.parse('/bk:book/hp:characters'); 17 | const characters = evaluator.select({ 18 | node: myBookNode, 19 | namespaces: { 20 | bk: 'http://sample.org/books/', 21 | hp: 'http://sample.org/harrypotter/' 22 | } 23 | }); 24 | ``` 25 | 26 | ## Namespace Resolver Type 2: Function 27 | 28 | A function that takes a namespace prefix as a parameter and returns the corresponding namespace URI. 29 | 30 | Example usage: 31 | 32 | ```typescript 33 | const evaluator = xpath.parse('/bk:book/hp:characters'); 34 | const characters = evaluator.select({ 35 | node: myBookNode, 36 | namespaces(prefix: string) { 37 | if (prefix === 'bk') { 38 | return 'http://sample.org/books/'; 39 | } 40 | if (prefix === 'hp') { 41 | return 'http://sample.org/books/'; 42 | } 43 | } 44 | }); 45 | ``` 46 | 47 | ## Namespace Resolver Type 3: Object with `getNamespace` method 48 | 49 | An object with a method named `getNamespace` that works in the same way as the function-based namespace resolver 50 | described above. 51 | 52 | Example usage: 53 | 54 | ```typescript 55 | const evaluator = xpath.parse('/bk:book/hp:characters'); 56 | const characters = evaluator.select({ 57 | node: myBookNode, 58 | namespaces: { 59 | getNamespace(prefix: string) { 60 | if (prefix === 'bk') { 61 | return 'http://sample.org/books/'; 62 | } 63 | if (prefix === 'hp') { 64 | return 'http://sample.org/books/'; 65 | } 66 | } 67 | } 68 | }); 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/parsed expressions.md: -------------------------------------------------------------------------------- 1 | # Using Parsed Expressions 2 | 3 | The `xpath.parse()` method allows pre-parsing an XPath expression and creating an XPath executor to evaluate the XPath as many times as needed. 4 | 5 | This can provide a performance benefit if you plan to evaluate the same XPath multiple times, because the expression only needs to be parsed once. 6 | 7 | This also provides access to additional features such as the use of variables and custom XPath functions, which are not available using the evaluation methods on the `xpath` object. 8 | 9 | #### xpath.parse(expression) 10 | 11 | Parses the specified XPath expression and returns an `XPathEvaluator`. See the [documentation page](XPathEvaluator.md) for API details. 12 | 13 | `expression` should be a string. 14 | 15 | Example usage: 16 | 17 | ```typescript 18 | const evaluator = xpath.parse('/book/characters'); 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/variable resolvers.md: -------------------------------------------------------------------------------- 1 | # Variable Resolvers 2 | 3 | The methods on the [XPathEvaluator](#) type can optionally take a variable resolver to resolve 4 | variable references in the XPath expression being evaluated. 5 | 6 | There are three ways to specify a variable resolver and you can use any one of them depending on which is 7 | most suited to your particular situation. 8 | 9 | Note that if your variables are in a namespace (e.g. `$myVars:var`), you must use the second or third 10 | type as the plain object implementation does not support namespaces. 11 | 12 | ## Variable values 13 | 14 | You can use any of five types of values to specify the values of variables: 15 | 16 | - string 17 | - number 18 | - boolean 19 | - single node (will be treated as a node set) 20 | - array of nodes or array-like collection of nodes (will be treated as a node set) 21 | 22 | ## Variable Resolver Type 1: Plain object 23 | 24 | A plain object with variable names as the keys and variable values as the values. 25 | 26 | Example usage: 27 | 28 | ```typescript 29 | const evaluator = xpath.parse('concat($character1, ", ", $character2, ", and ", $character3)'); 30 | const mainCharacters = evaluator.evaluateString({ 31 | variables: { 32 | character1: 'Harry', 33 | character2: 'Ron', 34 | character3: 'Hermione' 35 | } 36 | }); 37 | ``` 38 | 39 | ## Variable Resolver Type 2: Function 40 | 41 | A function that takes a variable name as its first parameter and an optional namespace URI as its second parameter 42 | and returns a value based on the name and namespace. 43 | 44 | Example usage: 45 | 46 | ```typescript 47 | const evaluator = xpath.parse('concat($hp:character1, ", ", $hp:character2, ", and ", $hp:character3)'); 48 | const mainCharacters = evaluator.evaluateString({ 49 | variables: function(name: string, namespace: string) { 50 | if (namespace === 'http://sample.org/harrypotter/') { 51 | switch (name) { 52 | case 'character1': 53 | return 'Harry'; 54 | case 'character2': 55 | return 'Ron'; 56 | case 'character3': 57 | return 'Hermione'; 58 | } 59 | } 60 | }, 61 | namespaces: { 62 | hp: 'http://sample.org/harrypotter/' 63 | } 64 | }); 65 | ``` 66 | 67 | ## Function Resolver Type 3: Object with `getFunction` method 68 | 69 | An object with a method named `getVariable` that works in the same way as the function-based variable resolver 70 | described above. 71 | 72 | Example usage: 73 | 74 | ```typescript 75 | const evaluator = xpath.parse('concat($hp:character1, ", ", $hp:character2, ", and ", $hp:character3)'); 76 | const mainCharacters = evaluator.evaluateString({ 77 | variables: { 78 | getVariable(name: string, namespace: string) { 79 | if (namespace === 'http://sample.org/harrypotter/') { 80 | switch (name) { 81 | case 'character1': 82 | return 'Harry'; 83 | case 'character2': 84 | return 'Ron'; 85 | case 'character3': 86 | return 'Hermione'; 87 | } 88 | } 89 | } 90 | }, 91 | namespaces: { 92 | hp: 'http://sample.org/harrypotter/' 93 | } 94 | }); 95 | ``` 96 | -------------------------------------------------------------------------------- /docs/xpath methods.md: -------------------------------------------------------------------------------- 1 | # xpath methods 2 | 3 | This page details the methods exposed on the `xpath` object. 4 | 5 | ### `xpath.parse(expression)` 6 | 7 | Creates a parsed expression. See the [documentation page](parsed%20expressions.md) for details. 8 | 9 | ### `xpath.select(expression[, node[, single]])` 10 | 11 | Evaluates an XPath expression and returns the result. The return value is determined based on the result type of the expression (which can always be predicted ahead of time based on the expression's syntax): 12 | 13 | - A boolean value if the expression evaluates to a boolean value. 14 | - A number if the expression evaluates to a numeric value. 15 | - A string if the expression evaluates to a string. 16 | - If the expression evaluates to a nodeset: 17 | 18 | - An array of 0 or more nodes if `single` is unspecified or falsy 19 | - A single node (the first node in document order) or `undefined` if `single` is truthy 20 | 21 | `node` is optional and if specified, is used as the context node for evaluating the expression. (It is necessary if the expression makes use of the current contex.) 22 | 23 | `single` is optional and is ignored if the expression evaluates to anything other than a nodeset. 24 | 25 | ### `xpath.select1(expression[, node])` 26 | 27 | Alias for [`xpath.select(expression, node, true)`](#xpathselectexpression-node-single). Selects a single node or value. 28 | 29 | ### `xpath.useNamespaces(mappings)` 30 | 31 | Produces a function with the same signature as [`xpath.select()`](#xpathselectexpression-node-single) that evaluates the provided xpath expression using the XML namespace definitions provided in `mapppings`. 32 | 33 | `mappings` should be an object with namespace prefixes as its property names and namespace URIs as its property values. 34 | 35 | Example usage: 36 | 37 | ```typescript 38 | const expr = xpath.useNamespaces({ hp: 'http://www.example.com/harryPotter', bk: 'http://www.example.com/books' }); 39 | const result = expr('/bk:books/bk:book[@name = "Harry Potter and the Half-Blood Prince"]/hp:characters', myBooks); 40 | ``` 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xpath-ts", 3 | "version": "1.3.13", 4 | "description": "DOM 3 and 4 XPath 1.0 implemention for browser and Node.js environment.", 5 | "author": { 6 | "name": "Cameron McCormack", 7 | "url": "https://mcc.id.au/" 8 | }, 9 | "license": "MIT", 10 | "keywords": [ 11 | "xpath", 12 | "xml", 13 | "dom" 14 | ], 15 | "contributors": [ 16 | { 17 | "name": "James Rishe" 18 | }, 19 | { 20 | "name": "Yaron Naveh", 21 | "url": "http://webservices20.blogspot.com/" 22 | }, 23 | { 24 | "name": "goto100" 25 | }, 26 | { 27 | "name": "Matus Zamborsky", 28 | "email": "zamborsky@gmail.com", 29 | "url": "https://github.com/backslash47" 30 | } 31 | ], 32 | "devDependencies": { 33 | "@types/chai": "^4.1.7", 34 | "@types/jsdom": "^12.2.0", 35 | "@types/mocha": "^5.2.5", 36 | "@types/node": "^10.12.0", 37 | "chai": "^4.2.0", 38 | "jsdom": "^13.0.0", 39 | "mocha": "^5.2.0", 40 | "ts-node": "^7.0.1", 41 | "tslint": "^5.11.0", 42 | "tslint-eslint-rules": "^5.3.1", 43 | "tslint-no-circular-imports": "^0.4.0", 44 | "typescript": "^3.5.1", 45 | "xmldom-ts": "^0.3.1" 46 | }, 47 | "main": "./dist/lib/index.js", 48 | "types": "./dist/types/index.d.ts", 49 | "scripts": { 50 | "test": "mocha --require ts-node/register test/**/*.ts", 51 | "lint": "tslint --project ./", 52 | "build": "tsc", 53 | "prepublishOnly": "npm run lint && npm run build && npm test" 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "git://github.com/backslash47/xpath" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import { XPathEvaluatorImpl } from './xpath-evaluator'; 2 | import { XPathParser } from './xpath-parser'; 3 | import { XPathResultImpl } from './xpath-result-impl'; 4 | import { FunctionResolver, VariableResolver } from './xpath-types'; 5 | 6 | const defaultEvaluator = new XPathEvaluatorImpl({}); 7 | 8 | export function select1(e: string, doc?: Node) { 9 | return select(e, doc, true); 10 | } 11 | 12 | export function select(e: string, doc?: Node, single: boolean = false) { 13 | return selectWithResolver(e, doc, null, single); 14 | } 15 | 16 | export function useNamespaces(mappings: { [key: string]: string | null }) { 17 | const resolver = { 18 | mappings: mappings || {}, 19 | lookupNamespaceURI(prefix: string) { 20 | return this.mappings[prefix] !== undefined ? this.mappings[prefix] : null; 21 | } 22 | }; 23 | 24 | return (e: string, doc: Node, single: boolean = false) => { 25 | return selectWithResolver(e, doc, resolver, single); 26 | }; 27 | } 28 | 29 | export function selectWithResolver( 30 | e: string, 31 | doc: Node | undefined, 32 | resolver: XPathNSResolver | null, 33 | single: boolean = false 34 | ) { 35 | const result = evaluate(e, doc, resolver, XPathResultImpl.ANY_TYPE, null); 36 | if (result.resultType === XPathResultImpl.STRING_TYPE) { 37 | return result.stringValue; 38 | } else if (result.resultType === XPathResultImpl.NUMBER_TYPE) { 39 | return result.numberValue; 40 | } else if (result.resultType === XPathResultImpl.BOOLEAN_TYPE) { 41 | return result.booleanValue; 42 | } else { 43 | if (single) { 44 | return result.nodes[0]; 45 | } 46 | 47 | return result.nodes; 48 | } 49 | } 50 | 51 | export function evaluate( 52 | expression: string, 53 | contextNode?: Node, 54 | resolver?: XPathNSResolver | null, 55 | type: number = 0, 56 | result?: XPathResult | null 57 | ) { 58 | return defaultEvaluator.evaluate(expression, contextNode!, resolver ? resolver : null, type, result ? result : null); 59 | } 60 | 61 | export function installXPathSupport( 62 | doc: Document, 63 | { fr, vr, p }: { fr?: FunctionResolver; vr?: VariableResolver; p?: XPathParser } 64 | ): Document & XPathEvaluator { 65 | const evaluator = new XPathEvaluatorImpl({ fr, vr, p }); 66 | 67 | (doc as any).createExpression = evaluator.createExpression; 68 | (doc as any).createNSResolver = evaluator.createNSResolver; 69 | (doc as any).evaluate = evaluator.evaluate; 70 | 71 | return (doc as unknown) as Document & XPathEvaluator; 72 | } 73 | -------------------------------------------------------------------------------- /src/avl-tree.ts: -------------------------------------------------------------------------------- 1 | import { isAttribute } from './utils/types'; 2 | 3 | // tslint:disable:no-bitwise 4 | 5 | export class AVLTree { 6 | left: AVLTree | null; 7 | right: AVLTree | null; 8 | node: Node; 9 | depth: number; 10 | 11 | constructor(n: Node) { 12 | this.left = null; 13 | this.right = null; 14 | this.node = n; 15 | this.depth = 1; 16 | } 17 | 18 | balance() { 19 | const ldepth = this.left == null ? 0 : this.left.depth; 20 | const rdepth = this.right == null ? 0 : this.right.depth; 21 | 22 | if (ldepth > rdepth + 1) { 23 | // LR or LL rotation 24 | const lldepth = this.left!.left == null ? 0 : this.left!.left!.depth; 25 | const lrdepth = this.left!.right == null ? 0 : this.left!.right!.depth; 26 | 27 | if (lldepth < lrdepth) { 28 | // LR rotation consists of a RR rotation of the left child 29 | this.left!.rotateRR(); 30 | // plus a LL rotation of this node, which happens anyway 31 | } 32 | this.rotateLL(); 33 | } else if (ldepth + 1 < rdepth) { 34 | // RR or RL rorarion 35 | const rrdepth = this.right!.right == null ? 0 : this.right!.right!.depth; 36 | const rldepth = this.right!.left == null ? 0 : this.right!.left!.depth; 37 | 38 | if (rldepth > rrdepth) { 39 | // RR rotation consists of a LL rotation of the right child 40 | this.right!.rotateLL(); 41 | // plus a RR rotation of this node, which happens anyway 42 | } 43 | this.rotateRR(); 44 | } 45 | } 46 | 47 | rotateLL() { 48 | // the left side is too long => rotate from the left (_not_ leftwards) 49 | const nodeBefore = this.node; 50 | const rightBefore = this.right; 51 | this.node = this.left!.node; 52 | this.right = this.left; 53 | this.left = this.left!.left; 54 | this.right!.left = this.right!.right; 55 | this.right!.right = rightBefore; 56 | this.right!.node = nodeBefore; 57 | this.right!.updateInNewLocation(); 58 | this.updateInNewLocation(); 59 | } 60 | 61 | rotateRR() { 62 | // the right side is too long => rotate from the right (_not_ rightwards) 63 | const nodeBefore = this.node; 64 | const leftBefore = this.left; 65 | this.node = this.right!.node; 66 | this.left = this.right!; 67 | this.right = this.right!.right; 68 | this.left.right = this.left.left; 69 | this.left.left = leftBefore; 70 | this.left.node = nodeBefore; 71 | this.left.updateInNewLocation(); 72 | this.updateInNewLocation(); 73 | } 74 | 75 | updateInNewLocation() { 76 | this.getDepthFromChildren(); 77 | } 78 | 79 | getDepthFromChildren() { 80 | this.depth = this.node == null ? 0 : 1; 81 | if (this.left != null) { 82 | this.depth = this.left.depth + 1; 83 | } 84 | if (this.right != null && this.depth <= this.right.depth) { 85 | this.depth = this.right.depth + 1; 86 | } 87 | } 88 | 89 | add(n: Node): boolean { 90 | if (n === this.node) { 91 | return false; 92 | } 93 | 94 | const o = nodeOrder(n, this.node); 95 | 96 | let ret = false; 97 | if (o === -1) { 98 | if (this.left == null) { 99 | this.left = new AVLTree(n); 100 | ret = true; 101 | } else { 102 | ret = this.left.add(n); 103 | if (ret) { 104 | this.balance(); 105 | } 106 | } 107 | } else if (o === 1) { 108 | if (this.right == null) { 109 | this.right = new AVLTree(n); 110 | ret = true; 111 | } else { 112 | ret = this.right.add(n); 113 | if (ret) { 114 | this.balance(); 115 | } 116 | } 117 | } 118 | 119 | if (ret) { 120 | this.getDepthFromChildren(); 121 | } 122 | return ret; 123 | } 124 | } 125 | 126 | function nodeOrder(n1: Node, n2: Node) { 127 | if (n1 === n2) { 128 | return 0; 129 | } 130 | 131 | if (n1.compareDocumentPosition !== undefined && n2.compareDocumentPosition !== undefined) { 132 | try { 133 | const cpos = n1.compareDocumentPosition(n2); 134 | 135 | if (cpos & 0x01) { 136 | // not in the same document; return an arbitrary result (is there a better way to do this) 137 | return 1; 138 | } 139 | if (cpos & 0x0a) { 140 | // n2 precedes or contains n1 141 | return 1; 142 | } 143 | if (cpos & 0x14) { 144 | // n2 follows or is contained by n1 145 | return -1; 146 | } 147 | 148 | return 0; 149 | } catch (_e) { 150 | // if compareDocumentPosition exists but is not supported ignore error 151 | } 152 | } 153 | 154 | let d1 = 0; 155 | let d2 = 0; 156 | for (let m1: Node | null = n1; m1 != null; m1 = parentNode(m1)) { 157 | d1++; 158 | } 159 | for (let m2: Node | null = n2; m2 != null; m2 = parentNode(m2)) { 160 | d2++; 161 | } 162 | 163 | // step up to same depth 164 | if (d1 > d2) { 165 | while (d1 > d2) { 166 | n1 = parentNode(n1)!; 167 | d1--; 168 | } 169 | if (n1 === n2) { 170 | return 1; 171 | } 172 | } else if (d2 > d1) { 173 | while (d2 > d1) { 174 | n2 = parentNode(n2)!; 175 | d2--; 176 | } 177 | if (n1 === n2) { 178 | return -1; 179 | } 180 | } 181 | 182 | let n1Par = parentNode(n1); 183 | let n2Par = parentNode(n2); 184 | 185 | // find common parent 186 | while (n1Par !== n2Par) { 187 | n1 = n1Par!; 188 | n2 = n2Par!; 189 | n1Par = parentNode(n1); 190 | n2Par = parentNode(n2); 191 | } 192 | 193 | const n1isAttr = isAttribute(n1); 194 | const n2isAttr = isAttribute(n2); 195 | 196 | if (n1isAttr && !n2isAttr) { 197 | return -1; 198 | } 199 | if (!n1isAttr && n2isAttr) { 200 | return 1; 201 | } 202 | 203 | if (n1Par) { 204 | const cn = n1isAttr ? (n1Par as Element).attributes : n1Par.childNodes; 205 | const len = cn.length; 206 | for (let i = 0; i < len; i += 1) { 207 | const n = cn[i]; 208 | if (n === n1) { 209 | return -1; 210 | } 211 | if (n === n2) { 212 | return 1; 213 | } 214 | } 215 | } 216 | 217 | throw new Error('Unexpected: could not determine node order'); 218 | } 219 | 220 | function parentNode(n: Node) { 221 | if (isAttribute(n)) { 222 | return n.ownerElement; 223 | } else { 224 | return n.parentNode; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/consts.ts: -------------------------------------------------------------------------------- 1 | export const XML_NAMESPACE_URI = 'http://www.w3.org/XML/1998/namespace'; 2 | export const XMLNS_NAMESPACE_URI = 'http://www.w3.org/2000/xmlns/'; 3 | -------------------------------------------------------------------------------- /src/function-call.ts: -------------------------------------------------------------------------------- 1 | import { FunctionResolverImpl } from './function-resolver'; 2 | import { Expression, XPathContext } from './xpath-types'; 3 | 4 | export class FunctionCall extends Expression { 5 | functionName: string; 6 | arguments: Expression[]; 7 | 8 | constructor(fn: string, args: Expression[]) { 9 | super(); 10 | 11 | this.functionName = fn; 12 | this.arguments = args; 13 | } 14 | 15 | evaluate(c: XPathContext) { 16 | const f = FunctionResolverImpl.getFunctionFromContext(this.functionName, c); 17 | 18 | if (f === undefined) { 19 | throw new Error('Unknown function ' + this.functionName); 20 | } 21 | 22 | return f(c, ...this.arguments); 23 | } 24 | 25 | toString() { 26 | const args = this.arguments.map((a) => a.toString()).join(', '); 27 | 28 | return `${this.functionName}(${args})`; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/function-resolver.ts: -------------------------------------------------------------------------------- 1 | import { Functions } from './functions'; 2 | import { resolveQName } from './utils/xml'; 3 | import { FunctionResolver, FunctionType, XPathContext } from './xpath-types'; 4 | 5 | export class FunctionResolverImpl implements FunctionResolver { 6 | static getFunctionFromContext(qName: string, context: XPathContext) { 7 | const parts = resolveQName(qName, context.namespaceResolver, context.contextNode, false); 8 | 9 | if (parts[0] === null) { 10 | throw new Error('Cannot resolve QName ' + name); 11 | } 12 | 13 | return context.functionResolver.getFunction(parts[1], parts[0]); 14 | } 15 | 16 | functions: { [key: string]: FunctionType | undefined }; 17 | 18 | constructor() { 19 | this.functions = {}; 20 | this.addStandardFunctions(); 21 | } 22 | 23 | addStandardFunctions() { 24 | this.functions['{}last'] = Functions.last; 25 | this.functions['{}position'] = Functions.position; 26 | this.functions['{}count'] = Functions.count; 27 | this.functions['{}id'] = Functions.id; 28 | this.functions['{}local-name'] = Functions.localName; 29 | this.functions['{}namespace-uri'] = Functions.namespaceURI; 30 | this.functions['{}name'] = Functions.name_; 31 | this.functions['{}string'] = Functions.string; 32 | this.functions['{}concat'] = Functions.concat; 33 | this.functions['{}starts-with'] = Functions.startsWith; 34 | this.functions['{}contains'] = Functions.contains; 35 | this.functions['{}substring-before'] = Functions.substringBefore; 36 | this.functions['{}substring-after'] = Functions.substringAfter; 37 | this.functions['{}substring'] = Functions.substring; 38 | this.functions['{}string-length'] = Functions.stringLength; 39 | this.functions['{}normalize-space'] = Functions.normalizeSpace; 40 | this.functions['{}translate'] = Functions.translate; 41 | this.functions['{}boolean'] = Functions.boolean_; 42 | this.functions['{}not'] = Functions.not; 43 | this.functions['{}true'] = Functions.true_; 44 | this.functions['{}false'] = Functions.false_; 45 | this.functions['{}lang'] = Functions.lang; 46 | this.functions['{}number'] = Functions.number; 47 | this.functions['{}sum'] = Functions.sum; 48 | this.functions['{}floor'] = Functions.floor; 49 | this.functions['{}ceiling'] = Functions.ceiling; 50 | this.functions['{}round'] = Functions.round; 51 | } 52 | 53 | addFunction(ns: string, ln: string, f: FunctionType) { 54 | this.functions['{' + ns + '}' + ln] = f; 55 | } 56 | 57 | getFunction(localName: string, namespace: string) { 58 | return this.functions['{' + namespace + '}' + localName]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/functions.ts: -------------------------------------------------------------------------------- 1 | import { XML_NAMESPACE_URI } from './consts'; 2 | import { isSpace } from './utils/character'; 3 | import { isAttribute, isDocument, isElement, isProcessingInstruction } from './utils/types'; 4 | import { getElementById } from './utils/xml'; 5 | import { Expression, XBoolean, XNodeSet, XNumber, XPathContext, XString } from './xpath-types'; 6 | 7 | // tslint:disable:prefer-for-of 8 | 9 | export class Functions { 10 | static last(c: XPathContext, ...args: Expression[]) { 11 | checkArguments(0, args, 'last'); 12 | 13 | return new XNumber(c.contextSize); 14 | } 15 | 16 | static position(c: XPathContext, ...args: Expression[]) { 17 | checkArguments(0, args, 'position'); 18 | 19 | return new XNumber(c.contextPosition); 20 | } 21 | 22 | static count(c: XPathContext, ...args: Expression[]) { 23 | checkArguments(1, args, 'count'); 24 | 25 | const ns = args[0].evaluate(c); 26 | if (!(ns instanceof XNodeSet)) { 27 | throw new Error('Function count expects (node-set)'); 28 | } 29 | return new XNumber(ns.size); 30 | } 31 | 32 | static id(c: XPathContext, ...args: Expression[]) { 33 | checkArguments(1, args, 'id'); 34 | 35 | const eRes: Expression = args[0].evaluate(c); 36 | 37 | let ids: string[]; 38 | 39 | if (eRes instanceof XNodeSet) { 40 | ids = eRes.toArray().map((node) => XNodeSet.prototype.stringForNode(node)); 41 | } else { 42 | const id = eRes.stringValue; 43 | ids = id.split(/[\x0d\x0a\x09\x20]+/); 44 | } 45 | 46 | const ns = new XNodeSet(); 47 | const doc = isDocument(c.contextNode) ? c.contextNode : c.contextNode.ownerDocument!; 48 | 49 | for (let i = 0; i < ids.length; i++) { 50 | let n: Element | null; 51 | if (doc.getElementById) { 52 | n = doc.getElementById(ids[i]); 53 | } else { 54 | n = getElementById(doc, ids[i]); 55 | } 56 | if (n != null) { 57 | ns.add(n); 58 | } 59 | } 60 | return ns; 61 | } 62 | 63 | static localName(c: XPathContext, ...args: Expression[]) { 64 | checkArguments([0, 1], args, 'local-name'); 65 | 66 | let n: Node | null; 67 | if (args.length === 0) { 68 | n = c.contextNode; 69 | } else { 70 | const eRes = args[0].evaluate(c); 71 | if (!(eRes instanceof XNodeSet)) { 72 | throw new Error('Function local-name expects (node-set?)'); 73 | } 74 | n = eRes.first(); 75 | } 76 | 77 | if (n == null) { 78 | return new XString(''); 79 | } 80 | 81 | if ((isElement(n) || isAttribute(n)) && n.localName != null) { 82 | return new XString(n.localName); 83 | } else if (isProcessingInstruction(n)) { 84 | return new XString(n.target); 85 | } else { 86 | return new XString(n.nodeName != null ? n.nodeName : ''); 87 | } 88 | } 89 | 90 | static namespaceURI(c: XPathContext, ...args: Expression[]) { 91 | checkArguments([0, 1], args, 'namespace-uri'); 92 | 93 | let n: Node | null; 94 | if (args.length === 0) { 95 | n = c.contextNode; 96 | } else { 97 | const eRes = args[0].evaluate(c); 98 | if (!(eRes instanceof XNodeSet)) { 99 | throw new Error('Function namspace-uri expects (node-set?)'); 100 | } 101 | n = eRes.first(); 102 | } 103 | 104 | if (n == null) { 105 | return new XString(''); 106 | } 107 | return new XString(n.namespaceURI); 108 | } 109 | 110 | static name_(c: XPathContext, ...args: Expression[]) { 111 | checkArguments([0, 1], args, 'name'); 112 | 113 | let n: Node | null; 114 | if (args.length === 0) { 115 | n = c.contextNode; 116 | } else { 117 | const eRes = args[0].evaluate(c); 118 | if (!(eRes instanceof XNodeSet)) { 119 | throw new Error('Function name expects (node-set?)'); 120 | } 121 | n = eRes.first(); 122 | } 123 | 124 | if (n == null) { 125 | return new XString(''); 126 | } 127 | if (isElement(n)) { 128 | return new XString(n.nodeName); 129 | } else if (isAttribute(n)) { 130 | return new XString(n.name || n.nodeName); 131 | } else if (isProcessingInstruction(n)) { 132 | return new XString(n.target || n.nodeName); 133 | } else if ((n as any).localName == null) { 134 | return new XString(''); 135 | } else { 136 | return new XString((n as any).localName); 137 | } 138 | } 139 | 140 | static string(c: XPathContext, ...args: Expression[]) { 141 | checkArguments([0, 1], args, 'string'); 142 | 143 | if (args.length === 0) { 144 | return new XString(XNodeSet.prototype.stringForNode(c.contextNode)); 145 | } else { 146 | return args[0].evaluate(c).string; 147 | } 148 | } 149 | 150 | static concat(c: XPathContext, ...args: Expression[]) { 151 | if (args.length < 2) { 152 | throw new Error('Function concat expects (string, string[, string]*)'); 153 | } 154 | let s = ''; 155 | for (let i = 0; i < args.length; i++) { 156 | s += args[i].evaluate(c).stringValue; 157 | } 158 | return new XString(s); 159 | } 160 | 161 | static startsWith(c: XPathContext, ...args: Expression[]) { 162 | checkArguments(2, args, 'starts-with'); 163 | 164 | const s1 = args[0].evaluate(c).stringValue; 165 | const s2 = args[1].evaluate(c).stringValue; 166 | return new XBoolean(s1.substring(0, s2.length) === s2); 167 | } 168 | 169 | static contains(c: XPathContext, ...args: Expression[]) { 170 | checkArguments(2, args, 'contains'); 171 | 172 | const s1 = args[0].evaluate(c).stringValue; 173 | const s2 = args[1].evaluate(c).stringValue; 174 | return new XBoolean(s1.indexOf(s2) !== -1); 175 | } 176 | 177 | static substringBefore(c: XPathContext, ...args: Expression[]) { 178 | checkArguments(2, args, 'substring-before'); 179 | 180 | const s1 = args[0].evaluate(c).stringValue; 181 | const s2 = args[1].evaluate(c).stringValue; 182 | return new XString(s1.substring(0, s1.indexOf(s2))); 183 | } 184 | 185 | static substringAfter(c: XPathContext, ...args: Expression[]) { 186 | checkArguments(2, args, 'substring-after'); 187 | 188 | const s1 = args[0].evaluate(c).stringValue; 189 | const s2 = args[1].evaluate(c).stringValue; 190 | if (s2.length === 0) { 191 | return new XString(s1); 192 | } 193 | const i = s1.indexOf(s2); 194 | if (i === -1) { 195 | return new XString(''); 196 | } 197 | return new XString(s1.substring(i + s2.length)); 198 | } 199 | 200 | static substring(c: XPathContext, ...args: Expression[]) { 201 | checkArguments([2, 3], args, 'substring'); 202 | 203 | const s = args[0].evaluate(c).stringValue; 204 | const n1 = Math.round(args[1].evaluate(c).numberValue) - 1; 205 | const n2 = args.length === 3 ? n1 + Math.round(args[2].evaluate(c).numberValue) : undefined; 206 | return new XString(s.substring(n1, n2)); 207 | } 208 | 209 | static stringLength(c: XPathContext, ...args: Expression[]) { 210 | checkArguments([0, 1], args, 'string-length'); 211 | 212 | let s: string; 213 | if (args.length === 0) { 214 | s = XNodeSet.prototype.stringForNode(c.contextNode) || ''; 215 | } else { 216 | s = args[0].evaluate(c).stringValue; 217 | } 218 | 219 | return new XNumber(s.length); 220 | } 221 | 222 | static normalizeSpace(c: XPathContext, ...args: Expression[]) { 223 | checkArguments([0, 1], args, 'normalize-space'); 224 | 225 | let s: string; 226 | if (args.length === 0) { 227 | s = XNodeSet.prototype.stringForNode(c.contextNode); 228 | } else { 229 | s = args[0].evaluate(c).stringValue; 230 | } 231 | 232 | let i = 0; 233 | let j = s.length - 1; 234 | while (isSpace(s.charCodeAt(j))) { 235 | j--; 236 | } 237 | let t = ''; 238 | while (i <= j && isSpace(s.charCodeAt(i))) { 239 | i++; 240 | } 241 | while (i <= j) { 242 | if (isSpace(s.charCodeAt(i))) { 243 | t += ' '; 244 | while (i <= j && isSpace(s.charCodeAt(i))) { 245 | i++; 246 | } 247 | } else { 248 | t += s.charAt(i); 249 | i++; 250 | } 251 | } 252 | return new XString(t); 253 | } 254 | 255 | static translate(c: XPathContext, ...args: Expression[]) { 256 | checkArguments(3, args, 'translate'); 257 | 258 | const value = args[0].evaluate(c).stringValue; 259 | const from = args[1].evaluate(c).stringValue; 260 | const to = args[2].evaluate(c).stringValue; 261 | 262 | const cMap = [...from].reduce( 263 | (acc, ch, i) => { 264 | if (!(ch in acc)) { 265 | acc[ch] = i > to.length ? '' : to[i]; 266 | } 267 | return acc; 268 | }, 269 | {} as { [key: string]: string } 270 | ); 271 | 272 | const t = [...value].map((ch) => (ch in cMap ? cMap[ch] : ch)).join(''); 273 | 274 | return new XString(t); 275 | } 276 | 277 | static boolean_(c: XPathContext, ...args: Expression[]) { 278 | checkArguments(1, args, 'boolean'); 279 | 280 | return args[0].evaluate(c).bool; 281 | } 282 | 283 | static not(c: XPathContext, ...args: Expression[]) { 284 | checkArguments(1, args, 'not'); 285 | 286 | return args[0].evaluate(c).bool.not(); 287 | } 288 | 289 | static true_(_c: XPathContext, ...args: Expression[]) { 290 | checkArguments(0, args, 'true'); 291 | 292 | return XBoolean.TRUE; 293 | } 294 | 295 | static false_(_c: XPathContext, ...args: Expression[]) { 296 | checkArguments(0, args, 'false'); 297 | 298 | return XBoolean.FALSE; 299 | } 300 | 301 | static lang(c: XPathContext, ...args: Expression[]) { 302 | checkArguments(1, args, 'lang'); 303 | 304 | let lang: string | null = null; 305 | for (let n: Node | null = c.contextNode; n != null && !isDocument(n); n = n.parentNode) { 306 | if (isElement(n)) { 307 | const a = n.getAttributeNS(XML_NAMESPACE_URI, 'lang'); 308 | if (a != null) { 309 | lang = String(a); 310 | break; 311 | } 312 | } 313 | } 314 | if (lang == null) { 315 | return XBoolean.FALSE; 316 | } 317 | const s = args[0].evaluate(c).stringValue; 318 | return new XBoolean( 319 | lang.substring(0, s.length) === s && (lang.length === s.length || lang.charAt(s.length) === '-') 320 | ); 321 | } 322 | 323 | static number(c: XPathContext, ...args: Expression[]) { 324 | checkArguments([0, 1], args, 'number'); 325 | 326 | if (args.length === 0) { 327 | return new XNumber(XNodeSet.prototype.stringForNode(c.contextNode)); 328 | } 329 | return args[0].evaluate(c).number; 330 | } 331 | 332 | static sum(c: XPathContext, ...args: Expression[]) { 333 | checkArguments(1, args, 'sum'); 334 | const ns = args[0].evaluate(c); 335 | 336 | if (!(ns instanceof XNodeSet)) { 337 | throw new Error('Function sum expects (node-set)'); 338 | } 339 | 340 | const ua = ns.toUnsortedArray(); 341 | let n = 0; 342 | for (let i = 0; i < ua.length; i++) { 343 | n += new XNumber(XNodeSet.prototype.stringForNode(ua[i])).numberValue; 344 | } 345 | return new XNumber(n); 346 | } 347 | 348 | static floor(c: XPathContext, ...args: Expression[]) { 349 | checkArguments(1, args, 'floor'); 350 | 351 | return new XNumber(Math.floor(args[0].evaluate(c).numberValue)); 352 | } 353 | 354 | static ceiling(c: XPathContext, ...args: Expression[]) { 355 | checkArguments(1, args, 'ceiling'); 356 | 357 | return new XNumber(Math.ceil(args[0].evaluate(c).numberValue)); 358 | } 359 | 360 | static round(c: XPathContext, ...args: Expression[]) { 361 | checkArguments(1, args, 'round'); 362 | 363 | return new XNumber(Math.round(args[0].evaluate(c).numberValue)); 364 | } 365 | } 366 | 367 | function checkArguments(expected: number | number[], args: Expression[], name: string) { 368 | if (typeof expected === 'number') { 369 | expected = [expected]; 370 | } 371 | 372 | if (!expected.includes(args.length)) { 373 | throw new Error(`Function ${name} expects ${expected.join(' or ')} arguments instead of ${args.length}`); 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { XPath } from './xpath'; 2 | export { XPathParser } from './xpath-parser'; 3 | export { XPathResultImpl as XPathResult } from './xpath-result-impl'; 4 | 5 | export { Step } from './step'; 6 | export { NodeTest } from './node-test'; 7 | export { BarOperation } from './operations/bar-operation'; 8 | 9 | export { NamespaceResolverImpl as NamespaceResolver } from './namespace-resolver'; 10 | export { FunctionResolverImpl as FunctionResolver } from './function-resolver'; 11 | export { VariableResolverImpl as VariableResolver } from './variable-resolver'; 12 | export { XPathEvaluatorImpl as XPathEvaluator } from './xpath-evaluator'; 13 | 14 | export { Expression, XNodeSet, XBoolean, XString, XNumber, XPathContext } from './xpath-types'; 15 | 16 | export { evaluate, select, useNamespaces, selectWithResolver, select1, installXPathSupport } from './api'; 17 | 18 | export { 19 | convertValue, 20 | EvalOptions, 21 | makeFunctionResolver, 22 | makeNSResolver, 23 | makeVariableResolver, 24 | parse 25 | } from './parse-api'; 26 | -------------------------------------------------------------------------------- /src/location-path.ts: -------------------------------------------------------------------------------- 1 | import { Step } from './step'; 2 | 3 | export class LocationPath { 4 | absolute: boolean; 5 | steps: Step[]; 6 | 7 | constructor(abs: boolean, steps: Step[]) { 8 | this.absolute = abs; 9 | this.steps = steps; 10 | } 11 | 12 | toString() { 13 | return (this.absolute ? '/' : '') + this.steps.map((s) => s.toString()).join('/'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/namespace-resolver.ts: -------------------------------------------------------------------------------- 1 | import { XML_NAMESPACE_URI, XMLNS_NAMESPACE_URI } from './consts'; 2 | import { PathExpr } from './path-expr'; 3 | import { isAttribute, isDocument, isElement } from './utils/types'; 4 | import { NamespaceResolver } from './xpath-types'; 5 | 6 | export class NamespaceResolverImpl implements NamespaceResolver { 7 | getNamespace(prefix: string, n: Node | null) { 8 | if (prefix === 'xml') { 9 | return XML_NAMESPACE_URI; 10 | } else if (prefix === 'xmlns') { 11 | return XMLNS_NAMESPACE_URI; 12 | } 13 | if (isDocument(n)) { 14 | n = n.documentElement; 15 | } else if (isAttribute(n)) { 16 | n = PathExpr.getOwnerElement(n); 17 | } 18 | 19 | while (n != null && isElement(n)) { 20 | const nnm = n.attributes; 21 | for (let i = 0; i < nnm.length; i++) { 22 | const a = nnm.item(i)!; 23 | const aname = a.name || a.nodeName; 24 | if ((aname === 'xmlns' && prefix === '') || aname === 'xmlns:' + prefix) { 25 | return String(a.value || a.nodeValue); 26 | } 27 | } 28 | n = n.parentNode; 29 | } 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/node-test.ts: -------------------------------------------------------------------------------- 1 | import { XPathNamespace } from './xpath-namespace'; 2 | import { XPathContext } from './xpath-types'; 3 | 4 | // tslint:disable:member-ordering 5 | 6 | export class NodeTest { 7 | static NAMETESTANY = 0; 8 | static NAMETESTPREFIXANY = 1; 9 | static NAMETESTQNAME = 2; 10 | static COMMENT = 3; 11 | static TEXT = 4; 12 | static PI = 5; 13 | static NODE = 6; 14 | 15 | static isNodeType(types: number[]) { 16 | return (n: Node) => { 17 | return types.includes(n.nodeType) || ((n as Attr).specified && types.includes(2)); // DOM4 support 18 | }; 19 | } 20 | 21 | // create invariant node test for certain node types 22 | static makeNodeTypeTest(type: number, nodeTypes: number[], stringVal: string): NodeTest { 23 | return new class extends NodeTest { 24 | constructor() { 25 | super(type); 26 | } 27 | 28 | matches = NodeTest.isNodeType(nodeTypes); 29 | toString = () => stringVal; 30 | }(); 31 | } 32 | 33 | static hasPrefix(node: Node) { 34 | return (node as any).prefix || (node.nodeName || (node as any).tagName).indexOf(':') !== -1; 35 | } 36 | 37 | static isElementOrAttribute = NodeTest.isNodeType([1, 2]); 38 | 39 | static nameSpaceMatches(prefix: string | null, xpc: XPathContext, n: Node) { 40 | const nNamespace = n.namespaceURI || ''; 41 | 42 | if (!prefix) { 43 | return !nNamespace || (xpc.allowAnyNamespaceForNoPrefix && !NodeTest.hasPrefix(n)); 44 | } 45 | 46 | const ns = xpc.namespaceResolver.getNamespace(prefix, n); 47 | 48 | if (ns == null) { 49 | // throw new Error('Cannot resolve QName ' + prefix); 50 | return false; 51 | } 52 | 53 | return ns === nNamespace; 54 | } 55 | 56 | static localNameMatches = (localName: string, xpc: XPathContext, n: Node) => { 57 | const nLocalName = (n as Element).localName || n.nodeName; 58 | 59 | return xpc.caseInsensitive ? localName.toLowerCase() === nLocalName.toLowerCase() : localName === nLocalName; 60 | // tslint:disable-next-line:semicolon 61 | }; 62 | 63 | // tslint:disable-next-line:variable-name 64 | static NameTestPrefixAny = class extends NodeTest { 65 | prefix: string; 66 | 67 | constructor(prefix: string) { 68 | super(NodeTest.NAMETESTPREFIXANY); 69 | 70 | this.prefix = prefix; 71 | } 72 | 73 | matches(n: Node, xpc: XPathContext) { 74 | return NodeTest.isElementOrAttribute(n) && NodeTest.nameSpaceMatches(this.prefix, xpc, n); 75 | } 76 | 77 | toString() { 78 | return this.prefix + ':*'; 79 | } 80 | }; 81 | 82 | // tslint:disable-next-line:variable-name 83 | static NameTestQName = class extends NodeTest { 84 | name: string; 85 | prefix: string | null; 86 | localName: string; 87 | 88 | constructor(name: string) { 89 | super(NodeTest.NAMETESTQNAME); 90 | 91 | const nameParts = name.split(':'); 92 | 93 | this.name = name; 94 | this.prefix = nameParts.length > 1 ? nameParts[0] : null; 95 | this.localName = nameParts[nameParts.length > 1 ? 1 : 0]; 96 | } 97 | 98 | matches(n: Node, xpc: XPathContext) { 99 | return ( 100 | NodeTest.isNodeType([1, 2, XPathNamespace.XPATH_NAMESPACE_NODE])(n) && 101 | NodeTest.nameSpaceMatches(this.prefix, xpc, n) && 102 | NodeTest.localNameMatches(this.localName, xpc, n) 103 | ); 104 | } 105 | toString() { 106 | return this.name; 107 | } 108 | }; 109 | 110 | // tslint:disable-next-line:variable-name 111 | static PITest = class extends NodeTest { 112 | name: string; 113 | 114 | constructor(name: string) { 115 | super(NodeTest.PI); 116 | 117 | this.name = name; 118 | } 119 | 120 | matches(n: Node, _xpc: XPathContext) { 121 | return NodeTest.isNodeType([7])(n) && ((n as ProcessingInstruction).target || n.nodeName) === this.name; 122 | } 123 | 124 | toString() { 125 | return `processing-instruction("${this.name}")`; 126 | } 127 | }; 128 | 129 | // elements, attributes, namespaces 130 | static nameTestAny = NodeTest.makeNodeTypeTest( 131 | NodeTest.NAMETESTANY, 132 | [1, 2, XPathNamespace.XPATH_NAMESPACE_NODE], 133 | '*' 134 | ); 135 | // text, cdata 136 | static textTest = NodeTest.makeNodeTypeTest(NodeTest.TEXT, [3, 4], 'text()'); 137 | static commentTest = NodeTest.makeNodeTypeTest(NodeTest.COMMENT, [8], 'comment()'); 138 | // elements, attributes, text, cdata, PIs, comments, document nodes 139 | static nodeTest = NodeTest.makeNodeTypeTest(NodeTest.NODE, [1, 2, 3, 4, 7, 8, 9], 'node()'); 140 | static anyPiTest = NodeTest.makeNodeTypeTest(NodeTest.PI, [7], 'processing-instruction()'); 141 | 142 | type: number; 143 | 144 | constructor(type: number) { 145 | this.type = type; 146 | } 147 | 148 | toString(): string { 149 | return ''; 150 | } 151 | 152 | matches(_n: Node, _xpc: XPathContext): boolean { 153 | // tslint:disable-next-line:no-console 154 | console.warn('unknown node test type'); 155 | return false; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/node-x-path-ns-resolver.ts: -------------------------------------------------------------------------------- 1 | import { NamespaceResolverImpl } from './namespace-resolver'; 2 | import { NamespaceResolver } from './xpath-types'; 3 | 4 | export class NodeXPathNSResolver { 5 | node?: Node; 6 | namespaceResolver: NamespaceResolver; 7 | 8 | constructor(n?: Node) { 9 | this.node = n; 10 | this.namespaceResolver = new NamespaceResolverImpl(); 11 | } 12 | 13 | lookupNamespaceURI(prefix: string) { 14 | return this.namespaceResolver.getNamespace(prefix, this.node !== undefined ? this.node : null); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/operations/and-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { BinaryOperation } from './binary-operation'; 3 | 4 | export class AndOperation extends BinaryOperation { 5 | evaluate(c: XPathContext) { 6 | const b = this.lhs.evaluate(c).bool; 7 | if (!b.booleanValue) { 8 | return b; 9 | } 10 | return this.rhs.evaluate(c).bool; 11 | } 12 | 13 | toString() { 14 | return '(' + this.lhs.toString() + ' and ' + this.rhs.toString() + ')'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/operations/bar-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { BinaryOperation } from './binary-operation'; 3 | 4 | export class BarOperation extends BinaryOperation { 5 | evaluate(c: XPathContext) { 6 | return this.lhs.evaluate(c).nodeset.union(this.rhs.evaluate(c).nodeset); 7 | } 8 | 9 | toString() { 10 | return this.lhs.toString() + ' | ' + this.rhs.toString(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/operations/binary-operation.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../xpath-types'; 2 | 3 | export class BinaryOperation extends Expression { 4 | lhs: Expression; 5 | rhs: Expression; 6 | 7 | constructor(lhs: Expression, rhs: Expression) { 8 | super(); 9 | 10 | this.lhs = lhs; 11 | this.rhs = rhs; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/operations/div-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { BinaryOperation } from './binary-operation'; 3 | 4 | export class DivOperation extends BinaryOperation { 5 | evaluate(c: XPathContext) { 6 | return this.lhs.evaluate(c).number.div(this.rhs.evaluate(c).number); 7 | } 8 | 9 | toString() { 10 | return '(' + this.lhs.toString() + ' div ' + this.rhs.toString() + ')'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/operations/equals-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { BinaryOperation } from './binary-operation'; 3 | 4 | export class EqualsOperation extends BinaryOperation { 5 | evaluate(c: XPathContext) { 6 | return this.lhs.evaluate(c).equals(this.rhs.evaluate(c)); 7 | } 8 | 9 | toString() { 10 | return '(' + this.lhs.toString() + ' = ' + this.rhs.toString() + ')'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/operations/greater-than-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { BinaryOperation } from './binary-operation'; 3 | 4 | export class GreaterThanOperation extends BinaryOperation { 5 | evaluate(c: XPathContext) { 6 | return this.lhs.evaluate(c).greaterthan(this.rhs.evaluate(c)); 7 | } 8 | 9 | toString() { 10 | return '(' + this.lhs.toString() + ' > ' + this.rhs.toString() + ')'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/operations/greater-than-or-equal-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { BinaryOperation } from './binary-operation'; 3 | 4 | export class GreaterThanOrEqualOperation extends BinaryOperation { 5 | evaluate(c: XPathContext) { 6 | return this.lhs.evaluate(c).greaterthanorequal(this.rhs.evaluate(c)); 7 | } 8 | 9 | toString() { 10 | return '(' + this.lhs.toString() + ' >= ' + this.rhs.toString() + ')'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/operations/less-than-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { BinaryOperation } from './binary-operation'; 3 | 4 | export class LessThanOperation extends BinaryOperation { 5 | evaluate(c: XPathContext) { 6 | return this.lhs.evaluate(c).lessthan(this.rhs.evaluate(c)); 7 | } 8 | 9 | toString() { 10 | return '(' + this.lhs.toString() + ' < ' + this.rhs.toString() + ')'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/operations/less-than-or-equal-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { BinaryOperation } from './binary-operation'; 3 | 4 | export class LessThanOrEqualOperation extends BinaryOperation { 5 | evaluate(c: XPathContext) { 6 | return this.lhs.evaluate(c).lessthanorequal(this.rhs.evaluate(c)); 7 | } 8 | 9 | toString() { 10 | return '(' + this.lhs.toString() + ' <= ' + this.rhs.toString() + ')'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/operations/minus-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { BinaryOperation } from './binary-operation'; 3 | 4 | export class MinusOperation extends BinaryOperation { 5 | evaluate(c: XPathContext) { 6 | return this.lhs.evaluate(c).number.minus(this.rhs.evaluate(c).number); 7 | } 8 | 9 | toString() { 10 | return '(' + this.lhs.toString() + ' - ' + this.rhs.toString() + ')'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/operations/mod-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { BinaryOperation } from './binary-operation'; 3 | 4 | export class ModOperation extends BinaryOperation { 5 | evaluate(c: XPathContext) { 6 | return this.lhs.evaluate(c).number.mod(this.rhs.evaluate(c).number); 7 | } 8 | 9 | toString() { 10 | return '(' + this.lhs.toString() + ' mod ' + this.rhs.toString() + ')'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/operations/multiply-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { BinaryOperation } from './binary-operation'; 3 | 4 | export class MultiplyOperation extends BinaryOperation { 5 | evaluate(c: XPathContext) { 6 | return this.lhs.evaluate(c).number.multiply(this.rhs.evaluate(c).number); 7 | } 8 | 9 | toString() { 10 | return '(' + this.lhs.toString() + ' * ' + this.rhs.toString() + ')'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/operations/not-equals-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { BinaryOperation } from './binary-operation'; 3 | 4 | export class NotEqualOperation extends BinaryOperation { 5 | evaluate(c: XPathContext) { 6 | return this.lhs.evaluate(c).notequal(this.rhs.evaluate(c)); 7 | } 8 | 9 | toString() { 10 | return '(' + this.lhs.toString() + ' != ' + this.rhs.toString() + ')'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/operations/or-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { BinaryOperation } from './binary-operation'; 3 | 4 | export class OrOperation extends BinaryOperation { 5 | evaluate(c: XPathContext) { 6 | const b = this.lhs.evaluate(c).bool; 7 | if (b.booleanValue) { 8 | return b; 9 | } 10 | return this.rhs.evaluate(c).bool; 11 | } 12 | 13 | toString() { 14 | return '(' + this.lhs.toString() + ' or ' + this.rhs.toString() + ')'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/operations/plus-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { BinaryOperation } from './binary-operation'; 3 | 4 | export class PlusOperation extends BinaryOperation { 5 | evaluate(c: XPathContext) { 6 | return this.lhs.evaluate(c).number.plus(this.rhs.evaluate(c).number); 7 | } 8 | 9 | toString() { 10 | return '(' + this.lhs.toString() + ' + ' + this.rhs.toString() + ')'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/operations/unary-minus-operation.ts: -------------------------------------------------------------------------------- 1 | import { XPathContext } from '../xpath-types'; 2 | import { UnaryOperation } from './unary-operation'; 3 | 4 | export class UnaryMinusOperation extends UnaryOperation { 5 | evaluate(c: XPathContext) { 6 | return this.rhs.evaluate(c).number.negate(); 7 | } 8 | 9 | toString() { 10 | return '-' + this.rhs.toString(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/operations/unary-operation.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '../xpath-types'; 2 | 3 | export class UnaryOperation extends Expression { 4 | rhs: Expression; 5 | 6 | constructor(rhs: Expression) { 7 | super(); 8 | 9 | this.rhs = rhs; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/parse-api.ts: -------------------------------------------------------------------------------- 1 | import { FunctionResolverImpl } from './function-resolver'; 2 | import { NamespaceResolverImpl } from './namespace-resolver'; 3 | import { VariableResolverImpl } from './variable-resolver'; 4 | import { XPath } from './xpath'; 5 | import { XPathParser } from './xpath-parser'; 6 | import { Expression, FunctionType, XBoolean, XNodeSet, XNumber, XPathContext, XString } from './xpath-types'; 7 | 8 | const parser = new XPathParser(); 9 | 10 | const defaultNSResolver = new NamespaceResolverImpl(); 11 | const defaultFunctionResolver = new FunctionResolverImpl(); 12 | const defaultVariableResolver = new VariableResolverImpl(); 13 | 14 | function makeNSResolverFromFunction(func: (prefix: string, n: Node) => string | null) { 15 | return { 16 | getNamespace(prefix: string, node: Node) { 17 | const ns = func(prefix, node); 18 | 19 | return ns || defaultNSResolver.getNamespace(prefix, node); 20 | } 21 | }; 22 | } 23 | 24 | function makeNSResolverFromObject(obj: { getNamespace: (prefix: string, n: Node) => string | null }) { 25 | return makeNSResolverFromFunction(obj.getNamespace.bind(obj)); 26 | } 27 | 28 | function makeNSResolverFromMap(map: { [key: string]: string | null }) { 29 | return makeNSResolverFromFunction((prefix) => { 30 | return map[prefix]; 31 | }); 32 | } 33 | 34 | export function makeNSResolver(resolver: any) { 35 | if (resolver && typeof resolver.getNamespace === 'function') { 36 | return makeNSResolverFromObject(resolver); 37 | } 38 | 39 | if (typeof resolver === 'function') { 40 | return makeNSResolverFromFunction(resolver); 41 | } 42 | 43 | // assume prefix -> uri mapping 44 | if (typeof resolver === 'object') { 45 | return makeNSResolverFromMap(resolver); 46 | } 47 | 48 | return defaultNSResolver; 49 | } 50 | 51 | /** Converts native JavaScript types to their XPath library equivalent */ 52 | export function convertValue(value: any) { 53 | if (value == null) { 54 | return null; 55 | } 56 | 57 | if (value instanceof XString || value instanceof XBoolean || value instanceof XNumber || value instanceof XNodeSet) { 58 | return value; 59 | } 60 | 61 | switch (typeof value) { 62 | case 'string': 63 | return new XString(value); 64 | case 'boolean': 65 | return new XBoolean(value); 66 | case 'number': 67 | return new XNumber(value); 68 | } 69 | 70 | // assume node(s) 71 | const ns = new XNodeSet(); 72 | ns.addArray([].concat(value)); 73 | return ns; 74 | } 75 | 76 | function makeEvaluator(func: FunctionType): FunctionType { 77 | return (context: XPathContext, ...args: Expression[]) => { 78 | args = args.map((a) => a.evaluate(context)); 79 | const result = func(context, ...args); 80 | 81 | return convertValue(result)!; // if result is not null convertValue will not return null 82 | }; 83 | } 84 | 85 | function makeFunctionResolverFromFunction(func: (name: string, ns: string) => FunctionType | undefined) { 86 | return { 87 | getFunction(name: string, namespace: string) { 88 | const found = func(name, namespace); 89 | if (found != null) { 90 | return makeEvaluator(found); 91 | } 92 | return defaultFunctionResolver.getFunction(name, namespace); 93 | } 94 | }; 95 | } 96 | 97 | function makeFunctionResolverFromObject(obj: { 98 | getFunction: (name: string, namespace: string) => FunctionType | undefined; 99 | }) { 100 | return makeFunctionResolverFromFunction(obj.getFunction.bind(obj)); 101 | } 102 | 103 | function makeFunctionResolverFromMap(map: { [key: string]: FunctionType | undefined }) { 104 | return makeFunctionResolverFromFunction((name: string) => { 105 | return map[name]; 106 | }); 107 | } 108 | 109 | export function makeFunctionResolver(resolver: any) { 110 | if (resolver && typeof resolver.getFunction === 'function') { 111 | return makeFunctionResolverFromObject(resolver); 112 | } 113 | 114 | if (typeof resolver === 'function') { 115 | return makeFunctionResolverFromFunction(resolver); 116 | } 117 | 118 | // assume map 119 | if (typeof resolver === 'object') { 120 | return makeFunctionResolverFromMap(resolver); 121 | } 122 | 123 | return defaultFunctionResolver; 124 | } 125 | 126 | function makeVariableResolverFromFunction(func: (n: string, ns: string) => any) { 127 | return { 128 | getVariable(name: string, namespace: string) { 129 | const value = func(name, namespace); 130 | return convertValue(value); 131 | } 132 | }; 133 | } 134 | 135 | export function makeVariableResolver(resolver: any) { 136 | if (resolver) { 137 | if (typeof resolver.getVariable === 'function') { 138 | return makeVariableResolverFromFunction(resolver.getVariable.bind(resolver)); 139 | } 140 | 141 | if (typeof resolver === 'function') { 142 | return makeVariableResolverFromFunction(resolver); 143 | } 144 | 145 | // assume map 146 | if (typeof resolver === 'object') { 147 | return makeVariableResolverFromFunction((name: string) => { 148 | return resolver[name]; 149 | }); 150 | } 151 | } 152 | 153 | return defaultVariableResolver; 154 | } 155 | 156 | export interface EvalOptions { 157 | [key: string]: any; 158 | } 159 | 160 | function copyIfPresent(prop: string, dest: { [key: string]: any }, source: EvalOptions) { 161 | if (prop in source) { 162 | dest[prop] = source[prop]; 163 | } 164 | } 165 | 166 | function makeContext(options?: EvalOptions) { 167 | const context = new XPathContext(new VariableResolverImpl(), new NamespaceResolverImpl(), new FunctionResolverImpl()); 168 | 169 | if (options !== undefined) { 170 | context.namespaceResolver = makeNSResolver(options.namespaces); 171 | context.functionResolver = makeFunctionResolver(options.functions); 172 | context.variableResolver = makeVariableResolver(options.variables); 173 | context.expressionContextNode = options.node; 174 | 175 | copyIfPresent('allowAnyNamespaceForNoPrefix', context, options); 176 | copyIfPresent('isHtml', context, options); 177 | } else { 178 | context.namespaceResolver = defaultNSResolver; 179 | } 180 | 181 | return context; 182 | } 183 | 184 | function evaluate(parsedExpression: XPath, options?: { [key: string]: any }) { 185 | const context = makeContext(options); 186 | 187 | return parsedExpression.evaluate(context); 188 | } 189 | 190 | export function parse(xpath: string) { 191 | const parsed = parser.parse(xpath); 192 | 193 | return new class { 194 | expression = parsed; 195 | 196 | evaluate(options?: EvalOptions) { 197 | return evaluate(this.expression, options); 198 | } 199 | 200 | evaluateNumber(options?: EvalOptions) { 201 | return this.evaluate(options).numberValue; 202 | } 203 | 204 | evaluateString(options?: EvalOptions) { 205 | return this.evaluate(options).stringValue; 206 | } 207 | 208 | evaluateBoolean(options?: EvalOptions) { 209 | return this.evaluate(options).booleanValue; 210 | } 211 | 212 | evaluateNodeSet(options?: EvalOptions) { 213 | return this.evaluate(options).nodeset; 214 | } 215 | 216 | select(options?: EvalOptions) { 217 | return this.evaluateNodeSet(options).toArray(); 218 | } 219 | 220 | select1(options?: EvalOptions) { 221 | return this.select(options)[0]; 222 | } 223 | }(); 224 | } 225 | -------------------------------------------------------------------------------- /src/path-expr.ts: -------------------------------------------------------------------------------- 1 | import { XML_NAMESPACE_URI, XMLNS_NAMESPACE_URI } from './consts'; 2 | import { LocationPath } from './location-path'; 3 | import { Step } from './step'; 4 | import { isAttribute, isDocument, isElement } from './utils/types'; 5 | import { XPathNamespace } from './xpath-namespace'; 6 | import { Expression, XNodeSet, XNumber, XPathContext, XString } from './xpath-types'; 7 | 8 | export class PathExpr extends Expression { 9 | static predicateMatches(pred: Expression, c: XPathContext) { 10 | const res = pred.evaluate(c); 11 | 12 | return res instanceof XNumber ? c.contextPosition === res.numberValue : res.booleanValue; 13 | } 14 | 15 | static applyLocationPath(locationPath: LocationPath | undefined, xpc: XPathContext, nodes: Node[]) { 16 | if (!locationPath) { 17 | return nodes; 18 | } 19 | 20 | const startNodes = locationPath.absolute ? [PathExpr.getRoot(xpc, nodes)] : nodes; 21 | 22 | return PathExpr.applySteps(locationPath.steps, xpc, startNodes); 23 | } 24 | 25 | static applyStep(step: Step, xpc: XPathContext, node: Node): Node[] { 26 | const newNodes = []; 27 | xpc.contextNode = node; 28 | 29 | switch (step.axis) { 30 | case Step.ANCESTOR: { 31 | // look at all the ancestor nodes 32 | if (xpc.contextNode === xpc.virtualRoot) { 33 | break; 34 | } 35 | let m: Node | null; 36 | if (isAttribute(xpc.contextNode)) { 37 | m = PathExpr.getOwnerElement(xpc.contextNode); 38 | } else { 39 | m = xpc.contextNode.parentNode; 40 | } 41 | while (m != null) { 42 | if (step.nodeTest.matches(m, xpc)) { 43 | newNodes.push(m); 44 | } 45 | if (m === xpc.virtualRoot) { 46 | break; 47 | } 48 | m = m.parentNode; 49 | } 50 | break; 51 | } 52 | case Step.ANCESTORORSELF: { 53 | // look at all the ancestor nodes and the current node 54 | for ( 55 | let m: Node | null = xpc.contextNode; 56 | m != null; 57 | m = isAttribute(m) ? PathExpr.getOwnerElement(m) : m.parentNode 58 | ) { 59 | if (step.nodeTest.matches(m, xpc)) { 60 | newNodes.push(m); 61 | } 62 | if (m === xpc.virtualRoot) { 63 | break; 64 | } 65 | } 66 | break; 67 | } 68 | case Step.ATTRIBUTE: { 69 | // look at the attributes 70 | const nnm = (xpc.contextNode as Element).attributes; 71 | if (nnm != null) { 72 | for (let k = 0; k < nnm.length; k++) { 73 | const m = nnm.item(k)!; 74 | if (step.nodeTest.matches(m, xpc)) { 75 | newNodes.push(m); 76 | } 77 | } 78 | } 79 | break; 80 | } 81 | case Step.CHILD: { 82 | // look at all child elements 83 | for (let m: Node | null = xpc.contextNode.firstChild; m != null; m = m.nextSibling) { 84 | if (step.nodeTest.matches(m, xpc)) { 85 | newNodes.push(m); 86 | } 87 | } 88 | break; 89 | } 90 | case Step.DESCENDANT: { 91 | // look at all descendant nodes 92 | const st: Array = [xpc.contextNode.firstChild]; 93 | while (st.length > 0) { 94 | for (let m = st.pop(); m != null; ) { 95 | if (step.nodeTest.matches(m, xpc)) { 96 | newNodes.push(m); 97 | } 98 | if (m.firstChild != null) { 99 | st.push(m.nextSibling); 100 | m = m.firstChild; 101 | } else { 102 | m = m.nextSibling; 103 | } 104 | } 105 | } 106 | break; 107 | } 108 | case Step.DESCENDANTORSELF: { 109 | // look at self 110 | if (step.nodeTest.matches(xpc.contextNode, xpc)) { 111 | newNodes.push(xpc.contextNode); 112 | } 113 | // look at all descendant nodes 114 | const st: Array = [xpc.contextNode.firstChild]; 115 | while (st.length > 0) { 116 | for (let m: Node | null = st.pop()!; m != null; ) { 117 | if (step.nodeTest.matches(m, xpc)) { 118 | newNodes.push(m); 119 | } 120 | if (m.firstChild != null) { 121 | st.push(m.nextSibling); 122 | m = m.firstChild; 123 | } else { 124 | m = m.nextSibling; 125 | } 126 | } 127 | } 128 | break; 129 | } 130 | case Step.FOLLOWING: { 131 | if (xpc.contextNode === xpc.virtualRoot) { 132 | break; 133 | } 134 | 135 | for (let n: Node | null = xpc.contextNode; n != null; n = n.parentNode) { 136 | for (let m: Node | null = n.nextSibling; m != null; m = m.nextSibling) { 137 | if (step.nodeTest.matches(m, xpc)) { 138 | newNodes.push(m); 139 | } 140 | } 141 | } 142 | 143 | break; 144 | } 145 | case Step.FOLLOWINGSIBLING: { 146 | if (xpc.contextNode === xpc.virtualRoot) { 147 | break; 148 | } 149 | for (let m = xpc.contextNode.nextSibling; m != null; m = m.nextSibling) { 150 | if (step.nodeTest.matches(m, xpc)) { 151 | newNodes.push(m); 152 | } 153 | } 154 | break; 155 | } 156 | case Step.NAMESPACE: { 157 | const n: { [name: string]: string } = {}; 158 | if (isElement(xpc.contextNode)) { 159 | n.xml = XML_NAMESPACE_URI; 160 | n.xmlns = XMLNS_NAMESPACE_URI; 161 | for (let m: Node | null = xpc.contextNode; m != null && isElement(m); m = m.parentNode) { 162 | for (let k = 0; k < m.attributes.length; k++) { 163 | const attr = m.attributes.item(k)!; 164 | const nm = String(attr.name); 165 | if (nm === 'xmlns') { 166 | if (n[''] === undefined) { 167 | n[''] = attr.value; 168 | } 169 | } else if (nm.length > 6 && nm.substring(0, 6) === 'xmlns:') { 170 | const pre = nm.substring(6, nm.length); 171 | if (n[pre] === undefined) { 172 | n[pre] = attr.value; 173 | } 174 | } 175 | } 176 | } 177 | 178 | // tslint:disable-next-line:forin 179 | for (const pre in n) { 180 | const nsn = new XPathNamespace(pre, n[pre], xpc.contextNode); 181 | if (step.nodeTest.matches(nsn, xpc)) { 182 | newNodes.push(nsn); 183 | } 184 | } 185 | } 186 | break; 187 | } 188 | case Step.PARENT: { 189 | let m: Node | null = null; 190 | if (xpc.contextNode !== xpc.virtualRoot) { 191 | if (isAttribute(xpc.contextNode)) { 192 | m = PathExpr.getOwnerElement(xpc.contextNode); 193 | } else { 194 | m = xpc.contextNode.parentNode; 195 | } 196 | } 197 | if (m != null && step.nodeTest.matches(m, xpc)) { 198 | newNodes.push(m); 199 | } 200 | break; 201 | } 202 | case Step.PRECEDING: { 203 | if (xpc.contextNode === xpc.virtualRoot) { 204 | break; 205 | } 206 | 207 | for (let n: Node | null = xpc.contextNode; n != null; n = n.parentNode) { 208 | for (let m: Node | null = n.previousSibling; m != null; m = m.previousSibling) { 209 | if (step.nodeTest.matches(m, xpc)) { 210 | newNodes.push(m); 211 | } 212 | } 213 | } 214 | 215 | break; 216 | } 217 | case Step.PRECEDINGSIBLING: { 218 | if (xpc.contextNode === xpc.virtualRoot) { 219 | break; 220 | } 221 | for (let m = xpc.contextNode.previousSibling; m != null; m = m.previousSibling) { 222 | if (step.nodeTest.matches(m, xpc)) { 223 | newNodes.push(m); 224 | } 225 | } 226 | break; 227 | } 228 | case Step.SELF: { 229 | if (step.nodeTest.matches(xpc.contextNode, xpc)) { 230 | newNodes.push(xpc.contextNode); 231 | } 232 | break; 233 | } 234 | default: 235 | } 236 | 237 | return newNodes; 238 | } 239 | 240 | static getRoot(xpc: XPathContext, nodes: Node[]) { 241 | const firstNode = nodes[0]; 242 | 243 | if (isDocument(firstNode)) { 244 | return firstNode; 245 | } 246 | 247 | if (xpc.virtualRoot) { 248 | return xpc.virtualRoot; 249 | } 250 | 251 | const ownerDoc = firstNode.ownerDocument; 252 | 253 | if (ownerDoc) { 254 | return ownerDoc; 255 | } 256 | 257 | // IE 5.5 doesn't have ownerDocument? 258 | let n = firstNode; 259 | while (n.parentNode != null) { 260 | n = n.parentNode; 261 | } 262 | return n; 263 | } 264 | 265 | static applySteps(steps: Step[], xpc: XPathContext, nodes: Node[]) { 266 | return steps.reduce((inNodes: Node[], step: Step) => { 267 | return inNodes 268 | .map((node) => { 269 | return PathExpr.applyPredicates(step.predicates, xpc, PathExpr.applyStep(step, xpc, node)); 270 | }) 271 | .flat(); 272 | }, nodes); 273 | } 274 | 275 | static getOwnerElement(n: Attr) { 276 | if (n.ownerElement) { 277 | return n.ownerElement; 278 | } 279 | 280 | return null; 281 | } 282 | 283 | static applyPredicates(predicates: Expression[], c: XPathContext, nodes: Node[]) { 284 | return predicates.reduce((inNodes, pred) => { 285 | const ctx = c.extend({ contextSize: inNodes.length }); 286 | 287 | return inNodes.filter((node: Node, i: number) => { 288 | return PathExpr.predicateMatches(pred, ctx.extend({ contextNode: node, contextPosition: i + 1 })); 289 | }); 290 | }, nodes); 291 | } 292 | 293 | static predicateString = (e: Expression) => `[${e.toString()}]`; 294 | static predicatesString = (es: Expression[]) => es.map(PathExpr.predicateString).join(''); 295 | 296 | filter?: Expression; 297 | filterPredicates?: Expression[]; 298 | locationPath?: LocationPath; 299 | 300 | constructor( 301 | filter: Expression | undefined, 302 | filterPreds: Expression[] | undefined, 303 | locpath: LocationPath | undefined 304 | ) { 305 | super(); 306 | 307 | this.filter = filter; 308 | this.filterPredicates = filterPreds; 309 | this.locationPath = locpath; 310 | } 311 | 312 | applyFilter(c: XPathContext, xpc: XPathContext) { 313 | if (!this.filter) { 314 | return { nodes: [c.contextNode!] }; 315 | } 316 | 317 | const ns = this.filter.evaluate(c); 318 | 319 | if (!(ns instanceof XNodeSet)) { 320 | if ((this.filterPredicates != null && this.filterPredicates.length > 0) || this.locationPath != null) { 321 | throw new Error('Path expression filter must evaluate to a nodeset if predicates or location path are used'); 322 | } 323 | 324 | return { nonNodes: ns }; 325 | } 326 | 327 | return { 328 | nodes: PathExpr.applyPredicates(this.filterPredicates || [], xpc, ns.toUnsortedArray()) 329 | }; 330 | } 331 | 332 | evaluate(c: XPathContext) { 333 | const xpc = c.clone(); 334 | 335 | const filterResult = this.applyFilter(c, xpc); 336 | 337 | if (filterResult.nonNodes !== undefined) { 338 | return filterResult.nonNodes; 339 | } 340 | 341 | // filterResult.nodes is defined because nonNodes is not 342 | const ns = new XNodeSet(); 343 | ns.addArray(PathExpr.applyLocationPath(this.locationPath, xpc, filterResult.nodes!)); 344 | return ns; 345 | } 346 | 347 | toString() { 348 | if (this.filter !== undefined) { 349 | const filterStr = this.filter.toString(); 350 | 351 | if (this.filter instanceof XString) { 352 | return `'${filterStr}'`; 353 | } 354 | if (this.filterPredicates !== undefined && this.filterPredicates.length) { 355 | return `(${filterStr})` + PathExpr.predicatesString(this.filterPredicates); 356 | } 357 | if (this.locationPath !== undefined) { 358 | return filterStr + (this.locationPath.absolute ? '' : '/') + this.locationPath.toString(); 359 | } 360 | 361 | return filterStr; 362 | } 363 | 364 | if (this.locationPath !== undefined) { 365 | return this.locationPath.toString(); 366 | } else { 367 | return ''; 368 | } 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/step.ts: -------------------------------------------------------------------------------- 1 | import { NodeTest } from './node-test'; 2 | import { Expression } from './xpath-types'; 3 | 4 | export class Step { 5 | static ANCESTOR = 0; 6 | static ANCESTORORSELF = 1; 7 | static ATTRIBUTE = 2; 8 | static CHILD = 3; 9 | static DESCENDANT = 4; 10 | static DESCENDANTORSELF = 5; 11 | static FOLLOWING = 6; 12 | static FOLLOWINGSIBLING = 7; 13 | static NAMESPACE = 8; 14 | static PARENT = 9; 15 | static PRECEDING = 10; 16 | static PRECEDINGSIBLING = 11; 17 | static SELF = 12; 18 | 19 | static STEPNAMES = ([ 20 | [Step.ANCESTOR, 'ancestor'], 21 | [Step.ANCESTORORSELF, 'ancestor-or-self'], 22 | [Step.ATTRIBUTE, 'attribute'], 23 | [Step.CHILD, 'child'], 24 | [Step.DESCENDANT, 'descendant'], 25 | [Step.DESCENDANTORSELF, 'descendant-or-self'], 26 | [Step.FOLLOWING, 'following'], 27 | [Step.FOLLOWINGSIBLING, 'following-sibling'], 28 | [Step.NAMESPACE, 'namespace'], 29 | [Step.PARENT, 'parent'], 30 | [Step.PRECEDING, 'preceding'], 31 | [Step.PRECEDINGSIBLING, 'preceding-sibling'], 32 | [Step.SELF, 'self'] 33 | ] as Array<[number, string]>).reduce( 34 | (acc, x) => { 35 | return (acc[x[0]] = x[1]), acc; 36 | }, 37 | {} as { [key: number]: string } 38 | ); 39 | 40 | static predicateString = (e: Expression) => `[${e.toString()}]`; 41 | static predicatesString = (es: Expression[]) => es.map(Step.predicateString).join(''); 42 | 43 | axis: number; 44 | nodeTest: NodeTest; 45 | predicates: Expression[]; 46 | 47 | constructor(axis: number, nodetest: NodeTest, preds: Expression[]) { 48 | this.axis = axis; 49 | this.nodeTest = nodetest; 50 | this.predicates = preds; 51 | } 52 | 53 | toString() { 54 | return Step.STEPNAMES[this.axis] + '::' + this.nodeTest.toString() + Step.predicatesString(this.predicates); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/utils/character.ts: -------------------------------------------------------------------------------- 1 | export function isSpace(c: number) { 2 | return c === 0x9 || c === 0xd || c === 0xa || c === 0x20; 3 | } 4 | 5 | export function isLetter(c: number) { 6 | return ( 7 | (c >= 0x0041 && c <= 0x005a) || 8 | (c >= 0x0061 && c <= 0x007a) || 9 | (c >= 0x00c0 && c <= 0x00d6) || 10 | (c >= 0x00d8 && c <= 0x00f6) || 11 | (c >= 0x00f8 && c <= 0x00ff) || 12 | (c >= 0x0100 && c <= 0x0131) || 13 | (c >= 0x0134 && c <= 0x013e) || 14 | (c >= 0x0141 && c <= 0x0148) || 15 | (c >= 0x014a && c <= 0x017e) || 16 | (c >= 0x0180 && c <= 0x01c3) || 17 | (c >= 0x01cd && c <= 0x01f0) || 18 | (c >= 0x01f4 && c <= 0x01f5) || 19 | (c >= 0x01fa && c <= 0x0217) || 20 | (c >= 0x0250 && c <= 0x02a8) || 21 | (c >= 0x02bb && c <= 0x02c1) || 22 | c === 0x0386 || 23 | (c >= 0x0388 && c <= 0x038a) || 24 | c === 0x038c || 25 | (c >= 0x038e && c <= 0x03a1) || 26 | (c >= 0x03a3 && c <= 0x03ce) || 27 | (c >= 0x03d0 && c <= 0x03d6) || 28 | c === 0x03da || 29 | c === 0x03dc || 30 | c === 0x03de || 31 | c === 0x03e0 || 32 | (c >= 0x03e2 && c <= 0x03f3) || 33 | (c >= 0x0401 && c <= 0x040c) || 34 | (c >= 0x040e && c <= 0x044f) || 35 | (c >= 0x0451 && c <= 0x045c) || 36 | (c >= 0x045e && c <= 0x0481) || 37 | (c >= 0x0490 && c <= 0x04c4) || 38 | (c >= 0x04c7 && c <= 0x04c8) || 39 | (c >= 0x04cb && c <= 0x04cc) || 40 | (c >= 0x04d0 && c <= 0x04eb) || 41 | (c >= 0x04ee && c <= 0x04f5) || 42 | (c >= 0x04f8 && c <= 0x04f9) || 43 | (c >= 0x0531 && c <= 0x0556) || 44 | c === 0x0559 || 45 | (c >= 0x0561 && c <= 0x0586) || 46 | (c >= 0x05d0 && c <= 0x05ea) || 47 | (c >= 0x05f0 && c <= 0x05f2) || 48 | (c >= 0x0621 && c <= 0x063a) || 49 | (c >= 0x0641 && c <= 0x064a) || 50 | (c >= 0x0671 && c <= 0x06b7) || 51 | (c >= 0x06ba && c <= 0x06be) || 52 | (c >= 0x06c0 && c <= 0x06ce) || 53 | (c >= 0x06d0 && c <= 0x06d3) || 54 | c === 0x06d5 || 55 | (c >= 0x06e5 && c <= 0x06e6) || 56 | (c >= 0x0905 && c <= 0x0939) || 57 | c === 0x093d || 58 | (c >= 0x0958 && c <= 0x0961) || 59 | (c >= 0x0985 && c <= 0x098c) || 60 | (c >= 0x098f && c <= 0x0990) || 61 | (c >= 0x0993 && c <= 0x09a8) || 62 | (c >= 0x09aa && c <= 0x09b0) || 63 | c === 0x09b2 || 64 | (c >= 0x09b6 && c <= 0x09b9) || 65 | (c >= 0x09dc && c <= 0x09dd) || 66 | (c >= 0x09df && c <= 0x09e1) || 67 | (c >= 0x09f0 && c <= 0x09f1) || 68 | (c >= 0x0a05 && c <= 0x0a0a) || 69 | (c >= 0x0a0f && c <= 0x0a10) || 70 | (c >= 0x0a13 && c <= 0x0a28) || 71 | (c >= 0x0a2a && c <= 0x0a30) || 72 | (c >= 0x0a32 && c <= 0x0a33) || 73 | (c >= 0x0a35 && c <= 0x0a36) || 74 | (c >= 0x0a38 && c <= 0x0a39) || 75 | (c >= 0x0a59 && c <= 0x0a5c) || 76 | c === 0x0a5e || 77 | (c >= 0x0a72 && c <= 0x0a74) || 78 | (c >= 0x0a85 && c <= 0x0a8b) || 79 | c === 0x0a8d || 80 | (c >= 0x0a8f && c <= 0x0a91) || 81 | (c >= 0x0a93 && c <= 0x0aa8) || 82 | (c >= 0x0aaa && c <= 0x0ab0) || 83 | (c >= 0x0ab2 && c <= 0x0ab3) || 84 | (c >= 0x0ab5 && c <= 0x0ab9) || 85 | c === 0x0abd || 86 | c === 0x0ae0 || 87 | (c >= 0x0b05 && c <= 0x0b0c) || 88 | (c >= 0x0b0f && c <= 0x0b10) || 89 | (c >= 0x0b13 && c <= 0x0b28) || 90 | (c >= 0x0b2a && c <= 0x0b30) || 91 | (c >= 0x0b32 && c <= 0x0b33) || 92 | (c >= 0x0b36 && c <= 0x0b39) || 93 | c === 0x0b3d || 94 | (c >= 0x0b5c && c <= 0x0b5d) || 95 | (c >= 0x0b5f && c <= 0x0b61) || 96 | (c >= 0x0b85 && c <= 0x0b8a) || 97 | (c >= 0x0b8e && c <= 0x0b90) || 98 | (c >= 0x0b92 && c <= 0x0b95) || 99 | (c >= 0x0b99 && c <= 0x0b9a) || 100 | c === 0x0b9c || 101 | (c >= 0x0b9e && c <= 0x0b9f) || 102 | (c >= 0x0ba3 && c <= 0x0ba4) || 103 | (c >= 0x0ba8 && c <= 0x0baa) || 104 | (c >= 0x0bae && c <= 0x0bb5) || 105 | (c >= 0x0bb7 && c <= 0x0bb9) || 106 | (c >= 0x0c05 && c <= 0x0c0c) || 107 | (c >= 0x0c0e && c <= 0x0c10) || 108 | (c >= 0x0c12 && c <= 0x0c28) || 109 | (c >= 0x0c2a && c <= 0x0c33) || 110 | (c >= 0x0c35 && c <= 0x0c39) || 111 | (c >= 0x0c60 && c <= 0x0c61) || 112 | (c >= 0x0c85 && c <= 0x0c8c) || 113 | (c >= 0x0c8e && c <= 0x0c90) || 114 | (c >= 0x0c92 && c <= 0x0ca8) || 115 | (c >= 0x0caa && c <= 0x0cb3) || 116 | (c >= 0x0cb5 && c <= 0x0cb9) || 117 | c === 0x0cde || 118 | (c >= 0x0ce0 && c <= 0x0ce1) || 119 | (c >= 0x0d05 && c <= 0x0d0c) || 120 | (c >= 0x0d0e && c <= 0x0d10) || 121 | (c >= 0x0d12 && c <= 0x0d28) || 122 | (c >= 0x0d2a && c <= 0x0d39) || 123 | (c >= 0x0d60 && c <= 0x0d61) || 124 | (c >= 0x0e01 && c <= 0x0e2e) || 125 | c === 0x0e30 || 126 | (c >= 0x0e32 && c <= 0x0e33) || 127 | (c >= 0x0e40 && c <= 0x0e45) || 128 | (c >= 0x0e81 && c <= 0x0e82) || 129 | c === 0x0e84 || 130 | (c >= 0x0e87 && c <= 0x0e88) || 131 | c === 0x0e8a || 132 | c === 0x0e8d || 133 | (c >= 0x0e94 && c <= 0x0e97) || 134 | (c >= 0x0e99 && c <= 0x0e9f) || 135 | (c >= 0x0ea1 && c <= 0x0ea3) || 136 | c === 0x0ea5 || 137 | c === 0x0ea7 || 138 | (c >= 0x0eaa && c <= 0x0eab) || 139 | (c >= 0x0ead && c <= 0x0eae) || 140 | c === 0x0eb0 || 141 | (c >= 0x0eb2 && c <= 0x0eb3) || 142 | c === 0x0ebd || 143 | (c >= 0x0ec0 && c <= 0x0ec4) || 144 | (c >= 0x0f40 && c <= 0x0f47) || 145 | (c >= 0x0f49 && c <= 0x0f69) || 146 | (c >= 0x10a0 && c <= 0x10c5) || 147 | (c >= 0x10d0 && c <= 0x10f6) || 148 | c === 0x1100 || 149 | (c >= 0x1102 && c <= 0x1103) || 150 | (c >= 0x1105 && c <= 0x1107) || 151 | c === 0x1109 || 152 | (c >= 0x110b && c <= 0x110c) || 153 | (c >= 0x110e && c <= 0x1112) || 154 | c === 0x113c || 155 | c === 0x113e || 156 | c === 0x1140 || 157 | c === 0x114c || 158 | c === 0x114e || 159 | c === 0x1150 || 160 | (c >= 0x1154 && c <= 0x1155) || 161 | c === 0x1159 || 162 | (c >= 0x115f && c <= 0x1161) || 163 | c === 0x1163 || 164 | c === 0x1165 || 165 | c === 0x1167 || 166 | c === 0x1169 || 167 | (c >= 0x116d && c <= 0x116e) || 168 | (c >= 0x1172 && c <= 0x1173) || 169 | c === 0x1175 || 170 | c === 0x119e || 171 | c === 0x11a8 || 172 | c === 0x11ab || 173 | (c >= 0x11ae && c <= 0x11af) || 174 | (c >= 0x11b7 && c <= 0x11b8) || 175 | c === 0x11ba || 176 | (c >= 0x11bc && c <= 0x11c2) || 177 | c === 0x11eb || 178 | c === 0x11f0 || 179 | c === 0x11f9 || 180 | (c >= 0x1e00 && c <= 0x1e9b) || 181 | (c >= 0x1ea0 && c <= 0x1ef9) || 182 | (c >= 0x1f00 && c <= 0x1f15) || 183 | (c >= 0x1f18 && c <= 0x1f1d) || 184 | (c >= 0x1f20 && c <= 0x1f45) || 185 | (c >= 0x1f48 && c <= 0x1f4d) || 186 | (c >= 0x1f50 && c <= 0x1f57) || 187 | c === 0x1f59 || 188 | c === 0x1f5b || 189 | c === 0x1f5d || 190 | (c >= 0x1f5f && c <= 0x1f7d) || 191 | (c >= 0x1f80 && c <= 0x1fb4) || 192 | (c >= 0x1fb6 && c <= 0x1fbc) || 193 | c === 0x1fbe || 194 | (c >= 0x1fc2 && c <= 0x1fc4) || 195 | (c >= 0x1fc6 && c <= 0x1fcc) || 196 | (c >= 0x1fd0 && c <= 0x1fd3) || 197 | (c >= 0x1fd6 && c <= 0x1fdb) || 198 | (c >= 0x1fe0 && c <= 0x1fec) || 199 | (c >= 0x1ff2 && c <= 0x1ff4) || 200 | (c >= 0x1ff6 && c <= 0x1ffc) || 201 | c === 0x2126 || 202 | (c >= 0x212a && c <= 0x212b) || 203 | c === 0x212e || 204 | (c >= 0x2180 && c <= 0x2182) || 205 | (c >= 0x3041 && c <= 0x3094) || 206 | (c >= 0x30a1 && c <= 0x30fa) || 207 | (c >= 0x3105 && c <= 0x312c) || 208 | (c >= 0xac00 && c <= 0xd7a3) || 209 | (c >= 0x4e00 && c <= 0x9fa5) || 210 | c === 0x3007 || 211 | (c >= 0x3021 && c <= 0x3029) 212 | ); 213 | } 214 | 215 | export function isNCNameChar(c: number) { 216 | return ( 217 | (c >= 0x0030 && c <= 0x0039) || 218 | (c >= 0x0660 && c <= 0x0669) || 219 | (c >= 0x06f0 && c <= 0x06f9) || 220 | (c >= 0x0966 && c <= 0x096f) || 221 | (c >= 0x09e6 && c <= 0x09ef) || 222 | (c >= 0x0a66 && c <= 0x0a6f) || 223 | (c >= 0x0ae6 && c <= 0x0aef) || 224 | (c >= 0x0b66 && c <= 0x0b6f) || 225 | (c >= 0x0be7 && c <= 0x0bef) || 226 | (c >= 0x0c66 && c <= 0x0c6f) || 227 | (c >= 0x0ce6 && c <= 0x0cef) || 228 | (c >= 0x0d66 && c <= 0x0d6f) || 229 | (c >= 0x0e50 && c <= 0x0e59) || 230 | (c >= 0x0ed0 && c <= 0x0ed9) || 231 | (c >= 0x0f20 && c <= 0x0f29) || 232 | c === 0x002e || 233 | c === 0x002d || 234 | c === 0x005f || 235 | isLetter(c) || 236 | (c >= 0x0300 && c <= 0x0345) || 237 | (c >= 0x0360 && c <= 0x0361) || 238 | (c >= 0x0483 && c <= 0x0486) || 239 | (c >= 0x0591 && c <= 0x05a1) || 240 | (c >= 0x05a3 && c <= 0x05b9) || 241 | (c >= 0x05bb && c <= 0x05bd) || 242 | c === 0x05bf || 243 | (c >= 0x05c1 && c <= 0x05c2) || 244 | c === 0x05c4 || 245 | (c >= 0x064b && c <= 0x0652) || 246 | c === 0x0670 || 247 | (c >= 0x06d6 && c <= 0x06dc) || 248 | (c >= 0x06dd && c <= 0x06df) || 249 | (c >= 0x06e0 && c <= 0x06e4) || 250 | (c >= 0x06e7 && c <= 0x06e8) || 251 | (c >= 0x06ea && c <= 0x06ed) || 252 | (c >= 0x0901 && c <= 0x0903) || 253 | c === 0x093c || 254 | (c >= 0x093e && c <= 0x094c) || 255 | c === 0x094d || 256 | (c >= 0x0951 && c <= 0x0954) || 257 | (c >= 0x0962 && c <= 0x0963) || 258 | (c >= 0x0981 && c <= 0x0983) || 259 | c === 0x09bc || 260 | c === 0x09be || 261 | c === 0x09bf || 262 | (c >= 0x09c0 && c <= 0x09c4) || 263 | (c >= 0x09c7 && c <= 0x09c8) || 264 | (c >= 0x09cb && c <= 0x09cd) || 265 | c === 0x09d7 || 266 | (c >= 0x09e2 && c <= 0x09e3) || 267 | c === 0x0a02 || 268 | c === 0x0a3c || 269 | c === 0x0a3e || 270 | c === 0x0a3f || 271 | (c >= 0x0a40 && c <= 0x0a42) || 272 | (c >= 0x0a47 && c <= 0x0a48) || 273 | (c >= 0x0a4b && c <= 0x0a4d) || 274 | (c >= 0x0a70 && c <= 0x0a71) || 275 | (c >= 0x0a81 && c <= 0x0a83) || 276 | c === 0x0abc || 277 | (c >= 0x0abe && c <= 0x0ac5) || 278 | (c >= 0x0ac7 && c <= 0x0ac9) || 279 | (c >= 0x0acb && c <= 0x0acd) || 280 | (c >= 0x0b01 && c <= 0x0b03) || 281 | c === 0x0b3c || 282 | (c >= 0x0b3e && c <= 0x0b43) || 283 | (c >= 0x0b47 && c <= 0x0b48) || 284 | (c >= 0x0b4b && c <= 0x0b4d) || 285 | (c >= 0x0b56 && c <= 0x0b57) || 286 | (c >= 0x0b82 && c <= 0x0b83) || 287 | (c >= 0x0bbe && c <= 0x0bc2) || 288 | (c >= 0x0bc6 && c <= 0x0bc8) || 289 | (c >= 0x0bca && c <= 0x0bcd) || 290 | c === 0x0bd7 || 291 | (c >= 0x0c01 && c <= 0x0c03) || 292 | (c >= 0x0c3e && c <= 0x0c44) || 293 | (c >= 0x0c46 && c <= 0x0c48) || 294 | (c >= 0x0c4a && c <= 0x0c4d) || 295 | (c >= 0x0c55 && c <= 0x0c56) || 296 | (c >= 0x0c82 && c <= 0x0c83) || 297 | (c >= 0x0cbe && c <= 0x0cc4) || 298 | (c >= 0x0cc6 && c <= 0x0cc8) || 299 | (c >= 0x0cca && c <= 0x0ccd) || 300 | (c >= 0x0cd5 && c <= 0x0cd6) || 301 | (c >= 0x0d02 && c <= 0x0d03) || 302 | (c >= 0x0d3e && c <= 0x0d43) || 303 | (c >= 0x0d46 && c <= 0x0d48) || 304 | (c >= 0x0d4a && c <= 0x0d4d) || 305 | c === 0x0d57 || 306 | c === 0x0e31 || 307 | (c >= 0x0e34 && c <= 0x0e3a) || 308 | (c >= 0x0e47 && c <= 0x0e4e) || 309 | c === 0x0eb1 || 310 | (c >= 0x0eb4 && c <= 0x0eb9) || 311 | (c >= 0x0ebb && c <= 0x0ebc) || 312 | (c >= 0x0ec8 && c <= 0x0ecd) || 313 | (c >= 0x0f18 && c <= 0x0f19) || 314 | c === 0x0f35 || 315 | c === 0x0f37 || 316 | c === 0x0f39 || 317 | c === 0x0f3e || 318 | c === 0x0f3f || 319 | (c >= 0x0f71 && c <= 0x0f84) || 320 | (c >= 0x0f86 && c <= 0x0f8b) || 321 | (c >= 0x0f90 && c <= 0x0f95) || 322 | c === 0x0f97 || 323 | (c >= 0x0f99 && c <= 0x0fad) || 324 | (c >= 0x0fb1 && c <= 0x0fb7) || 325 | c === 0x0fb9 || 326 | (c >= 0x20d0 && c <= 0x20dc) || 327 | c === 0x20e1 || 328 | (c >= 0x302a && c <= 0x302f) || 329 | c === 0x3099 || 330 | c === 0x309a || 331 | c === 0x00b7 || 332 | c === 0x02d0 || 333 | c === 0x02d1 || 334 | c === 0x0387 || 335 | c === 0x0640 || 336 | c === 0x0e46 || 337 | c === 0x0ec6 || 338 | c === 0x3005 || 339 | (c >= 0x3031 && c <= 0x3035) || 340 | (c >= 0x309d && c <= 0x309e) || 341 | (c >= 0x30fc && c <= 0x30fe) 342 | ); 343 | } 344 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export function isElement(e: Node | undefined | null): e is Element { 2 | return e != null && e.nodeType === 1; // Node.ELEMENT_NODE; 3 | } 4 | 5 | export function isAttribute(e: Node | undefined | null): e is Attr { 6 | return e != null && (e.nodeType === 2 || (e as Attr).specified); // Node.ATTRIBUTE_NODE; // DOM4 support 7 | } 8 | 9 | export function isText(e: Node | undefined | null): e is Text { 10 | return e != null && e.nodeType === 3; // Node.TEXT_NODE; 11 | } 12 | 13 | export function isCData(e: Node | undefined | null): e is CDATASection { 14 | return e != null && e.nodeType === 4; // Node.CDATA_SECTION_NODE; 15 | } 16 | 17 | export function isDocument(e: Node | undefined | null): e is Document { 18 | return e != null && e.nodeType === 9; // Node.DOCUMENT_NODE; 19 | } 20 | 21 | export function isFragment(e: Node | undefined | null): e is DocumentFragment { 22 | return e != null && e.nodeType === 11; // Node.DOCUMENT_FRAGMENT; 23 | } 24 | 25 | export function isProcessingInstruction(e: Node | undefined | null): e is ProcessingInstruction { 26 | return e != null && e.nodeType === 7; // Node.PROCESSING_INSTRUCTION_NODE; 27 | } 28 | 29 | export function isNamespaceNode(e: Node | undefined | null): e is Attr { 30 | if (isAttribute(e)) { 31 | return e.localName === 'xmlns' || e.prefix === 'xmlns'; 32 | } else { 33 | return false; 34 | } 35 | } 36 | 37 | export function isNSResolver(r: any): r is XPathNSResolver { 38 | return r != null && r.lookupNamespaceURI !== undefined; 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/xml.ts: -------------------------------------------------------------------------------- 1 | import { NamespaceResolver } from '../xpath-types'; 2 | import { isElement } from './types'; 3 | 4 | export function splitQName(qn: string): [string | null, string] { 5 | const i = qn.indexOf(':'); 6 | if (i === -1) { 7 | return [null, qn]; 8 | } 9 | return [qn.substring(0, i), qn.substring(i + 1)]; 10 | } 11 | 12 | export function resolveQName(qn: string, nr: NamespaceResolver, n: Node, useDefault: boolean) { 13 | const parts = splitQName(qn); 14 | if (parts[0] != null) { 15 | parts[0] = nr.getNamespace(parts[0], n); 16 | } else { 17 | if (useDefault) { 18 | parts[0] = nr.getNamespace('', n); 19 | if (parts[0] == null) { 20 | parts[0] = ''; 21 | } 22 | } else { 23 | parts[0] = ''; 24 | } 25 | } 26 | return parts; 27 | } 28 | 29 | export function getElementById(n: Node, id: string): Element | null { 30 | // Note that this does not check the DTD to check for actual 31 | // attributes of type ID, so this may be a bit wrong. 32 | if (isElement(n)) { 33 | if (n.getAttribute('id') === id || n.getAttributeNS(null, 'id') === id) { 34 | return n; 35 | } 36 | } 37 | for (let m: Node | null = n.firstChild; m != null; m = m.nextSibling) { 38 | const res = getElementById(m, id); 39 | if (res != null) { 40 | return res; 41 | } 42 | } 43 | return null; 44 | } 45 | -------------------------------------------------------------------------------- /src/variable-reference.ts: -------------------------------------------------------------------------------- 1 | import { resolveQName } from './utils/xml'; 2 | import { XPathException } from './xpath-exception'; 3 | import { Expression, XPathContext } from './xpath-types'; 4 | 5 | export class VariableReference extends Expression { 6 | variable: string; 7 | 8 | constructor(v: string) { 9 | super(); 10 | 11 | this.variable = v; 12 | } 13 | 14 | toString() { 15 | return '$' + this.variable; 16 | } 17 | 18 | evaluate(c: XPathContext) { 19 | const parts = resolveQName(this.variable, c.namespaceResolver, c.contextNode, false); 20 | 21 | if (parts[0] == null) { 22 | throw new Error('Cannot resolve QName ' + this.variable); 23 | } 24 | const result = c.variableResolver.getVariable(parts[1], parts[0]); 25 | if (!result) { 26 | throw XPathException.fromMessage('Undeclared variable: ' + this.toString()); 27 | } 28 | return result; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/variable-resolver.ts: -------------------------------------------------------------------------------- 1 | import { Expression, VariableResolver } from './xpath-types'; 2 | 3 | export class VariableResolverImpl implements VariableResolver { 4 | getVariable(_ln: string, _ns: string): Expression | null { 5 | return null; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/xpath-evaluator.ts: -------------------------------------------------------------------------------- 1 | import { NodeXPathNSResolver } from './node-x-path-ns-resolver'; 2 | import { isNSResolver } from './utils/types'; 3 | import { XPathException } from './xpath-exception'; 4 | import { XPathExpressionImpl } from './xpath-expression-impl'; 5 | import { XPathNSResolverWrapper } from './xpath-ns-resolver-wrapper'; 6 | import { XPathParser } from './xpath-parser'; 7 | import { FunctionResolver, NamespaceResolver, VariableResolver } from './xpath-types'; 8 | 9 | export class XPathEvaluatorImpl implements XPathEvaluator { 10 | functionResolver?: FunctionResolver; 11 | variableResolver?: VariableResolver; 12 | namespaceResolver?: NamespaceResolver; 13 | parser?: XPathParser; 14 | 15 | constructor({ fr, vr, p }: { fr?: FunctionResolver; vr?: VariableResolver; p?: XPathParser }) { 16 | this.functionResolver = fr; 17 | this.variableResolver = vr; 18 | this.parser = p; 19 | } 20 | 21 | createExpression(e: string, r: XPathNSResolver | null) { 22 | try { 23 | return new XPathExpressionImpl(e, { 24 | fr: this.functionResolver, 25 | nr: r == null ? this.namespaceResolver : new XPathNSResolverWrapper(r), 26 | vr: this.variableResolver, 27 | p: this.parser 28 | }); 29 | } catch (e) { 30 | throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, e); 31 | } 32 | } 33 | createNSResolver(n?: Node) { 34 | return new NodeXPathNSResolver(n); 35 | } 36 | evaluate( 37 | expression: string, 38 | contextNode: Node, 39 | resolver: XPathNSResolver | null, 40 | type: number, 41 | result: XPathResult | null 42 | ) { 43 | if (type < 0 || type > 9) { 44 | throw { 45 | code: 0, 46 | toString() { 47 | return 'Request type not supported'; 48 | } 49 | }; 50 | } 51 | 52 | if (resolver != null) { 53 | resolver = convertNSResolver(resolver); 54 | } 55 | 56 | const ex = this.createExpression(expression, resolver); 57 | return ex.evaluate(contextNode, type, result); 58 | } 59 | } 60 | 61 | function convertNSResolver(resolver: XPathNSResolver | null | undefined) { 62 | if (resolver == null) { 63 | return { 64 | lookupNamespaceURI(_prefix: string): null { 65 | return null; 66 | } 67 | }; 68 | } else if (!isNSResolver(resolver)) { 69 | const pr: ((prefix: string | null) => string | null) = resolver; 70 | return { 71 | lookupNamespaceURI(prefix: string): string | null { 72 | return pr(prefix); 73 | } 74 | }; 75 | } else { 76 | return resolver; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/xpath-exception.ts: -------------------------------------------------------------------------------- 1 | export class XPathException extends Error { 2 | static INVALID_EXPRESSION_ERR = 51; 3 | static TYPE_ERR = 52; 4 | 5 | static fromMessage(message: string, error?: Error) { 6 | return new XPathException(undefined, error, message); 7 | } 8 | 9 | code?: number; 10 | exception?: Error; 11 | 12 | constructor(code?: number, error?: Error, message?: string) { 13 | super(getMessage(code, error) || message); 14 | 15 | this.code = code; 16 | this.exception = error; 17 | } 18 | 19 | toString() { 20 | return this.message; 21 | } 22 | } 23 | 24 | function getMessage(code?: number, exception?: Error) { 25 | const msg = exception ? ': ' + exception.toString() : ''; 26 | switch (code) { 27 | case XPathException.INVALID_EXPRESSION_ERR: 28 | return 'Invalid expression' + msg; 29 | case XPathException.TYPE_ERR: 30 | return 'Type error' + msg; 31 | } 32 | return undefined; 33 | } 34 | -------------------------------------------------------------------------------- /src/xpath-expression-impl.ts: -------------------------------------------------------------------------------- 1 | import { FunctionResolverImpl } from './function-resolver'; 2 | import { NamespaceResolverImpl } from './namespace-resolver'; 3 | import { isDocument } from './utils/types'; 4 | import { VariableResolverImpl } from './variable-resolver'; 5 | import { XPath } from './xpath'; 6 | import { XPathParser } from './xpath-parser'; 7 | import { XPathResultImpl } from './xpath-result-impl'; 8 | import { FunctionResolver, NamespaceResolver, VariableResolver, XPathContext } from './xpath-types'; 9 | 10 | export class XPathExpressionImpl implements XPathExpression { 11 | static getOwnerDocument(n: Node) { 12 | return isDocument(n) ? n : n.ownerDocument; 13 | } 14 | 15 | static detectHtmlDom(n?: Node) { 16 | if (!n) { 17 | return false; 18 | } 19 | 20 | const doc = XPathExpressionImpl.getOwnerDocument(n); 21 | 22 | try { 23 | if (doc != null) { 24 | return doc.implementation.hasFeature('HTML', '2.0'); 25 | } else { 26 | return true; 27 | } 28 | } catch (e) { 29 | return true; 30 | } 31 | } 32 | 33 | xpath: XPath; 34 | context: XPathContext; 35 | 36 | constructor( 37 | e: string, 38 | { 39 | vr = new VariableResolverImpl(), 40 | nr = new NamespaceResolverImpl(), 41 | fr = new FunctionResolverImpl(), 42 | p = new XPathParser() 43 | }: { 44 | vr?: VariableResolver; 45 | nr?: NamespaceResolver; 46 | fr?: FunctionResolver; 47 | p?: XPathParser; 48 | } 49 | ) { 50 | this.xpath = p.parse(e); 51 | this.context = new XPathContext(vr, nr, fr); 52 | } 53 | 54 | evaluate(n: Node | undefined, t: number, _res: XPathResult | null) { 55 | // Intentionaly make the node as defined. 56 | // If no node is provided then the library will fail in case of context aware expression. 57 | n = n!; 58 | 59 | this.context.expressionContextNode = n; 60 | // backward compatibility - no reliable way to detect whether the DOM is HTML, but 61 | // this library has been using this method up until now, so we will continue to use it 62 | // ONLY when using an XPathExpression 63 | this.context.caseInsensitive = XPathExpressionImpl.detectHtmlDom(n); 64 | 65 | const result = this.xpath.evaluate(this.context); 66 | return new XPathResultImpl(result, t); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/xpath-namespace.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:member-ordering 2 | 3 | export class XPathNamespace implements Node { 4 | static XPATH_NAMESPACE_NODE = 13; 5 | 6 | isXPathNamespace = true; 7 | ownerDocument: Document | null; 8 | nodeName = '#namespace'; 9 | prefix: string; 10 | localName: string; 11 | namespaceURI: string; 12 | nodeValue: string; 13 | ownerElement: Element; 14 | nodeType: number; 15 | 16 | constructor(pre: string, ns: string, p: Element) { 17 | this.isXPathNamespace = true; 18 | this.ownerDocument = p.ownerDocument; 19 | this.nodeName = '#namespace'; 20 | this.prefix = pre; 21 | this.localName = pre; 22 | this.namespaceURI = ns; 23 | this.nodeValue = ns; 24 | this.ownerElement = p; 25 | this.nodeType = XPathNamespace.XPATH_NAMESPACE_NODE; 26 | } 27 | 28 | toString() { 29 | return '{ "' + this.prefix + '", "' + this.namespaceURI + '" }'; 30 | } 31 | 32 | /** 33 | * Unused and unsupported properties 34 | */ 35 | readonly baseURI: string; 36 | readonly childNodes: NodeListOf; 37 | readonly firstChild: ChildNode | null; 38 | readonly isConnected: boolean; 39 | readonly lastChild: ChildNode | null; 40 | readonly nextSibling: ChildNode | null; 41 | readonly parentElement: HTMLElement | null; 42 | readonly parentNode: Node & ParentNode | null; 43 | readonly previousSibling: ChildNode | null; 44 | textContent: string | null; 45 | appendChild = unsupported; 46 | cloneNode = unsupported; 47 | compareDocumentPosition = unsupported; 48 | contains = unsupported; 49 | getRootNode = unsupported; 50 | hasChildNodes = unsupported; 51 | insertBefore = unsupported; 52 | isDefaultNamespace = unsupported; 53 | isEqualNode = unsupported; 54 | isSameNode = unsupported; 55 | lookupNamespaceURI = unsupported; 56 | lookupPrefix = unsupported; 57 | normalize = unsupported; 58 | removeChild = unsupported; 59 | replaceChild = unsupported; 60 | readonly ATTRIBUTE_NODE: number; 61 | readonly CDATA_SECTION_NODE: number; 62 | readonly COMMENT_NODE: number; 63 | readonly DOCUMENT_FRAGMENT_NODE: number; 64 | readonly DOCUMENT_NODE: number; 65 | readonly DOCUMENT_POSITION_CONTAINED_BY: number; 66 | readonly DOCUMENT_POSITION_CONTAINS: number; 67 | readonly DOCUMENT_POSITION_DISCONNECTED: number; 68 | readonly DOCUMENT_POSITION_FOLLOWING: number; 69 | readonly DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: number; 70 | readonly DOCUMENT_POSITION_PRECEDING: number; 71 | readonly DOCUMENT_TYPE_NODE: number; 72 | readonly ELEMENT_NODE: number; 73 | readonly ENTITY_NODE: number; 74 | readonly ENTITY_REFERENCE_NODE: number; 75 | readonly NOTATION_NODE: number; 76 | readonly PROCESSING_INSTRUCTION_NODE: number; 77 | readonly TEXT_NODE: number; 78 | addEventListener = unsupported; 79 | dispatchEvent = unsupported; 80 | removeEventListener = unsupported; 81 | } 82 | 83 | function unsupported(): any { 84 | throw new Error('Unsupported'); 85 | } 86 | -------------------------------------------------------------------------------- /src/xpath-ns-resolver-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { isNSResolver } from './utils/types'; 2 | 3 | export class XPathNSResolverWrapper { 4 | xpathNSResolver: { lookupNamespaceURI(prefix: string | null): string | null; } | null; 5 | 6 | constructor(r: XPathNSResolver | null) { 7 | if (!isNSResolver(r)) { 8 | this.xpathNSResolver = null; 9 | } else { 10 | this.xpathNSResolver = r as { lookupNamespaceURI(prefix: string | null): string | null; }; 11 | } 12 | } 13 | 14 | getNamespace(prefix: string, _n: Node) { 15 | if (this.xpathNSResolver == null) { 16 | return null; 17 | } 18 | return this.xpathNSResolver.lookupNamespaceURI(prefix); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/xpath-parser.ts: -------------------------------------------------------------------------------- 1 | import { FunctionCall } from './function-call'; 2 | import { LocationPath } from './location-path'; 3 | import { NodeTest } from './node-test'; 4 | import { AndOperation } from './operations/and-operation'; 5 | import { BarOperation } from './operations/bar-operation'; 6 | import { DivOperation } from './operations/div-operation'; 7 | import { EqualsOperation } from './operations/equals-operation'; 8 | import { GreaterThanOperation } from './operations/greater-than-operation'; 9 | import { GreaterThanOrEqualOperation } from './operations/greater-than-or-equal-operation'; 10 | import { LessThanOperation } from './operations/less-than-operation'; 11 | import { LessThanOrEqualOperation } from './operations/less-than-or-equal-operation'; 12 | import { MinusOperation } from './operations/minus-operation'; 13 | import { ModOperation } from './operations/mod-operation'; 14 | import { MultiplyOperation } from './operations/multiply-operation'; 15 | import { NotEqualOperation } from './operations/not-equals-operation'; 16 | import { OrOperation } from './operations/or-operation'; 17 | import { PlusOperation } from './operations/plus-operation'; 18 | import { UnaryMinusOperation } from './operations/unary-minus-operation'; 19 | import { PathExpr } from './path-expr'; 20 | import { Step } from './step'; 21 | import { isLetter, isNCNameChar } from './utils/character'; 22 | import { VariableReference } from './variable-reference'; 23 | import { XPath } from './xpath'; 24 | import { XPathException } from './xpath-exception'; 25 | import { Expression, XNumber, XString } from './xpath-types'; 26 | 27 | // tslint:disable:quotemark 28 | 29 | export class XPathParser { 30 | static actionTable = [ 31 | ' s s sssssssss s ss s ss', 32 | ' s ', 33 | 'r rrrrrrrrr rrrrrrr rr r ', 34 | ' rrrrr ', 35 | ' s s sssssssss s ss s ss', 36 | 'rs rrrrrrrr s sssssrrrrrr rrs rs ', 37 | ' s s sssssssss s ss s ss', 38 | ' s ', 39 | ' s ', 40 | 'r rrrrrrrrr rrrrrrr rr rr ', 41 | 'r rrrrrrrrr rrrrrrr rr rr ', 42 | 'r rrrrrrrrr rrrrrrr rr rr ', 43 | 'r rrrrrrrrr rrrrrrr rr rr ', 44 | 'r rrrrrrrrr rrrrrrr rr rr ', 45 | ' s ', 46 | ' s ', 47 | ' s s sssss s s ', 48 | 'r rrrrrrrrr rrrrrrr rr r ', 49 | 'a ', 50 | 'r s rr r ', 51 | 'r sr rr r ', 52 | 'r s rr s rr r ', 53 | 'r rssrr rss rr r ', 54 | 'r rrrrr rrrss rr r ', 55 | 'r rrrrrsss rrrrr rr r ', 56 | 'r rrrrrrrr rrrrr rr r ', 57 | 'r rrrrrrrr rrrrrs rr r ', 58 | 'r rrrrrrrr rrrrrr rr r ', 59 | 'r rrrrrrrr rrrrrr rr r ', 60 | 'r srrrrrrrr rrrrrrs rr sr ', 61 | 'r srrrrrrrr rrrrrrs rr r ', 62 | 'r rrrrrrrrr rrrrrrr rr rr ', 63 | 'r rrrrrrrrr rrrrrrr rr rr ', 64 | 'r rrrrrrrrr rrrrrrr rr rr ', 65 | 'r rrrrrrrr rrrrrr rr r ', 66 | 'r rrrrrrrr rrrrrr rr r ', 67 | 'r rrrrrrrrr rrrrrrr rr r ', 68 | 'r rrrrrrrrr rrrrrrr rr r ', 69 | ' sssss ', 70 | 'r rrrrrrrrr rrrrrrr rr sr ', 71 | 'r rrrrrrrrr rrrrrrr rr r ', 72 | 'r rrrrrrrrr rrrrrrr rr rr ', 73 | 'r rrrrrrrrr rrrrrrr rr rr ', 74 | ' s ', 75 | 'r srrrrrrrr rrrrrrs rr r ', 76 | 'r rrrrrrrr rrrrr rr r ', 77 | ' s ', 78 | ' s ', 79 | ' rrrrr ', 80 | ' s s sssssssss s sss s ss', 81 | 'r srrrrrrrr rrrrrrs rr r ', 82 | ' s s sssssssss s ss s ss', 83 | ' s s sssssssss s ss s ss', 84 | ' s s sssssssss s ss s ss', 85 | ' s s sssssssss s ss s ss', 86 | ' s s sssssssss s ss s ss', 87 | ' s s sssssssss s ss s ss', 88 | ' s s sssssssss s ss s ss', 89 | ' s s sssssssss s ss s ss', 90 | ' s s sssssssss s ss s ss', 91 | ' s s sssssssss s ss s ss', 92 | ' s s sssssssss s ss s ss', 93 | ' s s sssssssss s ss s ss', 94 | ' s s sssssssss s ss s ss', 95 | ' s s sssssssss ss s ss', 96 | ' s s sssssssss s ss s ss', 97 | ' s s sssss s s ', 98 | ' s s sssss s s ', 99 | 'r rrrrrrrrr rrrrrrr rr rr ', 100 | ' s s sssss s s ', 101 | ' s s sssss s s ', 102 | 'r rrrrrrrrr rrrrrrr rr sr ', 103 | 'r rrrrrrrrr rrrrrrr rr sr ', 104 | 'r rrrrrrrrr rrrrrrr rr r ', 105 | 'r rrrrrrrrr rrrrrrr rr rr ', 106 | ' s ', 107 | 'r rrrrrrrrr rrrrrrr rr rr ', 108 | 'r rrrrrrrrr rrrrrrr rr rr ', 109 | ' rr ', 110 | ' s ', 111 | ' rs ', 112 | 'r sr rr r ', 113 | 'r s rr s rr r ', 114 | 'r rssrr rss rr r ', 115 | 'r rssrr rss rr r ', 116 | 'r rrrrr rrrss rr r ', 117 | 'r rrrrr rrrss rr r ', 118 | 'r rrrrr rrrss rr r ', 119 | 'r rrrrr rrrss rr r ', 120 | 'r rrrrrsss rrrrr rr r ', 121 | 'r rrrrrsss rrrrr rr r ', 122 | 'r rrrrrrrr rrrrr rr r ', 123 | 'r rrrrrrrr rrrrr rr r ', 124 | 'r rrrrrrrr rrrrr rr r ', 125 | 'r rrrrrrrr rrrrrr rr r ', 126 | ' r ', 127 | ' s ', 128 | 'r srrrrrrrr rrrrrrs rr r ', 129 | 'r srrrrrrrr rrrrrrs rr r ', 130 | 'r rrrrrrrrr rrrrrrr rr r ', 131 | 'r rrrrrrrrr rrrrrrr rr r ', 132 | 'r rrrrrrrrr rrrrrrr rr r ', 133 | 'r rrrrrrrrr rrrrrrr rr r ', 134 | 'r rrrrrrrrr rrrrrrr rr rr ', 135 | 'r rrrrrrrrr rrrrrrr rr rr ', 136 | ' s s sssssssss s ss s ss', 137 | 'r rrrrrrrrr rrrrrrr rr rr ', 138 | ' r ' 139 | ]; 140 | 141 | static actionTableNumber = [ 142 | ' 1 0 /.-,+*)(\' & %$ # "!', 143 | ' J ', 144 | 'a aaaaaaaaa aaaaaaa aa a ', 145 | ' YYYYY ', 146 | ' 1 0 /.-,+*)(\' & %$ # "!', 147 | 'K1 KKKKKKKK . +*)(\'KKKKKK KK# K" ', 148 | ' 1 0 /.-,+*)(\' & %$ # "!', 149 | ' N ', 150 | ' O ', 151 | 'e eeeeeeeee eeeeeee ee ee ', 152 | 'f fffffffff fffffff ff ff ', 153 | 'd ddddddddd ddddddd dd dd ', 154 | 'B BBBBBBBBB BBBBBBB BB BB ', 155 | 'A AAAAAAAAA AAAAAAA AA AA ', 156 | ' P ', 157 | ' Q ', 158 | ' 1 . +*)(\' # " ', 159 | 'b bbbbbbbbb bbbbbbb bb b ', 160 | ' ', 161 | '! S !! ! ', 162 | '" T" "" " ', 163 | '$ V $$ U $$ $ ', 164 | '& &ZY&& &XW && & ', 165 | ') ))))) )))\\[ )) ) ', 166 | '. ....._^] ..... .. . ', 167 | '1 11111111 11111 11 1 ', 168 | '5 55555555 55555` 55 5 ', 169 | '7 77777777 777777 77 7 ', 170 | '9 99999999 999999 99 9 ', 171 | ': c:::::::: ::::::b :: a: ', 172 | 'I fIIIIIIII IIIIIIe II I ', 173 | '= ========= ======= == == ', 174 | '? ????????? ??????? ?? ?? ', 175 | 'C CCCCCCCCC CCCCCCC CC CC ', 176 | 'J JJJJJJJJ JJJJJJ JJ J ', 177 | 'M MMMMMMMM MMMMMM MM M ', 178 | 'N NNNNNNNNN NNNNNNN NN N ', 179 | 'P PPPPPPPPP PPPPPPP PP P ', 180 | " +*)(' ", 181 | 'R RRRRRRRRR RRRRRRR RR aR ', 182 | 'U UUUUUUUUU UUUUUUU UU U ', 183 | 'Z ZZZZZZZZZ ZZZZZZZ ZZ ZZ ', 184 | 'c ccccccccc ccccccc cc cc ', 185 | ' j ', 186 | 'L fLLLLLLLL LLLLLLe LL L ', 187 | '6 66666666 66666 66 6 ', 188 | ' k ', 189 | ' l ', 190 | ' XXXXX ', 191 | ' 1 0 /.-,+*)(\' & %$m # "!', 192 | '_ f________ ______e __ _ ', 193 | ' 1 0 /.-,+*)(\' & %$ # "!', 194 | ' 1 0 /.-,+*)(\' & %$ # "!', 195 | ' 1 0 /.-,+*)(\' & %$ # "!', 196 | ' 1 0 /.-,+*)(\' & %$ # "!', 197 | ' 1 0 /.-,+*)(\' & %$ # "!', 198 | ' 1 0 /.-,+*)(\' & %$ # "!', 199 | ' 1 0 /.-,+*)(\' & %$ # "!', 200 | ' 1 0 /.-,+*)(\' & %$ # "!', 201 | ' 1 0 /.-,+*)(\' & %$ # "!', 202 | ' 1 0 /.-,+*)(\' & %$ # "!', 203 | ' 1 0 /.-,+*)(\' & %$ # "!', 204 | ' 1 0 /.-,+*)(\' & %$ # "!', 205 | ' 1 0 /.-,+*)(\' & %$ # "!', 206 | ' 1 0 /.-,+*)(\' %$ # "!', 207 | ' 1 0 /.-,+*)(\' & %$ # "!', 208 | ' 1 . +*)(\' # " ', 209 | ' 1 . +*)(\' # " ', 210 | '> >>>>>>>>> >>>>>>> >> >> ', 211 | ' 1 . +*)(\' # " ', 212 | ' 1 . +*)(\' # " ', 213 | 'Q QQQQQQQQQ QQQQQQQ QQ aQ ', 214 | 'V VVVVVVVVV VVVVVVV VV aV ', 215 | 'T TTTTTTTTT TTTTTTT TT T ', 216 | '@ @@@@@@@@@ @@@@@@@ @@ @@ ', 217 | ' \x87 ', 218 | '[ [[[[[[[[[ [[[[[[[ [[ [[ ', 219 | 'D DDDDDDDDD DDDDDDD DD DD ', 220 | ' HH ', 221 | ' \x88 ', 222 | ' F\x89 ', 223 | '# T# ## # ', 224 | '% V %% U %% % ', 225 | "' 'ZY'' 'XW '' ' ", 226 | '( (ZY(( (XW (( ( ', 227 | '+ +++++ +++\\[ ++ + ', 228 | '* ***** ***\\[ ** * ', 229 | '- ----- ---\\[ -- - ', 230 | ', ,,,,, ,,,\\[ ,, , ', 231 | '0 00000_^] 00000 00 0 ', 232 | '/ /////_^] ///// // / ', 233 | '2 22222222 22222 22 2 ', 234 | '3 33333333 33333 33 3 ', 235 | '4 44444444 44444 44 4 ', 236 | '8 88888888 888888 88 8 ', 237 | ' ^ ', 238 | ' \x8a ', 239 | '; f;;;;;;;; ;;;;;;e ;; ; ', 240 | '< f<<<<<<<< <<<<<?@ AB CDEFGH IJ ', 254 | ' ', 255 | ' ', 256 | ' ', 257 | 'L456789:;<=>?@ AB CDEFGH IJ ', 258 | ' M EFGH IJ ', 259 | ' N;<=>?@ AB CDEFGH IJ ', 260 | ' ', 261 | ' ', 262 | ' ', 263 | ' ', 264 | ' ', 265 | ' ', 266 | ' ', 267 | ' ', 268 | ' ', 269 | ' S EFGH IJ ', 270 | ' ', 271 | ' ', 272 | ' ', 273 | ' ', 274 | ' ', 275 | ' ', 276 | ' ', 277 | ' ', 278 | ' ', 279 | ' ', 280 | ' ', 281 | ' ', 282 | ' e ', 283 | ' ', 284 | ' ', 285 | ' ', 286 | ' ', 287 | ' ', 288 | ' ', 289 | ' ', 290 | ' ', 291 | ' h J ', 292 | ' i j ', 293 | ' ', 294 | ' ', 295 | ' ', 296 | ' ', 297 | ' ', 298 | ' ', 299 | ' ', 300 | ' ', 301 | ' ', 302 | 'o456789:;<=>?@ ABpqCDEFGH IJ ', 303 | ' ', 304 | ' r6789:;<=>?@ AB CDEFGH IJ ', 305 | ' s789:;<=>?@ AB CDEFGH IJ ', 306 | ' t89:;<=>?@ AB CDEFGH IJ ', 307 | ' u89:;<=>?@ AB CDEFGH IJ ', 308 | ' v9:;<=>?@ AB CDEFGH IJ ', 309 | ' w9:;<=>?@ AB CDEFGH IJ ', 310 | ' x9:;<=>?@ AB CDEFGH IJ ', 311 | ' y9:;<=>?@ AB CDEFGH IJ ', 312 | ' z:;<=>?@ AB CDEFGH IJ ', 313 | ' {:;<=>?@ AB CDEFGH IJ ', 314 | ' |;<=>?@ AB CDEFGH IJ ', 315 | ' };<=>?@ AB CDEFGH IJ ', 316 | ' ~;<=>?@ AB CDEFGH IJ ', 317 | ' \x7f=>?@ AB CDEFGH IJ ', 318 | '\x80456789:;<=>?@ AB CDEFGH IJ\x81', 319 | ' \x82 EFGH IJ ', 320 | ' \x83 EFGH IJ ', 321 | ' ', 322 | ' \x84 GH IJ ', 323 | ' \x85 GH IJ ', 324 | ' i \x86 ', 325 | ' i \x87 ', 326 | ' ', 327 | ' ', 328 | ' ', 329 | ' ', 330 | ' ', 331 | ' ', 332 | ' ', 333 | ' ', 334 | ' ', 335 | ' ', 336 | ' ', 337 | ' ', 338 | ' ', 339 | ' ', 340 | ' ', 341 | ' ', 342 | ' ', 343 | ' ', 344 | ' ', 345 | ' ', 346 | ' ', 347 | ' ', 348 | ' ', 349 | ' ', 350 | ' ', 351 | ' ', 352 | ' ', 353 | ' ', 354 | ' ', 355 | ' ', 356 | ' ', 357 | ' ', 358 | 'o456789:;<=>?@ AB\x8cqCDEFGH IJ ', 359 | ' ', 360 | ' ' 361 | ]; 362 | 363 | static productions = [ 364 | [1, 1, 2], 365 | [2, 1, 3], 366 | [3, 1, 4], 367 | [3, 3, 3, -9, 4], 368 | [4, 1, 5], 369 | [4, 3, 4, -8, 5], 370 | [5, 1, 6], 371 | [5, 3, 5, -22, 6], 372 | [5, 3, 5, -5, 6], 373 | [6, 1, 7], 374 | [6, 3, 6, -23, 7], 375 | [6, 3, 6, -24, 7], 376 | [6, 3, 6, -6, 7], 377 | [6, 3, 6, -7, 7], 378 | [7, 1, 8], 379 | [7, 3, 7, -25, 8], 380 | [7, 3, 7, -26, 8], 381 | [8, 1, 9], 382 | [8, 3, 8, -12, 9], 383 | [8, 3, 8, -11, 9], 384 | [8, 3, 8, -10, 9], 385 | [9, 1, 10], 386 | [9, 2, -26, 9], 387 | [10, 1, 11], 388 | [10, 3, 10, -27, 11], 389 | [11, 1, 12], 390 | [11, 1, 13], 391 | [11, 3, 13, -28, 14], 392 | [11, 3, 13, -4, 14], 393 | [13, 1, 15], 394 | [13, 2, 13, 16], 395 | [15, 1, 17], 396 | [15, 3, -29, 2, -30], 397 | [15, 1, -15], 398 | [15, 1, -16], 399 | [15, 1, 18], 400 | [18, 3, -13, -29, -30], 401 | [18, 4, -13, -29, 19, -30], 402 | [19, 1, 20], 403 | [19, 3, 20, -31, 19], 404 | [20, 1, 2], 405 | [12, 1, 14], 406 | [12, 1, 21], 407 | [21, 1, -28], 408 | [21, 2, -28, 14], 409 | [21, 1, 22], 410 | [14, 1, 23], 411 | [14, 3, 14, -28, 23], 412 | [14, 1, 24], 413 | [23, 2, 25, 26], 414 | [23, 1, 26], 415 | [23, 3, 25, 26, 27], 416 | [23, 2, 26, 27], 417 | [23, 1, 28], 418 | [27, 1, 16], 419 | [27, 2, 16, 27], 420 | [25, 2, -14, -3], 421 | [25, 1, -32], 422 | [26, 1, 29], 423 | [26, 3, -20, -29, -30], 424 | [26, 4, -21, -29, -15, -30], 425 | [16, 3, -33, 30, -34], 426 | [30, 1, 2], 427 | [22, 2, -4, 14], 428 | [24, 3, 14, -4, 23], 429 | [28, 1, -35], 430 | [28, 1, -2], 431 | [17, 2, -36, -18], 432 | [29, 1, -17], 433 | [29, 1, -19], 434 | [29, 1, -18] 435 | ]; 436 | 437 | static DOUBLEDOT = 2; 438 | static DOUBLECOLON = 3; 439 | static DOUBLESLASH = 4; 440 | static NOTEQUAL = 5; 441 | static LESSTHANOREQUAL = 6; 442 | static GREATERTHANOREQUAL = 7; 443 | static AND = 8; 444 | static OR = 9; 445 | static MOD = 10; 446 | static DIV = 11; 447 | static MULTIPLYOPERATOR = 12; 448 | static FUNCTIONNAME = 13; 449 | static AXISNAME = 14; 450 | static LITERAL = 15; 451 | static NUMBER = 16; 452 | static ASTERISKNAMETEST = 17; 453 | static QNAME = 18; 454 | static NCNAMECOLONASTERISK = 19; 455 | static NODETYPE = 20; 456 | static PROCESSINGINSTRUCTIONWITHLITERAL = 21; 457 | static EQUALS = 22; 458 | static LESSTHAN = 23; 459 | static GREATERTHAN = 24; 460 | static PLUS = 25; 461 | static MINUS = 26; 462 | static BAR = 27; 463 | static SLASH = 28; 464 | static LEFTPARENTHESIS = 29; 465 | static RIGHTPARENTHESIS = 30; 466 | static COMMA = 31; 467 | static AT = 32; 468 | static LEFTBRACKET = 33; 469 | static RIGHTBRACKET = 34; 470 | static DOT = 35; 471 | static DOLLAR = 36; 472 | 473 | static SHIFT = 's'; 474 | static REDUCE = 'r'; 475 | static ACCEPT = 'a'; 476 | 477 | reduceActions: Array<(rhs: any[]) => any>; 478 | 479 | constructor() { 480 | this.reduceActions = []; 481 | 482 | this.reduceActions[3] = (rhs) => { 483 | return new OrOperation(rhs[0], rhs[2]); 484 | }; 485 | this.reduceActions[5] = (rhs) => { 486 | return new AndOperation(rhs[0], rhs[2]); 487 | }; 488 | this.reduceActions[7] = (rhs) => { 489 | return new EqualsOperation(rhs[0], rhs[2]); 490 | }; 491 | this.reduceActions[8] = (rhs) => { 492 | return new NotEqualOperation(rhs[0], rhs[2]); 493 | }; 494 | this.reduceActions[10] = (rhs) => { 495 | return new LessThanOperation(rhs[0], rhs[2]); 496 | }; 497 | this.reduceActions[11] = (rhs) => { 498 | return new GreaterThanOperation(rhs[0], rhs[2]); 499 | }; 500 | this.reduceActions[12] = (rhs) => { 501 | return new LessThanOrEqualOperation(rhs[0], rhs[2]); 502 | }; 503 | this.reduceActions[13] = (rhs) => { 504 | return new GreaterThanOrEqualOperation(rhs[0], rhs[2]); 505 | }; 506 | this.reduceActions[15] = (rhs) => { 507 | return new PlusOperation(rhs[0], rhs[2]); 508 | }; 509 | this.reduceActions[16] = (rhs) => { 510 | return new MinusOperation(rhs[0], rhs[2]); 511 | }; 512 | this.reduceActions[18] = (rhs) => { 513 | return new MultiplyOperation(rhs[0], rhs[2]); 514 | }; 515 | this.reduceActions[19] = (rhs) => { 516 | return new DivOperation(rhs[0], rhs[2]); 517 | }; 518 | this.reduceActions[20] = (rhs) => { 519 | return new ModOperation(rhs[0], rhs[2]); 520 | }; 521 | this.reduceActions[22] = (rhs) => { 522 | return new UnaryMinusOperation(rhs[1]); 523 | }; 524 | this.reduceActions[24] = (rhs) => { 525 | return new BarOperation(rhs[0], rhs[2]); 526 | }; 527 | this.reduceActions[25] = (rhs) => { 528 | return new PathExpr(undefined, undefined, rhs[0]); 529 | }; 530 | this.reduceActions[27] = (rhs) => { 531 | rhs[0].locationPath = rhs[2]; 532 | return rhs[0]; 533 | }; 534 | this.reduceActions[28] = (rhs) => { 535 | rhs[0].locationPath = rhs[2]; 536 | rhs[0].locationPath.steps.unshift(new Step(Step.DESCENDANTORSELF, NodeTest.nodeTest, [])); 537 | return rhs[0]; 538 | }; 539 | this.reduceActions[29] = (rhs) => { 540 | return new PathExpr(rhs[0], [], undefined); 541 | }; 542 | this.reduceActions[30] = (rhs) => { 543 | if (rhs[0] instanceof PathExpr) { 544 | if (rhs[0].filterPredicates === undefined) { 545 | rhs[0].filterPredicates = []; 546 | } 547 | rhs[0].filterPredicates.push(rhs[1]); 548 | return rhs[0]; 549 | } else { 550 | return new PathExpr(rhs[0], [rhs[1]], undefined); 551 | } 552 | }; 553 | this.reduceActions[32] = (rhs) => { 554 | return rhs[1]; 555 | }; 556 | this.reduceActions[33] = (rhs) => { 557 | return new XString(rhs[0]); 558 | }; 559 | this.reduceActions[34] = (rhs) => { 560 | return new XNumber(rhs[0]); 561 | }; 562 | this.reduceActions[36] = (rhs) => { 563 | return new FunctionCall(rhs[0], []); 564 | }; 565 | this.reduceActions[37] = (rhs) => { 566 | return new FunctionCall(rhs[0], rhs[2]); 567 | }; 568 | this.reduceActions[38] = (rhs) => { 569 | return [rhs[0]]; 570 | }; 571 | this.reduceActions[39] = (rhs) => { 572 | rhs[2].unshift(rhs[0]); 573 | return rhs[2]; 574 | }; 575 | this.reduceActions[43] = (_rhs) => { 576 | return new LocationPath(true, []); 577 | }; 578 | this.reduceActions[44] = (rhs) => { 579 | rhs[1].absolute = true; 580 | return rhs[1]; 581 | }; 582 | this.reduceActions[46] = (rhs) => { 583 | return new LocationPath(false, [rhs[0]]); 584 | }; 585 | this.reduceActions[47] = (rhs) => { 586 | rhs[0].steps.push(rhs[2]); 587 | return rhs[0]; 588 | }; 589 | this.reduceActions[49] = (rhs) => { 590 | return new Step(rhs[0], rhs[1], []); 591 | }; 592 | this.reduceActions[50] = (rhs) => { 593 | return new Step(Step.CHILD, rhs[0], []); 594 | }; 595 | this.reduceActions[51] = (rhs) => { 596 | return new Step(rhs[0], rhs[1], rhs[2]); 597 | }; 598 | this.reduceActions[52] = (rhs) => { 599 | return new Step(Step.CHILD, rhs[0], rhs[1]); 600 | }; 601 | this.reduceActions[54] = (rhs) => { 602 | return [rhs[0]]; 603 | }; 604 | this.reduceActions[55] = (rhs) => { 605 | rhs[1].unshift(rhs[0]); 606 | return rhs[1]; 607 | }; 608 | this.reduceActions[56] = (rhs) => { 609 | if (rhs[0] === 'ancestor') { 610 | return Step.ANCESTOR; 611 | } else if (rhs[0] === 'ancestor-or-self') { 612 | return Step.ANCESTORORSELF; 613 | } else if (rhs[0] === 'attribute') { 614 | return Step.ATTRIBUTE; 615 | } else if (rhs[0] === 'child') { 616 | return Step.CHILD; 617 | } else if (rhs[0] === 'descendant') { 618 | return Step.DESCENDANT; 619 | } else if (rhs[0] === 'descendant-or-self') { 620 | return Step.DESCENDANTORSELF; 621 | } else if (rhs[0] === 'following') { 622 | return Step.FOLLOWING; 623 | } else if (rhs[0] === 'following-sibling') { 624 | return Step.FOLLOWINGSIBLING; 625 | } else if (rhs[0] === 'namespace') { 626 | return Step.NAMESPACE; 627 | } else if (rhs[0] === 'parent') { 628 | return Step.PARENT; 629 | } else if (rhs[0] === 'preceding') { 630 | return Step.PRECEDING; 631 | } else if (rhs[0] === 'preceding-sibling') { 632 | return Step.PRECEDINGSIBLING; 633 | } else if (rhs[0] === 'self') { 634 | return Step.SELF; 635 | } 636 | return -1; 637 | }; 638 | this.reduceActions[57] = (_rhs) => { 639 | return Step.ATTRIBUTE; 640 | }; 641 | this.reduceActions[59] = (rhs) => { 642 | if (rhs[0] === 'comment') { 643 | return NodeTest.commentTest; 644 | } else if (rhs[0] === 'text') { 645 | return NodeTest.textTest; 646 | } else if (rhs[0] === 'processing-instruction') { 647 | return NodeTest.anyPiTest; 648 | } else if (rhs[0] === 'node') { 649 | return NodeTest.nodeTest; 650 | } 651 | return new NodeTest(-1); 652 | }; 653 | this.reduceActions[60] = (rhs) => { 654 | return new NodeTest.PITest(rhs[2]); 655 | }; 656 | this.reduceActions[61] = (rhs) => { 657 | return rhs[1]; 658 | }; 659 | this.reduceActions[63] = (rhs) => { 660 | rhs[1].absolute = true; 661 | rhs[1].steps.unshift(new Step(Step.DESCENDANTORSELF, NodeTest.nodeTest, [])); 662 | return rhs[1]; 663 | }; 664 | this.reduceActions[64] = (rhs) => { 665 | rhs[0].steps.push(new Step(Step.DESCENDANTORSELF, NodeTest.nodeTest, [])); 666 | rhs[0].steps.push(rhs[2]); 667 | return rhs[0]; 668 | }; 669 | this.reduceActions[65] = (_rhs) => { 670 | return new Step(Step.SELF, NodeTest.nodeTest, []); 671 | }; 672 | this.reduceActions[66] = (_rhs) => { 673 | return new Step(Step.PARENT, NodeTest.nodeTest, []); 674 | }; 675 | this.reduceActions[67] = (rhs) => { 676 | return new VariableReference(rhs[1]); 677 | }; 678 | this.reduceActions[68] = (_rhs) => { 679 | return NodeTest.nameTestAny; 680 | }; 681 | this.reduceActions[69] = (rhs) => { 682 | return new NodeTest.NameTestPrefixAny(rhs[0].split(':')[0]); 683 | }; 684 | this.reduceActions[70] = (rhs) => { 685 | return new NodeTest.NameTestQName(rhs[0]); 686 | }; 687 | } 688 | 689 | tokenize(s1: string): [number[], string[]] { 690 | const s = s1 + '\0'; 691 | 692 | const types: number[] = []; 693 | const values: string[] = []; 694 | let pos = 0; 695 | let c = s.charAt(pos++); 696 | while (1) { 697 | while (c === ' ' || c === '\t' || c === '\r' || c === '\n') { 698 | c = s.charAt(pos++); 699 | } 700 | if (c === '\0' || pos >= s.length) { 701 | break; 702 | } 703 | 704 | if (c === '(') { 705 | types.push(XPathParser.LEFTPARENTHESIS); 706 | values.push(c); 707 | c = s.charAt(pos++); 708 | continue; 709 | } 710 | if (c === ')') { 711 | types.push(XPathParser.RIGHTPARENTHESIS); 712 | values.push(c); 713 | c = s.charAt(pos++); 714 | continue; 715 | } 716 | if (c === '[') { 717 | types.push(XPathParser.LEFTBRACKET); 718 | values.push(c); 719 | c = s.charAt(pos++); 720 | continue; 721 | } 722 | if (c === ']') { 723 | types.push(XPathParser.RIGHTBRACKET); 724 | values.push(c); 725 | c = s.charAt(pos++); 726 | continue; 727 | } 728 | if (c === '@') { 729 | types.push(XPathParser.AT); 730 | values.push(c); 731 | c = s.charAt(pos++); 732 | continue; 733 | } 734 | if (c === ',') { 735 | types.push(XPathParser.COMMA); 736 | values.push(c); 737 | c = s.charAt(pos++); 738 | continue; 739 | } 740 | if (c === '|') { 741 | types.push(XPathParser.BAR); 742 | values.push(c); 743 | c = s.charAt(pos++); 744 | continue; 745 | } 746 | if (c === '+') { 747 | types.push(XPathParser.PLUS); 748 | values.push(c); 749 | c = s.charAt(pos++); 750 | continue; 751 | } 752 | if (c === '-') { 753 | types.push(XPathParser.MINUS); 754 | values.push(c); 755 | c = s.charAt(pos++); 756 | continue; 757 | } 758 | if (c === '=') { 759 | types.push(XPathParser.EQUALS); 760 | values.push(c); 761 | c = s.charAt(pos++); 762 | continue; 763 | } 764 | if (c === '$') { 765 | types.push(XPathParser.DOLLAR); 766 | values.push(c); 767 | c = s.charAt(pos++); 768 | continue; 769 | } 770 | 771 | if (c === '.') { 772 | c = s.charAt(pos++); 773 | if (c === '.') { 774 | types.push(XPathParser.DOUBLEDOT); 775 | values.push('..'); 776 | c = s.charAt(pos++); 777 | continue; 778 | } 779 | if (c >= '0' && c <= '9') { 780 | let num = '.' + c; 781 | c = s.charAt(pos++); 782 | while (c >= '0' && c <= '9') { 783 | num += c; 784 | c = s.charAt(pos++); 785 | } 786 | types.push(XPathParser.NUMBER); 787 | values.push(num); 788 | continue; 789 | } 790 | types.push(XPathParser.DOT); 791 | values.push('.'); 792 | continue; 793 | } 794 | 795 | if (c === "'" || c === '"') { 796 | const delimiter = c; 797 | let literal = ''; 798 | // tslint:disable-next-line:no-conditional-assignment 799 | while (pos < s.length && (c = s.charAt(pos)) !== delimiter) { 800 | literal += c; 801 | pos += 1; 802 | } 803 | if (c !== delimiter) { 804 | throw XPathException.fromMessage('Unterminated string literal: ' + delimiter + literal); 805 | } 806 | pos += 1; 807 | types.push(XPathParser.LITERAL); 808 | values.push(literal); 809 | c = s.charAt(pos++); 810 | continue; 811 | } 812 | 813 | if (c >= '0' && c <= '9') { 814 | let num = c; 815 | c = s.charAt(pos++); 816 | while (c >= '0' && c <= '9') { 817 | num += c; 818 | c = s.charAt(pos++); 819 | } 820 | if (c === '.') { 821 | if (s.charAt(pos) >= '0' && s.charAt(pos) <= '9') { 822 | num += c; 823 | num += s.charAt(pos++); 824 | c = s.charAt(pos++); 825 | while (c >= '0' && c <= '9') { 826 | num += c; 827 | c = s.charAt(pos++); 828 | } 829 | } 830 | } 831 | types.push(XPathParser.NUMBER); 832 | values.push(num); 833 | continue; 834 | } 835 | 836 | if (c === '*') { 837 | if (types.length > 0) { 838 | const last = types[types.length - 1]; 839 | if ( 840 | last !== XPathParser.AT && 841 | last !== XPathParser.DOUBLECOLON && 842 | last !== XPathParser.LEFTPARENTHESIS && 843 | last !== XPathParser.LEFTBRACKET && 844 | last !== XPathParser.AND && 845 | last !== XPathParser.OR && 846 | last !== XPathParser.MOD && 847 | last !== XPathParser.DIV && 848 | last !== XPathParser.MULTIPLYOPERATOR && 849 | last !== XPathParser.SLASH && 850 | last !== XPathParser.DOUBLESLASH && 851 | last !== XPathParser.BAR && 852 | last !== XPathParser.PLUS && 853 | last !== XPathParser.MINUS && 854 | last !== XPathParser.EQUALS && 855 | last !== XPathParser.NOTEQUAL && 856 | last !== XPathParser.LESSTHAN && 857 | last !== XPathParser.LESSTHANOREQUAL && 858 | last !== XPathParser.GREATERTHAN && 859 | last !== XPathParser.GREATERTHANOREQUAL 860 | ) { 861 | types.push(XPathParser.MULTIPLYOPERATOR); 862 | values.push(c); 863 | c = s.charAt(pos++); 864 | continue; 865 | } 866 | } 867 | types.push(XPathParser.ASTERISKNAMETEST); 868 | values.push(c); 869 | c = s.charAt(pos++); 870 | continue; 871 | } 872 | 873 | if (c === ':') { 874 | if (s.charAt(pos) === ':') { 875 | types.push(XPathParser.DOUBLECOLON); 876 | values.push('::'); 877 | pos++; 878 | c = s.charAt(pos++); 879 | continue; 880 | } 881 | } 882 | 883 | if (c === '/') { 884 | c = s.charAt(pos++); 885 | if (c === '/') { 886 | types.push(XPathParser.DOUBLESLASH); 887 | values.push('//'); 888 | c = s.charAt(pos++); 889 | continue; 890 | } 891 | types.push(XPathParser.SLASH); 892 | values.push('/'); 893 | continue; 894 | } 895 | 896 | if (c === '!') { 897 | if (s.charAt(pos) === '=') { 898 | types.push(XPathParser.NOTEQUAL); 899 | values.push('!='); 900 | pos++; 901 | c = s.charAt(pos++); 902 | continue; 903 | } 904 | } 905 | 906 | if (c === '<') { 907 | if (s.charAt(pos) === '=') { 908 | types.push(XPathParser.LESSTHANOREQUAL); 909 | values.push('<='); 910 | pos++; 911 | c = s.charAt(pos++); 912 | continue; 913 | } 914 | types.push(XPathParser.LESSTHAN); 915 | values.push('<'); 916 | c = s.charAt(pos++); 917 | continue; 918 | } 919 | 920 | if (c === '>') { 921 | if (s.charAt(pos) === '=') { 922 | types.push(XPathParser.GREATERTHANOREQUAL); 923 | values.push('>='); 924 | pos++; 925 | c = s.charAt(pos++); 926 | continue; 927 | } 928 | types.push(XPathParser.GREATERTHAN); 929 | values.push('>'); 930 | c = s.charAt(pos++); 931 | continue; 932 | } 933 | 934 | if (c === '_' || isLetter(c.charCodeAt(0))) { 935 | let name = c; 936 | c = s.charAt(pos++); 937 | while (isNCNameChar(c.charCodeAt(0))) { 938 | name += c; 939 | c = s.charAt(pos++); 940 | } 941 | if (types.length > 0) { 942 | const last = types[types.length - 1]; 943 | if ( 944 | last !== XPathParser.AT && 945 | last !== XPathParser.DOUBLECOLON && 946 | last !== XPathParser.LEFTPARENTHESIS && 947 | last !== XPathParser.LEFTBRACKET && 948 | last !== XPathParser.AND && 949 | last !== XPathParser.OR && 950 | last !== XPathParser.MOD && 951 | last !== XPathParser.DIV && 952 | last !== XPathParser.MULTIPLYOPERATOR && 953 | last !== XPathParser.SLASH && 954 | last !== XPathParser.DOUBLESLASH && 955 | last !== XPathParser.BAR && 956 | last !== XPathParser.PLUS && 957 | last !== XPathParser.MINUS && 958 | last !== XPathParser.EQUALS && 959 | last !== XPathParser.NOTEQUAL && 960 | last !== XPathParser.LESSTHAN && 961 | last !== XPathParser.LESSTHANOREQUAL && 962 | last !== XPathParser.GREATERTHAN && 963 | last !== XPathParser.GREATERTHANOREQUAL 964 | ) { 965 | if (name === 'and') { 966 | types.push(XPathParser.AND); 967 | values.push(name); 968 | continue; 969 | } 970 | if (name === 'or') { 971 | types.push(XPathParser.OR); 972 | values.push(name); 973 | continue; 974 | } 975 | if (name === 'mod') { 976 | types.push(XPathParser.MOD); 977 | values.push(name); 978 | continue; 979 | } 980 | if (name === 'div') { 981 | types.push(XPathParser.DIV); 982 | values.push(name); 983 | continue; 984 | } 985 | } 986 | } 987 | if (c === ':') { 988 | if (s.charAt(pos) === '*') { 989 | types.push(XPathParser.NCNAMECOLONASTERISK); 990 | values.push(name + ':*'); 991 | pos++; 992 | c = s.charAt(pos++); 993 | continue; 994 | } 995 | if (s.charAt(pos) === '_' || isLetter(s.charCodeAt(pos))) { 996 | name += ':'; 997 | c = s.charAt(pos++); 998 | while (isNCNameChar(c.charCodeAt(0))) { 999 | name += c; 1000 | c = s.charAt(pos++); 1001 | } 1002 | if (c === '(') { 1003 | types.push(XPathParser.FUNCTIONNAME); 1004 | values.push(name); 1005 | continue; 1006 | } 1007 | types.push(XPathParser.QNAME); 1008 | values.push(name); 1009 | continue; 1010 | } 1011 | if (s.charAt(pos) === ':') { 1012 | types.push(XPathParser.AXISNAME); 1013 | values.push(name); 1014 | continue; 1015 | } 1016 | } 1017 | if (c === '(') { 1018 | if (name === 'comment' || name === 'text' || name === 'node') { 1019 | types.push(XPathParser.NODETYPE); 1020 | values.push(name); 1021 | continue; 1022 | } 1023 | if (name === 'processing-instruction') { 1024 | if (s.charAt(pos) === ')') { 1025 | types.push(XPathParser.NODETYPE); 1026 | } else { 1027 | types.push(XPathParser.PROCESSINGINSTRUCTIONWITHLITERAL); 1028 | } 1029 | values.push(name); 1030 | continue; 1031 | } 1032 | types.push(XPathParser.FUNCTIONNAME); 1033 | values.push(name); 1034 | continue; 1035 | } 1036 | types.push(XPathParser.QNAME); 1037 | values.push(name); 1038 | continue; 1039 | } 1040 | 1041 | throw new Error('Unexpected character ' + c); 1042 | } 1043 | types.push(1); 1044 | values.push('[EOF]'); 1045 | return [types, values]; 1046 | } 1047 | 1048 | parse(str: string): XPath { 1049 | const res = this.tokenize(str); 1050 | 1051 | const types = res[0]; 1052 | const values = res[1]; 1053 | let tokenPos: number = 0; 1054 | const state: number[] = []; 1055 | const tokenType: number[] = []; 1056 | const tokenValue: any[] = []; 1057 | let s: number; 1058 | let a: number; 1059 | let t: string; 1060 | 1061 | state.push(0); 1062 | tokenType.push(1); 1063 | tokenValue.push('_S'); 1064 | 1065 | a = types[tokenPos]; 1066 | t = values[tokenPos++]; 1067 | while (1) { 1068 | s = state[state.length - 1]; 1069 | switch (XPathParser.actionTable[s].charAt(a - 1)) { 1070 | case XPathParser.SHIFT: 1071 | tokenType.push(-a); 1072 | tokenValue.push(t); 1073 | state.push(XPathParser.actionTableNumber[s].charCodeAt(a - 1) - 32); 1074 | a = types[tokenPos]; 1075 | t = values[tokenPos++]; 1076 | break; 1077 | case XPathParser.REDUCE: 1078 | const num = XPathParser.productions[XPathParser.actionTableNumber[s].charCodeAt(a - 1) - 32][1]; 1079 | const rhs: string[] = []; 1080 | for (let i = 0; i < num; i++) { 1081 | tokenType.pop(); 1082 | rhs.unshift(tokenValue.pop()!); 1083 | state.pop(); 1084 | } 1085 | const lastState = state[state.length - 1]; 1086 | tokenType.push(XPathParser.productions[XPathParser.actionTableNumber[s].charCodeAt(a - 1) - 32][0]); 1087 | if (this.reduceActions[XPathParser.actionTableNumber[s].charCodeAt(a - 1) - 32] === undefined) { 1088 | tokenValue.push(rhs[0]); 1089 | } else { 1090 | tokenValue.push(this.reduceActions[XPathParser.actionTableNumber[s].charCodeAt(a - 1) - 32](rhs)); 1091 | } 1092 | state.push( 1093 | XPathParser.gotoTable[lastState].charCodeAt( 1094 | XPathParser.productions[XPathParser.actionTableNumber[s].charCodeAt(a - 1) - 32][0] - 2 1095 | ) - 33 1096 | ); 1097 | break; 1098 | case XPathParser.ACCEPT: 1099 | const expr = tokenValue.pop(); 1100 | if (!(expr instanceof Expression)) { 1101 | throw new Error('XPath parse error. Wrong result type.'); 1102 | } 1103 | return new XPath(expr); 1104 | default: 1105 | throw new Error('XPath parse error'); 1106 | } 1107 | } 1108 | 1109 | throw new Error('XPath parse error'); 1110 | } 1111 | } 1112 | -------------------------------------------------------------------------------- /src/xpath-result-impl.ts: -------------------------------------------------------------------------------- 1 | import { XPathException } from './xpath-exception'; 2 | import { Expression, XBoolean, XNodeSet, XNumber, XString } from './xpath-types'; 3 | 4 | export class XPathResultImpl implements XPathResult { 5 | static readonly ANY_TYPE = 0; 6 | static NUMBER_TYPE = 1; 7 | static STRING_TYPE = 2; 8 | static BOOLEAN_TYPE = 3; 9 | static UNORDERED_NODE_ITERATOR_TYPE = 4; 10 | static ORDERED_NODE_ITERATOR_TYPE = 5; 11 | static UNORDERED_NODE_SNAPSHOT_TYPE = 6; 12 | static ORDERED_NODE_SNAPSHOT_TYPE = 7; 13 | static ANY_UNORDERED_NODE_TYPE = 8; 14 | static FIRST_ORDERED_NODE_TYPE = 9; 15 | 16 | resultType: number; 17 | numberValue: number; 18 | stringValue: string; 19 | booleanValue: boolean; 20 | 21 | nodes: Node[]; 22 | singleNodeValue: Node; 23 | invalidIteratorState: boolean; 24 | iteratorIndex: number; 25 | snapshotLength: number; 26 | 27 | ANY_TYPE = XPathResultImpl.ANY_TYPE; 28 | NUMBER_TYPE = XPathResultImpl.NUMBER_TYPE; 29 | STRING_TYPE = XPathResultImpl.STRING_TYPE; 30 | BOOLEAN_TYPE = XPathResultImpl.BOOLEAN_TYPE; 31 | UNORDERED_NODE_ITERATOR_TYPE = XPathResultImpl.UNORDERED_NODE_ITERATOR_TYPE; 32 | ORDERED_NODE_ITERATOR_TYPE = XPathResultImpl.ORDERED_NODE_ITERATOR_TYPE; 33 | UNORDERED_NODE_SNAPSHOT_TYPE = XPathResultImpl.UNORDERED_NODE_SNAPSHOT_TYPE; 34 | ORDERED_NODE_SNAPSHOT_TYPE = XPathResultImpl.ORDERED_NODE_SNAPSHOT_TYPE; 35 | ANY_UNORDERED_NODE_TYPE = XPathResultImpl.ANY_UNORDERED_NODE_TYPE; 36 | FIRST_ORDERED_NODE_TYPE = XPathResultImpl.FIRST_ORDERED_NODE_TYPE; 37 | 38 | constructor(v: Expression, t: number) { 39 | if (t === XPathResultImpl.ANY_TYPE) { 40 | if (v instanceof XString) { 41 | t = XPathResultImpl.STRING_TYPE; 42 | } else if (v instanceof XNumber) { 43 | t = XPathResultImpl.NUMBER_TYPE; 44 | } else if (v instanceof XBoolean) { 45 | t = XPathResultImpl.BOOLEAN_TYPE; 46 | } else if (v instanceof XNodeSet || v == null) { 47 | t = XPathResultImpl.UNORDERED_NODE_ITERATOR_TYPE; 48 | } 49 | } 50 | this.resultType = t; 51 | switch (t) { 52 | case XPathResultImpl.NUMBER_TYPE: 53 | this.numberValue = v.numberValue; 54 | this.stringValue = v.stringValue; 55 | this.booleanValue = v.booleanValue; 56 | return; 57 | case XPathResultImpl.STRING_TYPE: 58 | this.numberValue = v.numberValue; 59 | this.stringValue = v.stringValue; 60 | this.booleanValue = v.booleanValue; 61 | return; 62 | case XPathResultImpl.BOOLEAN_TYPE: 63 | this.numberValue = v.numberValue; 64 | this.stringValue = v.stringValue; 65 | this.booleanValue = v.booleanValue; 66 | return; 67 | case XPathResultImpl.ANY_UNORDERED_NODE_TYPE: 68 | case XPathResultImpl.FIRST_ORDERED_NODE_TYPE: 69 | if (v instanceof XNodeSet) { 70 | const first = v.first(); 71 | 72 | this.singleNodeValue = first as never; 73 | this.numberValue = v.numberValue; 74 | this.stringValue = v.stringValue; 75 | this.booleanValue = v.booleanValue; 76 | this.nodes = first != null ? [first] : []; 77 | 78 | return; 79 | } 80 | break; 81 | case XPathResultImpl.UNORDERED_NODE_ITERATOR_TYPE: 82 | case XPathResultImpl.ORDERED_NODE_ITERATOR_TYPE: 83 | if (v instanceof XNodeSet) { 84 | this.invalidIteratorState = false; 85 | this.nodes = v.toArray(); 86 | this.iteratorIndex = 0; 87 | this.numberValue = v.numberValue; 88 | this.stringValue = v.stringValue; 89 | this.booleanValue = v.booleanValue; 90 | return; 91 | } else if (v == null) { 92 | this.nodes = []; 93 | this.snapshotLength = 0; 94 | this.numberValue = 0; 95 | this.stringValue = ''; 96 | this.booleanValue = false; 97 | return; 98 | } 99 | break; 100 | case XPathResultImpl.UNORDERED_NODE_SNAPSHOT_TYPE: 101 | case XPathResultImpl.ORDERED_NODE_SNAPSHOT_TYPE: 102 | if (v instanceof XNodeSet) { 103 | this.nodes = v.toArray(); 104 | this.snapshotLength = this.nodes.length; 105 | this.numberValue = v.numberValue; 106 | this.stringValue = v.stringValue; 107 | this.booleanValue = v.booleanValue; 108 | return; 109 | } 110 | break; 111 | } 112 | throw new XPathException(XPathException.TYPE_ERR); 113 | } 114 | 115 | iterateNext() { 116 | if ( 117 | this.resultType !== XPathResultImpl.UNORDERED_NODE_ITERATOR_TYPE && 118 | this.resultType !== XPathResultImpl.ORDERED_NODE_ITERATOR_TYPE 119 | ) { 120 | throw new XPathException(XPathException.TYPE_ERR); 121 | } 122 | 123 | if (this.iteratorIndex === this.nodes.length) { 124 | return null as never; 125 | } 126 | 127 | return this.nodes[this.iteratorIndex++]; 128 | } 129 | 130 | snapshotItem(i: number) { 131 | if ( 132 | this.resultType !== XPathResultImpl.UNORDERED_NODE_SNAPSHOT_TYPE && 133 | this.resultType !== XPathResultImpl.ORDERED_NODE_SNAPSHOT_TYPE 134 | ) { 135 | throw new XPathException(XPathException.TYPE_ERR); 136 | } 137 | return this.nodes[i]; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/xpath-types.ts: -------------------------------------------------------------------------------- 1 | import { AVLTree } from './avl-tree'; 2 | import { isAttribute, isCData, isDocument, isElement, isFragment, isNamespaceNode, isText } from './utils/types'; 3 | 4 | // tslint:disable:prefer-for-of 5 | // tslint:disable:member-ordering 6 | 7 | export abstract class Expression { 8 | toString() { 9 | return ''; 10 | } 11 | 12 | evaluate(_c: XPathContext): Expression { 13 | throw new Error('Could not evaluate expression.'); 14 | } 15 | 16 | get string(): XString { 17 | throw new Error('Could not evaluate expression.'); 18 | } 19 | 20 | get number(): XNumber { 21 | throw new Error('Could not evaluate expression.'); 22 | } 23 | 24 | get bool(): XBoolean { 25 | throw new Error('Could not evaluate expression.'); 26 | } 27 | 28 | get nodeset(): XNodeSet { 29 | throw new Error('Could not evaluate expression.'); 30 | } 31 | 32 | get stringValue(): string { 33 | throw new Error('Could not evaluate expression.'); 34 | } 35 | 36 | get numberValue(): number { 37 | throw new Error('Could not evaluate expression.'); 38 | } 39 | 40 | get booleanValue(): boolean { 41 | throw new Error('Could not evaluate expression.'); 42 | } 43 | 44 | equals(_r: Expression): XBoolean { 45 | throw new Error('Could not evaluate expression.'); 46 | } 47 | notequal(_r: Expression): XBoolean { 48 | throw new Error('Could not evaluate expression.'); 49 | } 50 | lessthan(_r: Expression): XBoolean { 51 | throw new Error('Could not evaluate expression.'); 52 | } 53 | greaterthan(_r: Expression): XBoolean { 54 | throw new Error('Could not evaluate expression.'); 55 | } 56 | lessthanorequal(_r: Expression): XBoolean { 57 | throw new Error('Could not evaluate expression.'); 58 | } 59 | greaterthanorequal(_r: Expression): XBoolean { 60 | throw new Error('Could not evaluate expression.'); 61 | } 62 | } 63 | 64 | export class XBoolean extends Expression { 65 | static TRUE = new XBoolean(true); 66 | static FALSE = new XBoolean(false); 67 | 68 | b: boolean; 69 | 70 | constructor(b: any) { 71 | super(); 72 | 73 | this.b = Boolean(b); 74 | } 75 | 76 | toString() { 77 | return this.b.toString(); 78 | } 79 | 80 | evaluate(_c: XPathContext) { 81 | return this; 82 | } 83 | 84 | get string() { 85 | return new XString(this.b); 86 | } 87 | 88 | get number() { 89 | return new XNumber(this.b); 90 | } 91 | 92 | get bool() { 93 | return this; 94 | } 95 | 96 | get nodeset(): XNodeSet { 97 | throw new Error('Cannot convert boolean to nodeset'); 98 | } 99 | 100 | get stringValue() { 101 | return this.string.stringValue; 102 | } 103 | 104 | get numberValue() { 105 | return this.number.numberValue; 106 | } 107 | 108 | get booleanValue() { 109 | return this.b; 110 | } 111 | 112 | not() { 113 | return new XBoolean(!this.b); 114 | } 115 | 116 | equals(r: Expression): XBoolean { 117 | if (r instanceof XString || r instanceof XNumber) { 118 | return this.equals(r.bool); 119 | } else if (r instanceof XNodeSet) { 120 | return r.compareWithBoolean(this, Operators.equals); 121 | } else if (r instanceof XBoolean) { 122 | return new XBoolean(this.b === r.b); 123 | } else { 124 | throw new Error('Unsupported type'); 125 | } 126 | } 127 | 128 | notequal(r: Expression): XBoolean { 129 | if (r instanceof XString || r instanceof XNumber) { 130 | return this.notequal(r.bool); 131 | } else if (r instanceof XNodeSet) { 132 | return r.compareWithBoolean(this, Operators.notequal); 133 | } else if (r instanceof XBoolean) { 134 | return new XBoolean(this.b !== r.b); 135 | } else { 136 | throw new Error('Unsupported type'); 137 | } 138 | } 139 | 140 | lessthan(r: Expression): XBoolean { 141 | return this.number.lessthan(r); 142 | } 143 | 144 | greaterthan(r: Expression): XBoolean { 145 | return this.number.greaterthan(r); 146 | } 147 | 148 | lessthanorequal(r: Expression): XBoolean { 149 | return this.number.lessthanorequal(r); 150 | } 151 | 152 | greaterthanorequal(r: Expression): XBoolean { 153 | return this.number.greaterthanorequal(r); 154 | } 155 | } 156 | 157 | export class XNumber extends Expression { 158 | num: number; 159 | constructor(n: any) { 160 | super(); 161 | 162 | this.num = typeof n === 'string' ? this.parse(n) : Number(n); 163 | } 164 | 165 | get numberFormat() { 166 | return /^\s*-?[0-9]*\.?[0-9]+\s*$/; 167 | } 168 | 169 | parse(s: string) { 170 | // XPath representation of numbers is more restrictive than what Number() or parseFloat() allow 171 | return this.numberFormat.test(s) ? parseFloat(s) : Number.NaN; 172 | } 173 | 174 | toString() { 175 | const strValue = this.num.toString(); 176 | 177 | if (strValue.indexOf('e-') !== -1) { 178 | return padSmallNumber(strValue); 179 | } 180 | 181 | if (strValue.indexOf('e') !== -1) { 182 | return padLargeNumber(strValue); 183 | } 184 | 185 | return strValue; 186 | } 187 | 188 | evaluate(_c: XPathContext) { 189 | return this; 190 | } 191 | 192 | get string() { 193 | return new XString(this.toString()); 194 | } 195 | 196 | get number() { 197 | return this; 198 | } 199 | 200 | get bool() { 201 | return new XBoolean(this.num); 202 | } 203 | 204 | get nodeset(): XNodeSet { 205 | throw new Error('Cannot convert string to nodeset'); 206 | } 207 | 208 | get stringValue() { 209 | return this.string.stringValue; 210 | } 211 | 212 | get numberValue() { 213 | return this.num; 214 | } 215 | 216 | get booleanValue() { 217 | return this.bool.booleanValue; 218 | } 219 | 220 | negate() { 221 | return new XNumber(-this.num); 222 | } 223 | 224 | equals(r: Expression): XBoolean { 225 | if (r instanceof XBoolean) { 226 | return this.bool.equals(r); 227 | } else if (r instanceof XString) { 228 | return this.string.equals(r); 229 | } else if (r instanceof XNodeSet) { 230 | return r.compareWithNumber(this, Operators.equals); 231 | } else if (r instanceof XNumber) { 232 | return new XBoolean(this.num === r.num); 233 | } else { 234 | throw new Error('Unsupported type'); 235 | } 236 | } 237 | 238 | notequal(r: Expression): XBoolean { 239 | if (r instanceof XBoolean) { 240 | return this.bool.notequal(r); 241 | } else if (r instanceof XString) { 242 | return this.notequal(r.number); 243 | } else if (r instanceof XNodeSet) { 244 | return r.compareWithNumber(this, Operators.notequal); 245 | } else if (r instanceof XNumber) { 246 | return new XBoolean(this.num !== r.num); 247 | } else { 248 | throw new Error('Unsupported type'); 249 | } 250 | } 251 | 252 | lessthan(r: Expression): XBoolean { 253 | if (r instanceof XNodeSet) { 254 | return r.compareWithNumber(this, Operators.greaterthan); 255 | } else if (r instanceof XBoolean || r instanceof XString) { 256 | return this.lessthan(r.number); 257 | } else if (r instanceof XNumber) { 258 | return new XBoolean(this.num < r.num); 259 | } else { 260 | throw new Error('Unsupported type'); 261 | } 262 | } 263 | 264 | greaterthan(r: Expression): XBoolean { 265 | if (r instanceof XNodeSet) { 266 | return r.compareWithNumber(this, Operators.lessthan); 267 | } else if (r instanceof XBoolean || r instanceof XString) { 268 | return this.greaterthan(r.number); 269 | } else if (r instanceof XNumber) { 270 | return new XBoolean(this.num > r.num); 271 | } else { 272 | throw new Error('Unsupported type'); 273 | } 274 | } 275 | 276 | lessthanorequal(r: Expression): XBoolean { 277 | if (r instanceof XNodeSet) { 278 | return r.compareWithNumber(this, Operators.greaterthanorequal); 279 | } else if (r instanceof XBoolean || r instanceof XString) { 280 | return this.lessthanorequal(r.number); 281 | } else if (r instanceof XNumber) { 282 | return new XBoolean(this.num <= r.num); 283 | } else { 284 | throw new Error('Unsupported type'); 285 | } 286 | } 287 | 288 | greaterthanorequal(r: Expression): XBoolean { 289 | if (r instanceof XNodeSet) { 290 | return r.compareWithNumber(this, Operators.lessthanorequal); 291 | } else if (r instanceof XBoolean || r instanceof XString) { 292 | return this.greaterthanorequal(r.number); 293 | } else if (r instanceof XNumber) { 294 | return new XBoolean(this.num >= r.num); 295 | } else { 296 | throw new Error('Unsupported type'); 297 | } 298 | } 299 | 300 | plus(r: XNumber) { 301 | return new XNumber(this.num + r.num); 302 | } 303 | 304 | minus(r: XNumber) { 305 | return new XNumber(this.num - r.num); 306 | } 307 | 308 | multiply(r: XNumber) { 309 | return new XNumber(this.num * r.num); 310 | } 311 | 312 | div(r: XNumber) { 313 | return new XNumber(this.num / r.num); 314 | } 315 | 316 | mod(r: XNumber) { 317 | return new XNumber(this.num % r.num); 318 | } 319 | } 320 | 321 | function padSmallNumber(numberStr: string) { 322 | const parts = numberStr.split('e-'); 323 | let base = parts[0].replace('.', ''); 324 | const exponent = Number(parts[1]); 325 | 326 | for (let i = 0; i < exponent - 1; i += 1) { 327 | base = '0' + base; 328 | } 329 | 330 | return '0.' + base; 331 | } 332 | 333 | function padLargeNumber(numberStr: string) { 334 | const parts = numberStr.split('e'); 335 | let base = parts[0].replace('.', ''); 336 | const exponent = Number(parts[1]); 337 | const zerosToAppend = exponent + 1 - base.length; 338 | 339 | for (let i = 0; i < zerosToAppend; i += 1) { 340 | base += '0'; 341 | } 342 | 343 | return base; 344 | } 345 | 346 | export class XString extends Expression { 347 | str: string; 348 | constructor(s: any) { 349 | super(); 350 | 351 | this.str = String(s); 352 | } 353 | 354 | toString() { 355 | return this.str; 356 | } 357 | 358 | evaluate(_c: XPathContext) { 359 | return this; 360 | } 361 | 362 | get string() { 363 | return this; 364 | } 365 | 366 | get number() { 367 | return new XNumber(this.str); 368 | } 369 | 370 | get bool() { 371 | return new XBoolean(this.str); 372 | } 373 | 374 | get nodeset(): XNodeSet { 375 | throw new Error('Cannot convert string to nodeset'); 376 | } 377 | 378 | get stringValue() { 379 | return this.str; 380 | } 381 | 382 | get numberValue() { 383 | return this.number.numberValue; 384 | } 385 | 386 | get booleanValue() { 387 | return this.bool.booleanValue; 388 | } 389 | 390 | equals(r: Expression): XBoolean { 391 | if (r instanceof XBoolean) { 392 | return this.bool.equals(r); 393 | } else if (r instanceof XNumber) { 394 | return this.number.equals(r); 395 | } else if (r instanceof XNodeSet) { 396 | return r.compareWithString(this, Operators.equals); 397 | } else if (r instanceof XString) { 398 | return new XBoolean(this.str === r.str); 399 | } else { 400 | throw new Error('Unsupported type'); 401 | } 402 | } 403 | 404 | notequal(r: Expression): XBoolean { 405 | if (r instanceof XBoolean) { 406 | return this.bool.notequal(r); 407 | } else if (r instanceof XNumber) { 408 | return this.number.notequal(r); 409 | } else if (r instanceof XNodeSet) { 410 | return r.compareWithString(this, Operators.notequal); 411 | } else if (r instanceof XString) { 412 | return new XBoolean(this.str !== r.str); 413 | } else { 414 | throw new Error('Unsupported type'); 415 | } 416 | } 417 | 418 | lessthan(r: Expression): XBoolean { 419 | return this.number.lessthan(r); 420 | } 421 | 422 | greaterthan(r: Expression): XBoolean { 423 | return this.number.greaterthan(r); 424 | } 425 | 426 | lessthanorequal(r: Expression): XBoolean { 427 | return this.number.lessthanorequal(r); 428 | } 429 | 430 | greaterthanorequal(r: Expression): XBoolean { 431 | return this.number.greaterthanorequal(r); 432 | } 433 | } 434 | 435 | export class XNodeSet extends Expression { 436 | static compareWith(o: Operator) { 437 | return function(this: XNodeSet, r: Expression): XBoolean { 438 | if (r instanceof XString) { 439 | return this.compareWithString(r, o); 440 | } else if (r instanceof XNumber) { 441 | return this.compareWithNumber(r, o); 442 | } else if (r instanceof XBoolean) { 443 | return this.compareWithBoolean(r, o); 444 | } else if (r instanceof XNodeSet) { 445 | return this.compareWithNodeSet(r, o); 446 | } else { 447 | throw new Error('Unsupported type'); 448 | } 449 | }; 450 | } 451 | 452 | tree: AVLTree | null; 453 | nodes: Node[]; 454 | size: number; 455 | 456 | constructor() { 457 | super(); 458 | 459 | this.tree = null; 460 | this.nodes = []; 461 | this.size = 0; 462 | } 463 | 464 | toString() { 465 | const p = this.first(); 466 | if (p == null) { 467 | return ''; 468 | } 469 | return this.stringForNode(p) || ''; 470 | } 471 | 472 | evaluate(_c: XPathContext) { 473 | return this; 474 | } 475 | 476 | get string() { 477 | return new XString(this.toString()); 478 | } 479 | 480 | get stringValue() { 481 | return this.toString(); 482 | } 483 | 484 | get number() { 485 | return new XNumber(this.string); 486 | } 487 | 488 | get numberValue() { 489 | return Number(this.string); 490 | } 491 | 492 | get bool() { 493 | return new XBoolean(this.booleanValue); 494 | } 495 | 496 | get booleanValue() { 497 | return !!this.size; 498 | } 499 | 500 | get nodeset() { 501 | return this; 502 | } 503 | 504 | stringForNode(n: Node) { 505 | if (isDocument(n) || isElement(n) || isFragment(n)) { 506 | return this.stringForContainerNode(n); 507 | } else if (isAttribute(n)) { 508 | return n.value || n.nodeValue || ''; 509 | } else if (isNamespaceNode(n)) { 510 | return n.value; 511 | } 512 | return n.nodeValue || ''; 513 | } 514 | 515 | stringForContainerNode(n: Node) { 516 | let s = ''; 517 | for (let n2 = n.firstChild; n2 != null; n2 = n2.nextSibling as ChildNode) { 518 | if (isElement(n2) || isText(n2) || isCData(n2) || isDocument(n2) || isFragment(n2)) { 519 | s += this.stringForNode(n2); 520 | } 521 | } 522 | return s; 523 | } 524 | 525 | buildTree() { 526 | if (!this.tree && this.nodes.length) { 527 | this.tree = new AVLTree(this.nodes[0]); 528 | for (let i = 1; i < this.nodes.length; i += 1) { 529 | this.tree.add(this.nodes[i]); 530 | } 531 | } 532 | 533 | return this.tree; 534 | } 535 | 536 | first() { 537 | let p = this.buildTree(); 538 | if (p == null) { 539 | return null; 540 | } 541 | while (p.left != null) { 542 | p = p.left; 543 | } 544 | return p.node; 545 | } 546 | 547 | add(n: Node) { 548 | for (let i = 0; i < this.nodes.length; i += 1) { 549 | if (n === this.nodes[i]) { 550 | return; 551 | } 552 | } 553 | 554 | this.tree = null; 555 | this.nodes.push(n); 556 | this.size += 1; 557 | } 558 | 559 | addArray(ns: Node[]) { 560 | const self = this; 561 | 562 | ns.forEach((x) => self.add(x)); 563 | } 564 | 565 | /** 566 | * Returns an array of the node set's contents in document order 567 | */ 568 | toArray() { 569 | const a: Node[] = []; 570 | this.toArrayRec(this.buildTree(), a); 571 | return a; 572 | } 573 | 574 | toArrayRec(t: AVLTree | null, a: Node[]) { 575 | if (t != null) { 576 | this.toArrayRec(t.left, a); 577 | a.push(t.node); 578 | this.toArrayRec(t.right, a); 579 | } 580 | } 581 | 582 | /** 583 | * Returns an array of the node set's contents in arbitrary order 584 | */ 585 | toUnsortedArray() { 586 | return this.nodes.slice(); 587 | } 588 | 589 | compareWithString(r: XString, o: Operator): XBoolean { 590 | const a = this.toUnsortedArray(); 591 | for (let i = 0; i < a.length; i++) { 592 | const n = a[i]; 593 | const l = new XString(this.stringForNode(n)); 594 | const res = o(l, r); 595 | if (res.booleanValue) { 596 | return res; 597 | } 598 | } 599 | return new XBoolean(false); 600 | } 601 | 602 | compareWithNumber(r: XNumber, o: Operator): XBoolean { 603 | const a = this.toUnsortedArray(); 604 | for (let i = 0; i < a.length; i++) { 605 | const n = a[i]; 606 | const l = new XNumber(this.stringForNode(n)); 607 | const res = o(l, r); 608 | if (res.booleanValue) { 609 | return res; 610 | } 611 | } 612 | return new XBoolean(false); 613 | } 614 | 615 | compareWithBoolean(r: XBoolean, o: Operator): XBoolean { 616 | return o(this.bool, r); 617 | } 618 | 619 | compareWithNodeSet(r: XNodeSet, o: Operator) { 620 | const arr = this.toUnsortedArray(); 621 | const oInvert = (lop: Expression, rop: Expression) => { 622 | return o(rop, lop); 623 | }; 624 | 625 | for (let i = 0; i < arr.length; i++) { 626 | const l = new XString(this.stringForNode(arr[i])); 627 | 628 | const res = r.compareWithString(l, oInvert); 629 | if (res.booleanValue) { 630 | return res; 631 | } 632 | } 633 | 634 | return new XBoolean(false); 635 | } 636 | 637 | equals = XNodeSet.compareWith(Operators.equals); 638 | notequal = XNodeSet.compareWith(Operators.notequal); 639 | lessthan = XNodeSet.compareWith(Operators.lessthan); 640 | greaterthan = XNodeSet.compareWith(Operators.greaterthan); 641 | lessthanorequal = XNodeSet.compareWith(Operators.lessthanorequal); 642 | greaterthanorequal = XNodeSet.compareWith(Operators.greaterthanorequal); 643 | 644 | union(r: XNodeSet) { 645 | const ns = new XNodeSet(); 646 | ns.addArray(this.toUnsortedArray()); 647 | ns.addArray(r.toUnsortedArray()); 648 | return ns; 649 | } 650 | } 651 | 652 | export type FunctionType = (c: XPathContext, ...args: Expression[]) => Expression; 653 | 654 | export interface FunctionResolver { 655 | getFunction(localName: string, namespace: string): FunctionType | undefined; 656 | } 657 | 658 | export interface VariableResolver { 659 | getVariable(ln: string, ns: string): Expression | null; 660 | } 661 | 662 | export interface NamespaceResolver { 663 | getNamespace(prefix: string, n: Node | null): string | null; 664 | } 665 | 666 | export class XPathContext { 667 | variableResolver: VariableResolver; 668 | namespaceResolver: NamespaceResolver; 669 | functionResolver: FunctionResolver; 670 | contextNode: Node; 671 | virtualRoot: Node | null; 672 | expressionContextNode: Node; 673 | isHtml: boolean; 674 | contextSize: number; 675 | contextPosition: number; 676 | allowAnyNamespaceForNoPrefix: boolean; 677 | caseInsensitive: boolean; 678 | 679 | constructor(vr: VariableResolver, nr: NamespaceResolver, fr: FunctionResolver) { 680 | this.variableResolver = vr; 681 | this.namespaceResolver = nr; 682 | this.functionResolver = fr; 683 | this.virtualRoot = null; 684 | this.contextSize = 0; 685 | this.contextPosition = 0; 686 | } 687 | 688 | clone() { 689 | return Object.assign(new XPathContext(this.variableResolver, this.namespaceResolver, this.functionResolver), this); 690 | } 691 | 692 | extend(newProps: { [P in keyof XPathContext]?: XPathContext[P] }): XPathContext { 693 | return Object.assign( 694 | new XPathContext(this.variableResolver, this.namespaceResolver, this.functionResolver), 695 | this, 696 | newProps 697 | ); 698 | } 699 | } 700 | 701 | export type Operator = (l: Expression, r: Expression) => XBoolean; 702 | 703 | export class Operators { 704 | static equals(l: Expression, r: Expression) { 705 | return l.equals(r); 706 | } 707 | 708 | static notequal(l: Expression, r: Expression) { 709 | return l.notequal(r); 710 | } 711 | 712 | static lessthan(l: Expression, r: Expression) { 713 | return l.lessthan(r); 714 | } 715 | 716 | static greaterthan(l: Expression, r: Expression) { 717 | return l.greaterthan(r); 718 | } 719 | 720 | static lessthanorequal(l: Expression, r: Expression) { 721 | return l.lessthanorequal(r); 722 | } 723 | 724 | static greaterthanorequal(l: Expression, r: Expression) { 725 | return l.greaterthanorequal(r); 726 | } 727 | } 728 | -------------------------------------------------------------------------------- /src/xpath.ts: -------------------------------------------------------------------------------- 1 | import { Expression, XPathContext } from './xpath-types'; 2 | 3 | export class XPath { 4 | expression: Expression; 5 | 6 | constructor(e: Expression) { 7 | this.expression = e; 8 | } 9 | 10 | toString() { 11 | return this.expression.toString(); 12 | } 13 | 14 | evaluate(c: XPathContext) { 15 | c.contextNode = c.expressionContextNode; 16 | c.contextSize = 1; 17 | c.contextPosition = 1; 18 | 19 | // [2017-11-25] Removed usage of .implementation.hasFeature() since it does 20 | // not reliably detect HTML DOMs (always returns false in xmldom and true in browsers) 21 | if (c.isHtml) { 22 | if (c.caseInsensitive === undefined) { 23 | c.caseInsensitive = true; 24 | } 25 | 26 | if (c.allowAnyNamespaceForNoPrefix === undefined) { 27 | c.allowAnyNamespaceForNoPrefix = true; 28 | } 29 | } 30 | 31 | if (c.caseInsensitive === undefined) { 32 | c.caseInsensitive = false; 33 | } 34 | 35 | return this.expression.evaluate(c); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/jsdom.test.ts: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom'; 2 | import { executeTests } from './tests'; 3 | 4 | executeTests('jsdom', new JSDOM().window.DOMParser, true); 5 | -------------------------------------------------------------------------------- /test/xmldom.test.ts: -------------------------------------------------------------------------------- 1 | import { DOMParserImpl } from 'xmldom-ts'; 2 | import { executeTests } from './tests'; 3 | 4 | executeTests('xmldom', DOMParserImpl, false); 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "lib": ["esnext", "dom"] /* Specify library files to be included in the compilation: */, 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | "declaration": true /* Generates corresponding '.d.ts' file. */, 11 | "declarationDir": "./dist/types" /* Output directory for generated declaration files. */, 12 | "sourceMap": true /* Generates corresponding '.map' file. */, 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./dist/lib" /* Redirect output structure to the directory. */, 15 | "rootDirs": [ 16 | "./src", 17 | "./test" 18 | ] /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | "strictPropertyInitialization": false, 33 | /* Additional Checks */ 34 | "noUnusedLocals": true /* Report errors on unused locals. */, 35 | "noUnusedParameters": true /* Report errors on unused parameters. */, 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 41 | "esModuleInterop": true 42 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 43 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 44 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 45 | // "typeRoots": [], /* List of folders to include type definitions from. */ 46 | // "types": [], /* Type declaration files to be included in compilation. */ 47 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | }, 60 | "include": ["./src/**/*"], 61 | "exclude": [] 62 | } 63 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended", "tslint-eslint-rules", "tslint-no-circular-imports"], 4 | "jsRules": {}, 5 | "rules": { 6 | "quotemark": [true, "single"], 7 | "ter-indent": [true, 2, { "SwitchCase": 1 }], 8 | "trailing-comma": [true, { "multiline": "never", "singleline": "never" }], 9 | "interface-name": [true, "never-prefix"], 10 | "member-access": false, 11 | "max-classes-per-file": false, 12 | "object-literal-sort-keys": false, 13 | "whitespace": [ 14 | true, 15 | "check-branch", 16 | "check-decl", 17 | "check-operator", 18 | "check-module", 19 | "check-separator", 20 | "check-rest-spread", 21 | "check-type", 22 | "check-typecast", 23 | "check-type-operator", 24 | "check-preblock" 25 | ], 26 | "variable-name": [true, "check-format", "allow-leading-underscore"] 27 | }, 28 | "rulesDirectory": [] 29 | } 30 | --------------------------------------------------------------------------------