Make WordPress Core

Changeset 58192


Ignore:
Timestamp:
05/24/2024 01:19:10 AM (2 months ago)
Author:
dmsnell
Message:

HTML API: Add expects_closer() method to HTML Processor

This patch adds a new method, WP_HTML_Processor->expects_closer() to indicate
if the currently-matched node expects to find a closing token. For example, a
DIV element expects a closing </div> tag, but an <img> expects none, because
it's a void element. Similarly, #text nodes and HTML comments only appear as
unitary nodes on the stack of open elements. Once proceeding further in the
document they are immediately removed without any closing tag.

This new method serves as a helper to indicate whether or not to expect the
closer, as this can be more complicated than it seems, and calling code
shouldn't have to build custom interpretations and implementations. Instead,
the HTML Processor ought to export its internal knowledge to make it easy for
consuming code and projects.

Developed in https://github.com/WordPress/wordpress-develop/pull/6600
Discussed in https://core.trac.wordpress.org/ticket/61257

Fixes #61257.
Props dmsnell, jonsurrell.

Location:
trunk
Files:
2 edited

Legend:

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

    r58191 r58192  
    507507
    508508        return false;
     509    }
     510
     511    /**
     512     * Indicates if the currently-matched node expects a closing
     513     * token, or if it will self-close on the next step.
     514     *
     515     * Most HTML elements expect a closer, such as a P element or
     516     * a DIV element. Others, like an IMG element are void and don't
     517     * have a closing tag. Special elements, such as SCRIPT and STYLE,
     518     * are treated just like void tags. Text nodes and self-closing
     519     * foreign content will also act just like a void tag, immediately
     520     * closing as soon as the processor advances to the next token.
     521     *
     522     * @since 6.6.0
     523     *
     524     * @todo When adding support for foreign content, ensure that
     525     *       this returns false for self-closing elements in the
     526     *       SVG and MathML namespace.
     527     *
     528     * @return bool Whether to expect a closer for the currently-matched node,
     529     *              or `null` if not matched on any token.
     530     */
     531    public function expects_closer() {
     532        $token_name = $this->get_token_name();
     533        if ( ! isset( $token_name ) ) {
     534            return null;
     535        }
     536
     537        return ! (
     538            // Comments, text nodes, and other atomic tokens.
     539            '#' === $token_name[0] ||
     540            // Doctype declarations.
     541            'html' === $token_name ||
     542            // Void elements.
     543            self::is_void( $token_name ) ||
     544            // Special atomic elements.
     545            in_array( $token_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true )
     546        );
    509547    }
    510548
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php

    r58191 r58192  
    180180            $processor->get_breadcrumbs(),
    181181            "DIV should have been a sibling of the {$tag_name}."
     182        );
     183    }
     184
     185    /**
     186     * Ensure reporting that normal non-void HTML elements expect a closer.
     187     *
     188     * @ticket 61257
     189     */
     190    public function test_expects_closer_regular_tags() {
     191        $processor = WP_HTML_Processor::create_fragment( '<div><p><b><em>' );
     192
     193        $tags = 0;
     194        while ( $processor->next_tag() ) {
     195            $this->assertTrue(
     196                $processor->expects_closer(),
     197                "Should have expected a closer for '{$processor->get_tag()}', but didn't."
     198            );
     199            ++$tags;
     200        }
     201
     202        $this->assertSame(
     203            4,
     204            $tags,
     205            'Did not find all the expected tags.'
     206        );
     207    }
     208
     209    /**
     210     * Ensure reporting that non-tag HTML nodes expect a closer.
     211     *
     212     * @ticket 61257
     213     *
     214     * @dataProvider data_self_contained_node_tokens
     215     *
     216     * @param string $self_contained_token String starting with HTML token that doesn't expect a closer,
     217     *                                     e.g. an HTML comment, text node, void tag, or special element.
     218     */
     219    public function test_expects_closer_expects_no_closer_for_self_contained_tokens( $self_contained_token ) {
     220        $processor   = WP_HTML_Processor::create_fragment( $self_contained_token );
     221        $found_token = $processor->next_token();
     222
     223        if ( WP_HTML_Processor::ERROR_UNSUPPORTED === $processor->get_last_error() ) {
     224            $this->markTestSkipped( "HTML '{$self_contained_token}' is not supported." );
     225        }
     226
     227        $this->assertTrue(
     228            $found_token,
     229            "Failed to find any tokens in '{$self_contained_token}': check test data provider."
     230        );
     231
     232        $this->assertFalse(
     233            $processor->expects_closer(),
     234            "Incorrectly expected a closer for node of type '{$processor->get_token_type()}'."
     235        );
     236    }
     237
     238    /**
     239     * Data provider.
     240     *
     241     * @return array[]
     242     */
     243    public static function data_self_contained_node_tokens() {
     244        $self_contained_nodes = array(
     245            'Normative comment'                => array( '<!-- comment -->' ),
     246            'Comment with invalid closing'     => array( '<!-- comment --!>' ),
     247            'CDATA Section lookalike'          => array( '<![CDATA[ comment ]]>' ),
     248            'Processing Instruction lookalike' => array( '<?ok comment ?>' ),
     249            'Funky comment'                    => array( '<//wp:post-meta key=isbn>' ),
     250            'Text node'                        => array( 'Trombone' ),
     251        );
     252
     253        foreach ( self::data_void_tags() as $tag_name => $_name ) {
     254            $self_contained_nodes[ "Void elements ({$tag_name})" ] = array( "<{$tag_name}>" );
     255        }
     256
     257        foreach ( self::data_special_tags() as $tag_name => $_name ) {
     258            $self_contained_nodes[ "Special atomic elements ({$tag_name})" ] = array( "<{$tag_name}>content</{$tag_name}>" );
     259        }
     260
     261        return $self_contained_nodes;
     262    }
     263
     264    /**
     265     * Data provider.
     266     *
     267     * @return array[]
     268     */
     269    public static function data_special_tags() {
     270        return array(
     271            'IFRAME'   => array( 'IFRAME' ),
     272            'NOEMBED'  => array( 'NOEMBED' ),
     273            'NOFRAMES' => array( 'NOFRAMES' ),
     274            'SCRIPT'   => array( 'SCRIPT' ),
     275            'STYLE'    => array( 'STYLE' ),
     276            'TEXTAREA' => array( 'TEXTAREA' ),
     277            'TITLE'    => array( 'TITLE' ),
     278            'XMP'      => array( 'XMP' ),
    182279        );
    183280    }
Note: See TracChangeset for help on using the changeset viewer.