001package org.jsoup.select;
002
003import org.jsoup.helper.Validate;
004import org.jsoup.nodes.Comment;
005import org.jsoup.nodes.Document;
006import org.jsoup.nodes.DocumentType;
007import org.jsoup.nodes.Element;
008import org.jsoup.nodes.LeafNode;
009import org.jsoup.nodes.Node;
010import org.jsoup.nodes.TextNode;
011import org.jsoup.nodes.XmlDeclaration;
012import org.jsoup.parser.ParseSettings;
013import org.jsoup.helper.Regex;
014
015import java.util.List;
016import java.util.function.Predicate;
017import java.util.regex.Pattern;
018
019import static org.jsoup.internal.Normalizer.lowerCase;
020import static org.jsoup.internal.Normalizer.normalize;
021import static org.jsoup.internal.StringUtil.normaliseWhitespace;
022
023
024/**
025 An Evaluator tests if an element (or a node) meets the selector's requirements. Obtain an evaluator for a given CSS selector
026 with {@link Selector#evaluatorOf(String css)}. If you are executing the same selector on many elements (or documents), it
027 can be more efficient to compile and reuse an Evaluator than to reparse the selector on each invocation of select().
028 <p>Evaluators are thread-safe and may be used concurrently across multiple documents.</p>
029 */
030public abstract class Evaluator {
031    protected Evaluator() {
032    }
033
034    /**
035     Provides a Predicate for this Evaluator, matching the test Element.
036     * @param root the root Element, for match evaluation
037     * @return a predicate that accepts an Element to test for matches with this Evaluator
038     * @since 1.17.1
039     */
040    public Predicate<Element> asPredicate(Element root) {
041        return element -> matches(root, element);
042    }
043
044    Predicate<Node> asNodePredicate(Element root) {
045        return node -> matches(root, node);
046    }
047
048    /**
049     * Test if the element meets the evaluator's requirements.
050     *
051     * @param root    Root of the matching subtree
052     * @param element tested element
053     * @return Returns <tt>true</tt> if the requirements are met or
054     * <tt>false</tt> otherwise
055     */
056    public abstract boolean matches(Element root, Element element);
057
058    final boolean matches(Element root, Node node) {
059        if (node instanceof Element) {
060            return matches(root, (Element) node);
061        } else if (node instanceof LeafNode && wantsNodes()) {
062            return matches(root, (LeafNode) node);
063        }
064        return false;
065    }
066
067    boolean matches(Element root, LeafNode leafNode) {
068        return false;
069    }
070
071    boolean wantsNodes() {
072        return false;
073    }
074
075    /**
076     Reset any internal state in this Evaluator before executing a new Collector evaluation.
077     */
078    protected void reset() {
079    }
080
081    /**
082     A relative evaluator cost function. During evaluation, Evaluators are sorted by ascending cost as an optimization.
083     * @return the relative cost of this Evaluator
084     */
085    protected int cost() {
086        return 5; // a nominal default cost
087    }
088
089    /**
090     * Evaluator for tag name
091     */
092    public static final class Tag extends Evaluator {
093        private final String tagName;
094
095        public Tag(String tagName) {
096            this.tagName = tagName;
097        }
098
099        @Override
100        public boolean matches(Element root, Element element) {
101            return (element.nameIs(tagName));
102        }
103
104        @Override protected int cost() {
105            return 1;
106        }
107
108        @Override
109        public String toString() {
110            return String.format("%s", tagName);
111        }
112    }
113
114    /**
115     * Evaluator for tag name that starts with prefix; used for ns|*
116     */
117    public static final class TagStartsWith extends Evaluator {
118        private final String tagName;
119
120        public TagStartsWith(String tagName) {
121            this.tagName = tagName;
122        }
123
124        @Override
125        public boolean matches(Element root, Element element) {
126            return (element.normalName().startsWith(tagName));
127        }
128
129        @Override
130        public String toString() {
131            return String.format("%s|*", tagName);
132        }
133    }
134
135
136    /**
137     * Evaluator for tag name that ends with suffix; used for *|el
138     */
139    public static final class TagEndsWith extends Evaluator {
140        private final String tagName;
141
142        public TagEndsWith(String tagName) {
143            this.tagName = tagName;
144        }
145
146        @Override
147        public boolean matches(Element root, Element element) {
148            return (element.normalName().endsWith(tagName));
149        }
150
151        @Override
152        public String toString() {
153            return String.format("*|%s", tagName);
154        }
155    }
156
157    /**
158     * Evaluator for element id
159     */
160    public static final class Id extends Evaluator {
161        private final String id;
162
163        public Id(String id) {
164            this.id = id;
165        }
166
167        @Override
168        public boolean matches(Element root, Element element) {
169            return (id.equals(element.id()));
170        }
171
172        @Override protected int cost() {
173            return 2;
174        }
175        @Override
176        public String toString() {
177            return String.format("#%s", id);
178        }
179    }
180
181    /**
182     * Evaluator for element class
183     */
184    public static final class Class extends Evaluator {
185        private final String className;
186
187        public Class(String className) {
188            this.className = className;
189        }
190
191        @Override
192        public boolean matches(Element root, Element element) {
193            return (element.hasClass(className));
194        }
195
196        @Override protected int cost() {
197            return 8; // does whitespace scanning; more than .contains()
198        }
199
200        @Override
201        public String toString() {
202            return String.format(".%s", className);
203        }
204
205    }
206
207    /**
208     * Evaluator for attribute name matching
209     */
210    public static final class Attribute extends Evaluator {
211        private final String key;
212
213        public Attribute(String key) {
214            this.key = key;
215        }
216
217        @Override
218        public boolean matches(Element root, Element element) {
219            return element.hasAttr(key);
220        }
221
222        @Override protected int cost() {
223            return 2;
224        }
225
226        @Override
227        public String toString() {
228            return String.format("[%s]", key);
229        }
230    }
231
232    /**
233     * Evaluator for attribute name prefix matching
234     */
235    public static final class AttributeStarting extends Evaluator {
236        private final String keyPrefix;
237
238        public AttributeStarting(String keyPrefix) {
239            Validate.notNull(keyPrefix); // OK to be empty - will find elements with any attributes
240            this.keyPrefix = lowerCase(keyPrefix);
241        }
242
243        @Override
244        public boolean matches(Element root, Element element) {
245            List<org.jsoup.nodes.Attribute> values = element.attributes().asList();
246            for (org.jsoup.nodes.Attribute attribute : values) {
247                if (lowerCase(attribute.getKey()).startsWith(keyPrefix))
248                    return true;
249            }
250            return false;
251        }
252
253        @Override protected int cost() {
254            return 6;
255        }
256
257        @Override
258        public String toString() {
259            return String.format("[^%s]", keyPrefix);
260        }
261
262    }
263
264    /**
265     * Evaluator for attribute name/value matching
266     */
267    public static final class AttributeWithValue extends AttributeKeyPair {
268        public AttributeWithValue(String key, String value) {
269            super(key, value);
270        }
271
272        @Override
273        public boolean matches(Element root, Element element) {
274            return element.hasAttr(key) && value.equalsIgnoreCase(element.attr(key));
275        }
276
277        @Override protected int cost() {
278            return 3;
279        }
280
281        @Override
282        public String toString() {
283            return String.format("[%s=%s]", key, value);
284        }
285
286    }
287
288    /**
289     * Evaluator for attribute name != value matching
290     */
291    public static final class AttributeWithValueNot extends AttributeKeyPair {
292        public AttributeWithValueNot(String key, String value) {
293            super(key, value);
294        }
295
296        @Override
297        public boolean matches(Element root, Element element) {
298            return !value.equalsIgnoreCase(element.attr(key));
299        }
300
301        @Override protected int cost() {
302            return 3;
303        }
304
305        @Override
306        public String toString() {
307            return String.format("[%s!=%s]", key, value);
308        }
309
310    }
311
312    /**
313     * Evaluator for attribute name/value matching (value prefix)
314     */
315    public static final class AttributeWithValueStarting extends AttributeKeyPair {
316        public AttributeWithValueStarting(String key, String value) {
317            super(key, value);
318        }
319
320        @Override
321        public boolean matches(Element root, Element element) {
322            return element.hasAttr(key) && lowerCase(element.attr(key)).startsWith(value); // value is lower case already
323        }
324
325        @Override protected int cost() {
326            return 4;
327        }
328
329        @Override
330        public String toString() {
331            return String.format("[%s^=%s]", key, value);
332        }
333    }
334
335    /**
336     * Evaluator for attribute name/value matching (value ending)
337     */
338    public static final class AttributeWithValueEnding extends AttributeKeyPair {
339        public AttributeWithValueEnding(String key, String value) {
340            super(key, value);
341        }
342
343        @Override
344        public boolean matches(Element root, Element element) {
345            return element.hasAttr(key) && lowerCase(element.attr(key)).endsWith(value); // value is lower case
346        }
347
348        @Override protected int cost() {
349            return 4;
350        }
351
352        @Override
353        public String toString() {
354            return String.format("[%s$=%s]", key, value);
355        }
356    }
357
358    /**
359     * Evaluator for attribute name/value matching (value containing)
360     */
361    public static final class AttributeWithValueContaining extends AttributeKeyPair {
362        public AttributeWithValueContaining(String key, String value) {
363            super(key, value);
364        }
365
366        @Override
367        public boolean matches(Element root, Element element) {
368            return element.hasAttr(key) && lowerCase(element.attr(key)).contains(value); // value is lower case
369        }
370
371        @Override protected int cost() {
372            return 6;
373        }
374
375        @Override
376        public String toString() {
377            return String.format("[%s*=%s]", key, value);
378        }
379
380    }
381
382    /**
383     * Evaluator for attribute name/value matching (value regex matching)
384     */
385    public static final class AttributeWithValueMatching extends Evaluator {
386        final String key;
387        final Regex pattern;
388
389        public AttributeWithValueMatching(String key, Regex pattern) {
390            this.key = normalize(key);
391            this.pattern = pattern;
392        }
393
394        public AttributeWithValueMatching(String key, Pattern pattern) {
395            this(key, Regex.fromPattern(pattern)); // api compat
396        }
397
398        @Override
399        public boolean matches(Element root, Element element) {
400            return element.hasAttr(key) && pattern.matcher(element.attr(key)).find();
401        }
402
403        @Override protected int cost() {
404            return 8;
405        }
406
407        @Override
408        public String toString() {
409            return String.format("[%s~=%s]", key, pattern.toString());
410        }
411
412    }
413
414    /**
415     * Abstract evaluator for attribute name/value matching
416     */
417    public abstract static class AttributeKeyPair extends Evaluator {
418        final String key;
419        final String value;
420
421        public AttributeKeyPair(String key, String value) {
422            Validate.notEmpty(key);
423            Validate.notNull(value);
424
425            this.key = normalize(key);
426            boolean quoted = value.startsWith("'") && value.endsWith("'")
427                || value.startsWith("\"") && value.endsWith("\"");
428            if (quoted) {
429                Validate.isTrue(value.length() > 1, "Quoted value must have content");
430                value = value.substring(1, value.length() - 1);
431            }
432
433            this.value = lowerCase(value); // case-insensitive match
434        }
435
436        /**
437         @deprecated since 1.22.1, use {@link #AttributeKeyPair(String, String)}; the previous trimQuoted parameter is no longer used.
438         This constructor will be removed in jsoup 1.24.1.
439         */
440        @Deprecated
441        public AttributeKeyPair(String key, String value, boolean ignored) {
442            this(key, value);
443        }
444
445
446    }
447
448    /**
449     * Evaluator for any / all element matching
450     */
451    public static final class AllElements extends Evaluator {
452
453        @Override
454        public boolean matches(Element root, Element element) {
455            return true;
456        }
457
458        @Override protected int cost() {
459            return 10;
460        }
461
462        @Override
463        public String toString() {
464            return "*";
465        }
466    }
467
468    /**
469     * Evaluator for matching by sibling index number (e {@literal <} idx)
470     */
471    public static final class IndexLessThan extends IndexEvaluator {
472        public IndexLessThan(int index) {
473            super(index);
474        }
475
476        @Override
477        public boolean matches(Element root, Element element) {
478            return root != element && element.elementSiblingIndex() < index;
479        }
480
481        @Override
482        public String toString() {
483            return String.format(":lt(%d)", index);
484        }
485
486    }
487
488    /**
489     * Evaluator for matching by sibling index number (e {@literal >} idx)
490     */
491    public static final class IndexGreaterThan extends IndexEvaluator {
492        public IndexGreaterThan(int index) {
493            super(index);
494        }
495
496        @Override
497        public boolean matches(Element root, Element element) {
498            return element.elementSiblingIndex() > index;
499        }
500
501        @Override
502        public String toString() {
503            return String.format(":gt(%d)", index);
504        }
505
506    }
507
508    /**
509     * Evaluator for matching by sibling index number (e = idx)
510     */
511    public static final class IndexEquals extends IndexEvaluator {
512        public IndexEquals(int index) {
513            super(index);
514        }
515
516        @Override
517        public boolean matches(Element root, Element element) {
518            return element.elementSiblingIndex() == index;
519        }
520
521        @Override
522        public String toString() {
523            return String.format(":eq(%d)", index);
524        }
525
526    }
527
528    /**
529     * Evaluator for matching the last sibling (css :last-child)
530     */
531    public static final class IsLastChild extends Evaluator {
532                @Override
533                public boolean matches(Element root, Element element) {
534                        final Element p = element.parent();
535                        return p != null && !(p instanceof Document) && element == p.lastElementChild();
536                }
537
538                @Override
539                public String toString() {
540                        return ":last-child";
541                }
542    }
543
544    public static final class IsFirstOfType extends IsNthOfType {
545                public IsFirstOfType() {
546                        super(0,1);
547                }
548                @Override
549                public String toString() {
550                        return ":first-of-type";
551                }
552    }
553
554    public static final class IsLastOfType extends IsNthLastOfType {
555                public IsLastOfType() {
556                        super(0,1);
557                }
558                @Override
559                public String toString() {
560                        return ":last-of-type";
561                }
562    }
563
564
565    public static abstract class CssNthEvaluator extends Evaluator {
566        /** Step */
567        protected final int a;
568        /** Offset */
569        protected final int b;
570
571        public CssNthEvaluator(int step, int offset) {
572            this.a = step;
573            this.b = offset;
574        }
575
576        public CssNthEvaluator(int offset) {
577            this(0, offset);
578        }
579
580        @Override
581        public boolean matches(Element root, Element element) {
582            final Element p = element.parent();
583            if (p == null || (p instanceof Document)) return false;
584
585            final int pos = calculatePosition(root, element);
586            if (a == 0) return pos == b;
587
588            return (pos - b) * a >= 0 && (pos - b) % a == 0;
589        }
590
591        @Override
592        public String toString() {
593            String format =
594                (a == 0) ? ":%s(%3$d)"    // only offset (b)
595                : (b == 0) ? ":%s(%2$dn)" // only step (a)
596                : ":%s(%2$dn%3$+d)";      // step, offset
597            return String.format(format, getPseudoClass(), a, b);
598        }
599
600        protected abstract String getPseudoClass();
601
602        protected abstract int calculatePosition(Element root, Element element);
603    }
604
605
606    /**
607     * css-compatible Evaluator for :eq (css :nth-child)
608     *
609     * @see IndexEquals
610     */
611    public static final class IsNthChild extends CssNthEvaluator {
612        public IsNthChild(int step, int offset) {
613            super(step, offset);
614        }
615
616        @Override
617        protected int calculatePosition(Element root, Element element) {
618            return element.elementSiblingIndex() + 1;
619        }
620
621        @Override
622        protected String getPseudoClass() {
623            return "nth-child";
624        }
625    }
626
627    /**
628     * css pseudo class :nth-last-child)
629     *
630     * @see IndexEquals
631     */
632    public static final class IsNthLastChild extends CssNthEvaluator {
633        public IsNthLastChild(int step, int offset) {
634            super(step, offset);
635        }
636
637        @Override
638        protected int calculatePosition(Element root, Element element) {
639            if (element.parent() == null) return 0;
640                return element.parent().childrenSize() - element.elementSiblingIndex();
641        }
642
643                @Override
644                protected String getPseudoClass() {
645                        return "nth-last-child";
646                }
647    }
648
649    /**
650     * css pseudo class nth-of-type
651     *
652     */
653    public static class IsNthOfType extends CssNthEvaluator {
654        public IsNthOfType(int step, int offset) {
655            super(step, offset);
656        }
657
658        @Override protected int calculatePosition(Element root, Element element) {
659            Element parent = element.parent();
660            if (parent == null)
661                return 0;
662
663            int pos = 0;
664            final int size = parent.childNodeSize();
665            for (int i = 0; i < size; i++) {
666                Node node = parent.childNode(i);
667                if (node.normalName().equals(element.normalName())) pos++;
668                if (node == element) break;
669            }
670            return pos;
671        }
672
673        @Override
674        protected String getPseudoClass() {
675            return "nth-of-type";
676        }
677    }
678
679    public static class IsNthLastOfType extends CssNthEvaluator {
680        public IsNthLastOfType(int step, int offset) {
681            super(step, offset);
682        }
683
684        @Override
685        protected int calculatePosition(Element root, Element element) {
686            Element parent = element.parent();
687            if (parent == null)
688                return 0;
689
690            int pos = 0;
691            Element next = element;
692            while (next != null) {
693                if (next.normalName().equals(element.normalName()))
694                    pos++;
695                next = next.nextElementSibling();
696            }
697            return pos;
698        }
699
700        @Override
701        protected String getPseudoClass() {
702            return "nth-last-of-type";
703        }
704    }
705
706    /**
707     * Evaluator for matching the first sibling (css :first-child)
708     */
709    public static final class IsFirstChild extends Evaluator {
710        @Override
711        public boolean matches(Element root, Element element) {
712                final Element p = element.parent();
713                return p != null && !(p instanceof Document) && element == p.firstElementChild();
714        }
715
716        @Override
717        public String toString() {
718                return ":first-child";
719        }
720    }
721
722    /**
723     * css3 pseudo-class :root
724     * @see <a href="http://www.w3.org/TR/selectors/#root-pseudo">:root selector</a>
725     *
726     */
727    public static final class IsRoot extends Evaluator {
728        @Override
729        public boolean matches(Element root, Element element) {
730                final Element r = root instanceof Document ? root.firstElementChild() : root;
731                return element == r;
732        }
733
734        @Override protected int cost() {
735            return 1;
736        }
737
738        @Override
739        public String toString() {
740                return ":root";
741        }
742    }
743
744    public static final class IsOnlyChild extends Evaluator {
745                @Override
746                public boolean matches(Element root, Element element) {
747                        final Element p = element.parent();
748                        return p!=null && !(p instanceof Document) && element.siblingElements().isEmpty();
749                }
750        @Override
751        public String toString() {
752                return ":only-child";
753        }
754    }
755
756    public static final class IsOnlyOfType extends Evaluator {
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                        int pos = 0;
763            Element next = p.firstElementChild();
764            while (next != null) {
765                if (next.normalName().equals(element.normalName()))
766                    pos++;
767                if (pos > 1)
768                    break;
769                next = next.nextElementSibling();
770            }
771                return pos == 1;
772                }
773        @Override
774        public String toString() {
775                return ":only-of-type";
776        }
777    }
778
779    public static final class IsEmpty extends Evaluator {
780        @Override
781        public boolean matches(Element root, Element el) {
782            for (Node n = el.firstChild(); n != null; n = n.nextSibling()) {
783                if (n instanceof TextNode) {
784                    if (!((TextNode) n).isBlank())
785                        return false; // non-blank text: not empty
786                } else if (!(n instanceof Comment || n instanceof XmlDeclaration || n instanceof DocumentType))
787                    return false; // non "blank" element: not empty
788            }
789            return true;
790        }
791
792        @Override
793        public String toString() {
794            return ":empty";
795        }
796    }
797
798    /**
799     * Abstract evaluator for sibling index matching
800     *
801     * @author ant
802     */
803    public abstract static class IndexEvaluator extends Evaluator {
804        final int index;
805
806        public IndexEvaluator(int index) {
807            this.index = index;
808        }
809    }
810
811    /**
812     * Evaluator for matching Element (and its descendants) text
813     */
814    public static final class ContainsText extends Evaluator {
815        private final String searchText;
816
817        public ContainsText(String searchText) {
818            this.searchText = lowerCase(normaliseWhitespace(searchText));
819        }
820
821        @Override
822        public boolean matches(Element root, Element element) {
823            return lowerCase(element.text()).contains(searchText);
824        }
825
826        @Override protected int cost() {
827            return 10;
828        }
829
830        @Override
831        public String toString() {
832            return String.format(":contains(%s)", searchText);
833        }
834    }
835
836    /**
837     * Evaluator for matching Element (and its descendants) wholeText. Neither the input nor the element text is
838     * normalized. <code>:containsWholeText()</code>
839     * @since 1.15.1.
840     */
841    public static final class ContainsWholeText extends Evaluator {
842        private final String searchText;
843
844        public ContainsWholeText(String searchText) {
845            this.searchText = searchText;
846        }
847
848        @Override
849        public boolean matches(Element root, Element element) {
850            return element.wholeText().contains(searchText);
851        }
852
853        @Override protected int cost() {
854            return 10;
855        }
856
857        @Override
858        public String toString() {
859            return String.format(":containsWholeText(%s)", searchText);
860        }
861    }
862
863    /**
864     * Evaluator for matching Element (but <b>not</b> its descendants) wholeText. Neither the input nor the element text is
865     * normalized. <code>:containsWholeOwnText()</code>
866     * @since 1.15.1.
867     */
868    public static final class ContainsWholeOwnText extends Evaluator {
869        private final String searchText;
870
871        public ContainsWholeOwnText(String searchText) {
872            this.searchText = searchText;
873        }
874
875        @Override
876        public boolean matches(Element root, Element element) {
877            return element.wholeOwnText().contains(searchText);
878        }
879
880        @Override
881        public String toString() {
882            return String.format(":containsWholeOwnText(%s)", searchText);
883        }
884    }
885
886    /**
887     * Evaluator for matching Element (and its descendants) data
888     */
889    public static final class ContainsData extends Evaluator {
890        private final String searchText;
891
892        public ContainsData(String searchText) {
893            this.searchText = lowerCase(searchText);
894        }
895
896        @Override
897        public boolean matches(Element root, Element element) {
898            return lowerCase(element.data()).contains(searchText); // not whitespace normalized
899        }
900
901        @Override
902        public String toString() {
903            return String.format(":containsData(%s)", searchText);
904        }
905    }
906
907    /**
908     * Evaluator for matching Element's own text
909     */
910    public static final class ContainsOwnText extends Evaluator {
911        private final String searchText;
912
913        public ContainsOwnText(String searchText) {
914            this.searchText = lowerCase(normaliseWhitespace(searchText));
915        }
916
917        @Override
918        public boolean matches(Element root, Element element) {
919            return lowerCase(element.ownText()).contains(searchText);
920        }
921
922        @Override
923        public String toString() {
924            return String.format(":containsOwn(%s)", searchText);
925        }
926    }
927
928    /**
929     * Evaluator for matching Element (and its descendants) text with regex
930     */
931    public static final class Matches extends Evaluator {
932        private final Regex pattern;
933
934        public Matches(Regex pattern) {
935            this.pattern = pattern;
936        }
937
938        public Matches(Pattern pattern) {
939            this(Regex.fromPattern(pattern));
940        }
941
942        @Override
943        public boolean matches(Element root, Element element) {
944            return pattern.matcher(element.text()).find();
945        }
946
947        @Override protected int cost() {
948            return 8;
949        }
950
951        @Override
952        public String toString() {
953            return String.format(":matches(%s)", pattern);
954        }
955    }
956
957    /**
958     * Evaluator for matching Element's own text with regex
959     */
960    public static final class MatchesOwn extends Evaluator {
961        private final Regex pattern;
962
963        public MatchesOwn(Regex pattern) {
964            this.pattern = pattern;
965        }
966
967        public MatchesOwn(Pattern pattern) {
968            this(Regex.fromPattern(pattern));
969        }
970
971        @Override
972        public boolean matches(Element root, Element element) {
973            return pattern.matcher(element.ownText()).find();
974        }
975
976        @Override protected int cost() {
977            return 7;
978        }
979
980        @Override
981        public String toString() {
982            return String.format(":matchesOwn(%s)", pattern);
983        }
984    }
985
986    /**
987     * Evaluator for matching Element (and its descendants) whole text with regex.
988     * @since 1.15.1.
989     */
990    public static final class MatchesWholeText extends Evaluator {
991        private final Regex pattern;
992
993        public MatchesWholeText(Regex pattern) {
994            this.pattern = pattern;
995        }
996
997        public MatchesWholeText(Pattern pattern) {
998            this.pattern = Regex.fromPattern(pattern);
999        }
1000
1001        @Override
1002        public boolean matches(Element root, Element element) {
1003            return pattern.matcher(element.wholeText()).find();
1004        }
1005
1006        @Override protected int cost() {
1007            return 8;
1008        }
1009
1010        @Override
1011        public String toString() {
1012            return String.format(":matchesWholeText(%s)", pattern);
1013        }
1014    }
1015
1016    /**
1017     * Evaluator for matching Element's own whole text with regex.
1018     * @since 1.15.1.
1019     */
1020    public static final class MatchesWholeOwnText extends Evaluator {
1021        private final Regex pattern;
1022
1023        public MatchesWholeOwnText(Regex pattern) {
1024            this.pattern = pattern;
1025        }
1026
1027        public MatchesWholeOwnText(Pattern pattern) {
1028            this(Regex.fromPattern(pattern));
1029        }
1030
1031        @Override
1032        public boolean matches(Element root, Element element) {
1033            Regex.Matcher m = pattern.matcher(element.wholeOwnText());
1034            return m.find();
1035        }
1036
1037        @Override protected int cost() {
1038            return 7;
1039        }
1040
1041        @Override
1042        public String toString() {
1043            return String.format(":matchesWholeOwnText(%s)", pattern);
1044        }
1045    }
1046
1047    /**
1048     @deprecated This selector is deprecated and will be removed in jsoup 1.24.1. Migrate to <code>::textnode</code> using the <code>Element#selectNodes()</code> method instead.
1049     */
1050    @Deprecated
1051    @SuppressWarnings("deprecation") // Uses PseudoTextElement for deprecated :matchText support until removal.
1052    public static final class MatchText extends Evaluator {
1053        private static boolean loggedError = false;
1054
1055        public MatchText() {
1056            // log a deprecated error on first use; users typically won't directly construct this Evaluator and so won't otherwise get deprecation warnings
1057            if (!loggedError) {
1058                loggedError = true;
1059                System.err.println("WARNING: :matchText selector is deprecated and will be removed in jsoup 1.24.1. Use Element#selectNodes(String, Class) with selector ::textnode and class TextNode instead.");
1060            }
1061        }
1062
1063        @Override
1064        public boolean matches(Element root, Element element) {
1065            if (element instanceof org.jsoup.nodes.PseudoTextElement)
1066                return true;
1067
1068            List<TextNode> textNodes = element.textNodes();
1069            for (TextNode textNode : textNodes) {
1070                org.jsoup.nodes.PseudoTextElement pel = new org.jsoup.nodes.PseudoTextElement(
1071                    org.jsoup.parser.Tag.valueOf(element.tagName(), element.tag().namespace(), ParseSettings.preserveCase), element.baseUri(), element.attributes());
1072                textNode.replaceWith(pel);
1073                pel.appendChild(textNode);
1074            }
1075            return false;
1076        }
1077
1078        @Override protected int cost() {
1079            return -1; // forces first evaluation, which prepares the DOM for later evaluator matches
1080        }
1081
1082        @Override
1083        public String toString() {
1084            return ":matchText";
1085        }
1086    }
1087}