Make WordPress Core

Changeset 57186


Ignore:
Timestamp:
12/13/2023 05:51:42 PM (3 years ago)
Author:
Bernhard Reiter
Message:

HTML API: Add support for H1-H6 elements in the HTML Processor.

Previously these have been unsupported, but in this patch, support is added for the tags so that the HTML Processor can process documents containing them.

There was a design discussion about introducing a constant to communicate "any of the H1 - H6 elements" but this posed a number of challenges that don't need to be answered in this patch. For the time being, because the HTML specification treats H1 - H6 specially as a single kind of element, the HTML Processor uses an internal hard-coded string to indicate this. By using a hard-coded string it's possible to avoid introducing a class constant which cannot be made private due to PHP's class design. In the future, this will probably appear as a special constant in a new constant-containing class.

Props dmsnell, jonsurrell.
Fixes #60060.

Location:
trunk
Files:
1 added
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/html-api/class-wp-html-open-elements.php

    r56753 r57186  
    114114                foreach ( $this->walk_up() as $node ) {
    115115                        if ( $node->node_name === $tag_name ) {
     116                                return true;
     117                        }
     118
     119                        if (
     120                                '(internal: H1 through H6 - do not use)' === $tag_name &&
     121                                in_array( $node->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true )
     122                        ) {
    116123                                return true;
    117124                        }
     
    271278                        $this->pop();
    272279
     280                        if (
     281                                '(internal: H1 through H6 - do not use)' === $tag_name &&
     282                                in_array( $item->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true )
     283                        ) {
     284                                return true;
     285                        }
     286
    273287                        if ( $tag_name === $item->node_name ) {
    274288                                return true;
  • trunk/src/wp-includes/html-api/class-wp-html-processor.php

    r57115 r57186  
    103103 *  - Form elements: BUTTON, FIELDSET, SEARCH.
    104104 *  - Formatting elements: B, BIG, CODE, EM, FONT, I, SMALL, STRIKE, STRONG, TT, U.
    105  *  - Heading elements: HGROUP.
     105 *  - Heading elements: H1, H2, H3, H4, H5, H6, HGROUP.
    106106 *  - Links: A.
    107107 *  - Lists: DL.
     
    696696                                }
    697697                                $this->state->stack_of_open_elements->pop_until( $tag_name );
     698                                return true;
     699
     700                        /*
     701                         * > A start tag whose tag name is one of: "h1", "h2", "h3", "h4", "h5", "h6"
     702                         */
     703                        case '+H1':
     704                        case '+H2':
     705                        case '+H3':
     706                        case '+H4':
     707                        case '+H5':
     708                        case '+H6':
     709                                if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
     710                                        $this->close_a_p_element();
     711                                }
     712
     713                                if (
     714                                        in_array(
     715                                                $this->state->stack_of_open_elements->current_node()->node_name,
     716                                                array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ),
     717                                                true
     718                                        )
     719                                ) {
     720                                        // @TODO: Indicate a parse error once it's possible.
     721                                        $this->state->stack_of_open_elements->pop();
     722                                }
     723
     724                                $this->insert_html_element( $this->state->current_token );
     725                                return true;
     726
     727                        /*
     728                         * > An end tag whose tag name is one of: "h1", "h2", "h3", "h4", "h5", "h6"
     729                         */
     730                        case '-H1':
     731                        case '-H2':
     732                        case '-H3':
     733                        case '-H4':
     734                        case '-H5':
     735                        case '-H6':
     736                                if ( ! $this->state->stack_of_open_elements->has_element_in_scope( '(internal: H1 through H6 - do not use)' ) ) {
     737                                        /*
     738                                         * This is a parse error; ignore the token.
     739                                         *
     740                                         * @TODO: Indicate a parse error once it's possible.
     741                                         */
     742                                        return $this->step();
     743                                }
     744
     745                                $this->generate_implied_end_tags();
     746
     747                                if ( $this->state->stack_of_open_elements->current_node()->node_name !== $tag_name ) {
     748                                        // @TODO: Record parse error: this error doesn't impact parsing.
     749                                }
     750
     751                                $this->state->stack_of_open_elements->pop_until( '(internal: H1 through H6 - do not use)' );
    698752                                return true;
    699753
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php

    r56790 r57186  
    9292         * @covers WP_HTML_Processor::next_tag
    9393         * @covers WP_HTML_Processor::seek
    94          *
    95          * @throws WP_HTML_Unsupported_Exception
    9694         */
    9795        public function test_clear_to_navigate_after_seeking() {
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php

    r57115 r57186  
    5757                        'FONT',
    5858                        'FOOTER',
     59                        'H1',
     60                        'H2',
     61                        'H3',
     62                        'H4',
     63                        'H5',
     64                        'H6',
    5965                        'HEADER',
    6066                        'HGROUP',
     
    143149                        'FRAME',
    144150                        'FRAMESET',
    145                         'H1',
    146                         'H2',
    147                         'H3',
    148                         'H4',
    149                         'H5',
    150                         'H6',
    151151                        'HEAD',
    152152                        'HR',
     
    353353                        'MAIN inside MAIN inside SPAN'          => array( '<span><main><main target>', array( 'HTML', 'BODY', 'SPAN', 'MAIN', 'MAIN' ), 1 ),
    354354                        'MAIN next to unclosed P'               => array( '<p><main target>', array( 'HTML', 'BODY', 'MAIN' ), 1 ),
     355
     356                        // H1 - H6 close out _any_ H1 - H6 when encountering _any_ of H1 - H6, making this section surprising.
     357                        'EM inside H3 after unclosed P'         => array( '<p><h3><em target>Important Message</em></h3>', array( 'HTML', 'BODY', 'H3', 'EM' ), 1 ),
     358                        'H4 after H2'                           => array( '<h2>Major</h2><h4 target>Minor</h4>', array( 'HTML', 'BODY', 'H4' ), 1 ),
     359                        'H4 after unclosed H2'                  => array( '<h2>Major<h4 target>Minor</h3>', array( 'HTML', 'BODY', 'H4' ), 1 ),
     360                        'H4 inside H2'                          => array( '<h2><span>Major<h4 target>Minor</h3></span>', array( 'HTML', 'BODY', 'H2', 'SPAN', 'H4' ), 1 ),
     361                        'H5 after unclosed H4 inside H2'        => array( '<h2><span>Major<h4>Minor</span></h3><h5 target>', array( 'HTML', 'BODY', 'H2', 'SPAN', 'H5' ), 1 ),
     362                        'H5 after H4 inside H2'                 => array( '<h2><span>Major<h4>Minor</h4></span></h3><h5 target>', array( 'HTML', 'BODY', 'H5' ), 1 ),
    355363                );
    356364        }
     
    388396                return array(
    389397                        // Test with void elements.
    390                         'Inner IMG'                      => array( '<div><span><figure><img target></figure></span></div>', array( 'span', 'figure', 'img' ), true ),
    391                         'Inner IMG wildcard'             => array( '<div><span><figure><img target></figure></span></div>', array( 'span', '*', 'img' ), true ),
    392                         'Inner IMG no wildcard'          => array( '<div><span><figure><img target></figure></span></div>', array( 'span', 'img' ), false ),
    393                         'Full specification'             => array( '<div><span><figure><img target></figure></span></div>', array( 'html', 'body', 'div', 'span', 'figure', 'img' ), true ),
    394                         'Invalid Full specification'     => array( '<div><span><figure><img target></figure></span></div>', array( 'html', 'div', 'span', 'figure', 'img' ), false ),
     398                        'Inner IMG'                               => array( '<div><span><figure><img target></figure></span></div>', array( 'span', 'figure', 'img' ), true ),
     399                        'Inner IMG wildcard'                      => array( '<div><span><figure><img target></figure></span></div>', array( 'span', '*', 'img' ), true ),
     400                        'Inner IMG no wildcard'                   => array( '<div><span><figure><img target></figure></span></div>', array( 'span', 'img' ), false ),
     401                        'Full specification'                      => array( '<div><span><figure><img target></figure></span></div>', array( 'html', 'body', 'div', 'span', 'figure', 'img' ), true ),
     402                        'Invalid Full specification'              => array( '<div><span><figure><img target></figure></span></div>', array( 'html', 'div', 'span', 'figure', 'img' ), false ),
    395403
    396404                        // Test also with non-void elements that open and close.
    397                         'Inner P'                        => array( '<div><span><figure><p target></figure></span></div>', array( 'span', 'figure', 'p' ), true ),
    398                         'Inner P wildcard'               => array( '<div><span><figure><p target></figure></span></div>', array( 'span', '*', 'p' ), true ),
    399                         'Inner P no wildcard'            => array( '<div><span><figure><p target></figure></span></div>', array( 'span', 'p' ), false ),
    400                         'Full specification (P)'         => array( '<div><span><figure><p target></figure></span></div>', array( 'html', 'body', 'div', 'span', 'figure', 'p' ), true ),
    401                         'Invalid Full specification (P)' => array( '<div><span><figure><p target></figure></span></div>', array( 'html', 'div', 'span', 'figure', 'p' ), false ),
     405                        'Inner P'                                 => array( '<div><span><figure><p target></figure></span></div>', array( 'span', 'figure', 'p' ), true ),
     406                        'Inner P wildcard'                        => array( '<div><span><figure><p target></figure></span></div>', array( 'span', '*', 'p' ), true ),
     407                        'Inner P no wildcard'                     => array( '<div><span><figure><p target></figure></span></div>', array( 'span', 'p' ), false ),
     408                        'Full specification (P)'                  => array( '<div><span><figure><p target></figure></span></div>', array( 'html', 'body', 'div', 'span', 'figure', 'p' ), true ),
     409                        'Invalid Full specification (P)'          => array( '<div><span><figure><p target></figure></span></div>', array( 'html', 'div', 'span', 'figure', 'p' ), false ),
    402410
    403411                        // Ensure that matches aren't on tag closers.
    404                         'Inner P'                        => array( '<div><span><figure></p target></figure></span></div>', array( 'span', 'figure', 'p' ), false ),
    405                         'Inner P wildcard'               => array( '<div><span><figure></p target></figure></span></div>', array( 'span', '*', 'p' ), false ),
    406                         'Inner P no wildcard'            => array( '<div><span><figure></p target></figure></span></div>', array( 'span', 'p' ), false ),
    407                         'Full specification (P)'         => array( '<div><span><figure></p target></figure></span></div>', array( 'html', 'body', 'div', 'span', 'figure', 'p' ), false ),
    408                         'Invalid Full specification (P)' => array( '<div><span><figure></p target></figure></span></div>', array( 'html', 'div', 'span', 'figure', 'p' ), false ),
     412                        'Inner P (Closer)'                        => array( '<div><span><figure></p target></figure></span></div>', array( 'span', 'figure', 'p' ), false ),
     413                        'Inner P wildcard (Closer)'               => array( '<div><span><figure></p target></figure></span></div>', array( 'span', '*', 'p' ), false ),
     414                        'Inner P no wildcard (Closer)'            => array( '<div><span><figure></p target></figure></span></div>', array( 'span', 'p' ), false ),
     415                        'Full specification (P) (Closer)'         => array( '<div><span><figure></p target></figure></span></div>', array( 'html', 'body', 'div', 'span', 'figure', 'p' ), false ),
     416                        'Invalid Full specification (P) (Closer)' => array( '<div><span><figure></p target></figure></span></div>', array( 'html', 'div', 'span', 'figure', 'p' ), false ),
    409417
    410418                        // Test wildcard behaviors.
    411                         'Single wildcard element'        => array( '<figure><code><div><p><span><img target></span></p></div></code></figure>', array( '*' ), true ),
    412                         'Child of wildcard element'      => array( '<figure><code><div><p><span><img target></span></p></div></code></figure>', array( 'SPAN', '*' ), true ),
     419                        'Single wildcard element'                 => array( '<figure><code><div><p><span><img target></span></p></div></code></figure>', array( '*' ), true ),
     420                        'Child of wildcard element'               => array( '<figure><code><div><p><span><img target></span></p></div></code></figure>', array( 'SPAN', '*' ), true ),
    413421                );
    414422        }
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php

    r57115 r57186  
    121121         *
    122122         * @ticket 58961
    123          *
    124          * @since 6.4.0
    125          *
    126          * @throws Exception
    127123         */
    128124        public function test_in_body_skips_unexpected_button_closer() {
     
    146142         *
    147143         * @ticket 58961
    148          *
    149          * @since 6.4.0
    150          *
    151          * @throws WP_HTML_Unsupported_Exception
    152144         */
    153145        public function test_in_body_button_with_no_button_in_scope() {
     
    175167         *
    176168         * @since 6.4.0
    177          *
    178          * @throws WP_HTML_Unsupported_Exception
    179169         */
    180170        public function test_in_body_button_with_button_in_scope_as_parent() {
     
    210200         *
    211201         * @since 6.4.0
    212          *
    213          * @throws WP_HTML_Unsupported_Exception
    214202         */
    215203        public function test_in_body_button_with_button_in_scope_as_ancestor() {
     
    237225        }
    238226
    239         /*
     227        /**
    240228         * Verifies that when "in body" and encountering "any other end tag"
    241229         * that the HTML processor ignores the end tag if there's a special
     
    260248        }
    261249
    262         /*
     250        /**
    263251         * Verifies that when "in body" and encountering "any other end tag"
    264252         * that the HTML processor closes appropriate elements on the stack of
Note: See TracChangeset for help on using the changeset viewer.