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