001package org.jsoup.select;
002
003import org.jsoup.internal.CssMatching;
004import org.jsoup.internal.QueryMode;
005import org.jsoup.nodes.Element;
006import org.jsoup.nodes.Node;
007import org.jspecify.annotations.Nullable;
008
009import java.util.function.Predicate;
010import java.util.stream.Stream;
011
012/**
013 Shared query operations for {@link Element}, {@link Collector}, and {@link org.jsoup.parser.StreamParser}.
014 Public for access between jsoup packages; not a supported user API.
015 String-query methods report compatibility differences when in check mode.
016 */
017public final class EvaluatorInternals {
018    private EvaluatorInternals() {}
019
020    /**
021     Filters candidates using the evaluator's captured query mode, excluding {@code root} in Web mode.
022     */
023    public static Stream<Element> stream(Evaluator evaluator, Element root, Stream<Element> candidates) {
024        return stream(evaluator, root, candidates, evaluator.asPredicate(root));
025    }
026
027    /**
028     Shares stream filtering between element and node queries.
029     */
030    static <T extends Node> Stream<T> stream(Evaluator evaluator, Element root, Stream<T> candidates,
031                                             Predicate<? super T> predicate) {
032        evaluator.reset();
033        if (evaluator.web)
034            candidates = candidates.filter(node -> node != root);
035        return candidates.filter(documentPredicate(evaluator, root, predicate));
036    }
037
038    /**
039     Resolves the context document lazily and reuses it for Web matching.
040     */
041    private static <T> Predicate<T> documentPredicate(Evaluator evaluator, Element root, Predicate<T> predicate) {
042        return evaluator.web ? CssMatching.withDocument(root, predicate) : predicate;
043    }
044
045    /**
046     Matches ID values exactly for {@link Element#getElementById(String)}, even in quirks mode.
047     */
048    public static Evaluator byId(String id) {
049        return new Evaluator() {
050            @Override
051            public boolean matches(Element root, Element element) {
052                String actual = web ?
053                    CssMatching.attributeValue(element, "id", CssMatching.isHtmlElement(root, element)) : element.id();
054                return id.equals(actual);
055            }
056        };
057    }
058
059    /**
060     Tests whether the supplied element matches the query.
061     */
062    public static boolean matches(String query, Element element) {
063        QueryMode mode = QueryMode.current();
064        boolean result = matches(QueryParser.parse(query, mode == QueryMode.Web), element);
065        if (mode == QueryMode.Check)
066            QueryCompat.check(query, result, evaluator -> matches(evaluator, element));
067        return result;
068    }
069
070    /**
071     Returns the element if it matches the query, or its nearest matching ancestor.
072     */
073    public static @Nullable Element closest(String query, Element element) {
074        QueryMode mode = QueryMode.current();
075        Element result = closest(QueryParser.parse(query, mode == QueryMode.Web), element);
076        if (mode == QueryMode.Check)
077            QueryCompat.check("closest", query, result, evaluator -> closest(evaluator, element));
078        return result;
079    }
080
081    /**
082     Tests the element itself, using it as {@code :scope} in Web mode.
083     */
084    public static boolean matches(Evaluator evaluator, Element element) {
085        evaluator.reset();
086        Element root = evaluator.web ? element : element.root();
087        boolean matches = evaluator.web ?
088            CssMatching.<Element>withDocument(root, candidate -> evaluator.matches(root, candidate)).test(element) :
089            evaluator.matches(root, element);
090        evaluator.reset();
091        return matches;
092    }
093
094    /**
095     Returns the element if it matches the evaluator, or its nearest matching ancestor.
096     In Web mode, {@code :scope} remains the supplied element.
097     */
098    public static @Nullable Element closest(Evaluator evaluator, Element element) {
099        evaluator.reset();
100        Element root = evaluator.web ? element : element.root();
101        Predicate<Element> predicate = documentPredicate(evaluator, root, candidate -> evaluator.matches(root, candidate));
102        Element candidate = element;
103        while (candidate != null && !predicate.test(candidate))
104            candidate = candidate.parent();
105        evaluator.reset();
106        return candidate;
107    }
108}