001package org.jsoup.select;
002
003import org.jsoup.helper.Validate;
004import org.jsoup.internal.LegacyMatching;
005import org.jsoup.internal.CssMatching;
006import org.jsoup.internal.CssMatching.AttributeOperator;
007import org.jsoup.internal.CssMatching.CaseSensitivity;
008import org.jsoup.internal.QueryMode;
009import org.jsoup.nodes.Comment;
010import org.jsoup.nodes.Document;
011import org.jsoup.nodes.DocumentType;
012import org.jsoup.nodes.Element;
013import org.jsoup.nodes.LeafNode;
014import org.jsoup.nodes.Node;
015import org.jsoup.nodes.ProcessingInstruction;
016import org.jsoup.nodes.TextNode;
017import org.jsoup.nodes.XmlDeclaration;
018import org.jsoup.helper.Regex;
019import org.jsoup.parser.TokenQueue;
020import org.jspecify.annotations.Nullable;
021
022import java.util.List;
023import java.util.function.Predicate;
024import java.util.function.Supplier;
025import java.util.regex.Pattern;
026
027import static org.jsoup.internal.Normalizer.lowerCase;
028import static org.jsoup.internal.Normalizer.asciiLowerCase;
029import static org.jsoup.internal.StringUtil.trimAsciiWhitespace;
030import static org.jsoup.internal.StringUtil.normaliseWhitespace;
031import static org.jsoup.internal.StringUtil.isWhitespace;
032
033
034/**
035 An Evaluator tests if an element (or a node) meets the selector's requirements. Obtain an evaluator for a given CSS selector
036 with {@link Selector#evaluatorOf(String css)}. If you are executing the same selector on many elements (or documents), it
037 can be more efficient to compile and reuse an Evaluator than to reparse the selector on each invocation of select().
038 <p>Each evaluator captures the behavior selected by {@code jsoup.queryMode} when constructed.
039 Standalone evaluators use legacy behavior in {@code check} mode; they do not report compatibility differences.</p>
040 <p>If you construct and combine evaluators yourself, each child keeps the matching rules selected when it was created.
041 The outer evaluator determines whether searches include the starting element and which element supplies the query context.</p>
042 <p>Evaluators are thread-safe and may be used concurrently across multiple documents.</p>
043
044 @see <a href="{@docRoot}/org/jsoup/select/Selector.html#query-compatibility">Query compatibility changes</a> */
045public abstract class Evaluator {
046    final boolean web; // web query semantics or legacy
047
048    private static final ThreadLocal<@Nullable Boolean> QueryModeOverride = new ThreadLocal<>();
049
050    /**
051     Sets the mode for evaluators created by the action, then restores the previous mode.
052     */
053    static <T> T withMode(boolean web, Supplier<T> action) {
054        Boolean previous = QueryModeOverride.get();
055        QueryModeOverride.set(web);
056        try {
057            return action.get();
058        } finally {
059            if (previous == null) QueryModeOverride.remove();
060            else QueryModeOverride.set(previous);
061        }
062    }
063
064    /**
065     Returns the supplied element. If it is a Document, returns its first element child instead,
066     or null if it has no element children.
067     */
068    static @Nullable Element documentElementOrSelf(Element element) {
069        return element instanceof Document ? element.firstElementChild() : element;
070    }
071
072    protected Evaluator() {
073        Boolean web = QueryModeOverride.get();
074        this.web = web != null ? web : QueryMode.current() == QueryMode.Web;
075    }
076
077    /**
078     Provides a Predicate for this Evaluator, matching the test Element.
079     * @param root the element the query starts from; see {@link #matches(Element, Element)}
080     * @return a predicate that accepts an Element to test for matches with this Evaluator
081     * @since 1.17.1
082     */
083    public Predicate<Element> asPredicate(Element root) {
084        return element -> matches(root, element);
085    }
086
087    Predicate<Node> asNodePredicate(Element root) {
088        return node -> matches(root, node);
089    }
090
091    /**
092     Tests whether the element matches this evaluator.
093
094     @param root    the query context, used by {@code :scope}; for a Document, {@code :scope} matches its document element
095     @param element the element to test
096     @return whether the element matches
097     */
098    public abstract boolean matches(Element root, Element element);
099
100    /**
101     Tests a {@code :has()} candidate starting at {@code anchor}, keeping {@code root} as the query context.
102     The caller has already checked whether the query accepts this node type.
103     */
104    boolean matchesRelative(Element root, Node node, Element anchor) {
105        return node instanceof Element ? matches(root, (Element) node) : matches(root, (LeafNode) node);
106    }
107
108    /** Checks node eligibility before relative matching; a null anchor uses ordinary matching. */
109    final boolean matches(Element root, Node node, @Nullable Element anchor) {
110        if (anchor == null) return matches(root, node);
111        if (node instanceof Element || node instanceof LeafNode && wantsNodes())
112            return matchesRelative(root, node, anchor);
113        return false;
114    }
115
116    final boolean matches(Element root, Node node) {
117        if (node instanceof Element) {
118            return matches(root, (Element) node);
119        } else if (node instanceof LeafNode && wantsNodes()) {
120            return matches(root, (LeafNode) node);
121        }
122        return false;
123    }
124
125    boolean matches(Element root, LeafNode leafNode) {
126        return false;
127    }
128
129    boolean wantsNodes() {
130        return false;
131    }
132
133    /**
134     Reset any internal state in this Evaluator before executing a new Collector evaluation.
135     */
136    protected void reset() {
137    }
138
139    final boolean sameType(Node first, Element second) {
140        return web ? CssMatching.sameType(first, second) : first.normalName().equals(second.normalName());
141    }
142
143    /**
144     A relative evaluator cost function. During evaluation, Evaluators are sorted by ascending cost as an optimization.
145     * @return the relative cost of this Evaluator
146     */
147    protected int cost() {
148        return 5; // a nominal default cost
149    }
150
151    /**
152     * Evaluator for tag name
153     */
154    public static final class Tag extends Evaluator {
155        private final String tagName;
156
157        public Tag(String tagName) {
158            this.tagName = web ? tagName : asciiLowerCase(tagName);
159        }
160
161        @Override
162        public boolean matches(Element root, Element element) {
163            if (!web) return element.nameIs(tagName);
164            return !CssMatching.isHtmlElement(root, element) ? element.tagName().equals(tagName) :
165                element.nameIs(asciiLowerCase(tagName));
166        }
167
168        @Override protected int cost() {
169            return 1;
170        }
171
172        @Override
173        public String toString() {
174            return String.format("%s", tagName);
175        }
176    }
177
178    /**
179     * Evaluator for tag name that starts with prefix; used for ns|*
180     */
181    public static final class TagStartsWith extends Evaluator {
182        private final String tagName;
183
184        public TagStartsWith(String tagName) {
185            this.tagName = tagName;
186        }
187
188        @Override
189        public boolean matches(Element root, Element element) {
190            boolean insensitive = !web || CssMatching.isHtmlElement(root, element);
191            String name = insensitive ? element.normalName() : element.tagName();
192            return name.startsWith(insensitive ? asciiLowerCase(tagName) : tagName);
193        }
194
195        @Override
196        public String toString() {
197            return String.format("%s|*", tagName);
198        }
199    }
200
201
202    /**
203     * Evaluator for tag name that ends with suffix; used for *|el
204     */
205    public static final class TagEndsWith extends Evaluator {
206        private final String tagName;
207
208        public TagEndsWith(String tagName) {
209            this.tagName = tagName;
210        }
211
212        @Override
213        public boolean matches(Element root, Element element) {
214            boolean insensitive = !web || CssMatching.isHtmlElement(root, element);
215            String name = insensitive ? element.normalName() : element.tagName();
216            return name.endsWith(insensitive ? asciiLowerCase(tagName) : tagName);
217        }
218
219        @Override
220        public String toString() {
221            return String.format("*|%s", tagName);
222        }
223    }
224
225    /**
226     * Evaluator for element id
227     */
228    public static final class Id extends Evaluator {
229        private final String id;
230
231        public Id(String id) {
232            this.id = id;
233        }
234
235        @Override
236        public boolean matches(Element root, Element element) {
237            if (!web) return id.equals(element.id());
238            String actual = CssMatching.attributeValue(element, "id", CssMatching.isHtmlElement(root, element));
239            return actual != null && (CssMatching.isQuirksMode(root) ? org.jsoup.internal.Normalizer.equalsIgnoreAsciiCase(actual, id) : actual.equals(id));
240        }
241
242        @Override protected int cost() {
243            return 2;
244        }
245        @Override
246        public String toString() {
247            return String.format("#%s", id);
248        }
249    }
250
251    /**
252     * Evaluator for element class
253     */
254    public static final class Class extends Evaluator {
255        private final String className;
256
257        public Class(String className) {
258            this.className = className;
259        }
260
261        @Override
262        public boolean matches(Element root, Element element) {
263            if (!web) return LegacyMatching.hasClass(element, className);
264            return CssMatching.hasClass(root, element, className);
265        }
266
267        @Override protected int cost() {
268            return 8; // does whitespace scanning; more than .contains()
269        }
270
271        @Override
272        public String toString() {
273            return String.format(".%s", className);
274        }
275
276    }
277
278    /** Shares attribute matching between elements and processing instructions. */
279    abstract static class AttributeEvaluator extends Evaluator {
280        @Override public boolean matches(Element root, Element element) {
281            return matchesNode(root, element);
282        }
283
284        @Override boolean matches(Element root, LeafNode leafNode) {
285            return leafNode instanceof ProcessingInstruction && matchesNode(root, leafNode);
286        }
287
288        /** Tests this evaluator against a node's attributes. */
289        boolean matchesNode(Element root, Node node) {
290            return false;
291        }
292
293        @Nullable String attribute(Element root, Node node, String key) {
294            return web ? CssMatching.attributeValue(node, key, CssMatching.isHtmlElement(root, node)) :
295                LegacyMatching.attribute(node, key);
296        }
297
298        boolean matchesValue(Element root, Node node, String key, String value, AttributeOperator operator,
299                             CaseSensitivity flag) {
300            boolean ignoreCase = flag == CaseSensitivity.Insensitive || flag == CaseSensitivity.Default &&
301                CssMatching.isHtmlElement(root, node) && CssMatching.isCaseInsensitiveAttribute(key);
302            return CssMatching.matchesAttributeValue(attribute(root, node, key), value, operator, ignoreCase);
303        }
304    }
305
306    /**
307     * Evaluator for attribute name matching
308     */
309    public static final class Attribute extends AttributeEvaluator {
310        private final String key;
311
312        public Attribute(String key) {
313            this.key = key;
314        }
315
316        @Override boolean matchesNode(Element root, Node node) {
317            return web ? attribute(root, node, key) != null : node.hasAttr(asciiLowerCase(key));
318        }
319
320        @Override protected int cost() {
321            return 2;
322        }
323
324        @Override
325        public String toString() {
326            return String.format("[%s]", key);
327        }
328    }
329
330    /**
331     * Evaluator for attribute name prefix matching
332     */
333    public static final class AttributeStarting extends AttributeEvaluator {
334        private final String keyPrefix;
335
336        public AttributeStarting(String keyPrefix) {
337            Validate.notNull(keyPrefix); // OK to be empty - will find elements with any attributes
338            this.keyPrefix = keyPrefix;
339        }
340
341        @Override boolean matchesNode(Element root, Node node) {
342            List<org.jsoup.nodes.Attribute> values = node.attributes().asList();
343            for (org.jsoup.nodes.Attribute attribute : values) {
344                boolean insensitive = !web || CssMatching.isHtmlElement(root, node) && attribute.namespace().isEmpty();
345                String name = insensitive ? asciiLowerCase(attribute.getKey()) : attribute.getKey();
346                if (name.startsWith(insensitive ? asciiLowerCase(keyPrefix) : keyPrefix))
347                    return true;
348            }
349            return false;
350        }
351
352        @Override protected int cost() {
353            return 6;
354        }
355
356        @Override
357        public String toString() {
358            return String.format("[^%s]", keyPrefix);
359        }
360
361    }
362
363    /**
364     * Evaluator for attribute name/value matching
365     */
366    public static final class AttributeWithValue extends AttributeKeyPair {
367        public AttributeWithValue(String key, String value) {
368            super(key, value);
369        }
370
371        AttributeWithValue(String key, String value, CaseSensitivity flag) {
372            super(key, value, flag);
373        }
374
375        @Override boolean matchesNode(Element root, Node node) {
376            return matchesValue(root, node, AttributeOperator.Equals);
377        }
378
379        @Override protected int cost() {
380            return 3;
381        }
382
383        @Override
384        public String toString() {
385            return attributeString("=");
386        }
387
388    }
389
390    /**
391     * Evaluator for attribute name != value matching
392     */
393    public static final class AttributeWithValueNot extends AttributeKeyPair {
394        public AttributeWithValueNot(String key, String value) {
395            super(key, value);
396        }
397
398        AttributeWithValueNot(String key, String value, CaseSensitivity flag) {
399            super(key, value, flag);
400        }
401
402        @Override boolean matchesNode(Element root, Node node) {
403            return matchesValue(root, node, AttributeOperator.NotEquals);
404        }
405
406        @Override protected int cost() {
407            return 3;
408        }
409
410        @Override
411        public String toString() {
412            return attributeString("!=");
413        }
414
415    }
416
417    /**
418     * Evaluator for attribute name/value matching (value prefix)
419     */
420    public static final class AttributeWithValueStarting extends AttributeKeyPair {
421        public AttributeWithValueStarting(String key, String value) {
422            super(key, value);
423        }
424
425        AttributeWithValueStarting(String key, String value, CaseSensitivity flag) {
426            super(key, value, flag);
427        }
428
429        @Override boolean matchesNode(Element root, Node node) {
430            return matchesValue(root, node, AttributeOperator.StartsWith);
431        }
432
433        @Override protected int cost() {
434            return 4;
435        }
436
437        @Override
438        public String toString() {
439            return attributeString("^=");
440        }
441    }
442
443    /**
444     * Evaluator for attribute name/value matching (value ending)
445     */
446    public static final class AttributeWithValueEnding extends AttributeKeyPair {
447        public AttributeWithValueEnding(String key, String value) {
448            super(key, value);
449        }
450
451        AttributeWithValueEnding(String key, String value, CaseSensitivity flag) {
452            super(key, value, flag);
453        }
454
455        @Override boolean matchesNode(Element root, Node node) {
456            return matchesValue(root, node, AttributeOperator.EndsWith);
457        }
458
459        @Override protected int cost() {
460            return 4;
461        }
462
463        @Override
464        public String toString() {
465            return attributeString("$=");
466        }
467    }
468
469    /**
470     * Evaluator for attribute name/value matching (value containing)
471     */
472    public static final class AttributeWithValueContaining extends AttributeKeyPair {
473        public AttributeWithValueContaining(String key, String value) {
474            super(key, value);
475        }
476
477        AttributeWithValueContaining(String key, String value, CaseSensitivity flag) {
478            super(key, value, flag);
479        }
480
481        @Override boolean matchesNode(Element root, Node node) {
482            return matchesValue(root, node, AttributeOperator.Contains);
483        }
484
485        @Override protected int cost() {
486            return 6;
487        }
488
489        @Override
490        public String toString() {
491            return attributeString("*=");
492        }
493
494    }
495
496    /** Matches a complete attribute token, as in [class~=card]. */
497    static final class AttributeWithValueContainingToken extends AttributeKeyPair {
498        AttributeWithValueContainingToken(String key, String value, CaseSensitivity flag) {
499            super(key, value, flag);
500        }
501
502        @Override boolean matchesNode(Element root, Node node) {
503            return matchesValue(root, node, AttributeOperator.ContainsToken);
504        }
505
506        @Override protected int cost() {
507            return 8;
508        }
509
510        @Override public String toString() {
511            return attributeString("~=");
512        }
513    }
514
515    /** Matches an attribute value or its hyphen-separated prefix, as in [lang|=en]. */
516    static final class AttributeWithValueDashPrefix extends AttributeKeyPair {
517        AttributeWithValueDashPrefix(String key, String value, CaseSensitivity flag) {
518            super(key, value, flag);
519        }
520
521        @Override boolean matchesNode(Element root, Node node) {
522            return matchesValue(root, node, AttributeOperator.DashMatch);
523        }
524
525        @Override protected int cost() {
526            return 4;
527        }
528
529        @Override public String toString() {
530            return attributeString("|=");
531        }
532    }
533
534    /**
535     * Evaluator for attribute name/value matching (value regex matching)
536     */
537    public static final class AttributeWithValueMatching extends AttributeEvaluator {
538        final String key;
539        final Regex pattern;
540
541        public AttributeWithValueMatching(String key, Regex pattern) {
542            this.key = trimAsciiWhitespace(key); // preserve key case in serialized queries
543            this.pattern = pattern;
544        }
545
546        public AttributeWithValueMatching(String key, Pattern pattern) {
547            this(key, Regex.fromPattern(pattern));
548        }
549
550        @Override boolean matchesNode(Element root, Node node) {
551            String value = attribute(root, node, key);
552            return value != null && pattern.matcher(value).find();
553        }
554
555        @Override protected int cost() {
556            return 8;
557        }
558
559        @Override
560        public String toString() {
561            if (!web) return String.format("[%s~=%s]", key, pattern);
562            String regex = pattern.toString();
563            // protect leading regex whitespace from the selector's whitespace after the comma
564            if (!regex.isEmpty() && isWhitespace(regex.charAt(0))) regex = "(?:)" + regex;
565            return String.format(":matchesAttr(%s, %s)", TokenQueue.escapeCssIdentifier(key), regex);
566        }
567
568    }
569
570    /**
571     * Abstract evaluator for attribute name/value matching
572     */
573    public abstract static class AttributeKeyPair extends AttributeEvaluator {
574        final String key;
575        final String value;
576        final CaseSensitivity flag;
577        private final boolean parsedCss; // parsed operands are already decoded; public inputs retain quote handling
578        private final char quote; // retains input quoting when unquoted output could look like a case flag
579
580        public AttributeKeyPair(String key, String value) {
581            Validate.notEmpty(key);
582            Validate.notNull(value);
583
584            key = trimAsciiWhitespace(key);
585            this.key = web ? key : asciiLowerCase(key);
586            boolean quoted = value.startsWith("'") && value.endsWith("'")
587                || value.startsWith("\"") && value.endsWith("\"");
588            quote = quoted ? value.charAt(0) : 0;
589            if (quoted) {
590                Validate.isTrue(value.length() > 1, "Quoted value must have content");
591                value = value.substring(1, value.length() - 1);
592            }
593
594            this.value = web ? value : lowerCase(value);
595            flag = CaseSensitivity.Default;
596            parsedCss = false;
597        }
598
599        AttributeKeyPair(String key, String value, CaseSensitivity flag) {
600            this.key = key;
601            this.value = value;
602            this.flag = flag;
603            parsedCss = true;
604            quote = 0;
605        }
606
607        boolean matchesValue(Element root, Node node, AttributeOperator operator) {
608            if (web) return super.matchesValue(root, node, key, value, operator, flag);
609            return parsedCss ? LegacyMatching.cssAttributeMatches(node, key, value, operator, flag) :
610                LegacyMatching.attributeMatches(node, key, value, operator);
611        }
612
613        String attributeString(String operator) {
614            if (!parsedCss && !web) {
615                return LegacyMatching.attributeString(key, value, operator, quote);
616            }
617            String cssValue = value.isEmpty() ? "\"\"" : TokenQueue.escapeCssIdentifier(value);
618            String suffix = flag == CaseSensitivity.Insensitive ? " i" : flag == CaseSensitivity.Sensitive ? " s" : "";
619            return String.format("[%s%s%s%s]", TokenQueue.escapeCssIdentifier(key), operator, cssValue, suffix);
620        }
621    }
622
623    /**
624     * Evaluator for any / all element matching
625     */
626    public static final class AllElements extends Evaluator {
627
628        @Override
629        public boolean matches(Element root, Element element) {
630            return true;
631        }
632
633        @Override protected int cost() {
634            return 10;
635        }
636
637        @Override
638        public String toString() {
639            return "*";
640        }
641    }
642
643    /**
644     * Evaluator for matching by sibling index number (e {@literal <} idx)
645     */
646    public static final class IndexLessThan extends IndexEvaluator {
647        public IndexLessThan(int index) {
648            super(index);
649        }
650
651        @Override
652        public boolean matches(Element root, Element element) {
653            return (web || root != element) && element.elementSiblingIndex() < index;
654        }
655
656        @Override
657        public String toString() {
658            return String.format(":lt(%d)", index);
659        }
660
661    }
662
663    /**
664     * Evaluator for matching by sibling index number (e {@literal >} idx)
665     */
666    public static final class IndexGreaterThan extends IndexEvaluator {
667        public IndexGreaterThan(int index) {
668            super(index);
669        }
670
671        @Override
672        public boolean matches(Element root, Element element) {
673            return element.elementSiblingIndex() > index;
674        }
675
676        @Override
677        public String toString() {
678            return String.format(":gt(%d)", index);
679        }
680
681    }
682
683    /**
684     * Evaluator for matching by sibling index number (e = idx)
685     */
686    public static final class IndexEquals extends IndexEvaluator {
687        public IndexEquals(int index) {
688            super(index);
689        }
690
691        @Override
692        public boolean matches(Element root, Element element) {
693            return element.elementSiblingIndex() == index;
694        }
695
696        @Override
697        public String toString() {
698            return String.format(":eq(%d)", index);
699        }
700
701    }
702
703    /**
704     * Evaluator for matching the last sibling (css :last-child)
705     */
706    public static final class IsLastChild extends Evaluator {
707                @Override
708                public boolean matches(Element root, Element element) {
709                        final Element p = element.parent();
710                        return p != null && !(p instanceof Document) && element == p.lastElementChild();
711                }
712
713                @Override
714                public String toString() {
715                        return ":last-child";
716                }
717    }
718
719    public static final class IsFirstOfType extends IsNthOfType {
720        public IsFirstOfType() {
721            super(0, 1);
722        }
723
724                @Override
725                public String toString() {
726                        return ":first-of-type";
727                }
728    }
729
730    public static final class IsLastOfType extends IsNthLastOfType {
731        public IsLastOfType() {
732            super(0, 1);
733        }
734
735                @Override
736                public String toString() {
737                        return ":last-of-type";
738                }
739    }
740
741
742    public static abstract class CssNthEvaluator extends Evaluator {
743        /** Step */
744        protected final int a;
745        /** Offset */
746        protected final int b;
747
748        public CssNthEvaluator(int step, int offset) {
749            this.a = step;
750            this.b = offset;
751        }
752
753        public CssNthEvaluator(int offset) {
754            this(0, offset);
755        }
756
757        @Override
758        public boolean matches(Element root, Element element) {
759            final Element p = element.parent();
760            if (p == null || (p instanceof Document)) return false;
761
762            final int pos = calculatePosition(root, element);
763            if (a == 0) return pos == b;
764
765            return (pos - b) * a >= 0 && (pos - b) % a == 0;
766        }
767
768        @Override
769        public String toString() {
770            String format =
771                (a == 0) ? ":%s(%3$d)"    // only offset (b)
772                : (b == 0) ? ":%s(%2$dn)" // only step (a)
773                : ":%s(%2$dn%3$+d)";      // step, offset
774            return String.format(format, getPseudoClass(), a, b);
775        }
776
777        protected abstract String getPseudoClass();
778
779        protected abstract int calculatePosition(Element root, Element element);
780    }
781
782
783    /**
784     * css-compatible Evaluator for :eq (css :nth-child)
785     *
786     * @see IndexEquals
787     */
788    public static final class IsNthChild extends CssNthEvaluator {
789        public IsNthChild(int step, int offset) {
790            super(step, offset);
791        }
792
793        @Override
794        protected int calculatePosition(Element root, Element element) {
795            return element.elementSiblingIndex() + 1;
796        }
797
798        @Override
799        protected String getPseudoClass() {
800            return "nth-child";
801        }
802    }
803
804    /**
805     * css pseudo class :nth-last-child)
806     *
807     * @see IndexEquals
808     */
809    public static final class IsNthLastChild extends CssNthEvaluator {
810        public IsNthLastChild(int step, int offset) {
811            super(step, offset);
812        }
813
814        @Override
815        protected int calculatePosition(Element root, Element element) {
816            if (element.parent() == null) return 0;
817                return element.parent().childrenSize() - element.elementSiblingIndex();
818        }
819
820                @Override
821                protected String getPseudoClass() {
822                        return "nth-last-child";
823                }
824    }
825
826    /**
827     * css pseudo class nth-of-type
828     *
829     */
830    public static class IsNthOfType extends CssNthEvaluator {
831        public IsNthOfType(int step, int offset) {
832            super(step, offset);
833        }
834
835        @Override protected int calculatePosition(Element root, Element element) {
836            Element parent = element.parent();
837            if (parent == null)
838                return 0;
839
840            int pos = 0;
841            final int size = parent.childNodeSize();
842            for (int i = 0; i < size; i++) {
843                Node node = parent.childNode(i);
844                if (sameType(node, element)) pos++;
845                if (node == element) break;
846            }
847            return pos;
848        }
849
850        @Override
851        protected String getPseudoClass() {
852            return "nth-of-type";
853        }
854    }
855
856    public static class IsNthLastOfType extends CssNthEvaluator {
857        public IsNthLastOfType(int step, int offset) {
858            super(step, offset);
859        }
860
861        @Override
862        protected int calculatePosition(Element root, Element element) {
863            Element parent = element.parent();
864            if (parent == null)
865                return 0;
866
867            int pos = 0;
868            Element next = element;
869            while (next != null) {
870                if (sameType(next, element))
871                    pos++;
872                next = next.nextElementSibling();
873            }
874            return pos;
875        }
876
877        @Override
878        protected String getPseudoClass() {
879            return "nth-last-of-type";
880        }
881    }
882
883    /**
884     * Evaluator for matching the first sibling (css :first-child)
885     */
886    public static final class IsFirstChild extends Evaluator {
887        @Override
888        public boolean matches(Element root, Element element) {
889                final Element p = element.parent();
890                return p != null && !(p instanceof Document) && element == p.firstElementChild();
891        }
892
893        @Override
894        public String toString() {
895                return ":first-child";
896        }
897    }
898
899    /**
900     * css3 pseudo-class :root
901     * @see <a href="http://www.w3.org/TR/selectors/#root-pseudo">:root selector</a>
902     *
903     */
904    public static final class IsRoot extends Evaluator {
905        @Override
906        public boolean matches(Element root, Element element) {
907            return element == documentElementOrSelf(root.root());
908        }
909
910        @Override protected int cost() {
911            return 1;
912        }
913
914        @Override
915        public String toString() {
916                return ":root";
917        }
918    }
919
920    /**
921     Matches the original query scope, including within relative selectors.
922     */
923    static final class IsScope extends Evaluator {
924        @Override public boolean matches(Element root, Element element) {
925            return element == documentElementOrSelf(root);
926        }
927
928        @Override protected int cost() {
929            return 1;
930        }
931
932        @Override public String toString() {
933            return ":scope";
934        }
935    }
936
937    public static final class IsOnlyChild extends Evaluator {
938                @Override
939                public boolean matches(Element root, Element element) {
940                        final Element p = element.parent();
941                        return p!=null && !(p instanceof Document) && element.siblingElements().isEmpty();
942                }
943        @Override
944        public String toString() {
945                return ":only-child";
946        }
947    }
948
949    public static final class IsOnlyOfType extends Evaluator {
950                @Override
951                public boolean matches(Element root, Element element) {
952                        final Element p = element.parent();
953                        if (p==null || p instanceof Document) return false;
954
955                        int pos = 0;
956            Element next = p.firstElementChild();
957            while (next != null) {
958                if (sameType(next, element))
959                    pos++;
960                if (pos > 1)
961                    break;
962                next = next.nextElementSibling();
963            }
964                return pos == 1;
965                }
966        @Override
967        public String toString() {
968                return ":only-of-type";
969        }
970    }
971
972    public static final class IsEmpty extends Evaluator {
973        @Override
974        public boolean matches(Element root, Element el) {
975            for (Node n = el.firstChild(); n != null; n = n.nextSibling()) {
976                if (n instanceof TextNode) {
977                    if (!((TextNode) n).isBlank())
978                        return false; // non-blank text: not empty
979                } else if (!(n instanceof Comment || n instanceof ProcessingInstruction ||
980                    n instanceof XmlDeclaration || n instanceof DocumentType))
981                    return false; // non "blank" element: not empty
982            }
983            return true;
984        }
985
986        @Override
987        public String toString() {
988            return ":empty";
989        }
990    }
991
992    /**
993     * Abstract evaluator for sibling index matching
994     *
995     * @author ant
996     */
997    public abstract static class IndexEvaluator extends Evaluator {
998        final int index;
999
1000        public IndexEvaluator(int index) {
1001            this.index = index;
1002        }
1003    }
1004
1005    /**
1006     * Evaluator for matching Element (and its descendants) text
1007     */
1008    public static final class ContainsText extends Evaluator {
1009        private final String searchText;
1010
1011        public ContainsText(String searchText) {
1012            this.searchText = lowerCase(normaliseWhitespace(searchText));
1013        }
1014
1015        @Override
1016        public boolean matches(Element root, Element element) {
1017            return lowerCase(element.text()).contains(searchText);
1018        }
1019
1020        @Override protected int cost() {
1021            return 10;
1022        }
1023
1024        @Override
1025        public String toString() {
1026            return String.format(":contains(%s)", searchText);
1027        }
1028    }
1029
1030    /**
1031     * Evaluator for matching Element (and its descendants) wholeText. Neither the input nor the element text is
1032     * normalized. <code>:containsWholeText()</code>
1033     * @since 1.15.1.
1034     */
1035    public static final class ContainsWholeText extends Evaluator {
1036        private final String searchText;
1037
1038        public ContainsWholeText(String searchText) {
1039            this.searchText = searchText;
1040        }
1041
1042        @Override
1043        public boolean matches(Element root, Element element) {
1044            return element.wholeText().contains(searchText);
1045        }
1046
1047        @Override protected int cost() {
1048            return 10;
1049        }
1050
1051        @Override
1052        public String toString() {
1053            return String.format(":containsWholeText(%s)", searchText);
1054        }
1055    }
1056
1057    /**
1058     * Evaluator for matching Element (but <b>not</b> its descendants) wholeText. Neither the input nor the element text is
1059     * normalized. <code>:containsWholeOwnText()</code>
1060     * @since 1.15.1.
1061     */
1062    public static final class ContainsWholeOwnText extends Evaluator {
1063        private final String searchText;
1064
1065        public ContainsWholeOwnText(String searchText) {
1066            this.searchText = searchText;
1067        }
1068
1069        @Override
1070        public boolean matches(Element root, Element element) {
1071            return element.wholeOwnText().contains(searchText);
1072        }
1073
1074        @Override
1075        public String toString() {
1076            return String.format(":containsWholeOwnText(%s)", searchText);
1077        }
1078    }
1079
1080    /**
1081     * Evaluator for matching Element (and its descendants) data
1082     */
1083    public static final class ContainsData extends Evaluator {
1084        private final String searchText;
1085
1086        public ContainsData(String searchText) {
1087            this.searchText = lowerCase(searchText);
1088        }
1089
1090        @Override
1091        public boolean matches(Element root, Element element) {
1092            return lowerCase(element.data()).contains(searchText); // not whitespace normalized
1093        }
1094
1095        @Override
1096        public String toString() {
1097            return String.format(":containsData(%s)", searchText);
1098        }
1099    }
1100
1101    /**
1102     * Evaluator for matching Element's own text
1103     */
1104    public static final class ContainsOwnText extends Evaluator {
1105        private final String searchText;
1106
1107        public ContainsOwnText(String searchText) {
1108            this.searchText = lowerCase(normaliseWhitespace(searchText));
1109        }
1110
1111        @Override
1112        public boolean matches(Element root, Element element) {
1113            return lowerCase(element.ownText()).contains(searchText);
1114        }
1115
1116        @Override
1117        public String toString() {
1118            return String.format(":containsOwn(%s)", searchText);
1119        }
1120    }
1121
1122    /**
1123     * Evaluator for matching Element (and its descendants) text with regex
1124     */
1125    public static final class Matches extends Evaluator {
1126        private final Regex pattern;
1127
1128        public Matches(Regex pattern) {
1129            this.pattern = pattern;
1130        }
1131
1132        public Matches(Pattern pattern) {
1133            this(Regex.fromPattern(pattern));
1134        }
1135
1136        @Override
1137        public boolean matches(Element root, Element element) {
1138            return pattern.matcher(element.text()).find();
1139        }
1140
1141        @Override protected int cost() {
1142            return 8;
1143        }
1144
1145        @Override
1146        public String toString() {
1147            return String.format(":matches(%s)", pattern);
1148        }
1149    }
1150
1151    /**
1152     * Evaluator for matching Element's own text with regex
1153     */
1154    public static final class MatchesOwn extends Evaluator {
1155        private final Regex pattern;
1156
1157        public MatchesOwn(Regex pattern) {
1158            this.pattern = pattern;
1159        }
1160
1161        public MatchesOwn(Pattern pattern) {
1162            this(Regex.fromPattern(pattern));
1163        }
1164
1165        @Override
1166        public boolean matches(Element root, Element element) {
1167            return pattern.matcher(element.ownText()).find();
1168        }
1169
1170        @Override protected int cost() {
1171            return 7;
1172        }
1173
1174        @Override
1175        public String toString() {
1176            return String.format(":matchesOwn(%s)", pattern);
1177        }
1178    }
1179
1180    /**
1181     * Evaluator for matching Element (and its descendants) whole text with regex.
1182     * @since 1.15.1.
1183     */
1184    public static final class MatchesWholeText extends Evaluator {
1185        private final Regex pattern;
1186
1187        public MatchesWholeText(Regex pattern) {
1188            this.pattern = pattern;
1189        }
1190
1191        public MatchesWholeText(Pattern pattern) {
1192            this(Regex.fromPattern(pattern));
1193        }
1194
1195        @Override
1196        public boolean matches(Element root, Element element) {
1197            return pattern.matcher(element.wholeText()).find();
1198        }
1199
1200        @Override protected int cost() {
1201            return 8;
1202        }
1203
1204        @Override
1205        public String toString() {
1206            return String.format(":matchesWholeText(%s)", pattern);
1207        }
1208    }
1209
1210    /**
1211     * Evaluator for matching Element's own whole text with regex.
1212     * @since 1.15.1.
1213     */
1214    public static final class MatchesWholeOwnText extends Evaluator {
1215        private final Regex pattern;
1216
1217        public MatchesWholeOwnText(Regex pattern) {
1218            this.pattern = pattern;
1219        }
1220
1221        public MatchesWholeOwnText(Pattern pattern) {
1222            this(Regex.fromPattern(pattern));
1223        }
1224
1225        @Override
1226        public boolean matches(Element root, Element element) {
1227            Regex.Matcher m = pattern.matcher(element.wholeOwnText());
1228            return m.find();
1229        }
1230
1231        @Override protected int cost() {
1232            return 7;
1233        }
1234
1235        @Override
1236        public String toString() {
1237            return String.format(":matchesWholeOwnText(%s)", pattern);
1238        }
1239    }
1240}