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;
014
015import java.util.List;
016import java.util.function.Predicate;
017import java.util.regex.Matcher;
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).trim());
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, false);
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, false);
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 Pattern pattern;
389
390        public AttributeWithValueMatching(String key, Pattern pattern) {
391            this.key = normalize(key);
392            this.pattern = pattern;
393        }
394
395        @Override
396        public boolean matches(Element root, Element element) {
397            return element.hasAttr(key) && pattern.matcher(element.attr(key)).find();
398        }
399
400        @Override protected int cost() {
401            return 8;
402        }
403
404        @Override
405        public String toString() {
406            return String.format("[%s~=%s]", key, pattern.toString());
407        }
408
409    }
410
411    /**
412     * Abstract evaluator for attribute name/value matching
413     */
414    public abstract static class AttributeKeyPair extends Evaluator {
415        final String key;
416        final String value;
417
418        public AttributeKeyPair(String key, String value) {
419            this(key, value, true);
420        }
421
422        public AttributeKeyPair(String key, String value, boolean trimQuoted) {
423            Validate.notEmpty(key);
424            Validate.notEmpty(value);
425
426            this.key = normalize(key);
427            boolean quoted = value.startsWith("'") && value.endsWith("'")
428                || value.startsWith("\"") && value.endsWith("\"");
429            if (quoted)
430                value = value.substring(1, value.length() - 1);
431
432            // normalize value based on whether it was quoted and trimQuoted flag
433            // keeps whitespace for attribute val starting or ending, when quoted
434            if (trimQuoted || !quoted)
435                this.value = normalize(value); // lowercase and trims
436            else
437                this.value = lowerCase(value); // only lowercase
438        }
439    }
440
441    /**
442     * Evaluator for any / all element matching
443     */
444    public static final class AllElements extends Evaluator {
445
446        @Override
447        public boolean matches(Element root, Element element) {
448            return true;
449        }
450
451        @Override protected int cost() {
452            return 10;
453        }
454
455        @Override
456        public String toString() {
457            return "*";
458        }
459    }
460
461    /**
462     * Evaluator for matching by sibling index number (e {@literal <} idx)
463     */
464    public static final class IndexLessThan extends IndexEvaluator {
465        public IndexLessThan(int index) {
466            super(index);
467        }
468
469        @Override
470        public boolean matches(Element root, Element element) {
471            return root != element && element.elementSiblingIndex() < index;
472        }
473
474        @Override
475        public String toString() {
476            return String.format(":lt(%d)", index);
477        }
478
479    }
480
481    /**
482     * Evaluator for matching by sibling index number (e {@literal >} idx)
483     */
484    public static final class IndexGreaterThan extends IndexEvaluator {
485        public IndexGreaterThan(int index) {
486            super(index);
487        }
488
489        @Override
490        public boolean matches(Element root, Element element) {
491            return element.elementSiblingIndex() > index;
492        }
493
494        @Override
495        public String toString() {
496            return String.format(":gt(%d)", index);
497        }
498
499    }
500
501    /**
502     * Evaluator for matching by sibling index number (e = idx)
503     */
504    public static final class IndexEquals extends IndexEvaluator {
505        public IndexEquals(int index) {
506            super(index);
507        }
508
509        @Override
510        public boolean matches(Element root, Element element) {
511            return element.elementSiblingIndex() == index;
512        }
513
514        @Override
515        public String toString() {
516            return String.format(":eq(%d)", index);
517        }
518
519    }
520
521    /**
522     * Evaluator for matching the last sibling (css :last-child)
523     */
524    public static final class IsLastChild extends Evaluator {
525                @Override
526                public boolean matches(Element root, Element element) {
527                        final Element p = element.parent();
528                        return p != null && !(p instanceof Document) && element == p.lastElementChild();
529                }
530
531                @Override
532                public String toString() {
533                        return ":last-child";
534                }
535    }
536
537    public static final class IsFirstOfType extends IsNthOfType {
538                public IsFirstOfType() {
539                        super(0,1);
540                }
541                @Override
542                public String toString() {
543                        return ":first-of-type";
544                }
545    }
546
547    public static final class IsLastOfType extends IsNthLastOfType {
548                public IsLastOfType() {
549                        super(0,1);
550                }
551                @Override
552                public String toString() {
553                        return ":last-of-type";
554                }
555    }
556
557
558    public static abstract class CssNthEvaluator extends Evaluator {
559        /** Step */
560        protected final int a;
561        /** Offset */
562        protected final int b;
563
564        public CssNthEvaluator(int step, int offset) {
565            this.a = step;
566            this.b = offset;
567        }
568
569        public CssNthEvaluator(int offset) {
570            this(0, offset);
571        }
572
573        @Override
574        public boolean matches(Element root, Element element) {
575            final Element p = element.parent();
576            if (p == null || (p instanceof Document)) return false;
577
578            final int pos = calculatePosition(root, element);
579            if (a == 0) return pos == b;
580
581            return (pos - b) * a >= 0 && (pos - b) % a == 0;
582        }
583
584        @Override
585        public String toString() {
586            String format =
587                (a == 0) ? ":%s(%3$d)"    // only offset (b)
588                : (b == 0) ? ":%s(%2$dn)" // only step (a)
589                : ":%s(%2$dn%3$+d)";      // step, offset
590            return String.format(format, getPseudoClass(), a, b);
591        }
592
593        protected abstract String getPseudoClass();
594
595        protected abstract int calculatePosition(Element root, Element element);
596    }
597
598
599    /**
600     * css-compatible Evaluator for :eq (css :nth-child)
601     *
602     * @see IndexEquals
603     */
604    public static final class IsNthChild extends CssNthEvaluator {
605        public IsNthChild(int step, int offset) {
606            super(step, offset);
607        }
608
609        @Override
610        protected int calculatePosition(Element root, Element element) {
611            return element.elementSiblingIndex() + 1;
612        }
613
614        @Override
615        protected String getPseudoClass() {
616            return "nth-child";
617        }
618    }
619
620    /**
621     * css pseudo class :nth-last-child)
622     *
623     * @see IndexEquals
624     */
625    public static final class IsNthLastChild extends CssNthEvaluator {
626        public IsNthLastChild(int step, int offset) {
627            super(step, offset);
628        }
629
630        @Override
631        protected int calculatePosition(Element root, Element element) {
632            if (element.parent() == null) return 0;
633                return element.parent().childrenSize() - element.elementSiblingIndex();
634        }
635
636                @Override
637                protected String getPseudoClass() {
638                        return "nth-last-child";
639                }
640    }
641
642    /**
643     * css pseudo class nth-of-type
644     *
645     */
646    public static class IsNthOfType extends CssNthEvaluator {
647        public IsNthOfType(int step, int offset) {
648            super(step, offset);
649        }
650
651        @Override protected int calculatePosition(Element root, Element element) {
652            Element parent = element.parent();
653            if (parent == null)
654                return 0;
655
656            int pos = 0;
657            final int size = parent.childNodeSize();
658            for (int i = 0; i < size; i++) {
659                Node node = parent.childNode(i);
660                if (node.normalName().equals(element.normalName())) pos++;
661                if (node == element) break;
662            }
663            return pos;
664        }
665
666        @Override
667        protected String getPseudoClass() {
668            return "nth-of-type";
669        }
670    }
671
672    public static class IsNthLastOfType extends CssNthEvaluator {
673        public IsNthLastOfType(int step, int offset) {
674            super(step, offset);
675        }
676
677        @Override
678        protected int calculatePosition(Element root, Element element) {
679            Element parent = element.parent();
680            if (parent == null)
681                return 0;
682
683            int pos = 0;
684            Element next = element;
685            while (next != null) {
686                if (next.normalName().equals(element.normalName()))
687                    pos++;
688                next = next.nextElementSibling();
689            }
690            return pos;
691        }
692
693        @Override
694        protected String getPseudoClass() {
695            return "nth-last-of-type";
696        }
697    }
698
699    /**
700     * Evaluator for matching the first sibling (css :first-child)
701     */
702    public static final class IsFirstChild extends Evaluator {
703        @Override
704        public boolean matches(Element root, Element element) {
705                final Element p = element.parent();
706                return p != null && !(p instanceof Document) && element == p.firstElementChild();
707        }
708
709        @Override
710        public String toString() {
711                return ":first-child";
712        }
713    }
714
715    /**
716     * css3 pseudo-class :root
717     * @see <a href="http://www.w3.org/TR/selectors/#root-pseudo">:root selector</a>
718     *
719     */
720    public static final class IsRoot extends Evaluator {
721        @Override
722        public boolean matches(Element root, Element element) {
723                final Element r = root instanceof Document ? root.firstElementChild() : root;
724                return element == r;
725        }
726
727        @Override protected int cost() {
728            return 1;
729        }
730
731        @Override
732        public String toString() {
733                return ":root";
734        }
735    }
736
737    public static final class IsOnlyChild extends Evaluator {
738                @Override
739                public boolean matches(Element root, Element element) {
740                        final Element p = element.parent();
741                        return p!=null && !(p instanceof Document) && element.siblingElements().isEmpty();
742                }
743        @Override
744        public String toString() {
745                return ":only-child";
746        }
747    }
748
749    public static final class IsOnlyOfType extends Evaluator {
750                @Override
751                public boolean matches(Element root, Element element) {
752                        final Element p = element.parent();
753                        if (p==null || p instanceof Document) return false;
754
755                        int pos = 0;
756            Element next = p.firstElementChild();
757            while (next != null) {
758                if (next.normalName().equals(element.normalName()))
759                    pos++;
760                if (pos > 1)
761                    break;
762                next = next.nextElementSibling();
763            }
764                return pos == 1;
765                }
766        @Override
767        public String toString() {
768                return ":only-of-type";
769        }
770    }
771
772    public static final class IsEmpty extends Evaluator {
773        @Override
774        public boolean matches(Element root, Element el) {
775            for (Node n = el.firstChild(); n != null; n = n.nextSibling()) {
776                if (n instanceof TextNode) {
777                    if (!((TextNode) n).isBlank())
778                        return false; // non-blank text: not empty
779                } else if (!(n instanceof Comment || n instanceof XmlDeclaration || n instanceof DocumentType))
780                    return false; // non "blank" element: not empty
781            }
782            return true;
783        }
784
785        @Override
786        public String toString() {
787            return ":empty";
788        }
789    }
790
791    /**
792     * Abstract evaluator for sibling index matching
793     *
794     * @author ant
795     */
796    public abstract static class IndexEvaluator extends Evaluator {
797        final int index;
798
799        public IndexEvaluator(int index) {
800            this.index = index;
801        }
802    }
803
804    /**
805     * Evaluator for matching Element (and its descendants) text
806     */
807    public static final class ContainsText extends Evaluator {
808        private final String searchText;
809
810        public ContainsText(String searchText) {
811            this.searchText = lowerCase(normaliseWhitespace(searchText));
812        }
813
814        @Override
815        public boolean matches(Element root, Element element) {
816            return lowerCase(element.text()).contains(searchText);
817        }
818
819        @Override protected int cost() {
820            return 10;
821        }
822
823        @Override
824        public String toString() {
825            return String.format(":contains(%s)", searchText);
826        }
827    }
828
829    /**
830     * Evaluator for matching Element (and its descendants) wholeText. Neither the input nor the element text is
831     * normalized. <code>:containsWholeText()</code>
832     * @since 1.15.1.
833     */
834    public static final class ContainsWholeText extends Evaluator {
835        private final String searchText;
836
837        public ContainsWholeText(String searchText) {
838            this.searchText = searchText;
839        }
840
841        @Override
842        public boolean matches(Element root, Element element) {
843            return element.wholeText().contains(searchText);
844        }
845
846        @Override protected int cost() {
847            return 10;
848        }
849
850        @Override
851        public String toString() {
852            return String.format(":containsWholeText(%s)", searchText);
853        }
854    }
855
856    /**
857     * Evaluator for matching Element (but <b>not</b> its descendants) wholeText. Neither the input nor the element text is
858     * normalized. <code>:containsWholeOwnText()</code>
859     * @since 1.15.1.
860     */
861    public static final class ContainsWholeOwnText extends Evaluator {
862        private final String searchText;
863
864        public ContainsWholeOwnText(String searchText) {
865            this.searchText = searchText;
866        }
867
868        @Override
869        public boolean matches(Element root, Element element) {
870            return element.wholeOwnText().contains(searchText);
871        }
872
873        @Override
874        public String toString() {
875            return String.format(":containsWholeOwnText(%s)", searchText);
876        }
877    }
878
879    /**
880     * Evaluator for matching Element (and its descendants) data
881     */
882    public static final class ContainsData extends Evaluator {
883        private final String searchText;
884
885        public ContainsData(String searchText) {
886            this.searchText = lowerCase(searchText);
887        }
888
889        @Override
890        public boolean matches(Element root, Element element) {
891            return lowerCase(element.data()).contains(searchText); // not whitespace normalized
892        }
893
894        @Override
895        public String toString() {
896            return String.format(":containsData(%s)", searchText);
897        }
898    }
899
900    /**
901     * Evaluator for matching Element's own text
902     */
903    public static final class ContainsOwnText extends Evaluator {
904        private final String searchText;
905
906        public ContainsOwnText(String searchText) {
907            this.searchText = lowerCase(normaliseWhitespace(searchText));
908        }
909
910        @Override
911        public boolean matches(Element root, Element element) {
912            return lowerCase(element.ownText()).contains(searchText);
913        }
914
915        @Override
916        public String toString() {
917            return String.format(":containsOwn(%s)", searchText);
918        }
919    }
920
921    /**
922     * Evaluator for matching Element (and its descendants) text with regex
923     */
924    public static final class Matches extends Evaluator {
925        private final Pattern pattern;
926
927        public Matches(Pattern pattern) {
928            this.pattern = pattern;
929        }
930
931        @Override
932        public boolean matches(Element root, Element element) {
933            Matcher m = pattern.matcher(element.text());
934            return m.find();
935        }
936
937        @Override protected int cost() {
938            return 8;
939        }
940
941        @Override
942        public String toString() {
943            return String.format(":matches(%s)", pattern);
944        }
945    }
946
947    /**
948     * Evaluator for matching Element's own text with regex
949     */
950    public static final class MatchesOwn extends Evaluator {
951        private final Pattern pattern;
952
953        public MatchesOwn(Pattern pattern) {
954            this.pattern = pattern;
955        }
956
957        @Override
958        public boolean matches(Element root, Element element) {
959            Matcher m = pattern.matcher(element.ownText());
960            return m.find();
961        }
962
963        @Override protected int cost() {
964            return 7;
965        }
966
967        @Override
968        public String toString() {
969            return String.format(":matchesOwn(%s)", pattern);
970        }
971    }
972
973    /**
974     * Evaluator for matching Element (and its descendants) whole text with regex.
975     * @since 1.15.1.
976     */
977    public static final class MatchesWholeText extends Evaluator {
978        private final Pattern pattern;
979
980        public MatchesWholeText(Pattern pattern) {
981            this.pattern = pattern;
982        }
983
984        @Override
985        public boolean matches(Element root, Element element) {
986            Matcher m = pattern.matcher(element.wholeText());
987            return m.find();
988        }
989
990        @Override protected int cost() {
991            return 8;
992        }
993
994        @Override
995        public String toString() {
996            return String.format(":matchesWholeText(%s)", pattern);
997        }
998    }
999
1000    /**
1001     * Evaluator for matching Element's own whole text with regex.
1002     * @since 1.15.1.
1003     */
1004    public static final class MatchesWholeOwnText extends Evaluator {
1005        private final Pattern pattern;
1006
1007        public MatchesWholeOwnText(Pattern pattern) {
1008            this.pattern = pattern;
1009        }
1010
1011        @Override
1012        public boolean matches(Element root, Element element) {
1013            Matcher m = pattern.matcher(element.wholeOwnText());
1014            return m.find();
1015        }
1016
1017        @Override protected int cost() {
1018            return 7;
1019        }
1020
1021        @Override
1022        public String toString() {
1023            return String.format(":matchesWholeOwnText(%s)", pattern);
1024        }
1025    }
1026
1027    /**
1028     @deprecated This selector is deprecated and will be removed in a future version. Migrate to <code>::textnode</code> using the <code>Element#selectNodes()</code> method instead.
1029     */
1030    @Deprecated
1031    public static final class MatchText extends Evaluator {
1032        private static boolean loggedError = false;
1033
1034        public MatchText() {
1035            // log a deprecated error on first use; users typically won't directly construct this Evaluator and so won't otherwise get deprecation warnings
1036            if (!loggedError) {
1037                loggedError = true;
1038                System.err.println("WARNING: :matchText selector is deprecated and will be removed in a future version. Use Element#selectNodes(String, Class) with selector ::textnode and class TextNode instead.");
1039            }
1040        }
1041
1042        @Override
1043        public boolean matches(Element root, Element element) {
1044            if (element instanceof PseudoTextElement)
1045                return true;
1046
1047            List<TextNode> textNodes = element.textNodes();
1048            for (TextNode textNode : textNodes) {
1049                PseudoTextElement pel = new PseudoTextElement(
1050                    org.jsoup.parser.Tag.valueOf(element.tagName(), element.tag().namespace(), ParseSettings.preserveCase), element.baseUri(), element.attributes());
1051                textNode.replaceWith(pel);
1052                pel.appendChild(textNode);
1053            }
1054            return false;
1055        }
1056
1057        @Override protected int cost() {
1058            return -1; // forces first evaluation, which prepares the DOM for later evaluator matches
1059        }
1060
1061        @Override
1062        public String toString() {
1063            return ":matchText";
1064        }
1065    }
1066}