001package org.jsoup.select; 002 003import org.jsoup.helper.Validate; 004import org.jsoup.internal.LegacyMatching; 005import org.jsoup.internal.CssMatching; 006import org.jsoup.internal.CssMatching.AttributeOperator; 007import org.jsoup.internal.CssMatching.CaseSensitivity; 008import org.jsoup.internal.QueryMode; 009import org.jsoup.nodes.Comment; 010import org.jsoup.nodes.Document; 011import org.jsoup.nodes.DocumentType; 012import org.jsoup.nodes.Element; 013import org.jsoup.nodes.LeafNode; 014import org.jsoup.nodes.Node; 015import org.jsoup.nodes.ProcessingInstruction; 016import org.jsoup.nodes.TextNode; 017import org.jsoup.nodes.XmlDeclaration; 018import org.jsoup.helper.Regex; 019import org.jsoup.parser.TokenQueue; 020import org.jspecify.annotations.Nullable; 021 022import java.util.List; 023import java.util.function.Predicate; 024import java.util.function.Supplier; 025import java.util.regex.Pattern; 026 027import static org.jsoup.internal.Normalizer.lowerCase; 028import static org.jsoup.internal.Normalizer.asciiLowerCase; 029import static org.jsoup.internal.StringUtil.trimAsciiWhitespace; 030import static org.jsoup.internal.StringUtil.normaliseWhitespace; 031import static org.jsoup.internal.StringUtil.isWhitespace; 032 033 034/** 035 An Evaluator tests if an element (or a node) meets the selector's requirements. Obtain an evaluator for a given CSS selector 036 with {@link Selector#evaluatorOf(String css)}. If you are executing the same selector on many elements (or documents), it 037 can be more efficient to compile and reuse an Evaluator than to reparse the selector on each invocation of select(). 038 <p>Each evaluator captures the behavior selected by {@code jsoup.queryMode} when constructed. 039 Standalone evaluators use legacy behavior in {@code check} mode; they do not report compatibility differences.</p> 040 <p>If you construct and combine evaluators yourself, each child keeps the matching rules selected when it was created. 041 The outer evaluator determines whether searches include the starting element and which element supplies the query context.</p> 042 <p>Evaluators are thread-safe and may be used concurrently across multiple documents.</p> 043 044 @see <a href="{@docRoot}/org/jsoup/select/Selector.html#query-compatibility">Query compatibility changes</a> */ 045public abstract class Evaluator { 046 final boolean web; // web query semantics or legacy 047 048 private static final ThreadLocal<@Nullable Boolean> QueryModeOverride = new ThreadLocal<>(); 049 050 /** 051 Sets the mode for evaluators created by the action, then restores the previous mode. 052 */ 053 static <T> T withMode(boolean web, Supplier<T> action) { 054 Boolean previous = QueryModeOverride.get(); 055 QueryModeOverride.set(web); 056 try { 057 return action.get(); 058 } finally { 059 if (previous == null) QueryModeOverride.remove(); 060 else QueryModeOverride.set(previous); 061 } 062 } 063 064 /** 065 Returns the supplied element. If it is a Document, returns its first element child instead, 066 or null if it has no element children. 067 */ 068 static @Nullable Element documentElementOrSelf(Element element) { 069 return element instanceof Document ? element.firstElementChild() : element; 070 } 071 072 protected Evaluator() { 073 Boolean web = QueryModeOverride.get(); 074 this.web = web != null ? web : QueryMode.current() == QueryMode.Web; 075 } 076 077 /** 078 Provides a Predicate for this Evaluator, matching the test Element. 079 * @param root the element the query starts from; see {@link #matches(Element, Element)} 080 * @return a predicate that accepts an Element to test for matches with this Evaluator 081 * @since 1.17.1 082 */ 083 public Predicate<Element> asPredicate(Element root) { 084 return element -> matches(root, element); 085 } 086 087 Predicate<Node> asNodePredicate(Element root) { 088 return node -> matches(root, node); 089 } 090 091 /** 092 Tests whether the element matches this evaluator. 093 094 @param root the query context, used by {@code :scope}; for a Document, {@code :scope} matches its document element 095 @param element the element to test 096 @return whether the element matches 097 */ 098 public abstract boolean matches(Element root, Element element); 099 100 /** 101 Tests a {@code :has()} candidate starting at {@code anchor}, keeping {@code root} as the query context. 102 The caller has already checked whether the query accepts this node type. 103 */ 104 boolean matchesRelative(Element root, Node node, Element anchor) { 105 return node instanceof Element ? matches(root, (Element) node) : matches(root, (LeafNode) node); 106 } 107 108 /** Checks node eligibility before relative matching; a null anchor uses ordinary matching. */ 109 final boolean matches(Element root, Node node, @Nullable Element anchor) { 110 if (anchor == null) return matches(root, node); 111 if (node instanceof Element || node instanceof LeafNode && wantsNodes()) 112 return matchesRelative(root, node, anchor); 113 return false; 114 } 115 116 final boolean matches(Element root, Node node) { 117 if (node instanceof Element) { 118 return matches(root, (Element) node); 119 } else if (node instanceof LeafNode && wantsNodes()) { 120 return matches(root, (LeafNode) node); 121 } 122 return false; 123 } 124 125 boolean matches(Element root, LeafNode leafNode) { 126 return false; 127 } 128 129 boolean wantsNodes() { 130 return false; 131 } 132 133 /** 134 Reset any internal state in this Evaluator before executing a new Collector evaluation. 135 */ 136 protected void reset() { 137 } 138 139 final boolean sameType(Node first, Element second) { 140 return web ? CssMatching.sameType(first, second) : first.normalName().equals(second.normalName()); 141 } 142 143 /** 144 A relative evaluator cost function. During evaluation, Evaluators are sorted by ascending cost as an optimization. 145 * @return the relative cost of this Evaluator 146 */ 147 protected int cost() { 148 return 5; // a nominal default cost 149 } 150 151 /** 152 * Evaluator for tag name 153 */ 154 public static final class Tag extends Evaluator { 155 private final String tagName; 156 157 public Tag(String tagName) { 158 this.tagName = web ? tagName : asciiLowerCase(tagName); 159 } 160 161 @Override 162 public boolean matches(Element root, Element element) { 163 if (!web) return element.nameIs(tagName); 164 return !CssMatching.isHtmlElement(root, element) ? element.tagName().equals(tagName) : 165 element.nameIs(asciiLowerCase(tagName)); 166 } 167 168 @Override protected int cost() { 169 return 1; 170 } 171 172 @Override 173 public String toString() { 174 return String.format("%s", tagName); 175 } 176 } 177 178 /** 179 * Evaluator for tag name that starts with prefix; used for ns|* 180 */ 181 public static final class TagStartsWith extends Evaluator { 182 private final String tagName; 183 184 public TagStartsWith(String tagName) { 185 this.tagName = tagName; 186 } 187 188 @Override 189 public boolean matches(Element root, Element element) { 190 boolean insensitive = !web || CssMatching.isHtmlElement(root, element); 191 String name = insensitive ? element.normalName() : element.tagName(); 192 return name.startsWith(insensitive ? asciiLowerCase(tagName) : tagName); 193 } 194 195 @Override 196 public String toString() { 197 return String.format("%s|*", tagName); 198 } 199 } 200 201 202 /** 203 * Evaluator for tag name that ends with suffix; used for *|el 204 */ 205 public static final class TagEndsWith extends Evaluator { 206 private final String tagName; 207 208 public TagEndsWith(String tagName) { 209 this.tagName = tagName; 210 } 211 212 @Override 213 public boolean matches(Element root, Element element) { 214 boolean insensitive = !web || CssMatching.isHtmlElement(root, element); 215 String name = insensitive ? element.normalName() : element.tagName(); 216 return name.endsWith(insensitive ? asciiLowerCase(tagName) : tagName); 217 } 218 219 @Override 220 public String toString() { 221 return String.format("*|%s", tagName); 222 } 223 } 224 225 /** 226 * Evaluator for element id 227 */ 228 public static final class Id extends Evaluator { 229 private final String id; 230 231 public Id(String id) { 232 this.id = id; 233 } 234 235 @Override 236 public boolean matches(Element root, Element element) { 237 if (!web) return id.equals(element.id()); 238 String actual = CssMatching.attributeValue(element, "id", CssMatching.isHtmlElement(root, element)); 239 return actual != null && (CssMatching.isQuirksMode(root) ? org.jsoup.internal.Normalizer.equalsIgnoreAsciiCase(actual, id) : actual.equals(id)); 240 } 241 242 @Override protected int cost() { 243 return 2; 244 } 245 @Override 246 public String toString() { 247 return String.format("#%s", id); 248 } 249 } 250 251 /** 252 * Evaluator for element class 253 */ 254 public static final class Class extends Evaluator { 255 private final String className; 256 257 public Class(String className) { 258 this.className = className; 259 } 260 261 @Override 262 public boolean matches(Element root, Element element) { 263 if (!web) return LegacyMatching.hasClass(element, className); 264 return CssMatching.hasClass(root, element, className); 265 } 266 267 @Override protected int cost() { 268 return 8; // does whitespace scanning; more than .contains() 269 } 270 271 @Override 272 public String toString() { 273 return String.format(".%s", className); 274 } 275 276 } 277 278 /** Shares attribute matching between elements and processing instructions. */ 279 abstract static class AttributeEvaluator extends Evaluator { 280 @Override public boolean matches(Element root, Element element) { 281 return matchesNode(root, element); 282 } 283 284 @Override boolean matches(Element root, LeafNode leafNode) { 285 return leafNode instanceof ProcessingInstruction && matchesNode(root, leafNode); 286 } 287 288 /** Tests this evaluator against a node's attributes. */ 289 boolean matchesNode(Element root, Node node) { 290 return false; 291 } 292 293 @Nullable String attribute(Element root, Node node, String key) { 294 return web ? CssMatching.attributeValue(node, key, CssMatching.isHtmlElement(root, node)) : 295 LegacyMatching.attribute(node, key); 296 } 297 298 boolean matchesValue(Element root, Node node, String key, String value, AttributeOperator operator, 299 CaseSensitivity flag) { 300 boolean ignoreCase = flag == CaseSensitivity.Insensitive || flag == CaseSensitivity.Default && 301 CssMatching.isHtmlElement(root, node) && CssMatching.isCaseInsensitiveAttribute(key); 302 return CssMatching.matchesAttributeValue(attribute(root, node, key), value, operator, ignoreCase); 303 } 304 } 305 306 /** 307 * Evaluator for attribute name matching 308 */ 309 public static final class Attribute extends AttributeEvaluator { 310 private final String key; 311 312 public Attribute(String key) { 313 this.key = key; 314 } 315 316 @Override boolean matchesNode(Element root, Node node) { 317 return web ? attribute(root, node, key) != null : node.hasAttr(asciiLowerCase(key)); 318 } 319 320 @Override protected int cost() { 321 return 2; 322 } 323 324 @Override 325 public String toString() { 326 return String.format("[%s]", key); 327 } 328 } 329 330 /** 331 * Evaluator for attribute name prefix matching 332 */ 333 public static final class AttributeStarting extends AttributeEvaluator { 334 private final String keyPrefix; 335 336 public AttributeStarting(String keyPrefix) { 337 Validate.notNull(keyPrefix); // OK to be empty - will find elements with any attributes 338 this.keyPrefix = keyPrefix; 339 } 340 341 @Override boolean matchesNode(Element root, Node node) { 342 List<org.jsoup.nodes.Attribute> values = node.attributes().asList(); 343 for (org.jsoup.nodes.Attribute attribute : values) { 344 boolean insensitive = !web || CssMatching.isHtmlElement(root, node) && attribute.namespace().isEmpty(); 345 String name = insensitive ? asciiLowerCase(attribute.getKey()) : attribute.getKey(); 346 if (name.startsWith(insensitive ? asciiLowerCase(keyPrefix) : keyPrefix)) 347 return true; 348 } 349 return false; 350 } 351 352 @Override protected int cost() { 353 return 6; 354 } 355 356 @Override 357 public String toString() { 358 return String.format("[^%s]", keyPrefix); 359 } 360 361 } 362 363 /** 364 * Evaluator for attribute name/value matching 365 */ 366 public static final class AttributeWithValue extends AttributeKeyPair { 367 public AttributeWithValue(String key, String value) { 368 super(key, value); 369 } 370 371 AttributeWithValue(String key, String value, CaseSensitivity flag) { 372 super(key, value, flag); 373 } 374 375 @Override boolean matchesNode(Element root, Node node) { 376 return matchesValue(root, node, AttributeOperator.Equals); 377 } 378 379 @Override protected int cost() { 380 return 3; 381 } 382 383 @Override 384 public String toString() { 385 return attributeString("="); 386 } 387 388 } 389 390 /** 391 * Evaluator for attribute name != value matching 392 */ 393 public static final class AttributeWithValueNot extends AttributeKeyPair { 394 public AttributeWithValueNot(String key, String value) { 395 super(key, value); 396 } 397 398 AttributeWithValueNot(String key, String value, CaseSensitivity flag) { 399 super(key, value, flag); 400 } 401 402 @Override boolean matchesNode(Element root, Node node) { 403 return matchesValue(root, node, AttributeOperator.NotEquals); 404 } 405 406 @Override protected int cost() { 407 return 3; 408 } 409 410 @Override 411 public String toString() { 412 return attributeString("!="); 413 } 414 415 } 416 417 /** 418 * Evaluator for attribute name/value matching (value prefix) 419 */ 420 public static final class AttributeWithValueStarting extends AttributeKeyPair { 421 public AttributeWithValueStarting(String key, String value) { 422 super(key, value); 423 } 424 425 AttributeWithValueStarting(String key, String value, CaseSensitivity flag) { 426 super(key, value, flag); 427 } 428 429 @Override boolean matchesNode(Element root, Node node) { 430 return matchesValue(root, node, AttributeOperator.StartsWith); 431 } 432 433 @Override protected int cost() { 434 return 4; 435 } 436 437 @Override 438 public String toString() { 439 return attributeString("^="); 440 } 441 } 442 443 /** 444 * Evaluator for attribute name/value matching (value ending) 445 */ 446 public static final class AttributeWithValueEnding extends AttributeKeyPair { 447 public AttributeWithValueEnding(String key, String value) { 448 super(key, value); 449 } 450 451 AttributeWithValueEnding(String key, String value, CaseSensitivity flag) { 452 super(key, value, flag); 453 } 454 455 @Override boolean matchesNode(Element root, Node node) { 456 return matchesValue(root, node, AttributeOperator.EndsWith); 457 } 458 459 @Override protected int cost() { 460 return 4; 461 } 462 463 @Override 464 public String toString() { 465 return attributeString("$="); 466 } 467 } 468 469 /** 470 * Evaluator for attribute name/value matching (value containing) 471 */ 472 public static final class AttributeWithValueContaining extends AttributeKeyPair { 473 public AttributeWithValueContaining(String key, String value) { 474 super(key, value); 475 } 476 477 AttributeWithValueContaining(String key, String value, CaseSensitivity flag) { 478 super(key, value, flag); 479 } 480 481 @Override boolean matchesNode(Element root, Node node) { 482 return matchesValue(root, node, AttributeOperator.Contains); 483 } 484 485 @Override protected int cost() { 486 return 6; 487 } 488 489 @Override 490 public String toString() { 491 return attributeString("*="); 492 } 493 494 } 495 496 /** Matches a complete attribute token, as in [class~=card]. */ 497 static final class AttributeWithValueContainingToken extends AttributeKeyPair { 498 AttributeWithValueContainingToken(String key, String value, CaseSensitivity flag) { 499 super(key, value, flag); 500 } 501 502 @Override boolean matchesNode(Element root, Node node) { 503 return matchesValue(root, node, AttributeOperator.ContainsToken); 504 } 505 506 @Override protected int cost() { 507 return 8; 508 } 509 510 @Override public String toString() { 511 return attributeString("~="); 512 } 513 } 514 515 /** Matches an attribute value or its hyphen-separated prefix, as in [lang|=en]. */ 516 static final class AttributeWithValueDashPrefix extends AttributeKeyPair { 517 AttributeWithValueDashPrefix(String key, String value, CaseSensitivity flag) { 518 super(key, value, flag); 519 } 520 521 @Override boolean matchesNode(Element root, Node node) { 522 return matchesValue(root, node, AttributeOperator.DashMatch); 523 } 524 525 @Override protected int cost() { 526 return 4; 527 } 528 529 @Override public String toString() { 530 return attributeString("|="); 531 } 532 } 533 534 /** 535 * Evaluator for attribute name/value matching (value regex matching) 536 */ 537 public static final class AttributeWithValueMatching extends AttributeEvaluator { 538 final String key; 539 final Regex pattern; 540 541 public AttributeWithValueMatching(String key, Regex pattern) { 542 this.key = trimAsciiWhitespace(key); // preserve key case in serialized queries 543 this.pattern = pattern; 544 } 545 546 public AttributeWithValueMatching(String key, Pattern pattern) { 547 this(key, Regex.fromPattern(pattern)); 548 } 549 550 @Override boolean matchesNode(Element root, Node node) { 551 String value = attribute(root, node, key); 552 return value != null && pattern.matcher(value).find(); 553 } 554 555 @Override protected int cost() { 556 return 8; 557 } 558 559 @Override 560 public String toString() { 561 if (!web) return String.format("[%s~=%s]", key, pattern); 562 String regex = pattern.toString(); 563 // protect leading regex whitespace from the selector's whitespace after the comma 564 if (!regex.isEmpty() && isWhitespace(regex.charAt(0))) regex = "(?:)" + regex; 565 return String.format(":matchesAttr(%s, %s)", TokenQueue.escapeCssIdentifier(key), regex); 566 } 567 568 } 569 570 /** 571 * Abstract evaluator for attribute name/value matching 572 */ 573 public abstract static class AttributeKeyPair extends AttributeEvaluator { 574 final String key; 575 final String value; 576 final CaseSensitivity flag; 577 private final boolean parsedCss; // parsed operands are already decoded; public inputs retain quote handling 578 private final char quote; // retains input quoting when unquoted output could look like a case flag 579 580 public AttributeKeyPair(String key, String value) { 581 Validate.notEmpty(key); 582 Validate.notNull(value); 583 584 key = trimAsciiWhitespace(key); 585 this.key = web ? key : asciiLowerCase(key); 586 boolean quoted = value.startsWith("'") && value.endsWith("'") 587 || value.startsWith("\"") && value.endsWith("\""); 588 quote = quoted ? value.charAt(0) : 0; 589 if (quoted) { 590 Validate.isTrue(value.length() > 1, "Quoted value must have content"); 591 value = value.substring(1, value.length() - 1); 592 } 593 594 this.value = web ? value : lowerCase(value); 595 flag = CaseSensitivity.Default; 596 parsedCss = false; 597 } 598 599 AttributeKeyPair(String key, String value, CaseSensitivity flag) { 600 this.key = key; 601 this.value = value; 602 this.flag = flag; 603 parsedCss = true; 604 quote = 0; 605 } 606 607 boolean matchesValue(Element root, Node node, AttributeOperator operator) { 608 if (web) return super.matchesValue(root, node, key, value, operator, flag); 609 return parsedCss ? LegacyMatching.cssAttributeMatches(node, key, value, operator, flag) : 610 LegacyMatching.attributeMatches(node, key, value, operator); 611 } 612 613 String attributeString(String operator) { 614 if (!parsedCss && !web) { 615 return LegacyMatching.attributeString(key, value, operator, quote); 616 } 617 String cssValue = value.isEmpty() ? "\"\"" : TokenQueue.escapeCssIdentifier(value); 618 String suffix = flag == CaseSensitivity.Insensitive ? " i" : flag == CaseSensitivity.Sensitive ? " s" : ""; 619 return String.format("[%s%s%s%s]", TokenQueue.escapeCssIdentifier(key), operator, cssValue, suffix); 620 } 621 } 622 623 /** 624 * Evaluator for any / all element matching 625 */ 626 public static final class AllElements extends Evaluator { 627 628 @Override 629 public boolean matches(Element root, Element element) { 630 return true; 631 } 632 633 @Override protected int cost() { 634 return 10; 635 } 636 637 @Override 638 public String toString() { 639 return "*"; 640 } 641 } 642 643 /** 644 * Evaluator for matching by sibling index number (e {@literal <} idx) 645 */ 646 public static final class IndexLessThan extends IndexEvaluator { 647 public IndexLessThan(int index) { 648 super(index); 649 } 650 651 @Override 652 public boolean matches(Element root, Element element) { 653 return (web || root != element) && element.elementSiblingIndex() < index; 654 } 655 656 @Override 657 public String toString() { 658 return String.format(":lt(%d)", index); 659 } 660 661 } 662 663 /** 664 * Evaluator for matching by sibling index number (e {@literal >} idx) 665 */ 666 public static final class IndexGreaterThan extends IndexEvaluator { 667 public IndexGreaterThan(int index) { 668 super(index); 669 } 670 671 @Override 672 public boolean matches(Element root, Element element) { 673 return element.elementSiblingIndex() > index; 674 } 675 676 @Override 677 public String toString() { 678 return String.format(":gt(%d)", index); 679 } 680 681 } 682 683 /** 684 * Evaluator for matching by sibling index number (e = idx) 685 */ 686 public static final class IndexEquals extends IndexEvaluator { 687 public IndexEquals(int index) { 688 super(index); 689 } 690 691 @Override 692 public boolean matches(Element root, Element element) { 693 return element.elementSiblingIndex() == index; 694 } 695 696 @Override 697 public String toString() { 698 return String.format(":eq(%d)", index); 699 } 700 701 } 702 703 /** 704 * Evaluator for matching the last sibling (css :last-child) 705 */ 706 public static final class IsLastChild extends Evaluator { 707 @Override 708 public boolean matches(Element root, Element element) { 709 final Element p = element.parent(); 710 return p != null && !(p instanceof Document) && element == p.lastElementChild(); 711 } 712 713 @Override 714 public String toString() { 715 return ":last-child"; 716 } 717 } 718 719 public static final class IsFirstOfType extends IsNthOfType { 720 public IsFirstOfType() { 721 super(0, 1); 722 } 723 724 @Override 725 public String toString() { 726 return ":first-of-type"; 727 } 728 } 729 730 public static final class IsLastOfType extends IsNthLastOfType { 731 public IsLastOfType() { 732 super(0, 1); 733 } 734 735 @Override 736 public String toString() { 737 return ":last-of-type"; 738 } 739 } 740 741 742 public static abstract class CssNthEvaluator extends Evaluator { 743 /** Step */ 744 protected final int a; 745 /** Offset */ 746 protected final int b; 747 748 public CssNthEvaluator(int step, int offset) { 749 this.a = step; 750 this.b = offset; 751 } 752 753 public CssNthEvaluator(int offset) { 754 this(0, offset); 755 } 756 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 final int pos = calculatePosition(root, element); 763 if (a == 0) return pos == b; 764 765 return (pos - b) * a >= 0 && (pos - b) % a == 0; 766 } 767 768 @Override 769 public String toString() { 770 String format = 771 (a == 0) ? ":%s(%3$d)" // only offset (b) 772 : (b == 0) ? ":%s(%2$dn)" // only step (a) 773 : ":%s(%2$dn%3$+d)"; // step, offset 774 return String.format(format, getPseudoClass(), a, b); 775 } 776 777 protected abstract String getPseudoClass(); 778 779 protected abstract int calculatePosition(Element root, Element element); 780 } 781 782 783 /** 784 * css-compatible Evaluator for :eq (css :nth-child) 785 * 786 * @see IndexEquals 787 */ 788 public static final class IsNthChild extends CssNthEvaluator { 789 public IsNthChild(int step, int offset) { 790 super(step, offset); 791 } 792 793 @Override 794 protected int calculatePosition(Element root, Element element) { 795 return element.elementSiblingIndex() + 1; 796 } 797 798 @Override 799 protected String getPseudoClass() { 800 return "nth-child"; 801 } 802 } 803 804 /** 805 * css pseudo class :nth-last-child) 806 * 807 * @see IndexEquals 808 */ 809 public static final class IsNthLastChild extends CssNthEvaluator { 810 public IsNthLastChild(int step, int offset) { 811 super(step, offset); 812 } 813 814 @Override 815 protected int calculatePosition(Element root, Element element) { 816 if (element.parent() == null) return 0; 817 return element.parent().childrenSize() - element.elementSiblingIndex(); 818 } 819 820 @Override 821 protected String getPseudoClass() { 822 return "nth-last-child"; 823 } 824 } 825 826 /** 827 * css pseudo class nth-of-type 828 * 829 */ 830 public static class IsNthOfType extends CssNthEvaluator { 831 public IsNthOfType(int step, int offset) { 832 super(step, offset); 833 } 834 835 @Override protected int calculatePosition(Element root, Element element) { 836 Element parent = element.parent(); 837 if (parent == null) 838 return 0; 839 840 int pos = 0; 841 final int size = parent.childNodeSize(); 842 for (int i = 0; i < size; i++) { 843 Node node = parent.childNode(i); 844 if (sameType(node, element)) pos++; 845 if (node == element) break; 846 } 847 return pos; 848 } 849 850 @Override 851 protected String getPseudoClass() { 852 return "nth-of-type"; 853 } 854 } 855 856 public static class IsNthLastOfType extends CssNthEvaluator { 857 public IsNthLastOfType(int step, int offset) { 858 super(step, offset); 859 } 860 861 @Override 862 protected int calculatePosition(Element root, Element element) { 863 Element parent = element.parent(); 864 if (parent == null) 865 return 0; 866 867 int pos = 0; 868 Element next = element; 869 while (next != null) { 870 if (sameType(next, element)) 871 pos++; 872 next = next.nextElementSibling(); 873 } 874 return pos; 875 } 876 877 @Override 878 protected String getPseudoClass() { 879 return "nth-last-of-type"; 880 } 881 } 882 883 /** 884 * Evaluator for matching the first sibling (css :first-child) 885 */ 886 public static final class IsFirstChild extends Evaluator { 887 @Override 888 public boolean matches(Element root, Element element) { 889 final Element p = element.parent(); 890 return p != null && !(p instanceof Document) && element == p.firstElementChild(); 891 } 892 893 @Override 894 public String toString() { 895 return ":first-child"; 896 } 897 } 898 899 /** 900 * css3 pseudo-class :root 901 * @see <a href="http://www.w3.org/TR/selectors/#root-pseudo">:root selector</a> 902 * 903 */ 904 public static final class IsRoot extends Evaluator { 905 @Override 906 public boolean matches(Element root, Element element) { 907 return element == documentElementOrSelf(root.root()); 908 } 909 910 @Override protected int cost() { 911 return 1; 912 } 913 914 @Override 915 public String toString() { 916 return ":root"; 917 } 918 } 919 920 /** 921 Matches the original query scope, including within relative selectors. 922 */ 923 static final class IsScope extends Evaluator { 924 @Override public boolean matches(Element root, Element element) { 925 return element == documentElementOrSelf(root); 926 } 927 928 @Override protected int cost() { 929 return 1; 930 } 931 932 @Override public String toString() { 933 return ":scope"; 934 } 935 } 936 937 public static final class IsOnlyChild extends Evaluator { 938 @Override 939 public boolean matches(Element root, Element element) { 940 final Element p = element.parent(); 941 return p!=null && !(p instanceof Document) && element.siblingElements().isEmpty(); 942 } 943 @Override 944 public String toString() { 945 return ":only-child"; 946 } 947 } 948 949 public static final class IsOnlyOfType extends Evaluator { 950 @Override 951 public boolean matches(Element root, Element element) { 952 final Element p = element.parent(); 953 if (p==null || p instanceof Document) return false; 954 955 int pos = 0; 956 Element next = p.firstElementChild(); 957 while (next != null) { 958 if (sameType(next, element)) 959 pos++; 960 if (pos > 1) 961 break; 962 next = next.nextElementSibling(); 963 } 964 return pos == 1; 965 } 966 @Override 967 public String toString() { 968 return ":only-of-type"; 969 } 970 } 971 972 public static final class IsEmpty extends Evaluator { 973 @Override 974 public boolean matches(Element root, Element el) { 975 for (Node n = el.firstChild(); n != null; n = n.nextSibling()) { 976 if (n instanceof TextNode) { 977 if (!((TextNode) n).isBlank()) 978 return false; // non-blank text: not empty 979 } else if (!(n instanceof Comment || n instanceof ProcessingInstruction || 980 n instanceof XmlDeclaration || n instanceof DocumentType)) 981 return false; // non "blank" element: not empty 982 } 983 return true; 984 } 985 986 @Override 987 public String toString() { 988 return ":empty"; 989 } 990 } 991 992 /** 993 * Abstract evaluator for sibling index matching 994 * 995 * @author ant 996 */ 997 public abstract static class IndexEvaluator extends Evaluator { 998 final int index; 999 1000 public IndexEvaluator(int index) { 1001 this.index = index; 1002 } 1003 } 1004 1005 /** 1006 * Evaluator for matching Element (and its descendants) text 1007 */ 1008 public static final class ContainsText extends Evaluator { 1009 private final String searchText; 1010 1011 public ContainsText(String searchText) { 1012 this.searchText = lowerCase(normaliseWhitespace(searchText)); 1013 } 1014 1015 @Override 1016 public boolean matches(Element root, Element element) { 1017 return lowerCase(element.text()).contains(searchText); 1018 } 1019 1020 @Override protected int cost() { 1021 return 10; 1022 } 1023 1024 @Override 1025 public String toString() { 1026 return String.format(":contains(%s)", searchText); 1027 } 1028 } 1029 1030 /** 1031 * Evaluator for matching Element (and its descendants) wholeText. Neither the input nor the element text is 1032 * normalized. <code>:containsWholeText()</code> 1033 * @since 1.15.1. 1034 */ 1035 public static final class ContainsWholeText extends Evaluator { 1036 private final String searchText; 1037 1038 public ContainsWholeText(String searchText) { 1039 this.searchText = searchText; 1040 } 1041 1042 @Override 1043 public boolean matches(Element root, Element element) { 1044 return element.wholeText().contains(searchText); 1045 } 1046 1047 @Override protected int cost() { 1048 return 10; 1049 } 1050 1051 @Override 1052 public String toString() { 1053 return String.format(":containsWholeText(%s)", searchText); 1054 } 1055 } 1056 1057 /** 1058 * Evaluator for matching Element (but <b>not</b> its descendants) wholeText. Neither the input nor the element text is 1059 * normalized. <code>:containsWholeOwnText()</code> 1060 * @since 1.15.1. 1061 */ 1062 public static final class ContainsWholeOwnText extends Evaluator { 1063 private final String searchText; 1064 1065 public ContainsWholeOwnText(String searchText) { 1066 this.searchText = searchText; 1067 } 1068 1069 @Override 1070 public boolean matches(Element root, Element element) { 1071 return element.wholeOwnText().contains(searchText); 1072 } 1073 1074 @Override 1075 public String toString() { 1076 return String.format(":containsWholeOwnText(%s)", searchText); 1077 } 1078 } 1079 1080 /** 1081 * Evaluator for matching Element (and its descendants) data 1082 */ 1083 public static final class ContainsData extends Evaluator { 1084 private final String searchText; 1085 1086 public ContainsData(String searchText) { 1087 this.searchText = lowerCase(searchText); 1088 } 1089 1090 @Override 1091 public boolean matches(Element root, Element element) { 1092 return lowerCase(element.data()).contains(searchText); // not whitespace normalized 1093 } 1094 1095 @Override 1096 public String toString() { 1097 return String.format(":containsData(%s)", searchText); 1098 } 1099 } 1100 1101 /** 1102 * Evaluator for matching Element's own text 1103 */ 1104 public static final class ContainsOwnText extends Evaluator { 1105 private final String searchText; 1106 1107 public ContainsOwnText(String searchText) { 1108 this.searchText = lowerCase(normaliseWhitespace(searchText)); 1109 } 1110 1111 @Override 1112 public boolean matches(Element root, Element element) { 1113 return lowerCase(element.ownText()).contains(searchText); 1114 } 1115 1116 @Override 1117 public String toString() { 1118 return String.format(":containsOwn(%s)", searchText); 1119 } 1120 } 1121 1122 /** 1123 * Evaluator for matching Element (and its descendants) text with regex 1124 */ 1125 public static final class Matches extends Evaluator { 1126 private final Regex pattern; 1127 1128 public Matches(Regex pattern) { 1129 this.pattern = pattern; 1130 } 1131 1132 public Matches(Pattern pattern) { 1133 this(Regex.fromPattern(pattern)); 1134 } 1135 1136 @Override 1137 public boolean matches(Element root, Element element) { 1138 return pattern.matcher(element.text()).find(); 1139 } 1140 1141 @Override protected int cost() { 1142 return 8; 1143 } 1144 1145 @Override 1146 public String toString() { 1147 return String.format(":matches(%s)", pattern); 1148 } 1149 } 1150 1151 /** 1152 * Evaluator for matching Element's own text with regex 1153 */ 1154 public static final class MatchesOwn extends Evaluator { 1155 private final Regex pattern; 1156 1157 public MatchesOwn(Regex pattern) { 1158 this.pattern = pattern; 1159 } 1160 1161 public MatchesOwn(Pattern pattern) { 1162 this(Regex.fromPattern(pattern)); 1163 } 1164 1165 @Override 1166 public boolean matches(Element root, Element element) { 1167 return pattern.matcher(element.ownText()).find(); 1168 } 1169 1170 @Override protected int cost() { 1171 return 7; 1172 } 1173 1174 @Override 1175 public String toString() { 1176 return String.format(":matchesOwn(%s)", pattern); 1177 } 1178 } 1179 1180 /** 1181 * Evaluator for matching Element (and its descendants) whole text with regex. 1182 * @since 1.15.1. 1183 */ 1184 public static final class MatchesWholeText extends Evaluator { 1185 private final Regex pattern; 1186 1187 public MatchesWholeText(Regex pattern) { 1188 this.pattern = pattern; 1189 } 1190 1191 public MatchesWholeText(Pattern pattern) { 1192 this(Regex.fromPattern(pattern)); 1193 } 1194 1195 @Override 1196 public boolean matches(Element root, Element element) { 1197 return pattern.matcher(element.wholeText()).find(); 1198 } 1199 1200 @Override protected int cost() { 1201 return 8; 1202 } 1203 1204 @Override 1205 public String toString() { 1206 return String.format(":matchesWholeText(%s)", pattern); 1207 } 1208 } 1209 1210 /** 1211 * Evaluator for matching Element's own whole text with regex. 1212 * @since 1.15.1. 1213 */ 1214 public static final class MatchesWholeOwnText extends Evaluator { 1215 private final Regex pattern; 1216 1217 public MatchesWholeOwnText(Regex pattern) { 1218 this.pattern = pattern; 1219 } 1220 1221 public MatchesWholeOwnText(Pattern pattern) { 1222 this(Regex.fromPattern(pattern)); 1223 } 1224 1225 @Override 1226 public boolean matches(Element root, Element element) { 1227 Regex.Matcher m = pattern.matcher(element.wholeOwnText()); 1228 return m.find(); 1229 } 1230 1231 @Override protected int cost() { 1232 return 7; 1233 } 1234 1235 @Override 1236 public String toString() { 1237 return String.format(":matchesWholeOwnText(%s)", pattern); 1238 } 1239 } 1240}