esquery
Advanced tools
Comparing version 0.1.0 to 0.2.0
184
esquery.js
@@ -7,3 +7,3 @@ (function () { | ||
var STR = '("(?:\\"|[^"])*")'; | ||
var OP = "(\\*)"; | ||
var OP = "(\\*|\\.|#)"; | ||
var S_DOP_S = "\\s*(!=|<=|>=)\\s*"; | ||
@@ -54,3 +54,3 @@ var S_OP = "\\s*(\\]|\\)|!)"; | ||
}; | ||
} else if (/!=|<=|>=|<|>|,|~|=|!|:|\+|\[|\]|\(|\)|\s/.test(token)) { | ||
} else if (/!=|<=|>=|<|>|,|~|=|!|:|#|\.|\+|\[|\]|\(|\)|\s/.test(token)) { | ||
return { | ||
@@ -109,9 +109,2 @@ type: "operator", | ||
var operatorMap = { | ||
" ": "descendant", | ||
">": "child", | ||
"~": "sibling", | ||
"+": "adjacent" | ||
}; | ||
function peekOp(tokens, opValue) { | ||
@@ -148,21 +141,31 @@ if (tokens.length > 0 && peekType(tokens, "operator") && | ||
var operatorMap = { | ||
" ": "descendant", | ||
">": "child", | ||
"~": "sibling", | ||
"+": "adjacent" | ||
}; | ||
function consumeComplexSelector(tokens) { | ||
var selector; | ||
selector = consumeCompoundSelector(tokens); | ||
if (tokens.length > 0) { | ||
if (peekOp(tokens, /[\s+~>]/)) { | ||
var op = tokens.shift() | ||
var right = consumeComplexSelector(tokens) | ||
if (right) { | ||
selector = { | ||
type: operatorMap[op.value], | ||
left: selector, | ||
right: right | ||
}; | ||
} else { | ||
throw createError("Invalid right side of complex selector: ", op, tokens); | ||
} | ||
var result, selector; | ||
result = consumeCompoundSelector(tokens); | ||
while (peekOp(tokens, /[\s+~>]/)) { | ||
op = tokens.shift(); | ||
selector = consumeCompoundSelector(tokens); | ||
if (selector) { | ||
result = { | ||
type: operatorMap[op.value], | ||
operator: op.value, | ||
left: result, | ||
right: selector | ||
}; | ||
} else { | ||
throw createError("Expected compound selector: ", op, tokens); | ||
} | ||
} | ||
return selector; | ||
return result || selector; | ||
} | ||
@@ -210,2 +213,11 @@ | ||
selector = consumeAttribute(tokens); | ||
} else if (peekOp(tokens, ".")) { | ||
selector = consumeField(tokens); | ||
} else if (peekOp(tokens, "#")) { | ||
tokens.shift(); | ||
selector = consumeType(tokens, /keyword|identifier/); | ||
selector = { | ||
type: "identifier", | ||
value: selector.value | ||
}; | ||
} | ||
@@ -269,2 +281,15 @@ | ||
function consumeName(tokens) { | ||
var name = ""; | ||
while (!name || peekOp(tokens, ".")) { | ||
if (name) { | ||
consumeOp(tokens, "."); | ||
name += "."; | ||
} | ||
name += consumeType(tokens, /keyword|identifier/).value; | ||
} | ||
return name; | ||
} | ||
/** | ||
@@ -275,3 +300,3 @@ * Consume an attribute selector ([]) | ||
var op = consumeOp(tokens, "["); | ||
var id = consumeType(tokens, /keyword|identifier/); | ||
var name = consumeName(tokens); | ||
@@ -282,3 +307,3 @@ op = consumeType(tokens, "operator"); | ||
type: "attribute", | ||
name: id.value | ||
name: name | ||
}; | ||
@@ -288,3 +313,3 @@ } else { | ||
type: "attribute", | ||
name: id.value, | ||
name: name, | ||
operator: op.value, | ||
@@ -299,2 +324,14 @@ value: consumeValue(tokens) | ||
/** | ||
* Consume the various types of pseudo selectors (:*-child). | ||
*/ | ||
function consumeField(tokens) { | ||
var op = consumeOp(tokens, "."); | ||
var name = consumeName(tokens, /keyword|identifier/); | ||
return { | ||
type: "field", | ||
name: name | ||
}; | ||
} | ||
function consumeArgList(tokens) { | ||
@@ -375,13 +412,14 @@ consumeOp(tokens, "("); | ||
*/ | ||
function visitPre(ast, fn) { | ||
fn(ast); | ||
function visitPre(ast, fn, path) { | ||
fn(ast, path); | ||
var key; | ||
var key, newPath; | ||
for (key in ast) { | ||
newPath = path ? path + "." + key : key; | ||
if (ast[key] && ast[key].forEach) { | ||
ast[key].forEach(function (node) { | ||
visitPre(node, fn); | ||
visitPre(node, fn, newPath); | ||
}); | ||
} else if (ast[key] && ast[key].type) { | ||
visitPre(ast[key], fn); | ||
visitPre(ast[key], fn, newPath); | ||
} | ||
@@ -415,3 +453,3 @@ } | ||
for (i = 0; i < keys.length; i++) { | ||
if (value[keys[i]] !== undefined) { | ||
if (value && value[keys[i]] !== undefined) { | ||
value = value[keys[i]]; | ||
@@ -445,3 +483,3 @@ } else { | ||
*/ | ||
function match(ast, selector) { | ||
function match(ast, selector, cache) { | ||
var leftResults, rightResults, subject = [], matches = []; | ||
@@ -459,3 +497,3 @@ var results = { | ||
case "wildcard": | ||
visitPre(ast, function (node) { | ||
cache.nodes.forEach(function (node) { | ||
matches.push(node); | ||
@@ -470,4 +508,4 @@ | ||
case "identifier": | ||
visitPre(ast, function (node) { | ||
if (node.type === selector.value) { | ||
if (cache.types.hasOwnProperty(selector.value)) { | ||
cache.types[selector.value].forEach(function (node) { | ||
matches.push(node); | ||
@@ -478,4 +516,4 @@ | ||
} | ||
} | ||
}); | ||
}); | ||
} | ||
break; | ||
@@ -485,3 +523,3 @@ | ||
case "nth-child": | ||
visitPre(ast, function (node, context) { | ||
visitPre(ast, function (node) { | ||
var index = selector.index.value; | ||
@@ -505,3 +543,3 @@ Object.keys(node).forEach(function (key) { | ||
case "nth-last-child": | ||
visitPre(ast, function (node, context) { | ||
visitPre(ast, function (node) { | ||
var index = selector.index.value; | ||
@@ -579,6 +617,21 @@ Object.keys(node).forEach(function (key) { | ||
case "field": | ||
visitPre(ast, function (node, path) { | ||
if (path) { | ||
var i = path.indexOf(selector.name); | ||
if (i > -1 && i === path.length - selector.name.length) { | ||
matches.push(node); | ||
if (selector.subject) { | ||
subject.push([node]); | ||
} | ||
} | ||
} | ||
}); | ||
break; | ||
case "matches": | ||
selector.selectors.forEach(function (matchesSelector) { | ||
finalMatches(match(ast, matchesSelector)).forEach(function (node) { | ||
finalMatches(match(ast, matchesSelector, cache)).forEach(function (node) { | ||
matches.push(node); | ||
@@ -596,3 +649,3 @@ | ||
selector.selectors.forEach(function (selector) { | ||
rightResults = rightResults.concat(finalMatches(match(ast, selector))); | ||
rightResults = rightResults.concat(finalMatches(match(ast, selector, cache))); | ||
}); | ||
@@ -614,3 +667,3 @@ | ||
selector.selectors.forEach(function (selector) { | ||
rightResults.push(finalMatches(match(ast, selector))); | ||
rightResults.push(finalMatches(match(ast, selector, cache))); | ||
}); | ||
@@ -637,4 +690,4 @@ | ||
case "descendant": | ||
leftResults = match(ast, selector.left); | ||
rightResults = match(ast, selector.right) | ||
leftResults = match(ast, selector.left, cache); | ||
rightResults = match(ast, selector.right, cache); | ||
@@ -665,4 +718,4 @@ leftResults.matches.forEach(function (leftNode, leftI) { | ||
case "child": | ||
leftResults = match(ast, selector.left); | ||
rightResults = match(ast, selector.right); | ||
leftResults = match(ast, selector.left, cache); | ||
rightResults = match(ast, selector.right, cache); | ||
@@ -693,4 +746,4 @@ leftResults.matches.forEach(function (leftNode, leftI) { | ||
case "sibling": | ||
leftResults = match(ast, selector.left); | ||
rightResults = match(ast, selector.right); | ||
leftResults = match(ast, selector.left, cache); | ||
rightResults = match(ast, selector.right, cache); | ||
@@ -732,4 +785,4 @@ visitPre(ast, function (node, context) { | ||
case "adjacent": | ||
leftResults = match(ast, selector.left); | ||
rightResults = match(ast, selector.right); | ||
leftResults = match(ast, selector.left, cache); | ||
rightResults = match(ast, selector.right, cache); | ||
@@ -771,2 +824,22 @@ visitPre(ast, function (node, context) { | ||
// Holds cached info to speed up matches | ||
function Cache(ast) { | ||
this.ast = ast; | ||
var nodes = []; | ||
var types = {}; | ||
visitPre(ast, function (node) { | ||
nodes.push(node); | ||
if (!types.hasOwnProperty(node.type)) { | ||
types[node.type] = []; | ||
} | ||
types[node.type].push(node); | ||
}); | ||
this.nodes = nodes; | ||
this.types = types; | ||
} | ||
/** | ||
@@ -782,4 +855,4 @@ * Parse a selector string and return it's AST. | ||
*/ | ||
function query(ast, selector) { | ||
return finalMatches(match(ast, parse(selector))); | ||
function query(ast, selector, cache) { | ||
return finalMatches(match(ast, parse(selector), cache || new Cache(ast))); | ||
} | ||
@@ -792,2 +865,3 @@ | ||
query.finalMatches = finalMatches; | ||
query.Cache = Cache; | ||
return query; | ||
@@ -794,0 +868,0 @@ } |
{ | ||
"name": "esquery", | ||
"preferGlobal": false, | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"author": "Joel Feenstra <jrfeenst+esquery@gmail.com>", | ||
"description": "A query library for ECMAScript AST using a CSS selector like query language.", | ||
"main": "esquery.js", | ||
"scripts": { | ||
"test": "node_modules/jstestr/bin/jstestr.js path=tests" | ||
"test": "node node_modules/jstestr/bin/jstestr.js path=tests" | ||
}, | ||
@@ -10,0 +11,0 @@ "repository": { |
@@ -27,2 +27,3 @@ | ||
var opRBracket = {type: "operator", value: "]"}; | ||
var opDot = {type: "operator", value: "."}; | ||
@@ -93,2 +94,10 @@ var num1 = {type: "number", value: 1}; | ||
"attribute with dot access": function () { | ||
var ast = esquery.processTokens([opLBracket, idAttr, opDot, idAttr, opRBracket]); | ||
assert.matches({ | ||
type: "attribute", | ||
name: "attr.attr" | ||
}, ast); | ||
}, | ||
"descendant selector with identifiers": function () { | ||
@@ -235,30 +244,30 @@ var ast = esquery.processTokens([idFoo, opSpace, idAsdf]); | ||
assert.matches({ | ||
type: "adjacent", | ||
type: "child", | ||
left: { | ||
type: "identifier", | ||
value: "foo" | ||
}, | ||
right: { | ||
type: "descendant", | ||
left: { | ||
type: "nth-child", | ||
index: { | ||
type: "literal", | ||
value: 1 | ||
type: "adjacent", | ||
left: { | ||
type: "identifier", | ||
value: "foo" | ||
}, | ||
right: { | ||
type: "nth-child", | ||
index: { | ||
type: "literal", | ||
value: 1 | ||
} | ||
} | ||
}, | ||
right: { | ||
type: "child", | ||
left: { | ||
type: "attribute", | ||
name: "attr", | ||
operator: "=", | ||
value: { | ||
type: "literal", | ||
value: 2 | ||
} | ||
}, | ||
right: idFoo | ||
type: "attribute", | ||
name: "attr", | ||
operator: "=", | ||
value: { | ||
type: "literal", | ||
value: 2 | ||
} | ||
} | ||
} | ||
}, | ||
right: idFoo | ||
}, ast); | ||
@@ -333,4 +342,8 @@ }, | ||
assert.doesThrow(Error, esquery.processTokens.bind(this, [opColon, keywordNot, opLParen, wildcard, wildcard])); | ||
}, | ||
"invalid field": function () { | ||
assert.doesThrow(Error, esquery.processTokens.bind(this, [opDot])); | ||
} | ||
}); | ||
}); |
@@ -22,2 +22,9 @@ | ||
"three types child": function () { | ||
var matches = esquery(conditional, "IfStatement > BinaryExpression > Identifier"); | ||
assert.contains([ | ||
conditional.body[0].test.left | ||
], matches); | ||
}, | ||
"two types descendant": function () { | ||
@@ -24,0 +31,0 @@ var matches = esquery(conditional, "IfStatement BinaryExpression"); |
@@ -74,2 +74,9 @@ | ||
"field subject": function () { | ||
var matches = esquery(forLoop, '.test!'); | ||
assert.contains([ | ||
forLoop.body[0].test | ||
], matches); | ||
}, | ||
":matches subject": function () { | ||
@@ -76,0 +83,0 @@ var matches = esquery(forLoop, ':matches(*)! > [name="foo"]'); |
@@ -101,3 +101,20 @@ | ||
}, | ||
"# type": function () { | ||
var matches = esquery(forLoop, "#Program"); | ||
assert.contains([ | ||
forLoop | ||
], matches); | ||
matches = esquery(forLoop, "#ForStatement"); | ||
assert.contains([ | ||
forLoop.body[0] | ||
], matches); | ||
matches = esquery(forLoop, "#BinaryExpression"); | ||
assert.contains([ | ||
forLoop.body[0].test | ||
], matches); | ||
} | ||
}); | ||
}); |
@@ -13,3 +13,7 @@ | ||
tokens = esquery.tokenize("id.name"); | ||
assert.matches([{type: "identifier", value: "id.name"}], tokens); | ||
assert.matches([ | ||
{type: "identifier", value: "id"}, | ||
{type: "operator", value: "."}, | ||
{type: "identifier", value: "name"} | ||
], tokens); | ||
}, | ||
@@ -23,5 +27,11 @@ | ||
"individual operators": function () { | ||
var tokens = esquery.tokenize(":"); | ||
var tokens = esquery.tokenize("."); | ||
assert.matches([{type: "operator", value: "."}], tokens); | ||
tokens = esquery.tokenize(":"); | ||
assert.matches([{type: "operator", value: ":"}], tokens); | ||
tokens = esquery.tokenize("#"); | ||
assert.matches([{type: "operator", value: "#"}], tokens); | ||
tokens = esquery.tokenize("["); | ||
@@ -28,0 +38,0 @@ assert.matches([{type: "operator", value: "["}], tokens); |
Sorry, the diff of this file is not supported yet
118910
27
2664