Make WordPress Core

Changeset 58677


Ignore:
Timestamp:
07/05/2024 12:50:19 AM (9 months ago)
Author:
dmsnell
Message:

HTML API: Support SELECT insertion mode.

As part of work to add more spec support to the HTML API, this patch adds
support for the SELECT, OPTION, and OPTGROUP elements, including the
requisite support for the IN SELECT insertion mode.

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

Props dmsnell, jonsurrell.
See #61576.

Location:
trunk
Files:
6 edited

Legend:

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

    r58676 r58677  
    190190     * Returns whether an element is in a specific scope.
    191191     *
    192      * ## HTML Support
    193      *
    194      * This function skips checking for the termination list because there
    195      * are no supported elements which appear in the termination list.
    196      *
    197192     * @since 6.4.0
    198193     *
     
    312307     * Returns whether a particular element is in select scope.
    313308     *
    314      * @since 6.4.0
     309     * This test differs from the others like it, in that its rules are inverted.
     310     * Instead of arriving at a match when one of any tag in a termination group
     311     * is reached, this one terminates if any other tag is reached.
     312     *
     313     * > The stack of open elements is said to have a particular element in select scope when it has
     314     * > that element in the specific scope consisting of all element types except the following:
     315     * >   - optgroup in the HTML namespace
     316     * >   - option in the HTML namespace
     317     *
     318     * @since 6.4.0 Stub implementation (throws).
     319     * @since 6.7.0 Full implementation.
    315320     *
    316321     * @see https://html.spec.whatwg.org/#has-an-element-in-select-scope
    317322     *
    318      * @throws WP_HTML_Unsupported_Exception Always until this function is implemented.
    319      *
    320323     * @param string $tag_name Name of tag to check.
    321      * @return bool Whether given element is in scope.
     324     * @return bool Whether the given element is in SELECT scope.
    322325     */
    323326    public function has_element_in_select_scope( $tag_name ) {
    324         throw new WP_HTML_Unsupported_Exception( 'Cannot process elements depending on select scope.' );
    325 
    326         return false; // The linter requires this unreachable code until the function is implemented and can return.
     327        foreach ( $this->walk_up() as $node ) {
     328            if ( $node->node_name === $tag_name ) {
     329                return true;
     330            }
     331
     332            if (
     333                'OPTION' !== $node->node_name &&
     334                'OPTGROUP' !== $node->node_name
     335            ) {
     336                return false;
     337            }
     338        }
     339
     340        return false;
    327341    }
    328342
  • trunk/src/wp-includes/html-api/class-wp-html-processor.php

    r58676 r58677  
    102102 *  - Containers: ADDRESS, BLOCKQUOTE, DETAILS, DIALOG, DIV, FOOTER, HEADER, MAIN, MENU, SPAN, SUMMARY.
    103103 *  - Custom elements: All custom elements are supported. :)
    104  *  - Form elements: BUTTON, DATALIST, FIELDSET, INPUT, LABEL, LEGEND, METER, PROGRESS, SEARCH.
     104 *  - Form elements: BUTTON, DATALIST, FIELDSET, INPUT, LABEL, LEGEND, METER, OPTGROUP, OPTION, PROGRESS, SEARCH, SELECT.
    105105 *  - Formatting elements: B, BIG, CODE, EM, FONT, I, PRE, SMALL, STRIKE, STRONG, TT, U, WBR.
    106106 *  - Heading elements: H1, H2, H3, H4, H5, H6, HGROUP.
     
    757757                case WP_HTML_Processor_State::INSERTION_MODE_IN_BODY:
    758758                    return $this->step_in_body();
     759
     760                case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD:
     761                    return $this->step_in_head();
     762
     763                case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT:
     764                    return $this->step_in_select();
    759765
    760766                default:
     
    13351341            case '+SOURCE':
    13361342            case '+TRACK':
     1343                $this->insert_html_element( $this->state->current_token );
     1344                return true;
     1345
     1346            /*
     1347             * > A start tag whose tag name is "select"
     1348             */
     1349            case '+SELECT':
     1350                $this->reconstruct_active_formatting_elements();
     1351                $this->insert_html_element( $this->state->current_token );
     1352                $this->state->frameset_ok = false;
     1353
     1354                switch ( $this->state->insertion_mode ) {
     1355                    /*
     1356                     * > If the insertion mode is one of "in table", "in caption", "in table body", "in row",
     1357                     * > or "in cell", then switch the insertion mode to "in select in table".
     1358                     */
     1359                    case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE:
     1360                    case WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION:
     1361                    case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY:
     1362                    case WP_HTML_Processor_State::INSERTION_MODE_IN_ROW:
     1363                    case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL:
     1364                        $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE;
     1365                        break;
     1366
     1367                    /*
     1368                     * > Otherwise, switch the insertion mode to "in select".
     1369                     */
     1370                    default:
     1371                        $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT;
     1372                        break;
     1373                }
     1374                return true;
     1375
     1376            /*
     1377             * > A start tag whose tag name is one of: "optgroup", "option"
     1378             */
     1379            case '+OPTGROUP':
     1380            case '+OPTION':
     1381                if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) {
     1382                    $this->state->stack_of_open_elements->pop();
     1383                }
     1384                $this->reconstruct_active_formatting_elements();
    13371385                $this->insert_html_element( $this->state->current_token );
    13381386                return true;
     
    13791427            case 'NOSCRIPT':
    13801428            case 'OBJECT':
    1381             case 'OPTGROUP':
    1382             case 'OPTION':
    13831429            case 'PLAINTEXT':
    13841430            case 'RB':
     
    13881434            case 'SARCASM':
    13891435            case 'SCRIPT':
    1390             case 'SELECT':
    13911436            case 'STYLE':
    13921437            case 'SVG':
     
    14471492            }
    14481493        }
     1494    }
     1495
     1496    /**
     1497     * Parses next element in the 'in head' insertion mode.
     1498     *
     1499     * This internal function performs the 'in head' insertion mode
     1500     * logic for the generalized WP_HTML_Processor::step() function.
     1501     *
     1502     * @since 6.7.0 Stub implementation.
     1503     *
     1504     * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
     1505     *
     1506     * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inhead
     1507     * @see WP_HTML_Processor::step
     1508     *
     1509     * @return bool Whether an element was found.
     1510     */
     1511    private function step_in_head() {
     1512        $this->last_error = self::ERROR_UNSUPPORTED;
     1513        throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
     1514    }
     1515
     1516    /**
     1517     * Parses next element in the 'in select' insertion mode.
     1518     *
     1519     * This internal function performs the 'in select' insertion mode
     1520     * logic for the generalized WP_HTML_Processor::step() function.
     1521     *
     1522     * @since 6.7.0
     1523     *
     1524     * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
     1525     *
     1526     * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inselect
     1527     * @see WP_HTML_Processor::step
     1528     *
     1529     * @return bool Whether an element was found.
     1530     */
     1531    private function step_in_select() {
     1532        $token_name = $this->get_token_name();
     1533        $token_type = $this->get_token_type();
     1534        $op_sigil   = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : '';
     1535        $op         = "{$op_sigil}{$token_name}";
     1536
     1537        switch ( $op ) {
     1538            /*
     1539             * > Any other character token
     1540             */
     1541            case '#text':
     1542                $current_token = $this->bookmarks[ $this->state->current_token->bookmark_name ];
     1543
     1544                /*
     1545                 * > A character token that is U+0000 NULL
     1546                 *
     1547                 * If a text node only comprises null bytes then it should be
     1548                 * entirely ignored and should not return to calling code.
     1549                 */
     1550                if (
     1551                    1 <= $current_token->length &&
     1552                    "\x00" === $this->html[ $current_token->start ] &&
     1553                    strspn( $this->html, "\x00", $current_token->start, $current_token->length ) === $current_token->length
     1554                ) {
     1555                    // Parse error: ignore the token.
     1556                    return $this->step();
     1557                }
     1558
     1559                $this->insert_html_element( $this->state->current_token );
     1560                return true;
     1561
     1562            /*
     1563             * > A comment token
     1564             */
     1565            case '#comment':
     1566            case '#funky-comment':
     1567            case '#presumptuous-tag':
     1568                $this->insert_html_element( $this->state->current_token );
     1569                return true;
     1570
     1571            /*
     1572             * > A DOCTYPE token
     1573             */
     1574            case 'html':
     1575                // Parse error: ignore the token.
     1576                return $this->step();
     1577
     1578            /*
     1579             * > A start tag whose tag name is "html"
     1580             */
     1581            case '+HTML':
     1582                return $this->step_in_body();
     1583
     1584            /*
     1585             * > A start tag whose tag name is "option"
     1586             */
     1587            case '+OPTION':
     1588                if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) {
     1589                    $this->state->stack_of_open_elements->pop();
     1590                }
     1591                $this->insert_html_element( $this->state->current_token );
     1592                return true;
     1593
     1594            /*
     1595             * > A start tag whose tag name is "optgroup"
     1596             * > A start tag whose tag name is "hr"
     1597             *
     1598             * These rules are identical except for the treatment of the self-closing flag and
     1599             * the subsequent pop of the HR void element, all of which is handled elsewhere in the processor.
     1600             */
     1601            case '+OPTGROUP':
     1602            case '+HR':
     1603                if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) {
     1604                    $this->state->stack_of_open_elements->pop();
     1605                }
     1606
     1607                if ( $this->state->stack_of_open_elements->current_node_is( 'OPTGROUP' ) ) {
     1608                    $this->state->stack_of_open_elements->pop();
     1609                }
     1610
     1611                $this->insert_html_element( $this->state->current_token );
     1612                return true;
     1613
     1614            /*
     1615             * > An end tag whose tag name is "optgroup"
     1616             */
     1617            case '-OPTGROUP':
     1618                $current_node = $this->state->stack_of_open_elements->current_node();
     1619                if ( $current_node && 'OPTION' === $current_node->node_name ) {
     1620                    foreach ( $this->state->stack_of_open_elements->walk_up( $current_node ) as $parent ) {
     1621                        break;
     1622                    }
     1623                    if ( $parent && 'OPTGROUP' === $parent->node_name ) {
     1624                        $this->state->stack_of_open_elements->pop();
     1625                    }
     1626                }
     1627
     1628                if ( $this->state->stack_of_open_elements->current_node_is( 'OPTGROUP' ) ) {
     1629                    $this->state->stack_of_open_elements->pop();
     1630                    return true;
     1631                }
     1632
     1633                // Parse error: ignore the token.
     1634                return $this->step();
     1635
     1636            /*
     1637             * > An end tag whose tag name is "option"
     1638             */
     1639            case '-OPTION':
     1640                if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) {
     1641                    $this->state->stack_of_open_elements->pop();
     1642                    return true;
     1643                }
     1644
     1645                // Parse error: ignore the token.
     1646                return $this->step();
     1647
     1648            /*
     1649             * > An end tag whose tag name is "select"
     1650             * > A start tag whose tag name is "select"
     1651             *
     1652             * > It just gets treated like an end tag.
     1653             */
     1654            case '-SELECT':
     1655            case '+SELECT':
     1656                if ( ! $this->state->stack_of_open_elements->has_element_in_select_scope( 'SELECT' ) ) {
     1657                    // Parse error: ignore the token.
     1658                    return $this->step();
     1659                }
     1660                $this->state->stack_of_open_elements->pop_until( 'SELECT' );
     1661                $this->reset_insertion_mode();
     1662                return true;
     1663
     1664            /*
     1665             * > A start tag whose tag name is one of: "input", "keygen", "textarea"
     1666             *
     1667             * All three of these tags are considered a parse error when found in this insertion mode.
     1668             */
     1669            case '+INPUT':
     1670            case '+KEYGEN':
     1671            case '+TEXTAREA':
     1672                if ( ! $this->state->stack_of_open_elements->has_element_in_select_scope( 'SELECT' ) ) {
     1673                    // Ignore the token.
     1674                    return $this->step();
     1675                }
     1676                $this->state->stack_of_open_elements->pop_until( 'SELECT' );
     1677                $this->reset_insertion_mode();
     1678                return $this->step( self::REPROCESS_CURRENT_NODE );
     1679
     1680            /*
     1681             * > A start tag whose tag name is one of: "script", "template"
     1682             * > An end tag whose tag name is "template"
     1683             */
     1684            case '+SCRIPT':
     1685            case '+TEMPLATE':
     1686            case '-TEMPLATE':
     1687                return $this->step_in_head();
     1688        }
     1689
     1690        /*
     1691         * > Anything else
     1692         * >   Parse error: ignore the token.
     1693         */
     1694        return $this->step();
    14491695    }
    14501696
     
    20372283     *
    20382284     * @since 6.4.0
     2285     * @since 6.7.0 Full spec support.
    20392286     *
    20402287     * @see https://html.spec.whatwg.org/#generate-implied-end-tags
     
    20472294            'DT',
    20482295            'LI',
     2296            'OPTGROUP',
     2297            'OPTION',
    20492298            'P',
     2299            'RB',
     2300            'RP',
     2301            'RT',
     2302            'RTC',
    20502303        );
    20512304
    2052         $current_node = $this->state->stack_of_open_elements->current_node();
     2305        $no_exclusions = ! isset( $except_for_this_element );
     2306
    20532307        while (
    2054             $current_node && $current_node->node_name !== $except_for_this_element &&
     2308            ( $no_exclusions || ! $this->state->stack_of_open_elements->current_node_is( $except_for_this_element ) ) &&
    20552309            in_array( $this->state->stack_of_open_elements->current_node(), $elements_with_implied_end_tags, true )
    20562310        ) {
     
    20662320     *
    20672321     * @since 6.4.0
     2322     * @since 6.7.0 Full spec support.
    20682323     *
    20692324     * @see WP_HTML_Processor::generate_implied_end_tags
     
    20722327    private function generate_implied_end_tags_thoroughly() {
    20732328        $elements_with_implied_end_tags = array(
     2329            'CAPTION',
     2330            'COLGROUP',
    20742331            'DD',
    20752332            'DT',
    20762333            'LI',
     2334            'OPTGROUP',
     2335            'OPTION',
    20772336            'P',
     2337            'RB',
     2338            'RP',
     2339            'RT',
     2340            'RTC',
     2341            'TBODY',
     2342            'TD',
     2343            'TFOOT',
     2344            'TH',
     2345            'THEAD',
     2346            'TR',
    20782347        );
    20792348
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php

    r58363 r58677  
    407407            'NOSCRIPT'  => array( 'NOSCRIPT' ),
    408408            'OBJECT'    => array( 'OBJECT' ),
    409             'OPTGROUP'  => array( 'OPTGROUP' ),
    410             'OPTION'    => array( 'OPTION' ),
    411409            'PLAINTEXT' => array( 'PLAINTEXT' ),
    412410            'RB'        => array( 'RB' ),
     
    416414            'SARCASM'   => array( 'SARCASM' ),
    417415            'SCRIPT'    => array( 'SCRIPT' ),
    418             'SELECT'    => array( 'SELECT' ),
    419416            'STYLE'     => array( 'STYLE' ),
    420417            'SVG'       => array( 'SVG' ),
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php

    r58592 r58677  
    190190            'NOSCRIPT',
    191191            'OBJECT',
    192             'OPTGROUP',
    193             'OPTION',
    194192            'PLAINTEXT', // Neutralized.
    195193            'RB', // Neutralized.
     
    198196            'RTC', // Neutralized.
    199197            'SCRIPT',
    200             'SELECT',
    201198            'STYLE',
    202199            'SVG',
  • trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php

    r57508 r58677  
    5959     */
    6060    public function test_generate_implied_end_tags_needs_support() {
    61         $this->ensure_support_is_added_everywhere( 'OPTGROUP' );
    62         $this->ensure_support_is_added_everywhere( 'OPTION' );
    6361        $this->ensure_support_is_added_everywhere( 'RB' );
    6462        $this->ensure_support_is_added_everywhere( 'RP' );
     
    8078        $this->ensure_support_is_added_everywhere( 'CAPTION' );
    8179        $this->ensure_support_is_added_everywhere( 'COLGROUP' );
    82         $this->ensure_support_is_added_everywhere( 'OPTGROUP' );
    83         $this->ensure_support_is_added_everywhere( 'OPTION' );
    8480        $this->ensure_support_is_added_everywhere( 'RB' );
    8581        $this->ensure_support_is_added_everywhere( 'RP' );
  • trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php

    r57508 r58677  
    309309         */
    310310        $this->ensure_support_is_added_everywhere( 'SVG' );
    311 
    312         // These elements are specific to SELECT scope.
    313         $this->ensure_support_is_added_everywhere( 'OPTGROUP' );
    314         $this->ensure_support_is_added_everywhere( 'OPTION' );
    315311    }
    316312}
Note: See TracChangeset for help on using the changeset viewer.