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