Make WordPress Core


Ignore:
Timestamp:
09/26/2023 09:15:21 AM (3 years ago)
Author:
Bernhard Reiter
Message:

HTML API: Add class name utilities has_class() and class_list().

This patch adds two new public methods to the HTML Tag Processor:

  • has_class() indicates if a matched tag contains a given CSS class name.
  • class_list() returns a generator to iterate over all the class names in a matched tag.

Included in this patch is a refactoring of the internal logic when matching
a tag to reuse the new has_class() function. Previously it was relying on
optimized code in the matches() function which performed byte-for-byte
class name comparison. With the change in this patch it will perform class
name matching on the decoded value, which might differ if a class attribute
contains character references.

These methods may be useful for running more complicated queries based
on the presence or absence of CSS class names. The use of these methods
avoids the need to manually decode the class attribute as reported by
$process->get_attribute( 'class' ).

Props dmsnell.
Fixes #59209.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php

    r56684 r56703  
    500500
    501501        /**
     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="&lt;egg&gt;">' );
     508
     509                $this->assertTrue( $p->next_tag( array( 'class_name' => '<egg>' ) ), 'Failed to find tag with HTML-encoded class name.' );
     510        }
     511
     512        /**
    502513         * @ticket 56299
    503514         * @ticket 57852
    … …  
    19551966                        'STYLE'            => array( '<style>* { margin: 0 }</style><div target>' ),
    19561967                        '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="&notin;-class &lt;egg&gt; &#xff03;">' );
     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 &#x6f;ne">' );
     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="&hellip;">', '…', 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 ),
    19572112                );
    19582113        }
Note: See TracChangeset for help on using the changeset viewer.