001package org.jsoup.select;
002
003import org.jsoup.helper.Regex;
004import org.jsoup.internal.StringUtil;
005import org.jsoup.internal.QueryMode;
006import org.jsoup.helper.Validate;
007import org.jsoup.internal.CssMatching.AttributeOperator;
008import org.jsoup.internal.CssMatching.CaseSensitivity;
009import org.jsoup.nodes.CDataNode;
010import org.jsoup.nodes.Comment;
011import org.jsoup.nodes.DataNode;
012import org.jsoup.nodes.LeafNode;
013import org.jsoup.nodes.Node;
014import org.jsoup.nodes.ProcessingInstruction;
015import org.jsoup.nodes.TextNode;
016import org.jsoup.parser.TokenQueue;
017import org.jsoup.select.HasEvaluator.Traversal;
018import org.jspecify.annotations.Nullable;
019
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.List;
023import java.util.function.Function;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026
027import static org.jsoup.internal.Normalizer.asciiLowerCase;
028import static org.jsoup.internal.StringUtil.trimAsciiWhitespace;
029import static org.jsoup.select.StructuralEvaluator.ImmediateParentRun;
030
031/**
032 * Parses a CSS selector into an Evaluator tree.
033 */
034public class QueryParser implements AutoCloseable {
035    private final static char[] Combinators = {'>', '+', '~'}; // ' ' is also a combinator, but found implicitly
036    private static final AttributeOperator[] AttributeOperators = AttributeOperator.values();
037    private final static char[] SequenceEnders = {',', ')'};
038    static final int MaxQueryComplexity = 128;
039
040    private final TokenQueue tq;
041    private final String query;
042    private final boolean web;
043    private boolean inNodeContext; // ::comment:contains should act on node value, vs element text
044    private int complexity;
045
046    /**
047     * Create a new QueryParser.
048     * @param query CSS query
049     */
050    private QueryParser(String query, boolean web) {
051        this.web = web;
052        Validate.notEmpty(query);
053        query = trimAsciiWhitespace(query);
054        this.query = query;
055        this.tq = new TokenQueue(query);
056    }
057
058    /**
059     Parse a CSS query into an Evaluator. If you are evaluating the same query repeatedly, it may be more efficient to
060     parse it once and reuse the Evaluator.
061
062     <p>A query may contain at most {@value #MaxQueryComplexity} combinators, node selectors, and {@code :has()},
063     {@code :is()}, or {@code :not()} expressions in total, across all selector alternatives.</p>
064
065     @param query CSS query
066     @return Evaluator
067     @see Selector selector query syntax
068     @throws Selector.SelectorParseException if the CSS query is invalid or exceeds the complexity limit
069     */
070    public static Evaluator parse(String query) {
071        return parse(query, QueryMode.current() == QueryMode.Web);
072    }
073
074    static Evaluator parse(String query, boolean web) {
075        return Evaluator.withMode(web, () -> {
076            try (QueryParser p = new QueryParser(query, web)) {
077                return p.parse();
078            } catch (IllegalArgumentException e) {
079                throw new Selector.SelectorParseException(e.getMessage());
080            }
081        });
082    }
083
084    /**
085     Parse the query. We use this simplified expression of the grammar:
086     <pre>
087     SelectorGroup   ::= Selector (',' Selector)*
088     Selector        ::= [ Combinator ] SimpleSequence ( Combinator SimpleSequence )*
089     SimpleSequence  ::= [ TypeSelector ] ( ID | Class | Attribute | Pseudo )*
090     Pseudo           ::= ':' Name [ '(' SelectorGroup ')' ]
091     Combinator      ::= S+         // descendant (whitespace)
092     | '>'       // child
093     | '+'       // adjacent sibling
094     | '~'       // general sibling
095     </pre>
096
097     See <a href="https://www.w3.org/TR/selectors-4/#grammar">selectors-4</a> for the real thing
098     */
099    Evaluator parse() {
100        Evaluator eval = parseSelectorGroup().evaluator();
101        tq.consumeWhitespace();
102        if (!tq.isEmpty())
103            throw new Selector.SelectorParseException("Could not parse query '%s': unexpected token at '%s'", query, tq.remainder());
104        return eval;
105    }
106
107    /** Parses an ordered group of comma-separated selectors. */
108    private SelectorGroup parseSelectorGroup() {
109        List<ComplexSelector> selectors = new ArrayList<>();
110        do {
111            selectors.add(parseSelector());
112        } while (tq.matchChomp(','));
113        return new SelectorGroup(selectors);
114    }
115
116    private ComplexSelector parseSelector() {
117        tq.consumeWhitespace();
118        char leading = tq.matchesAny(Combinators) ? tq.consume() : 0;
119        if (leading != 0) consumeStructuralStep();
120        List<Evaluator> compounds = Collections.singletonList(parseSimpleSequence());
121        List<Character> combinators = Collections.emptyList();
122        while (true) {
123            char combinator = tq.consumeWhitespace() ? ' ' : 0;
124            if (tq.matchesAny(Combinators)) combinator = tq.consume();
125            else if (tq.matchesAny(SequenceEnders) || tq.isEmpty()) break;
126            if (combinator == 0) break;
127            consumeStructuralStep();
128            if (combinators.isEmpty()) {
129                compounds = new ArrayList<>(compounds);
130                combinators = new ArrayList<>();
131            }
132            combinators.add(combinator);
133            compounds.add(parseSimpleSequence());
134        }
135        return new ComplexSelector(compounds, combinators, leading);
136    }
137
138    /** Stores selector parts and relationships, such as {@code div.product > a}, for ordinary queries and {@code :has()}. */
139    private final class ComplexSelector {
140        final List<Evaluator> compounds;
141        final List<Character> combinators;
142        final char leadingCombinator;
143
144        ComplexSelector(List<Evaluator> compounds, List<Character> combinators, char leading) {
145            this.compounds = compounds;
146            this.combinators = combinators;
147            leadingCombinator = leading;
148        }
149
150        Evaluator evaluator() {
151            return evaluator(leadingCombinator != 0 ? new StructuralEvaluator.QueryRoot() : null);
152        }
153
154        Evaluator relativeEvaluator() {
155            if (compounds.size() == 1 && (leadingCombinator != '+' || !wantsNodes()))
156                return compounds.get(0); // traversal already guarantees the leading relationship
157            return evaluator(leadingCombinator != 0 || web ? new StructuralEvaluator.RelativeAnchor() : null);
158        }
159
160        private Evaluator evaluator(@Nullable Evaluator anchor) {
161            Evaluator left = compounds.get(0);
162            if (anchor != null) left = combinator(anchor, leadingCombinator == 0 ? ' ' : leadingCombinator, left);
163            for (int i = 0; i < combinators.size(); i++)
164                left = combinator(left, combinators.get(i), compounds.get(i + 1));
165            return left;
166        }
167
168        boolean wantsNodes() {
169            for (Evaluator compound : compounds) if (compound.wantsNodes()) return true;
170            return false;
171        }
172
173        @Override public String toString() {
174            StringBuilder out = new StringBuilder();
175            if (leadingCombinator != 0) out.append(leadingCombinator).append(' ');
176            out.append(compounds.get(0));
177            for (int i = 0; i < combinators.size(); i++) {
178                char relation = combinators.get(i);
179                out.append(' ');
180                if (relation != ' ') out.append(relation).append(' ');
181                out.append(compounds.get(i + 1));
182            }
183            return out.toString();
184        }
185    }
186
187    /** A parsed selector group that can be compiled or rendered in source order. */
188    private static final class SelectorGroup {
189        final List<ComplexSelector> selectors;
190
191        /** Creates a group from its selectors in source order. */
192        SelectorGroup(List<ComplexSelector> selectors) {
193            this.selectors = selectors;
194        }
195
196        /** Build the selectors into a single evaluator. */
197        Evaluator evaluator() {
198            Evaluator first = selectors.get(0).evaluator();
199            if (selectors.size() == 1)
200                return first;
201
202            ArrayList<Evaluator> evaluators = new ArrayList<>(selectors.size());
203            // preserve a leading Or's flat evaluation order, e.g. the two alternatives compiled for *|tag
204            if (first instanceof CombiningEvaluator.Or)
205                evaluators.addAll(((CombiningEvaluator.Or) first).evaluators);
206            else
207                evaluators.add(first);
208            for (int i = 1; i < selectors.size(); i++)
209                evaluators.add(selectors.get(i).evaluator());
210            return new CombiningEvaluator.Or(evaluators);
211        }
212
213        @Override public String toString() {
214            return StringUtil.join(selectors, ", ");
215        }
216    }
217
218    Evaluator parseSimpleSequence() {
219        // SimpleSequence ::= TypeSelector? ( Hash | Class | Pseudo )*
220        Evaluator first = null;
221        tq.consumeWhitespace();
222
223        // one optional type selector
224        if (tq.matchesWord() || tq.matches("*|"))
225            first = byTag();
226        else if (tq.matchChomp('*'))
227            first = new Evaluator.AllElements();
228
229        // use the first subclass when there is no type selector
230        if (first == null)
231            first = parseSubclass();
232
233        if (first == null)
234            throw new Selector.SelectorParseException("Could not parse query '%s': unexpected token at '%s'", query, tq.remainder());
235
236        return parseRemainingSubclasses(first);
237    }
238
239    /** Parses and combines any subclasses that follow the first evaluator. */
240    private Evaluator parseRemainingSubclasses(Evaluator first) {
241        Evaluator next = parseSubclass();
242        if (next == null)
243            return first;
244
245        ArrayList<Evaluator> evaluators = new ArrayList<>(4);
246        evaluators.add(first);
247        do {
248            evaluators.add(next);
249        } while ((next = parseSubclass()) != null);
250        return new CombiningEvaluator.And(evaluators);
251    }
252
253    static Evaluator combinator(Evaluator left, char combinator, Evaluator right) {
254        switch (combinator) {
255            case '>':
256                ImmediateParentRun run = left instanceof ImmediateParentRun ?
257                    (ImmediateParentRun) left : new ImmediateParentRun(left);
258                run.add(right);
259                return run;
260            case ' ':
261                return new CombiningEvaluator.And(new StructuralEvaluator.Ancestor(left), right);
262            case '+':
263                return new CombiningEvaluator.And(new StructuralEvaluator.ImmediatePreviousSibling(left), right);
264            case '~':
265                return new CombiningEvaluator.And(new StructuralEvaluator.PreviousSibling(left), right);
266            default:
267                throw new Selector.SelectorParseException("Unknown combinator '%s'", combinator);
268        }
269    }
270
271    @Nullable Evaluator parseSubclass() {
272        //  Subclass: ID | Class | Attribute | Pseudo
273        if      (tq.matchChomp('#'))    return byId();
274        else if (tq.matchChomp('.'))    return byClass();
275        else if (tq.matches('['))       return byAttribute();
276        else if (tq.matchChomp("::"))   return parseNodeSelector(); // ::comment etc
277        else if (tq.matchChomp(':'))    return parsePseudoSelector();
278        else                            return null;
279    }
280
281    private Evaluator parsePseudoSelector() {
282        final String pseudo = tq.consumeCssIdentifier();
283        switch (pseudo) {
284            case "lt":
285                return new Evaluator.IndexLessThan(consumeIndex());
286            case "gt":
287                return new Evaluator.IndexGreaterThan(consumeIndex());
288            case "eq":
289                return new Evaluator.IndexEquals(consumeIndex());
290            case "has":
291                return has();
292            case "is":
293                return is();
294            case "contains":
295                return contains(false);
296            case "containsOwn":
297                return contains(true);
298            case "containsWholeText":
299                return containsWholeText(false);
300            case "containsWholeOwnText":
301                return containsWholeText(true);
302            case "containsData":
303                return containsData();
304            case "matches":
305                return matches(false);
306            case "matchesAttr":
307                return matchesAttr();
308            case "matchesOwn":
309                return matches(true);
310            case "matchesWholeText":
311                return matchesWholeText(false);
312            case "matchesWholeOwnText":
313                return matchesWholeText(true);
314            case "not":
315                return not();
316            case "nth-child":
317                return cssNthChild(false, false);
318            case "nth-last-child":
319                return cssNthChild(true, false);
320            case "nth-of-type":
321                return cssNthChild(false, true);
322            case "nth-last-of-type":
323                return cssNthChild(true, true);
324            case "first-child":
325                return new Evaluator.IsFirstChild();
326            case "last-child":
327                return new Evaluator.IsLastChild();
328            case "first-of-type":
329                return new Evaluator.IsFirstOfType();
330            case "last-of-type":
331                return new Evaluator.IsLastOfType();
332            case "only-child":
333                return new Evaluator.IsOnlyChild();
334            case "only-of-type":
335                return new Evaluator.IsOnlyOfType();
336            case "empty":
337                return new Evaluator.IsEmpty();
338            case "blank":
339                return new NodeEvaluator.BlankValue();
340            case "scope":
341                return new Evaluator.IsScope();
342            case "root":
343                return new Evaluator.IsRoot();
344            case "matchText": {
345                throw new Selector.SelectorParseException(":matchText is no longer supported. Use Element#selectNodes(String, Class) with selector ::text and class TextNode instead."); // todo remove this in 1.25.1
346            }
347            default:
348                throw new Selector.SelectorParseException("Could not parse query '%s': unexpected token at '%s'", query, tq.remainder());
349        }
350    }
351
352    /** Parses a node selector and its subclasses in node-value context. */
353    private Evaluator parseNodeSelector() {
354        consumeStructuralStep();
355        final String pseudo = tq.consumeCssIdentifier();
356        Evaluator nodeType;
357        switch (pseudo) {
358            case "node":
359                nodeType = new NodeEvaluator.InstanceType(Node.class, pseudo);
360                break;
361            case "leafnode":
362                nodeType = new NodeEvaluator.InstanceType(LeafNode.class, pseudo);
363                break;
364            case "text":
365                nodeType = new NodeEvaluator.InstanceType(TextNode.class, pseudo);
366                break;
367            case "comment":
368                nodeType = new NodeEvaluator.InstanceType(Comment.class, pseudo);
369                break;
370            case "data":
371                nodeType = new NodeEvaluator.InstanceType(DataNode.class, pseudo);
372                break;
373            case "cdata":
374                nodeType = new NodeEvaluator.InstanceType(CDataNode.class, pseudo);
375                break;
376            case "pi":
377                nodeType = piSelector();
378                break;
379            default:
380                throw new Selector.SelectorParseException(
381                    "Could not parse query '%s': unknown node type '::%s'", query, pseudo);
382        }
383
384        // nested selectors can enter node context again, e.g. ::comment:not(::text):contains(foo)
385        // restore the caller's context even if parsing a subclass throws
386        boolean previousNodeContext = inNodeContext;
387        inNodeContext = true;
388        try {
389            return parseRemainingSubclasses(nodeType);
390        } finally {
391            inNodeContext = previousNodeContext;
392        }
393    }
394
395    /** Parses an optional processing-instruction target. */
396    private Evaluator piSelector() {
397        if (!tq.matches('('))
398            return new NodeEvaluator.InstanceType(ProcessingInstruction.class, "pi");
399
400        String argument = trimAsciiWhitespace(consumeParens());
401        Validate.notEmpty(argument, "::pi target must not be empty");
402        try (TokenQueue targetQueue = new TokenQueue(argument)) {
403            String target = targetQueue.consumeCssIdentifier();
404            targetQueue.consumeWhitespace();
405            Validate.isTrue(targetQueue.isEmpty(), "::pi target must be a CSS identifier");
406            return new NodeEvaluator.ProcessingInstructionTarget(target);
407        }
408    }
409
410    private Evaluator byId() {
411        String id = tq.consumeCssIdentifier();
412        Validate.notEmpty(id);
413        return new Evaluator.Id(id);
414    }
415
416    private Evaluator byClass() {
417        String className = tq.consumeCssIdentifier();
418        Validate.notEmpty(className);
419        return new Evaluator.Class(className.trim());
420    }
421
422    private Evaluator byTag() {
423        String tagName = tq.consumeElementSelector();
424        Validate.notEmpty(tagName);
425
426        // namespaces:
427        if (tagName.startsWith("*|")) { // namespaces: wildcard match equals(tagName) or ending in ":"+tagName
428            String plainTag = tagName.substring(2); // strip *|
429            return new CombiningEvaluator.Or(
430                new Evaluator.Tag(plainTag),
431                new Evaluator.TagEndsWith(":" + plainTag)
432            );
433        } else if (tagName.endsWith("|*")) { // ns|*
434            String ns = tagName.substring(0, tagName.length() - 2) + ":"; // strip |*, to ns:
435            return new Evaluator.TagStartsWith(ns);
436        } else if (tagName.contains("|")) { // flip "abc|def" to "abc:def"
437            tagName = tagName.replace("|", ":");
438        }
439
440        return new Evaluator.Tag(tagName);
441    }
442
443    private Evaluator byAttribute() {
444        if (!web) return LegacyQueryParser.attribute(tq, query);
445        String attribute = peekCssAttribute(query, tq.pos(), true);
446        try (TokenQueue css = new TokenQueue(attribute)) {
447            Evaluator eval = cssAttribute(css, true);
448            consumeAttribute(tq, attribute);
449            return eval;
450        }
451    }
452
453    /** Reads the contents of {@code [...]} without advancing the query, allowing legacy parsing to retry. */
454    static String peekCssAttribute(String query, int offset, boolean extendedValues) {
455        int start = offset + 1;
456        char quote = 0;
457        int depth = 1;
458        for (int pos = start; pos < query.length(); pos++) {
459            char c = query.charAt(pos);
460            if (c == '\\') {
461                pos++;
462                continue;
463            }
464            if (quote != 0) {
465                if (c == quote) quote = 0;
466            } else if (c == '\'' || c == '"') {
467                quote = c;
468            } else if (extendedValues && c == '[') {
469                depth++; // preserve unquoted values such as http://[::1]/
470            } else if (c == ']' && --depth == 0) {
471                return query.substring(start, pos);
472            }
473        }
474        throw new Selector.SelectorParseException("Did not find balanced marker at '%s'", query.substring(start));
475    }
476
477    static void consumeAttribute(TokenQueue tq, String attribute) {
478        for (int i = 0; i < attribute.length() + 2; i++) tq.advance();
479    }
480
481    /**
482     Parses an attribute selector. With {@code extendedValues} false, unquoted values must be CSS identifiers
483     so legacy values ending in {@code i} or {@code s} are not mistaken for case flags.
484     */
485    static Evaluator cssAttribute(TokenQueue cq, boolean extendedValues) {
486        cq.consumeWhitespace();
487        boolean prefix = cq.matchChomp('^');
488        if (prefix && cq.isEmpty()) return new Evaluator.AttributeStarting("");
489        String key = consumeAttributeName(cq, cq.matchChomp('*') ? "*" : cq.consumeCssIdentifier());
490        cq.consumeWhitespace();
491        if (cq.isEmpty()) return prefix ? new Evaluator.AttributeStarting(key) : key.equals("*") ?
492            new Evaluator.AttributeStarting("") : new Evaluator.Attribute(key);
493        Validate.isFalse(prefix || key.equals("*"), "Attribute value comparison requires an attribute name");
494        AttributeOperator operator = null;
495        for (AttributeOperator candidate : AttributeOperators) {
496            if (cq.matchChomp(candidate.symbol)) { operator = candidate; break; }
497        }
498        Validate.notNull(operator, "Attribute operator expected");
499        AttributeValue operand = consumeAttributeValue(cq, extendedValues);
500        String value = operand.value;
501        CaseSensitivity flag = operand.flag;
502        switch (operator) {
503            case Equals: return new Evaluator.AttributeWithValue(key, value, flag);
504            case NotEquals: return new Evaluator.AttributeWithValueNot(key, value, flag);
505            case StartsWith: return new Evaluator.AttributeWithValueStarting(key, value, flag);
506            case EndsWith: return new Evaluator.AttributeWithValueEnding(key, value, flag);
507            case Contains: return new Evaluator.AttributeWithValueContaining(key, value, flag);
508            case ContainsToken: return new Evaluator.AttributeWithValueContainingToken(key, value, flag);
509            case DashMatch: return new Evaluator.AttributeWithValueDashPrefix(key, value, flag);
510            default: throw new IllegalArgumentException("Unknown attribute operator: " + operator);
511        }
512    }
513
514    private static AttributeValue consumeAttributeValue(TokenQueue cq, boolean extendedValues) {
515        cq.consumeWhitespace();
516        boolean quoted = cq.matches('\'') || cq.matches('"');
517        String value;
518        CaseSensitivity flag = CaseSensitivity.Default;
519        if (extendedValues && !quoted) {
520            StringBuilder out = new StringBuilder();
521            String whitespace = "";
522            while (!cq.isEmpty()) {
523                String part = cq.consumeCssValue(true);
524                StringBuilder nextWhitespace = new StringBuilder();
525                while (cq.matchesWhitespace()) nextWhitespace.append(cq.consume());
526                CaseSensitivity nextFlag = attributeCase(part);
527                if (cq.isEmpty() && out.length() != 0 && nextFlag != CaseSensitivity.Default)
528                    flag = nextFlag;
529                else
530                    out.append(whitespace).append(part);
531                whitespace = nextWhitespace.toString();
532            }
533            value = out.toString();
534        } else {
535            Validate.isFalse(cq.isEmpty(), "Attribute value expected");
536            value = cq.consumeCssValue();
537            boolean separated = cq.consumeWhitespace();
538            if (!cq.isEmpty()) {
539                Validate.isTrue(quoted || separated, "Attribute flag must follow a quoted value or whitespace");
540                String name = cq.consumeCssIdentifier();
541                flag = attributeCase(name);
542                Validate.isTrue(flag != CaseSensitivity.Default, "Unknown attribute case flag: " + asciiLowerCase(name));
543                cq.consumeWhitespace();
544            }
545        }
546        Validate.isTrue(cq.isEmpty(), "Unexpected token after attribute value");
547        return new AttributeValue(value, flag);
548    }
549
550    private static final class AttributeValue {
551        final String value;
552        final CaseSensitivity flag;
553
554        AttributeValue(String value, CaseSensitivity flag) {
555            this.value = value;
556            this.flag = flag;
557        }
558    }
559
560    private static CaseSensitivity attributeCase(String value) {
561        String name = asciiLowerCase(value);
562        return name.equals("i") ? CaseSensitivity.Insensitive :
563            name.equals("s") ? CaseSensitivity.Sensitive : CaseSensitivity.Default;
564    }
565
566    /**
567     Allows attribute names containing a colon, including {@code abs:href}.
568     */
569    private static String consumeAttributeName(TokenQueue cq, String key) {
570        Validate.notEmpty(key, "Attribute name expected");
571        if (cq.matchChomp(':')) {
572            String local = cq.consumeCssIdentifier();
573            Validate.isFalse(key.equals("abs") && local.isEmpty(), "Absolute attribute key must have a name");
574            Validate.notEmpty(local, "Attribute name expected");
575            key += ":" + local;
576        }
577        return key;
578    }
579
580    /**
581     Parses {@code :matchesAttr()}, leaving regex escapes intact.
582     */
583    private Evaluator matchesAttr() {
584        Validate.isTrue(tq.matchChomp('('), ":matchesAttr requires an attribute name and regex");
585        tq.consumeWhitespace();
586        String key = consumeAttributeName(tq, tq.consumeCssIdentifier());
587        tq.consumeWhitespace();
588        Validate.isTrue(tq.matchChomp(','), ":matchesAttr requires an attribute name and regex");
589        tq.consumeWhitespace();
590        String pattern = tq.chompBalancedRemainder('(', ')');
591        Validate.notEmpty(pattern, ":matchesAttr requires a regex");
592        return new Evaluator.AttributeWithValueMatching(key, Regex.compile(pattern));
593    }
594
595    //pseudo selectors :first-child, :last-child, :nth-child, ...
596    private static final Pattern NthStepOffset = Pattern.compile("(([+-])?(\\d+)?)n(\\s*([+-])?\\s*\\d+)?", Pattern.CASE_INSENSITIVE);
597    private static final Pattern NthOffset = Pattern.compile("([+-])?(\\d+)");
598
599    private Evaluator cssNthChild(boolean last, boolean ofType) {
600        String arg = asciiLowerCase(trimAsciiWhitespace(consumeParens())); // arg is like "odd", or "-n+2", within nth-child(odd)
601        final int step, offset;
602        if ("odd".equals(arg)) {
603            step = 2;
604            offset = 1;
605        } else if ("even".equals(arg)) {
606            step = 2;
607            offset = 0;
608        } else {
609            Matcher stepOffsetM, stepM;
610            if ((stepOffsetM = NthStepOffset.matcher(arg)).matches()) {
611                if (stepOffsetM.group(3) != null) // has digits, like 3n+2 or -3n+2
612                    step = Integer.parseInt(stepOffsetM.group(1).replaceFirst("^\\+", ""));
613                else // no digits, might be like n+2, or -n+2. if group(2) == "-", it’s -1;
614                    step = "-".equals(stepOffsetM.group(2)) ? -1 : 1;
615                offset =
616                    stepOffsetM.group(4) != null ? Integer.parseInt(stepOffsetM.group(4).replaceFirst("^\\+", "")) : 0;
617            } else if ((stepM = NthOffset.matcher(arg)).matches()) {
618                step = 0;
619                offset = Integer.parseInt(stepM.group().replaceFirst("^\\+", ""));
620            } else {
621                throw new Selector.SelectorParseException("Could not parse nth-index '%s': unexpected format", arg);
622            }
623        }
624
625        return ofType
626            ? (last ? new Evaluator.IsNthLastOfType(step, offset) : new Evaluator.IsNthOfType(step, offset))
627            : (last ? new Evaluator.IsNthLastChild(step, offset) : new Evaluator.IsNthChild(step, offset));
628    }
629
630    /** Consumes a parenthesized argument, requiring its opening delimiter. */
631    private String consumeParens() {
632        Validate.isTrue(tq.matches('('), "Expected '(' in query '" + query + "'");
633        return tq.chompBalanced('(', ')');
634    }
635
636    private int consumeIndex() {
637        String index = consumeParens().trim();
638        Validate.isTrue(StringUtil.isNumeric(index), "Index must be numeric");
639        return Integer.parseInt(index);
640    }
641
642    /** Parses {@code :has()}, grouping consecutive alternatives that can share a traversal. */
643    private Evaluator has() {
644        String err = ":has() must have a selector";
645        SelectorGroup group = parseNestedSelectorGroup(err);
646        return new HasEvaluator(group.toString(), planTraversals(group));
647    }
648
649    /**
650     Groups alternatives that check the same elements.
651     For example, {@code :has(> a, > span)} checks the children once.
652     */
653    private List<Traversal> planTraversals(SelectorGroup group) {
654        List<Traversal> traversals = new ArrayList<>();
655        ArrayList<Evaluator> alternatives = new ArrayList<>();
656        boolean startsAtSibling = false, descend = false, nextSiblingOnly = false;
657        for (ComplexSelector selector : group.selectors) {
658            char leading = selector.leadingCombinator;
659            boolean siblings = leading == '+' || leading == '~';
660            boolean trees = selector.compounds.size() > 1 || !siblings && leading != '>';
661            boolean nextOnly = leading == '+' && !selector.wantsNodes() &&
662                !selector.combinators.contains('+') && !selector.combinators.contains('~');
663            if (!alternatives.isEmpty() &&
664                (siblings != startsAtSibling || trees != descend || nextOnly != nextSiblingOnly)) {
665                traversals.add(new Traversal(alternatives, startsAtSibling, descend, nextSiblingOnly));
666                alternatives = new ArrayList<>();
667            }
668            alternatives.add(selector.relativeEvaluator());
669            startsAtSibling = siblings;
670            descend = trees;
671            nextSiblingOnly = nextOnly;
672        }
673        traversals.add(new Traversal(alternatives, startsAtSibling, descend, nextSiblingOnly));
674        return traversals;
675    }
676
677    // pseudo selector :is()
678    private Evaluator is() {
679        return parseNested(StructuralEvaluator.Is::new, ":is() must have a selector");
680    }
681
682    /** Parses a nested selector group and applies its compiled evaluator. */
683    private Evaluator parseNested(Function<Evaluator, Evaluator> func, String err) {
684        return func.apply(parseNestedSelectorGroup(err).evaluator());
685    }
686
687    /** Parses a non-empty selector group enclosed in parentheses. */
688    private SelectorGroup parseNestedSelectorGroup(String err) {
689        Validate.isTrue(tq.matchChomp('('), err);
690        tq.consumeWhitespace();
691        Validate.isTrue(!tq.isEmpty() && !tq.matches(')'), err);
692        consumeStructuralStep();
693        SelectorGroup group = parseSelectorGroup();
694        Validate.isTrue(tq.matchChomp(')'), err);
695        return group;
696    }
697
698    /** Bounds parser and evaluator recursion by counting structural steps across the whole query. */
699    private void consumeStructuralStep() {
700        Validate.isTrue(++complexity <= MaxQueryComplexity, "Query complexity exceeds maximum of " + MaxQueryComplexity);
701    }
702
703    // pseudo selector :contains(text), containsOwn(text)
704    private Evaluator contains(boolean own) {
705        String query = own ? ":containsOwn" : ":contains";
706        String searchText = TokenQueue.unescape(consumeParens());
707        Validate.notEmpty(searchText, query + "(text) query must not be empty");
708
709        if (inNodeContext)
710            return new NodeEvaluator.ContainsValue(searchText);
711
712        return own
713            ? new Evaluator.ContainsOwnText(searchText)
714            : new Evaluator.ContainsText(searchText);
715    }
716
717    private Evaluator containsWholeText(boolean own) {
718        String query = own ? ":containsWholeOwnText" : ":containsWholeText";
719        String searchText = TokenQueue.unescape(consumeParens());
720        Validate.notEmpty(searchText, query + "(text) query must not be empty");
721        return own
722            ? new Evaluator.ContainsWholeOwnText(searchText)
723            : new Evaluator.ContainsWholeText(searchText);
724    }
725
726    // pseudo selector :containsData(data)
727    private Evaluator containsData() {
728        String searchText = TokenQueue.unescape(consumeParens());
729        Validate.notEmpty(searchText, ":containsData(text) query must not be empty");
730        return new Evaluator.ContainsData(searchText);
731    }
732
733    // :matches(regex), matchesOwn(regex)
734    private Evaluator matches(boolean own) {
735        String query = own ? ":matchesOwn" : ":matches";
736        Regex pattern = consumeRegex(query);
737
738        if (inNodeContext)
739            return new NodeEvaluator.MatchesValue(pattern);
740
741        return own
742            ? new Evaluator.MatchesOwn(pattern)
743            : new Evaluator.Matches(pattern);
744    }
745
746    // :matchesWholeText(regex), matchesWholeOwnText(regex)
747    private Evaluator matchesWholeText(boolean own) {
748        String query = own ? ":matchesWholeOwnText" : ":matchesWholeText";
749        Regex pattern = consumeRegex(query);
750        return own
751            ? new Evaluator.MatchesWholeOwnText(pattern)
752            : new Evaluator.MatchesWholeText(pattern);
753    }
754
755    /** Consumes and compiles a non-empty regex argument, preserving its escapes. */
756    private Regex consumeRegex(String pseudo) {
757        String regex = consumeParens();
758        Validate.notEmpty(regex, pseudo + "(regex) query must not be empty");
759        return Regex.compile(regex);
760    }
761
762    /** Parses the selector group negated by {@code :not()}. */
763    private Evaluator not() {
764        return parseNested(StructuralEvaluator.Not::new, ":not(selector) subselect must not be empty");
765    }
766
767    @Override
768    public String toString() {
769        return query;
770    }
771
772    @Override
773    public void close() {
774        tq.close();
775    }
776}