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}