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