Make WordPress Core


Ignore:
Timestamp:
09/02/2019 10:24:18 AM (6 years ago)
Author:
flixos90
Message:

Formatting: Improve accuracy of force_balance_tags() and add support for custom element tags.

This changeset includes a major iteration on the regular expression used to balance tags, with comprehensive test coverage to ensure that all scenarios are supported or unsupported as expected.

Props dmsnell, westonruter, birgire.
Fixes #47014.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/formatting.php

    r45887 r45929  
    24302430 */
    24312431function balanceTags( $text, $force = false ) {  // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
    2432     if ( $force || get_option( 'use_balanceTags' ) == 1 ) {
     2432    if ( $force || (int) get_option( 'use_balanceTags' ) === 1 ) {
    24332433        return force_balance_tags( $text );
    24342434    } else {
     
    24412441 *
    24422442 * @since 2.0.4
     2443 * @since 5.3.0 Improve accuracy and add support for custom element tags.
    24432444 *
    24442445 * @author Leonard Lin <leonard@acm.org>
     
    24702471    $text = preg_replace( '#<([0-9]{1})#', '&lt;$1', $text );
    24712472
    2472     while ( preg_match( '/<(\/?[\w:]*)\s*([^>]*)>/', $text, $regex ) ) {
     2473    /**
     2474     * Matches supported tags.
     2475     *
     2476     * To get the pattern as a string without the comments paste into a PHP
     2477     * REPL like `php -a`.
     2478     *
     2479     * @see https://html.spec.whatwg.org/#elements-2
     2480     * @see https://w3c.github.io/webcomponents/spec/custom/#valid-custom-element-name
     2481     *
     2482     * @example
     2483     * ~# php -a
     2484     * php > $s = [paste copied contents of expression below including parentheses];
     2485     * php > echo $s;
     2486     */
     2487    $tag_pattern = (
     2488        '#<' . // Start with an opening bracket.
     2489        '(/?)' . // Group 1 - If it's a closing tag it'll have a leading slash.
     2490        '(' . // Group 2 - Tag name.
     2491            // Custom element tags have more lenient rules than HTML tag names.
     2492            '(?:[a-z](?:[a-z0-9._]*)-(?:[a-z0-9._-]+)+)' .
     2493                '|' .
     2494            // Traditional tag rules approximate HTML tag names.
     2495            '(?:[\w:]+)' .
     2496        ')' .
     2497        '(?:' .
     2498            // We either immediately close the tag with its '>' and have nothing here.
     2499            '\s*' .
     2500            '(/?)' . // Group 3 - "attributes" for empty tag.
     2501                '|' .
     2502            // Or we must start with space characters to separate the tag name from the attributes (or whitespace).
     2503            '(\s+)' . // Group 4 - Pre-attribute whitespace.
     2504            '([^>]*)' . // Group 5 - Attributes.
     2505        ')' .
     2506        '>#' // End with a closing bracket.
     2507    );
     2508
     2509    while ( preg_match( $tag_pattern, $text, $regex ) ) {
     2510        $full_match        = $regex[0];
     2511        $has_leading_slash = ! empty( $regex[1] );
     2512        $tag_name          = $regex[2];
     2513        $tag               = strtolower( $tag_name );
     2514        $is_single_tag     = in_array( $tag, $single_tags, true );
     2515        $pre_attribute_ws  = isset( $regex[4] ) ? $regex[4] : '';
     2516        $attributes        = trim( isset( $regex[5] ) ? $regex[5] : $regex[3] );
     2517        $has_self_closer   = '/' === substr( $attributes, -1 );
     2518
    24732519        $newtext .= $tagqueue;
    24742520
    2475         $i = strpos( $text, $regex[0] );
    2476         $l = strlen( $regex[0] );
    2477 
    2478         // clear the shifter
     2521        $i = strpos( $text, $full_match );
     2522        $l = strlen( $full_match );
     2523
     2524        // Clear the shifter.
    24792525        $tagqueue = '';
    2480         // Pop or Push
    2481         if ( isset( $regex[1][0] ) && '/' == $regex[1][0] ) { // End Tag
    2482             $tag = strtolower( substr( $regex[1], 1 ) );
    2483             // if too many closing tags
     2526        if ( $has_leading_slash ) { // End Tag.
     2527            // If too many closing tags.
    24842528            if ( $stacksize <= 0 ) {
    24852529                $tag = '';
    2486                 // or close to be safe $tag = '/' . $tag;
    2487 
    2488                 // if stacktop value = tag close value then pop
    2489             } elseif ( $tagstack[ $stacksize - 1 ] == $tag ) { // found closing tag
    2490                 $tag = '</' . $tag . '>'; // Close Tag
    2491                 // Pop
     2530                // Or close to be safe $tag = '/' . $tag.
     2531
     2532                // If stacktop value = tag close value, then pop.
     2533            } elseif ( $tagstack[ $stacksize - 1 ] === $tag ) { // Found closing tag.
     2534                $tag = '</' . $tag . '>'; // Close Tag.
    24922535                array_pop( $tagstack );
    24932536                $stacksize--;
    2494             } else { // closing tag not at top, search for it
     2537            } else { // Closing tag not at top, search for it.
    24952538                for ( $j = $stacksize - 1; $j >= 0; $j-- ) {
    2496                     if ( $tagstack[ $j ] == $tag ) {
    2497                         // add tag to tagqueue
     2539                    if ( $tagstack[ $j ] === $tag ) {
     2540                        // Add tag to tagqueue.
    24982541                        for ( $k = $stacksize - 1; $k >= $j; $k-- ) {
    24992542                            $tagqueue .= '</' . array_pop( $tagstack ) . '>';
     
    25052548                $tag = '';
    25062549            }
    2507         } else { // Begin Tag
    2508             $tag = strtolower( $regex[1] );
    2509 
    2510             // Tag Cleaning
    2511 
    2512             // If it's an empty tag "< >", do nothing
    2513             if ( '' == $tag ) {
    2514                 // do nothing
    2515             } elseif ( substr( $regex[2], -1 ) == '/' ) { // ElseIf it presents itself as a self-closing tag...
     2550        } else { // Begin Tag.
     2551            if ( $has_self_closer ) { // If it presents itself as a self-closing tag...
    25162552                // ...but it isn't a known single-entity self-closing tag, then don't let it be treated as such and
    25172553                // immediately close it with a closing tag (the tag will encapsulate no text as a result)
    2518                 if ( ! in_array( $tag, $single_tags ) ) {
    2519                     $regex[2] = trim( substr( $regex[2], 0, -1 ) ) . "></$tag";
     2554                if ( ! $is_single_tag ) {
     2555                    $attributes = trim( substr( $attributes, 0, -1 ) ) . "></$tag";
    25202556                }
    2521             } elseif ( in_array( $tag, $single_tags ) ) { // ElseIf it's a known single-entity tag but it doesn't close itself, do so
    2522                 $regex[2] .= '/';
    2523             } else { // Else it's not a single-entity tag
    2524                 // If the top of the stack is the same as the tag we want to push, close previous tag
    2525                 if ( $stacksize > 0 && ! in_array( $tag, $nestable_tags ) && $tagstack[ $stacksize - 1 ] == $tag ) {
     2557            } elseif ( $is_single_tag ) { // ElseIf it's a known single-entity tag but it doesn't close itself, do so
     2558                $pre_attribute_ws = ' ';
     2559                $attributes      .= '/';
     2560            } else { // It's not a single-entity tag.
     2561                // If the top of the stack is the same as the tag we want to push, close previous tag.
     2562                if ( $stacksize > 0 && ! in_array( $tag, $nestable_tags, true ) && $tagstack[ $stacksize - 1 ] === $tag ) {
    25262563                    $tagqueue = '</' . array_pop( $tagstack ) . '>';
    25272564                    $stacksize--;
     
    25302567            }
    25312568
    2532             // Attributes
    2533             $attributes = $regex[2];
    2534             if ( ! empty( $attributes ) && $attributes[0] != '>' ) {
    2535                 $attributes = ' ' . $attributes;
     2569            // Attributes.
     2570            if ( $has_self_closer && $is_single_tag ) {
     2571                // We need some space - avoid <br/> and prefer <br />.
     2572                $pre_attribute_ws = ' ';
    25362573            }
    25372574
    2538             $tag = '<' . $tag . $attributes . '>';
    2539             //If already queuing a close tag, then put this tag on, too
     2575            $tag = '<' . $tag . $pre_attribute_ws . $attributes . '>';
     2576            // If already queuing a close tag, then put this tag on too.
    25402577            if ( ! empty( $tagqueue ) ) {
    25412578                $tagqueue .= $tag;
     
    25472584    }
    25482585
    2549     // Clear Tag Queue
     2586    // Clear Tag Queue.
    25502587    $newtext .= $tagqueue;
    25512588
    2552     // Add Remaining text
     2589    // Add remaining text.
    25532590    $newtext .= $text;
    25542591
    2555     // Empty Stack
    25562592    while ( $x = array_pop( $tagstack ) ) {
    2557         $newtext .= '</' . $x . '>'; // Add remaining tags to close
    2558     }
    2559 
    2560     // WP fix for the bug with HTML comments
     2593        $newtext .= '</' . $x . '>'; // Add remaining tags to close.
     2594    }
     2595
     2596    // WP fix for the bug with HTML comments.
    25612597    $newtext = str_replace( '< !--', '<!--', $newtext );
    25622598    $newtext = str_replace( '<    !--', '< !--', $newtext );
Note: See TracChangeset for help on using the changeset viewer.