Make WordPress Core

Changeset 58833


Ignore:
Timestamp:
07/30/2024 06:44:45 PM (6 months ago)
Author:
dmsnell
Message:

HTML API: Add TEMPLATE and related support in HTML Processor.

As part of work to add more spec support to the HTML API, this patch adds
support for the IN TEMPLATE and IN HEAD insertion modes. These changes are
primarily about adding support for TEMPLATE elements in the HTML Processor,
but include support for other tags commonly found in the document head, such
as LINK, META, SCRIPT, STYLE, and TITLE.

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

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

Location:
trunk
Files:
4 edited

Legend:

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

    r58806 r58833  
    309309                'OBJECT',
    310310                'TEMPLATE',
    311                 // @todo: Support SVG and MathML nodes when support for foreign content is added.
     311
     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                 */
    312325            )
    313326        );
     
    350363                'TEMPLATE',
    351364                'UL',
    352                 // @todo: Support SVG and MathML nodes when support for foreign content is added.
     365
     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                 */
    353379            )
    354380        );
     
    387413                'OBJECT',
    388414                'TEMPLATE',
    389                 // @todo: Support SVG and MathML nodes when support for foreign content is added.
     415
     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                 */
    390429            )
    391430        );
  • trunk/src/wp-includes/html-api/class-wp-html-processor.php

    r58828 r58833  
    10411041     * logic for the generalized WP_HTML_Processor::step() function.
    10421042     *
    1043      * @since 6.7.0 Stub implementation.
     1043     * @since 6.7.0
    10441044     *
    10451045     * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
     
    10511051     */
    10521052    private function step_in_head(): bool {
    1053         $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD . ' state.' );
     1053        $token_name = $this->get_token_name();
     1054        $token_type = $this->get_token_type();
     1055        $is_closer  = parent::is_tag_closer();
     1056        $op_sigil   = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : '';
     1057        $op         = "{$op_sigil}{$token_name}";
     1058
     1059        /*
     1060         * > A character token that is one of U+0009 CHARACTER TABULATION,
     1061         * > U+000A LINE FEED (LF), U+000C FORM FEED (FF),
     1062         * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE
     1063         */
     1064        if ( '#text' === $op ) {
     1065            $text = $this->get_modifiable_text();
     1066            if ( '' === $text ) {
     1067                /*
     1068                 * If the text is empty after processing HTML entities and stripping
     1069                 * U+0000 NULL bytes then ignore the token.
     1070                 */
     1071                return $this->step();
     1072            }
     1073
     1074            if ( strlen( $text ) === strspn( $text, " \t\n\f\r" ) ) {
     1075                // Insert the character.
     1076                $this->insert_html_element( $this->state->current_token );
     1077                return true;
     1078            }
     1079        }
     1080
     1081        switch ( $op ) {
     1082            /*
     1083             * > A comment token
     1084             */
     1085            case '#comment':
     1086            case '#funky-comment':
     1087            case '#presumptuous-tag':
     1088                $this->insert_html_element( $this->state->current_token );
     1089                return true;
     1090
     1091            /*
     1092             * > A DOCTYPE token
     1093             */
     1094            case 'html':
     1095                // Parse error: ignore the token.
     1096                return $this->step();
     1097
     1098            /*
     1099             * > A start tag whose tag name is "html"
     1100             */
     1101            case '+HTML':
     1102                return $this->step_in_body();
     1103
     1104            /*
     1105             * > A start tag whose tag name is one of: "base", "basefont", "bgsound", "link"
     1106             */
     1107            case '+BASE':
     1108            case '+BASEFONT':
     1109            case '+BGSOUND':
     1110            case '+LINK':
     1111                $this->insert_html_element( $this->state->current_token );
     1112                return true;
     1113
     1114            /*
     1115             * > A start tag whose tag name is "meta"
     1116             */
     1117            case '+META':
     1118                $this->insert_html_element( $this->state->current_token );
     1119
     1120                /*
     1121                 * > If the active speculative HTML parser is null, then:
     1122                 * >   - If the element has a charset attribute, and getting an encoding from
     1123                 * >     its value results in an encoding, and the confidence is currently
     1124                 * >     tentative, then change the encoding to the resulting encoding.
     1125                 */
     1126                $charset = $this->get_attribute( 'charset' );
     1127                if ( is_string( $charset ) ) {
     1128                    $this->bail( 'Cannot yet process META tags with charset to determine encoding.' );
     1129                }
     1130
     1131                /*
     1132                 * >   - Otherwise, if the element has an http-equiv attribute whose value is
     1133                 * >     an ASCII case-insensitive match for the string "Content-Type", and
     1134                 * >     the element has a content attribute, and applying the algorithm for
     1135                 * >     extracting a character encoding from a meta element to that attribute's
     1136                 * >     value returns an encoding, and the confidence is currently tentative,
     1137                 * >     then change the encoding to the extracted encoding.
     1138                 */
     1139                $http_equiv = $this->get_attribute( 'http-equiv' );
     1140                $content    = $this->get_attribute( 'content' );
     1141                if (
     1142                    is_string( $http_equiv ) &&
     1143                    is_string( $content ) &&
     1144                    0 === strcasecmp( $http_equiv, 'Content-Type' )
     1145                ) {
     1146                    $this->bail( 'Cannot yet process META tags with http-equiv Content-Type to determine encoding.' );
     1147                }
     1148
     1149                return true;
     1150
     1151            /*
     1152             * > A start tag whose tag name is "title"
     1153             */
     1154            case '+TITLE':
     1155                $this->insert_html_element( $this->state->current_token );
     1156                return true;
     1157
     1158            /*
     1159             * > A start tag whose tag name is "noscript", if the scripting flag is enabled
     1160             * > A start tag whose tag name is one of: "noframes", "style"
     1161             *
     1162             * The scripting flag is never enabled in this parser.
     1163             */
     1164            case '+NOFRAMES':
     1165            case '+STYLE':
     1166                $this->insert_html_element( $this->state->current_token );
     1167                return true;
     1168
     1169            /*
     1170             * > A start tag whose tag name is "noscript", if the scripting flag is disabled
     1171             */
     1172            case '+NOSCRIPT':
     1173                $this->insert_html_element( $this->state->current_token );
     1174                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD_NOSCRIPT;
     1175                return true;
     1176
     1177            /*
     1178             * > A start tag whose tag name is "script"
     1179             *
     1180             * @todo Could the adjusted insertion location be anything other than the current location?
     1181             */
     1182            case '+SCRIPT':
     1183                $this->insert_html_element( $this->state->current_token );
     1184                return true;
     1185
     1186            /*
     1187             * > An end tag whose tag name is "head"
     1188             */
     1189            case '-HEAD':
     1190                $this->state->stack_of_open_elements->pop();
     1191                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD;
     1192                return true;
     1193
     1194            /*
     1195             * > An end tag whose tag name is one of: "body", "html", "br"
     1196             */
     1197            case '-BODY':
     1198            case '-HTML':
     1199            case '-BR':
     1200                /*
     1201                 * > Act as described in the "anything else" entry below.
     1202                 */
     1203                goto in_head_anything_else;
     1204                break;
     1205
     1206            /*
     1207             * > A start tag whose tag name is "template"
     1208             *
     1209             * @todo Could the adjusted insertion location be anything other than the current location?
     1210             */
     1211            case '+TEMPLATE':
     1212                $this->state->active_formatting_elements->insert_marker();
     1213                $this->state->frameset_ok = false;
     1214
     1215                $this->state->insertion_mode                      = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE;
     1216                $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE;
     1217
     1218                $this->insert_html_element( $this->state->current_token );
     1219                return true;
     1220
     1221            /*
     1222             * > An end tag whose tag name is "template"
     1223             */
     1224            case '-TEMPLATE':
     1225                if ( ! $this->state->stack_of_open_elements->contains( 'TEMPLATE' ) ) {
     1226                    // @todo Indicate a parse error once it's possible.
     1227                    return $this->step();
     1228                }
     1229
     1230                $this->generate_implied_end_tags_thoroughly();
     1231                if ( ! $this->state->stack_of_open_elements->current_node_is( 'TEMPLATE' ) ) {
     1232                    // @todo Indicate a parse error once it's possible.
     1233                }
     1234
     1235                $this->state->stack_of_open_elements->pop_until( 'TEMPLATE' );
     1236                $this->state->active_formatting_elements->clear_up_to_last_marker();
     1237                array_pop( $this->state->stack_of_template_insertion_modes );
     1238                $this->reset_insertion_mode();
     1239                return true;
     1240        }
     1241
     1242        /*
     1243         * > A start tag whose tag name is "head"
     1244         * > Any other end tag
     1245         */
     1246        if ( '+HEAD' === $op || $is_closer ) {
     1247            // Parse error: ignore the token.
     1248            return $this->step();
     1249        }
     1250
     1251        /*
     1252         * > Anything else
     1253         */
     1254        in_head_anything_else:
     1255        $this->state->stack_of_open_elements->pop();
     1256        $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD;
     1257        return $this->step( self::REPROCESS_CURRENT_NODE );
    10541258    }
    10551259
     
    29923196     */
    29933197    private function step_in_template(): bool {
    2994         $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE . ' state.' );
     3198        $token_name = $this->get_token_name();
     3199        $token_type = $this->get_token_type();
     3200        $is_closer  = $this->is_tag_closer();
     3201        $op_sigil   = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : '';
     3202        $op         = "{$op_sigil}{$token_name}";
     3203
     3204        switch ( $op ) {
     3205            /*
     3206             * > A character token
     3207             * > A comment token
     3208             * > A DOCTYPE token
     3209             */
     3210            case '#text':
     3211            case '#comment':
     3212            case '#funky-comment':
     3213            case '#presumptuous-tag':
     3214            case 'html':
     3215                return $this->step_in_body();
     3216
     3217            /*
     3218             * > A start tag whose tag name is one of: "base", "basefont", "bgsound", "link",
     3219             * > "meta", "noframes", "script", "style", "template", "title"
     3220             * > An end tag whose tag name is "template"
     3221             */
     3222            case '+BASE':
     3223            case '+BASEFONT':
     3224            case '+BGSOUND':
     3225            case '+LINK':
     3226            case '+META':
     3227            case '+NOFRAMES':
     3228            case '+SCRIPT':
     3229            case '+STYLE':
     3230            case '+TEMPLATE':
     3231            case '+TITLE':
     3232            case '-TEMPLATE':
     3233                return $this->step_in_head();
     3234
     3235            /*
     3236             * > A start tag whose tag name is one of: "caption", "colgroup", "tbody", "tfoot", "thead"
     3237             */
     3238            case '+CAPTION':
     3239            case '+COLGROUP':
     3240            case '+TBODY':
     3241            case '+TFOOT':
     3242            case '+THEAD':
     3243                array_pop( $this->state->stack_of_template_insertion_modes );
     3244                $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE;
     3245                $this->state->insertion_mode                      = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE;
     3246                return $this->step( self::REPROCESS_CURRENT_NODE );
     3247
     3248            /*
     3249             * > A start tag whose tag name is "col"
     3250             */
     3251            case '+COL':
     3252                array_pop( $this->state->stack_of_template_insertion_modes );
     3253                $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP;
     3254                $this->state->insertion_mode                      = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP;
     3255                return $this->step( self::REPROCESS_CURRENT_NODE );
     3256
     3257            /*
     3258             * > A start tag whose tag name is "tr"
     3259             */
     3260            case '+TR':
     3261                array_pop( $this->state->stack_of_template_insertion_modes );
     3262                $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
     3263                $this->state->insertion_mode                      = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
     3264                return $this->step( self::REPROCESS_CURRENT_NODE );
     3265
     3266            /*
     3267             * > A start tag whose tag name is one of: "td", "th"
     3268             */
     3269            case '+TD':
     3270            case '+TH':
     3271                array_pop( $this->state->stack_of_template_insertion_modes );
     3272                $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
     3273                $this->state->insertion_mode                      = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
     3274                return $this->step( self::REPROCESS_CURRENT_NODE );
     3275        }
     3276
     3277        /*
     3278         * > Any other start tag
     3279         */
     3280        if ( ! $is_closer ) {
     3281            array_pop( $this->state->stack_of_template_insertion_modes );
     3282            $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
     3283            $this->state->insertion_mode                      = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
     3284            return $this->step( self::REPROCESS_CURRENT_NODE );
     3285        }
     3286
     3287        /*
     3288         * > Any other end tag
     3289         */
     3290        if ( $is_closer ) {
     3291            // Parse error: ignore the token.
     3292            return $this->step();
     3293        }
     3294
     3295        /*
     3296         * > An end-of-file token
     3297         */
     3298        if ( ! $this->state->stack_of_open_elements->contains( 'TEMPLATE' ) ) {
     3299            // Stop parsing.
     3300            return false;
     3301        }
     3302
     3303        // @todo Indicate a parse error once it's possible.
     3304        $this->state->stack_of_open_elements->pop_until( 'TEMPLATE' );
     3305        $this->state->active_formatting_elements->clear_up_to_last_marker();
     3306        array_pop( $this->state->stack_of_template_insertion_modes );
     3307        $this->reset_insertion_mode();
     3308        return $this->step( self::REPROCESS_CURRENT_NODE );
    29953309    }
    29963310
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php

    r58806 r58833  
    4747            'AUDIO',
    4848            'B',
     49            'BASE',
    4950            'BDI',
    5051            'BDO',
     52            'BGSOUND', // Deprectated.
    5153            'BIG',
    5254            'BLINK', // Deprecated.
     
    9496            'LABEL',
    9597            'LEGEND',
     98            'LINK',
    9699            'LISTING', // Deprecated.
    97100            'MAIN',
     
    100103            'MARQUEE', // Deprecated.
    101104            'MENU',
     105            'META',
    102106            'METER',
    103107            'MULTICOL', // Deprecated.
     
    179183    public static function data_unsupported_elements() {
    180184        $unsupported_elements = array(
    181             'BASE',
    182             'BGSOUND', // Deprecated; self-closing if self-closing flag provided, otherwise normal.
    183185            'BODY',
    184186            'FRAME',
     
    187189            'HTML',
    188190            'IFRAME',
    189             'LINK',
    190191            'MATH',
    191             'META',
    192192            'NOEMBED', // Neutralized.
    193193            'NOFRAMES', // Neutralized.
     
    196196            'STYLE',
    197197            'SVG',
    198             'TEMPLATE',
    199198            'TEXTAREA',
    200199            'TITLE',
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php

    r58779 r58833  
    3535        'adoption01/line0159' => 'Unimplemented: Reconstruction of active formatting elements.',
    3636        'adoption01/line0318' => 'Unimplemented: Reconstruction of active formatting elements.',
     37        'template/line0885'   => 'Unimplemented: no parsing of attributes on context node.',
    3738        'tests1/line0720'     => 'Unimplemented: Reconstruction of active formatting elements.',
    3839        'tests15/line0001'    => 'Unimplemented: Reconstruction of active formatting elements.',
     
    164165            }
    165166
    166             if ( $was_text && '#text' !== $processor->get_token_name() ) {
     167            $token_name = $processor->get_token_name();
     168            $token_type = $processor->get_token_type();
     169            $is_closer  = $processor->is_tag_closer();
     170
     171            if ( $was_text && '#text' !== $token_name ) {
    167172                $output   .= "{$text_node}\"\n";
    168173                $was_text  = false;
     
    170175            }
    171176
    172             switch ( $processor->get_token_type() ) {
     177            switch ( $token_type ) {
    173178                case '#tag':
    174                     $tag_name = strtolower( $processor->get_tag() );
    175 
    176                     if ( $processor->is_tag_closer() ) {
     179                    $tag_name = strtolower( $token_name );
     180
     181                    if ( $is_closer ) {
    177182                        --$indent_level;
     183
     184                        if ( 'TEMPLATE' === $token_name ) {
     185                            --$indent_level;
     186                        }
     187
    178188                        break;
    179189                    }
    180190
    181                     $tag_indent = count( $processor->get_breadcrumbs() ) - 1;
     191                    $tag_indent = $indent_level;
    182192
    183193                    if ( ! WP_HTML_Processor::is_void( $tag_name ) ) {
    184                         $indent_level = $tag_indent + 1;
     194                        ++$indent_level;
    185195                    }
    186196
     
    208218                    if ( '' !== $modifiable_text ) {
    209219                        $output .= str_repeat( $indent, $indent_level ) . "\"{$modifiable_text}\"\n";
     220                    }
     221
     222                    if ( 'TEMPLATE' === $token_name ) {
     223                        $output .= str_repeat( $indent, $indent_level ) . "content\n";
     224                        ++$indent_level;
    210225                    }
    211226
Note: See TracChangeset for help on using the changeset viewer.