Make WordPress Core

Changeset 58867


Ignore:
Timestamp:
08/08/2024 07:23:53 AM (2 years ago)
Author:
dmsnell
Message:

HTML API: Add support for SVG and MathML (Foreign content)

As part of work to add more spec support to the HTML API, this patch adds
support for SVG and MathML elements, or more generally, "foreign content."

The rules in foreign content are a mix of XML and HTML parsing rules and
introduce additional complexity into the processor, but is important in
order to avoid getting lost when inside these elements.

Developed in https://github.com/wordpress/wordpress-develop/pull/6006
Discussed in https://core.trac.wordpress.org/ticket/61576

Props: dmsnell, jonsurrell, westonruter.
See #61576.

Location:
trunk
Files:
10 edited

Legend:

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

    r58833 r58867  
    114114         * @param int $nth Retrieve the nth item on the stack, with 1 being
    115115         *                 the top element, 2 being the second, etc...
    116          * @return string|null Name of the node on the stack at the given location,
    117          *                     or `null` if the location isn't on the stack.
    118          */
    119         public function at( int $nth ): ?string {
     116         * @return WP_HTML_Token|null Name of the node on the stack at the given location,
     117         *                            or `null` if the location isn't on the stack.
     118         */
     119        public function at( int $nth ): ?WP_HTML_Token {
    120120                foreach ( $this->walk_down() as $item ) {
    121121                        if ( 0 === --$nth ) {
    122                                 return $item->node_name;
     122                                return $item;
    123123                        }
    124124                }
     
    243243        public function has_element_in_specific_scope( string $tag_name, $termination_list ): bool {
    244244                foreach ( $this->walk_up() as $node ) {
    245                         if ( $node->node_name === $tag_name ) {
     245                        $namespaced_name = 'html' === $node->namespace
     246                                ? $node->node_name
     247                                : "{$node->namespace} {$node->node_name}";
     248
     249                        if ( $namespaced_name === $tag_name ) {
    246250                                return true;
    247251                        }
     
    249253                        if (
    250254                                '(internal: H1 through H6 - do not use)' === $tag_name &&
    251                                 in_array( $node->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true )
     255                                in_array( $namespaced_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true )
    252256                        ) {
    253257                                return true;
    254258                        }
    255259
    256                         if ( in_array( $node->node_name, $termination_list, true ) ) {
     260                        if ( in_array( $namespaced_name, $termination_list, true ) ) {
    257261                                return false;
    258262                        }
     
    289293         *
    290294         * @since 6.4.0
    291          * @since 6.7.0 Supports all required HTML elements.
     295         * @since 6.7.0 Full support.
    292296         *
    293297         * @see https://html.spec.whatwg.org/#has-an-element-in-scope
     
    310314                                'TEMPLATE',
    311315
    312                                 /*
    313                                  * @todo Support SVG and MathML nodes when support for foreign content is added.
    314                                  *
    315                                  * - MathML mi
    316                                  * - MathML mo
    317                                  * - MathML mn
    318                                  * - MathML ms
    319                                  * - MathML mtext
    320                                  * - MathML annotation-xml
    321                                  * - SVG foreignObject
    322                                  * - SVG desc
    323                                  * - SVG title
    324                                  */
     316                                'math MI',
     317                                'math MO',
     318                                'math MN',
     319                                'math MS',
     320                                'math MTEXT',
     321                                'math ANNOTATION-XML',
     322
     323                                'svg FOREIGNOBJECT',
     324                                'svg DESC',
     325                                'svg TITLE',
    325326                        )
    326327                );
     
    364365                                'UL',
    365366
    366                                 /*
    367                                  * @todo Support SVG and MathML nodes when support for foreign content is added.
    368                                  *
    369                                  * - MathML mi
    370                                  * - MathML mo
    371                                  * - MathML mn
    372                                  * - MathML ms
    373                                  * - MathML mtext
    374                                  * - MathML annotation-xml
    375                                  * - SVG foreignObject
    376                                  * - SVG desc
    377                                  * - SVG title
    378                                  */
     367                                'math MI',
     368                                'math MO',
     369                                'math MN',
     370                                'math MS',
     371                                'math MTEXT',
     372                                'math ANNOTATION-XML',
     373
     374                                'svg FOREIGNOBJECT',
     375                                'svg DESC',
     376                                'svg TITLE',
    379377                        )
    380378                );
     
    414412                                'TEMPLATE',
    415413
    416                                 /*
    417                                  * @todo Support SVG and MathML nodes when support for foreign content is added.
    418                                  *
    419                                  * - MathML mi
    420                                  * - MathML mo
    421                                  * - MathML mn
    422                                  * - MathML ms
    423                                  * - MathML mtext
    424                                  * - MathML annotation-xml
    425                                  * - SVG foreignObject
    426                                  * - SVG desc
    427                                  * - SVG title
    428                                  */
     414                                'math MI',
     415                                'math MO',
     416                                'math MN',
     417                                'math MS',
     418                                'math MTEXT',
     419                                'math ANNOTATION-XML',
     420
     421                                'svg FOREIGNOBJECT',
     422                                'svg DESC',
     423                                'svg TITLE',
    429424                        )
    430425                );
     
    693688         */
    694689        public function after_element_push( WP_HTML_Token $item ): void {
     690                $namespaced_name = 'html' === $item->namespace
     691                        ? $item->node_name
     692                        : "{$item->namespace} {$item->node_name}";
     693
    695694                /*
    696695                 * When adding support for new elements, expand this switch to trap
    697696                 * cases where the precalculated value needs to change.
    698697                 */
    699                 switch ( $item->node_name ) {
     698                switch ( $namespaced_name ) {
    700699                        case 'APPLET':
    701700                        case 'BUTTON':
     
    708707                        case 'OBJECT':
    709708                        case 'TEMPLATE':
     709                        case 'math MI':
     710                        case 'math MO':
     711                        case 'math MN':
     712                        case 'math MS':
     713                        case 'math MTEXT':
     714                        case 'math ANNOTATION-XML':
     715                        case 'svg FOREIGNOBJECT':
     716                        case 'svg DESC':
     717                        case 'svg TITLE':
    710718                                $this->has_p_in_button_scope = false;
    711719                                break;
     
    751759                        case 'OBJECT':
    752760                        case 'TEMPLATE':
     761                        case 'math MI':
     762                        case 'math MO':
     763                        case 'math MN':
     764                        case 'math MS':
     765                        case 'math MTEXT':
     766                        case 'math ANNOTATION-XML':
     767                        case 'svg FOREIGNOBJECT':
     768                        case 'svg DESC':
     769                        case 'svg TITLE':
    753770                                $this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' );
    754771                                break;
  • trunk/src/wp-includes/html-api/class-wp-html-processor-state.php

    r58836 r58867  
    301301
    302302        /**
    303          * In foreign content insertion mode for full HTML parser.
    304          *
    305          * @since 6.7.0
    306          *
    307          * @see https://html.spec.whatwg.org/#parsing-main-inforeign
    308          * @see WP_HTML_Processor_State::$insertion_mode
    309          *
    310          * @var string
    311          */
    312         const INSERTION_MODE_IN_FOREIGN_CONTENT = 'insertion-mode-in-foreign-content';
    313 
    314         /**
    315303         * No-quirks mode document compatability mode.
    316304         *
  • trunk/src/wp-includes/html-api/class-wp-html-processor.php

    r58841 r58867  
    308308                $processor->bookmarks['context-node'] = new WP_HTML_Span( 0, 0 );
    309309
    310                 $processor->state->stack_of_open_elements->push(
    311                         new WP_HTML_Token(
    312                                 'root-node',
    313                                 'HTML',
    314                                 false
    315                         )
     310                $root_node = new WP_HTML_Token(
     311                        'root-node',
     312                        'HTML',
     313                        false
    316314                );
     315
     316                $processor->state->stack_of_open_elements->push( $root_node );
    317317
    318318                $context_node = new WP_HTML_Token(
     
    393393                                $provenance            = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real';
    394394                                $this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::PUSH, $provenance );
     395
     396                                $this->change_parsing_namespace( $token->namespace );
    395397                        }
    396398                );
     
    402404                                $provenance            = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real';
    403405                                $this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::POP, $provenance );
     406                                $adjusted_current_node = $this->get_adjusted_current_node();
     407                                $this->change_parsing_namespace(
     408                                        $adjusted_current_node
     409                                                ? $adjusted_current_node->namespace
     410                                                : 'html'
     411                                );
    404412                        }
    405413                );
     
    768776         * closing as soon as the processor advances to the next token.
    769777         *
     778         * @todo Review the self-closing logic when no node is present, ensure it
     779         *       matches the expectations in `step()`.
     780         *
    770781         * @since 6.6.0
    771          *
    772          * @todo When adding support for foreign content, ensure that
    773          *       this returns false for self-closing elements in the
    774          *       SVG and MathML namespace.
    775782         *
    776783         * @param WP_HTML_Token|null $node Optional. Node to examine, if provided.
     
    779786         *                   or `null` if not matched on any token.
    780787         */
    781         public function expects_closer( $node = null ): ?bool {
    782                 $token_name = $node->node_name ?? $this->get_token_name();
     788        public function expects_closer( WP_HTML_Token $node = null ): ?bool {
     789                $token_name      = $node->node_name ?? $this->get_token_name();
     790                $token_namespace = $node->namespace ?? $this->get_namespace();
     791
    783792                if ( ! isset( $token_name ) ) {
    784793                        return null;
     
    793802                        self::is_void( $token_name ) ||
    794803                        // Special atomic elements.
    795                         in_array( $token_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true )
     804                        ( 'html' === $token_namespace && in_array( $token_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true ) ) ||
     805                        // Self-closing elements in foreign content.
     806                        ( isset( $node ) && 'html' !== $node->namespace && $node->has_self_closing_flag )
    796807                );
    797808        }
     
    825836                         * When moving on to the next node, therefore, if the bottom-most element
    826837                         * on the stack is a void element, it must be closed.
    827                          *
    828                          * @todo Once self-closing foreign elements and BGSOUND are supported,
    829                          *        they must also be implicitly closed here too. BGSOUND is
    830                          *        special since it's only self-closing if the self-closing flag
    831                          *        is provided in the opening tag, otherwise it expects a tag closer.
    832838                         */
    833839                        $top_node = $this->state->stack_of_open_elements->current_node();
    834                         if ( isset( $top_node ) && ! static::expects_closer( $top_node ) ) {
     840                        if ( isset( $top_node ) && ! $this->expects_closer( $top_node ) ) {
    835841                                $this->state->stack_of_open_elements->pop();
    836842                        }
     
    849855                }
    850856
    851                 $this->state->current_token = new WP_HTML_Token(
    852                         $this->bookmark_token(),
    853                         $this->get_token_name(),
    854                         $this->has_self_closing_flag(),
    855                         $this->release_internal_bookmark_on_destruct
     857                $adjusted_current_node = $this->get_adjusted_current_node();
     858                $is_closer             = $this->is_tag_closer();
     859                $is_start_tag          = WP_HTML_Tag_Processor::STATE_MATCHED_TAG === $this->parser_state && ! $is_closer;
     860                $token_name            = $this->get_token_name();
     861
     862                if ( self::REPROCESS_CURRENT_NODE !== $node_to_process ) {
     863                        $this->state->current_token = new WP_HTML_Token(
     864                                $this->bookmark_token(),
     865                                $token_name,
     866                                $this->has_self_closing_flag(),
     867                                $this->release_internal_bookmark_on_destruct
     868                        );
     869                }
     870
     871                $parse_in_current_insertion_mode = (
     872                        0 === $this->state->stack_of_open_elements->count() ||
     873                        'html' === $adjusted_current_node->namespace ||
     874                        (
     875                                'math' === $adjusted_current_node->integration_node_type &&
     876                                (
     877                                        ( $is_start_tag && ! in_array( $token_name, array( 'MGLYPH', 'MALIGNMARK' ), true ) ) ||
     878                                        '#text' === $token_name
     879                                )
     880                        ) ||
     881                        (
     882                                'math' === $adjusted_current_node->namespace &&
     883                                'ANNOTATION-XML' === $adjusted_current_node->node_name &&
     884                                $is_start_tag && 'SVG' === $token_name
     885                        ) ||
     886                        (
     887                                'html' === $adjusted_current_node->integration_node_type &&
     888                                ( $is_start_tag || '#text' === $token_name )
     889                        )
    856890                );
    857891
    858892                try {
     893                        if ( ! $parse_in_current_insertion_mode ) {
     894                                return $this->step_in_foreign_content();
     895                        }
     896
    859897                        switch ( $this->state->insertion_mode ) {
    860898                                case WP_HTML_Processor_State::INSERTION_MODE_INITIAL:
     
    923961                                case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_FRAMESET:
    924962                                        return $this->step_after_after_frameset();
    925 
    926                                 case WP_HTML_Processor_State::INSERTION_MODE_IN_FOREIGN_CONTENT:
    927                                         return $this->step_in_foreign_content();
    928963
    929964                                // This should be unreachable but PHP doesn't have total type checking on switch.
     
    18541889                                if (
    18551890                                        1 === $this->state->stack_of_open_elements->count() ||
    1856                                         'BODY' !== $this->state->stack_of_open_elements->at( 2 ) ||
     1891                                        'BODY' !== ( $this->state->stack_of_open_elements->at( 2 )->node_name ?? null ) ||
    18571892                                        $this->state->stack_of_open_elements->contains( 'TEMPLATE' )
    18581893                                ) {
     
    18801915                                if (
    18811916                                        1 === $this->state->stack_of_open_elements->count() ||
    1882                                         'BODY' !== $this->state->stack_of_open_elements->at( 2 ) ||
     1917                                        'BODY' !== ( $this->state->stack_of_open_elements->at( 2 )->node_name ?? null ) ||
    18831918                                        false === $this->state->frameset_ok
    18841919                                ) {
     
    20762111                                        'DIV' !== $node->node_name &&
    20772112                                        'P' !== $node->node_name &&
    2078                                         $this->is_special( $node->node_name )
     2113                                        self::is_special( $node )
    20792114                                ) {
    20802115                                        /*
     
    21372172                         * > "figcaption", "figure", "footer", "header", "hgroup", "listing", "main",
    21382173                         * > "menu", "nav", "ol", "pre", "search", "section", "summary", "ul"
    2139                          *
    2140                          * @todo This needs to check if the element in scope is an HTML element, meaning that
    2141                          *       when SVG and MathML support is added, this needs to differentiate between an
    2142                          *       HTML element of the given name, such as `<center>`, and a foreign element of
    2143                          *       the same given name.
    21442174                         */
    21452175                        case '-ADDRESS':
     
    24122442                        /*
    24132443                         * > A end tag token whose tag name is one of: "applet", "marquee", "object"
    2414                          *
    2415                          * @todo This needs to check if the element in scope is an HTML element, meaning that
    2416                          *       when SVG and MathML support is added, this needs to differentiate between an
    2417                          *       HTML element of the given name, such as `<object>`, and a foreign element of
    2418                          *       the same given name.
    24192444                         */
    24202445                        case '-APPLET':
     
    26802705                                 * These ought to be handled in the attribute methods.
    26812706                                 */
    2682 
    2683                                 $this->bail( 'Cannot process MATH element, opening foreign content.' );
    2684                                 break;
     2707                                $this->state->current_token->namespace = 'math';
     2708                                $this->insert_html_element( $this->state->current_token );
     2709                                if ( $this->state->current_token->has_self_closing_flag ) {
     2710                                        $this->state->stack_of_open_elements->pop();
     2711                                }
     2712                                return true;
    26852713
    26862714                        /*
     
    26962724                                 * These ought to be handled in the attribute methods.
    26972725                                 */
    2698 
    2699                                 $this->bail( 'Cannot process SVG element, opening foreign content.' );
    2700                                 break;
     2726                                $this->state->current_token->namespace = 'svg';
     2727                                $this->insert_html_element( $this->state->current_token );
     2728                                if ( $this->state->current_token->has_self_closing_flag ) {
     2729                                        $this->state->stack_of_open_elements->pop();
     2730                                }
     2731                                return true;
    27012732
    27022733                        /*
     
    27382769                         */
    27392770                        foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) {
    2740                                 /*
    2741                                  * @todo This needs to check if the element in scope is an HTML element, meaning that
    2742                                  *       when SVG and MathML support is added, this needs to differentiate between an
    2743                                  *       HTML element of the given name, such as `<object>`, and a foreign element of
    2744                                  *       the same given name.
    2745                                  */
    2746                                 if ( $token_name === $node->node_name ) {
     2771                                if ( 'html' === $node->namespace && $token_name === $node->node_name ) {
    27472772                                        break;
    27482773                                }
    27492774
    2750                                 if ( self::is_special( $node->node_name ) ) {
     2775                                if ( self::is_special( $node ) ) {
    27512776                                        // This is a parse error, ignore the token.
    27522777                                        return $this->step();
     
    40704095         */
    40714096        private function step_in_foreign_content(): bool {
    4072                 $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_FOREIGN_CONTENT . ' state.' );
     4097                $tag_name   = $this->get_token_name();
     4098                $token_type = $this->get_token_type();
     4099                $op_sigil   = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : '';
     4100                $op         = "{$op_sigil}{$tag_name}";
     4101
     4102                /*
     4103                 * > A start tag whose name is "font", if the token has any attributes named "color", "face", or "size"
     4104                 *
     4105                 * This section drawn out above the switch to more easily incorporate
     4106                 * the additional rules based on the presence of the attributes.
     4107                 */
     4108                if (
     4109                        '+FONT' === $op &&
     4110                        (
     4111                                null !== $this->get_attribute( 'color' ) ||
     4112                                null !== $this->get_attribute( 'face' ) ||
     4113                                null !== $this->get_attribute( 'size' )
     4114                        )
     4115                ) {
     4116                        $op = '+FONT with attributes';
     4117                }
     4118
     4119                switch ( $op ) {
     4120                        case '#text':
     4121                                /*
     4122                                 * > A character token that is U+0000 NULL
     4123                                 *
     4124                                 * This is handled by `get_modifiable_text()`.
     4125                                 */
     4126
     4127                                /*
     4128                                 * Whitespace-only text does not affect the frameset-ok flag.
     4129                                 * It is probably inter-element whitespace, but it may also
     4130                                 * contain character references which decode only to whitespace.
     4131                                 */
     4132                                $text = $this->get_modifiable_text();
     4133                                if ( strlen( $text ) !== strspn( $text, " \t\n\f\r" ) ) {
     4134                                        $this->state->frameset_ok = false;
     4135                                }
     4136
     4137                                $this->insert_foreign_element( $this->state->current_token, false );
     4138                                return true;
     4139
     4140                        /*
     4141                         * > A comment token
     4142                         */
     4143                        case '#cdata-section':
     4144                        case '#comment':
     4145                        case '#funky_comment':
     4146                                $this->insert_foreign_element( $this->state->current_token, false );
     4147                                return true;
     4148
     4149                        /*
     4150                         * > A DOCTYPE token
     4151                         */
     4152                        case 'html':
     4153                                // Parse error: ignore the token.
     4154                                return $this->step();
     4155
     4156                        /*
     4157                         * > A start tag whose tag name is "b", "big", "blockquote", "body", "br", "center",
     4158                         * > "code", "dd", "div", "dl", "dt", "em", "embed", "h1", "h2", "h3", "h4", "h5",
     4159                         * > "h6", "head", "hr", "i", "img", "li", "listing", "menu", "meta", "nobr", "ol",
     4160                         * > "p", "pre", "ruby", "s", "small", "span", "strong", "strike", "sub", "sup",
     4161                         * > "table", "tt", "u", "ul", "var"
     4162                         *
     4163                         * > A start tag whose name is "font", if the token has any attributes named "color", "face", or "size"
     4164                         *
     4165                         * > An end tag whose tag name is "br", "p"
     4166                         *
     4167                         * Closing BR tags are always reported by the Tag Processor as opening tags.
     4168                         */
     4169                        case '+B':
     4170                        case '+BIG':
     4171                        case '+BLOCKQUOTE':
     4172                        case '+BODY':
     4173                        case '+BR':
     4174                        case '+CENTER':
     4175                        case '+CODE':
     4176                        case '+DD':
     4177                        case '+DIV':
     4178                        case '+DL':
     4179                        case '+DT':
     4180                        case '+EM':
     4181                        case '+EMBED':
     4182                        case '+H1':
     4183                        case '+H2':
     4184                        case '+H3':
     4185                        case '+H4':
     4186                        case '+H5':
     4187                        case '+H6':
     4188                        case '+HEAD':
     4189                        case '+HR':
     4190                        case '+I':
     4191                        case '+IMG':
     4192                        case '+LI':
     4193                        case '+LISTING':
     4194                        case '+MENU':
     4195                        case '+META':
     4196                        case '+NOBR':
     4197                        case '+OL':
     4198                        case '+P':
     4199                        case '+PRE':
     4200                        case '+RUBY':
     4201                        case '+S':
     4202                        case '+SMALL':
     4203                        case '+SPAN':
     4204                        case '+STRONG':
     4205                        case '+STRIKE':
     4206                        case '+SUB':
     4207                        case '+SUP':
     4208                        case '+TABLE':
     4209                        case '+TT':
     4210                        case '+U':
     4211                        case '+UL':
     4212                        case '+VAR':
     4213                        case '+FONT with attributes':
     4214                        case '-BR':
     4215                        case '-P':
     4216                                // @todo Indicate a parse error once it's possible.
     4217                                foreach ( $this->state->stack_of_open_elements->walk_up() as $current_node ) {
     4218                                        if (
     4219                                                'math' === $current_node->integration_node_type ||
     4220                                                'html' === $current_node->integration_node_type ||
     4221                                                'html' === $current_node->namespace
     4222                                        ) {
     4223                                                break;
     4224                                        }
     4225
     4226                                        $this->state->stack_of_open_elements->pop();
     4227                                }
     4228                                return $this->step( self::REPROCESS_CURRENT_NODE );
     4229                }
     4230
     4231                /*
     4232                 * > Any other start tag
     4233                 */
     4234                if ( ! $this->is_tag_closer() ) {
     4235                        $this->insert_foreign_element( $this->state->current_token, false );
     4236
     4237                        /*
     4238                         * > If the token has its self-closing flag set, then run
     4239                         * > the appropriate steps from the following list:
     4240                         */
     4241                        if ( $this->state->current_token->has_self_closing_flag ) {
     4242                                if ( 'SCRIPT' === $this->state->current_token->node_name && 'svg' === $this->state->current_token->namespace ) {
     4243                                        /*
     4244                                         * > Acknowledge the token's self-closing flag, and then act as
     4245                                         * > described in the steps for a "script" end tag below.
     4246                                         *
     4247                                         * @todo Verify that this shouldn't be handled by the rule for
     4248                                         *       "An end tag whose name is 'script', if the current node
     4249                                         *       is an SVG script element."
     4250                                         */
     4251                                        goto in_foreign_content_any_other_end_tag;
     4252                                } else {
     4253                                        $this->state->stack_of_open_elements->pop();
     4254                                }
     4255                        }
     4256                        return true;
     4257                }
     4258
     4259                /*
     4260                 * > An end tag whose name is "script", if the current node is an SVG script element.
     4261                 */
     4262                if ( $this->is_tag_closer() && 'SCRIPT' === $this->state->current_token->node_name && 'svg' === $this->state->current_token->namespace ) {
     4263                        $this->state->stack_of_open_elements->pop();
     4264                }
     4265
     4266                /*
     4267                 * > Any other end tag
     4268                 */
     4269                if ( $this->is_tag_closer() ) {
     4270                        in_foreign_content_any_other_end_tag:
     4271                        $node = $this->state->stack_of_open_elements->current_node();
     4272                        if ( $tag_name !== $node->node_name ) {
     4273                                // @todo Indicate a parse error once it's possible.
     4274                        }
     4275                        in_foreign_content_end_tag_loop:
     4276                        if ( $node === $this->state->stack_of_open_elements->at( 1 ) ) {
     4277                                return true;
     4278                        }
     4279
     4280                        /*
     4281                         * > If node's tag name, converted to ASCII lowercase, is the same as the tag name
     4282                         * > of the token, pop elements from the stack of open elements until node has
     4283                         * > been popped from the stack, and then return.
     4284                         */
     4285                        if ( 0 === strcasecmp( $node->node_name, $tag_name ) ) {
     4286                                foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
     4287                                        $this->state->stack_of_open_elements->pop();
     4288                                        if ( $node === $item ) {
     4289                                                return true;
     4290                                        }
     4291                                }
     4292                        }
     4293
     4294                        foreach ( $this->state->stack_of_open_elements->walk_up( $node ) as $item ) {
     4295                                $node = $item;
     4296                                break;
     4297                        }
     4298
     4299                        if ( 'html' !== $node->namespace ) {
     4300                                goto in_foreign_content_end_tag_loop;
     4301                        }
     4302
     4303                        switch ( $this->state->insertion_mode ) {
     4304                                case WP_HTML_Processor_State::INSERTION_MODE_INITIAL:
     4305                                        return $this->step_initial();
     4306
     4307                                case WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HTML:
     4308                                        return $this->step_before_html();
     4309
     4310                                case WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD:
     4311                                        return $this->step_before_head();
     4312
     4313                                case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD:
     4314                                        return $this->step_in_head();
     4315
     4316                                case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD_NOSCRIPT:
     4317                                        return $this->step_in_head_noscript();
     4318
     4319                                case WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD:
     4320                                        return $this->step_after_head();
     4321
     4322                                case WP_HTML_Processor_State::INSERTION_MODE_IN_BODY:
     4323                                        return $this->step_in_body();
     4324
     4325                                case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE:
     4326                                        return $this->step_in_table();
     4327
     4328                                case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_TEXT:
     4329                                        return $this->step_in_table_text();
     4330
     4331                                case WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION:
     4332                                        return $this->step_in_caption();
     4333
     4334                                case WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP:
     4335                                        return $this->step_in_column_group();
     4336
     4337                                case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY:
     4338                                        return $this->step_in_table_body();
     4339
     4340                                case WP_HTML_Processor_State::INSERTION_MODE_IN_ROW:
     4341                                        return $this->step_in_row();
     4342
     4343                                case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL:
     4344                                        return $this->step_in_cell();
     4345
     4346                                case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT:
     4347                                        return $this->step_in_select();
     4348
     4349                                case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE:
     4350                                        return $this->step_in_select_in_table();
     4351
     4352                                case WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE:
     4353                                        return $this->step_in_template();
     4354
     4355                                case WP_HTML_Processor_State::INSERTION_MODE_AFTER_BODY:
     4356                                        return $this->step_after_body();
     4357
     4358                                case WP_HTML_Processor_State::INSERTION_MODE_IN_FRAMESET:
     4359                                        return $this->step_in_frameset();
     4360
     4361                                case WP_HTML_Processor_State::INSERTION_MODE_AFTER_FRAMESET:
     4362                                        return $this->step_after_frameset();
     4363
     4364                                case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_BODY:
     4365                                        return $this->step_after_after_body();
     4366
     4367                                case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_FRAMESET:
     4368                                        return $this->step_after_after_frameset();
     4369
     4370                                // This should be unreachable but PHP doesn't have total type checking on switch.
     4371                                default:
     4372                                        $this->bail( "Unaware of the requested parsing mode: '{$this->state->insertion_mode}'." );
     4373                        }
     4374                }
    40734375        }
    40744376
     
    40994401         * HTML semantic overrides for Tag Processor
    41004402         */
     4403
     4404        /**
     4405         * Indicates the namespace of the current token, or "html" if there is none.
     4406         *
     4407         * @return string One of "html", "math", or "svg".
     4408         */
     4409        public function get_namespace(): string {
     4410                if ( ! isset( $this->current_element ) ) {
     4411                        return 'html';
     4412                }
     4413
     4414                return $this->current_element->token->namespace;
     4415        }
    41014416
    41024417        /**
     
    47365051
    47375052        /**
     5053         * Returns the adjusted current node.
     5054         *
     5055         * > The adjusted current node is the context element if the parser was created as
     5056         * > part of the HTML fragment parsing algorithm and the stack of open elements
     5057         * > has only one element in it (fragment case); otherwise, the adjusted current
     5058         * > node is the current node.
     5059         *
     5060         * @see https://html.spec.whatwg.org/#adjusted-current-node
     5061         *
     5062         * @since 6.7.0
     5063         *
     5064         * @return WP_HTML_Token|null The adjusted current node.
     5065         */
     5066        private function get_adjusted_current_node(): ?WP_HTML_Token {
     5067                if ( isset( $this->context_node ) && 1 === $this->state->stack_of_open_elements->count() ) {
     5068                        return $this->context_node;
     5069                }
     5070
     5071                return $this->state->stack_of_open_elements->current_node();
     5072        }
     5073
     5074        /**
    47385075         * Reconstructs the active formatting elements.
    47395076         *
     
    50445381                                }
    50455382
    5046                                 if ( self::is_special( $item->node_name ) ) {
     5383                                if ( self::is_special( $item ) ) {
    50475384                                        $furthest_block = $item;
    50485385                                        break;
     
    51135450
    51145451        /**
     5452         * Inserts a foreign element on to the stack of open elements.
     5453         *
     5454         * @since 6.7.0
     5455         *
     5456         * @see https://html.spec.whatwg.org/#insert-a-foreign-element
     5457         *
     5458         * @param WP_HTML_Token $token                     Insert this token. The token's namespace and
     5459         *                                                 insertion point will be updated correctly.
     5460         * @param bool          $only_add_to_element_stack Whether to skip the "insert an element at the adjusted
     5461         *                                                 insertion location" algorithm when adding this element.
     5462         */
     5463        private function insert_foreign_element( WP_HTML_Token $token, bool $only_add_to_element_stack ): void {
     5464                $adjusted_current_node = $this->get_adjusted_current_node();
     5465
     5466                $token->namespace = $adjusted_current_node ? $adjusted_current_node->namespace : 'html';
     5467
     5468                if ( $this->is_mathml_integration_point() ) {
     5469                        $token->integration_node_type = 'math';
     5470                } elseif ( $this->is_html_integration_point() ) {
     5471                        $token->integration_node_type = 'html';
     5472                }
     5473
     5474                if ( false === $only_add_to_element_stack ) {
     5475                        /*
     5476                         * @todo Implement the "appropriate place for inserting a node" and the
     5477                         *       "insert an element at the adjusted insertion location" algorithms.
     5478                         *
     5479                         * These algorithms mostly impacts DOM tree construction and not the HTML API.
     5480                         * Here, there's no DOM node onto which the element will be appended, so the
     5481                         * parser will skip this step.
     5482                         *
     5483                         * @see https://html.spec.whatwg.org/#insert-an-element-at-the-adjusted-insertion-location
     5484                         */
     5485                }
     5486
     5487                $this->insert_html_element( $token );
     5488        }
     5489
     5490        /**
    51155491         * Inserts a virtual element on the stack of open elements.
    51165492         *
     
    51385514
    51395515        /**
     5516         * Indicates if the current token is a MathML integration point.
     5517         *
     5518         * @since 6.7.0
     5519         *
     5520         * @see https://html.spec.whatwg.org/#mathml-text-integration-point
     5521         *
     5522         * @return bool Whether the current token is a MathML integration point.
     5523         */
     5524        private function is_mathml_integration_point(): bool {
     5525                $current_token = $this->state->current_token;
     5526                if ( ! isset( $current_token ) ) {
     5527                        return false;
     5528                }
     5529
     5530                if ( 'math' !== $current_token->namespace || 'M' !== $current_token->node_name[0] ) {
     5531                        return false;
     5532                }
     5533
     5534                $tag_name = $current_token->node_name;
     5535
     5536                return (
     5537                        'MI' === $tag_name ||
     5538                        'MO' === $tag_name ||
     5539                        'MN' === $tag_name ||
     5540                        'MS' === $tag_name ||
     5541                        'MTEXT' === $tag_name
     5542                );
     5543        }
     5544
     5545        /**
     5546         * Indicates if the current token is an HTML integration point.
     5547         *
     5548         * Note that this method must be an instance method with access
     5549         * to the current token, since it needs to examine the attributes
     5550         * of the currently-matched tag, if it's in the MathML namespace.
     5551         * Otherwise it would be required to scan the HTML and ensure that
     5552         * no other accounting is overlooked.
     5553         *
     5554         * @since 6.7.0
     5555         *
     5556         * @see https://html.spec.whatwg.org/#html-integration-point
     5557         *
     5558         * @return bool Whether the current token is an HTML integration point.
     5559         */
     5560        private function is_html_integration_point(): bool {
     5561                $current_token = $this->state->current_token;
     5562                if ( ! isset( $current_token ) ) {
     5563                        return false;
     5564                }
     5565
     5566                if ( 'html' === $current_token->namespace ) {
     5567                        return false;
     5568                }
     5569
     5570                $tag_name = $current_token->node_name;
     5571
     5572                if ( 'svg' === $current_token->namespace ) {
     5573                        return (
     5574                                'DESC' === $tag_name ||
     5575                                'FOREIGNOBJECT' === $tag_name ||
     5576                                'TITLE' === $tag_name
     5577                        );
     5578                }
     5579
     5580                if ( 'math' === $current_token->namespace ) {
     5581                        if ( 'ANNOTATION-XML' !== $tag_name ) {
     5582                                return false;
     5583                        }
     5584
     5585                        $encoding = $this->get_attribute( 'encoding' );
     5586
     5587                        return (
     5588                                is_string( $encoding ) &&
     5589                                (
     5590                                        0 === strcasecmp( $encoding, 'application/xhtml+xml' ) ||
     5591                                        0 === strcasecmp( $encoding, 'text/html' )
     5592                                )
     5593                        );
     5594                }
     5595        }
     5596
     5597        /**
    51405598         * Returns whether an element of a given name is in the HTML special category.
    51415599         *
     
    51445602         * @see https://html.spec.whatwg.org/#special
    51455603         *
    5146          * @param string $tag_name Name of element to check.
     5604         * @param WP_HTML_Token|string $tag_name Node to check, or only its name if in the HTML namespace.
    51475605         * @return bool Whether the element of the given name is in the special category.
    51485606         */
    51495607        public static function is_special( $tag_name ): bool {
    5150                 $tag_name = strtoupper( $tag_name );
     5608                if ( is_string( $tag_name ) ) {
     5609                        $tag_name = strtoupper( $tag_name );
     5610                } else {
     5611                        $tag_name = 'html' === $tag_name->namespace
     5612                                ? strtoupper( $tag_name->node_name )
     5613                                : "{$tag_name->namespace} {$tag_name->node_name}";
     5614                }
    51515615
    51525616                return (
     
    52365700
    52375701                        // MathML.
    5238                         'MI' === $tag_name ||
    5239                         'MO' === $tag_name ||
    5240                         'MN' === $tag_name ||
    5241                         'MS' === $tag_name ||
    5242                         'MTEXT' === $tag_name ||
    5243                         'ANNOTATION-XML' === $tag_name ||
     5702                        'math MI' === $tag_name ||
     5703                        'math MO' === $tag_name ||
     5704                        'math MN' === $tag_name ||
     5705                        'math MS' === $tag_name ||
     5706                        'math MTEXT' === $tag_name ||
     5707                        'math ANNOTATION-XML' === $tag_name ||
    52445708
    52455709                        // SVG.
    5246                         'FOREIGNOBJECT' === $tag_name ||
    5247                         'DESC' === $tag_name ||
    5248                         'TITLE' === $tag_name
     5710                        'svg DESC' === $tag_name ||
     5711                        'svg FOREIGNOBJECT' === $tag_name ||
     5712                        'svg TITLE' === $tag_name
    52495713                );
    52505714        }
  • trunk/src/wp-includes/html-api/class-wp-html-tag-processor.php

    r58866 r58867  
    513513
    514514        /**
     515         * Indicates whether the parser is inside foreign content,
     516         * e.g. inside an SVG or MathML element.
     517         *
     518         * One of 'html', 'svg', or 'math'.
     519         *
     520         * Several parsing rules change based on whether the parser
     521         * is inside foreign content, including whether CDATA sections
     522         * are allowed and whether a self-closing flag indicates that
     523         * an element has no content.
     524         *
     525         * @since 6.7.0
     526         *
     527         * @var string
     528         */
     529        private $parsing_namespace = 'html';
     530
     531        /**
    515532         * What kind of syntax token became an HTML comment.
    516533         *
     
    779796        public function __construct( $html ) {
    780797                $this->html = $html;
     798        }
     799
     800        /**
     801         * Switches parsing mode into a new namespace, such as when
     802         * encountering an SVG tag and entering foreign content.
     803         *
     804         * @since 6.7.0
     805         *
     806         * @param string $new_namespace One of 'html', 'svg', or 'math' indicating into what
     807         *                              namespace the next tokens will be processed.
     808         * @return bool Whether the namespace was valid and changed.
     809         */
     810        public function change_parsing_namespace( string $new_namespace ): bool {
     811                if ( ! in_array( $new_namespace, array( 'html', 'math', 'svg' ), true ) ) {
     812                        return false;
     813                }
     814
     815                $this->parsing_namespace = $new_namespace;
     816                return true;
    781817        }
    782818
     
    844880         *
    845881         * @since 6.5.0
     882         * @since 6.7.0 Recognizes CDATA sections within foreign content.
    846883         *
    847884         * @return bool Whether a token was parsed.
     
    957994                if (
    958995                        $this->is_closing_tag ||
     996                        'html' !== $this->parsing_namespace ||
    959997                        1 !== strspn( $this->html, 'iIlLnNpPsStTxX', $this->tag_name_starts_at, 1 )
    960998                ) {
     
    9971035
    9981036                // Find the closing tag if necessary.
    999                 $found_closer = false;
    10001037                switch ( $tag_name ) {
    10011038                        case 'SCRIPT':
     
    17571794                                        $this->text_length          = $closer_at - $this->text_starts_at;
    17581795                                        $this->bytes_already_parsed = $closer_at + 1;
     1796                                        return true;
     1797                                }
     1798
     1799                                if (
     1800                                        'html' !== $this->parsing_namespace &&
     1801                                        strlen( $html ) > $at + 8 &&
     1802                                        '[' === $html[ $at + 2 ] &&
     1803                                        'C' === $html[ $at + 3 ] &&
     1804                                        'D' === $html[ $at + 4 ] &&
     1805                                        'A' === $html[ $at + 5 ] &&
     1806                                        'T' === $html[ $at + 6 ] &&
     1807                                        'A' === $html[ $at + 7 ] &&
     1808                                        '[' === $html[ $at + 8 ]
     1809                                ) {
     1810                                        $closer_at = strpos( $html, ']]>', $at + 9 );
     1811                                        if ( false === $closer_at ) {
     1812                                                $this->parser_state = self::STATE_INCOMPLETE_INPUT;
     1813
     1814                                                return false;
     1815                                        }
     1816
     1817                                        $this->parser_state         = self::STATE_CDATA_NODE;
     1818                                        $this->text_starts_at       = $at + 9;
     1819                                        $this->text_length          = $closer_at - $this->text_starts_at;
     1820                                        $this->token_length         = $closer_at + 3 - $this->token_starts_at;
     1821                                        $this->bytes_already_parsed = $closer_at + 3;
    17591822                                        return true;
    17601823                                }
     
    26552718
    26562719        /**
     2720         * Returns the namespace of the matched token.
     2721         *
     2722         * @since 6.7.0
     2723         *
     2724         * @return string One of 'html', 'math', or 'svg'.
     2725         */
     2726        public function get_namespace(): string {
     2727                return $this->parsing_namespace;
     2728        }
     2729
     2730        /**
    26572731         * Returns the uppercase name of the matched tag.
    26582732         *
     
    26892763
    26902764                return null;
     2765        }
     2766
     2767        /**
     2768         * Returns the adjusted tag name for a given token, taking into
     2769         * account the current parsing context, whether HTML, SVG, or MathML.
     2770         *
     2771         * @since 6.7.0
     2772         *
     2773         * @return string|null Name of current tag name.
     2774         */
     2775        public function get_qualified_tag_name(): ?string {
     2776                $tag_name = $this->get_tag();
     2777                if ( null === $tag_name ) {
     2778                        return null;
     2779                }
     2780
     2781                if ( 'html' === $this->get_namespace() ) {
     2782                        return $tag_name;
     2783                }
     2784
     2785                $lower_tag_name = strtolower( $tag_name );
     2786                if ( 'math' === $this->get_namespace() ) {
     2787                        return $lower_tag_name;
     2788                }
     2789
     2790                if ( 'svg' === $this->get_namespace() ) {
     2791                        switch ( $lower_tag_name ) {
     2792                                case 'altglyph':
     2793                                        return 'altGlyph';
     2794
     2795                                case 'altglyphdef':
     2796                                        return 'altGlyphDef';
     2797
     2798                                case 'altglyphitem':
     2799                                        return 'altGlyphItem';
     2800
     2801                                case 'animatecolor':
     2802                                        return 'animateColor';
     2803
     2804                                case 'animatemotion':
     2805                                        return 'animateMotion';
     2806
     2807                                case 'animatetransform':
     2808                                        return 'animateTransform';
     2809
     2810                                case 'clippath':
     2811                                        return 'clipPath';
     2812
     2813                                case 'feblend':
     2814                                        return 'feBlend';
     2815
     2816                                case 'fecolormatrix':
     2817                                        return 'feColorMatrix';
     2818
     2819                                case 'fecomponenttransfer':
     2820                                        return 'feComponentTransfer';
     2821
     2822                                case 'fecomposite':
     2823                                        return 'feComposite';
     2824
     2825                                case 'feconvolvematrix':
     2826                                        return 'feConvolveMatrix';
     2827
     2828                                case 'fediffuselighting':
     2829                                        return 'feDiffuseLighting';
     2830
     2831                                case 'fedisplacementmap':
     2832                                        return 'feDisplacementMap';
     2833
     2834                                case 'fedistantlight':
     2835                                        return 'feDistantLight';
     2836
     2837                                case 'fedropshadow':
     2838                                        return 'feDropShadow';
     2839
     2840                                case 'feflood':
     2841                                        return 'feFlood';
     2842
     2843                                case 'fefunca':
     2844                                        return 'feFuncA';
     2845
     2846                                case 'fefuncb':
     2847                                        return 'feFuncB';
     2848
     2849                                case 'fefuncg':
     2850                                        return 'feFuncG';
     2851
     2852                                case 'fefuncr':
     2853                                        return 'feFuncR';
     2854
     2855                                case 'fegaussianblur':
     2856                                        return 'feGaussianBlur';
     2857
     2858                                case 'feimage':
     2859                                        return 'feImage';
     2860
     2861                                case 'femerge':
     2862                                        return 'feMerge';
     2863
     2864                                case 'femergenode':
     2865                                        return 'feMergeNode';
     2866
     2867                                case 'femorphology':
     2868                                        return 'feMorphology';
     2869
     2870                                case 'feoffset':
     2871                                        return 'feOffset';
     2872
     2873                                case 'fepointlight':
     2874                                        return 'fePointLight';
     2875
     2876                                case 'fespecularlighting':
     2877                                        return 'feSpecularLighting';
     2878
     2879                                case 'fespotlight':
     2880                                        return 'feSpotLight';
     2881
     2882                                case 'fetile':
     2883                                        return 'feTile';
     2884
     2885                                case 'feturbulence':
     2886                                        return 'feTurbulence';
     2887
     2888                                case 'foreignobject':
     2889                                        return 'foreignObject';
     2890
     2891                                case 'glyphref':
     2892                                        return 'glyphRef';
     2893
     2894                                case 'lineargradient':
     2895                                        return 'linearGradient';
     2896
     2897                                case 'radialgradient':
     2898                                        return 'radialGradient';
     2899
     2900                                case 'textpath':
     2901                                        return 'textPath';
     2902
     2903                                default:
     2904                                        return $lower_tag_name;
     2905                        }
     2906                }
     2907        }
     2908
     2909        /**
     2910         * Returns the adjusted attribute name for a given attribute, taking into
     2911         * account the current parsing context, whether HTML, SVG, or MathML.
     2912         *
     2913         * @since 6.7.0
     2914         *
     2915         * @param string $attribute_name Which attribute to adjust.
     2916         *
     2917         * @return string|null
     2918         */
     2919        public function get_qualified_attribute_name( $attribute_name ): ?string {
     2920                if ( self::STATE_MATCHED_TAG !== $this->parser_state ) {
     2921                        return null;
     2922                }
     2923
     2924                $namespace = $this->get_namespace();
     2925                $lower_name = strtolower( $attribute_name );
     2926
     2927                if ( 'math' === $namespace && 'definitionurl' === $lower_name ) {
     2928                        return 'definitionURL';
     2929                }
     2930
     2931                if ( 'svg' === $this->get_namespace() ) {
     2932                        switch ( $lower_name ) {
     2933                                case 'attributename':
     2934                                        return 'attributeName';
     2935
     2936                                case 'attributetype':
     2937                                        return 'attributeType';
     2938
     2939                                case 'basefrequency':
     2940                                        return 'baseFrequency';
     2941
     2942                                case 'baseprofile':
     2943                                        return 'baseProfile';
     2944
     2945                                case 'calcmode':
     2946                                        return 'calcMode';
     2947
     2948                                case 'clippathunits':
     2949                                        return 'clipPathUnits';
     2950
     2951                                case 'diffuseconstant':
     2952                                        return 'diffuseConstant';
     2953
     2954                                case 'edgemode':
     2955                                        return 'edgeMode';
     2956
     2957                                case 'filterunits':
     2958                                        return 'filterUnits';
     2959
     2960                                case 'glyphref':
     2961                                        return 'glyphRef';
     2962
     2963                                case 'gradienttransform':
     2964                                        return 'gradientTransform';
     2965
     2966                                case 'gradientunits':
     2967                                        return 'gradientUnits';
     2968
     2969                                case 'kernelmatrix':
     2970                                        return 'kernelMatrix';
     2971
     2972                                case 'kernelunitlength':
     2973                                        return 'kernelUnitLength';
     2974
     2975                                case 'keypoints':
     2976                                        return 'keyPoints';
     2977
     2978                                case 'keysplines':
     2979                                        return 'keySplines';
     2980
     2981                                case 'keytimes':
     2982                                        return 'keyTimes';
     2983
     2984                                case 'lengthadjust':
     2985                                        return 'lengthAdjust';
     2986
     2987                                case 'limitingconeangle':
     2988                                        return 'limitingConeAngle';
     2989
     2990                                case 'markerheight':
     2991                                        return 'markerHeight';
     2992
     2993                                case 'markerunits':
     2994                                        return 'markerUnits';
     2995
     2996                                case 'markerwidth':
     2997                                        return 'markerWidth';
     2998
     2999                                case 'maskcontentunits':
     3000                                        return 'maskContentUnits';
     3001
     3002                                case 'maskunits':
     3003                                        return 'maskUnits';
     3004
     3005                                case 'numoctaves':
     3006                                        return 'numOctaves';
     3007
     3008                                case 'pathlength':
     3009                                        return 'pathLength';
     3010
     3011                                case 'patterncontentunits':
     3012                                        return 'patternContentUnits';
     3013
     3014                                case 'patterntransform':
     3015                                        return 'patternTransform';
     3016
     3017                                case 'patternunits':
     3018                                        return 'patternUnits';
     3019
     3020                                case 'pointsatx':
     3021                                        return 'pointsAtX';
     3022
     3023                                case 'pointsaty':
     3024                                        return 'pointsAtY';
     3025
     3026                                case 'pointsatz':
     3027                                        return 'pointsAtZ';
     3028
     3029                                case 'preservealpha':
     3030                                        return 'preserveAlpha';
     3031
     3032                                case 'preserveaspectratio':
     3033                                        return 'preserveAspectRatio';
     3034
     3035                                case 'primitiveunits':
     3036                                        return 'primitiveUnits';
     3037
     3038                                case 'refx':
     3039                                        return 'refX';
     3040
     3041                                case 'refy':
     3042                                        return 'refY';
     3043
     3044                                case 'repeatcount':
     3045                                        return 'repeatCount';
     3046
     3047                                case 'repeatdur':
     3048                                        return 'repeatDur';
     3049
     3050                                case 'requiredextensions':
     3051                                        return 'requiredExtensions';
     3052
     3053                                case 'requiredfeatures':
     3054                                        return 'requiredFeatures';
     3055
     3056                                case 'specularconstant':
     3057                                        return 'specularConstant';
     3058
     3059                                case 'specularexponent':
     3060                                        return 'specularExponent';
     3061
     3062                                case 'spreadmethod':
     3063                                        return 'spreadMethod';
     3064
     3065                                case 'startoffset':
     3066                                        return 'startOffset';
     3067
     3068                                case 'stddeviation':
     3069                                        return 'stdDeviation';
     3070
     3071                                case 'stitchtiles':
     3072                                        return 'stitchTiles';
     3073
     3074                                case 'surfacescale':
     3075                                        return 'surfaceScale';
     3076
     3077                                case 'systemlanguage':
     3078                                        return 'systemLanguage';
     3079
     3080                                case 'tablevalues':
     3081                                        return 'tableValues';
     3082
     3083                                case 'targetx':
     3084                                        return 'targetX';
     3085
     3086                                case 'targety':
     3087                                        return 'targetY';
     3088
     3089                                case 'textlength':
     3090                                        return 'textLength';
     3091
     3092                                case 'viewbox':
     3093                                        return 'viewBox';
     3094
     3095                                case 'viewtarget':
     3096                                        return 'viewTarget';
     3097
     3098                                case 'xchannelselector':
     3099                                        return 'xChannelSelector';
     3100
     3101                                case 'ychannelselector':
     3102                                        return 'yChannelSelector';
     3103
     3104                                case 'zoomandpan':
     3105                                        return 'zoomAndPan';
     3106                        }
     3107                }
     3108
     3109                if ( 'html' !== $namespace ) {
     3110                        switch ( $lower_name ) {
     3111                                case 'xlink:actuate':
     3112                                        return 'xlink actuate';
     3113
     3114                                case 'xlink:arcrole':
     3115                                        return 'xlink arcrole';
     3116
     3117                                case 'xlink:href':
     3118                                        return 'xlink href';
     3119
     3120                                case 'xlink:role':
     3121                                        return 'xlink role';
     3122
     3123                                case 'xlink:show':
     3124                                        return 'xlink show';
     3125
     3126                                case 'xlink:title':
     3127                                        return 'xlink title';
     3128
     3129                                case 'xlink:type':
     3130                                        return 'xlink type';
     3131
     3132                                case 'xml:lang':
     3133                                        return 'xml lang';
     3134
     3135                                case 'xml:space':
     3136                                        return 'xml space';
     3137
     3138                                case 'xmlns':
     3139                                        return 'xmlns';
     3140
     3141                                case 'xmlns:xlink':
     3142                                        return 'xmlns xlink';
     3143                        }
     3144                }
     3145
     3146                return $attribute_name;
    26913147        }
    26923148
     
    29643420                 * for security reasons (to avoid joining together strings that were safe
    29653421                 * when separated, but not when joined).
     3422                 *
     3423                 * @todo Inside HTML integration points and MathML integration points, the
     3424                 *       text is processed according to the insertion mode, not according
     3425                 *       to the foreign content rules. This should strip the NULL bytes.
    29663426                 */
    2967                 return '#text' === $tag_name
     3427                return ( '#text' === $tag_name && 'html' === $this->get_namespace() )
    29683428                        ? str_replace( "\x00", '', $decoded )
    29693429                        : str_replace( "\x00", "\u{FFFD}", $decoded );
  • trunk/src/wp-includes/html-api/class-wp-html-token.php

    r58779 r58867  
    6262
    6363        /**
     64         * Indicates if the element is an HTML element or if it's inside foreign content.
     65         *
     66         * @since 6.7.0
     67         *
     68         * @var string 'html', 'svg', or 'math'.
     69         */
     70        public $namespace = 'html';
     71
     72        /**
     73         * Indicates which kind of integration point the element is, if any.
     74         *
     75         * @since 6.7.0
     76         *
     77         * @var string|null 'math', 'html', or null if not an integration point.
     78         */
     79        public $integration_node_type = null;
     80
     81        /**
    6482         * Called when token is garbage-collected or otherwise destroyed.
    6583         *
     
    8199        public function __construct( ?string $bookmark_name, string $node_name, bool $has_self_closing_flag, ?callable $on_destroy = null ) {
    82100                $this->bookmark_name         = $bookmark_name;
     101                $this->namespace             = 'html';
    83102                $this->node_name             = $node_name;
    84103                $this->has_self_closing_flag = $has_self_closing_flag;
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php

    r58828 r58867  
    360360
    361361        /**
    362          * Ensures that special handling of unsupported tags is cleaned up
    363          * as handling is implemented. Otherwise there's risk of leaving special
    364          * handling (that is never reached) when tag handling is implemented.
    365          *
    366          * @ticket 60092
    367          *
    368          * @dataProvider data_unsupported_special_in_body_tags
    369          *
    370          * @covers WP_HTML_Processor::step_in_body
    371          *
    372          * @param string $tag_name Name of the tag to test.
    373          */
    374         public function test_step_in_body_fails_on_unsupported_tags( $tag_name ) {
    375                 $fragment = WP_HTML_Processor::create_fragment( '<' . $tag_name . '></' . $tag_name . '>' );
    376                 $this->assertFalse( $fragment->next_tag(), 'Should fail to find tag: ' . $tag_name . '.' );
    377                 $this->assertEquals( $fragment->get_last_error(), WP_HTML_Processor::ERROR_UNSUPPORTED, 'Should have unsupported last error.' );
    378         }
    379 
    380         /**
    381          * Data provider.
    382          *
    383          * @return array[]
    384          */
    385         public static function data_unsupported_special_in_body_tags() {
    386                 return array(
    387                         'MATH' => array( 'MATH' ),
    388                         'SVG'  => array( 'SVG' ),
    389                 );
    390         }
    391 
    392         /**
    393362         * Ensures that the HTML Processor properly reports the depth of a given element.
    394363         *
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php

    r58836 r58867  
    166166
    167167        /**
    168          * Ensures that no new HTML elements are accidentally partially-supported.
    169          *
    170          * When introducing support for new HTML elements, there are multiple places
    171          * in the HTML Processor that need to be updated, until the time that the class
    172          * has full HTML5 support. Because of this, these tests lock down the interface
    173          * to ensure that support isn't accidentally updated in one place for a new
    174          * element while overlooked in another.
    175          *
    176          * @ticket 58517
    177          *
    178          * @covers WP_HTML_Processor::step
    179          *
    180          * @dataProvider data_unsupported_elements
    181          *
    182          * @param string $html HTML string containing unsupported elements.
    183          */
    184         public function test_fails_when_encountering_unsupported_tag( $html ) {
    185                 $processor = WP_HTML_Processor::create_fragment( $html );
    186 
    187                 $this->assertFalse( $processor->step(), "Should not have stepped into unsupported {$processor->get_tag()} element." );
    188         }
    189 
    190         /**
    191          * Data provider.
    192          *
    193          * @return array[]
    194          */
    195         public static function data_unsupported_elements() {
    196                 $unsupported_elements = array(
    197                         'MATH',
    198                         'PLAINTEXT', // Neutralized.
    199                         'SVG',
    200                 );
    201 
    202                 $data = array();
    203                 foreach ( $unsupported_elements as $tag_name ) {
    204                         $data[ $tag_name ] = array( "<{$tag_name}>" );
    205                 }
    206 
    207                 return $data;
    208         }
    209 
    210         /**
    211168         * @ticket 58517
    212169         *
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php

    r58859 r58867  
    182182
    183183                        if ( $was_text && '#text' !== $token_name ) {
    184                                 $output   .= "{$text_node}\"\n";
     184                                if ( '' !== $text_node ) {
     185                                        $output .= "{$text_node}\"\n";
     186                                }
    185187                                $was_text  = false;
    186188                                $text_node = '';
     
    189191                        switch ( $token_type ) {
    190192                                case '#tag':
    191                                         $tag_name = strtolower( $token_name );
     193                                        $namespace = $processor->get_namespace();
     194                                        $tag_name  = 'html' === $namespace
     195                                                ? strtolower( $processor->get_tag() )
     196                                                : "{$namespace} {$processor->get_qualified_tag_name()}";
    192197
    193198                                        if ( $is_closer ) {
    194199                                                --$indent_level;
    195200
    196                                                 if ( 'TEMPLATE' === $token_name ) {
     201                                                if ( 'html' === $namespace && 'TEMPLATE' === $token_name ) {
    197202                                                        --$indent_level;
    198203                                                }
     
    203208                                        $tag_indent = $indent_level;
    204209
    205                                         if ( ! WP_HTML_Processor::is_void( $tag_name ) ) {
     210                                        if ( 'html' !== $namespace ) {
     211                                                if ( ! $processor->has_self_closing_flag() ) {
     212                                                        ++$indent_level;
     213                                                }
     214                                        } elseif ( ! WP_HTML_Processor::is_void( $tag_name ) ) {
    206215                                                ++$indent_level;
    207216                                        }
     
    211220                                        $attribute_names = $processor->get_attribute_names_with_prefix( '' );
    212221                                        if ( $attribute_names ) {
    213                                                 sort( $attribute_names, SORT_STRING );
    214 
     222                                                $sorted_attributes = array();
    215223                                                foreach ( $attribute_names as $attribute_name ) {
     224                                                        $sorted_attributes[ $attribute_name ] = $processor->get_qualified_attribute_name( $attribute_name );
     225                                                }
     226
     227                                                /*
     228                                                 * Sorts attributes to match html5lib sort order.
     229                                                 *
     230                                                 *  - First comes normal HTML attributes.
     231                                                 *  - Then come adjusted foreign attributes; these have spaces in their names.
     232                                                 *  - Finally come non-adjusted foreign attributes; these have a colon in their names.
     233                                                 *
     234                                                 * Example:
     235                                                 *
     236                                                 *       From: <math xlink:author definitionurl xlink:title xlink:show>
     237                                                 *     Sorted: 'definitionURL', 'xlink show', 'xlink title', 'xlink:author'
     238                                                 */
     239                                                uasort(
     240                                                        $sorted_attributes,
     241                                                        static function ( $a, $b ) {
     242                                                                $a_has_ns = str_contains( $a, ':' );
     243                                                                $b_has_ns = str_contains( $b, ':' );
     244
     245                                                                // Attributes with `:` should follow all other attributes.
     246                                                                if ( $a_has_ns !== $b_has_ns ) {
     247                                                                        return $a_has_ns ? 1 : -1;
     248                                                                }
     249
     250                                                                $a_has_sp = str_contains( $a, ' ' );
     251                                                                $b_has_sp = str_contains( $b, ' ' );
     252
     253                                                                // Attributes with a namespace ' ' should come after those without.
     254                                                                if ( $a_has_sp !== $b_has_sp ) {
     255                                                                        return $a_has_sp ? 1 : -1;
     256                                                                }
     257
     258                                                                return $a <=> $b;
     259                                                        }
     260                                                );
     261
     262                                                foreach ( $sorted_attributes as $attribute_name => $display_name ) {
    216263                                                        $val = $processor->get_attribute( $attribute_name );
    217264                                                        /*
     
    222269                                                                $val = '';
    223270                                                        }
    224                                                         $output .= str_repeat( $indent, $tag_indent + 1 ) . "{$attribute_name}=\"{$val}\"\n";
     271                                                        $output .= str_repeat( $indent, $tag_indent + 1 ) . "{$display_name}=\"{$val}\"\n";
    225272                                                }
    226273                                        }
     
    232279                                        }
    233280
    234                                         if ( 'TEMPLATE' === $token_name ) {
     281                                        if ( 'html' === $namespace && 'TEMPLATE' === $token_name ) {
    235282                                                $output .= str_repeat( $indent, $indent_level ) . "content\n";
    236283                                                ++$indent_level;
     
    243290                                        break;
    244291
     292                                case '#cdata-section':
    245293                                case '#text':
     294                                        $text_content = $processor->get_modifiable_text();
     295                                        if ( '' === $text_content ) {
     296                                                break;
     297                                        }
    246298                                        $was_text = true;
    247299                                        if ( '' === $text_node ) {
    248300                                                $text_node .= str_repeat( $indent, $indent_level ) . '"';
    249301                                        }
    250                                         $text_node .= $processor->get_modifiable_text();
     302                                        $text_node .= $text_content;
    251303                                        break;
    252304
  • trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php

    r58779 r58867  
    1 <?php
    2 /**
    3  * Unit tests for the HTML API indicating that changes are needed to the
    4  * WP_HTML_Open_Elements class before specific features are added to the API.
    5  *
    6  * Note! Duplication of test cases and the helper function in this file are intentional.
    7  * This test file exists to warn developers of related areas of code that need to update
    8  * together when adding support for new elements to the HTML Processor. For example,
    9  * when adding support for the BUTTON element it's necessary to update multiple methods
    10  * in the class governing the stack of open elements as well as the HTML Processor class
    11  * itself. This is because each element might bring with it semantic rules that impact
    12  * the way the document should be parsed. BUTTON creates a kind of boundary in the
    13  * DOM tree and implicitly closes existing open BUTTON elements.
    14  *
    15  * Without these tests a developer needs to investigate all possible places they
    16  * might need to update when adding support for more elements and risks overlooking
    17  * important parts that, in the absence of the related support, will lead to errors.
    18  *
    19  * @package WordPress
    20  * @subpackage HTML-API
    21  *
    22  * @since 6.4.0
    23  *
    24  * @group html-api
    25  *
    26  * @coversDefaultClass WP_HTML_Processor
    27  */
    28 class Tests_HtmlApi_WpHtmlSupportRequiredOpenElements extends WP_UnitTestCase {
    29         /**
    30          * Fails to assert if the HTML Processor handles the given tag.
    31          *
    32          * This test helper is used throughout this test file for one purpose only: to
    33          * fail a test if the HTML Processor handles the given tag. In other words, it
    34          * ensures that the HTML Processor aborts when encountering the given tag.
    35          *
    36          * This is used to ensure that when support for a new tag is added to the
    37          * HTML Processor it receives full support and not partial support, which
    38          * could lead to a variety of issues.
    39          *
    40          * Do not remove this helper function as it provides semantic meaning to the
    41          * assertions in the tests in this file and its behavior is incredibly specific
    42          * and limited and doesn't warrant adding a new abstraction into WP_UnitTestCase.
    43          *
    44          * @param string $tag_name the HTML Processor should abort when encountering this tag, e.g. "BUTTON".
    45          */
    46         private function ensure_support_is_added_everywhere( $tag_name ) {
    47                 $processor = WP_HTML_Processor::create_fragment( "<$tag_name>" );
    48 
    49                 $this->assertFalse( $processor->step(), "Must support terminating elements in specific scope check before adding support for the {$tag_name} element." );
    50         }
    51 
    52         /**
    53          * The check for whether an element is in a scope depends on
    54          * looking for a number of terminating elements in the stack of open
    55          * elements. Until the listed elements are supported in the HTML
    56          * processor, there are no terminating elements and there's no
    57          * point in taking the time to look for them.
    58          *
    59          * @since 6.4.0
    60          *
    61          * @ticket 58517
    62          */
    63         public function test_has_element_in_scope_needs_support() {
    64                 // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML.
    65                 $this->ensure_support_is_added_everywhere( 'MATH' );
    66 
    67                 /*
    68                  * SVG elements: note that TITLE is both an HTML element and an SVG element
    69                  * so care must be taken when adding support for either one.
    70                  *
    71                  * FOREIGNOBJECT, DESC, TITLE.
    72                  */
    73                 $this->ensure_support_is_added_everywhere( 'SVG' );
    74         }
    75 
    76         /**
    77          * The check for whether an element is in list item scope depends on
    78          * the elements for any scope, plus UL and OL.
    79          *
    80          * The method for asserting list item scope doesn't currently exist
    81          * because the LI element isn't yet supported and the LI element is
    82          * the only element that needs to know about list item scope.
    83          *
    84          * @since 6.4.0
    85          *
    86          * @ticket 58517
    87          *
    88          * @covers WP_HTML_Open_Elements::has_element_in_list_item_scope
    89          */
    90         public function test_has_element_in_list_item_scope_needs_support() {
    91                 // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML.
    92                 $this->ensure_support_is_added_everywhere( 'MATH' );
    93 
    94                 /*
    95                  * SVG elements: note that TITLE is both an HTML element and an SVG element
    96                  * so care must be taken when adding support for either one.
    97                  *
    98                  * FOREIGNOBJECT, DESC, TITLE.
    99                  */
    100                 $this->ensure_support_is_added_everywhere( 'SVG' );
    101         }
    102 
    103         /**
    104          * The check for whether an element is in BUTTON scope depends on
    105          * the elements for any scope, plus BUTTON.
    106          *
    107          * @since 6.4.0
    108          *
    109          * @ticket 58517
    110          *
    111          * @covers WP_HTML_Open_Elements::has_element_in_button_scope
    112          */
    113         public function test_has_element_in_button_scope_needs_support() {
    114                 // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML.
    115                 $this->ensure_support_is_added_everywhere( 'MATH' );
    116 
    117                 /*
    118                  * SVG elements: note that TITLE is both an HTML element and an SVG element
    119                  * so care must be taken when adding support for either one.
    120                  *
    121                  * FOREIGNOBJECT, DESC, TITLE.
    122                  */
    123                 $this->ensure_support_is_added_everywhere( 'SVG' );
    124         }
    125 
    126         /**
    127          * The optimization maintaining a flag for "P is in BUTTON scope" requires
    128          * updating that flag every time an element is popped from the stack of
    129          * open elements.
    130          *
    131          * @since 6.4.0
    132          *
    133          * @ticket 58517
    134          *
    135          * @covers WP_HTML_Open_Elements::after_element_pop
    136          */
    137         public function test_after_element_pop_must_maintain_p_in_button_scope_flag() {
    138                 // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML.
    139                 $this->ensure_support_is_added_everywhere( 'MATH' );
    140 
    141                 /*
    142                  * SVG elements: note that TITLE is both an HTML element and an SVG element
    143                  * so care must be taken when adding support for either one.
    144                  *
    145                  * FOREIGNOBJECT, DESC, TITLE.
    146                  */
    147                 $this->ensure_support_is_added_everywhere( 'SVG' );
    148         }
    149 
    150         /**
    151          * The optimization maintaining a flag for "P is in BUTTON scope" requires
    152          * updating that flag every time an element is pushed onto the stack of
    153          * open elements.
    154          *
    155          * @since 6.4.0
    156          *
    157          * @ticket 58517
    158          *
    159          * @covers WP_HTML_Open_Elements::after_element_push
    160          */
    161         public function test_after_element_push_must_maintain_p_in_button_scope_flag() {
    162                 // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML.
    163                 $this->ensure_support_is_added_everywhere( 'MATH' );
    164 
    165                 /*
    166                  * SVG elements: note that TITLE is both an HTML element and an SVG element
    167                  * so care must be taken when adding support for either one.
    168                  *
    169                  * FOREIGNOBJECT, DESC, TITLE.
    170                  */
    171                 $this->ensure_support_is_added_everywhere( 'SVG' );
    172         }
    173 
    174         /**
    175          * The check for whether an element is in TABLE scope depends on
    176          * the HTML, TABLE, and TEMPLATE elements.
    177          *
    178          * @since 6.4.0
    179          *
    180          * @ticket 58517
    181          *
    182          * @covers WP_HTML_Open_Elements::has_element_in_table_scope
    183          */
    184         public function test_has_element_in_table_scope_needs_support() {
    185                 // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML.
    186                 $this->ensure_support_is_added_everywhere( 'MATH' );
    187 
    188                 /*
    189                  * SVG elements: note that TITLE is both an HTML element and an SVG element
    190                  * so care must be taken when adding support for either one.
    191                  *
    192                  * FOREIGNOBJECT, DESC, TITLE.
    193                  */
    194                 $this->ensure_support_is_added_everywhere( 'SVG' );
    195         }
    196 
    197         /**
    198          * The check for whether an element is in SELECT scope depends on
    199          * the OPTGROUP and OPTION elements.
    200          *
    201          * @since 6.4.0
    202          *
    203          * @ticket 58517
    204          *
    205          * @covers WP_HTML_Open_Elements::has_element_in_select_scope
    206          */
    207         public function test_has_element_in_select_scope_needs_support() {
    208                 // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML.
    209                 $this->ensure_support_is_added_everywhere( 'MATH' );
    210 
    211                 /*
    212                  * SVG elements: note that TITLE is both an HTML element and an SVG element
    213                  * so care must be taken when adding support for either one.
    214                  *
    215                  * FOREIGNOBJECT, DESC, TITLE.
    216                  */
    217                 $this->ensure_support_is_added_everywhere( 'SVG' );
    218         }
    219 }
  • trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor-token-scanning.php

    r58779 r58867  
    514514
    515515        /**
     516         * Ensures that basic CDATA sections inside foreign content are detected.
     517         *
     518         * @ticket 61576
     519         */
     520        public function test_basic_cdata_in_foreign_content() {
     521                $processor = new WP_HTML_Tag_Processor( '<svg><![CDATA[this is >&gt; real CDATA]]></svg>' );
     522                $processor->next_token();
     523
     524                // Artificially change namespace; this should be done in the HTML Processor.
     525                $processor->change_parsing_namespace( 'svg' );
     526                $processor->next_token();
     527
     528                $this->assertSame(
     529                        '#cdata-section',
     530                        $processor->get_token_name(),
     531                        "Should have found a CDATA section but found {$processor->get_token_name()} instead."
     532                );
     533
     534                $this->assertNull(
     535                        $processor->get_tag(),
     536                        'Should not have been able to query tag name on non-element token.'
     537                );
     538
     539                $this->assertNull(
     540                        $processor->get_attribute( 'type' ),
     541                        'Should not have been able to query attributes on non-element token.'
     542                );
     543
     544                $this->assertSame(
     545                        'this is >&gt; real CDATA',
     546                        $processor->get_modifiable_text(),
     547                        'Found incorrect modifiable text.'
     548                );
     549        }
     550
     551        /**
     552         * Ensures that empty CDATA sections inside foreign content are detected.
     553         *
     554         * @ticket 61576
     555         */
     556        public function test_empty_cdata_in_foreign_content() {
     557                $processor = new WP_HTML_Tag_Processor( '<svg><![CDATA[]]></svg>' );
     558                $processor->next_token();
     559
     560                // Artificially change namespace; this should be done in the HTML Processor.
     561                $processor->change_parsing_namespace( 'svg' );
     562                $processor->next_token();
     563
     564                $this->assertSame(
     565                        '#cdata-section',
     566                        $processor->get_token_name(),
     567                        "Should have found a CDATA section but found {$processor->get_token_name()} instead."
     568                );
     569
     570                $this->assertEmpty(
     571                        $processor->get_modifiable_text(),
     572                        'Found non-empty modifiable text.'
     573                );
     574        }
     575
     576        /**
    516577         * Ensures that normative Processing Instruction nodes are properly parsed.
    517578         *
Note: See TracChangeset for help on using the changeset viewer.