Make WordPress Core

Changeset 58806


Ignore:
Timestamp:
07/24/2024 06:39:46 PM (10 months ago)
Author:
dmsnell
Message:

HTML API: Add TABLE support in HTML Processor.

As part of work to add more spec support to the HTML API, this patch adds
support for various table-related insertion modes. This includes support
for tables, table rows, table cells, table column groups, etc...

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

Props: dmsnell, jonsurrell.
See #61576.

Location:
trunk
Files:
4 edited

Legend:

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

    r58779 r58806  
    722722
    723723    /**
     724     * Clear the stack back to a table context.
     725     *
     726     * > When the steps above require the UA to clear the stack back to a table context, it means
     727     * > that the UA must, while the current node is not a table, template, or html element, pop
     728     * > elements from the stack of open elements.
     729     *
     730     * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-context
     731     *
     732     * @since 6.7.0
     733     */
     734    public function clear_to_table_context(): void {
     735        foreach ( $this->walk_up() as $item ) {
     736            if (
     737                'TABLE' === $item->node_name ||
     738                'TEMPLATE' === $item->node_name ||
     739                'HTML' === $item->node_name
     740            ) {
     741                break;
     742            }
     743            $this->pop();
     744        }
     745    }
     746
     747    /**
     748     * Clear the stack back to a table body context.
     749     *
     750     * > When the steps above require the UA to clear the stack back to a table body context, it
     751     * > means that the UA must, while the current node is not a tbody, tfoot, thead, template, or
     752     * > html element, pop elements from the stack of open elements.
     753     *
     754     * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-body-context
     755     *
     756     * @since 6.7.0
     757     */
     758    public function clear_to_table_body_context(): void {
     759        foreach ( $this->walk_up() as $item ) {
     760            if (
     761                'TBODY' === $item->node_name ||
     762                'TFOOT' === $item->node_name ||
     763                'THEAD' === $item->node_name ||
     764                'TEMPLATE' === $item->node_name ||
     765                'HTML' === $item->node_name
     766            ) {
     767                break;
     768            }
     769            $this->pop();
     770        }
     771    }
     772
     773    /**
     774     * Clear the stack back to a table row context.
     775     *
     776     * > When the steps above require the UA to clear the stack back to a table row context, it
     777     * > means that the UA must, while the current node is not a tr, template, or html element, pop
     778     * > elements from the stack of open elements.
     779     *
     780     * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-row-context
     781     *
     782     * @since 6.7.0
     783     */
     784    public function clear_to_table_row_context(): void {
     785        foreach ( $this->walk_up() as $item ) {
     786            if (
     787                'TR' === $item->node_name ||
     788                'TEMPLATE' === $item->node_name ||
     789                'HTML' === $item->node_name
     790            ) {
     791                break;
     792            }
     793            $this->pop();
     794        }
     795    }
     796
     797    /**
    724798     * Wakeup magic method.
    725799     *
  • trunk/src/wp-includes/html-api/class-wp-html-processor.php

    r58781 r58806  
    17871787             */
    17881788            case '+TABLE':
     1789                /*
     1790                 * > If the Document is not set to quirks mode, and the stack of open elements
     1791                 * > has a p element in button scope, then close a p element.
     1792                 */
    17891793                if (
    17901794                    WP_HTML_Processor_State::QUIRKS_MODE !== $this->state->document_mode &&
     
    21182122     * logic for the generalized WP_HTML_Processor::step() function.
    21192123     *
    2120      * @since 6.7.0 Stub implementation.
     2124     * @since 6.7.0
    21212125     *
    21222126     * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
     
    21282132     */
    21292133    private function step_in_table(): bool {
    2130         $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE . ' state.' );
     2134        $token_name = $this->get_token_name();
     2135        $token_type = $this->get_token_type();
     2136        $op_sigil   = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : '';
     2137        $op         = "{$op_sigil}{$token_name}";
     2138
     2139        switch ( $op ) {
     2140            /*
     2141             * > A character token, if the current node is table,
     2142             * > tbody, template, tfoot, thead, or tr element
     2143             */
     2144            case '#text':
     2145                $current_node      = $this->state->stack_of_open_elements->current_node();
     2146                $current_node_name = $current_node ? $current_node->node_name : null;
     2147                if (
     2148                    $current_node_name && (
     2149                        'TABLE' === $current_node_name ||
     2150                        'TBODY' === $current_node_name ||
     2151                        'TEMPLATE' === $current_node_name ||
     2152                        'TFOOT' === $current_node_name ||
     2153                        'THEAD' === $current_node_name ||
     2154                        'TR' === $current_node_name
     2155                    )
     2156                ) {
     2157                    $text = $this->get_modifiable_text();
     2158                    /*
     2159                     * If the text is empty after processing HTML entities and stripping
     2160                     * U+0000 NULL bytes then ignore the token.
     2161                     */
     2162                    if ( '' === $text ) {
     2163                        return $this->step();
     2164                    }
     2165
     2166                    /*
     2167                     * This follows the rules for "in table text" insertion mode.
     2168                     *
     2169                     * Whitespace-only text nodes are inserted in-place. Otherwise
     2170                     * foster parenting is enabled and the nodes would be
     2171                     * inserted out-of-place.
     2172                     *
     2173                     * > If any of the tokens in the pending table character tokens
     2174                     * > list are character tokens that are not ASCII whitespace,
     2175                     * > then this is a parse error: reprocess the character tokens
     2176                     * > in the pending table character tokens list using the rules
     2177                     * > given in the "anything else" entry in the "in table"
     2178                     * > insertion mode.
     2179                     * >
     2180                     * > Otherwise, insert the characters given by the pending table
     2181                     * > character tokens list.
     2182                     *
     2183                     * @see https://html.spec.whatwg.org/#parsing-main-intabletext
     2184                     */
     2185                    if ( strlen( $text ) === strspn( $text, " \t\f\r\n" ) ) {
     2186                        $this->insert_html_element( $this->state->current_token );
     2187                        return true;
     2188                    }
     2189
     2190                    // Non-whitespace would trigger fostering, unsupported at this time.
     2191                    $this->bail( 'Foster parenting is not supported.' );
     2192                    break;
     2193                }
     2194                break;
     2195
     2196            /*
     2197             * > A comment token
     2198             */
     2199            case '#comment':
     2200            case '#funky-comment':
     2201            case '#presumptuous-tag':
     2202                $this->insert_html_element( $this->state->current_token );
     2203                return true;
     2204
     2205            /*
     2206             * > A DOCTYPE token
     2207             */
     2208            case 'html':
     2209                // Parse error: ignore the token.
     2210                return $this->step();
     2211
     2212            /*
     2213             * > A start tag whose tag name is "caption"
     2214             */
     2215            case '+CAPTION':
     2216                $this->state->stack_of_open_elements->clear_to_table_context();
     2217                $this->state->active_formatting_elements->insert_marker();
     2218                $this->insert_html_element( $this->state->current_token );
     2219                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION;
     2220                return true;
     2221
     2222            /*
     2223             * > A start tag whose tag name is "colgroup"
     2224             */
     2225            case '+COLGROUP':
     2226                $this->state->stack_of_open_elements->clear_to_table_context();
     2227                $this->insert_html_element( $this->state->current_token );
     2228                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP;
     2229                return true;
     2230
     2231            /*
     2232             * > A start tag whose tag name is "col"
     2233             */
     2234            case '+COL':
     2235                $this->state->stack_of_open_elements->clear_to_table_context();
     2236
     2237                /*
     2238                 * > Insert an HTML element for a "colgroup" start tag token with no attributes,
     2239                 * > then switch the insertion mode to "in column group".
     2240                 */
     2241                $this->insert_virtual_node( 'COLGROUP' );
     2242                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP;
     2243                return $this->step( self::REPROCESS_CURRENT_NODE );
     2244
     2245            /*
     2246             * > A start tag whose tag name is one of: "tbody", "tfoot", "thead"
     2247             */
     2248            case '+TBODY':
     2249            case '+TFOOT':
     2250            case '+THEAD':
     2251                $this->state->stack_of_open_elements->clear_to_table_context();
     2252                $this->insert_html_element( $this->state->current_token );
     2253                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
     2254                return true;
     2255
     2256            /*
     2257             * > A start tag whose tag name is one of: "td", "th", "tr"
     2258             */
     2259            case '+TD':
     2260            case '+TH':
     2261            case '+TR':
     2262                $this->state->stack_of_open_elements->clear_to_table_context();
     2263                /*
     2264                 * > Insert an HTML element for a "tbody" start tag token with no attributes,
     2265                 * > then switch the insertion mode to "in table body".
     2266                 */
     2267                $this->insert_virtual_node( 'TBODY' );
     2268                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
     2269                return $this->step( self::REPROCESS_CURRENT_NODE );
     2270
     2271            /*
     2272             * > A start tag whose tag name is "table"
     2273             *
     2274             * This tag in the IN TABLE insertion mode is a parse error.
     2275             */
     2276            case '+TABLE':
     2277                if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TABLE' ) ) {
     2278                    return $this->step();
     2279                }
     2280
     2281                $this->state->stack_of_open_elements->pop_until( 'TABLE' );
     2282                $this->reset_insertion_mode();
     2283                return $this->step( self::REPROCESS_CURRENT_NODE );
     2284
     2285            /*
     2286             * > An end tag whose tag name is "table"
     2287             */
     2288            case '-TABLE':
     2289                if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TABLE' ) ) {
     2290                    // @todo Indicate a parse error once it's possible.
     2291                    return $this->step();
     2292                }
     2293
     2294                $this->state->stack_of_open_elements->pop_until( 'TABLE' );
     2295                $this->reset_insertion_mode();
     2296                return true;
     2297
     2298            /*
     2299             * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr"
     2300             */
     2301            case '-BODY':
     2302            case '-CAPTION':
     2303            case '-COL':
     2304            case '-COLGROUP':
     2305            case '-HTML':
     2306            case '-TBODY':
     2307            case '-TD':
     2308            case '-TFOOT':
     2309            case '-TH':
     2310            case '-THEAD':
     2311            case '-TR':
     2312                // Parse error: ignore the token.
     2313                return $this->step();
     2314
     2315            /*
     2316             * > A start tag whose tag name is one of: "style", "script", "template"
     2317             * > An end tag whose tag name is "template"
     2318             */
     2319            case '+STYLE':
     2320            case '+SCRIPT':
     2321            case '+TEMPLATE':
     2322            case '-TEMPLATE':
     2323                /*
     2324                 * > Process the token using the rules for the "in head" insertion mode.
     2325                 */
     2326                return $this->step_in_head();
     2327
     2328            /*
     2329             * > A start tag whose tag name is "input"
     2330             *
     2331             * > If the token does not have an attribute with the name "type", or if it does, but
     2332             * > that attribute's value is not an ASCII case-insensitive match for the string
     2333             * > "hidden", then: act as described in the "anything else" entry below.
     2334             */
     2335            case '+INPUT':
     2336                $type_attribute = $this->get_attribute( 'type' );
     2337                if ( ! is_string( $type_attribute ) || 'hidden' !== strtolower( $type_attribute ) ) {
     2338                    goto anything_else;
     2339                }
     2340                // @todo Indicate a parse error once it's possible.
     2341                $this->insert_html_element( $this->state->current_token );
     2342                return true;
     2343
     2344            /*
     2345             * > A start tag whose tag name is "form"
     2346             *
     2347             * This tag in the IN TABLE insertion mode is a parse error.
     2348             */
     2349            case '+FORM':
     2350                if (
     2351                    $this->state->stack_of_open_elements->has_element_in_scope( 'TEMPLATE' ) ||
     2352                    isset( $this->state->form_element )
     2353                ) {
     2354                    return $this->step();
     2355                }
     2356
     2357                // This FORM is special because it immediately closes and cannot have other children.
     2358                $this->insert_html_element( $this->state->current_token );
     2359                $this->state->form_element = $this->state->current_token;
     2360                $this->state->stack_of_open_elements->pop();
     2361                return true;
     2362        }
     2363
     2364        /*
     2365         * > Anything else
     2366         * > Parse error. Enable foster parenting, process the token using the rules for the
     2367         * > "in body" insertion mode, and then disable foster parenting.
     2368         *
     2369         * @todo Indicate a parse error once it's possible.
     2370         */
     2371        anything_else:
     2372        $this->bail( 'Foster parenting is not supported.' );
    21312373    }
    21322374
     
    21942436     * logic for the generalized WP_HTML_Processor::step() function.
    21952437     *
    2196      * @since 6.7.0 Stub implementation.
     2438     * @since 6.7.0
    21972439     *
    21982440     * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
     
    22042446     */
    22052447    private function step_in_table_body(): bool {
    2206         $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY . ' state.' );
     2448        $tag_name = $this->get_tag();
     2449        $op_sigil = $this->is_tag_closer() ? '-' : '+';
     2450        $op       = "{$op_sigil}{$tag_name}";
     2451
     2452        switch ( $op ) {
     2453            /*
     2454             * > A start tag whose tag name is "tr"
     2455             */
     2456            case '+TR':
     2457                $this->state->stack_of_open_elements->clear_to_table_body_context();
     2458                $this->insert_html_element( $this->state->current_token );
     2459                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
     2460                return true;
     2461
     2462            /*
     2463             * > A start tag whose tag name is one of: "th", "td"
     2464             */
     2465            case '+TH':
     2466            case '+TD':
     2467                // @todo Indicate a parse error once it's possible.
     2468                $this->state->stack_of_open_elements->clear_to_table_body_context();
     2469                $this->insert_virtual_node( 'TR' );
     2470                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
     2471                return $this->step( self::REPROCESS_CURRENT_NODE );
     2472
     2473            /*
     2474             * > An end tag whose tag name is one of: "tbody", "tfoot", "thead"
     2475             */
     2476            case '-TBODY':
     2477            case '-TFOOT':
     2478            case '-THEAD':
     2479                /*
     2480                 * @todo This needs to check if the element in scope is an HTML element, meaning that
     2481                 *       when SVG and MathML support is added, this needs to differentiate between an
     2482                 *       HTML element of the given name, such as `<center>`, and a foreign element of
     2483                 *       the same given name.
     2484                 */
     2485                if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) {
     2486                    // Parse error: ignore the token.
     2487                    return $this->step();
     2488                }
     2489
     2490                $this->state->stack_of_open_elements->clear_to_table_body_context();
     2491                $this->state->stack_of_open_elements->pop();
     2492                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE;
     2493                return true;
     2494
     2495            /*
     2496             * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "tfoot", "thead"
     2497             * > An end tag whose tag name is "table"
     2498             */
     2499            case '+CAPTION':
     2500            case '+COL':
     2501            case '+COLGROUP':
     2502            case '+TBODY':
     2503            case '+TFOOT':
     2504            case '+THEAD':
     2505            case '-TABLE':
     2506                if (
     2507                    ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TBODY' ) &&
     2508                    ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'THEAD' ) &&
     2509                    ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TFOOT' )
     2510                ) {
     2511                    // Parse error: ignore the token.
     2512                    return $this->step();
     2513                }
     2514                $this->state->stack_of_open_elements->clear_to_table_body_context();
     2515                $this->state->stack_of_open_elements->pop();
     2516                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE;
     2517                return $this->step( self::REPROCESS_CURRENT_NODE );
     2518
     2519            /*
     2520             * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "td", "th", "tr"
     2521             */
     2522            case '-BODY':
     2523            case '-CAPTION':
     2524            case '-COL':
     2525            case '-COLGROUP':
     2526            case '-HTML':
     2527            case '-TD':
     2528            case '-TH':
     2529            case '-TR':
     2530                // Parse error: ignore the token.
     2531                return $this->step();
     2532        }
     2533
     2534        /*
     2535         * > Anything else
     2536         * > Process the token using the rules for the "in table" insertion mode.
     2537         */
     2538        return $this->step_in_table();
    22072539    }
    22082540
     
    22132545     * logic for the generalized WP_HTML_Processor::step() function.
    22142546     *
    2215      * @since 6.7.0 Stub implementation.
     2547     * @since 6.7.0
    22162548     *
    22172549     * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
     
    22232555     */
    22242556    private function step_in_row(): bool {
    2225         $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_ROW . ' state.' );
     2557        $tag_name = $this->get_tag();
     2558        $op_sigil = $this->is_tag_closer() ? '-' : '+';
     2559        $op       = "{$op_sigil}{$tag_name}";
     2560
     2561        switch ( $op ) {
     2562            /*
     2563             * > A start tag whose tag name is one of: "th", "td"
     2564             */
     2565            case '+TH':
     2566            case '+TD':
     2567                $this->state->stack_of_open_elements->clear_to_table_row_context();
     2568                $this->insert_html_element( $this->state->current_token );
     2569                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_CELL;
     2570                $this->state->active_formatting_elements->insert_marker();
     2571                return true;
     2572
     2573            /*
     2574             * > An end tag whose tag name is "tr"
     2575             */
     2576            case '-TR':
     2577                if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TR' ) ) {
     2578                    // Parse error: ignore the token.
     2579                    return $this->step();
     2580                }
     2581
     2582                $this->state->stack_of_open_elements->clear_to_table_row_context();
     2583                $this->state->stack_of_open_elements->pop();
     2584                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
     2585                return true;
     2586
     2587            /*
     2588             * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "tfoot", "thead", "tr"
     2589             * > An end tag whose tag name is "table"
     2590             */
     2591            case '+CAPTION':
     2592            case '+COL':
     2593            case '+COLGROUP':
     2594            case '+TBODY':
     2595            case '+TFOOT':
     2596            case '+THEAD':
     2597            case '+TR':
     2598            case '-TABLE':
     2599                if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TR' ) ) {
     2600                    // Parse error: ignore the token.
     2601                    return $this->step();
     2602                }
     2603
     2604                $this->state->stack_of_open_elements->clear_to_table_row_context();
     2605                $this->state->stack_of_open_elements->pop();
     2606                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
     2607                return $this->step( self::REPROCESS_CURRENT_NODE );
     2608
     2609            /*
     2610             * > An end tag whose tag name is one of: "tbody", "tfoot", "thead"
     2611             */
     2612            case '-TBODY':
     2613            case '-TFOOT':
     2614            case '-THEAD':
     2615                /*
     2616                 * @todo This needs to check if the element in scope is an HTML element, meaning that
     2617                 *       when SVG and MathML support is added, this needs to differentiate between an
     2618                 *       HTML element of the given name, such as `<center>`, and a foreign element of
     2619                 *       the same given name.
     2620                 */
     2621                if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) {
     2622                    // Parse error: ignore the token.
     2623                    return $this->step();
     2624                }
     2625
     2626                if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TR' ) ) {
     2627                    // Ignore the token.
     2628                    return $this->step();
     2629                }
     2630
     2631                $this->state->stack_of_open_elements->clear_to_table_row_context();
     2632                $this->state->stack_of_open_elements->pop();
     2633                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
     2634                return $this->step( self::REPROCESS_CURRENT_NODE );
     2635
     2636            /*
     2637             * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "td", "th"
     2638             */
     2639            case '-BODY':
     2640            case '-CAPTION':
     2641            case '-COL':
     2642            case '-COLGROUP':
     2643            case '-HTML':
     2644            case '-TD':
     2645            case '-TH':
     2646                // Parse error: ignore the token.
     2647                return $this->step();
     2648        }
     2649
     2650        /*
     2651         * > Anything else
     2652         * >   Process the token using the rules for the "in table" insertion mode.
     2653         */
     2654        return $this->step_in_table();
    22262655    }
    22272656
     
    22322661     * logic for the generalized WP_HTML_Processor::step() function.
    22332662     *
    2234      * @since 6.7.0 Stub implementation.
     2663     * @since 6.7.0
    22352664     *
    22362665     * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
     
    22422671     */
    22432672    private function step_in_cell(): bool {
    2244         $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_CELL . ' state.' );
     2673        $tag_name = $this->get_tag();
     2674        $op_sigil = $this->is_tag_closer() ? '-' : '+';
     2675        $op       = "{$op_sigil}{$tag_name}";
     2676
     2677        switch ( $op ) {
     2678            /*
     2679             * > An end tag whose tag name is one of: "td", "th"
     2680             */
     2681            case '-TD':
     2682            case '-TH':
     2683                /*
     2684                 * @todo This needs to check if the element in scope is an HTML element, meaning that
     2685                 *       when SVG and MathML support is added, this needs to differentiate between an
     2686                 *       HTML element of the given name, such as `<center>`, and a foreign element of
     2687                 *       the same given name.
     2688                 */
     2689                if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) {
     2690                    // Parse error: ignore the token.
     2691                    return $this->step();
     2692                }
     2693
     2694                $this->generate_implied_end_tags();
     2695
     2696                /*
     2697                 * @todo This needs to check if the current node is an HTML element, meaning that
     2698                 *       when SVG and MathML support is added, this needs to differentiate between an
     2699                 *       HTML element of the given name, such as `<center>`, and a foreign element of
     2700                 *       the same given name.
     2701                 */
     2702                if ( ! $this->state->stack_of_open_elements->current_node_is( $tag_name ) ) {
     2703                    // @todo Indicate a parse error once it's possible.
     2704                }
     2705
     2706                $this->state->stack_of_open_elements->pop_until( $tag_name );
     2707                $this->state->active_formatting_elements->clear_up_to_last_marker();
     2708                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
     2709                return true;
     2710
     2711            /*
     2712             * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "td",
     2713             * > "tfoot", "th", "thead", "tr"
     2714             */
     2715            case '+CAPTION':
     2716            case '+COL':
     2717            case '+COLGROUP':
     2718            case '+TBODY':
     2719            case '+TD':
     2720            case '+TFOOT':
     2721            case '+TH':
     2722            case '+THEAD':
     2723            case '+TR':
     2724                /*
     2725                 * > Assert: The stack of open elements has a td or th element in table scope.
     2726                 *
     2727                 * Nothing to do here, except to verify in tests that this never appears.
     2728                 */
     2729
     2730                $this->close_cell();
     2731                return $this->step( self::REPROCESS_CURRENT_NODE );
     2732
     2733            /*
     2734             * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html"
     2735             */
     2736            case '-BODY':
     2737            case '-CAPTION':
     2738            case '-COL':
     2739            case '-COLGROUP':
     2740            case '-HTML':
     2741                // Parse error: ignore the token.
     2742                return $this->step();
     2743
     2744            /*
     2745             * > An end tag whose tag name is one of: "table", "tbody", "tfoot", "thead", "tr"
     2746             */
     2747            case '-TABLE':
     2748            case '-TBODY':
     2749            case '-TFOOT':
     2750            case '-THEAD':
     2751            case '-TR':
     2752                /*
     2753                 * @todo This needs to check if the element in scope is an HTML element, meaning that
     2754                 *       when SVG and MathML support is added, this needs to differentiate between an
     2755                 *       HTML element of the given name, such as `<center>`, and a foreign element of
     2756                 *       the same given name.
     2757                 */
     2758                if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) {
     2759                    // Parse error: ignore the token.
     2760                    return $this->step();
     2761                }
     2762                $this->close_cell();
     2763                return $this->step( self::REPROCESS_CURRENT_NODE );
     2764        }
     2765
     2766        /*
     2767         * > Anything else
     2768         * >   Process the token using the rules for the "in body" insertion mode.
     2769         */
     2770        return $this->step_in_body();
    22452771    }
    22462772
     
    35784104
    35794105    /**
     4106     * Runs the "close the cell" algorithm.
     4107     *
     4108     * > Where the steps above say to close the cell, they mean to run the following algorithm:
     4109     * >   1. Generate implied end tags.
     4110     * >   2. If the current node is not now a td element or a th element, then this is a parse error.
     4111     * >   3. Pop elements from the stack of open elements stack until a td element or a th element has been popped from the stack.
     4112     * >   4. Clear the list of active formatting elements up to the last marker.
     4113     * >   5. Switch the insertion mode to "in row".
     4114     *
     4115     * @see https://html.spec.whatwg.org/multipage/parsing.html#close-the-cell
     4116     *
     4117     * @since 6.7.0
     4118     */
     4119    private function close_cell(): void {
     4120        $this->generate_implied_end_tags();
     4121        // @todo Parse error if the current node is a "td" or "th" element.
     4122        foreach ( $this->state->stack_of_open_elements->walk_up() as $element ) {
     4123            $this->state->stack_of_open_elements->pop();
     4124            if ( 'TD' === $element->node_name || 'TH' === $element->node_name ) {
     4125                break;
     4126            }
     4127        }
     4128        $this->state->active_formatting_elements->clear_up_to_last_marker();
     4129        $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
     4130    }
     4131
     4132    /**
    35804133     * Inserts an HTML element on the stack of open elements.
    35814134     *
     
    35884141    private function insert_html_element( WP_HTML_Token $token ): void {
    35894142        $this->state->stack_of_open_elements->push( $token );
     4143    }
     4144
     4145    /**
     4146     * Inserts a virtual element on the stack of open elements.
     4147     *
     4148     * @since 6.7.0
     4149     *
     4150     * @param string      $token_name    Name of token to create and insert into the stack of open elements.
     4151     * @param string|null $bookmark_name Optional. Name to give bookmark for created virtual node.
     4152     *                                   Defaults to auto-creating a bookmark name.
     4153     */
     4154    private function insert_virtual_node( $token_name, $bookmark_name = null ): void {
     4155        $here = $this->bookmarks[ $this->state->current_token->bookmark_name ];
     4156        $name = $bookmark_name ?? $this->bookmark_token();
     4157
     4158        $this->bookmarks[ $name ] = new WP_HTML_Span( $here->start, 0 );
     4159
     4160        $this->insert_html_element( new WP_HTML_Token( $name, $token_name, false ) );
    35904161    }
    35914162
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php

    r58779 r58806  
    182182            'BGSOUND', // Deprecated; self-closing if self-closing flag provided, otherwise normal.
    183183            'BODY',
    184             'CAPTION',
    185             'COL',
    186             'COLGROUP',
    187184            'FRAME',
    188185            'FRAMESET',
     
    199196            'STYLE',
    200197            'SVG',
    201             'TBODY',
    202             'TD',
    203198            'TEMPLATE',
    204199            'TEXTAREA',
    205             'TFOOT',
    206             'TH',
    207             'THEAD',
    208200            'TITLE',
    209             'TR',
    210201            'XMP', // Deprecated, use PRE instead.
    211202        );
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php

    r58779 r58806  
    425425        $this->assertNull( $processor->get_attribute_names_with_prefix( '' ), 'Should have ignored any attributes on the tag.' );
    426426    }
     427
     428    /*******************************************************************
     429     * RULES FOR "IN TABLE" MODE
     430     *******************************************************************/
     431
     432    /**
     433     * Ensure that form elements in tables (but not cells) are immediately popped off the stack.
     434     *
     435     * @ticket 61576
     436     */
     437    public function test_table_form_element_immediately_popped() {
     438        $processor = WP_HTML_Processor::create_fragment( '<table><form><!--comment-->' );
     439
     440        // There should be a FORM opener and a (virtual) FORM closer.
     441        $this->assertTrue( $processor->next_tag( 'FORM' ) );
     442        $this->assertTrue( $processor->next_token() );
     443        $this->assertSame( 'FORM', $processor->get_token_name() );
     444        $this->assertTrue( $processor->is_tag_closer() );
     445
     446        // Followed by the comment token.
     447        $this->assertTrue( $processor->next_token() );
     448        $this->assertSame( '#comment', $processor->get_token_name() );
     449        $this->assertsame( array( 'HTML', 'BODY', 'TABLE', '#comment' ), $processor->get_breadcrumbs() );
     450    }
    427451}
Note: See TracChangeset for help on using the changeset viewer.