├── .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 | [](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 |Blah
12 |Bax
13 |Bax
14 |Bax
20 |Bax
21 |Blah
22 |allamas
28 |llama
29 |32 | A lot 33 | 34 | of 35 | whitespace 36 |
37 | 38 |chimp
40 |flamingo
42 |