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 selectFirst and expectFirst to Elements #2263

Merged
merged 4 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
26 changes: 26 additions & 0 deletions src/main/java/org/jsoup/select/Elements.java
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,32 @@ public Elements select(String query) {
return Selector.select(query, this);
}

/**
* Find the first Element that matches the {@link Selector} CSS query within this element list.
* <p>This is effectively the same as calling {@code elements.select(query).first()}, but is more efficient as query
* execution stops on the first hit.</p>
* @param query A {@link Selector} query
* @return the first matching element, or <b>{@code null}</b> if there is no match.
* @see #expectFirst(String)
*/
public @Nullable Element selectFirst(String query) {
return Selector.selectFirst(query, this);
}

/**
* Just like {@link #selectFirst(String)}, but if there is no match, throws an {@link IllegalArgumentException}.
* @param query A {@link Selector} query
* @return the first matching element
* @throws IllegalArgumentException if no match is found
*/
public Element expectFirst(String query) {
return (Element) Validate.ensureNotNull(
Selector.selectFirst(query, this),
"No elements matched the query '%s'."
, query
);
}

/**
* Remove elements from this list that match the {@link Selector} query.
* <p>
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/org/jsoup/select/Selector.java
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,28 @@
return Collector.findFirst(QueryParser.parse(cssQuery), root);
}

/**
Find the first element matching the query, across multiple roots.

@param query CSS selector
@param roots root elements to descend into
@return matching elements, empty if none
*/
public static @Nullable Element selectFirst(String query, Iterable<Element> roots) {
Fixed Show fixed Hide fixed
Validate.notEmpty(query);
Validate.notNull(roots);
Evaluator evaluator = QueryParser.parse(query);

for (Element root : roots) {
Element first = Collector.findFirst(evaluator, root);
if (first != null) {
return first;
}
}

return null;
}

public static class SelectorParseException extends IllegalStateException {
public SelectorParseException(String msg) {
super(msg);
Expand Down
33 changes: 33 additions & 0 deletions src/test/java/org/jsoup/select/ElementsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -599,4 +599,37 @@
// check dom
assertEquals("<div> One</div><div> Two</div><div> Three</div><div> Four</div>", TextUtil.normalizeSpaces(doc.body().html()));
}

@Test public void selectFirst() {
Document doc = Jsoup.parse("<p>One</p><p>Two <span>Jsoup</span></p><p><span>Three</span></p>");
Element span = doc.children().selectFirst("span");
assertNotNull(span);
assertEquals("Jsoup", span.text());
}

@Test public void selectFirstNullOnNoMatch() {
Document doc = Jsoup.parse("<p>One</p><p>Two</p><p>Three</p>");
Element span = doc.children().selectFirst("span");
assertNull(span);
}

@Test public void expectFirst() {
Document doc = Jsoup.parse("<p>One</p><p>Two <span>Jsoup</span></p><p><span>Three</span></p>");
Element span = doc.children().expectFirst("span");
assertNotNull(span);
assertEquals("Jsoup", span.text());
}

@Test public void expectFirstThrowsOnNoMatch() {
Document doc = Jsoup.parse("<p>One</p><p>Two</p><p>Three</p>");

boolean threw = false;
try {
Element span = doc.children().expectFirst("span");
Dismissed Show dismissed Hide dismissed
} catch (IllegalArgumentException e) {
threw = true;
assertEquals("No elements matched the query 'span'.", e.getMessage());
}
assertTrue(threw);
}
}
Loading