Changeset 56703
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/html-api/class-wp-html-tag-processor.php
r56684 r56703 624 624 625 625 return true; 626 } 627 628 629 /** 630 * Generator for a foreach loop to step through each class name for the matched tag. 631 * 632 * This generator function is designed to be used inside a "foreach" loop. 633 * 634 * Example: 635 * 636 * $p = new WP_HTML_Tag_Processor( "<div class='free <egg<\tlang-en'>" ); 637 * $p->next_tag(); 638 * foreach ( $p->class_list() as $class_name ) { 639 * echo "{$class_name} "; 640 * } 641 * // Outputs: "free <egg> lang-en " 642 * 643 * @since 6.4.0 644 */ 645 public function class_list() { 646 /** @var string $class contains the string value of the class attribute, with character references decoded. */ 647 $class = $this->get_attribute( 'class' ); 648 649 if ( ! is_string( $class ) ) { 650 return; 651 } 652 653 $seen = array(); 654 655 $at = 0; 656 while ( $at < strlen( $class ) ) { 657 // Skip past any initial boundary characters. 658 $at += strspn( $class, " \t\f\r\n", $at ); 659 if ( $at >= strlen( $class ) ) { 660 return; 661 } 662 663 // Find the byte length until the next boundary. 664 $length = strcspn( $class, " \t\f\r\n", $at ); 665 if ( 0 === $length ) { 666 return; 667 } 668 669 /* 670 * CSS class names are case-insensitive in the ASCII range. 671 * 672 * @see https://www.w3.org/TR/CSS2/syndata.html#x1 673 */ 674 $name = strtolower( substr( $class, $at, $length ) ); 675 $at += $length; 676 677 /* 678 * It's expected that the number of class names for a given tag is relatively small. 679 * Given this, it is probably faster overall to scan an array for a value rather 680 * than to use the class name as a key and check if it's a key of $seen. 681 */ 682 if ( in_array( $name, $seen, true ) ) { 683 continue; 684 } 685 686 $seen[] = $name; 687 yield $name; 688 } 689 } 690 691 692 /** 693 * Returns if a matched tag contains the given ASCII case-insensitive class name. 694 * 695 * @since 6.4.0 696 * 697 * @param string $wanted_class Look for this CSS class name, ASCII case-insensitive. 698 * @return bool|null Whether the matched tag contains the given class name, or null if not matched. 699 */ 700 public function has_class( $wanted_class ) { 701 if ( ! $this->tag_name_starts_at ) { 702 return null; 703 } 704 705 $wanted_class = strtolower( $wanted_class ); 706 707 foreach ( $this->class_list() as $class_name ) { 708 if ( $class_name === $wanted_class ) { 709 return true; 710 } 711 } 712 713 return false; 626 714 } 627 715 … … 2348 2436 } 2349 2437 2350 $needs_class_name = null !== $this->sought_class_name; 2351 2352 if ( $needs_class_name && ! isset( $this->attributes['class'] ) ) { 2438 if ( null !== $this->sought_class_name && ! $this->has_class( $this->sought_class_name ) ) { 2353 2439 return false; 2354 2440 } 2355 2441 2356 /*2357 * Match byte-for-byte (case-sensitive and encoding-form-sensitive) on the class name.2358 *2359 * This will overlook certain classes that exist in other lexical variations2360 * than was supplied to the search query, but requires more complicated searching.2361 */2362 if ( $needs_class_name ) {2363 $class_start = $this->attributes['class']->value_starts_at;2364 $class_end = $class_start + $this->attributes['class']->value_length;2365 $class_at = $class_start;2366 2367 /*2368 * Ensure that boundaries surround the class name to avoid matching on2369 * substrings of a longer name. For example, the sequence "not-odd"2370 * should not match for the class "odd" even though "odd" is found2371 * within the class attribute text.2372 *2373 * See https://html.spec.whatwg.org/#attributes-32374 * See https://html.spec.whatwg.org/#space-separated-tokens2375 */2376 while (2377 // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition2378 false !== ( $class_at = strpos( $this->html, $this->sought_class_name, $class_at ) ) &&2379 $class_at < $class_end2380 ) {2381 /*2382 * Verify this class starts at a boundary.2383 */2384 if ( $class_at > $class_start ) {2385 $character = $this->html[ $class_at - 1 ];2386 2387 if ( ' ' !== $character && "\t" !== $character && "\f" !== $character && "\r" !== $character && "\n" !== $character ) {2388 $class_at += strlen( $this->sought_class_name );2389 continue;2390 }2391 }2392 2393 /*2394 * Verify this class ends at a boundary as well.2395 */2396 if ( $class_at + strlen( $this->sought_class_name ) < $class_end ) {2397 $character = $this->html[ $class_at + strlen( $this->sought_class_name ) ];2398 2399 if ( ' ' !== $character && "\t" !== $character && "\f" !== $character && "\r" !== $character && "\n" !== $character ) {2400 $class_at += strlen( $this->sought_class_name );2401 continue;2402 }2403 }2404 2405 return true;2406 }2407 2408 return false;2409 }2410 2411 2442 return true; 2412 2443 } -
trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php
r56684 r56703 500 500 501 501 /** 502 * @ticket 59209 503 * 504 * @covers WP_HTML_Tag_Processor::next_tag 505 */ 506 public function test_next_tag_matches_decoded_class_names() { 507 $p = new WP_HTML_Tag_Processor( '<div class="<egg>">' ); 508 509 $this->assertTrue( $p->next_tag( array( 'class_name' => '<egg>' ) ), 'Failed to find tag with HTML-encoded class name.' ); 510 } 511 512 /** 502 513 * @ticket 56299 503 514 * @ticket 57852 … … 1955 1966 'STYLE' => array( '<style>* { margin: 0 }</style><div target>' ), 1956 1967 'STYLE hiding DIV' => array( '<style>li::before { content: "<div non-target>" }</style><div target>' ), 1968 ); 1969 } 1970 1971 /** 1972 * @ticket 59209 1973 * 1974 * @covers WP_HTML_Tag_Processor::class_list 1975 */ 1976 public function test_class_list_empty_when_missing_class() { 1977 $p = new WP_HTML_Tag_Processor( '<div>' ); 1978 $p->next_tag(); 1979 1980 $found_classes = false; 1981 foreach ( $p->class_list() as $class ) { 1982 $found_classes = true; 1983 } 1984 1985 $this->assertFalse( $found_classes, 'Found classes when none exist.' ); 1986 } 1987 1988 /** 1989 * @ticket 59209 1990 * 1991 * @covers WP_HTML_Tag_Processor::class_list 1992 */ 1993 public function test_class_list_empty_when_class_is_boolean() { 1994 $p = new WP_HTML_Tag_Processor( '<div class>' ); 1995 $p->next_tag(); 1996 1997 $found_classes = false; 1998 foreach ( $p->class_list() as $class ) { 1999 $found_classes = true; 2000 } 2001 2002 $this->assertFalse( $found_classes, 'Found classes when none exist.' ); 2003 } 2004 2005 /** 2006 * @ticket 59209 2007 * 2008 * @covers WP_HTML_Tag_Processor::class_list 2009 */ 2010 public function test_class_list_empty_when_class_is_empty() { 2011 $p = new WP_HTML_Tag_Processor( '<div class="">' ); 2012 $p->next_tag(); 2013 2014 $found_classes = false; 2015 foreach ( $p->class_list() as $class ) { 2016 $found_classes = true; 2017 } 2018 2019 $this->assertFalse( $found_classes, 'Found classes when none exist.' ); 2020 } 2021 2022 /** 2023 * @ticket 59209 2024 * 2025 * @covers WP_HTML_Tag_Processor::class_list 2026 */ 2027 public function test_class_list_visits_each_class_in_order() { 2028 $p = new WP_HTML_Tag_Processor( '<div class="one two three">' ); 2029 $p->next_tag(); 2030 2031 $found_classes = array(); 2032 foreach ( $p->class_list() as $class ) { 2033 $found_classes[] = $class; 2034 } 2035 2036 $this->assertSame( array( 'one', 'two', 'three' ), $found_classes, 'Failed to visit the class names in their original order.' ); 2037 } 2038 2039 /** 2040 * @ticket 59209 2041 * 2042 * @covers WP_HTML_Tag_Processor::class_list 2043 */ 2044 public function test_class_list_decodes_class_names() { 2045 $p = new WP_HTML_Tag_Processor( '<div class="∉-class <egg> #">' ); 2046 $p->next_tag(); 2047 2048 $found_classes = array(); 2049 foreach ( $p->class_list() as $class ) { 2050 $found_classes[] = $class; 2051 } 2052 2053 $this->assertSame( array( '∉-class', '<egg>', "\u{ff03}" ), $found_classes, 'Failed to report class names in their decoded form.' ); 2054 } 2055 2056 /** 2057 * @ticket 59209 2058 * 2059 * @covers WP_HTML_Tag_Processor::class_list 2060 */ 2061 public function test_class_list_visits_unique_class_names_only_once() { 2062 $p = new WP_HTML_Tag_Processor( '<div class="one one one">' ); 2063 $p->next_tag(); 2064 2065 $found_classes = array(); 2066 foreach ( $p->class_list() as $class ) { 2067 $found_classes[] = $class; 2068 } 2069 2070 $this->assertSame( array( 'one' ), $found_classes, 'Visited multiple copies of the same class name when it should have skipped the duplicates.' ); 2071 } 2072 2073 /** 2074 * @ticket 59209 2075 * 2076 * @covers WP_HTML_Tag_Processor::has_class 2077 * 2078 * @dataProvider data_html_with_variations_of_class_values_and_sought_class_names 2079 * 2080 * @param string $html Contains a tag optionally containing a `class` attribute. 2081 * @param string $sought_class Name of class to find in the input tag's `class`. 2082 * @param bool $has_class Whether the sought class exists in the given HTML. 2083 */ 2084 public function test_has_class_handles_expected_class_name_variations( $html, $sought_class, $has_class ) { 2085 $p = new WP_HTML_Tag_Processor( $html ); 2086 $p->next_tag(); 2087 2088 if ( $has_class ) { 2089 $this->assertTrue( $p->has_class( $sought_class ), "Failed to find expected class {$sought_class}." ); 2090 } else { 2091 $this->assertFalse( $p->has_class( $sought_class ), "Found class {$sought_class} when it doesn't exist." ); 2092 } 2093 } 2094 2095 /** 2096 * Data provider. 2097 * 2098 * @return array[] 2099 */ 2100 public function data_html_with_variations_of_class_values_and_sought_class_names() { 2101 return array( 2102 'Tag without any classes' => array( '<div>', 'foo', false ), 2103 'Tag with boolean class' => array( '<img class>', 'foo', false ), 2104 'Tag with empty class' => array( '<p class="">', 'foo', false ), 2105 'Tag with exact match' => array( '<button class="foo">', 'foo', true ), 2106 'Tag with duplicate matches' => array( '<span class="foo bar foo">', 'foo', true ), 2107 'Tag with non-initial match' => array( '<section class="bar foo">', 'foo', true ), 2108 'Tag with encoded match' => array( '<main class="…">', '…', true ), 2109 'Class with tab separator' => array( "<div class='one\ttwo'>", 'two', true ), 2110 'Class with newline separator' => array( "<div class='one\ntwo\n'>", 'two', true ), 2111 'False duplicate attribute' => array( '<img class=dog class=cat>', 'cat', false ), 1957 2112 ); 1958 2113 }
Note: See TracChangeset
for help on using the changeset viewer.