Make WordPress Core

Changeset 56380


Ignore:
Timestamp:
08/10/2023 08:35:55 AM (3 years ago)
Author:
Bernhard Reiter
Message:

HTML API: Add support for BUTTON element.

This patch adds support to process the BUTTON element. This requires adding some additional semantic rules to handle situations where a BUTTON element is already in scope.

Also included is a fixup to enforce that WP_HTML_Processor::next_tag() never returns for a tag closer. This is useful with the Tag Processor, but not for the HTML Processor. There were tests relying on this behavior to assert that internal processes were working as they should, but those tests have been updated to use the semi-private step() function, which does stop on tag closers.

This patch is one in a series of changes to expand support within the HTML API, moving gradually to allow for more focused changes that are easier to review and test. The HTML Processor is a work in progress with a certain set of features slated to be ready and tested by 6.4.0, but it will only contain partial support of the HTML5 specification even after that. Whenever it cannot positively recognize and process its input it bails, and certain function stubs and logical stubs exist to structure future expansions of support.

Props dmsnell.
Fixes #58961.

Location:
trunk
Files:
6 edited

Legend:

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

    r56363 r56380  
    114114                foreach ( $this->walk_up() as $node ) {
    115115                        if ( $node->node_name === $tag_name ) {
     116                                return true;
     117                        }
     118
     119                        switch ( $node->node_name ) {
     120                                case 'HTML':
     121                                        return false;
     122                        }
     123
     124                        if ( in_array( $node->node_name, $termination_list, true ) ) {
    116125                                return true;
    117126                        }
    … …  
    176185         */
    177186        public function has_element_in_button_scope( $tag_name ) {
    178                 return $this->has_element_in_specific_scope(
    179                         $tag_name,
    180                         array(
    181 
    182                                 /*
    183                                  * Because it's not currently possible to encounter
    184                                  * one of the termination elements, they don't need
    185                                  * to be listed here. If they were, they would be
    186                                  * unreachable and only waste CPU cycles while
    187                                  * scanning through HTML.
    188                                  */
    189                         )
    190                 );
     187                return $this->has_element_in_specific_scope( $tag_name, array( 'BUTTON' ) );
    191188        }
    192189
    … …  
    395392                 */
    396393                switch ( $item->node_name ) {
     394                        case 'BUTTON':
     395                                $this->has_p_in_button_scope = false;
     396                                break;
     397
    397398                        case 'P':
    398399                                $this->has_p_in_button_scope = true;
    … …  
    420421                 */
    421422                switch ( $item->node_name ) {
     423                        case 'BUTTON':
     424                                $this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' );
     425                                break;
     426
    422427                        case 'P':
    423428                                $this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' );
  • trunk/src/wp-includes/html-api/class-wp-html-processor-state.php

    r56274 r56380  
    109109
    110110        /**
     111         * The frameset-ok flag indicates if a `FRAMESET` element is allowed in the current state.
     112         *
     113         * > The frameset-ok flag is set to "ok" when the parser is created. It is set to "not ok" after certain tokens are seen.
     114         *
     115         * @since 6.4.0
     116         *
     117         * @see https://html.spec.whatwg.org/#frameset-ok-flag
     118         *
     119         * @var bool
     120         */
     121        public $frameset_ok = true;
     122
     123        /**
    111124         * Constructor - creates a new and empty state value.
    112125         *
  • trunk/src/wp-includes/html-api/class-wp-html-processor.php

    r56376 r56380  
    350350        public function next_tag( $query = null ) {
    351351                if ( null === $query ) {
    352                         return $this->step();
     352                        while ( $this->step() ) {
     353                                if ( ! $this->is_tag_closer() ) {
     354                                        return true;
     355                                }
     356                        }
     357
     358                        return false;
    353359                }
    354360
    … …  
    367373
    368374                if ( ! ( array_key_exists( 'breadcrumbs', $query ) && is_array( $query['breadcrumbs'] ) ) ) {
    369                         return $this->step();
     375                        while ( $this->step() ) {
     376                                if ( ! $this->is_tag_closer() ) {
     377                                        return true;
     378                                }
     379                        }
     380
     381                        return false;
    370382                }
    371383
    … …  
    384396                $crumb  = end( $breadcrumbs );
    385397                $target = strtoupper( $crumb );
    386                 while ( $this->step() ) {
     398                while ( $match_offset > 0 && $this->step() ) {
    387399                        if ( $target !== $this->get_tag() ) {
    388400                                continue;
    … …  
    396408
    397409                                $crumb = prev( $breadcrumbs );
    398                                 if ( false === $crumb && 0 === --$match_offset ) {
     410                                if ( false === $crumb && 0 === --$match_offset && ! $this->is_tag_closer() ) {
    399411                                        return true;
    400412                                }
    … …  
    511523
    512524                switch ( $op ) {
     525                        /*
     526                         * > A start tag whose tag name is "button"
     527                         */
     528                        case '+BUTTON':
     529                                if ( $this->state->stack_of_open_elements->has_element_in_scope( 'BUTTON' ) ) {
     530                                        // @TODO: Indicate a parse error once it's possible. This error does not impact the logic here.
     531                                        $this->generate_implied_end_tags();
     532                                        $this->state->stack_of_open_elements->pop_until( 'BUTTON' );
     533                                }
     534
     535                                $this->reconstruct_active_formatting_elements();
     536                                $this->insert_html_element( $this->current_token );
     537                                $this->state->frameset_ok = false;
     538
     539                                return true;
     540
    513541                        /*
    514542                         * > A start tag whose tag name is one of: "address", "article", "aside",
    … …  
    536564                         */
    537565                        case '-BLOCKQUOTE':
     566                        case '-BUTTON':
    538567                        case '-DIV':
    539568                        case '-FIGCAPTION':
    540569                        case '-FIGURE':
    541570                                if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $tag_name ) ) {
     571                                        // @TODO: Report parse error.
    542572                                        // Ignore the token.
    543573                                        return $this->step();
    … …  
    545575
    546576                                $this->generate_implied_end_tags();
     577                                if ( $this->state->stack_of_open_elements->current_node()->node_name !== $tag_name ) {
     578                                        // @TODO: Record parse error: this error doesn't impact parsing.
     579                                }
    547580                                $this->state->stack_of_open_elements->pop_until( $tag_name );
    548581                                return true;
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php

    r56331 r56380  
    4040                        'B',
    4141                        'BIG',
     42                        'BUTTON',
    4243                        'CODE',
    4344                        'DIV',
    … …  
    112113                        'BODY',
    113114                        'BR',
    114                         'BUTTON',
    115115                        'CANVAS',
    116116                        'CAPTION',
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php

    r56331 r56380  
    1616         * RULES FOR "IN BODY" MODE
    1717         *******************************************************************/
     18
     19        /**
     20         * Verifies that when encountering an end tag for which there is no corresponding
     21         * element in scope, that it skips the tag entirely.
     22         *
     23         * @ticket 58961
     24         *
     25         * @since 6.4.0
     26         *
     27         * @throws Exception
     28         */
     29        public function test_in_body_skips_unexpected_button_closer() {
     30                $p = WP_HTML_Processor::createFragment( '<div>Test</button></div>' );
     31
     32                $p->step();
     33                $this->assertEquals( 'DIV', $p->get_tag(), 'Did not stop at initial DIV tag.' );
     34                $this->assertFalse( $p->is_tag_closer(), 'Did not find that initial DIV tag is an opener.' );
     35
     36                /*
     37                 * When encountering the BUTTON closing tag, there is no BUTTON in the stack of open elements.
     38                 * It should be ignored as there's no BUTTON to close.
     39                 */
     40                $this->assertTrue( $p->step(), 'Found no further tags when it should have found the closing DIV' );
     41                $this->assertEquals( 'DIV', $p->get_tag(), "Did not skip unexpected BUTTON; stopped at {$p->get_tag()}." );
     42                $this->assertTrue( $p->is_tag_closer(), 'Did not find that the terminal DIV tag is a closer.' );
     43        }
     44
     45        /**
     46         * Verifies insertion of a BUTTON element when no existing BUTTON is already in scope.
     47         *
     48         * @ticket 58961
     49         *
     50         * @since 6.4.0
     51         *
     52         * @throws WP_HTML_Unsupported_Exception
     53         */
     54        public function test_in_body_button_with_no_button_in_scope() {
     55                $p = WP_HTML_Processor::createFragment( '<div><p>Click the button <button one>here</button>!</p></div><button two>not here</button>' );
     56
     57                $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected first button.' );
     58                $this->assertTrue( $p->get_attribute( 'one' ), 'Failed to match expected attribute on first button.' );
     59                $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'P', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for first button.' );
     60
     61                /*
     62                 * There's nothing special about this HTML construction, but it's important to verify that
     63                 * the HTML Processor can find a BUTTON under normal and normative scenarios, not just the
     64                 * malformed and unexpected ones.
     65                 */
     66                $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected second button.' );
     67                $this->assertTrue( $p->get_attribute( 'two' ), 'Failed to match expected attribute on second button.' );
     68                $this->assertSame( array( 'HTML', 'BODY', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for second button.' );
     69        }
     70
     71        /**
     72         * Verifies what when inserting a BUTTON element, when a BUTTON is already in scope,
     73         * that the open button is closed with all other elements inside of it.
     74         *
     75         * @ticket 58961
     76         *
     77         * @since 6.4.0
     78         *
     79         * @throws WP_HTML_Unsupported_Exception
     80         */
     81        public function test_in_body_button_with_button_in_scope_as_parent() {
     82                $p = WP_HTML_Processor::createFragment( '<div><p>Click the button <button one>almost<button two>here</button>!</p></div><button three>not here</button>' );
     83
     84                $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected first button.' );
     85                $this->assertTrue( $p->get_attribute( 'one' ), 'Failed to match expected attribute on first button.' );
     86                $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'P', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for first button.' );
     87
     88                /*
     89                 * A naive parser might skip the second BUTTON because it's looking for the close of the first one,
     90                 * or it may place it as a child of the first one, but it implicitly closes the open BUTTON.
     91                 */
     92                $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected second button.' );
     93                $this->assertTrue( $p->get_attribute( 'two' ), 'Failed to match expected attribute on second button.' );
     94                $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'P', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for second button.' );
     95
     96                /*
     97                 * This is another form of the test for the second button, but from a different side. The test is
     98                 * looking for proper handling of the open and close sequence for the BUTTON tags.
     99                 */
     100                $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected third button.' );
     101                $this->assertTrue( $p->get_attribute( 'three' ), 'Failed to match expected attribute on third button.' );
     102                $this->assertSame( array( 'HTML', 'BODY', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for third button.' );
     103        }
     104
     105        /**
     106         * Verifies what when inserting a BUTTON element, when a BUTTON is already in scope,
     107         * that the open button is closed with all other elements inside of it, even if the
     108         * BUTTON in scope is not a direct parent of the new BUTTON element.
     109         *
     110         * @ticket 58961
     111         *
     112         * @since 6.4.0
     113         *
     114         * @throws WP_HTML_Unsupported_Exception
     115         */
     116        public function test_in_body_button_with_button_in_scope_as_ancestor() {
     117                $p = WP_HTML_Processor::createFragment( '<div><button one><p>Click the button <span><button two>here</button>!</span></p></div><button three>not here</button>' );
     118
     119                // This button finds itself normally nesting inside the DIV.
     120                $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected first button.' );
     121                $this->assertTrue( $p->get_attribute( 'one' ), 'Failed to match expected attribute on first button.' );
     122                $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for first button.' );
     123
     124                /*
     125                 * Because the second button appears while a BUTTON is in scope, it generates implied end tags and closes
     126                 * the BUTTON, P, and SPAN elements. It looks like the BUTTON is inside the SPAN, but it's another case
     127                 * of an unexpected closing SPAN tag because the SPAN was closed by the second BUTTON. This element finds
     128                 * itself a child of the most-recent open element above the most-recent BUTTON, or the DIV.
     129                 */
     130                $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected second button.' );
     131                $this->assertTrue( $p->get_attribute( 'two' ), 'Failed to match expected attribute on second button.' );
     132                $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for second button.' );
     133
     134                // The third button is back to normal, because everything has been implicitly or explicitly closed by now.
     135                $this->assertTrue( $p->next_tag( 'BUTTON' ), 'Could not find expected third button.' );
     136                $this->assertTrue( $p->get_attribute( 'three' ), 'Failed to match expected attribute on third button.' );
     137                $this->assertSame( array( 'HTML', 'BODY', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for third button.' );
     138        }
    18139
    19140        /*
    … …  
    58179                $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'CODE' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting.' );
    59180
    60                 $this->assertTrue( $p->next_tag(), 'Failed to advance past CODE tag to expected SPAN closer.' );
     181                $this->assertTrue( $p->step(), 'Failed to advance past CODE tag to expected SPAN closer.' );
    61182                $this->assertTrue( $p->is_tag_closer(), 'Expected to find closing SPAN, but found opener instead.' );
    62183                $this->assertSame( array( 'HTML', 'BODY', 'DIV' ), $p->get_breadcrumbs(), 'Failed to advance past CODE tag to expected DIV opener.' );
  • trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php

    r56299 r56380  
    177177                $this->ensure_support_is_added_everywhere( 'DESC' );
    178178                $this->ensure_support_is_added_everywhere( 'TITLE' );
    179 
    180                 $this->ensure_support_is_added_everywhere( 'BUTTON' );
    181179        }
    182180
    … …  
    219217                $this->ensure_support_is_added_everywhere( 'DESC' );
    220218                $this->ensure_support_is_added_everywhere( 'TITLE' );
    221 
    222                 // This element is specific to BUTTON scope.
    223                 $this->ensure_support_is_added_everywhere( 'BUTTON' );
    224219        }
    225220
    … …  
    262257                $this->ensure_support_is_added_everywhere( 'DESC' );
    263258                $this->ensure_support_is_added_everywhere( 'TITLE' );
    264 
    265                 $this->ensure_support_is_added_everywhere( 'BUTTON' );
    266259        }
    267260
Note: See TracChangeset for help on using the changeset viewer.