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