001package org.jsoup.select;
002
003import org.jsoup.nodes.Element;
004import org.jsoup.nodes.LeafNode;
005import org.jsoup.nodes.Node;
006import org.jsoup.nodes.TextNode;
007import org.jspecify.annotations.Nullable;
008
009import java.util.stream.Stream;
010
011import static java.util.stream.Collectors.toCollection;
012
013/**
014 Collects elements and nodes that match an evaluator.
015 <p>The evaluator determines whether searches include the starting element. See
016 <a href="{@docRoot}/org/jsoup/select/Selector.html#query-compatibility">Query compatibility changes</a>.</p>
017
018 @author Jonathan Hedley */
019public class Collector {
020
021    private Collector() {}
022
023    /**
024     Build a list of elements, by visiting the root and every descendant of root, and testing it against the Evaluator.
025     @param eval Evaluator to test elements against
026     @param root root of tree to descend
027     @return list of matches; empty if none
028     */
029    public static Elements collect(Evaluator eval, Element root) {
030        Stream<Element> stream = eval.wantsNodes() ?
031            streamNodes(eval, root, Element.class) :
032            stream(eval, root);
033        Elements els = stream.collect(toCollection(Elements::new));
034        eval.reset(); // drops any held memos
035        return els;
036    }
037
038    /**
039     Obtain a Stream of elements by visiting the root and every descendant of root and testing it against the evaluator.
040
041     @param evaluator Evaluator to test elements against
042     @param root root of tree to descend
043     @return A {@link Stream} of matches
044     @since 1.19.1
045     */
046    public static Stream<Element> stream(Evaluator evaluator, Element root) {
047        return EvaluatorInternals.stream(evaluator, root, root.stream());
048    }
049
050    /**
051     Obtain a Stream of nodes, of the specified type, by visiting the root and every descendant of root and testing it
052     against the evaluator.
053
054     @param evaluator Evaluator to test elements against
055     @param root root of tree to descend
056     @param type the type of node to collect (e.g. {@link Element}, {@link LeafNode}, {@link TextNode} etc)
057     @param <T> the type of node to collect
058     @return A {@link Stream} of matches
059     @since 1.21.1
060     */
061    public static <T extends Node> Stream<T> streamNodes(Evaluator evaluator, Element root, Class<T> type) {
062        return EvaluatorInternals.stream(evaluator, root, root.nodeStream(type), evaluator.asNodePredicate(root));
063    }
064
065    /**
066     Finds the first Element that matches the Evaluator that descends from the root, and stops the query once that first
067     match is found.
068     @param eval Evaluator to test elements against
069     @param root root of tree to descend
070     @return the first match; {@code null} if none
071     */
072    public static @Nullable Element findFirst(Evaluator eval, Element root) {
073        Element el = stream(eval, root).findFirst().orElse(null);
074        eval.reset();
075        return el;
076    }
077
078    /**
079     Finds the first Node that matches the Evaluator that descends from the root, and stops the query once that first
080     match is found.
081
082     @param eval Evaluator to test elements against
083     @param root root of tree to descend
084     @param type the type of node to collect (e.g. {@link Element}, {@link LeafNode}, {@link TextNode} etc)
085     @return the first match; {@code null} if none
086     @since 1.21.1
087     */
088    public static <T extends Node> @Nullable T findFirstNode(Evaluator eval, Element root, Class<T> type) {
089        T node = streamNodes(eval, root, type).findFirst().orElse(null);
090        eval.reset();
091        return node;
092    }
093
094    /**
095     Build a list of nodes that match the supplied criteria, by visiting the root and every descendant of root, and
096     testing it against the Evaluator.
097
098     @param evaluator Evaluator to test elements against
099     @param root root of tree to descend
100     @param type the type of node to collect (e.g. {@link Element}, {@link LeafNode}, {@link TextNode} etc)
101     @param <T> the type of node to collect
102     @return list of matches; empty if none
103     */
104    public static <T extends Node> Nodes<T> collectNodes(Evaluator evaluator, Element root, Class<T> type) {
105        return streamNodes(evaluator, root, type).collect(toCollection(Nodes::new));
106    }
107}