Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add :hasChild(el) pseudo-selector to support :has(el) selections that search in direct children only, see #1116 #1175

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/main/java/org/jsoup/select/QueryParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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("))
Expand Down Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/jsoup/select/Selector.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
* <tr><td><code>:gt(<em>n</em>)</code></td><td>elements whose sibling index is greater than <em>n</em></td><td><code>td:gt(1)</code> finds cells after skipping the first two</td></tr>
* <tr><td><code>:eq(<em>n</em>)</code></td><td>elements whose sibling index is equal to <em>n</em></td><td><code>td:eq(0)</code> finds the first cell of each row</td></tr>
* <tr><td><code>:has(<em>selector</em>)</code></td><td>elements that contains at least one element matching the <em>selector</em></td><td><code>div:has(p)</code> finds divs that contain p elements </td></tr>
* <tr><td><code>:hasChild(<em>selector</em>)</code></td><td>elements that <b>directly</b> contains at least one element matching the <em>selector</em></td><td><code>div:hasChild(p)</code> finds divs that contain p elements as their direct children</td></tr>
* <tr><td><code>:not(<em>selector</em>)</code></td><td>elements that do not match the <em>selector</em>. See also {@link Elements#not(String)}</td><td><code>div:not(.logo)</code> finds all divs that do not have the "logo" class.<p><code>div:not(:has(div))</code> finds divs that do not contain divs.</p></td></tr>
* <tr><td><code>:contains(<em>text</em>)</code></td><td>elements that contains the specified text. The search is case insensitive. The text may appear in the found element, or any of its descendants.</td><td><code>p:contains(jsoup)</code> finds p elements containing the text "jsoup".</td></tr>
* <tr><td><code>:matches(<em>regex</em>)</code></td><td>elements whose text matches the specified regular expression. The text may appear in the found element, or any of its descendants.</td><td><code>td:matches(\\d+)</code> finds table cells containing digits. <code>div:matches((?i)login)</code> finds divs containing the text, case insensitively.</td></tr>
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/org/jsoup/select/StructuralEvaluator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 19 additions & 0 deletions src/test/java/org/jsoup/select/SelectorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,25 @@ public class SelectorTest {
assertEquals("2", els1.get(2).id());
}

@Test public void testPseudoHasChild(){
Document doc = Jsoup.parse("<div id='1'><a><p>Non-Direct P</p></a></div>" +
"<div id='2'><p>Direct P 0th Child</p></div>" +
"<div id='3'><a></a><p>Direct P 1th Child</p></div>");

//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("<div><p><span>One</span></p></div> <div><p>Two</p></div>");
Elements divs = doc.select("div:has(p:has(span))");
Expand Down