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