├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── js ├── dsl.js ├── expression.js ├── literal.js └── renderer.js ├── package.json ├── src ├── dsl.pogo ├── expression.pogo ├── literal.pogo └── renderer.pogo └── test ├── fixtures └── simple.html ├── mocha.opts └── xpath_spec.pogo /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | - 0.10 -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: compile test 2 | 3 | compile: 4 | ./node_modules/.bin/pogo -c ./src/*.pogo 5 | mv ./src/*.js ./js 6 | 7 | test: 8 | ./node_modules/.bin/mocha test/*spec.pogo 9 | 10 | .PHONY: test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xpath-builder 2 | 3 | A JavaScript DSL around a subset of XPath 1.0. Its primary purpose is to 4 | facilitate writing complex XPath queries from JavaScript code. 5 | 6 | [![Build Status](https://secure.travis-ci.org/featurist/xpath-builder.png?branch=master)](http://travis-ci.org/featurist/xpath-builder) 7 | 8 | xpath-builder is a port of the [xpath](https://github.com/jnicklas/xpath) Ruby gem. 9 | 10 | 11 | ## Generating expressions 12 | 13 | To create quick, one-off expressions, create an xpath builder: 14 | 15 | ```js 16 | x = require('xpath-builder').dsl(); 17 | 18 | x.descendant('ul').where(x.attr('id').equals('foo')) 19 | ``` 20 | 21 | However for more complex expressions, it is probably more convenient to include 22 | the builder object in your prototype chain: 23 | 24 | ```js 25 | function MyXPaths() {} 26 | 27 | MyXPaths.prototype = require('xpath-builder').dsl(); 28 | 29 | MyXPaths.prototype.fooList = function() { 30 | return this.descendant('ul').where(this.attr('id').equals('foo')); 31 | }; 32 | 33 | MyXPaths.prototype.passwordField = function(id) { 34 | return this.descendant('input') 35 | .where(this.attr('type').equals('password')) 36 | .where(this.attr('id').equals(id)); 37 | }; 38 | ``` 39 | 40 | Both ways return an 41 | [`Expression`](./src/expression.pogo) 42 | instance, which can be further modified. To convert the expression to a 43 | string, just call `.toString()` on it. All available expressions are defined in 44 | [`DSL`](./src/dsl.pogo). 45 | 46 | ## Strings and Literals 47 | 48 | When you send a string as an argument to any xpath-builder function, it may be interepreted as a string literal, or an xpath literal, depending on the function: 49 | 50 | ```js 51 | x.descendant('p').where(x.attr('id').equals('foo')) 52 | ``` 53 | 54 | Which generates: 55 | 56 | ``` 57 | .//p[./@id = 'foo'] 58 | ``` 59 | 60 | Occasionally you might want XPath literals instead of string literals, in which case wrap your string in a call to .literal(): 61 | 62 | ```js 63 | x.descendant('p').where(x.attr('id').equals(x.literal('foo'))) 64 | ``` 65 | 66 | Which generates: 67 | 68 | ``` 69 | .//p[./@id = foo] 70 | ``` 71 | 72 | This expression would match any p tag whose id attribute matches a 'foo' tag it contains. Most of the time, this is not what you want. 73 | 74 | 75 | ## License 76 | 77 | (The MIT License) 78 | 79 | Copyright © 2013 Josh Chisholm 80 | 81 | Copyright © 2010 Jonas Nicklas 82 | 83 | Permission is hereby granted, free of charge, to any person obtaining a copy of 84 | this software and associated documentation files (the ‘Software’), to deal in 85 | the Software without restriction, including without limitation the rights to 86 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 87 | of the Software, and to permit persons to whom the Software is furnished to do 88 | so, subject to the following conditions: 89 | 90 | The above copyright notice and this permission notice shall be included in all 91 | copies or substantial portions of the Software. 92 | 93 | THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 94 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 95 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 96 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 97 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 98 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 99 | SOFTWARE. -------------------------------------------------------------------------------- /js/dsl.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var self = this; 3 | var expression, Literal, topLevelMethods, expressionLevelMethods, literals, TopLevel, ExpressionLevel, method, Expression, Union; 4 | expression = require("./expression"); 5 | Literal = require("./literal").Literal; 6 | topLevelMethods = { 7 | current: function() { 8 | var self = this; 9 | return new Expression("thisNode"); 10 | }, 11 | name: function() { 12 | var self = this; 13 | return new Expression("nodeName", self.current()); 14 | }, 15 | descendant: function() { 16 | var self = this; 17 | var elementNames = Array.prototype.slice.call(arguments, 0, arguments.length); 18 | return new Expression("descendant", self.current(), literals(elementNames)); 19 | }, 20 | child: function() { 21 | var self = this; 22 | var elementNames = Array.prototype.slice.call(arguments, 0, arguments.length); 23 | return new Expression("child", self.current(), literals(elementNames)); 24 | }, 25 | axis: function(name, tagName) { 26 | var self = this; 27 | return new Expression("axis", self.current(), new Literal(name), new Literal(tagName || "*")); 28 | }, 29 | nextSibling: function() { 30 | var self = this; 31 | var names = Array.prototype.slice.call(arguments, 0, arguments.length); 32 | return new Expression("nextSibling", self.current(), literals(names)); 33 | }, 34 | previousSibling: function() { 35 | var self = this; 36 | var names = Array.prototype.slice.call(arguments, 0, arguments.length); 37 | return new Expression("previousSibling", self.current(), literals(names)); 38 | }, 39 | anywhere: function() { 40 | var self = this; 41 | var names = Array.prototype.slice.call(arguments, 0, arguments.length); 42 | return new Expression("anywhere", literals(names)); 43 | }, 44 | attr: function(name) { 45 | var self = this; 46 | return new Expression("attribute", self.current(), new Literal(name)); 47 | }, 48 | contains: function(expression) { 49 | var self = this; 50 | return new Expression("contains", self.current(), expression); 51 | }, 52 | startsWith: function(expression) { 53 | var self = this; 54 | return new Expression("startsWith", self.current(), expression); 55 | }, 56 | endsWith: function(expression) { 57 | var self = this; 58 | return new Expression("endsWith", self.current(), expression); 59 | }, 60 | text: function() { 61 | var self = this; 62 | return new Expression("text", self.current()); 63 | }, 64 | string: function() { 65 | var self = this; 66 | return new Expression("stringFunction", self.current()); 67 | }, 68 | substring: function(expressionA, expressionB) { 69 | var self = this; 70 | var expressions; 71 | expressions = [ expressionA ]; 72 | if (expressionB) { 73 | expressions.push(expressionB); 74 | } 75 | return new Expression("substringFunction", self.current(), expressions); 76 | }, 77 | stringLength: function() { 78 | var self = this; 79 | return new Expression("stringLengthFunction", self.current()); 80 | }, 81 | literal: function(string) { 82 | var self = this; 83 | return new Literal(string); 84 | }, 85 | concat: function() { 86 | var self = this; 87 | var expressions = Array.prototype.slice.call(arguments, 0, arguments.length); 88 | return new Expression("concatFunction", expressions); 89 | }, 90 | nthChild: function(n) { 91 | var self = this; 92 | return new Expression("nthChild", n); 93 | }, 94 | nthLastChild: function(n) { 95 | var self = this; 96 | return new Expression("nthLastChild", n); 97 | }, 98 | firstChild: function() { 99 | var self = this; 100 | return new Expression("firstChild"); 101 | }, 102 | lastChild: function() { 103 | var self = this; 104 | return new Expression("lastChild"); 105 | }, 106 | onlyChild: function() { 107 | var self = this; 108 | return new Expression("onlyChild"); 109 | }, 110 | onlyOfType: function() { 111 | var self = this; 112 | return new Expression("onlyOfType"); 113 | }, 114 | firstOfType: function() { 115 | var self = this; 116 | return new Expression("nthOfType", 1); 117 | }, 118 | lastOfType: function() { 119 | var self = this; 120 | return new Expression("lastOfType"); 121 | }, 122 | nthOfType: function(n) { 123 | var self = this; 124 | return new Expression("nthOfType", n); 125 | }, 126 | nthLastOfType: function(n) { 127 | var self = this; 128 | return new Expression("nthLastOfType", n); 129 | }, 130 | nthOfTypeMod: function(m, n) { 131 | var self = this; 132 | return new Expression("nthOfTypeMod", m, n || 0); 133 | }, 134 | nthOfTypeOdd: function() { 135 | var self = this; 136 | return new Expression("nthOfTypeOdd"); 137 | }, 138 | nthOfTypeEven: function() { 139 | var self = this; 140 | return new Expression("nthOfTypeEven"); 141 | }, 142 | nthLastOfTypeMod: function(m, n) { 143 | var self = this; 144 | return new Expression("nthLastOfTypeMod", m, n || 0); 145 | }, 146 | nthLastOfTypeOdd: function() { 147 | var self = this; 148 | return new Expression("nthLastOfTypeOdd"); 149 | }, 150 | nthLastOfTypeEven: function() { 151 | var self = this; 152 | return new Expression("nthLastOfTypeEven"); 153 | }, 154 | empty: function() { 155 | var self = this; 156 | return new Expression("empty"); 157 | } 158 | }; 159 | expressionLevelMethods = { 160 | where: function(expression) { 161 | var self = this; 162 | return new Expression("where", self.current(), expression); 163 | }, 164 | oneOf: function() { 165 | var self = this; 166 | var expressions = Array.prototype.slice.call(arguments, 0, arguments.length); 167 | return new Expression("oneOf", self.current(), expressions); 168 | }, 169 | equals: function(expression) { 170 | var self = this; 171 | return new Expression("equality", self.current(), expression); 172 | }, 173 | is: function(expression) { 174 | var self = this; 175 | return new Expression("is", self.current(), expression); 176 | }, 177 | or: function(expression) { 178 | var self = this; 179 | return new Expression("or", self.current(), expression); 180 | }, 181 | and: function(expression) { 182 | var self = this; 183 | return new Expression("and", self.current(), expression); 184 | }, 185 | union: function() { 186 | var self = this; 187 | var expressions = Array.prototype.slice.call(arguments, 0, arguments.length); 188 | return new Union([ self ].concat(expressions)); 189 | }, 190 | inverse: function() { 191 | var self = this; 192 | return new Expression("inverse", self.current()); 193 | }, 194 | stringLiteral: function() { 195 | var self = this; 196 | return new Expression("stringLiteral", self); 197 | }, 198 | normalize: function() { 199 | var self = this; 200 | return new Expression("normalizedSpace", self.current()); 201 | }, 202 | n: function() { 203 | var self = this; 204 | return self.normalize(); 205 | }, 206 | add: function(number) { 207 | var self = this; 208 | return new Expression("addition", self.current(), number); 209 | }, 210 | subtract: function(number) { 211 | var self = this; 212 | return new Expression("subtraction", self.current(), number); 213 | }, 214 | count: function() { 215 | var self = this; 216 | return new Expression("countFunction", self.current()); 217 | } 218 | }; 219 | literals = function(items) { 220 | return items.map(function(item) { 221 | return new Literal(item); 222 | }); 223 | }; 224 | TopLevel = function() { 225 | return this; 226 | }; 227 | TopLevel.prototype = topLevelMethods; 228 | ExpressionLevel = function() { 229 | return this; 230 | }; 231 | ExpressionLevel.prototype = new TopLevel(); 232 | for (method in expressionLevelMethods) { 233 | (function(method) { 234 | ExpressionLevel.prototype[method] = expressionLevelMethods[method]; 235 | })(method); 236 | } 237 | Expression = expression.createExpression(ExpressionLevel.prototype); 238 | Union = function(expressions) { 239 | this.expression = "union"; 240 | this.args = expressions; 241 | return this; 242 | }; 243 | Union.prototype = Expression.prototype; 244 | exports.dsl = function() { 245 | var self = this; 246 | return new TopLevel(); 247 | }; 248 | }).call(this); -------------------------------------------------------------------------------- /js/expression.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var self = this; 3 | var Renderer, createExpression; 4 | Renderer = require("./renderer").Renderer; 5 | createExpression = function(prototype) { 6 | var Expression; 7 | Expression = function(expression) { 8 | var args = Array.prototype.slice.call(arguments, 1, arguments.length); 9 | this.expression = expression; 10 | this.args = args; 11 | return this; 12 | }; 13 | Expression.prototype = prototype; 14 | Expression.prototype.isExpression = true; 15 | Expression.prototype.toXPath = function(type) { 16 | var self = this; 17 | return Renderer.render(self, type); 18 | }; 19 | Expression.prototype.toString = function() { 20 | var self = this; 21 | return self.toXPath(); 22 | }; 23 | Expression.prototype.inspect = function() { 24 | var self = this; 25 | return self.toXPath(); 26 | }; 27 | Expression.prototype.current = function() { 28 | var self = this; 29 | return self; 30 | }; 31 | return Expression; 32 | }; 33 | exports.createExpression = createExpression; 34 | }).call(this); -------------------------------------------------------------------------------- /js/literal.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var self = this; 3 | var Literal; 4 | Literal = function(value) { 5 | this.value = value; 6 | this.isLiteral = true; 7 | return this; 8 | }; 9 | Literal.prototype.isLiteral = true; 10 | exports.Literal = Literal; 11 | }).call(this); -------------------------------------------------------------------------------- /js/renderer.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var self = this; 3 | var Renderer; 4 | Renderer = function(type) { 5 | this.type = type; 6 | return this; 7 | }; 8 | Renderer.prototype = { 9 | render: function(node) { 10 | var self = this; 11 | var a; 12 | a = node.args.map(function(arg) { 13 | return self.convertArgument(arg); 14 | }); 15 | return self[node.expression].apply(self, a); 16 | }, 17 | convertArgument: function(argument) { 18 | var self = this; 19 | if (argument.isExpression || argument.isUnion) { 20 | return self.render(argument); 21 | } else if (argument instanceof Array) { 22 | return argument.map(function(e) { 23 | return self.convertArgument(e); 24 | }); 25 | } else if (typeof argument === "string") { 26 | return self.stringLiteral(argument); 27 | } else if (typeof argument === "number") { 28 | return argument; 29 | } else if (argument.isLiteral) { 30 | return argument.value; 31 | } else { 32 | return argument.toString(); 33 | } 34 | }, 35 | stringLiteral: function(string) { 36 | var self = this; 37 | if (string.indexOf("'") > -1) { 38 | string = string.split("'", -1).map(function(substr) { 39 | return "'" + substr + "'"; 40 | }).join(',"\'",'); 41 | return "concat(" + string + ")"; 42 | } else { 43 | return "'" + string + "'"; 44 | } 45 | }, 46 | thisNode: function() { 47 | var self = this; 48 | return "."; 49 | }, 50 | descendant: function(parent, elementNames) { 51 | var self = this; 52 | var names; 53 | if (elementNames.length === 1) { 54 | return parent + "//" + elementNames[0]; 55 | } else if (elementNames.length > 1) { 56 | names = elementNames.map(function(e) { 57 | return "self::" + e; 58 | }); 59 | return parent + "//*[" + names.join(" | ") + "]"; 60 | } else { 61 | return parent + "//*"; 62 | } 63 | }, 64 | child: function(parent, elementNames) { 65 | var self = this; 66 | var names; 67 | if (elementNames.length === 1) { 68 | return parent + "/" + elementNames[0]; 69 | } else if (elementNames.length > 1) { 70 | names = elementNames.map(function(e) { 71 | return "self::" + e; 72 | }); 73 | return parent + "/*[" + names.join(" | ") + "]"; 74 | } else { 75 | return parent + "/*"; 76 | } 77 | }, 78 | axis: function(parent, name, tagName) { 79 | var self = this; 80 | return parent + "/" + name + "::" + tagName; 81 | }, 82 | nodeName: function(current) { 83 | var self = this; 84 | return "name(" + current + ")"; 85 | }, 86 | where: function(on, condition) { 87 | var self = this; 88 | return on + "[" + condition + "]"; 89 | }, 90 | attribute: function(current, name) { 91 | var self = this; 92 | return current + "/@" + name; 93 | }, 94 | equality: function(one, two) { 95 | var self = this; 96 | return one + " = " + two; 97 | }, 98 | addition: function(one, two) { 99 | var self = this; 100 | return one + " + " + two; 101 | }, 102 | subtraction: function(one, two) { 103 | var self = this; 104 | return one + " - " + two; 105 | }, 106 | is: function(one, two) { 107 | var self = this; 108 | if (self.type === "exact") { 109 | return self.equality(one, two); 110 | } else { 111 | return self.contains(one, two); 112 | } 113 | }, 114 | variable: function(name) { 115 | var self = this; 116 | return "%{" + name + "}"; 117 | }, 118 | text: function(current) { 119 | var self = this; 120 | return current + "/text()"; 121 | }, 122 | normalizedSpace: function(current) { 123 | var self = this; 124 | return "normalize-space(" + current + ")"; 125 | }, 126 | literal: function(node) { 127 | var self = this; 128 | return node; 129 | }, 130 | union: function() { 131 | var self = this; 132 | var expressions = Array.prototype.slice.call(arguments, 0, arguments.length); 133 | return expressions.join(" | "); 134 | }, 135 | anywhere: function(elementNames) { 136 | var self = this; 137 | var names; 138 | if (elementNames.length === 1) { 139 | return "//" + elementNames[0]; 140 | } else if (elementNames.length > 1) { 141 | names = elementNames.map(function(e) { 142 | return "self::" + e; 143 | }).join(" | "); 144 | return "//*[" + names + "]"; 145 | } else { 146 | return "//*"; 147 | } 148 | }, 149 | contains: function(current, value) { 150 | var self = this; 151 | return "contains(" + current + ", " + value + ")"; 152 | }, 153 | startsWith: function(current, value) { 154 | var self = this; 155 | return "starts-with(" + current + ", " + value + ")"; 156 | }, 157 | endsWith: function(current, value) { 158 | var self = this; 159 | return "substring(" + current + ", string-length(" + current + ") - string-length(" + value + ") + 1, string-length(" + current + ")) = " + value; 160 | }, 161 | and: function(one, two) { 162 | var self = this; 163 | return "(" + one + " and " + two + ")"; 164 | }, 165 | or: function(one, two) { 166 | var self = this; 167 | return "(" + one + " or " + two + ")"; 168 | }, 169 | oneOf: function(current, values) { 170 | var self = this; 171 | return values.map(function(value) { 172 | return current + " = " + value; 173 | }).join(" or "); 174 | }, 175 | nextSibling: function(current, elementNames) { 176 | var self = this; 177 | var names; 178 | if (elementNames.length === 1) { 179 | return current + "/following-sibling::*[1]/self::" + elementNames[0]; 180 | } else if (elementNames.length > 1) { 181 | names = elementNames.map(function(e) { 182 | return "self::" + e; 183 | }); 184 | return current + "/following-sibling::*[1]/self::*[" + names.join(" | ") + "]"; 185 | } else { 186 | return current + "/following-sibling::*[1]/self::*"; 187 | } 188 | }, 189 | previousSibling: function(current, elementNames) { 190 | var self = this; 191 | var names; 192 | if (elementNames.length === 1) { 193 | return current + "/preceding-sibling::*[1]/self::" + elementNames[0]; 194 | } else if (elementNames.length > 1) { 195 | names = elementNames.map(function(e) { 196 | return "self::" + e; 197 | }); 198 | return current + "/preceding-sibling::*[1]/self::*[" + names.join(" | ") + "]"; 199 | } else { 200 | return current + "/preceding-sibling::*[1]/self::*"; 201 | } 202 | }, 203 | inverse: function(current) { 204 | var self = this; 205 | return "not(" + current + ")"; 206 | }, 207 | stringFunction: function(current) { 208 | var self = this; 209 | return "string(" + current + ")"; 210 | }, 211 | substringFunction: function(current, args) { 212 | var self = this; 213 | return "substring(" + current + ", " + args.join(", ") + ")"; 214 | }, 215 | concatFunction: function(args) { 216 | var self = this; 217 | return "concat(" + args.join(", ") + ")"; 218 | }, 219 | stringLengthFunction: function(current) { 220 | var self = this; 221 | return "string-length(" + current + ")"; 222 | }, 223 | countFunction: function(current) { 224 | var self = this; 225 | return "count(" + current + ")"; 226 | }, 227 | nthOfType: function(n) { 228 | var self = this; 229 | return "position() = " + n; 230 | }, 231 | nthOfTypeMod: function(m, n) { 232 | var self = this; 233 | if (m === -1) { 234 | return "(position() <= " + n + ") and (((position() - " + n + ") mod 1) = 0)"; 235 | } else if (n > 0) { 236 | return "(position() >= " + n + ") and (((position() - " + n + ") mod " + m + ") = 0)"; 237 | } else { 238 | return "(position() mod " + m + ") = 0"; 239 | } 240 | }, 241 | nthOfTypeOdd: function() { 242 | var self = this; 243 | return "(position() mod 2) = 1"; 244 | }, 245 | nthOfTypeEven: function() { 246 | var self = this; 247 | return "(position() mod 2) = 0"; 248 | }, 249 | nthLastOfType: function(n) { 250 | var self = this; 251 | return "position() = last() - " + (n - 1); 252 | }, 253 | nthLastOfTypeMod: function(m, n) { 254 | var self = this; 255 | if (m === -1) { 256 | return "((last() - position() + 1) <= " + n + ") and ((((last() - position() + 1) - " + n + ") mod 1) = 0)"; 257 | } else if (n > 0) { 258 | return "((last() - position() + 1) >= " + n + ") and ((((last() - position() + 1) - " + n + ") mod " + m + ") = 0)"; 259 | } else { 260 | return "((last() - position() + 1) mod " + m + ") = 0"; 261 | } 262 | }, 263 | nthLastOfTypeOdd: function() { 264 | var self = this; 265 | return "((last() - position() + 1) >= 1) and ((((last() - position() + 1) - 1) mod 2) = 0)"; 266 | }, 267 | nthLastOfTypeEven: function() { 268 | var self = this; 269 | return "((last() - position() + 1) mod 2) = 0"; 270 | }, 271 | lastOfType: function() { 272 | var self = this; 273 | return "position() = last()"; 274 | }, 275 | nthChild: function(n) { 276 | var self = this; 277 | return "count(preceding-sibling::*) = " + (n - 1); 278 | }, 279 | nthLastChild: function(n) { 280 | var self = this; 281 | return "count(following-sibling::*) = " + (n - 1); 282 | }, 283 | firstChild: function() { 284 | var self = this; 285 | return "count(preceding-sibling::*) = 0"; 286 | }, 287 | lastChild: function() { 288 | var self = this; 289 | return "count(following-sibling::*) = 0"; 290 | }, 291 | onlyChild: function() { 292 | var self = this; 293 | return "count(preceding-sibling::*) = 0 and count(following-sibling::*) = 0"; 294 | }, 295 | onlyOfType: function() { 296 | var self = this; 297 | return "last() = 1"; 298 | }, 299 | empty: function() { 300 | var self = this; 301 | return "not(node())"; 302 | } 303 | }; 304 | Renderer.render = function(node, type) { 305 | var self = this; 306 | if (typeof type === "undefined") { 307 | type = "*"; 308 | } 309 | return new Renderer(type).render(node); 310 | }; 311 | exports.Renderer = Renderer; 312 | }).call(this); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xpath-builder", 3 | "version": "0.0.7", 4 | "description": "DSL for building complex XPath expressions", 5 | "main": "js/dsl.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "make test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/featurist/xpath-builder.git" 15 | }, 16 | "dependencies": {}, 17 | "devDependencies": { 18 | "pogo": "~0.6.4", 19 | "mocha": "~1.16.2", 20 | "should": "~2.1.1", 21 | "cheerio": "~0.12.4", 22 | "libxmljs": "~0.8.1" 23 | }, 24 | "keywords": [ 25 | "xpath", 26 | "builder" 27 | ], 28 | "author": { 29 | "name": "Josh Chisholm", 30 | "email": "joshchisholm@gmail.com", 31 | "url": "http://featurist.co.uk/" 32 | }, 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /src/dsl.pogo: -------------------------------------------------------------------------------- 1 | expression = require './expression' 2 | Literal = require './literal'.Literal 3 | 4 | top level methods = { 5 | 6 | current () = 7 | @new Expression 'thisNode' 8 | 9 | name () = 10 | @new Expression ('nodeName', self.current()) 11 | 12 | descendant (element names, ...) = 13 | @new Expression ('descendant', self.current(), literals(element names)) 14 | 15 | child (element names, ...) = 16 | @new Expression ('child', self.current(), literals(element names)) 17 | 18 | axis (name, tag name) = 19 | @new Expression ('axis', self.current(), @new Literal(name), @new Literal(tag name || '*')) 20 | 21 | next sibling (names, ...) = 22 | @new Expression ('nextSibling', self.current(), literals(names)) 23 | 24 | previous sibling (names, ...) = 25 | @new Expression ('previousSibling', self.current(), literals(names)) 26 | 27 | anywhere (names, ...) = 28 | @new Expression ('anywhere', literals(names)) 29 | 30 | attr (name) = 31 | @new Expression ('attribute', self.current(), @new Literal(name)) 32 | 33 | contains (expression) = 34 | @new Expression ('contains', self.current(), expression) 35 | 36 | starts with (expression) = 37 | @new Expression ('startsWith', self.current(), expression) 38 | 39 | ends with (expression) = 40 | @new Expression ('endsWith', self.current(), expression) 41 | 42 | text () = 43 | @new Expression ('text', self.current()) 44 | 45 | string () = 46 | @new Expression ('stringFunction', self.current()) 47 | 48 | substring (expression a, expression b) = 49 | expressions = [expression a] 50 | if (expression b) @{ expressions.push (expression b) } 51 | @new Expression ('substringFunction', self.current(), expressions) 52 | 53 | string length () = 54 | @new Expression ('stringLengthFunction', self.current()) 55 | 56 | literal (string) = 57 | @new Literal(string) 58 | 59 | concat (expressions, ...) = 60 | @new Expression ('concatFunction', expressions) 61 | 62 | nth child (n) = 63 | @new Expression ('nthChild', n) 64 | 65 | nth last child (n) = 66 | @new Expression ('nthLastChild', n) 67 | 68 | first child () = 69 | @new Expression ('firstChild') 70 | 71 | last child () = 72 | @new Expression ('lastChild') 73 | 74 | only child () = 75 | @new Expression ('onlyChild') 76 | 77 | only of type () = 78 | @new Expression ('onlyOfType') 79 | 80 | first of type () = 81 | @new Expression ('nthOfType', 1) 82 | 83 | last of type () = 84 | @new Expression ('lastOfType') 85 | 86 | nth of type (n) = 87 | @new Expression ('nthOfType', n) 88 | 89 | nth last of type (n) = 90 | @new Expression ('nthLastOfType', n) 91 | 92 | nth of type mod (m, n) = 93 | @new Expression ('nthOfTypeMod', m, n || 0) 94 | 95 | nth of type odd () = 96 | @new Expression ('nthOfTypeOdd') 97 | 98 | nth of type even () = 99 | @new Expression ('nthOfTypeEven') 100 | 101 | nth last of type mod (m, n) = 102 | @new Expression ('nthLastOfTypeMod', m, n || 0) 103 | 104 | nth last of type odd () = 105 | @new Expression ('nthLastOfTypeOdd') 106 | 107 | nth last of type even () = 108 | @new Expression ('nthLastOfTypeEven') 109 | 110 | empty () = 111 | @new Expression ('empty') 112 | 113 | } 114 | 115 | expression level methods = { 116 | 117 | where (expression) = 118 | @new Expression('where', self.current(), expression) 119 | 120 | one of (expressions, ...) = 121 | @new Expression('oneOf', self.current(), expressions) 122 | 123 | equals (expression) = 124 | @new Expression('equality', self.current(), expression) 125 | 126 | is (expression) = 127 | @new Expression('is', self.current(), expression) 128 | 129 | or (expression) = 130 | @new Expression('or', self.current(), expression) 131 | 132 | and (expression) = 133 | @new Expression('and', self.current(), expression) 134 | 135 | union (expressions, ...) = 136 | @new Union([self].concat(expressions)) 137 | 138 | inverse () = 139 | @new Expression('inverse', self.current()) 140 | 141 | string literal () = 142 | @new Expression('stringLiteral', self) 143 | 144 | normalize () = 145 | @new Expression('normalizedSpace', self.current()) 146 | 147 | n () = 148 | self.normalize() 149 | 150 | add (number) = 151 | @new Expression('addition', self.current(), number) 152 | 153 | subtract (number) = 154 | @new Expression('subtraction', self.current(), number) 155 | 156 | count () = 157 | @new Expression ('countFunction', self.current()) 158 | 159 | } 160 | 161 | literals (items) = items.map @(item) @{ @new Literal(item) } 162 | 163 | Top Level () = this 164 | Top Level.prototype = top level methods 165 | 166 | Expression Level () = this 167 | Expression Level.prototype = @new Top Level() 168 | 169 | for @(method) in (expression level methods) 170 | Expression Level.prototype.(method) = expression level methods.(method) 171 | 172 | Expression = expression.create expression (Expression Level.prototype) 173 | 174 | Union (expressions) = 175 | this.expression = 'union' 176 | this.args = expressions 177 | this 178 | 179 | Union.prototype = Expression.prototype 180 | 181 | exports.dsl () = @new Top Level() 182 | -------------------------------------------------------------------------------- /src/expression.pogo: -------------------------------------------------------------------------------- 1 | Renderer = require './renderer'.Renderer 2 | 3 | create expression (prototype) = 4 | 5 | Expression (expression, args, ...) = 6 | this.expression = expression 7 | this.args = args 8 | this 9 | 10 | Expression.prototype = prototype 11 | Expression.prototype.is expression = true 12 | Expression.prototype.to XPath (type) = Renderer.render(self, type) 13 | Expression.prototype.to string() = self.to XPath() 14 | Expression.prototype.inspect() = self.to XPath() 15 | Expression.prototype.current() = self 16 | 17 | Expression 18 | 19 | exports.create expression = create expression 20 | -------------------------------------------------------------------------------- /src/literal.pogo: -------------------------------------------------------------------------------- 1 | Literal (value) = 2 | this.value = value 3 | this.is literal = true 4 | this 5 | 6 | Literal.prototype.is literal = true 7 | 8 | exports.Literal = Literal -------------------------------------------------------------------------------- /src/renderer.pogo: -------------------------------------------------------------------------------- 1 | Renderer (type) = 2 | this.type = type 3 | this 4 | 5 | Renderer.prototype = { 6 | 7 | render (node) = 8 | a = node.args.map @(arg) @{ self.convert argument (arg) } 9 | self.(node.expression).apply (self, a) 10 | 11 | convert argument (argument) = 12 | if ((argument.is expression) || (argument.is union)) 13 | self.render (argument) 14 | else if (argument :: Array) 15 | argument.map @(e) @{ self.convert argument(e) } 16 | else if (argument :: String) 17 | self.string literal (argument) 18 | else if (argument :: Number) 19 | argument 20 | else if (argument.is literal) 21 | argument.value 22 | else 23 | argument.to string() 24 | 25 | string literal (string) = 26 | if (string.index of ("'") > -1) 27 | string := string.split("'", -1).map( 28 | @(substr) @{ "'#(substr)'" } 29 | ).join(',"''",') 30 | "concat(#(string))" 31 | else 32 | "'#(string)'" 33 | 34 | this node () = 35 | '.' 36 | 37 | descendant (parent, element names) = 38 | if (element names.length == 1) 39 | "#(parent)//#(element names.0)" 40 | else if (element names.length > 1) 41 | names = element names.map @(e) @{ "self::#(e)" } 42 | "#(parent)//*[#(names.join(' | '))]" 43 | else 44 | "#(parent)//*" 45 | 46 | child (parent, element names) = 47 | if (element names.length == 1) 48 | "#(parent)/#(element names.0)" 49 | else if (element names.length > 1) 50 | names = element names.map @(e) @{ "self::#(e)" } 51 | "#(parent)/*[#(names.join(' | '))]" 52 | else 53 | "#(parent)/*" 54 | 55 | axis (parent, name, tag name) = 56 | "#(parent)/#(name)::#(tag name)" 57 | 58 | node name (current) = 59 | "name(#(current))" 60 | 61 | where (on, condition) = 62 | "#(on)[#(condition)]" 63 | 64 | attribute (current, name) = 65 | "#(current)/@#(name)" 66 | 67 | equality (one, two) = 68 | "#(one) = #(two)" 69 | 70 | addition (one, two) = 71 | "#(one) + #(two)" 72 | 73 | subtraction (one, two) = 74 | "#(one) - #(two)" 75 | 76 | is (one, two) = 77 | if (self.type == 'exact') 78 | self.equality (one, two) 79 | else 80 | self.contains (one, two) 81 | 82 | variable (name) = 83 | "%{#(name)}" 84 | 85 | text (current) = 86 | "#(current)/text()" 87 | 88 | normalized space (current) = 89 | "normalize-space(#(current))" 90 | 91 | literal (node) = 92 | node 93 | 94 | union (expressions, ...) = 95 | expressions.join(' | ') 96 | 97 | anywhere (element names) = 98 | if (element names.length == 1) 99 | "//#(element names.0)" 100 | else if (element names.length > 1) 101 | names = element names.map(@(e) @{ "self::#(e)" }).join " | " 102 | "//*[#(names)]" 103 | else 104 | "//*" 105 | 106 | contains (current, value) = 107 | "contains(#(current), #(value))" 108 | 109 | starts with (current, value) = 110 | "starts-with(#(current), #(value))" 111 | 112 | ends with (current, value) = 113 | "substring(#(current), string-length(#(current)) - string-length(#(value)) + 1, string-length(#(current))) = #(value)" 114 | 115 | and (one, two) = 116 | "(#(one) and #(two))" 117 | 118 | or (one, two) = 119 | "(#(one) or #(two))" 120 | 121 | one of (current, values) = 122 | values.map(@(value) @{ "#(current) = #(value)" }).join(' or ') 123 | 124 | next sibling (current, element names) = 125 | if (element names.length == 1) 126 | "#(current)/following-sibling::*[1]/self::#(element names.0)" 127 | else if (element names.length > 1) 128 | names = element names.map @(e) @{ "self::#(e)" } 129 | "#(current)/following-sibling::*[1]/self::*[#(names.join(' | '))]" 130 | else 131 | "#(current)/following-sibling::*[1]/self::*" 132 | 133 | previous sibling (current, element names) = 134 | if (element names.length == 1) 135 | "#(current)/preceding-sibling::*[1]/self::#(element names.0)" 136 | else if (element names.length > 1) 137 | names = element names.map @(e) @{ "self::#(e)" } 138 | "#(current)/preceding-sibling::*[1]/self::*[#(names.join(" | "))]" 139 | else 140 | "#(current)/preceding-sibling::*[1]/self::*" 141 | 142 | inverse (current) = 143 | "not(#(current))" 144 | 145 | string function (current) = 146 | "string(#(current))" 147 | 148 | substring function (current, args) = 149 | "substring(#(current), #(args.join(', ')))" 150 | 151 | concat function (args) = 152 | "concat(#(args.join(', ')))" 153 | 154 | string length function (current) = 155 | "string-length(#(current))" 156 | 157 | count function (current) = 158 | "count(#(current))" 159 | 160 | nth of type (n) = 161 | "position() = #(n)" 162 | 163 | nth of type mod (m, n) = 164 | if (m == -1) 165 | "(position() <= #(n)) and (((position() - #(n)) mod 1) = 0)" 166 | else if (n > 0) 167 | "(position() >= #(n)) and (((position() - #(n)) mod #(m)) = 0)" 168 | else 169 | "(position() mod #(m)) = 0" 170 | 171 | nth of type odd () = 172 | "(position() mod 2) = 1" 173 | 174 | nth of type even () = 175 | "(position() mod 2) = 0" 176 | 177 | nth last of type (n) = 178 | "position() = last() - #(n - 1)" 179 | 180 | nth last of type mod (m, n) = 181 | if (m == -1) 182 | "((last() - position() + 1) <= #(n)) and ((((last() - position() + 1) - #(n)) mod 1) = 0)" 183 | else if (n > 0) 184 | "((last() - position() + 1) >= #(n)) and ((((last() - position() + 1) - #(n)) mod #(m)) = 0)" 185 | else 186 | "((last() - position() + 1) mod #(m)) = 0" 187 | 188 | nth last of type odd () = 189 | "((last() - position() + 1) >= 1) and ((((last() - position() + 1) - 1) mod 2) = 0)" 190 | 191 | nth last of type even () = 192 | "((last() - position() + 1) mod 2) = 0" 193 | 194 | last of type () = 195 | "position() = last()" 196 | 197 | nth child (n) = 198 | "count(preceding-sibling::*) = #(n - 1)" 199 | 200 | nth last child (n) = 201 | "count(following-sibling::*) = #(n - 1)" 202 | 203 | first child () = 204 | "count(preceding-sibling::*) = 0" 205 | 206 | last child () = 207 | "count(following-sibling::*) = 0" 208 | 209 | only child () = 210 | "count(preceding-sibling::*) = 0 and count(following-sibling::*) = 0" 211 | 212 | only of type () = 213 | "last() = 1" 214 | 215 | empty () = 216 | "not(node())" 217 | 218 | } 219 | 220 | Renderer.render (node, type) = 221 | if (typeof (type) == 'undefined') @{ type := '*' } 222 | @new Renderer (type).render (node) 223 | 224 | exports.Renderer = Renderer -------------------------------------------------------------------------------- /test/fixtures/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 |

Blah

12 |

Bax

13 |

Bax

14 | 15 |
16 | 17 |
18 | 19 |

Bax

20 |

Bax

21 |

Blah

22 |
23 | 24 |
25 | 26 |
27 |

allamas

28 |

llama

29 |
30 | 31 |

32 | A lot 33 | 34 | of 35 | whitespace 36 |

37 | 38 |
39 |

chimp

40 |
elephant
41 |

flamingo

42 |
43 | 44 | Hello there 45 | 46 | Hello there 47 | 48 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | -r ./node_modules/pogo -r ./node_modules/should -------------------------------------------------------------------------------- /test/xpath_spec.pogo: -------------------------------------------------------------------------------- 1 | libxmljs = require "libxmljs" 2 | dsl = require '../js/dsl'.dsl () 3 | cheerio = require 'cheerio' 4 | 5 | Thingy() = this 6 | Thingy.prototype = dsl 7 | Thingy.prototype.fooDiv () = 8 | this.descendant('div').where(this.attr('id').equals 'foo') 9 | 10 | describe 'dsl' 11 | 12 | template = require('fs').read file sync "#(__dirname)/fixtures/simple.html" 13 | doc = libxmljs.parseXml(template.to string()) 14 | 15 | select (expression, type) = 16 | x = expression.to XPath(type) 17 | selected = doc.find(expression.to XPath(type)) 18 | if (selected :: Array) 19 | (selected.map @(el) 20 | $ = cheerio(el.to string()) 21 | $.name = el.name() 22 | $ 23 | ) || [] 24 | else 25 | selected 26 | 27 | it "works as a mixin" 28 | xpath = (@new Thingy).fooDiv() 29 | select(xpath).0.attr('title').should.equal 'fooDiv' 30 | 31 | describe '.descendant()' 32 | 33 | it "finds nodes that are nested below the current node" 34 | results = select(dsl.descendant 'p') 35 | results.0.text().should.equal 'Blah' 36 | results.1.text().should.equal 'Bax' 37 | 38 | it "does not find nodes outside the context" 39 | foo div = dsl.descendant('div').where(dsl.attr('id').equals('foo')) 40 | results = select (dsl.descendant('p').where(dsl.attr('id').equals(foo div.attr('title')))) 41 | results.length.should.equal 0 42 | 43 | it "finds multiple kinds of nodes" 44 | results = select (dsl.descendant('p', 'ul')) 45 | results.0.text().should.equal 'Blah' 46 | results.3.text().should.equal 'A list' 47 | 48 | it "finds all nodes when no arguments given" 49 | results = select (dsl.descendant().where(dsl.attr('id').equals 'foo').descendant()) 50 | results.0.text().should.equal 'Blah' 51 | results.4.text().should.equal 'A list' 52 | 53 | describe '.child()' 54 | 55 | it "finds nodes that are nested directly below the current node" 56 | results = select(dsl.descendant('div').child('p')) 57 | results.0.text().should.equal 'Blah' 58 | results.1.text().should.equal 'Bax' 59 | 60 | it "does not find nodes that are nested further down below the current node" 61 | results = select(dsl.child('p')) 62 | results.length.should.equal 0 63 | 64 | it "finds multiple kinds of nodes" 65 | results = select(dsl.descendant('div').child('p', 'ul')) 66 | results.0.text().should.equal 'Blah' 67 | results.3.text().should.equal 'A list' 68 | 69 | it "finds all nodes when no arguments given" 70 | results = select(dsl.descendant().where(dsl.attr('id').equals('foo')).child()) 71 | results.0.text().should.equal 'Blah' 72 | results.3.text().should.equal 'A list' 73 | 74 | describe '.axis()' 75 | 76 | it "finds nodes given the xpath axis" 77 | results = select(dsl.axis('descendant', 'p')) 78 | results.0.text().should == "Blah" 79 | 80 | it "finds nodes given the xpath axis without a specific tag" 81 | results = select(dsl.descendant('div').where(dsl.attr('id').equals 'foo').axis('descendant')) 82 | results.0.attr('id').should.equal "fooDiv" 83 | 84 | describe '.nextSibling()' 85 | 86 | it "finds nodes which are immediate siblings of the current node" 87 | select(dsl.descendant('p').where(dsl.attr('id').equals 'fooDiv').next sibling('p')).0.text().should.equal 'Bax' 88 | select(dsl.descendant('p').where(dsl.attr('id').equals 'fooDiv').next sibling('ul', 'p')).0.text().should.equal 'Bax' 89 | select(dsl.descendant('p').where(dsl.attr('title').equals 'monkey').next sibling('ul', 'p')).0.text().should.equal 'A list' 90 | select(dsl.descendant('p').where(dsl.attr('id').equals 'fooDiv').next sibling('ul', 'li')).length.should.equal 0 91 | select(dsl.descendant('p').where(dsl.attr('id').equals 'fooDiv').next sibling()).0.text().should.equal 'Bax' 92 | 93 | describe '.previousSibling()' 94 | 95 | it "finds nodes which are exactly preceding the current node" 96 | select(dsl.descendant('p').where(dsl.attr('id').equals 'wooDiv').previous sibling('p')).0.text().should.equal 'Bax' 97 | select(dsl.descendant('p').where(dsl.attr('id').equals 'wooDiv').previous sibling('ul', 'p')).0.text().should.equal 'Bax' 98 | select(dsl.descendant('p').where(dsl.attr('title').equals 'gorilla').previous sibling('ul', 'p')).0.text().should.equal 'A list' 99 | select(dsl.descendant('p').where(dsl.attr('id').equals 'wooDiv').previous sibling('ul', 'li')).length.should.equal 0 100 | select(dsl.descendant('p').where(dsl.attr('id').equals 'wooDiv').previous sibling()).0.text().should.equal 'Bax' 101 | 102 | describe '.anywhere()' 103 | 104 | it "finds nodes regardless of the context" 105 | foo div = dsl.anywhere('div').where(dsl.attr('id').equals 'foo') 106 | results = select(dsl.descendant('p').where(dsl.attr('id').equals (foo div.attr 'title'))) 107 | results.0.text().should.equal "Blah" 108 | 109 | it "finds multiple kinds of nodes regardless of the context" 110 | context = dsl.descendant('div').where(dsl.attr('id').equals 'woo') 111 | results = select(context.anywhere('p', 'ul')) 112 | results.0.text().should.equal 'Blah' 113 | results.3.text().should.equal 'A list' 114 | results.4.text().should.equal 'A list' 115 | results.6.text().should.equal 'Bax' 116 | 117 | it "finds all nodes when no arguments given regardless of the context" 118 | results = select(dsl.descendant('div').where(dsl.attr('id').equals 'woo').anywhere()) 119 | results.0.name.should.equal 'html' 120 | results.1.name.should.equal 'head' 121 | results.2.name.should.equal 'body' 122 | results.6.text().should.equal 'Blah' 123 | results.10.text().should.equal 'A list' 124 | results.13.text().should.equal 'A list' 125 | results.15.text().should.equal 'Bax' 126 | 127 | describe '.contains()' 128 | 129 | it "finds nodes that contain the given string" 130 | results = select(dsl.descendant('div').where(dsl.attr('title').contains('ooD'))) 131 | results.0.attr('id').should.equal "foo" 132 | 133 | it "finds nodes that contain the given expression" 134 | expression = dsl.anywhere('div').where(dsl.attr('title').equals 'fooDiv').attr('id') 135 | results = select(dsl.descendant('div').where(dsl.attr('title').contains(expression))) 136 | results.0.attr('id').should.equal "foo" 137 | 138 | describe '.concat()' 139 | 140 | it "concatenates expressions" 141 | foo = dsl.anywhere('div').where(dsl.attr('title').equals 'fooDiv').attr('id') 142 | results = select(dsl.descendant('div').where(dsl.attr('title').equals(dsl.concat(foo, 'Div')))) 143 | results.0.attr('id').should.equal "foo" 144 | 145 | describe '.count()' 146 | 147 | it "counts elements" 148 | results = select(dsl.descendant('div').where(dsl.child().count().equals(2))) 149 | results.0.attr('id').should.equal "preference" 150 | 151 | describe '.nthChild()' 152 | 153 | it "finds the nth child element" 154 | results = select(dsl.descendant('div').where(dsl.nth child(3))) 155 | results.0.attr('id').should.equal "foo" 156 | 157 | describe '.nthLastChild()' 158 | 159 | it "finds the nth last child element" 160 | results = select(dsl.descendant('div').where(dsl.nth last child(3))) 161 | results.0.attr('id').should.equal "moar" 162 | 163 | describe '.firstChild()' 164 | 165 | it "finds the first child element" 166 | results = select(dsl.descendant('div').where(dsl.first child())) 167 | results.0.attr('id').should.equal "bar" 168 | 169 | describe '.onlyChild()' 170 | 171 | it "finds the only child element" 172 | results = select(dsl.descendant().where(dsl.only child())) 173 | results.0.name.should.equal 'li' 174 | 175 | describe '.onlyOfType()' 176 | 177 | it "finds the only element of the given type" 178 | woo = dsl.descendant().where(dsl.attr('id').equals('woo')) 179 | results = select(woo.descendant('li').where(dsl.only of type())) 180 | results.0.name.should.equal 'li' 181 | 182 | describe '.startsWith()' 183 | 184 | it "finds nodes that begin with the given string" 185 | results = select(dsl.descendant('*').where(dsl.attr('id').starts with('foo'))) 186 | results.length.should.equal 2 187 | results.0.attr('id').should.equal "foo" 188 | results.1.attr('id').should.equal "fooDiv" 189 | 190 | it "finds nodes that start with the given expression" 191 | expression = dsl.anywhere('div').where(dsl.attr('title').equals 'fooDiv').attr('id') 192 | results = select(dsl.descendant('div').where(dsl.attr('title').starts with(expression))) 193 | results.0.attr('id').should.equal "foo" 194 | 195 | describe '.endsWith()' 196 | 197 | it "finds nodes that end with the given string" 198 | results = select(dsl.descendant('*').where(dsl.attr('title').ends with('ooDiv'))) 199 | results.length.should.equal 2 200 | results.0.attr('id').should.equal "foo" 201 | results.1.attr('id').should.equal "woo" 202 | 203 | it "finds nodes that end with the given expression" 204 | expression = dsl.concat('ooD', 'iv') 205 | results = select(dsl.descendant('*').where(dsl.attr('title').ends with(expression))) 206 | results.length.should.equal 2 207 | results.0.attr('id').should.equal "foo" 208 | results.1.attr('id').should.equal "woo" 209 | 210 | describe '.text()' 211 | 212 | it "finds a node's text" 213 | results = select(dsl.descendant('p').where(dsl.text().equals 'Bax')) 214 | results.0.text().should.equal 'Bax' 215 | results.1.attr('title').should.equal 'monkey' 216 | results := select(dsl.descendant('div').where(dsl.descendant('p').text().equals 'Bax')) 217 | results.0.attr('title').should.equal 'fooDiv' 218 | 219 | describe '.substring()' 220 | 221 | describe "when called with one argument" 222 | 223 | it "finds the part of a string after the specified character" 224 | results = select(dsl.descendant('span').where(dsl.attr('id').equals "substring").text().substring(7)) 225 | results.should.equal "there" 226 | 227 | describe "when called with two arguments" 228 | 229 | it "finds the part of a string after the specified character, up to the given length" 230 | results = select(dsl.descendant('span').where(dsl.attr('id').equals "substring").text().substring(2, 4)) 231 | results.should.equal "ello" 232 | 233 | describe '.stringLength()' 234 | 235 | it "returns the length of a string" 236 | results = select(dsl.descendant('span').where(dsl.attr('id').equals "string-length").text().string length()) 237 | results.should.equal 11 238 | 239 | describe '.where()' 240 | 241 | it "limits the expression to find only certain nodes" 242 | select(dsl.descendant('div').where(dsl.attr('id').equals 'foo')).0.attr('title').should.equal "fooDiv" 243 | 244 | describe '.inverse()' 245 | 246 | it "inverts the expression" 247 | select(dsl.descendant('p').where(dsl.attr('id').equals('fooDiv').inverse())).0.text().should.equal 'Bax' 248 | 249 | describe '.equals()' 250 | 251 | it "limits the expression to find only certain nodes" 252 | select(dsl.descendant('div').where(dsl.attr('id').equals('foo'))).0.attr('title').should.equal "fooDiv" 253 | 254 | describe '.add()' 255 | 256 | it "adds numbers together" 257 | select(dsl.descendant().where(dsl.string length(dsl.attr('title')).add(1).equals(7))).0.attr('title').should.equal "barDiv" 258 | 259 | describe '.subtract()' 260 | 261 | it "subtracts a number" 262 | select(dsl.descendant().where(dsl.string length(dsl.attr('title')).subtract(1).equals(5))).0.attr('title').should.equal "barDiv" 263 | 264 | describe '.is()' 265 | 266 | it "uses equality when 'exact' is given" 267 | expression = dsl.descendant('div').where(dsl.attr('id').is('foo')) 268 | select(expression, 'exact').0.attr('title').should.equal "fooDiv" 269 | expression := dsl.descendant('div').where(dsl.attr('id').is('oo')) 270 | select(expression, 'exact').length.should.equal 0 271 | 272 | it "uses substring matching when 'exact' is not given" 273 | expression = dsl.descendant('div').where(dsl.attr('id').is('foo')) 274 | select(expression).0.attr('title').should.equal "fooDiv" 275 | expression := dsl.descendant('div').where(dsl.attr('id').is('oo')) 276 | select(expression).0.attr('title').should.equal "fooDiv" 277 | 278 | describe '.oneOf()' 279 | 280 | it "returns all nodes where the condition matches" 281 | p = dsl.anywhere('div').where(dsl.attr('id').equals 'foo').attr('title') 282 | results = select(dsl.descendant('*').where(dsl.attr('id').one of('foo', p, 'baz'))) 283 | results.0.attr('title').should.equal "fooDiv" 284 | results.1.text().should.equal "Blah" 285 | results.2.attr('title').should.equal "bazDiv" 286 | 287 | describe '.and()' 288 | 289 | it "finds all nodes in both expressions" 290 | results = select(dsl.descendant('*').where(dsl.contains('Bax').and(dsl.attr('title').equals('monkey')))) 291 | results.0.attr('title').should.equal "monkey" 292 | 293 | describe '.or()' 294 | 295 | it "finds all nodes in either expression" 296 | results = select(dsl.descendant('*').where(dsl.attr('id').equals('foo').or(dsl.attr('id').equals('fooDiv')))) 297 | results.0.attr('title').should.equal "fooDiv" 298 | results.1.text().should.equal "Blah" 299 | 300 | describe '.attr()' 301 | 302 | it "returns an attribute value" 303 | results = select(dsl.descendant('div').where(dsl.attr('id'))) 304 | results.0.attr('title').should.equal "barDiv" 305 | results.1.attr('title').should.equal "fooDiv" 306 | 307 | describe '.name()' 308 | 309 | it "matches the node's name" 310 | results = select(dsl.descendant('*').where(dsl.name().equals 'ul')) 311 | results.0.text().should.equal "A list" 312 | 313 | describe '.union()' 314 | 315 | it "creates a union expression" 316 | expr1 = dsl.descendant('p') 317 | expr2 = dsl.descendant('div') 318 | collection = expr1.union(expr2) 319 | other1 = collection.where(dsl.attr('id').equals 'foo') 320 | other2 = collection.where(dsl.attr('id').equals 'fooDiv') 321 | select(other1).0.attr('title').should.equal 'fooDiv' 322 | select(other2).0.attr('id').should.equal 'fooDiv' 323 | 324 | describe '.literal()' 325 | 326 | it "embeds the string argument in the XPath without escaping anything" 327 | dsl.descendant().where(dsl.attr('x').equals(dsl.literal('foo'))).to XPath().should.equal('.//*[./@x = foo]') 328 | 329 | describe '.firstOfType()' 330 | 331 | it "finds the first element of the given type" 332 | first p = select(dsl.descendant('p').where(dsl.firstOfType())) 333 | first p.0.attr('id').should.equal 'fooDiv' 334 | 335 | describe '.lastOfType()' 336 | 337 | it "finds the last element of the given type" 338 | first p = select(dsl.descendant('p').where(dsl.lastOfType())) 339 | first p.0.attr('id').should.equal 'amingoflay' 340 | 341 | describe '.nthOfType()' 342 | 343 | it "finds the nth element of the given type" 344 | nth = select(dsl.descendant('div').where(dsl.nthOfType(4))) 345 | nth.length.should.equal 1 346 | nth.0.attr('id').should.equal 'woo' 347 | 348 | describe '.nthLastOfType()' 349 | 350 | it "finds the nth last element of the given type" 351 | second last p = select(dsl.descendant('p').where(dsl.nthLastOfType(2))) 352 | second last p.0.attr('id').should.equal 'impchay' 353 | 354 | describe '.nthOfTypeMod()' 355 | 356 | it "finds elements where (position - 0) mod 1 is 0" 357 | nth = select(dsl.descendant('div').where(dsl.nthOfTypeMod(1, 0))) 358 | nth.0.attr('title').should.equal 'barDiv' 359 | nth.length.should.equal 8 360 | 361 | it "finds elements where position >= 3 and (position - 3) mod 1 is 0" 362 | nth = select(dsl.descendant('body').child('div').where(dsl.nthOfTypeMod(1, 3))) 363 | nth.0.attr('title').should.equal 'fooDiv' 364 | nth.length.should.equal 5 365 | 366 | it "finds elements where (position - 2) mod 1 is 0" 367 | nth = select(dsl.descendant('div').where(dsl.nthOfTypeMod(1, 2))) 368 | nth.0.attr('title').should.equal 'noId' 369 | nth.length.should.equal 7 370 | 371 | it "finds elements where position mod 2 is 0" 372 | nth = select(dsl.descendant('div').where(dsl.nthOfTypeMod(2))) 373 | nth.0.attr('title').should.equal 'noId' 374 | nth.length.should.equal 4 375 | 376 | it "finds elements where position <= 3 and (position - 3 mod 2) is 0" 377 | nth = select(dsl.descendant('div').where(dsl.nthOfTypeMod(-1, 3))) 378 | nth.0.attr('title').should.equal 'barDiv' 379 | nth.1.attr('title').should.equal 'noId' 380 | nth.2.attr('title').should.equal 'fooDiv' 381 | nth.length.should.equal 3 382 | 383 | describe '.nthOfTypeOdd()' 384 | 385 | it "finds elements where position is an odd number" 386 | nth = select(dsl.descendant('div').where(dsl.nthOfTypeOdd())) 387 | nth.0.attr('title').should.equal 'barDiv' 388 | nth.length.should.equal 4 389 | 390 | describe '.nthOfTypeEven()' 391 | 392 | it "finds elements where position is an even number" 393 | nth = select(dsl.descendant('div').where(dsl.nthOfTypeEven())) 394 | nth.0.attr('title').should.equal 'noId' 395 | nth.length.should.equal 4 396 | 397 | describe '.nthLastOfTypeMod()' 398 | 399 | it "finds elements where (last - position + 1) mod 1 is 0" 400 | nth = select(dsl.descendant('div').where(dsl.nthLastOfTypeMod(1, 0))) 401 | nth.0.attr('title').should.equal 'barDiv' 402 | nth.length.should.equal 8 403 | 404 | it "finds elements where (last()-position()+1) >= 3) and ((((last()-position()+1)-3) mod 1) is 0" 405 | nth = select(dsl.descendant('body').child('div').where(dsl.nthLastOfTypeMod(1, 3))) 406 | nth.0.attr('title').should.equal 'barDiv' 407 | nth.4.attr('title').should.equal 'bazDiv' 408 | nth.length.should.equal 5 409 | 410 | it "finds elements where (last()-position()+1) >= 2) and ((((last()-position()+1)-2) mod 1) is 0" 411 | nth = select(dsl.descendant('div').where(dsl.nthLastOfTypeMod(1, 2))) 412 | nth.0.attr('title').should.equal 'barDiv' 413 | nth.6.attr('id').should.equal 'moar' 414 | nth.length.should.equal 7 415 | 416 | it "finds elements where (last()-position()+1) mod 2) is 0" 417 | nth = select(dsl.descendant('div').where(dsl.nthLastOfTypeMod(2))) 418 | nth.0.attr('title').should.equal 'barDiv' 419 | nth.3.attr('id').should.equal 'moar' 420 | nth.length.should.equal 4 421 | 422 | it "finds elements where (last()-position()+1) <= 3) and ((((last()-position()+1)-3) mod 1) = 0" 423 | nth = select(dsl.descendant('div').where(dsl.nthLastOfTypeMod(-1, 3))) 424 | nth.0.attr('id').should.equal 'preference' 425 | nth.length.should.equal 3 426 | 427 | describe '.nthLastOfTypeOdd()' 428 | 429 | it "finds elements where position is an odd number, counting backwards from the end" 430 | nth = select(dsl.descendant('div').where(dsl.nthLastOfTypeOdd())) 431 | nth.3.attr('id').should.equal 'elephantay' 432 | nth.length.should.equal 4 433 | 434 | describe '.nthLastOfTypeEven()' 435 | 436 | it "finds elements where position is an even number, counting backwards from the end" 437 | nth = select(dsl.descendant('div').where(dsl.nthLastOfTypeEven())) 438 | nth.0.attr('title').should.equal 'barDiv' 439 | nth.3.attr('id').should.equal 'moar' 440 | nth.length.should.equal 4 441 | 442 | describe '.empty()' 443 | 444 | it "finds elements with no children" 445 | with no children = select(dsl.descendant('*').where(dsl.empty())) 446 | with no children.length.should.equal 3 447 | --------------------------------------------------------------------------------