Make WordPress Core

Ticket #50683: shortcode-parser.4.diff

File shortcode-parser.4.diff, 22.5 KB (added by cfinke, 4 years ago)

Optimizes inner content parsing by skipping ahead to the next bracket inside the content. This improves this parser's performance on the above-mentioned speed test suite to about 0.05 seconds total.

  • src/wp-includes/shortcodes.php

     
    214214
    215215        $content = do_shortcodes_in_html_tags( $content, $ignore_html, $tagnames );
    216216
    217         $pattern = get_shortcode_regex( $tagnames );
    218         $content = preg_replace_callback( "/$pattern/", 'do_shortcode_tag', $content );
     217        $shortcode_parser = new Shortcode_Parser( $content, $tagnames );
     218        $content = $shortcode_parser->parse();
    219219
    220220        // Always restore square braces so we don't break things like <!--[if IE ]>.
    221221        $content = unescape_invalid_shortcodes( $content );
     
    224224}
    225225
    226226/**
     227 * A collection of methods for parsing and executing shortcodes in content.
     228 */
     229class Shortcode_Parser {
     230        private $content;
     231        private $state;
     232        private $cursor_position;
     233        private $stack;
     234        private $tagnames;
     235        private $current_shortcode;
     236
     237        const SHORTCODE_PARSE_STATE_DEFAULT          = 0;
     238        const SHORTCODE_PARSE_STATE_IN_TAG           = 1;
     239        const SHORTCODE_PARSE_STATE_IN_CONTENT       = 2;
     240        const SHORTCODE_PARSE_STATE_IN_QUOTED_STRING = 3;
     241
     242        private $DEBUG = false;
     243
     244        public function __construct( $content, $tagnames ) {
     245                $this->content  = $content;
     246                $this->tagnames = $tagnames;
     247        }
     248
     249        /**
     250         * Parse shortcodes in content and replace them with the output that their handler functions generate.
     251         */
     252        public function parse() {
     253                $this->stack = array();
     254
     255                /**
     256                 * A regular expression that checks whether a string appears to begin with a tag for
     257                 * a registered shortcode.
     258                 */
     259                $registered_shortcode_regex= '/^(?P<extra_opening_bracket>\\[?)(?P<opening_bracket>\\[)(?P<tag_slug>' . join( '|', array_map( 'preg_quote', $this->tagnames ) ) . ')(?![\\w-])/u';
     260
     261                $this->cursor_position = 0;
     262
     263                // Save some parsing time by starting a few characters before the first bracket.
     264                $this->forward_cursor_to_next_bracket();
     265
     266                $this->state = self::SHORTCODE_PARSE_STATE_DEFAULT;
     267
     268                $is_escaped = false;
     269                $delimiter = null;
     270
     271                $this->debug( 'Parsing content: ' . $this->content );
     272
     273                while ( $this->cursor_position < strlen( $this->content ) ) {
     274                        $char = substr( $this->content, $this->cursor_position, 1 );
     275
     276                        $this->debug( 'In position ' . $this->cursor_position . ' with state ' . $this->state . ', looking at character "' . $char . '"' );
     277
     278                        $found_escape_character = false;
     279
     280                        switch ( $this->state ) {
     281                                case self::SHORTCODE_PARSE_STATE_DEFAULT:
     282                                case self::SHORTCODE_PARSE_STATE_IN_CONTENT:
     283                                        if (
     284                                                   ! $is_escaped
     285                                                && '[' === $char
     286                                                && preg_match( $registered_shortcode_regex, substr( $this->content, $this->cursor_position ), $m ) ) {
     287                                                if ( $this->current_shortcode ) {
     288                                                        $this->stack[] = $this->current_shortcode;
     289                                                }
     290
     291                                                // We have found the beginning of a shortcode.
     292                                                $this->current_shortcode = array(
     293                                                        'full_tag' => $m[0],
     294                                                        'extra_opening_bracket' => $m['extra_opening_bracket'],
     295                                                        'tag_slug' => $m['tag_slug'],
     296                                                        'atts_and_values' => '',
     297                                                        'self_closing_slash' => '',
     298                                                        'inner_content' => '',
     299                                                        'extra_closing_bracket' => '',
     300                                                        'cursor_position' => $this->cursor_position,
     301                                                );
     302
     303                                                $this->cursor_position += strlen( $m[0] );
     304
     305                                                $this->debug( 'Found "' . $m[0] . '", moving to position ' . $this->cursor_position );
     306                                                $this->debug( $this->current_shortcode );
     307
     308                                                // Move back one so it's as if we just processed the last character of the shortcode slug.
     309                                                $this->cursor_position--;
     310
     311                                                $this->state = self::SHORTCODE_PARSE_STATE_IN_TAG;
     312                                        } else {
     313                                                if ( self::SHORTCODE_PARSE_STATE_IN_CONTENT === $this->state ) {
     314                                                        $this->current_shortcode['full_tag'] .= $char;
     315                                                }
     316
     317                                                if ( ! $is_escaped && '\\' === $char ) {
     318                                                        // The next character is escaped.
     319                                                        $found_escape_character = true;
     320
     321                                                        if ( self::SHORTCODE_PARSE_STATE_IN_CONTENT === $this->state ) {
     322                                                                $this->current_shortcode['inner_content'] .= $char;
     323                                                        }
     324                                                } elseif ( $is_escaped ) {
     325                                                        if ( self::SHORTCODE_PARSE_STATE_IN_CONTENT === $this->state ) {
     326                                                                $this->current_shortcode['inner_content'] .= $char;
     327                                                        }
     328                                                } elseif ( self::SHORTCODE_PARSE_STATE_IN_CONTENT === $this->state && '[' === $char ) {
     329                                                        // Check whether it's a closing tag of any currently open shortcode.
     330                                                        $rest_of_closing_tag = '/' . $this->current_shortcode['tag_slug'] . ']';
     331
     332                                                        if ( $rest_of_closing_tag === substr( $this->content, $this->cursor_position + 1, strlen( $rest_of_closing_tag ) ) ) {
     333                                                                // The end of this shortcode.
     334
     335                                                                $this->current_shortcode['full_tag'] .= $rest_of_closing_tag;
     336
     337                                                                // Move the cursor to the end of the closing tag.
     338                                                                $this->cursor_position += strlen( $rest_of_closing_tag );
     339
     340                                                                if ( $this->current_shortcode['extra_opening_bracket'] ) {
     341                                                                        if ( ']' === substr( $this->content, $this->cursor_position + 1, 1 ) ) {
     342                                                                                $this->current_shortcode['full_tag'] .= ']';
     343                                                                                $this->current_shortcode['extra_closing_bracket'] = ']';
     344                                                                                $this->cursor_position++;
     345                                                                        } else {
     346                                                                                // If there was an extra opening bracket but not an extra closing bracket,
     347                                                                                // ignore the extra opening bracket.
     348
     349                                                                                $this->current_shortcode['full_tag'] = substr( $this->current_shortcode['full_tag'], 1 );
     350                                                                                $this->current_shortcode['extra_opening_bracket'] = '';
     351
     352                                                                                // We initially thought it had an extra opening bracket, but it doesn't,
     353                                                                                // so it started one character later than we thought.
     354                                                                                $this->current_shortcode['cursor_position'] += 1;
     355                                                                        }
     356                                                                }
     357
     358                                                                $this->process_current_shortcode();
     359                                                        } else {
     360                                                                $this->debug( 'The closing tag was not for the currently open shortcode.' );
     361
     362                                                                $found_matching_shortcode = false;
     363
     364                                                                for ( $stack_index = count( $this->stack ) - 1; $stack_index >= 0; $stack_index-- ) {
     365                                                                        $rest_of_closing_tag = '/' . $this->stack[ $stack_index ]['tag_slug'] . ']';
     366
     367                                                                        if ( $rest_of_closing_tag === substr( $this->content, $this->cursor_position + 1, strlen( $rest_of_closing_tag ) ) ) {
     368                                                                                // Yes, it closes this one.
     369                                                                                $found_matching_shortcode = true;
     370
     371                                                                                if ( self::SHORTCODE_PARSE_STATE_IN_CONTENT === $this->state ) {
     372                                                                                        // We already saved the bracket as part of the full tag, expecting that the closing tag would be for the current shortcode.
     373                                                                                        // It's not, so remove it.
     374                                                                                        $this->current_shortcode['full_tag'] = substr( $this->current_shortcode['full_tag'], 0, -1 );
     375                                                                                }
     376
     377                                                                                $this->debug( 'The closing tag was for this shortcode:', $this->stack[ $stack_index ] );
     378
     379                                                                                // This means that the "current" shortcode and any others above this one on the stack need to be closed out, because they are self-closing.
     380                                                                                do {
     381                                                                                        $this->debug( 'Inner content was:', $this->current_shortcode['inner_content'], 'Full tag was:', $this->current_shortcode['full_tag'] );
     382
     383                                                                                        $this->current_shortcode['full_tag'] = substr( $this->current_shortcode['full_tag'], 0, -1 * strlen( $this->current_shortcode['inner_content'] ) );
     384
     385                                                                                        // And there is no inner content.
     386                                                                                        $this->current_shortcode['inner_content'] = '';
     387
     388                                                                                        $this->process_current_shortcode(); // This sets $current_shortcode using the top stack item, so we don't need to do it.
     389                                                                                } while ( count( $this->stack ) > $stack_index + 1 );
     390
     391                                                                                // At this point, the shortcode that is being closed right now is $this->current_shortcode.
     392                                                                                // The easiest way to process this without duplicating code is to reprocess the current
     393                                                                                // character with the new stack and current shortcode, so the section above will get
     394                                                                                // triggered, since the closing tag will be for the current shortcode.
     395
     396                                                                                $this->debug( 'Restarting this iteration of the parsing loop with new stack structure.' );
     397                                                                                continue 3;
     398                                                                        }
     399                                                                }
     400
     401
     402                                                                if ( ! $found_matching_shortcode ) {
     403                                                                        $this->current_shortcode['inner_content'] .= $char;
     404                                                                }
     405                                                        }
     406                                                } elseif ( self::SHORTCODE_PARSE_STATE_IN_CONTENT === $this->state ) {
     407                                                        $this->current_shortcode['inner_content'] .= $char;
     408                                                }
     409                                        }
     410
     411                                        break;
     412                                case self::SHORTCODE_PARSE_STATE_IN_TAG:
     413                                        $this->current_shortcode['full_tag'] .= $char;
     414
     415                                        if ( ! $is_escaped && '/' === $char && substr( $this->content, $this->cursor_position + 1, 1 ) === ']' ) {
     416                                                // The shortcode is over.
     417                                                $this->current_shortcode['self_closing_slash'] = '/';
     418                                                $this->current_shortcode['full_tag'] .= ']';
     419                                                $this->cursor_position++;
     420
     421                                                // If the shortcode had an extra opening bracket but doesn't have an extra
     422                                                // closing bracket, ignore the extra opening bracket.
     423
     424                                                if ( $this->current_shortcode['extra_opening_bracket'] ) {
     425                                                        if ( ']' === substr( $this->content, $this->cursor_position + 1, 1 ) ) {
     426                                                                $this->current_shortcode['extra_closing_bracket'] = ']';
     427                                                                $this->current_shortcode['full_tag'] .= ']';
     428                                                                $this->cursor_position++;
     429                                                        } else {
     430                                                                $this->current_shortcode['full_tag'] = substr( $this->current_shortcode['full_tag'], 1 );
     431                                                                $this->current_shortcode['extra_opening_bracket'] = '';
     432
     433                                                                // We initially thought it had an extra opening bracket, but it doesn't,
     434                                                                // so it started one character later than we thought.
     435                                                                $this->current_shortcode['cursor_position'] += 1;
     436                                                        }
     437                                                }
     438
     439                                                $this->process_current_shortcode();
     440
     441                                                break;
     442                                        } elseif ( ! $is_escaped && ']' === $char ) {
     443                                                if ( $this->current_shortcode['extra_opening_bracket'] ) {
     444                                                        // This makes the assumption that this shortcode is closed as soon as the double brackets are found:
     445                                                        //
     446                                                        // [[my-shortcode]][/my-shortcode]]
     447                                                        //
     448                                                        // But in theory, this could just be a shortcode with the content "]".
     449
     450                                                        if ( ']' === substr( $this->content, $this->cursor_position + 1, 1 ) ) {
     451                                                                $this->current_shortcode['extra_closing_bracket'] = ']';
     452                                                                $this->current_shortcode['full_tag'] .= ']';
     453                                                                $this->cursor_position++;
     454
     455                                                                $this->process_current_shortcode();
     456                                                                break;
     457                                                        } else {
     458                                                                // There was not an extra closing bracket.
     459                                                                $this->debug( 'Extra closing bracket not found; the character was ' . substr( $this->content, $this->cursor_position + 1, 1 ) );
     460                                                        }
     461                                                }
     462
     463                                                if ( false === strpos( substr( $this->content, $this->cursor_position ), '[/' . $this->current_shortcode['tag_slug'] . ']' ) ) {
     464                                                        // If there's no closing tag, it's a self-enclosed shortcode, and we're done with it.
     465                                                        $this->process_current_shortcode();
     466                                                } else {
     467                                                        $this->debug( 'Expecting to find a closing tag for ' . $this->current_shortcode['tag_slug'] );
     468                                                        $this->state = self::SHORTCODE_PARSE_STATE_IN_CONTENT;
     469                                                       
     470                                                        $current_cursor_position = $this->cursor_position;
     471                                                        $this->forward_cursor_to_next_bracket();
     472                       
     473                                                        if ( $this->cursor_position != $current_cursor_position ) {
     474                                                                /*
     475                                                                 * The +1 is because the character at $current_cursor_position has already been recorded.
     476                                                                 */
     477                                                                $skipped_content = substr( $this->content, $current_cursor_position + 1, $this->cursor_position - $current_cursor_position );
     478                               
     479                                                                $this->current_shortcode['inner_content'] .= $skipped_content;
     480                                                                $this->current_shortcode['full_tag'] .= $skipped_content;
     481                                                        }
     482                                                }
     483                                        } else {
     484                                                $this->current_shortcode['atts_and_values'] .= $char;
     485
     486                                                if ( ! $is_escaped && '\\' === $char ) {
     487                                                        $found_escape_character = true;
     488                                                } elseif ( ! $is_escaped && ( '"' === $char || "'" === $char ) ) {
     489                                                        $this->state = self::SHORTCODE_PARSE_STATE_IN_QUOTED_STRING;
     490                                                        $delimiter = $char;
     491                                                } else {
     492                                                        // Nothing to do.
     493                                                }
     494                                        }
     495
     496                                        break;
     497                                case self::SHORTCODE_PARSE_STATE_IN_QUOTED_STRING:
     498                                        $this->current_shortcode['full_tag'] .= $char;
     499                                        $this->current_shortcode['atts_and_values'] .= $char;
     500
     501                                        if ( $is_escaped ) {
     502                                                // Nothing to do. This is just an escaped character to be taken literally.
     503                                        } else {
     504                                                // Not escaped.
     505                                                if ( '\\' === $char ) {
     506                                                        // The next character is escaped.
     507                                                        $found_escape_character = true;
     508                                                } elseif ( $char === $delimiter ) {
     509                                                        $this->state = self::SHORTCODE_PARSE_STATE_IN_TAG;
     510                                                        $delimiter = null;
     511                                                }
     512                                        }
     513
     514                                        break;
     515                        }
     516
     517                        // Is the next character escaped?
     518                        if ( $found_escape_character ) {
     519                                $is_escaped = true;
     520                        } else {
     521                                // If we didn't find an escape character here, then no.
     522                                $is_escaped = false;
     523                        }
     524
     525                        $this->cursor_position++;
     526
     527                        $this->debug( 'Cursor position is ' . $this->cursor_position . '; strlen is ' . strlen( $this->content ) );
     528                }
     529
     530                if ( self::SHORTCODE_PARSE_STATE_IN_QUOTED_STRING === $this->state ) {
     531                        // example: This is my content [footag foo=" [bartag]
     532                        // Should it be reprocessed in order to convert [bartag] or is this considered malformed?
     533                }
     534
     535                if ( self::SHORTCODE_PARSE_STATE_IN_TAG === $this->state ) {
     536                        // example: This is my content [footag foo="abc" bar="def" [bartag]
     537                        // Should it be reprocessed in order to convert [bartag] or is this considered malformed?
     538                }
     539
     540                if ( $this->current_shortcode ) {
     541                        $this->debug( 'There are still pending shortcodes to process.', $this->current_shortcode, $this->stack );
     542
     543                        /*
     544                         * If we end with shortcodes still on the stack, then there was a situation like this:
     545                         *
     546                         * [footag] [bartag] [baztag] [footag]content[/footag]
     547                         *
     548                         * i.e., a scenario where the parser was unsure whether the first [footag] was self-closing or not.
     549                         *
     550                         * By this point, $content will be in this format:
     551                         *
     552                         * [footag] bartag-output baztag-output footag-content-output
     553                         *
     554                         * so we need to back up and process the still-stored shortcodes as unclosed.
     555                         *
     556                         * An extreme version of this would look like:
     557                         *
     558                         * [footag] [footag] [footag] [footag] [footag] [footag] [footag] [footag] ... [footag][/footag]
     559                         *
     560                         * where the last tag would be the only one processed normally above and there would be n-1 [footag]s still on the stack.
     561                         */
     562                        while ( $this->current_shortcode ) {
     563                                // What we thought was part of this tag was just regular content.
     564                                $this->current_shortcode['full_tag'] = substr( $this->current_shortcode['full_tag'], 0, -1 * strlen( $this->current_shortcode['inner_content'] ) );
     565
     566                                // And there is no inner content.
     567                                $this->current_shortcode['inner_content'] = '';
     568
     569                                $this->process_current_shortcode(); // This sets $current_shortcode, so we don't need to do it.
     570                        }
     571                }
     572
     573                return $this->content;
     574        }
     575
     576        /**
     577         * Create an argument to pass to do_shortcode_tag. The format of this argument was determined
     578         * by the capture groups of the regular expression that used to be used to parse shortcodes out of content.
     579         *
     580         * @param array $shortcode An associative array comprising data about a shortcode in the text.
     581         * @return array A numerically-indexed array of the shortcode data ready for do_shortcode_tag().
     582         */
     583        private function shortcode_argument( $shortcode ) {
     584                return array(
     585                        $shortcode['full_tag'],
     586                        $shortcode['extra_opening_bracket'],
     587                        $shortcode['tag_slug'],
     588                        $shortcode['atts_and_values'],
     589                        $shortcode['self_closing_slash'],
     590                        $shortcode['inner_content'],
     591                        $shortcode['extra_closing_bracket'],
     592                );
     593        }
     594
     595        /**
     596         * The shortcode at the top of the stack is complete and can be processed.
     597         * Process it and modify the enclosing shortcode as if the content was passed in
     598         * with this shortcode already converted into HTML.
     599         */
     600        private function process_current_shortcode() {
     601                $this->debug( 'Content is: ' . $this->content );
     602
     603                $this->debug( $this->current_shortcode );
     604
     605                $argument_for_do_shortcode_tag = $this->shortcode_argument( $this->current_shortcode );
     606
     607                $shortcode_output = do_shortcode_tag( $argument_for_do_shortcode_tag );
     608
     609                // Replace based on position rather than find and replace, since this content is possible:
     610                //
     611                // Test 123 [some-shortcode] To use my shortcode, type [[some-shortcode]].
     612                $this->content =
     613                          substr( $this->content, 0, $this->current_shortcode['cursor_position'] )
     614                        . $shortcode_output
     615                        . substr( $this->content, $this->current_shortcode['cursor_position'] + strlen( $this->current_shortcode['full_tag'] ) )
     616                        ;
     617
     618                // Update the cursor position to the end of this shortcode's output.
     619                // The -1 is because the position is incremented after this gets called to move it to the next character.
     620                $this->cursor_position = $this->current_shortcode['cursor_position'] + strlen( $shortcode_output ) - 1;
     621
     622                // For any enclosing shortcode, its inner content needs to include the full output of this shortcode.
     623                if ( ! empty( $this->stack ) ) {
     624                        $this->current_shortcode = array_pop( $this->stack );
     625
     626                        $this->current_shortcode['inner_content'] .= $shortcode_output;
     627                        $this->current_shortcode['full_tag'] .= $shortcode_output;
     628
     629                        $this->state = self::SHORTCODE_PARSE_STATE_IN_CONTENT;
     630                       
     631                        $current_cursor_position = $this->cursor_position;
     632                        $this->forward_cursor_to_next_bracket();
     633                       
     634                        if ( $this->cursor_position != $current_cursor_position ) {
     635                                /*
     636                                 * The +1 is because the character at $current_cursor_position has already been recorded.
     637                                 */
     638                                $skipped_content = substr( $this->content, $current_cursor_position + 1, $this->cursor_position - $current_cursor_position );
     639                               
     640                                $this->current_shortcode['inner_content'] .= $skipped_content;
     641                                $this->current_shortcode['full_tag'] .= $skipped_content;
     642                        }
     643                } else {
     644                        $this->current_shortcode = null;
     645
     646                        $this->state = self::SHORTCODE_PARSE_STATE_DEFAULT;
     647
     648                        // In the default state, we can skip over any content that couldn't be a shortcode,
     649                        // so let's move forward near the next bracket.
     650                        $this->forward_cursor_to_next_bracket();
     651                }
     652
     653                $this->debug( 'Content is: ' . $this->content );
     654        }
     655
     656        private function forward_cursor_to_next_bracket() {
     657                /*
     658                 * The max() here is because $cursor_position can be -1 if a shortcode
     659                 * at the beginning of the content didn't have any output and reset the
     660                 * cursor back to the beginning. It's -1 instead of zero because it will
     661                 * be incremented later in the loop to set it to zero for the next iteration.
     662                 */
     663                $next_bracket_location = strpos( $this->content, '[', max( 0, $this->cursor_position ) );
     664
     665                if ( false !== $next_bracket_location ) {
     666                        $this->debug( 'Current cursor position: ' . $this->cursor_position );
     667
     668                        /*
     669                         * Again, the -1 is because this will be incremented before it is used,
     670                         * and we really want it to have a minimum value of zero.
     671                         */
     672                        $this->cursor_position = max( -1, $next_bracket_location - 1 );
     673
     674                        $this->debug( 'Jumped ahead to position ' . $this->cursor_position );
     675                }
     676        }
     677
     678        /**
     679         * Outputs debug data.  Useful when running unit tests to see how content is being parsed.
     680         *
     681         * @param mixed One of more variables of any type.
     682         */
     683        private function debug( /* ... */ ) {
     684                if ( defined( 'WP_DEBUG' ) && WP_DEBUG && $this->DEBUG ) {
     685                        foreach ( func_get_args() as $arg ) {
     686                                if ( 'string' === gettype( $arg ) ) {
     687                                        error_log( 'Shortcode_Parser debug: ' . $arg );
     688                                } else {
     689                                        error_log( 'Shortcode_Parser debug: ' . var_export( $arg, true ) );
     690                                }
     691                        }
     692                }
     693        }
     694
     695}
     696
     697/**
    227698 * Retrieve the shortcode regular expression for searching.
    228699 *
    229700 * The regular expression combines the shortcode tags in the regular expression
  • tests/phpunit/tests/shortcode.php

     
    972972                );
    973973                $this->assertEquals( 'test-shortcode-tag', $this->tagname );
    974974        }
    975 }
     975       
     976        function test_bracket_in_shortcode_attribute() {
     977                do_shortcode( '[test-shortcode-tag subject="[This is my subject]" /]' );
     978                $expected_attrs = array(
     979                        "subject" => "[This is my subject]",
     980                );
     981                $this->assertEquals( $expected_attrs, $this->atts );
     982        }
     983       
     984        function test_self_closing_shortcode_with_quoted_end_tag() {
     985                $out = do_shortcode( '[test-shortcode-tag]Test 123[footag foo="[/test-shortcode-tag]"/] [baztag]bazcontent[/baztag]' );
     986               
     987                $this->assertEquals( 'Test 123foo = [/test-shortcode-tag] content = bazcontent', $out );
     988        }
     989       
     990        function test_nested_shortcodes() {
     991                /*
     992                do_shortcode( '[test-shortcode-tag]Some content [footag foo="foo content"/] some other content[/test-shortcode-tag]' );
     993               
     994                $this->assertEquals( 'Some content foo = foo content some other content', $this->content );
     995
     996                $out = do_shortcode( '[footag foo="1"][footag foo="2"][footag foo="3"][footag foo="4"][/footag][footag foo="4a"][/footag][/footag][/footag][/footag]' );
     997               
     998                $this->assertEquals( 'foo = 1', $out );
     999                */
     1000                $out = do_shortcode( '[footag foo="1"] abc [bartag foo="2"] def [/footag] something else [test-shortcode-tag attr="[/footag]" attr2="[/bartag]"][/test-shortcode-tag]' );
     1001               
     1002                $this->assertEquals( 'foo = 1something else ', $out );
     1003        }
     1004       
     1005        /**
     1006         * @ticket 49955
     1007         */
     1008        function test_ticket_49955() {
     1009                add_shortcode( 'ucase', function ( $atts, $content ) {
     1010                        return strtoupper( $content );
     1011                } );
     1012                       
     1013                $out = do_shortcode( 'This [[ucase]] shortcode [ucase]demonstrates[/ucase] the usage of enclosing shortcodes.' );
     1014               
     1015                $this->assertEquals( 'This [ucase] shortcode DEMONSTRATES the usage of enclosing shortcodes.', $out );
     1016        }
     1017       
     1018                /**
     1019         * @ticket 43725
     1020         */
     1021        public function test_same_tag_multiple_formats_open_closed_one() {
     1022                $in = <<<EOT
     1023This post uses URL multiple times.
     1024
     1025[url]Now this is wrapped[/url]
     1026
     1027[url] This one is standalone
     1028
     1029[url]Now this is wrapped too[/url]
     1030EOT;
     1031                $expected = <<<EOT
     1032This post uses URL multiple times.
     1033
     1034http://www.wordpress.org/
     1035
     1036http://www.wordpress.org/ This one is standalone
     1037
     1038http://www.wordpress.org/
     1039EOT;
     1040                $out      = do_shortcode( $in );
     1041                $this->assertEquals( strip_ws( $expected ), strip_ws( $out ) );
     1042        }
     1043
     1044        /**
     1045         * @ticket 43725
     1046         */
     1047        public function test_same_tag_multiple_formats_open_closed_two() {
     1048                $in = <<<EOT
     1049This post uses URL multiple times.
     1050
     1051[url]Now this is wrapped[/url]
     1052
     1053[url/] This one is standalone
     1054
     1055[url]Now this is wrapped too[/url]
     1056EOT;
     1057                $expected = <<<EOT
     1058This post uses URL multiple times.
     1059
     1060http://www.wordpress.org/
     1061
     1062http://www.wordpress.org/ This one is standalone
     1063
     1064http://www.wordpress.org/
     1065EOT;
     1066                $out      = do_shortcode( $in );
     1067                $this->assertEquals( strip_ws( $expected ), strip_ws( $out ) );
     1068        }
     1069}
     1070 No newline at end of file