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}