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