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 n | td:gt(1) finds cells after skipping the first two |
* :eq(n) | elements whose sibling index is equal to n | td:eq(0) finds the first cell of each row |
* :has(selector) | elements that contains at least one element matching the selector | div:has(p) finds divs that contain p elements |
+ * :hasChild(selector) | elements that directly contains at least one element matching the selector | div: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 :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(" ");
Elements divs = doc.select("div:has(p:has(span))");