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; 014import org.jsoup.helper.Regex; 015 016import java.util.List; 017import java.util.function.Predicate; 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)); 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); 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); 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 Regex pattern; 389 390 public AttributeWithValueMatching(String key, Regex pattern) { 391 this.key = normalize(key); 392 this.pattern = pattern; 393 } 394 395 public AttributeWithValueMatching(String key, Pattern pattern) { 396 this(key, Regex.fromPattern(pattern)); // api compat 397 } 398 399 @Override 400 public boolean matches(Element root, Element element) { 401 return element.hasAttr(key) && pattern.matcher(element.attr(key)).find(); 402 } 403 404 @Override protected int cost() { 405 return 8; 406 } 407 408 @Override 409 public String toString() { 410 return String.format("[%s~=%s]", key, pattern.toString()); 411 } 412 413 } 414 415 /** 416 * Abstract evaluator for attribute name/value matching 417 */ 418 public abstract static class AttributeKeyPair extends Evaluator { 419 final String key; 420 final String value; 421 422 public AttributeKeyPair(String key, String value) { 423 Validate.notEmpty(key); 424 Validate.notNull(value); 425 426 this.key = normalize(key); 427 boolean quoted = value.startsWith("'") && value.endsWith("'") 428 || value.startsWith("\"") && value.endsWith("\""); 429 if (quoted) { 430 Validate.isTrue(value.length() > 1, "Quoted value must have content"); 431 value = value.substring(1, value.length() - 1); 432 } 433 434 this.value = lowerCase(value); // case-insensitive match 435 } 436 437 /** 438 @deprecated since 1.22.1, use {@link #AttributeKeyPair(String, String)}; the previous trimQuoted parameter is no longer used. 439 This constructor will be removed in jsoup 1.24.1. 440 */ 441 @Deprecated 442 public AttributeKeyPair(String key, String value, boolean ignored) { 443 this(key, value); 444 } 445 446 447 } 448 449 /** 450 * Evaluator for any / all element matching 451 */ 452 public static final class AllElements extends Evaluator { 453 454 @Override 455 public boolean matches(Element root, Element element) { 456 return true; 457 } 458 459 @Override protected int cost() { 460 return 10; 461 } 462 463 @Override 464 public String toString() { 465 return "*"; 466 } 467 } 468 469 /** 470 * Evaluator for matching by sibling index number (e {@literal <} idx) 471 */ 472 public static final class IndexLessThan extends IndexEvaluator { 473 public IndexLessThan(int index) { 474 super(index); 475 } 476 477 @Override 478 public boolean matches(Element root, Element element) { 479 return root != element && element.elementSiblingIndex() < index; 480 } 481 482 @Override 483 public String toString() { 484 return String.format(":lt(%d)", index); 485 } 486 487 } 488 489 /** 490 * Evaluator for matching by sibling index number (e {@literal >} idx) 491 */ 492 public static final class IndexGreaterThan extends IndexEvaluator { 493 public IndexGreaterThan(int index) { 494 super(index); 495 } 496 497 @Override 498 public boolean matches(Element root, Element element) { 499 return element.elementSiblingIndex() > index; 500 } 501 502 @Override 503 public String toString() { 504 return String.format(":gt(%d)", index); 505 } 506 507 } 508 509 /** 510 * Evaluator for matching by sibling index number (e = idx) 511 */ 512 public static final class IndexEquals extends IndexEvaluator { 513 public IndexEquals(int index) { 514 super(index); 515 } 516 517 @Override 518 public boolean matches(Element root, Element element) { 519 return element.elementSiblingIndex() == index; 520 } 521 522 @Override 523 public String toString() { 524 return String.format(":eq(%d)", index); 525 } 526 527 } 528 529 /** 530 * Evaluator for matching the last sibling (css :last-child) 531 */ 532 public static final class IsLastChild extends Evaluator { 533 @Override 534 public boolean matches(Element root, Element element) { 535 final Element p = element.parent(); 536 return p != null && !(p instanceof Document) && element == p.lastElementChild(); 537 } 538 539 @Override 540 public String toString() { 541 return ":last-child"; 542 } 543 } 544 545 public static final class IsFirstOfType extends IsNthOfType { 546 public IsFirstOfType() { 547 super(0,1); 548 } 549 @Override 550 public String toString() { 551 return ":first-of-type"; 552 } 553 } 554 555 public static final class IsLastOfType extends IsNthLastOfType { 556 public IsLastOfType() { 557 super(0,1); 558 } 559 @Override 560 public String toString() { 561 return ":last-of-type"; 562 } 563 } 564 565 566 public static abstract class CssNthEvaluator extends Evaluator { 567 /** Step */ 568 protected final int a; 569 /** Offset */ 570 protected final int b; 571 572 public CssNthEvaluator(int step, int offset) { 573 this.a = step; 574 this.b = offset; 575 } 576 577 public CssNthEvaluator(int offset) { 578 this(0, offset); 579 } 580 581 @Override 582 public boolean matches(Element root, Element element) { 583 final Element p = element.parent(); 584 if (p == null || (p instanceof Document)) return false; 585 586 final int pos = calculatePosition(root, element); 587 if (a == 0) return pos == b; 588 589 return (pos - b) * a >= 0 && (pos - b) % a == 0; 590 } 591 592 @Override 593 public String toString() { 594 String format = 595 (a == 0) ? ":%s(%3$d)" // only offset (b) 596 : (b == 0) ? ":%s(%2$dn)" // only step (a) 597 : ":%s(%2$dn%3$+d)"; // step, offset 598 return String.format(format, getPseudoClass(), a, b); 599 } 600 601 protected abstract String getPseudoClass(); 602 603 protected abstract int calculatePosition(Element root, Element element); 604 } 605 606 607 /** 608 * css-compatible Evaluator for :eq (css :nth-child) 609 * 610 * @see IndexEquals 611 */ 612 public static final class IsNthChild extends CssNthEvaluator { 613 public IsNthChild(int step, int offset) { 614 super(step, offset); 615 } 616 617 @Override 618 protected int calculatePosition(Element root, Element element) { 619 return element.elementSiblingIndex() + 1; 620 } 621 622 @Override 623 protected String getPseudoClass() { 624 return "nth-child"; 625 } 626 } 627 628 /** 629 * css pseudo class :nth-last-child) 630 * 631 * @see IndexEquals 632 */ 633 public static final class IsNthLastChild extends CssNthEvaluator { 634 public IsNthLastChild(int step, int offset) { 635 super(step, offset); 636 } 637 638 @Override 639 protected int calculatePosition(Element root, Element element) { 640 if (element.parent() == null) return 0; 641 return element.parent().childrenSize() - element.elementSiblingIndex(); 642 } 643 644 @Override 645 protected String getPseudoClass() { 646 return "nth-last-child"; 647 } 648 } 649 650 /** 651 * css pseudo class nth-of-type 652 * 653 */ 654 public static class IsNthOfType extends CssNthEvaluator { 655 public IsNthOfType(int step, int offset) { 656 super(step, offset); 657 } 658 659 @Override protected int calculatePosition(Element root, Element element) { 660 Element parent = element.parent(); 661 if (parent == null) 662 return 0; 663 664 int pos = 0; 665 final int size = parent.childNodeSize(); 666 for (int i = 0; i < size; i++) { 667 Node node = parent.childNode(i); 668 if (node.normalName().equals(element.normalName())) pos++; 669 if (node == element) break; 670 } 671 return pos; 672 } 673 674 @Override 675 protected String getPseudoClass() { 676 return "nth-of-type"; 677 } 678 } 679 680 public static class IsNthLastOfType extends CssNthEvaluator { 681 public IsNthLastOfType(int step, int offset) { 682 super(step, offset); 683 } 684 685 @Override 686 protected int calculatePosition(Element root, Element element) { 687 Element parent = element.parent(); 688 if (parent == null) 689 return 0; 690 691 int pos = 0; 692 Element next = element; 693 while (next != null) { 694 if (next.normalName().equals(element.normalName())) 695 pos++; 696 next = next.nextElementSibling(); 697 } 698 return pos; 699 } 700 701 @Override 702 protected String getPseudoClass() { 703 return "nth-last-of-type"; 704 } 705 } 706 707 /** 708 * Evaluator for matching the first sibling (css :first-child) 709 */ 710 public static final class IsFirstChild extends Evaluator { 711 @Override 712 public boolean matches(Element root, Element element) { 713 final Element p = element.parent(); 714 return p != null && !(p instanceof Document) && element == p.firstElementChild(); 715 } 716 717 @Override 718 public String toString() { 719 return ":first-child"; 720 } 721 } 722 723 /** 724 * css3 pseudo-class :root 725 * @see <a href="http://www.w3.org/TR/selectors/#root-pseudo">:root selector</a> 726 * 727 */ 728 public static final class IsRoot extends Evaluator { 729 @Override 730 public boolean matches(Element root, Element element) { 731 final Element r = root instanceof Document ? root.firstElementChild() : root; 732 return element == r; 733 } 734 735 @Override protected int cost() { 736 return 1; 737 } 738 739 @Override 740 public String toString() { 741 return ":root"; 742 } 743 } 744 745 public static final class IsOnlyChild extends Evaluator { 746 @Override 747 public boolean matches(Element root, Element element) { 748 final Element p = element.parent(); 749 return p!=null && !(p instanceof Document) && element.siblingElements().isEmpty(); 750 } 751 @Override 752 public String toString() { 753 return ":only-child"; 754 } 755 } 756 757 public static final class IsOnlyOfType extends Evaluator { 758 @Override 759 public boolean matches(Element root, Element element) { 760 final Element p = element.parent(); 761 if (p==null || p instanceof Document) return false; 762 763 int pos = 0; 764 Element next = p.firstElementChild(); 765 while (next != null) { 766 if (next.normalName().equals(element.normalName())) 767 pos++; 768 if (pos > 1) 769 break; 770 next = next.nextElementSibling(); 771 } 772 return pos == 1; 773 } 774 @Override 775 public String toString() { 776 return ":only-of-type"; 777 } 778 } 779 780 public static final class IsEmpty extends Evaluator { 781 @Override 782 public boolean matches(Element root, Element el) { 783 for (Node n = el.firstChild(); n != null; n = n.nextSibling()) { 784 if (n instanceof TextNode) { 785 if (!((TextNode) n).isBlank()) 786 return false; // non-blank text: not empty 787 } else if (!(n instanceof Comment || n instanceof XmlDeclaration || n instanceof DocumentType)) 788 return false; // non "blank" element: not empty 789 } 790 return true; 791 } 792 793 @Override 794 public String toString() { 795 return ":empty"; 796 } 797 } 798 799 /** 800 * Abstract evaluator for sibling index matching 801 * 802 * @author ant 803 */ 804 public abstract static class IndexEvaluator extends Evaluator { 805 final int index; 806 807 public IndexEvaluator(int index) { 808 this.index = index; 809 } 810 } 811 812 /** 813 * Evaluator for matching Element (and its descendants) text 814 */ 815 public static final class ContainsText extends Evaluator { 816 private final String searchText; 817 818 public ContainsText(String searchText) { 819 this.searchText = lowerCase(normaliseWhitespace(searchText)); 820 } 821 822 @Override 823 public boolean matches(Element root, Element element) { 824 return lowerCase(element.text()).contains(searchText); 825 } 826 827 @Override protected int cost() { 828 return 10; 829 } 830 831 @Override 832 public String toString() { 833 return String.format(":contains(%s)", searchText); 834 } 835 } 836 837 /** 838 * Evaluator for matching Element (and its descendants) wholeText. Neither the input nor the element text is 839 * normalized. <code>:containsWholeText()</code> 840 * @since 1.15.1. 841 */ 842 public static final class ContainsWholeText extends Evaluator { 843 private final String searchText; 844 845 public ContainsWholeText(String searchText) { 846 this.searchText = searchText; 847 } 848 849 @Override 850 public boolean matches(Element root, Element element) { 851 return element.wholeText().contains(searchText); 852 } 853 854 @Override protected int cost() { 855 return 10; 856 } 857 858 @Override 859 public String toString() { 860 return String.format(":containsWholeText(%s)", searchText); 861 } 862 } 863 864 /** 865 * Evaluator for matching Element (but <b>not</b> its descendants) wholeText. Neither the input nor the element text is 866 * normalized. <code>:containsWholeOwnText()</code> 867 * @since 1.15.1. 868 */ 869 public static final class ContainsWholeOwnText extends Evaluator { 870 private final String searchText; 871 872 public ContainsWholeOwnText(String searchText) { 873 this.searchText = searchText; 874 } 875 876 @Override 877 public boolean matches(Element root, Element element) { 878 return element.wholeOwnText().contains(searchText); 879 } 880 881 @Override 882 public String toString() { 883 return String.format(":containsWholeOwnText(%s)", searchText); 884 } 885 } 886 887 /** 888 * Evaluator for matching Element (and its descendants) data 889 */ 890 public static final class ContainsData extends Evaluator { 891 private final String searchText; 892 893 public ContainsData(String searchText) { 894 this.searchText = lowerCase(searchText); 895 } 896 897 @Override 898 public boolean matches(Element root, Element element) { 899 return lowerCase(element.data()).contains(searchText); // not whitespace normalized 900 } 901 902 @Override 903 public String toString() { 904 return String.format(":containsData(%s)", searchText); 905 } 906 } 907 908 /** 909 * Evaluator for matching Element's own text 910 */ 911 public static final class ContainsOwnText extends Evaluator { 912 private final String searchText; 913 914 public ContainsOwnText(String searchText) { 915 this.searchText = lowerCase(normaliseWhitespace(searchText)); 916 } 917 918 @Override 919 public boolean matches(Element root, Element element) { 920 return lowerCase(element.ownText()).contains(searchText); 921 } 922 923 @Override 924 public String toString() { 925 return String.format(":containsOwn(%s)", searchText); 926 } 927 } 928 929 /** 930 * Evaluator for matching Element (and its descendants) text with regex 931 */ 932 public static final class Matches extends Evaluator { 933 private final Regex pattern; 934 935 public Matches(Regex pattern) { 936 this.pattern = pattern; 937 } 938 939 public Matches(Pattern pattern) { 940 this(Regex.fromPattern(pattern)); 941 } 942 943 @Override 944 public boolean matches(Element root, Element element) { 945 return pattern.matcher(element.text()).find(); 946 } 947 948 @Override protected int cost() { 949 return 8; 950 } 951 952 @Override 953 public String toString() { 954 return String.format(":matches(%s)", pattern); 955 } 956 } 957 958 /** 959 * Evaluator for matching Element's own text with regex 960 */ 961 public static final class MatchesOwn extends Evaluator { 962 private final Regex pattern; 963 964 public MatchesOwn(Regex pattern) { 965 this.pattern = pattern; 966 } 967 968 public MatchesOwn(Pattern pattern) { 969 this(Regex.fromPattern(pattern)); 970 } 971 972 @Override 973 public boolean matches(Element root, Element element) { 974 return pattern.matcher(element.ownText()).find(); 975 } 976 977 @Override protected int cost() { 978 return 7; 979 } 980 981 @Override 982 public String toString() { 983 return String.format(":matchesOwn(%s)", pattern); 984 } 985 } 986 987 /** 988 * Evaluator for matching Element (and its descendants) whole text with regex. 989 * @since 1.15.1. 990 */ 991 public static final class MatchesWholeText extends Evaluator { 992 private final Regex pattern; 993 994 public MatchesWholeText(Regex pattern) { 995 this.pattern = pattern; 996 } 997 998 public MatchesWholeText(Pattern pattern) { 999 this.pattern = Regex.fromPattern(pattern); 1000 } 1001 1002 @Override 1003 public boolean matches(Element root, Element element) { 1004 return pattern.matcher(element.wholeText()).find(); 1005 } 1006 1007 @Override protected int cost() { 1008 return 8; 1009 } 1010 1011 @Override 1012 public String toString() { 1013 return String.format(":matchesWholeText(%s)", pattern); 1014 } 1015 } 1016 1017 /** 1018 * Evaluator for matching Element's own whole text with regex. 1019 * @since 1.15.1. 1020 */ 1021 public static final class MatchesWholeOwnText extends Evaluator { 1022 private final Regex pattern; 1023 1024 public MatchesWholeOwnText(Regex pattern) { 1025 this.pattern = pattern; 1026 } 1027 1028 public MatchesWholeOwnText(Pattern pattern) { 1029 this(Regex.fromPattern(pattern)); 1030 } 1031 1032 @Override 1033 public boolean matches(Element root, Element element) { 1034 Regex.Matcher m = pattern.matcher(element.wholeOwnText()); 1035 return m.find(); 1036 } 1037 1038 @Override protected int cost() { 1039 return 7; 1040 } 1041 1042 @Override 1043 public String toString() { 1044 return String.format(":matchesWholeOwnText(%s)", pattern); 1045 } 1046 } 1047 1048 /** 1049 @deprecated This selector is deprecated and will be removed in jsoup 1.24.1. Migrate to <code>::textnode</code> using the <code>Element#selectNodes()</code> method instead. 1050 */ 1051 @Deprecated 1052 public static final class MatchText extends Evaluator { 1053 private static boolean loggedError = false; 1054 1055 public MatchText() { 1056 // log a deprecated error on first use; users typically won't directly construct this Evaluator and so won't otherwise get deprecation warnings 1057 if (!loggedError) { 1058 loggedError = true; 1059 System.err.println("WARNING: :matchText selector is deprecated and will be removed in jsoup 1.24.1. Use Element#selectNodes(String, Class) with selector ::textnode and class TextNode instead."); 1060 } 1061 } 1062 1063 @Override 1064 public boolean matches(Element root, Element element) { 1065 if (element instanceof PseudoTextElement) 1066 return true; 1067 1068 List<TextNode> textNodes = element.textNodes(); 1069 for (TextNode textNode : textNodes) { 1070 PseudoTextElement pel = new PseudoTextElement( 1071 org.jsoup.parser.Tag.valueOf(element.tagName(), element.tag().namespace(), ParseSettings.preserveCase), element.baseUri(), element.attributes()); 1072 textNode.replaceWith(pel); 1073 pel.appendChild(textNode); 1074 } 1075 return false; 1076 } 1077 1078 @Override protected int cost() { 1079 return -1; // forces first evaluation, which prepares the DOM for later evaluator matches 1080 } 1081 1082 @Override 1083 public String toString() { 1084 return ":matchText"; 1085 } 1086 } 1087}