Make WordPress Core


Ignore:
Timestamp:
07/05/2024 12:50:19 AM (2 years 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.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • 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
Note: See TracChangeset for help on using the changeset viewer.