Make WordPress Core


Ignore:
Timestamp:
09/10/2022 12:37:00 PM (2 years ago)
Author:
SergeyBiryukov
Message:

Editor: Backport Elements API updates.

This commit backports the original PRs from Gutenberg repository:

Props onemaggie, bernhard-reiter, cbravobernal, mmaattiiaass, scruffian, andraganescu, dpcalhoun, get_dave, Mamaduka, SergeyBiryukov.
See #56467.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-theme-json.php

    r54105 r54118  
    345345
    346346    /**
     347     * Defines which pseudo selectors are enabled for which elements.
     348     *
     349     * Note: this will affect both top-level and block-level elements.
     350     *
     351     * @since 6.1.0
     352     */
     353    const VALID_ELEMENT_PSEUDO_SELECTORS = array(
     354        'link'   => array( ':hover', ':focus', ':active', ':visited' ),
     355        'button' => array( ':hover', ':focus', ':active', ':visited' ),
     356    );
     357
     358    /**
    347359     * The valid elements that can be found under styles.
    348360     *
    349361     * @since 5.8.0
    350      * @since 6.1.0 Added `heading`, `button`, and `caption` to the elements.
     362     * @since 6.1.0 Added `heading`, `button`. and `caption` elements.
    351363     * @var string[]
    352364     */
    353365    const ELEMENTS = array(
    354         'link'    => 'a',
     366        'link'    => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity.
    355367        'heading' => 'h1, h2, h3, h4, h5, h6',
    356368        'h1'      => 'h1',
     
    366378    );
    367379
     380    const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array(
     381        'button'  => 'wp-element-button',
     382        'caption' => 'wp-element-caption',
     383    );
     384
     385    /**
     386     * Returns a class name by an element name.
     387     *
     388     * @since 6.1.0
     389     *
     390     * @param string $element The name of the element.
     391     * @return string The name of the class.
     392     */
     393    public static function get_element_class_name( $element ) {
     394        $class_name = '';
     395
     396        if ( array_key_exists( $element, static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES ) ) {
     397            $class_name = static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ];
     398        }
     399
     400        return $class_name;
     401    }
     402
    368403    /**
    369404     * Options that settings.appearanceTools enables.
     
    489524     */
    490525    protected static function sanitize( $input, $valid_block_names, $valid_element_names ) {
     526
    491527        $output = array();
    492528
     
    495531        }
    496532
     533        // Preserve only the top most level keys.
    497534        $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) );
    498535
    499         // Some styles are only meant to be available at the top-level (e.g.: blockGap),
    500         // hence, the schema for blocks & elements should not have them.
     536        /*
     537         * Remove any rules that are annotated as "top" in VALID_STYLES constant.
     538         * Some styles are only meant to be available at the top-level (e.g.: blockGap),
     539         * hence, the schema for blocks & elements should not have them.
     540         */
    501541        $styles_non_top_level = static::VALID_STYLES;
    502542        foreach ( array_keys( $styles_non_top_level ) as $section ) {
     
    511551        $schema                 = array();
    512552        $schema_styles_elements = array();
     553
     554        /*
     555         * Set allowed element pseudo selectors based on per element allow list.
     556         * Target data structure in schema:
     557         * e.g.
     558         * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
     559         * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
     560         */
    513561        foreach ( $valid_element_names as $element ) {
    514562            $schema_styles_elements[ $element ] = $styles_non_top_level;
    515         }
     563
     564            if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
     565                foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
     566                    $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level;
     567                }
     568            }
     569        }
     570
    516571        $schema_styles_blocks   = array();
    517572        $schema_settings_blocks = array();
     
    521576            $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
    522577        }
     578
    523579        $schema['styles']             = static::VALID_STYLES;
    524580        $schema['styles']['blocks']   = $schema_styles_blocks;
     
    548604
    549605        return $output;
     606    }
     607
     608    /**
     609     * Appends a sub-selector to an existing one.
     610     *
     611     * Given the compounded $selector "h1, h2, h3"
     612     * and the $to_append selector ".some-class" the result will be
     613     * "h1.some-class, h2.some-class, h3.some-class".
     614     *
     615     * @since 5.8.0
     616     * @since 6.1.0 Added append position.
     617     *
     618     * @param string $selector  Original selector.
     619     * @param string $to_append Selector to append.
     620     * @param string $position  A position sub-selector should be appended. Default 'right'.
     621     * @return string
     622     */
     623    protected static function append_to_selector( $selector, $to_append, $position = 'right' ) {
     624        $new_selectors = array();
     625        $selectors     = explode( ',', $selector );
     626        foreach ( $selectors as $sel ) {
     627            $new_selectors[] = 'right' === $position ? $sel . $to_append : $to_append . $sel;
     628        }
     629        return implode( ',', $new_selectors );
    550630    }
    551631
     
    612692                $element_selector = array();
    613693                foreach ( $block_selectors as $selector ) {
    614                     $element_selector[] = $selector . ' ' . $el_selector;
     694                    if ( $selector === $el_selector ) {
     695                        $element_selector = array( $el_selector );
     696                        break;
     697                    }
     698                    $element_selector[] = static::append_to_selector( $el_selector, $selector . ' ', 'left' );
    615699                }
    616700                static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector );
     
    811895                continue;
    812896            }
    813 
    814             $node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
    815             $selector     = $metadata['selector'];
    816             $settings     = _wp_array_get( $this->theme_json, array( 'settings' ) );
    817             $declarations = static::compute_style_properties( $node, $settings );
    818 
    819             // 1. Separate the ones who use the general selector
    820             // and the ones who use the duotone selector.
    821             $declarations_duotone = array();
    822             foreach ( $declarations as $index => $declaration ) {
    823                 if ( 'filter' === $declaration['name'] ) {
    824                     unset( $declarations[ $index ] );
    825                     $declarations_duotone[] = $declaration;
    826                 }
    827             }
    828 
    829             /*
    830              * Reset default browser margin on the root body element.
    831              * This is set on the root selector **before** generating the ruleset
    832              * from the `theme.json`. This is to ensure that if the `theme.json` declares
    833              * `margin` in its `spacing` declaration for the `body` element then these
    834              * user-generated values take precedence in the CSS cascade.
    835              * @link https://github.com/WordPress/gutenberg/issues/36147.
    836              */
    837             if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
    838                 $block_rules .= 'body { margin: 0; }';
    839             }
    840 
    841             // 2. Generate the rules that use the general selector.
    842             $block_rules .= static::to_ruleset( $selector, $declarations );
    843 
    844             // 3. Generate the rules that use the duotone selector.
    845             if ( isset( $metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
    846                 $selector_duotone = static::scope_selector( $metadata['selector'], $metadata['duotone'] );
    847                 $block_rules     .= static::to_ruleset( $selector_duotone, $declarations_duotone );
    848             }
    849 
    850             if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
    851                 $block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
    852                 $block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
    853                 $block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
    854 
    855                 $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
    856                 if ( $has_block_gap_support ) {
    857                     $block_rules .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }';
    858                     $block_rules .= '.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }';
    859                 }
    860             }
     897            $block_rules .= static::get_styles_for_block( $metadata );
    861898        }
    862899
     
    9711008
    9721009        return $selector . '{' . $declaration_block . '}';
    973     }
    974 
    975     /**
    976      * Function that appends a sub-selector to a existing one.
    977      *
    978      * Given the compounded $selector "h1, h2, h3"
    979      * and the $to_append selector ".some-class" the result will be
    980      * "h1.some-class, h2.some-class, h3.some-class".
    981      *
    982      * @since 5.8.0
    983      *
    984      * @param string $selector  Original selector.
    985      * @param string $to_append Selector to append.
    986      * @return string
    987      */
    988     protected static function append_to_selector( $selector, $to_append ) {
    989         $new_selectors = array();
    990         $selectors     = explode( ',', $selector );
    991         foreach ( $selectors as $sel ) {
    992             $new_selectors[] = $sel . $to_append;
    993         }
    994 
    995         return implode( ',', $new_selectors );
    9961010    }
    9971011
     
    13131327     * @since 5.8.0
    13141328     * @since 5.9.0 Added the `$settings` and `$properties` parameters.
     1329     * @since 6.1.0 Added the `$theme_json` parameter.
    13151330     *
    13161331     * @param array $styles    Styles to process.
    13171332     * @param array $settings  Theme settings.
    13181333     * @param array $properties Properties metadata.
     1334     * @param array $theme_json Theme JSON array.
    13191335     * @return array Returns the modified $declarations.
    13201336     */
    1321     protected static function compute_style_properties( $styles, $settings = array(), $properties = null ) {
     1337    protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null ) {
    13221338        if ( null === $properties ) {
    13231339            $properties = static::PROPERTIES_METADATA;
     
    13301346
    13311347        foreach ( $properties as $css_property => $value_path ) {
    1332             $value = static::get_property_value( $styles, $value_path );
     1348            $value = static::get_property_value( $styles, $value_path, $theme_json );
    13331349
    13341350            // Look up protected properties, keyed by value path.
     
    13661382     * "--wp--preset--color--secondary".
    13671383     *
     1384     * It also converts references to a path to the value
     1385     * stored at that location, e.g.
     1386     * { "ref": "style.color.background" } => "#fff".
     1387     *
    13681388     * @since 5.8.0
    13691389     * @since 5.9.0 Added support for values of array type, which are returned as is.
     1390     * @since 6.1.0 Added the `$theme_json` parameter.
    13701391     *
    13711392     * @param array $styles Styles subtree.
    13721393     * @param array $path   Which property to process.
     1394     * @param array $theme_json Theme JSON array.
    13731395     * @return string|array Style property value.
    13741396     */
    1375     protected static function get_property_value( $styles, $path ) {
     1397    protected static function get_property_value( $styles, $path, $theme_json = null ) {
    13761398        $value = _wp_array_get( $styles, $path, '' );
     1399
     1400        /*
     1401         * This converts references to a path to the value at that path
     1402         * where the values is an array with a "ref" key, pointing to a path.
     1403         * For example: { "ref": "style.color.background" } => "#fff".
     1404         */
     1405        if ( is_array( $value ) && array_key_exists( 'ref', $value ) ) {
     1406            $value_path = explode( '.', $value['ref'] );
     1407            $ref_value  = _wp_array_get( $theme_json, $value_path );
     1408            // Only use the ref value if we find anything.
     1409            if ( ! empty( $ref_value ) && is_string( $ref_value ) ) {
     1410                $value = $ref_value;
     1411            }
     1412
     1413            if ( is_array( $ref_value ) && array_key_exists( 'ref', $ref_value ) ) {
     1414                $path_string      = json_encode( $path );
     1415                $ref_value_string = json_encode( $ref_value );
     1416                _doing_it_wrong(
     1417                    'get_property_value',
     1418                    sprintf(
     1419                        /* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */
     1420                        __( 'Your %1$s file uses a dynamic value (%2$s) for the path at %3$s. However, the value at %3$s is also a dynamic value (pointing to %4$s) and pointing to another dynamic value is not supported. Please update %3$s to point directly to %4$s.' ),
     1421                        'theme.json',
     1422                        $ref_value_string,
     1423                        $path_string,
     1424                        $ref_value['ref']
     1425                    ),
     1426                    '6.1.0'
     1427                );
     1428            }
     1429        }
    13771430
    13781431        if ( '' === $value || is_array( $value ) ) {
     
    13801433        }
    13811434
     1435        // Convert custom CSS properties.
    13821436        $prefix     = 'var:';
    13831437        $prefix_len = strlen( $prefix );
     
    14911545                    'selector' => static::ELEMENTS[ $element ],
    14921546                );
    1493             }
     1547
     1548                // Handle any pseudo selectors for the element.
     1549                if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
     1550                    foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
     1551
     1552                        if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) {
     1553                            $nodes[] = array(
     1554                                'path'     => array( 'styles', 'elements', $element ),
     1555                                'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ),
     1556                            );
     1557                        }
     1558                    }
     1559                }
     1560            }
     1561        }
     1562
     1563        // Blocks.
     1564        if ( ! isset( $theme_json['styles']['blocks'] ) ) {
     1565            return $nodes;
     1566        }
     1567
     1568        $nodes = array_merge( $nodes, static::get_block_nodes( $theme_json ) );
     1569
     1570        /**
     1571         * Filters the list of style nodes with metadata.
     1572         *
     1573         * This allows for things like loading block CSS independently.
     1574         *
     1575         * @since 6.1.0
     1576         *
     1577         * @param array $nodes Style nodes with metadata.
     1578         */
     1579        return apply_filters( 'get_style_nodes', $nodes );
     1580    }
     1581
     1582    /**
     1583     * A public helper to get the block nodes from a theme.json file.
     1584     *
     1585     * @since 6.1.0
     1586     *
     1587     * @return array The block nodes in theme.json.
     1588     */
     1589    public function get_styles_block_nodes() {
     1590        return static::get_block_nodes( $this->theme_json );
     1591    }
     1592
     1593    /**
     1594     * An internal method to get the block nodes from a theme.json file.
     1595     *
     1596     * @since 6.1.0
     1597     *
     1598     * @param array $theme_json The theme.json converted to an array.
     1599     * @return array The block nodes in theme.json.
     1600     */
     1601    private static function get_block_nodes( $theme_json ) {
     1602        $selectors = static::get_blocks_metadata();
     1603        $nodes     = array();
     1604        if ( ! isset( $theme_json['styles'] ) ) {
     1605            return $nodes;
    14941606        }
    14951607
     
    15111623
    15121624            $nodes[] = array(
     1625                'name'     => $name,
    15131626                'path'     => array( 'styles', 'blocks', $name ),
    15141627                'selector' => $selector,
     
    15221635                        'selector' => $selectors[ $name ]['elements'][ $element ],
    15231636                    );
     1637
     1638                    // Handle any pseudo selectors for the element.
     1639                    if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
     1640                        foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
     1641                            if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
     1642                                $nodes[] = array(
     1643                                    'path'     => array( 'styles', 'blocks', $name, 'elements', $element ),
     1644                                    'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ),
     1645                                );
     1646                            }
     1647                        }
     1648                    }
    15241649                }
    15251650            }
     
    15271652
    15281653        return $nodes;
     1654    }
     1655
     1656    /**
     1657     * Gets the CSS rules for a particular block from theme.json.
     1658     *
     1659     * @since 6.1.0
     1660     *
     1661     * @param array $block_metadata Meta data about the block to get styles for.
     1662     * @return array Styles for the block.
     1663     */
     1664    public function get_styles_for_block( $block_metadata ) {
     1665
     1666        $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
     1667
     1668        $selector = $block_metadata['selector'];
     1669        $settings = _wp_array_get( $this->theme_json, array( 'settings' ) );
     1670
     1671        /*
     1672         * Get a reference to element name from path.
     1673         * $block_metadata['path'] = array( 'styles','elements','link' );
     1674         * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ].
     1675         * Skip non-element paths like just ['styles'].
     1676         */
     1677        $is_processing_element = in_array( 'elements', $block_metadata['path'], true );
     1678
     1679        $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null;
     1680
     1681        $element_pseudo_allowed = array();
     1682
     1683        if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
     1684            $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ];
     1685        }
     1686
     1687        /*
     1688         * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover").
     1689         * This also resets the array keys.
     1690         */
     1691        $pseudo_matches = array_values(
     1692            array_filter(
     1693                $element_pseudo_allowed,
     1694                function( $pseudo_selector ) use ( $selector ) {
     1695                    return str_contains( $selector, $pseudo_selector );
     1696                }
     1697            )
     1698        );
     1699
     1700        $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null;
     1701
     1702        /*
     1703         * If the current selector is a pseudo selector that's defined in the allow list for the current
     1704         * element then compute the style properties for it.
     1705         * Otherwise just compute the styles for the default selector as normal.
     1706         */
     1707        if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) &&
     1708            array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS )
     1709            && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true )
     1710        ) {
     1711            $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json );
     1712        } else {
     1713            $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json );
     1714        }
     1715
     1716        $block_rules = '';
     1717
     1718        /*
     1719         * 1. Separate the declarations that use the general selector
     1720         * from the ones using the duotone selector.
     1721         */
     1722        $declarations_duotone = array();
     1723        foreach ( $declarations as $index => $declaration ) {
     1724            if ( 'filter' === $declaration['name'] ) {
     1725                unset( $declarations[ $index ] );
     1726                $declarations_duotone[] = $declaration;
     1727            }
     1728        }
     1729
     1730        /*
     1731         * Reset default browser margin on the root body element.
     1732         * This is set on the root selector **before** generating the ruleset
     1733         * from the `theme.json`. This is to ensure that if the `theme.json` declares
     1734         * `margin` in its `spacing` declaration for the `body` element then these
     1735         * user-generated values take precedence in the CSS cascade.
     1736         * @link https://github.com/WordPress/gutenberg/issues/36147.
     1737         */
     1738        if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
     1739            $block_rules .= 'body { margin: 0; }';
     1740        }
     1741
     1742        // 2. Generate and append the rules that use the general selector.
     1743        $block_rules .= static::to_ruleset( $selector, $declarations );
     1744
     1745        // 3. Generate and append the rules that use the duotone selector.
     1746        if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
     1747            $selector_duotone = static::scope_selector( $block_metadata['selector'], $block_metadata['duotone'] );
     1748            $block_rules     .= static::to_ruleset( $selector_duotone, $declarations_duotone );
     1749        }
     1750
     1751        if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
     1752            $block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
     1753            $block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
     1754            $block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
     1755
     1756            $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
     1757            if ( $has_block_gap_support ) {
     1758                $block_rules .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }';
     1759                $block_rules .= '.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }';
     1760            }
     1761        }
     1762
     1763        return $block_rules;
    15291764    }
    15301765
     
    18382073        $valid_block_names   = array_keys( static::get_blocks_metadata() );
    18392074        $valid_element_names = array_keys( static::ELEMENTS );
    1840         $theme_json          = static::sanitize( $theme_json, $valid_block_names, $valid_element_names );
     2075
     2076        $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names );
    18412077
    18422078        $blocks_metadata = static::get_blocks_metadata();
    18432079        $style_nodes     = static::get_style_nodes( $theme_json, $blocks_metadata );
     2080
    18442081        foreach ( $style_nodes as $metadata ) {
    18452082            $input = _wp_array_get( $theme_json, $metadata['path'], array() );
     
    18492086
    18502087            $output = static::remove_insecure_styles( $input );
     2088
     2089            /*
     2090             * Get a reference to element name from path.
     2091             * $metadata['path'] = array( 'styles', 'elements', 'link' );
     2092             */
     2093            $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ];
     2094
     2095            /*
     2096             * $output is stripped of pseudo selectors. Re-add and process them
     2097             * or insecure styles here.
     2098             */
     2099            if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
     2100                foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) {
     2101                    if ( isset( $input[ $pseudo_selector ] ) ) {
     2102                        $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] );
     2103                    }
     2104                }
     2105            }
     2106
    18512107            if ( ! empty( $output ) ) {
    18522108                _wp_array_set( $sanitized, $metadata['path'], $output );
Note: See TracChangeset for help on using the changeset viewer.