HasEvaluator.java
package org.jsoup.select;
import org.jsoup.internal.SoftPool;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.NodeIterator;
import org.jsoup.nodes.TextNode;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Evaluates the relative selectors inside {@code :has()}. */
final class HasEvaluator extends Evaluator {
private final String selector;
final List<Traversal> traversals;
private final boolean wantsNodes;
private final int cost;
HasEvaluator(String selector, List<Traversal> traversals) {
this.selector = selector;
this.traversals = Collections.unmodifiableList(new ArrayList<>(traversals));
boolean wantsNodes = false;
int evaluatorCost = 0;
for (Traversal traversal : traversals) {
wantsNodes |= traversal.evaluator.wantsNodes();
evaluatorCost += traversal.evaluator.cost();
}
this.wantsNodes = wantsNodes;
this.cost = 10 * evaluatorCost;
}
@Override public boolean matches(Element root, Element element) {
if (web) {
element = documentElementOrSelf(element); // use the document element when testing :has() on a Document
if (element == null) return false;
}
if (!web) root = element;
for (int i = 0; i < traversals.size(); i++) {
if (traversals.get(i).matchesAny(root, element)) return true;
}
return false;
}
@Override boolean wantsNodes() {
return wantsNodes;
}
@Override protected void reset() {
for (Traversal traversal : traversals) traversal.evaluator.reset();
}
@Override protected int cost() {
return cost;
}
@Override public String toString() {
return ":has(" + selector + ')';
}
/**
Searches the children, descendants, or following siblings needed by a {@code :has()} query.
For example, {@code :has(> a)} checks children, while {@code :has(+ div span)} checks the next sibling and its descendants.
*/
static final class Traversal {
static final SoftPool<NodeIterator<Node>> NodeIterPool =
new SoftPool<>(() -> new NodeIterator<>(new TextNode(""), Node.class)); // restart() replaces the placeholder root
final Evaluator evaluator;
final boolean startsAtSibling;
final boolean descend;
final boolean nextSiblingOnly;
private final boolean wantsNodes;
Traversal(ArrayList<Evaluator> alternatives, boolean startsAtSibling, boolean descend, boolean nextSiblingOnly) {
evaluator = alternatives.size() == 1 ? alternatives.get(0) : new CombiningEvaluator.Or(alternatives);
this.startsAtSibling = startsAtSibling;
this.descend = descend;
this.nextSiblingOnly = nextSiblingOnly;
wantsNodes = evaluator.wantsNodes();
}
boolean matchesAny(Element context, Element subject) {
Node start = first(subject);
if (start == null) return false;
@Nullable NodeIterator<Node> nodes = descend ? NodeIterPool.borrow() : null;
try {
return findMatch(context, subject, start, nodes);
} finally {
if (nodes != null) NodeIterPool.release(nodes);
// cached matches are only valid for this starting element
evaluator.reset();
}
}
private boolean findMatch(Element context, Element subject, Node start, @Nullable NodeIterator<Node> nodes) {
for (; start != null; start = next(start)) {
if (nodes != null) {
nodes.restart(start);
if (!startsAtSibling) nodes.next(); // search the whole tree, excluding the subject
}
do {
Node candidate = nodes != null ? nodes.next() : start;
if (evaluator.matches(context, candidate, subject)) return true;
} while (nodes != null && nodes.hasNext());
}
return false;
}
private @Nullable Node first(Element subject) {
if (!startsAtSibling) {
Node child = subject.firstChild();
return descend && child != null ? subject : child;
}
return nextSiblingOnly || !descend && !wantsNodes ? subject.nextElementSibling() : subject.nextSibling();
}
private @Nullable Node next(Node start) {
if (nextSiblingOnly || descend && !startsAtSibling) return null;
return startsAtSibling && !descend && !wantsNodes ? start.nextElementSibling() : start.nextSibling();
}
}
}