diff --git a/esquery.js b/esquery.js index 5b67924..d4d9fea 100644 --- a/esquery.js +++ b/esquery.js @@ -83,6 +83,22 @@ } return true; + case 'has': + var a, collector = []; + for (i = 0, l = selector.selectors.length; i < l; ++i) { + a = []; + estraverse.traverse(node, { + enter: function (node, parent) { + if (parent != null) { a.unshift(parent); } + if (matches(node, selector.selectors[i], a)) { + collector.push(node); + } + }, + leave: function () { a.shift(); } + }); + } + return collector.length !== 0; + case 'child': if (matches(node, selector.right, ancestry)) { return matches(ancestry[0], selector.left, ancestry.slice(1)); diff --git a/grammar.pegjs b/grammar.pegjs index d124c51..34e1c59 100644 --- a/grammar.pegjs +++ b/grammar.pegjs @@ -49,7 +49,7 @@ sequence atom = wildcard / identifier / attr / field / negation / matches - / firstChild / lastChild / nthChild / nthLastChild / class + / has / firstChild / lastChild / nthChild / nthLastChild / class wildcard = a:"*" { return { type: 'wildcard', value: a }; } identifier = "#"? i:identifierName { return { type: 'identifier', value: i }; } @@ -88,12 +88,14 @@ field = "." i:identifierName is:("." identifierName)* { negation = ":not(" _ ss:selectors _ ")" { return { type: 'not', selectors: ss }; } matches = ":matches(" _ ss:selectors _ ")" { return { type: 'matches', selectors: ss }; } +has = ":has(" _ ss:selectors _ ")" { return { type: 'has', selectors: ss }; } firstChild = ":first-child" { return nth(1); } lastChild = ":last-child" { return nthLast(1); } nthChild = ":nth-child(" _ n:[0-9]+ _ ")" { return nth(parseInt(n.join(''), 10)); } nthLastChild = ":nth-last-child(" _ n:[0-9]+ _ ")" { return nthLast(parseInt(n.join(''), 10)); } + class = ":" c:("statement"i / "expression"i / "declaration"i / "function"i / "pattern"i) { return { type: 'class', name: c }; } diff --git a/parser.js b/parser.js index 2ce8134..5e250f7 100644 --- a/parser.js +++ b/parser.js @@ -60,6 +60,7 @@ var result = (function(){ "field": parse_field, "negation": parse_negation, "matches": parse_matches, + "has": parse_has, "firstChild": parse_firstChild, "lastChild": parse_lastChild, "nthChild": parse_nthChild, @@ -700,15 +701,18 @@ var result = (function(){ if (result0 === null) { result0 = parse_matches(); if (result0 === null) { - result0 = parse_firstChild(); + result0 = parse_has(); if (result0 === null) { - result0 = parse_lastChild(); + result0 = parse_firstChild(); if (result0 === null) { - result0 = parse_nthChild(); + result0 = parse_lastChild(); if (result0 === null) { - result0 = parse_nthLastChild(); + result0 = parse_nthChild(); if (result0 === null) { - result0 = parse_class(); + result0 = parse_nthLastChild(); + if (result0 === null) { + result0 = parse_class(); + } } } } @@ -2073,6 +2077,80 @@ var result = (function(){ return result0; } + function parse_has() { + var cacheKey = "has@" + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + var result0, result1, result2, result3, result4; + var pos0, pos1; + + pos0 = pos; + pos1 = pos; + if (input.substr(pos, 5) === ":has(") { + result0 = ":has("; + pos += 5; + } else { + result0 = null; + if (reportFailures === 0) { + matchFailed("\":has(\""); + } + } + if (result0 !== null) { + result1 = parse__(); + if (result1 !== null) { + result2 = parse_selectors(); + if (result2 !== null) { + result3 = parse__(); + if (result3 !== null) { + if (input.charCodeAt(pos) === 41) { + result4 = ")"; + pos++; + } else { + result4 = null; + if (reportFailures === 0) { + matchFailed("\")\""); + } + } + if (result4 !== null) { + result0 = [result0, result1, result2, result3, result4]; + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + if (result0 !== null) { + result0 = (function(offset, ss) { return { type: 'has', selectors: ss }; })(pos0, result0[2]); + } + if (result0 === null) { + pos = pos0; + } + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + function parse_firstChild() { var cacheKey = "firstChild@" + pos; var cachedResult = cache[cacheKey]; diff --git a/tests/queryHas.js b/tests/queryHas.js new file mode 100644 index 0000000..eb9aa09 --- /dev/null +++ b/tests/queryHas.js @@ -0,0 +1,37 @@ + +define([ + "esquery", + "jstestr/assert", + "jstestr/test", + "./fixtures/conditional" +], function (esquery, assert, test, conditional) { + + test.defineSuite("Parent selector query", { + + "conditional": function () { + var matches = esquery(conditional, 'ExpressionStatement:has([name="foo"][type="Identifier"])'); + assert.isEqual(1, matches.length); + }, + + "one of": function () { + var matches = esquery(conditional, 'IfStatement:has(LogicalExpression [name="foo"], LogicalExpression [name="x"])'); + assert.isEqual(1, matches.length); + }, + + "chaining": function () { + var matches = esquery(conditional, 'BinaryExpression:has(Identifier[name="x"]):has(Literal[value="test"])'); + assert.isEqual(1, matches.length); + }, + + "nesting": function () { + var matches = esquery(conditional, 'Program:has(IfStatement:has(Literal[value=true], Literal[value=false]))'); + assert.isEqual(1, matches.length); + }, + + "non-matching": function () { + var matches = esquery(conditional, ':has([value="impossible"])'); + assert.isEqual(0, matches.length); + } + + }); +});