diff --git a/src/main/java/org/jsoup/select/QueryParser.java b/src/main/java/org/jsoup/select/QueryParser.java index 768f70834c..3f1fbbfcaf 100644 --- a/src/main/java/org/jsoup/select/QueryParser.java +++ b/src/main/java/org/jsoup/select/QueryParser.java @@ -162,6 +162,8 @@ else if (tq.matchChomp(":gt(")) indexGreaterThan(); else if (tq.matchChomp(":eq(")) indexEquals(); + else if (tq.matches(":hasChild(")) + hasChild(); else if (tq.matches(":has(")) has(); else if (tq.matches(":contains(")) @@ -338,6 +340,15 @@ private void has() { evals.add(new StructuralEvaluator.Has(parse(subQuery))); } + // pseudo selector :hasChild(el). + // Works similar to :has(el) but only searches in direct children. + private void hasChild() { + tq.consume(":hasChild"); + String subQuery = tq.chompBalanced('(', ')'); + Validate.notEmpty(subQuery, ":hasChild(el) subselect must not be empty"); + evals.add(new StructuralEvaluator.HasChild(parse(subQuery))); + } + // pseudo selector :contains(text), containsOwn(text) private void contains(boolean own) { tq.consume(own ? ":containsOwn" : ":contains"); diff --git a/src/main/java/org/jsoup/select/Selector.java b/src/main/java/org/jsoup/select/Selector.java index 8b7aa47fe7..be6784af65 100644 --- a/src/main/java/org/jsoup/select/Selector.java +++ b/src/main/java/org/jsoup/select/Selector.java @@ -47,6 +47,7 @@ * :gt(n)elements whose sibling index is greater than ntd:gt(1) finds cells after skipping the first two * :eq(n)elements whose sibling index is equal to ntd:eq(0) finds the first cell of each row * :has(selector)elements that contains at least one element matching the selectordiv:has(p) finds divs that contain p elements + * :hasChild(selector)elements that directly contains at least one element matching the selectordiv:hasChild(p) finds divs that contain p elements as their direct children * :not(selector)elements that do not match the selector. See also {@link Elements#not(String)}div:not(.logo) finds all divs that do not have the "logo" class.

div:not(:has(div)) finds divs that do not contain divs.

* :contains(text)elements that contains the specified text. The search is case insensitive. The text may appear in the found element, or any of its descendants.p:contains(jsoup) finds p elements containing the text "jsoup". * :matches(regex)elements whose text matches the specified regular expression. The text may appear in the found element, or any of its descendants.td:matches(\\d+) finds table cells containing digits. div:matches((?i)login) finds divs containing the text, case insensitively. diff --git a/src/main/java/org/jsoup/select/StructuralEvaluator.java b/src/main/java/org/jsoup/select/StructuralEvaluator.java index 8083d9e399..c53553af92 100644 --- a/src/main/java/org/jsoup/select/StructuralEvaluator.java +++ b/src/main/java/org/jsoup/select/StructuralEvaluator.java @@ -33,6 +33,25 @@ public String toString() { } } + static class HasChild extends StructuralEvaluator { + public HasChild(Evaluator evaluator){ + this.evaluator = evaluator; + } + + public boolean matches(Element root, Element element){ + for (Element e : element.children()) { + if (evaluator.matches(root, e)) + return true; + } + return false; + } + + @Override + public String toString(){ + return String.format(":hasDirect(%s)", evaluator); + } + } + static class Not extends StructuralEvaluator { public Not(Evaluator evaluator) { this.evaluator = evaluator; diff --git a/src/test/java/org/jsoup/select/SelectorTest.java b/src/test/java/org/jsoup/select/SelectorTest.java index 41649b7f08..b92ae2f194 100644 --- a/src/test/java/org/jsoup/select/SelectorTest.java +++ b/src/test/java/org/jsoup/select/SelectorTest.java @@ -506,6 +506,25 @@ public class SelectorTest { assertEquals("2", els1.get(2).id()); } + @Test public void testPseudoHasChild(){ + Document doc = Jsoup.parse("

Non-Direct P

" + + "

Direct P 0th Child

" + + "

Direct P 1th Child

"); + + //Non-direct :has selection, should contain all divs. + Elements divs1 = doc.select("div:has(p)"); + assertEquals(3, divs1.size()); + assertEquals("1", divs1.get(0).id()); + assertEquals("2", divs1.get(1).id()); + assertEquals("3", divs1.get(2).id()); + + //:hasChild selection, should contain only div#2 and div#3. + Elements divs2 = doc.select("div:hasChild(p)"); + assertEquals(2, divs2.size()); + assertEquals("2", divs2.get(0).id()); + assertEquals("3", divs2.get(1).id()); + } + @Test public void testNestedHas() { Document doc = Jsoup.parse("

One

Two

"); Elements divs = doc.select("div:has(p:has(span))");