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