Make WordPress Core

Changeset 52049


Ignore:
Timestamp:
11/08/2021 07:18:39 PM (15 months ago)
Author:
jorgefilipecosta
Message:

Update theme.json classes for WordPress 5.9.

This commit ports to core the changes to the classes that deal with theme.json code.

See #54336.
Props oandregal, spacedmonkey, noisysocks, hellofromtonya, youknowriad.

Location:
trunk
Files:
5 added
15 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/block-editor.php

    r52042 r52049  
    304304    );
    305305
    306     $theme_json = WP_Theme_JSON_Resolver::get_merged_data( $editor_settings );
     306    $theme_json = WP_Theme_JSON_Resolver::get_merged_data();
    307307
    308308    if ( WP_Theme_JSON_Resolver::theme_has_support() ) {
    309309        $editor_settings['styles'][] = array(
    310             'css'            => $theme_json->get_stylesheet( 'block_styles' ),
     310            'css'            => $theme_json->get_stylesheet( array( 'styles', 'presets' ) ),
    311311            '__unstableType' => 'globalStyles',
    312312        );
    313313        $editor_settings['styles'][] = array(
    314             'css'                     => $theme_json->get_stylesheet( 'css_variables' ),
     314            'css'                     => $theme_json->get_stylesheet( array( 'variables' ) ),
    315315            '__experimentalNoWrapper' => true,
    316316            '__unstableType'          => 'globalStyles',
     
    359359        unset( $editor_settings['__experimentalFeatures']['typography']['customFontSize'] );
    360360    }
    361     if ( isset( $editor_settings['__experimentalFeatures']['typography']['customLineHeight'] ) ) {
    362         $editor_settings['enableCustomLineHeight'] = $editor_settings['__experimentalFeatures']['typography']['customLineHeight'];
    363         unset( $editor_settings['__experimentalFeatures']['typography']['customLineHeight'] );
     361    if ( isset( $editor_settings['__experimentalFeatures']['typography']['lineHeight'] ) ) {
     362        $editor_settings['enableCustomLineHeight'] = $editor_settings['__experimentalFeatures']['typography']['lineHeight'];
     363        unset( $editor_settings['__experimentalFeatures']['typography']['lineHeight'] );
    364364    }
    365365    if ( isset( $editor_settings['__experimentalFeatures']['spacing']['units'] ) ) {
     
    367367        unset( $editor_settings['__experimentalFeatures']['spacing']['units'] );
    368368    }
    369     if ( isset( $editor_settings['__experimentalFeatures']['spacing']['customPadding'] ) ) {
    370         $editor_settings['enableCustomSpacing'] = $editor_settings['__experimentalFeatures']['spacing']['customPadding'];
    371         unset( $editor_settings['__experimentalFeatures']['spacing']['customPadding'] );
     369    if ( isset( $editor_settings['__experimentalFeatures']['spacing']['padding'] ) ) {
     370        $editor_settings['enableCustomSpacing'] = $editor_settings['__experimentalFeatures']['spacing']['padding'];
     371        unset( $editor_settings['__experimentalFeatures']['spacing']['padding'] );
    372372    }
    373373
  • trunk/src/wp-includes/block-supports/duotone.php

    r51657 r52049  
    6868    // Convert into [0, 1] range if it isn't already.
    6969    return ( $n % $max ) / (float) $max;
     70}
     71
     72/**
     73 * Direct port of tinycolor's boundAlpha function to maintain consistency with
     74 * how tinycolor works.
     75 *
     76 * @see https://github.com/bgrins/TinyColor
     77 *
     78 * @since 5.9.0
     79 * @access private
     80 *
     81 * @param  mixed $n   Number of unknown type.
     82 * @return float      Value in the range [0,1].
     83 */
     84function _wp_tinycolor_bound_alpha( $n ) {
     85    if ( is_numeric( $n ) ) {
     86        $n = (float) $n;
     87        if ( $n >= 0 && $n <= 1 ) {
     88            return $n;
     89        }
     90    }
     91    return 1;
    7092}
    7193
     
    171193/**
    172194 * Parses hex, hsl, and rgb CSS strings using the same regex as TinyColor v1.4.2
    173  * used in the JavaScript. Only colors output from react-color are implemented
    174  * and the alpha value is ignored as it is not used in duotone.
     195 * used in the JavaScript. Only colors output from react-color are implemented.
    175196 *
    176197 * Direct port of TinyColor's function, lightly simplified to maintain
     
    181202 *
    182203 * @since 5.8.0
     204 * @since 5.9.0 Added alpha processing.
    183205 * @access private
    184206 *
     
    200222    $rgb_regexp = '/^rgb' . $permissive_match3 . '$/';
    201223    if ( preg_match( $rgb_regexp, $color_str, $match ) ) {
    202         return wp_tinycolor_rgb_to_rgb(
     224        $rgb = wp_tinycolor_rgb_to_rgb(
    203225            array(
    204226                'r' => $match[1],
     
    207229            )
    208230        );
     231
     232        $rgb['a'] = 1;
     233
     234        return $rgb;
    209235    }
    210236
    211237    $rgba_regexp = '/^rgba' . $permissive_match4 . '$/';
    212238    if ( preg_match( $rgba_regexp, $color_str, $match ) ) {
    213         return wp_tinycolor_rgb_to_rgb(
     239        $rgb = wp_tinycolor_rgb_to_rgb(
    214240            array(
    215241                'r' => $match[1],
     
    218244            )
    219245        );
     246
     247        $rgb['a'] = _wp_tinycolor_bound_alpha( $match[4] );
     248
     249        return $rgb;
    220250    }
    221251
    222252    $hsl_regexp = '/^hsl' . $permissive_match3 . '$/';
    223253    if ( preg_match( $hsl_regexp, $color_str, $match ) ) {
    224         return wp_tinycolor_hsl_to_rgb(
     254        $rgb = wp_tinycolor_hsl_to_rgb(
    225255            array(
    226256                'h' => $match[1],
     
    229259            )
    230260        );
     261
     262        $rgb['a'] = 1;
     263
     264        return $rgb;
    231265    }
    232266
     
    240274            )
    241275        );
     276
     277        $rgb['a'] = _wp_tinycolor_bound_alpha( $match[4] );
     278
     279        return $rgb;
    242280    }
    243281
    244282    $hex8_regexp = '/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/';
    245283    if ( preg_match( $hex8_regexp, $color_str, $match ) ) {
    246         return wp_tinycolor_rgb_to_rgb(
     284        $rgb = wp_tinycolor_rgb_to_rgb(
    247285            array(
    248286                'r' => base_convert( $match[1], 16, 10 ),
     
    251289            )
    252290        );
     291
     292        $rgb['a'] = _wp_tinycolor_bound_alpha(
     293            base_convert( $match[4], 16, 10 ) / 255
     294        );
     295
     296        return $rgb;
    253297    }
    254298
    255299    $hex6_regexp = '/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/';
    256300    if ( preg_match( $hex6_regexp, $color_str, $match ) ) {
    257         return wp_tinycolor_rgb_to_rgb(
     301        $rgb = wp_tinycolor_rgb_to_rgb(
    258302            array(
    259303                'r' => base_convert( $match[1], 16, 10 ),
     
    262306            )
    263307        );
     308
     309        $rgb['a'] = 1;
     310
     311        return $rgb;
    264312    }
    265313
    266314    $hex4_regexp = '/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/';
    267315    if ( preg_match( $hex4_regexp, $color_str, $match ) ) {
    268         return wp_tinycolor_rgb_to_rgb(
     316        $rgb = wp_tinycolor_rgb_to_rgb(
    269317            array(
    270318                'r' => base_convert( $match[1] . $match[1], 16, 10 ),
     
    273321            )
    274322        );
     323
     324        $rgb['a'] = _wp_tinycolor_bound_alpha(
     325            base_convert( $match[4] . $match[4], 16, 10 ) / 255
     326        );
     327
     328        return $rgb;
    275329    }
    276330
    277331    $hex3_regexp = '/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/';
    278332    if ( preg_match( $hex3_regexp, $color_str, $match ) ) {
    279         return wp_tinycolor_rgb_to_rgb(
     333        $rgb = wp_tinycolor_rgb_to_rgb(
    280334            array(
    281335                'r' => base_convert( $match[1] . $match[1], 16, 10 ),
     
    283337                'b' => base_convert( $match[3] . $match[3], 16, 10 ),
    284338            )
     339        );
     340
     341        $rgb['a'] = 1;
     342
     343        return $rgb;
     344    }
     345
     346    /*
     347     * The JS color picker considers the string "transparent" to be a hex value,
     348     * so we need to handle it here as a special case.
     349     */
     350    if ( 'transparent' === $color_str ) {
     351        return array(
     352            'r' => 0,
     353            'g' => 0,
     354            'b' => 0,
     355            'a' => 0,
    285356        );
    286357    }
     
    313384        }
    314385    }
     386}
     387/**
     388 * Renders the duotone filter SVG and returns the CSS filter property to
     389 * reference the rendered SVG.
     390 *
     391 * @since 5.9.0
     392 *
     393 * @param array $preset Duotone preset value as seen in theme.json.
     394 * @return string Duotone CSS filter property.
     395 */
     396function wp_render_duotone_filter_preset( $preset ) {
     397    $duotone_id     = $preset['slug'];
     398    $duotone_colors = $preset['colors'];
     399    $filter_id      = 'wp-duotone-' . $duotone_id;
     400    $duotone_values = array(
     401        'r' => array(),
     402        'g' => array(),
     403        'b' => array(),
     404        'a' => array(),
     405    );
     406    foreach ( $duotone_colors as $color_str ) {
     407        $color = wp_tinycolor_string_to_rgb( $color_str );
     408
     409        $duotone_values['r'][] = $color['r'] / 255;
     410        $duotone_values['g'][] = $color['g'] / 255;
     411        $duotone_values['b'][] = $color['b'] / 255;
     412        $duotone_values['a'][] = $color['a'];
     413    }
     414
     415    ob_start();
     416
     417    ?>
     418
     419    <svg
     420        xmlns="http://www.w3.org/2000/svg"
     421        viewBox="0 0 0 0"
     422        width="0"
     423        height="0"
     424        focusable="false"
     425        role="none"
     426        style="visibility: hidden; position: absolute; left: -9999px; overflow: hidden;"
     427    >
     428        <defs>
     429            <filter id="<?php echo esc_attr( $filter_id ); ?>">
     430                <feColorMatrix
     431                    color-interpolation-filters="sRGB"
     432                    type="matrix"
     433                    values="
     434                        .299 .587 .114 0 0
     435                        .299 .587 .114 0 0
     436                        .299 .587 .114 0 0
     437                        .299 .587 .114 0 0
     438                    "
     439                />
     440                <feComponentTransfer color-interpolation-filters="sRGB" >
     441                    <feFuncR type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['r'] ) ); ?>" />
     442                    <feFuncG type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['g'] ) ); ?>" />
     443                    <feFuncB type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['b'] ) ); ?>" />
     444                    <feFuncA type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['a'] ) ); ?>" />
     445                </feComponentTransfer>
     446                <feComposite in2="SourceGraphic" operator="in" />
     447            </filter>
     448        </defs>
     449    </svg>
     450
     451    <?php
     452
     453    $svg = ob_get_clean();
     454
     455    if ( ! defined( 'SCRIPT_DEBUG' ) || ! SCRIPT_DEBUG ) {
     456        // Clean up the whitespace.
     457        $svg = preg_replace( "/[\r\n\t ]+/", ' ', $svg );
     458        $svg = preg_replace( '/> </', '><', $svg );
     459        $svg = trim( $svg );
     460    }
     461
     462    add_action(
     463        /*
     464         * Safari doesn't render SVG filters defined in data URIs,
     465         * and SVG filters won't render in the head of a document,
     466         * so the next best place to put the SVG is in the footer.
     467         */
     468        is_admin() ? 'admin_footer' : 'wp_footer',
     469        static function () use ( $svg ) {
     470            echo $svg;
     471        }
     472    );
     473
     474    return "url('#" . $filter_id . "')";
    315475}
    316476
  • trunk/src/wp-includes/class-wp-theme-json-resolver.php

    r51693 r52049  
    1212 * for site-level config and offers an API to work with them.
    1313 *
     14 * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes).
     15 * This is a low-level API that may need to do breaking changes. Please,
     16 * use get_global_settings, get_global_styles, and get_global_stylesheet instead.
     17 *
    1418 * @access private
    1519 */
     
    4145
    4246    /**
     47     * Container for data coming from the user.
     48     *
     49     * @since 5.9.0
     50     * @var WP_Theme_JSON
     51     */
     52    private static $user = null;
     53
     54    /**
     55     * Stores the ID of the custom post type
     56     * that holds the user data.
     57     *
     58     * @since 5.9.0
     59     * @var integer
     60     */
     61    private static $user_custom_post_type_id = null;
     62
     63    /**
    4364     * Container to keep loaded i18n schema for `theme.json`.
    4465     *
    45      * @since 5.9.0
     66     * @since 5.8.0
     67     * @since 5.9.0 Renamed from $theme_json_i18n
    4668     * @var array
    4769     */
     
    123145     * Returns the theme's data.
    124146     *
    125      * Data from theme.json can be augmented via the $theme_support_data variable.
    126      * This is useful, for example, to backfill the gaps in theme.json that a theme
    127      * has declared via add_theme_supports.
    128      *
    129      * Note that if the same data is present in theme.json and in $theme_support_data,
    130      * the theme.json's is not overwritten.
    131      *
    132      * @since 5.8.0
    133      *
    134      * @param array $theme_support_data Optional. Theme support data in theme.json format.
    135      *                                  Default empty array.
     147     * Data from theme.json will be backfilled from existing
     148     * theme supports, if any. Note that if the same data
     149     * is present in theme.json and in theme supports,
     150     * the theme.json takes precendence.
     151     *
     152     * @since 5.8.0
     153     * @since 5.9.0 Theme supports have been inlined and the argument removed.
     154     *
    136155     * @return WP_Theme_JSON Entity that holds theme data.
    137156     */
    138     public static function get_theme_data( $theme_support_data = array() ) {
     157    public static function get_theme_data( $deprecated = array() ) {
     158        if ( ! empty( $deprecated ) ) {
     159            _deprecated_argument( __METHOD__, '5.9' );
     160        }
    139161        if ( null === self::$theme ) {
    140162            $theme_json_data = self::read_json_file( self::get_file_path_from_theme( 'theme.json' ) );
    141163            $theme_json_data = self::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) );
    142164            self::$theme     = new WP_Theme_JSON( $theme_json_data );
    143         }
    144 
    145         if ( empty( $theme_support_data ) ) {
    146             return self::$theme;
     165
     166            if ( wp_get_theme()->parent() ) {
     167                // Get parent theme.json.
     168                $parent_theme_json_data = self::read_json_file( self::get_file_path_from_theme( 'theme.json', true ) );
     169                $parent_theme_json_data = self::translate( $parent_theme_json_data, wp_get_theme()->parent()->get( 'TextDomain' ) );
     170                $parent_theme           = new WP_Theme_JSON( $parent_theme_json_data );
     171
     172                // Merge the child theme.json into the parent theme.json.
     173                // The child theme takes precedence over the parent.
     174                $parent_theme->merge( self::$theme );
     175                self::$theme = $parent_theme;
     176            }
    147177        }
    148178
    149179        /*
    150          * We want the presets and settings declared in theme.json
    151          * to override the ones declared via add_theme_support.
    152          */
     180        * We want the presets and settings declared in theme.json
     181        * to override the ones declared via theme supports.
     182        * So we take theme supports, transform it to theme.json shape
     183        * and merge the self::$theme upon that.
     184        */
     185        $theme_support_data  = WP_Theme_JSON::get_from_editor_settings( get_default_block_editor_settings() );
    153186        $with_theme_supports = new WP_Theme_JSON( $theme_support_data );
    154187        $with_theme_supports->merge( self::$theme );
     
    158191
    159192    /**
    160      * There are different sources of data for a site: core and theme.
    161      *
    162      * While the getters {@link get_core_data}, {@link get_theme_data} return the raw data
    163      * from the respective origins, this method merges them all together.
    164      *
    165      * If the same piece of data is declared in different origins (core and theme),
    166      * the last origin overrides the previous. For example, if core disables custom colors
    167      * but a theme enables them, the theme config wins.
    168      *
    169      * @since 5.8.0
    170      *
    171      * @param array $settings Optional. Existing block editor settings. Default empty array.
     193     * Returns the CPT that contains the user's origin config
     194     * for the current theme or a void array if none found.
     195     *
     196     * It can also create and return a new draft CPT.
     197     *
     198     * @since 5.9.0
     199     *
     200     * @param bool  $should_create_cpt  Optional. Whether a new CPT should be created if no one was found.
     201     *                                  False by default.
     202     * @param array $post_status_filter Filter Optional. CPT by post status.
     203     *                                   ['publish'] by default, so it only fetches published posts.
     204     *
     205     * @return array Custom Post Type for the user's origin config.
     206     */
     207    private static function get_user_data_from_custom_post_type( $should_create_cpt = false, $post_status_filter = array( 'publish' ) ) {
     208        $user_cpt         = array();
     209        $post_type_filter = 'wp_global_styles';
     210        $query            = new WP_Query(
     211            array(
     212                'posts_per_page' => 1,
     213                'orderby'        => 'date',
     214                'order'          => 'desc',
     215                'post_type'      => $post_type_filter,
     216                'post_status'    => $post_status_filter,
     217                'tax_query'      => array(
     218                    array(
     219                        'taxonomy' => 'wp_theme',
     220                        'field'    => 'name',
     221                        'terms'    => wp_get_theme()->get_stylesheet(),
     222                    ),
     223                ),
     224            )
     225        );
     226
     227        if ( is_array( $query->posts ) && ( 1 === $query->post_count ) ) {
     228            $user_cpt = $query->posts[0]->to_array();
     229        } elseif ( $should_create_cpt ) {
     230            $cpt_post_id = wp_insert_post(
     231                array(
     232                    'post_content' => '{"version": ' . WP_Theme_JSON::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }',
     233                    'post_status'  => 'publish',
     234                    'post_title'   => __( 'Custom Styles', 'default' ),
     235                    'post_type'    => $post_type_filter,
     236                    'post_name'    => 'wp-global-styles-' . urlencode( wp_get_theme()->get_stylesheet() ),
     237                    'tax_input'    => array(
     238                        'wp_theme' => array( wp_get_theme()->get_stylesheet() ),
     239                    ),
     240                ),
     241                true
     242            );
     243
     244            if ( is_wp_error( $cpt_post_id ) ) {
     245                $user_cpt = array();
     246            } else {
     247                $user_cpt = get_post( $cpt_post_id, ARRAY_A );
     248            }
     249        }
     250
     251        return $user_cpt;
     252    }
     253
     254    /**
     255     * Returns the user's origin config.
     256     *
     257     * @since 5.9.0
     258     *
     259     * @return WP_Theme_JSON Entity that holds user data.
     260     */
     261    public static function get_user_data() {
     262        if ( null !== self::$user ) {
     263            return self::$user;
     264        }
     265
     266        $config   = array();
     267        $user_cpt = self::get_user_data_from_custom_post_type();
     268
     269        if ( array_key_exists( 'post_content', $user_cpt ) ) {
     270            $decoded_data = json_decode( $user_cpt['post_content'], true );
     271
     272            $json_decoding_error = json_last_error();
     273            if ( JSON_ERROR_NONE !== $json_decoding_error ) {
     274                trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() );
     275                return new WP_Theme_JSON( $config, 'user' );
     276            }
     277
     278            // Very important to verify if the flag isGlobalStylesUserThemeJSON is true.
     279            // If is not true the content was not escaped and is not safe.
     280            if (
     281                is_array( $decoded_data ) &&
     282                isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
     283                $decoded_data['isGlobalStylesUserThemeJSON']
     284            ) {
     285                unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
     286                $config = $decoded_data;
     287            }
     288        }
     289        self::$user = new WP_Theme_JSON( $config, 'user' );
     290
     291        return self::$user;
     292    }
     293
     294    /**
     295     * There are three sources of data (origins) for a site:
     296     * core, theme, and user. The user's has higher priority
     297     * than the theme's, and the theme's higher than core's.
     298     *
     299     * Unlike the getters {@link get_core_data},
     300     * {@link get_theme_data}, and {@link get_user_data},
     301     * this method returns data after it has been merged
     302     * with the previous origins. This means that if the same piece of data
     303     * is declared in different origins (user, theme, and core),
     304     * the last origin overrides the previous.
     305     *
     306     * For example, if the user has set a background color
     307     * for the paragraph block, and the theme has done it as well,
     308     * the user preference wins.
     309     *
     310     * @since 5.8.0
     311     * @since 5.9.0 Add user data and change the arguments.
     312     *
     313     * @param string $origin Optional. To what level should we merge data.
     314     *                       Valid values are 'theme' or 'user'.
     315     *                       Default is 'user'.
    172316     * @return WP_Theme_JSON
    173317     */
    174     public static function get_merged_data( $settings = array() ) {
    175         $theme_support_data = WP_Theme_JSON::get_from_editor_settings( $settings );
     318    public static function get_merged_data( $origin = 'user' ) {
     319        if ( is_array( $origin ) ) {
     320            _deprecated_argument( __FUNCTION__, '5.9' );
     321        }
    176322
    177323        $result = new WP_Theme_JSON();
    178324        $result->merge( self::get_core_data() );
    179         $result->merge( self::get_theme_data( $theme_support_data ) );
     325        $result->merge( self::get_theme_data() );
     326
     327        if ( 'user' === $origin ) {
     328            $result->merge( self::get_user_data() );
     329        }
    180330
    181331        return $result;
     
    183333
    184334    /**
     335     * Returns the ID of the custom post type
     336     * that stores user data.
     337     *
     338     * @since 5.9.0
     339     *
     340     * @return integer|null
     341     */
     342    public static function get_user_custom_post_type_id() {
     343        if ( null !== self::$user_custom_post_type_id ) {
     344            return self::$user_custom_post_type_id;
     345        }
     346
     347        $user_cpt = self::get_user_data_from_custom_post_type( true );
     348
     349        if ( array_key_exists( 'ID', $user_cpt ) ) {
     350            self::$user_custom_post_type_id = $user_cpt['ID'];
     351        }
     352
     353        return self::$user_custom_post_type_id;
     354    }
     355
     356    /**
    185357     * Whether the current theme has a theme.json file.
    186358     *
    187359     * @since 5.8.0
     360     * @since 5.9.0 Also check in the parent theme.
    188361     *
    189362     * @return bool
     
    191364    public static function theme_has_support() {
    192365        if ( ! isset( self::$theme_has_support ) ) {
    193             self::$theme_has_support = (bool) self::get_file_path_from_theme( 'theme.json' );
     366            self::$theme_has_support = is_readable( get_theme_file_path( 'theme.json' ) );
    194367        }
    195368
     
    203376     *
    204377     * @since 5.8.0
     378     * @since 5.9.0 Adapt to work with child themes.
    205379     *
    206380     * @param string $file_name Name of the file.
     381     * @param bool   $template  Optional. Use template theme directory. Default false.
    207382     * @return string The whole file path or empty if the file doesn't exist.
    208383     */
    209     private static function get_file_path_from_theme( $file_name ) {
    210         /*
    211          * This used to be a locate_template call. However, that method proved problematic
    212          * due to its use of constants (STYLESHEETPATH) that threw errors in some scenarios.
    213          *
    214          * When the theme.json merge algorithm properly supports child themes,
    215          * this should also fall back to the template path, as locate_template did.
    216          */
    217         $located   = '';
    218         $candidate = get_stylesheet_directory() . '/' . $file_name;
    219         if ( is_readable( $candidate ) ) {
    220             $located = $candidate;
    221         }
    222         return $located;
     384    private static function get_file_path_from_theme( $file_name, $template = false ) {
     385        $path      = $template ? get_template_directory() : get_stylesheet_directory();
     386        $candidate = $path . '/' . $file_name;
     387
     388        return is_readable( $candidate ) ? $candidate : '';
    223389    }
    224390
     
    227393     *
    228394     * @since 5.8.0
     395     * @since 5.9.0 Added new variables to reset.
    229396     */
    230397    public static function clean_cached_data() {
    231         self::$core              = null;
    232         self::$theme             = null;
    233         self::$theme_has_support = null;
     398        self::$core                     = null;
     399        self::$theme                    = null;
     400        self::$user                     = null;
     401        self::$user_custom_post_type_id = null;
     402        self::$theme_has_support        = null;
     403        self::$i18n_schema              = null;
    234404    }
    235405
  • trunk/src/wp-includes/class-wp-theme-json.php

    r51657 r52049  
    1010/**
    1111 * Class that encapsulates the processing of structures that adhere to the theme.json spec.
     12 *
     13 * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes).
     14 * This is a low-level API that may need to do breaking changes. Please,
     15 * use get_global_settings, get_global_styles, and get_global_stylesheet instead.
    1216 *
    1317 * @access private
     
    7175     * This contains the necessary metadata to process them:
    7276     *
    73      * - path          => where to find the preset within the settings section
    74      *
    75      * - value_key     => the key that represents the value
    76      *
    77      * - css_var_infix => infix to use in generating the CSS Custom Property. Example:
    78      *                   --wp--preset--<preset_infix>--<slug>: <preset_value>
    79      *
    80      * - classes      => array containing a structure with the classes to
    81      *                   generate for the presets. Each class should have
    82      *                   the class suffix and the property name. Example:
    83      *
    84      *                   .has-<slug>-<class_suffix> {
    85      *                       <property_name>: <preset_value>
    86      *                   }
    87      *
    88      * @since 5.8.0
     77     * - path       => where to find the preset within the settings section
     78     * - value_key  => the key that represents the value
     79     * - value_func => the callback to render the value (either value_key or value_func should be present)
     80     * - css_vars   => template string to use in generating the CSS Custom Property.
     81     *                 Example output: "--wp--preset--duotone--blue: <value>" will generate as many CSS Custom Properties as presets defined
     82     *                 substituting the $slug for the slug's value for each preset value.
     83     * - classes    => array containing a structure with the classes to generate for the presets.
     84     *                 Each key is a template string to resolve similarly to the css_vars and each value is the CSS property to use for that class.
     85     *                 Example output: ".has-blue-color { color: <value> }"
     86     * - properties  => a list of CSS properties to be used by kses to check the preset value is safe.
     87     *
     88     * @since 5.8.0
     89     * @since 5.9.0 Added new presets and simplified the metadata structure.
    8990     * @var array
    9091     */
    9192    const PRESETS_METADATA = array(
    9293        array(
    93             'path'          => array( 'color', 'palette' ),
    94             'value_key'     => 'color',
    95             'css_var_infix' => 'color',
    96             'classes'       => array(
    97                 array(
    98                     'class_suffix'  => 'color',
    99                     'property_name' => 'color',
    100                 ),
    101                 array(
    102                     'class_suffix'  => 'background-color',
    103                     'property_name' => 'background-color',
    104                 ),
     94            'path'       => array( 'color', 'palette' ),
     95            'value_key'  => 'color',
     96            'css_vars'   => '--wp--preset--color--$slug',
     97            'classes'    => array(
     98                '.has-$slug-color'            => 'color',
     99                '.has-$slug-background-color' => 'background-color',
     100                '.has-$slug-border-color'     => 'border-color',
    105101            ),
     102            'properties' => array( 'color', 'background-color', 'border-color' ),
    106103        ),
    107104        array(
    108             'path'          => array( 'color', 'gradients' ),
    109             'value_key'     => 'gradient',
    110             'css_var_infix' => 'gradient',
    111             'classes'       => array(
    112                 array(
    113                     'class_suffix'  => 'gradient-background',
    114                     'property_name' => 'background',
    115                 ),
    116             ),
     105            'path'       => array( 'color', 'gradients' ),
     106            'value_key'  => 'gradient',
     107            'css_vars'   => '--wp--preset--gradient--$slug',
     108            'classes'    => array( '.has-$slug-gradient-background' => 'background' ),
     109            'properties' => array( 'background' ),
    117110        ),
    118111        array(
    119             'path'          => array( 'typography', 'fontSizes' ),
    120             'value_key'     => 'size',
    121             'css_var_infix' => 'font-size',
    122             'classes'       => array(
    123                 array(
    124                     'class_suffix'  => 'font-size',
    125                     'property_name' => 'font-size',
    126                 ),
    127             ),
     112            'path'       => array( 'color', 'duotone' ),
     113            'value_func' => 'wp_render_duotone_filter_preset',
     114            'css_vars'   => '--wp--preset--duotone--$slug',
     115            'classes'    => array(),
     116            'properties' => array( 'filter' ),
     117        ),
     118        array(
     119            'path'       => array( 'typography', 'fontSizes' ),
     120            'value_key'  => 'size',
     121            'css_vars'   => '--wp--preset--font-size--$slug',
     122            'classes'    => array( '.has-$slug-font-size' => 'font-size' ),
     123            'properties' => array( 'font-size' ),
     124        ),
     125        array(
     126            'path'       => array( 'typography', 'fontFamilies' ),
     127            'value_key'  => 'fontFamily',
     128            'css_vars'   => '--wp--preset--font-family--$slug',
     129            'classes'    => array( '.has-$slug-font-family' => 'font-family' ),
     130            'properties' => array( 'font-family' ),
    128131        ),
    129132    );
     
    132135     * Metadata for style properties.
    133136     *
    134      * Each property declares:
    135      *
    136      * - 'value': path to the value in theme.json and block attributes.
    137      *
    138      * @since 5.8.0
     137     * Each element is a direct mapping from the CSS property name to the
     138     * path to the value in theme.json & block attributes.
     139     *
     140     * @since 5.8.0
     141     * @since 5.9.0 Added new properties and simplified the metadata structure.
    139142     * @var array
    140143     */
    141144    const PROPERTIES_METADATA = array(
    142         'background'       => array(
    143             'value' => array( 'color', 'gradient' ),
    144         ),
    145         'background-color' => array(
    146             'value' => array( 'color', 'background' ),
    147         ),
    148         'color'            => array(
    149             'value' => array( 'color', 'text' ),
    150         ),
    151         'font-size'        => array(
    152             'value' => array( 'typography', 'fontSize' ),
    153         ),
    154         'line-height'      => array(
    155             'value' => array( 'typography', 'lineHeight' ),
    156         ),
    157         'margin'           => array(
    158             'value'      => array( 'spacing', 'margin' ),
    159             'properties' => array( 'top', 'right', 'bottom', 'left' ),
    160         ),
    161         'padding'          => array(
    162             'value'      => array( 'spacing', 'padding' ),
    163             'properties' => array( 'top', 'right', 'bottom', 'left' ),
    164         ),
     145        'background'                 => array( 'color', 'gradient' ),
     146        'background-color'           => array( 'color', 'background' ),
     147        'border-radius'              => array( 'border', 'radius' ),
     148        'border-top-left-radius'     => array( 'border', 'radius', 'topLeft' ),
     149        'border-top-right-radius'    => array( 'border', 'radius', 'topRight' ),
     150        'border-bottom-left-radius'  => array( 'border', 'radius', 'bottomLeft' ),
     151        'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ),
     152        'border-color'               => array( 'border', 'color' ),
     153        'border-width'               => array( 'border', 'width' ),
     154        'border-style'               => array( 'border', 'style' ),
     155        'color'                      => array( 'color', 'text' ),
     156        'font-family'                => array( 'typography', 'fontFamily' ),
     157        'font-size'                  => array( 'typography', 'fontSize' ),
     158        'font-style'                 => array( 'typography', 'fontStyle' ),
     159        'font-weight'                => array( 'typography', 'fontWeight' ),
     160        'letter-spacing'             => array( 'typography', 'letterSpacing' ),
     161        'line-height'                => array( 'typography', 'lineHeight' ),
     162        'margin'                     => array( 'spacing', 'margin' ),
     163        'margin-top'                 => array( 'spacing', 'margin', 'top' ),
     164        'margin-right'               => array( 'spacing', 'margin', 'right' ),
     165        'margin-bottom'              => array( 'spacing', 'margin', 'bottom' ),
     166        'margin-left'                => array( 'spacing', 'margin', 'left' ),
     167        'padding'                    => array( 'spacing', 'padding' ),
     168        'padding-top'                => array( 'spacing', 'padding', 'top' ),
     169        'padding-right'              => array( 'spacing', 'padding', 'right' ),
     170        'padding-bottom'             => array( 'spacing', 'padding', 'bottom' ),
     171        'padding-left'               => array( 'spacing', 'padding', 'left' ),
     172        '--wp--style--block-gap'     => array( 'spacing', 'blockGap' ),
     173        'text-decoration'            => array( 'typography', 'textDecoration' ),
     174        'text-transform'             => array( 'typography', 'textTransform' ),
     175        'filter'                     => array( 'filter', 'duotone' ),
    165176    );
    166177
    167178    /**
    168      * @since 5.8.0
     179     * Protected style properties.
     180     *
     181     * These style properties are only rendered if a setting enables it
     182     * via a value other than `null`.
     183     *
     184     * Each element maps the style property to the corresponding theme.json
     185     * setting key.
     186     *
     187     * @since 5.9.0
     188     */
     189    const PROTECTED_PROPERTIES = array(
     190        'spacing.blockGap' => array( 'spacing', 'blockGap' ),
     191    );
     192
     193    /**
     194     * The top-level keys a theme.json can have.
     195     *
     196     * @since 5.8.0
     197     * @since 5.9.0 Renamed from ALLOWED_TOP_LEVEL_KEYS and added new values.
    169198     * @var string[]
    170199     */
    171     const ALLOWED_TOP_LEVEL_KEYS = array(
     200    const VALID_TOP_LEVEL_KEYS = array(
     201        'customTemplates',
    172202        'settings',
    173203        'styles',
     204        'templateParts',
    174205        'version',
    175206    );
    176207
    177208    /**
    178      * @since 5.8.0
     209     * The valid properties under the settings key.
     210     *
     211     * @since 5.8.0
     212     * @since 5.9.0 Renamed from ALLOWED_SETTINGS, gained new properties, and renamed others according to the new schema.
    179213     * @var array
    180214     */
    181     const ALLOWED_SETTINGS = array(
     215    const VALID_SETTINGS = array(
    182216        'border'     => array(
    183             'customRadius' => null,
     217            'color'  => null,
     218            'radius' => null,
     219            'style'  => null,
     220            'width'  => null,
    184221        ),
    185222        'color'      => array(
     223            'background'     => null,
    186224            'custom'         => null,
    187225            'customDuotone'  => null,
     
    191229            'link'           => null,
    192230            'palette'        => null,
     231            'text'           => null,
    193232        ),
    194233        'custom'     => null,
     
    198237        ),
    199238        'spacing'    => array(
    200             'customMargin'  => null,
    201             'customPadding' => null,
    202             'units'         => null,
     239            'blockGap' => null,
     240            'margin'   => null,
     241            'padding'  => null,
     242            'units'    => null,
    203243        ),
    204244        'typography' => array(
    205             'customFontSize'   => null,
    206             'customLineHeight' => null,
    207             'dropCap'          => null,
    208             'fontSizes'        => null,
     245            'customFontSize' => null,
     246            'dropCap'        => null,
     247            'fontFamilies'   => null,
     248            'fontSizes'      => null,
     249            'fontStyle'      => null,
     250            'fontWeight'     => null,
     251            'letterSpacing'  => null,
     252            'lineHeight'     => null,
     253            'textDecoration' => null,
     254            'textTransform'  => null,
    209255        ),
    210256    );
    211257
    212258    /**
    213      * @since 5.8.0
     259     * The valid properties under the styles key.
     260     *
     261     * @since 5.8.0
     262     * @since 5.9.0 Renamed from ALLOWED_SETTINGS, gained new properties.
    214263     * @var array
    215264     */
    216     const ALLOWED_STYLES = array(
     265    const VALID_STYLES = array(
    217266        'border'     => array(
     267            'color'  => null,
    218268            'radius' => null,
     269            'style'  => null,
     270            'width'  => null,
    219271        ),
    220272        'color'      => array(
     
    223275            'text'       => null,
    224276        ),
     277        'filter'     => array(
     278            'duotone' => null,
     279        ),
    225280        'spacing'    => array(
    226             'margin'  => array(
    227                 'top'    => null,
    228                 'right'  => null,
    229                 'bottom' => null,
    230                 'left'   => null,
    231             ),
    232             'padding' => array(
    233                 'bottom' => null,
    234                 'left'   => null,
    235                 'right'  => null,
    236                 'top'    => null,
    237             ),
     281            'margin'   => null,
     282            'padding'  => null,
     283            'blockGap' => null,
    238284        ),
    239285        'typography' => array(
    240             'fontSize'   => null,
    241             'lineHeight' => null,
     286            'fontFamily'     => null,
     287            'fontSize'       => null,
     288            'fontStyle'      => null,
     289            'fontWeight'     => null,
     290            'letterSpacing'  => null,
     291            'lineHeight'     => null,
     292            'textDecoration' => null,
     293            'textTransform'  => null,
    242294        ),
    243295    );
     
    258310
    259311    /**
    260      * @since 5.8.0
     312     * The latest version of the schema in use.
     313     *
     314     * @since 5.8.0
     315     * @since 5.9.0 Changed value.
    261316     * @var int
    262317     */
    263     const LATEST_SCHEMA = 1;
     318    const LATEST_SCHEMA = 2;
    264319
    265320    /**
     
    277332        }
    278333
    279         if ( ! isset( $theme_json['version'] ) || self::LATEST_SCHEMA !== $theme_json['version'] ) {
    280             $this->theme_json = array();
    281             return;
    282         }
    283 
    284         $this->theme_json = self::sanitize( $theme_json );
     334        $this->theme_json    = WP_Theme_JSON_Schema::migrate( $theme_json );
     335        $valid_block_names   = array_keys( self::get_blocks_metadata() );
     336        $valid_element_names = array_keys( self::ELEMENTS );
     337        $this->theme_json    = self::sanitize( $this->theme_json, $valid_block_names, $valid_element_names );
    285338
    286339        // Internally, presets are keyed by origin.
    287340        $nodes = self::get_setting_nodes( $this->theme_json );
    288341        foreach ( $nodes as $node ) {
    289             foreach ( self::PRESETS_METADATA as $preset ) {
    290                 $path   = array_merge( $node['path'], $preset['path'] );
     342            foreach ( self::PRESETS_METADATA as $preset_metadata ) {
     343                $path   = array_merge( $node['path'], $preset_metadata['path'] );
    291344                $preset = _wp_array_get( $this->theme_json, $path, null );
    292345                if ( null !== $preset ) {
     
    301354     *
    302355     * @since 5.8.0
     356     * @since 5.9.0 Has new parameters.
    303357     *
    304358     * @param array $input Structure to sanitize.
     359     * @param array $valid_block_names List of valid block names.
     360     * @param array $valid_element_names List of valid element names.
    305361     * @return array The sanitized output.
    306362     */
    307     private static function sanitize( $input ) {
     363    private static function sanitize( $input, $valid_block_names, $valid_element_names ) {
    308364        $output = array();
    309365
     
    312368        }
    313369
    314         $allowed_top_level_keys = self::ALLOWED_TOP_LEVEL_KEYS;
    315         $allowed_settings       = self::ALLOWED_SETTINGS;
    316         $allowed_styles         = self::ALLOWED_STYLES;
    317         $allowed_blocks         = array_keys( self::get_blocks_metadata() );
    318         $allowed_elements       = array_keys( self::ELEMENTS );
    319 
    320         $output = array_intersect_key( $input, array_flip( $allowed_top_level_keys ) );
    321 
    322         // Build the schema.
     370        $output = array_intersect_key( $input, array_flip( self::VALID_TOP_LEVEL_KEYS ) );
     371
     372        // Build the schema based on valid block & element names.
    323373        $schema                 = array();
    324374        $schema_styles_elements = array();
    325         foreach ( $allowed_elements as $element ) {
    326             $schema_styles_elements[ $element ] = $allowed_styles;
     375        foreach ( $valid_element_names as $element ) {
     376            $schema_styles_elements[ $element ] = self::VALID_STYLES;
    327377        }
    328378        $schema_styles_blocks   = array();
    329379        $schema_settings_blocks = array();
    330         foreach ( $allowed_blocks as $block ) {
    331             $schema_settings_blocks[ $block ]           = $allowed_settings;
    332             $schema_styles_blocks[ $block ]             = $allowed_styles;
     380        foreach ( $valid_block_names as $block ) {
     381            $schema_settings_blocks[ $block ]           = self::VALID_SETTINGS;
     382            $schema_styles_blocks[ $block ]             = self::VALID_STYLES;
    333383            $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
    334384        }
    335         $schema['styles']             = $allowed_styles;
     385        $schema['styles']             = self::VALID_STYLES;
    336386        $schema['styles']['blocks']   = $schema_styles_blocks;
    337387        $schema['styles']['elements'] = $schema_styles_elements;
    338         $schema['settings']           = $allowed_settings;
     388        $schema['settings']           = self::VALID_SETTINGS;
    339389        $schema['settings']['blocks'] = $schema_settings_blocks;
    340390
     
    361411        return $output;
    362412    }
    363 
    364413    /**
    365414     * Returns the metadata for each block.
     
    378427     *         'selector': 'h1',
    379428     *         'elements': {}
    380      *       }
    381      *       'core/group': {
    382      *         'selector': '.wp-block-group',
     429     *       },
     430     *       'core/image': {
     431     *         'selector': '.wp-block-image',
     432     *         'duotone': 'img',
    383433     *         'elements': {}
    384434     *       }
     
    386436     *
    387437     * @since 5.8.0
     438     * @since 5.9.0 Added duotone key with CSS selector.
    388439     *
    389440     * @return array Block metadata.
     
    408459            }
    409460
    410             /*
    411              * Assign defaults, then overwrite those that the block sets by itself.
    412              * If the block selector is compounded, will append the element to each
    413              * individual block selector.
    414              */
     461            if (
     462                isset( $block_type->supports['color']['__experimentalDuotone'] ) &&
     463                is_string( $block_type->supports['color']['__experimentalDuotone'] )
     464            ) {
     465                self::$blocks_metadata[ $block_name ]['duotone'] = $block_type->supports['color']['__experimentalDuotone'];
     466            }
     467
     468            // Assign defaults, then overwrite those that the block sets by itself.
     469            // If the block selector is compounded, will append the element to each
     470            // individual block selector.
    415471            $block_selectors = explode( ',', self::$blocks_metadata[ $block_name ]['selector'] );
    416472            foreach ( self::ELEMENTS as $el_name => $el_selector ) {
     
    494550     *
    495551     * @since 5.8.0
    496      *
    497      * @param string $type Optional. Type of stylesheet we want. Accepts 'all',
    498      *                     'block_styles', and 'css_variables'. Default 'all'.
     552     * @since 5.9.0 Changed the arguments passed to the function.
     553     *
     554     * @param array $types    Types of styles to load. Will load all by default. It accepts:
     555     *                         'variables': only the CSS Custom Properties for presets & custom ones.
     556     *                         'styles': only the styles section in theme.json.
     557     *                         'presets': only the classes for the presets.
     558     * @param array $origins A list of origins to include. By default it includes self::VALID_ORIGINS.
    499559     * @return string Stylesheet.
    500560     */
    501     public function get_stylesheet( $type = 'all' ) {
     561    public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = self::VALID_ORIGINS ) {
     562        if ( is_string( $types ) ) {
     563            // Dispatch error and map old arguments to new ones.
     564            _deprecated_argument( __FUNCTION__, '5.9' );
     565            if ( 'block_styles' === $types ) {
     566                $types = array( 'styles', 'presets' );
     567            } elseif ( 'css_variables' === $types ) {
     568                $types = array( 'variables' );
     569            } else {
     570                $types = array( 'variables', 'styles', 'presets' );
     571            }
     572        }
     573
    502574        $blocks_metadata = self::get_blocks_metadata();
    503575        $style_nodes     = self::get_style_nodes( $this->theme_json, $blocks_metadata );
    504576        $setting_nodes   = self::get_setting_nodes( $this->theme_json, $blocks_metadata );
    505577
    506         switch ( $type ) {
    507             case 'block_styles':
    508                 return $this->get_block_styles( $style_nodes, $setting_nodes );
    509             case 'css_variables':
    510                 return $this->get_css_variables( $setting_nodes );
    511             default:
    512                 return $this->get_css_variables( $setting_nodes ) . $this->get_block_styles( $style_nodes, $setting_nodes );
    513         }
    514 
     578        $stylesheet = '';
     579
     580        if ( in_array( 'variables', $types, true ) ) {
     581            $stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
     582        }
     583
     584        if ( in_array( 'styles', $types, true ) ) {
     585            $stylesheet .= $this->get_block_classes( $style_nodes );
     586        }
     587
     588        if ( in_array( 'presets', $types, true ) ) {
     589            $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
     590        }
     591
     592        return $stylesheet;
     593    }
     594
     595    /**
     596     * Returns the page templates of the current theme.
     597     *
     598     * @since 5.9.0
     599     *
     600     * @return array
     601     */
     602    public function get_custom_templates() {
     603        $custom_templates = array();
     604        if ( ! isset( $this->theme_json['customTemplates'] ) ) {
     605            return $custom_templates;
     606        }
     607
     608        foreach ( $this->theme_json['customTemplates'] as $item ) {
     609            if ( isset( $item['name'] ) ) {
     610                $custom_templates[ $item['name'] ] = array(
     611                    'title'     => isset( $item['title'] ) ? $item['title'] : '',
     612                    'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ),
     613                );
     614            }
     615        }
     616        return $custom_templates;
     617    }
     618
     619    /**
     620     * Returns the template part data of current theme.
     621     *
     622     * @since 5.9.0
     623     *
     624     * @return array
     625     */
     626    public function get_template_parts() {
     627        $template_parts = array();
     628        if ( ! isset( $this->theme_json['templateParts'] ) ) {
     629            return $template_parts;
     630        }
     631
     632        foreach ( $this->theme_json['templateParts'] as $item ) {
     633            if ( isset( $item['name'] ) ) {
     634                $template_parts[ $item['name'] ] = array(
     635                    'title' => isset( $item['title'] ) ? $item['title'] : '',
     636                    'area'  => isset( $item['area'] ) ? $item['area'] : '',
     637                );
     638            }
     639        }
     640        return $template_parts;
    515641    }
    516642
     
    527653     *   }
    528654     *
    529      * Additionally, it'll also create new rulesets
    530      * as classes for each preset value such as:
    531      *
    532      *     .has-value-color {
    533      *       color: value;
    534      *     }
    535      *
    536      *     .has-value-background-color {
    537      *       background-color: value;
    538      *     }
    539      *
    540      *     .has-value-font-size {
    541      *       font-size: value;
    542      *     }
    543      *
    544      *     .has-value-gradient-background {
    545      *       background: value;
    546      *     }
    547      *
    548      *     p.has-value-gradient-background {
    549      *       background: value;
    550      *     }
    551      *
    552      * @since 5.8.0
    553      *
    554      * @param array $style_nodes   Nodes with styles.
    555      * @param array $setting_nodes Nodes with settings.
     655     * @since 5.8.0
     656     * @since 5.9.0 Renamed to get_block_classes and no longer returns preset classes.
     657     *
     658     * @param array $style_nodes Nodes with styles.
    556659     * @return string The new stylesheet.
    557660     */
    558     private function get_block_styles( $style_nodes, $setting_nodes ) {
     661    private function get_block_classes( $style_nodes ) {
    559662        $block_rules = '';
     663
    560664        foreach ( $style_nodes as $metadata ) {
    561665            if ( null === $metadata['selector'] ) {
     
    565669            $node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
    566670            $selector     = $metadata['selector'];
    567             $declarations = self::compute_style_properties( $node );
     671            $settings     = _wp_array_get( $this->theme_json, array( 'settings' ) );
     672            $declarations = self::compute_style_properties( $node, $settings );
     673
     674            // 1. Separate the ones who use the general selector
     675            // and the ones who use the duotone selector.
     676            $declarations_duotone = array();
     677            foreach ( $declarations as $index => $declaration ) {
     678                if ( 'filter' === $declaration['name'] ) {
     679                    unset( $declarations[ $index ] );
     680                    $declarations_duotone[] = $declaration;
     681                }
     682            }
     683
     684            // 2. Generate the rules that use the general selector.
    568685            $block_rules .= self::to_ruleset( $selector, $declarations );
    569         }
    570 
     686
     687            // 3. Generate the rules that use the duotone selector.
     688            if ( isset( $metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
     689                $selector_duotone = self::scope_selector( $metadata['selector'], $metadata['duotone'] );
     690                $block_rules     .= self::to_ruleset( $selector_duotone, $declarations_duotone );
     691            }
     692
     693            if ( self::ROOT_BLOCK_SELECTOR === $selector ) {
     694                $block_rules .= 'body { margin: 0; }';
     695                $block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
     696                $block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
     697                $block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
     698
     699                $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
     700                if ( $has_block_gap_support ) {
     701                    $block_rules .= '.wp-site-blocks > * { margin-top: 0; margin-bottom: 0; }';
     702                    $block_rules .= '.wp-site-blocks > * + * { margin-top: var( --wp--style--block-gap ); }';
     703                }
     704            }
     705        }
     706
     707        return $block_rules;
     708    }
     709
     710    /**
     711     * Creates new rulesets as classes for each preset value such as:
     712     *
     713     *   .has-value-color {
     714     *     color: value;
     715     *   }
     716     *
     717     *   .has-value-background-color {
     718     *     background-color: value;
     719     *   }
     720     *
     721     *   .has-value-font-size {
     722     *     font-size: value;
     723     *   }
     724     *
     725     *   .has-value-gradient-background {
     726     *     background: value;
     727     *   }
     728     *
     729     *   p.has-value-gradient-background {
     730     *     background: value;
     731     *   }
     732     *
     733     * @since 5.9.0
     734     *
     735     * @param array $setting_nodes Nodes with settings.
     736     * @param array $origins       List of origins to process presets from.
     737     * @return string The new stylesheet.
     738     */
     739    private function get_preset_classes( $setting_nodes, $origins ) {
    571740        $preset_rules = '';
     741
    572742        foreach ( $setting_nodes as $metadata ) {
    573743            if ( null === $metadata['selector'] ) {
     
    577747            $selector      = $metadata['selector'];
    578748            $node          = _wp_array_get( $this->theme_json, $metadata['path'], array() );
    579             $preset_rules .= self::compute_preset_classes( $node, $selector );
    580         }
    581 
    582         return $block_rules . $preset_rules;
     749            $preset_rules .= self::compute_preset_classes( $node, $selector, $origins );
     750        }
     751
     752        return $preset_rules;
    583753    }
    584754
     
    598768     *
    599769     * @since 5.8.0
     770     * @since 5.9.0 Added origins parameter.
    600771     *
    601772     * @param array $nodes Nodes with settings.
     773     * @param array $origins List of origins to process.
    602774     * @return string The new stylesheet.
    603775     */
    604     private function get_css_variables( $nodes ) {
     776    private function get_css_variables( $nodes, $origins ) {
    605777        $stylesheet = '';
    606778        foreach ( $nodes as $metadata ) {
     
    612784
    613785            $node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
    614             $declarations = array_merge( self::compute_preset_vars( $node ), self::compute_theme_vars( $node ) );
     786            $declarations = array_merge( self::compute_preset_vars( $node, $origins ), self::compute_theme_vars( $node ) );
    615787
    616788            $stylesheet .= self::to_ruleset( $selector, $declarations );
     
    669841
    670842    /**
    671      * Given an array of presets keyed by origin and the value key of the preset,
    672      * it returns an array where each key is the preset slug and each value the preset value.
    673      *
    674      * @since 5.8.0
    675      *
    676      * @param array  $preset_per_origin Array of presets keyed by origin.
    677      * @param string $value_key         The property of the preset that contains its value.
    678      * @return array Array of presets where each key is a slug and each value is the preset value.
    679      */
    680     private static function get_merged_preset_by_slug( $preset_per_origin, $value_key ) {
    681         $result = array();
    682         foreach ( self::VALID_ORIGINS as $origin ) {
    683             if ( ! isset( $preset_per_origin[ $origin ] ) ) {
    684                 continue;
    685             }
    686             foreach ( $preset_per_origin[ $origin ] as $preset ) {
    687                 /*
    688                  * We don't want to use kebabCase here,
    689                  * see https://github.com/WordPress/gutenberg/issues/32347
    690                  * However, we need to make sure the generated class or CSS variable
    691                  * doesn't contain spaces.
    692                  */
    693                 $result[ preg_replace( '/\s+/', '-', $preset['slug'] ) ] = $preset[ $value_key ];
    694             }
    695         }
    696         return $result;
    697     }
    698 
    699     /**
    700843     * Given a settings array, it returns the generated rulesets
    701844     * for the preset classes.
    702845     *
    703846     * @since 5.8.0
     847     * @since 5.9.0 Added origins parameter.
    704848     *
    705849     * @param array  $settings Settings to process.
    706850     * @param string $selector Selector wrapping the classes.
     851     * @param array  $origins  List of origins to process.
    707852     * @return string The result of processing the presets.
    708853     */
    709     private static function compute_preset_classes( $settings, $selector ) {
     854    private static function compute_preset_classes( $settings, $selector, $origins ) {
    710855        if ( self::ROOT_BLOCK_SELECTOR === $selector ) {
    711856            // Classes at the global level do not need any CSS prefixed,
     
    715860
    716861        $stylesheet = '';
    717         foreach ( self::PRESETS_METADATA as $preset ) {
    718             $preset_per_origin = _wp_array_get( $settings, $preset['path'], array() );
    719             $preset_by_slug    = self::get_merged_preset_by_slug( $preset_per_origin, $preset['value_key'] );
    720             foreach ( $preset['classes'] as $class ) {
    721                 foreach ( $preset_by_slug as $slug => $value ) {
     862        foreach ( self::PRESETS_METADATA as $preset_metadata ) {
     863            $slugs = self::get_settings_slugs( $settings, $preset_metadata, $origins );
     864            foreach ( $preset_metadata['classes'] as $class => $property ) {
     865                foreach ( $slugs as $slug ) {
     866                    $css_var     = self::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
     867                    $class_name  = self::replace_slug_in_string( $class, $slug );
    722868                    $stylesheet .= self::to_ruleset(
    723                         self::append_to_selector( $selector, '.has-' . _wp_to_kebab_case( $slug ) . '-' . $class['class_suffix'] ),
     869                        self::append_to_selector( $selector, $class_name ),
    724870                        array(
    725871                            array(
    726                                 'name'  => $class['property_name'],
    727                                 'value' => 'var(--wp--preset--' . $preset['css_var_infix'] . '--' . _wp_to_kebab_case( $slug ) . ') !important',
     872                                'name'  => $property,
     873                                'value' => 'var(' . $css_var . ') !important',
    728874                            ),
    729875                        )
     
    737883
    738884    /**
     885     * Function that scopes a selector with another one. This works a bit like
     886     * SCSS nesting except the `&` operator isn't supported.
     887     *
     888     * <code>
     889     * $scope = '.a, .b .c';
     890     * $selector = '> .x, .y';
     891     * $merged = scope_selector( $scope, $selector );
     892     * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
     893     * </code>
     894     *
     895     * @since 5.9.0
     896     *
     897     * @param string $scope    Selector to scope to.
     898     * @param string $selector Original selector.
     899     *
     900     * @return string Scoped selector.
     901     */
     902    private static function scope_selector( $scope, $selector ) {
     903        $scopes    = explode( ',', $scope );
     904        $selectors = explode( ',', $selector );
     905
     906        $selectors_scoped = array();
     907        foreach ( $scopes as $outer ) {
     908            foreach ( $selectors as $inner ) {
     909                $selectors_scoped[] = trim( $outer ) . ' ' . trim( $inner );
     910            }
     911        }
     912
     913        return implode( ', ', $selectors_scoped );
     914    }
     915
     916    /**
     917     * Gets preset values keyed by slugs based on settings and metadata.
     918     *
     919     * <code>
     920     * $settings = array(
     921     *     'typography' => array(
     922     *         'fontFamilies' => array(
     923     *             array(
     924     *                 'slug'       => 'sansSerif',
     925     *                 'fontFamily' => '"Helvetica Neue", sans-serif',
     926     *             ),
     927     *             array(
     928     *                 'slug'   => 'serif',
     929     *                 'colors' => 'Georgia, serif',
     930     *             )
     931     *         ),
     932     *     ),
     933     * );
     934     * $meta = array(
     935     *    'path'      => array( 'typography', 'fontFamilies' ),
     936     *    'value_key' => 'fontFamily',
     937     * );
     938     * $values_by_slug = get_settings_values_by_slug();
     939     * // $values_by_slug === array(
     940     * //   'sans-serif' => '"Helvetica Neue", sans-serif',
     941     * //   'serif'      => 'Georgia, serif',
     942     * // );
     943     * </code>
     944     *
     945     * @since 5.9.0
     946     *
     947     * @param array $settings Settings to process.
     948     * @param array $preset_metadata One of the PRESETS_METADATA values.
     949     * @param array $origins List of origins to process.
     950     * @return array Array of presets where each key is a slug and each value is the preset value.
     951     */
     952    private static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) {
     953        $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
     954
     955        $result = array();
     956        foreach ( $origins as $origin ) {
     957            if ( ! isset( $preset_per_origin[ $origin ] ) ) {
     958                continue;
     959            }
     960            foreach ( $preset_per_origin[ $origin ] as $preset ) {
     961                $slug = _wp_to_kebab_case( $preset['slug'] );
     962
     963                $value = '';
     964                if ( isset( $preset_metadata['value_key'] ) ) {
     965                    $value_key = $preset_metadata['value_key'];
     966                    $value     = $preset[ $value_key ];
     967                } elseif (
     968                    isset( $preset_metadata['value_func'] ) &&
     969                    is_callable( $preset_metadata['value_func'] )
     970                ) {
     971                    $value_func = $preset_metadata['value_func'];
     972                    $value      = call_user_func( $value_func, $preset );
     973                } else {
     974                    // If we don't have a value, then don't add it to the result.
     975                    continue;
     976                }
     977
     978                $result[ $slug ] = $value;
     979            }
     980        }
     981        return $result;
     982    }
     983
     984    /**
     985     * Similar to get_settings_values_by_slug, but doesn't compute the value.
     986     *
     987     * @since 5.9.0
     988     *
     989     * @param array $settings Settings to process.
     990     * @param array $preset_metadata One of the PRESETS_METADATA values.
     991     * @param array $origins List of origins to process.
     992     * @return array Array of presets where the key and value are both the slug.
     993     */
     994    private static function get_settings_slugs( $settings, $preset_metadata, $origins = self::VALID_ORIGINS ) {
     995        $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
     996
     997        $result = array();
     998        foreach ( $origins as $origin ) {
     999            if ( ! isset( $preset_per_origin[ $origin ] ) ) {
     1000                continue;
     1001            }
     1002            foreach ( $preset_per_origin[ $origin ] as $preset ) {
     1003                $slug = _wp_to_kebab_case( $preset['slug'] );
     1004
     1005                // Use the array as a set so we don't get duplicates.
     1006                $result[ $slug ] = $slug;
     1007            }
     1008        }
     1009        return $result;
     1010    }
     1011
     1012    /**
     1013     * Transform a slug into a CSS Custom Property.
     1014     *
     1015     * @since 5.9.0
     1016     *
     1017     * @param string $input String to replace.
     1018     * @param string $slug The slug value to use to generate the custom property.
     1019     * @return string The CSS Custom Property. Something along the lines of --wp--preset--color--black.
     1020     */
     1021    private static function replace_slug_in_string( $input, $slug ) {
     1022        return strtr( $input, array( '$slug' => $slug ) );
     1023    }
     1024
     1025    /**
    7391026     * Given the block settings, it extracts the CSS Custom Properties
    7401027     * for the presets and adds them to the $declarations array
     
    7491036     *
    7501037     * @param array $settings Settings to process.
     1038     * @param array $origins  List of origins to process.
    7511039     * @return array Returns the modified $declarations.
    7521040     */
    753     private static function compute_preset_vars( $settings ) {
     1041    private static function compute_preset_vars( $settings, $origins ) {
    7541042        $declarations = array();
    755         foreach ( self::PRESETS_METADATA as $preset ) {
    756             $preset_per_origin = _wp_array_get( $settings, $preset['path'], array() );
    757             $preset_by_slug    = self::get_merged_preset_by_slug( $preset_per_origin, $preset['value_key'] );
    758             foreach ( $preset_by_slug as $slug => $value ) {
     1043        foreach ( self::PRESETS_METADATA as $preset_metadata ) {
     1044            $values_by_slug = self::get_settings_values_by_slug( $settings, $preset_metadata, $origins );
     1045            foreach ( $values_by_slug as $slug => $value ) {
    7591046                $declarations[] = array(
    760                     'name'  => '--wp--preset--' . $preset['css_var_infix'] . '--' . _wp_to_kebab_case( $slug ),
     1047                    'name'  => self::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
    7611048                    'value' => $value,
    7621049                );
     
    8651152     *
    8661153     * @since 5.8.0
     1154     * @since 5.9.0 Added theme setting and properties parameters.
    8671155     *
    8681156     * @param array $styles Styles to process.
     1157     * @param array $settings Theme settings.
     1158     * @param array $properties Properties metadata.
    8691159     * @return array Returns the modified $declarations.
    8701160     */
    871     private static function compute_style_properties( $styles ) {
     1161    private static function compute_style_properties( $styles, $settings = array(), $properties = self::PROPERTIES_METADATA ) {
    8721162        $declarations = array();
    8731163        if ( empty( $styles ) ) {
     
    8751165        }
    8761166
    877         $properties = array();
    878         foreach ( self::PROPERTIES_METADATA as $name => $metadata ) {
    879             /*
    880              * Some properties can be shorthand properties, meaning that
    881              * they contain multiple values instead of a single one.
    882              * An example of this is the padding property.
    883              */
    884             if ( self::has_properties( $metadata ) ) {
    885                 foreach ( $metadata['properties'] as $property ) {
    886                     $properties[] = array(
    887                         'name'  => $name . '-' . $property,
    888                         'value' => array_merge( $metadata['value'], array( $property ) ),
    889                     );
     1167        foreach ( $properties as $css_property => $value_path ) {
     1168            $value = self::get_property_value( $styles, $value_path );
     1169
     1170            // Look up protected properties, keyed by value path.
     1171            // Skip protected properties that are explicitly set to `null`.
     1172            if ( is_array( $value_path ) ) {
     1173                $path_string = implode( '.', $value_path );
     1174                if (
     1175                    array_key_exists( $path_string, self::PROTECTED_PROPERTIES ) &&
     1176                    _wp_array_get( $settings, self::PROTECTED_PROPERTIES[ $path_string ], null ) === null
     1177                ) {
     1178                    continue;
    8901179                }
    891             } else {
    892                 $properties[] = array(
    893                     'name'  => $name,
    894                     'value' => $metadata['value'],
    895                 );
    896             }
    897         }
    898 
    899         foreach ( $properties as $prop ) {
    900             $value = self::get_property_value( $styles, $prop['value'] );
    901             if ( empty( $value ) ) {
     1180            }
     1181
     1182            // Skip if empty and not "0" or value represents array of longhand values.
     1183            $has_missing_value = empty( $value ) && ! is_numeric( $value );
     1184            if ( $has_missing_value || is_array( $value ) ) {
    9021185                continue;
    9031186            }
    9041187
    9051188            $declarations[] = array(
    906                 'name'  => $prop['name'],
     1189                'name'  => $css_property,
    9071190                'value' => $value,
    9081191            );
     
    9101193
    9111194        return $declarations;
    912     }
    913 
    914     /**
    915      * Whether the metadata contains a key named properties.
    916      *
    917      * @since 5.8.0
    918      *
    919      * @param array $metadata Description of the style property.
    920      * @return bool True if properties exists, false otherwise.
    921      */
    922     private static function has_properties( $metadata ) {
    923         if ( array_key_exists( 'properties', $metadata ) ) {
    924             return true;
    925         }
    926 
    927         return false;
    9281195    }
    9291196
     
    9361203     *
    9371204     * @since 5.8.0
     1205     * @since 5.9.0 Consider $value that are arrays as well.
    9381206     *
    9391207     * @param array $styles Styles subtree.
     
    9441212        $value = _wp_array_get( $styles, $path, '' );
    9451213
    946         if ( '' === $value ) {
     1214        if ( '' === $value || is_array( $value ) ) {
    9471215            return $value;
    9481216        }
     
    10161284    }
    10171285
    1018 
    10191286    /**
    10201287     * Builds metadata for the style nodes, which returns in the form of:
     
    10231290     *       [
    10241291     *         'path'     => [ 'path', 'to', 'some', 'node' ],
    1025      *         'selector' => 'CSS selector for some node'
     1292     *         'selector' => 'CSS selector for some node',
     1293     *         'duotone'  => 'CSS selector for duotone for some node'
    10261294     *       ],
    10271295     *       [
    10281296     *         'path'     => ['path', 'to', 'other', 'node' ],
    1029      *         'selector' => 'CSS selector for other node'
     1297     *         'selector' => 'CSS selector for other node',
     1298     *         'duotone'  => null
    10301299     *       ],
    10311300     *     ]
     
    10691338            }
    10701339
     1340            $duotone_selector = null;
     1341            if ( isset( $selectors[ $name ]['duotone'] ) ) {
     1342                $duotone_selector = $selectors[ $name ]['duotone'];
     1343            }
     1344
    10711345            $nodes[] = array(
    10721346                'path'     => array( 'styles', 'blocks', $name ),
    10731347                'selector' => $selector,
     1348                'duotone'  => $duotone_selector,
    10741349            );
    10751350
     
    10911366     *
    10921367     * @since 5.8.0
     1368     * @since 5.9.0 Duotone preset also has origins.
    10931369     *
    10941370     * @param WP_Theme_JSON $incoming Data to merge.
     
    10991375
    11001376        /*
    1101          * The array_replace_recursive() algorithm merges at the leaf level.
     1377         * The array_replace_recursive algorithm merges at the leaf level.
    11021378         * For leaf values that are arrays it will use the numeric indexes for replacement.
    11031379         * In those cases, we want to replace the existing with the incoming value, if it exists.
     
    11051381        $to_replace   = array();
    11061382        $to_replace[] = array( 'spacing', 'units' );
    1107         $to_replace[] = array( 'color', 'duotone' );
    11081383        foreach ( self::VALID_ORIGINS as $origin ) {
     1384            $to_replace[] = array( 'color', 'duotone', $origin );
    11091385            $to_replace[] = array( 'color', 'palette', $origin );
    11101386            $to_replace[] = array( 'color', 'gradients', $origin );
     
    11231399            }
    11241400        }
     1401
     1402    }
     1403
     1404    /**
     1405     * Removes insecure data from theme.json.
     1406     *
     1407     * @since 5.9.0
     1408     *
     1409     * @param array $theme_json Structure to sanitize.
     1410     * @return array Sanitized structure.
     1411     */
     1412    public static function remove_insecure_properties( $theme_json ) {
     1413        $sanitized = array();
     1414
     1415        $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json );
     1416
     1417        $valid_block_names   = array_keys( self::get_blocks_metadata() );
     1418        $valid_element_names = array_keys( self::ELEMENTS );
     1419        $theme_json          = self::sanitize( $theme_json, $valid_block_names, $valid_element_names );
     1420
     1421        $blocks_metadata = self::get_blocks_metadata();
     1422        $style_nodes     = self::get_style_nodes( $theme_json, $blocks_metadata );
     1423        foreach ( $style_nodes as $metadata ) {
     1424            $input = _wp_array_get( $theme_json, $metadata['path'], array() );
     1425            if ( empty( $input ) ) {
     1426                continue;
     1427            }
     1428
     1429            $output = self::remove_insecure_styles( $input );
     1430            if ( ! empty( $output ) ) {
     1431                _wp_array_set( $sanitized, $metadata['path'], $output );
     1432            }
     1433        }
     1434
     1435        $setting_nodes = self::get_setting_nodes( $theme_json );
     1436        foreach ( $setting_nodes as $metadata ) {
     1437            $input = _wp_array_get( $theme_json, $metadata['path'], array() );
     1438            if ( empty( $input ) ) {
     1439                continue;
     1440            }
     1441
     1442            $output = self::remove_insecure_settings( $input );
     1443            if ( ! empty( $output ) ) {
     1444                _wp_array_set( $sanitized, $metadata['path'], $output );
     1445            }
     1446        }
     1447
     1448        if ( empty( $sanitized['styles'] ) ) {
     1449            unset( $theme_json['styles'] );
     1450        } else {
     1451            $theme_json['styles'] = $sanitized['styles'];
     1452        }
     1453
     1454        if ( empty( $sanitized['settings'] ) ) {
     1455            unset( $theme_json['settings'] );
     1456        } else {
     1457            $theme_json['settings'] = $sanitized['settings'];
     1458        }
     1459
     1460        return $theme_json;
     1461    }
     1462
     1463    /**
     1464     * Processes a setting node and returns the same node
     1465     * without the insecure settings.
     1466     *
     1467     * @since 5.9.0
     1468     *
     1469     * @param array $input Node to process.
     1470     * @return array
     1471     */
     1472    private static function remove_insecure_settings( $input ) {
     1473        $output = array();
     1474        foreach ( self::PRESETS_METADATA as $preset_metadata ) {
     1475            $presets = _wp_array_get( $input, $preset_metadata['path'], null );
     1476            if ( null === $presets ) {
     1477                continue;
     1478            }
     1479
     1480            $escaped_preset = array();
     1481            foreach ( $presets as $preset ) {
     1482                if (
     1483                    esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] &&
     1484                    sanitize_html_class( $preset['slug'] ) === $preset['slug']
     1485                ) {
     1486                    $value = null;
     1487                    if ( isset( $preset_metadata['value_key'] ) ) {
     1488                        $value = $preset[ $preset_metadata['value_key'] ];
     1489                    } elseif (
     1490                        isset( $preset_metadata['value_func'] ) &&
     1491                        is_callable( $preset_metadata['value_func'] )
     1492                    ) {
     1493                        $value = call_user_func( $preset_metadata['value_func'], $preset );
     1494                    }
     1495
     1496                    $preset_is_valid = true;
     1497                    foreach ( $preset_metadata['properties'] as $property ) {
     1498                        if ( ! self::is_safe_css_declaration( $property, $value ) ) {
     1499                            $preset_is_valid = false;
     1500                            break;
     1501                        }
     1502                    }
     1503
     1504                    if ( $preset_is_valid ) {
     1505                        $escaped_preset[] = $preset;
     1506                    }
     1507                }
     1508            }
     1509
     1510            if ( ! empty( $escaped_preset ) ) {
     1511                _wp_array_set( $output, $preset_metadata['path'], $escaped_preset );
     1512            }
     1513        }
     1514
     1515        return $output;
     1516    }
     1517
     1518    /**
     1519     * Processes a style node and returns the same node
     1520     * without the insecure styles.
     1521     *
     1522     * @since 5.9.0
     1523     *
     1524     * @param array $input Node to process.
     1525     * @return array
     1526     */
     1527    private static function remove_insecure_styles( $input ) {
     1528        $output       = array();
     1529        $declarations = self::compute_style_properties( $input );
     1530
     1531        foreach ( $declarations as $declaration ) {
     1532            if ( self::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) {
     1533                $path = self::PROPERTIES_METADATA[ $declaration['name'] ];
     1534
     1535                // Check the value isn't an array before adding so as to not
     1536                // double up shorthand and longhand styles.
     1537                $value = _wp_array_get( $input, $path, array() );
     1538                if ( ! is_array( $value ) ) {
     1539                    _wp_array_set( $output, $path, $value );
     1540                }
     1541            }
     1542        }
     1543        return $output;
     1544    }
     1545
     1546    /**
     1547     * Checks that a declaration provided by the user is safe.
     1548     *
     1549     * @since 5.9.0
     1550     *
     1551     * @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`.
     1552     * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`.
     1553     * @return boolean
     1554     */
     1555    private static function is_safe_css_declaration( $property_name, $property_value ) {
     1556        $style_to_validate = $property_name . ': ' . $property_value;
     1557        $filtered          = esc_html( safecss_filter_attr( $style_to_validate ) );
     1558        return ! empty( trim( $filtered ) );
    11251559    }
    11261560
     
    11771611                $theme_settings['settings']['typography'] = array();
    11781612            }
    1179             $theme_settings['settings']['typography']['customLineHeight'] = $settings['enableCustomLineHeight'];
     1613            $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight'];
    11801614        }
    11811615
     
    12211655                $theme_settings['settings']['spacing'] = array();
    12221656            }
    1223             $theme_settings['settings']['spacing']['customPadding'] = $settings['enableCustomSpacing'];
     1657            $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing'];
    12241658        }
    12251659
  • trunk/src/wp-includes/default-filters.php

    r51388 r52049  
    337337add_action( 'init', array( 'WP_Block_Supports', 'init' ), 22 );
    338338add_action( 'switch_theme', array( 'WP_Theme_JSON_Resolver', 'clean_cached_data' ) );
     339add_action( 'start_previewing_theme', array( 'WP_Theme_JSON_Resolver', 'clean_cached_data' ) );
    339340add_action( 'after_switch_theme', '_wp_menus_changed' );
    340341add_action( 'after_switch_theme', '_wp_sidebars_changed' );
  • trunk/src/wp-includes/kses.php

    r51963 r52049  
    22612261            'border-bottom-style',
    22622262            'border-bottom-width',
     2263            'border-bottom-right-radius',
     2264            'border-bottom-left-radius',
    22632265            'border-left',
    22642266            'border-left-color',
     
    22692271            'border-top-style',
    22702272            'border-top-width',
     2273            'border-top-left-radius',
     2274            'border-top-right-radius',
    22712275
    22722276            'border-spacing',
     
    22832287
    22842288            'color',
     2289            'filter',
    22852290            'font',
    22862291            'font-family',
  • trunk/src/wp-includes/script-loader.php

    r52036 r52049  
    23222322
    23232323    if ( null === $stylesheet ) {
    2324         $settings   = get_default_block_editor_settings();
    2325         $theme_json = WP_Theme_JSON_Resolver::get_merged_data( $settings );
     2324        $theme_json = WP_Theme_JSON_Resolver::get_merged_data();
    23262325        $stylesheet = $theme_json->get_stylesheet();
    23272326
  • trunk/src/wp-includes/theme-i18n.json

    r50959 r52049  
    55                    {
    66                        "name": "Font size name"
     7                    }
     8                ],
     9                "fontFamilies": [
     10                    {
     11                        "name": "Font family name"
    712                    }
    813                ]
     
    3237                            "name": "Font size name"
    3338                        }
     39                    ],
     40                    "fontFamilies": [
     41                        {
     42                            "name": "Font family name"
     43                        }
    3444                    ]
    3545                },
     
    4858            }
    4959        }
    50     }
     60    },
     61    "customTemplates": [
     62        {
     63            "title": "Custom template name"
     64        }
     65    ],
     66    "templateParts": [
     67        {
     68            "title": "Template part name"
     69        }
     70    ]
    5171}
  • trunk/src/wp-includes/theme.json

    r51538 r52049  
    11{
    2     "version": 1,
     2    "version": 2,
    33    "settings": {
    44        "border": {
    5             "customRadius": false
     5            "color": false,
     6            "radius": false,
     7            "style": false,
     8            "width": false
    69        },
    710        "color": {
     
    1013            "customGradient": true,
    1114            "link": false,
     15            "background": true,
     16            "text": true,
    1217            "duotone": [
    1318                {
     
    178183        },
    179184        "spacing": {
    180             "customMargin": false,
    181             "customPadding": false,
     185            "blockGap": null,
     186            "margin": false,
     187            "padding": false,
    182188            "units": [ "px", "em", "rem", "vh", "vw", "%" ]
    183189        },
    184190        "typography": {
    185191            "customFontSize": true,
    186             "customLineHeight": false,
    187192            "dropCap": true,
     193            "fontStyle": true,
     194            "fontWeight": true,
     195            "letterSpacing": true,
     196            "lineHeight": false,
     197            "textDecoration": true,
     198            "textTransform": true,
    188199            "fontSizes": [
    189200                {
     
    217228            "core/button": {
    218229                "border": {
    219                     "customRadius": true
     230                    "radius": true
     231                }
     232            },
     233            "core/pullquote": {
     234                "border": {
     235                    "color": true,
     236                    "radius": true,
     237                    "style": true,
     238                    "width": true
    220239                }
    221240            }
    222241        }
     242    },
     243    "styles": {
     244        "spacing": { "blockGap": "24px" }
    223245    }
    224246}
  • trunk/src/wp-settings.php

    r52026 r52049  
    171171require ABSPATH . WPINC . '/theme.php';
    172172require ABSPATH . WPINC . '/class-wp-theme.php';
     173require ABSPATH . WPINC . '/class-wp-theme-json-schema.php';
    173174require ABSPATH . WPINC . '/class-wp-theme-json.php';
    174175require ABSPATH . WPINC . '/class-wp-theme-json-resolver.php';
  • trunk/tests/phpunit/data/languages/themes/block-theme-pl_PL.po

    r51370 r52049  
    2323msgstr "Szablon strony głównej"
    2424
     25msgctxt "Template part name"
     26msgid "Small Header"
     27msgstr "Mały nagłówek"
     28
    2529msgctxt "Color name"
    2630msgid "Light"
  • trunk/tests/phpunit/data/themedir1/block-theme/theme.json

    r51370 r52049  
    6565        {
    6666            "name": "small-header",
     67            "title": "Small Header",
    6768            "area": "header"
    6869        }
  • trunk/tests/phpunit/tests/theme/themeDir.php

    r52010 r52049  
    163163            'REST Theme',
    164164            'Block Theme',
     165            'Block Theme Child Theme',
    165166        );
    166167
  • trunk/tests/phpunit/tests/theme/wpThemeJson.php

    r51443 r52049  
    1616    /**
    1717     * @ticket 52991
     18     * @ticket 54336
    1819     */
    1920    public function test_get_settings() {
     
    2425                    'color'       => array(
    2526                        'custom' => false,
     27                    ),
     28                    'layout'      => array(
     29                        'contentSize' => 'value',
     30                        'invalid/key' => 'value',
    2631                    ),
    2732                    'invalid/key' => 'value',
     
    5358                'custom' => false,
    5459            ),
     60            'layout' => array(
     61                'contentSize' => 'value',
     62            ),
    5563            'blocks' => array(
    5664                'core/group' => array(
     
    6270        );
    6371
    64         $this->assertSameSetsWithIndex( $expected, $actual );
     72        $this->assertEqualSetsWithIndex( $expected, $actual );
    6573    }
    6674
     
    194202
    195203    /**
     204     * @ticket 54336
     205     */
     206    public function test_get_stylesheet_support_for_shorthand_and_longhand_values() {
     207        $theme_json = new WP_Theme_JSON(
     208            array(
     209                'version' => WP_Theme_JSON::LATEST_SCHEMA,
     210                'styles'  => array(
     211                    'blocks' => array(
     212                        'core/group' => array(
     213                            'border'  => array(
     214                                'radius' => '10px',
     215                            ),
     216                            'spacing' => array(
     217                                'padding' => '24px',
     218                                'margin'  => '1em',
     219                            ),
     220                        ),
     221                        'core/image' => array(
     222                            'border'  => array(
     223                                'radius' => array(
     224                                    'topLeft'     => '10px',
     225                                    'bottomRight' => '1em',
     226                                ),
     227                            ),
     228                            'spacing' => array(
     229                                'padding' => array(
     230                                    'top' => '15px',
     231                                ),
     232                                'margin'  => array(
     233                                    'bottom' => '30px',
     234                                ),
     235                            ),
     236                        ),
     237                    ),
     238                ),
     239            )
     240        );
     241
     242        $styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.wp-block-group{border-radius: 10px;margin: 1em;padding: 24px;}.wp-block-image{border-top-left-radius: 10px;border-bottom-right-radius: 1em;margin-bottom: 30px;padding-top: 15px;}';
     243        $this->assertEquals( $styles, $theme_json->get_stylesheet() );
     244        $this->assertEquals( $styles, $theme_json->get_stylesheet( array( 'styles' ) ) );
     245    }
     246
     247    /**
     248     * @ticket 54336
     249     */
     250    public function test_get_stylesheet_skips_disabled_protected_properties() {
     251        $theme_json = new WP_Theme_JSON(
     252            array(
     253                'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     254                'settings' => array(
     255                    'spacing' => array(
     256                        'blockGap' => null,
     257                    ),
     258                ),
     259                'styles'   => array(
     260                    'spacing' => array(
     261                        'blockGap' => '1em',
     262                    ),
     263                    'blocks'  => array(
     264                        'core/columns' => array(
     265                            'spacing' => array(
     266                                'blockGap' => '24px',
     267                            ),
     268                        ),
     269                    ),
     270                ),
     271            )
     272        );
     273
     274        $expected = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
     275        $this->assertEquals( $expected, $theme_json->get_stylesheet() );
     276        $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) );
     277    }
     278
     279    /**
     280     * @ticket 54336
     281     */
     282    public function test_get_stylesheet_renders_enabled_protected_properties() {
     283        $theme_json = new WP_Theme_JSON(
     284            array(
     285                'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     286                'settings' => array(
     287                    'spacing' => array(
     288                        'blockGap' => true,
     289                    ),
     290                ),
     291                'styles'   => array(
     292                    'spacing' => array(
     293                        'blockGap' => '1em',
     294                    ),
     295                    'blocks'  => array(
     296                        'core/columns' => array(
     297                            'spacing' => array(
     298                                'blockGap' => '24px',
     299                            ),
     300                        ),
     301                    ),
     302                ),
     303            )
     304        );
     305
     306        $expected = 'body{--wp--style--block-gap: 1em;}body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.wp-site-blocks > * { margin-top: 0; margin-bottom: 0; }.wp-site-blocks > * + * { margin-top: var( --wp--style--block-gap ); }.wp-block-columns{--wp--style--block-gap: 24px;}';
     307        $this->assertEquals( $expected, $theme_json->get_stylesheet() );
     308        $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) );
     309    }
     310
     311    /**
    196312     * @ticket 53175
     313     * @ticket 54336
    197314     */
    198315    public function test_get_stylesheet() {
     
    221338                            ),
    222339                        ),
     340                    ),
     341                    'spacing'    => array(
     342                        'blockGap' => false,
    223343                    ),
    224344                    'misc'       => 'value',
     
    251371                    'blocks'   => array(
    252372                        'core/group'     => array(
     373                            'border'   => array(
     374                                'radius' => '10px',
     375                            ),
    253376                            'elements' => array(
    254377                                'link' => array(
     
    259382                            ),
    260383                            'spacing'  => array(
    261                                 'padding' => array(
    262                                     'top'    => '12px',
    263                                     'bottom' => '24px',
    264                                 ),
     384                                'padding' => '24px',
    265385                            ),
    266386                        ),
     
    294414                            ),
    295415                        ),
     416                        'core/image'     => array(
     417                            'border'  => array(
     418                                'radius' => array(
     419                                    'topLeft'     => '10px',
     420                                    'bottomRight' => '1em',
     421                                ),
     422                            ),
     423                            'spacing' => array(
     424                                'margin' => array(
     425                                    'bottom' => '30px',
     426                                ),
     427                            ),
     428                        ),
    296429                    ),
    297430                ),
     
    300433        );
    301434
    302         $this->assertSame(
    303             'body{--wp--preset--color--grey: grey;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}body{color: var(--wp--preset--color--grey);}a{background-color: #333;color: #111;}.wp-block-group{padding-top: 12px;padding-bottom: 24px;}.wp-block-group a{color: #111;}h1,h2,h3,h4,h5,h6{color: #123456;}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a{background-color: #777;color: #555;}.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}',
    304             $theme_json->get_stylesheet()
    305         );
    306         $this->assertSame(
    307             'body{color: var(--wp--preset--color--grey);}a{background-color: #333;color: #111;}.wp-block-group{padding-top: 12px;padding-bottom: 24px;}.wp-block-group a{color: #111;}h1,h2,h3,h4,h5,h6{color: #123456;}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a{background-color: #777;color: #555;}.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}',
    308             $theme_json->get_stylesheet( 'block_styles' )
    309         );
    310         $this->assertSame(
    311             'body{--wp--preset--color--grey: grey;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}',
    312             $theme_json->get_stylesheet( 'css_variables' )
    313         );
     435        $variables = 'body{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}';
     436        $styles    = 'body{color: var(--wp--preset--color--grey);}body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.wp-site-blocks > * { margin-top: 0; margin-bottom: 0; }.wp-site-blocks > * + * { margin-top: var( --wp--style--block-gap ); }a{background-color: #333;color: #111;}.wp-block-group{border-radius: 10px;padding: 24px;}.wp-block-group a{color: #111;}h1,h2,h3,h4,h5,h6{color: #123456;}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a{background-color: #777;color: #555;}.wp-block-image{border-top-left-radius: 10px;border-bottom-right-radius: 1em;margin-bottom: 30px;}';
     437        $presets   = '.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}.has-small-font-family{font-family: var(--wp--preset--font-family--small) !important;}.has-big-font-family{font-family: var(--wp--preset--font-family--big) !important;}';
     438        $all       = $variables . $styles . $presets;
     439        $this->assertEquals( $all, $theme_json->get_stylesheet() );
     440        $this->assertEquals( $styles, $theme_json->get_stylesheet( array( 'styles' ) ) );
     441        $this->assertEquals( $presets, $theme_json->get_stylesheet( array( 'presets' ) ) );
     442        $this->assertEquals( $variables, $theme_json->get_stylesheet( array( 'variables' ) ) );
    314443    }
    315444
    316445    /**
    317446     * @ticket 52991
     447     * @ticket 54336
    318448     */
    319449    public function test_get_stylesheet_preset_classes_work_with_compounded_selectors() {
     
    338468        );
    339469
    340         $this->assertSame(
    341             'h1.has-white-color,h2.has-white-color,h3.has-white-color,h4.has-white-color,h5.has-white-color,h6.has-white-color{color: var(--wp--preset--color--white) !important;}h1.has-white-background-color,h2.has-white-background-color,h3.has-white-background-color,h4.has-white-background-color,h5.has-white-background-color,h6.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}',
    342             $theme_json->get_stylesheet( 'block_styles' )
     470        $this->assertEquals(
     471            'h1.has-white-color,h2.has-white-color,h3.has-white-color,h4.has-white-color,h5.has-white-color,h6.has-white-color{color: var(--wp--preset--color--white) !important;}h1.has-white-background-color,h2.has-white-background-color,h3.has-white-background-color,h4.has-white-background-color,h5.has-white-background-color,h6.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}h1.has-white-border-color,h2.has-white-border-color,h3.has-white-border-color,h4.has-white-border-color,h5.has-white-border-color,h6.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}',
     472            $theme_json->get_stylesheet( array( 'presets' ) )
    343473        );
    344474    }
     
    346476    /**
    347477     * @ticket 53175
     478     * @ticket 54336
    348479     */
    349480    public function test_get_stylesheet_preset_rules_come_after_block_rules() {
     
    377508        );
    378509
    379         $this->assertSame(
    380             '.wp-block-group{--wp--preset--color--grey: grey;}.wp-block-group{color: red;}.wp-block-group.has-grey-color{color: var(--wp--preset--color--grey) !important;}.wp-block-group.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}',
    381             $theme_json->get_stylesheet()
    382         );
    383         $this->assertSame(
    384             '.wp-block-group{color: red;}.wp-block-group.has-grey-color{color: var(--wp--preset--color--grey) !important;}.wp-block-group.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}',
    385             $theme_json->get_stylesheet( 'block_styles' )
    386         );
     510        $styles    = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.wp-block-group{color: red;}';
     511        $presets   = '.wp-block-group.has-grey-color{color: var(--wp--preset--color--grey) !important;}.wp-block-group.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.wp-block-group.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}';
     512        $variables = '.wp-block-group{--wp--preset--color--grey: grey;}';
     513        $all       = $variables . $styles . $presets;
     514        $this->assertEquals( $all, $theme_json->get_stylesheet() );
     515        $this->assertEquals( $styles, $theme_json->get_stylesheet( array( 'styles' ) ) );
     516        $this->assertEquals( $presets, $theme_json->get_stylesheet( array( 'presets' ) ) );
     517        $this->assertEquals( $variables, $theme_json->get_stylesheet( array( 'variables' ) ) );
     518    }
     519
     520    /**
     521     * @ticket 54336
     522     */
     523    public function test_get_stylesheet_generates_proper_classes_from_slugs() {
     524        $theme_json = new WP_Theme_JSON(
     525            array(
     526                'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     527                'settings' => array(
     528                    'color' => array(
     529                        'palette' => array(
     530                            array(
     531                                'slug'  => 'grey',
     532                                'color' => 'grey',
     533                            ),
     534                            array(
     535                                'slug'  => 'dark grey',
     536                                'color' => 'grey',
     537                            ),
     538                            array(
     539                                'slug'  => 'light-grey',
     540                                'color' => 'grey',
     541                            ),
     542                            array(
     543                                'slug'  => 'white2black',
     544                                'color' => 'grey',
     545                            ),
     546                        ),
     547                    ),
     548                ),
     549            )
     550        );
     551
     552        $this->assertEquals(
     553            '.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-dark-grey-color{color: var(--wp--preset--color--dark-grey) !important;}.has-light-grey-color{color: var(--wp--preset--color--light-grey) !important;}.has-white-2-black-color{color: var(--wp--preset--color--white-2-black) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-dark-grey-background-color{background-color: var(--wp--preset--color--dark-grey) !important;}.has-light-grey-background-color{background-color: var(--wp--preset--color--light-grey) !important;}.has-white-2-black-background-color{background-color: var(--wp--preset--color--white-2-black) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}.has-dark-grey-border-color{border-color: var(--wp--preset--color--dark-grey) !important;}.has-light-grey-border-color{border-color: var(--wp--preset--color--light-grey) !important;}.has-white-2-black-border-color{border-color: var(--wp--preset--color--white-2-black) !important;}',
     554            $theme_json->get_stylesheet( array( 'presets' ) )
     555        );
     556        $this->assertEquals(
     557            'body{--wp--preset--color--grey: grey;--wp--preset--color--dark-grey: grey;--wp--preset--color--light-grey: grey;--wp--preset--color--white-2-black: grey;}',
     558            $theme_json->get_stylesheet( array( 'variables' ) )
     559        );
     560
    387561    }
    388562
    389563    /**
    390564     * @ticket 53175
     565     * @ticket 54336
    391566     */
    392567    public function test_get_stylesheet_preset_values_are_marked_as_important() {
     
    422597        );
    423598
    424         $this->assertSame(
    425             'body{--wp--preset--color--grey: grey;}p{background-color: blue;color: red;font-size: 12px;line-height: 1.3;}.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}',
     599        $this->assertEquals(
     600            'body{--wp--preset--color--grey: grey;}body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }p{background-color: blue;color: red;font-size: 12px;line-height: 1.3;}.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}',
    426601            $theme_json->get_stylesheet()
    427602        );
     
    430605    /**
    431606     * @ticket 52991
     607     * @ticket 54336
    432608     */
    433609    public function test_merge_incoming_data() {
     
    609785                ),
    610786                'typography' => array(
    611                     'fontSizes' => array(
     787                    'fontSizes'    => array(
    612788                        'theme' => array(
    613789                            array(
    614790                                'slug' => 'fontSize',
    615791                                'size' => 'fontSize',
     792                            ),
     793                        ),
     794                    ),
     795                    'fontFamilies' => array(
     796                        'theme' => array(
     797                            array(
     798                                'slug'       => 'fontFamily',
     799                                'fontFamily' => 'fontFamily',
    616800                            ),
    617801                        ),
     
    670854    /**
    671855     * @ticket 53175
     856     * @ticket 54336
    672857     */
    673858    public function test_merge_incoming_data_empty_presets() {
     
    737922            'settings' => array(
    738923                'color'      => array(
    739                     'duotone'   => array(),
     924                    'duotone'   => array(
     925                        'theme' => array(),
     926                    ),
    740927                    'gradients' => array(
    741928                        'theme' => array(),
     
    761948    /**
    762949     * @ticket 53175
     950     * @ticket 54336
    763951     */
    764952    public function test_merge_incoming_data_null_presets() {
     
    811999                        ),
    8121000                        'spacing'    => array(
    813                             'customMargin' => false,
     1001                            'margin' => false,
    8141002                        ),
    8151003                        'typography' => array(
    816                             'customLineHeight' => false,
     1004                            'lineHeight' => false,
    8171005                        ),
    8181006                    ),
     
    8281016                    'custom'    => false,
    8291017                    'duotone'   => array(
    830                         array(
    831                             'slug'   => 'value',
    832                             'colors' => array( 'red', 'green' ),
     1018                        'theme' => array(
     1019                            array(
     1020                                'slug'   => 'value',
     1021                                'colors' => array( 'red', 'green' ),
     1022                            ),
    8331023                        ),
    8341024                    ),
     
    8511041                ),
    8521042                'spacing'    => array(
    853                     'customMargin' => false,
    854                     'units'        => array( 'px', 'em' ),
     1043                    'margin' => false,
     1044                    'units'  => array( 'px', 'em' ),
    8551045                ),
    8561046                'typography' => array(
    857                     'customLineHeight' => false,
    858                     'fontSizes'        => array(
     1047                    'lineHeight' => false,
     1048                    'fontSizes'  => array(
    8591049                        'theme' => array(
    8601050                            array(
     
    8691059
    8701060        $this->assertEqualSetsWithIndex( $expected, $actual );
     1061    }
     1062
     1063    /**
     1064     * @ticket 54336
     1065     */
     1066    public function test_remove_insecure_properties_removes_unsafe_styles() {
     1067        $actual = WP_Theme_JSON::remove_insecure_properties(
     1068            array(
     1069                'version' => WP_Theme_JSON::LATEST_SCHEMA,
     1070                'styles'  => array(
     1071                    'color'    => array(
     1072                        'gradient' => 'url(\'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMCcgaGVpZ2h0PScxMCc+PHNjcmlwdD5hbGVydCgnb2snKTwvc2NyaXB0PjxsaW5lYXJHcmFkaWVudCBpZD0nZ3JhZGllbnQnPjxzdG9wIG9mZnNldD0nMTAlJyBzdG9wLWNvbG9yPScjRjAwJy8+PHN0b3Agb2Zmc2V0PSc5MCUnIHN0b3AtY29sb3I9JyNmY2MnLz4gPC9saW5lYXJHcmFkaWVudD48cmVjdCBmaWxsPSd1cmwoI2dyYWRpZW50KScgeD0nMCcgeT0nMCcgd2lkdGg9JzEwMCUnIGhlaWdodD0nMTAwJScvPjwvc3ZnPg==\')',
     1073                        'text'     => 'var:preset|color|dark-red',
     1074                    ),
     1075                    'elements' => array(
     1076                        'link' => array(
     1077                            'color' => array(
     1078                                'gradient'   => 'url(\'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMCcgaGVpZ2h0PScxMCc+PHNjcmlwdD5hbGVydCgnb2snKTwvc2NyaXB0PjxsaW5lYXJHcmFkaWVudCBpZD0nZ3JhZGllbnQnPjxzdG9wIG9mZnNldD0nMTAlJyBzdG9wLWNvbG9yPScjRjAwJy8+PHN0b3Agb2Zmc2V0PSc5MCUnIHN0b3AtY29sb3I9JyNmY2MnLz4gPC9saW5lYXJHcmFkaWVudD48cmVjdCBmaWxsPSd1cmwoI2dyYWRpZW50KScgeD0nMCcgeT0nMCcgd2lkdGg9JzEwMCUnIGhlaWdodD0nMTAwJScvPjwvc3ZnPg==\')',
     1079                                'text'       => 'var:preset|color|dark-pink',
     1080                                'background' => 'var:preset|color|dark-red',
     1081                            ),
     1082                        ),
     1083                    ),
     1084                    'blocks'   => array(
     1085                        'core/image'  => array(
     1086                            'filter' => array(
     1087                                'duotone' => 'var:preset|duotone|blue-red',
     1088                            ),
     1089                        ),
     1090                        'core/cover'  => array(
     1091                            'filter' => array(
     1092                                'duotone' => 'var(--wp--preset--duotone--blue-red, var(--fallback-unsafe))',
     1093                            ),
     1094                        ),
     1095                        'core/group'  => array(
     1096                            'color'    => array(
     1097                                'gradient' => 'url(\'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMCcgaGVpZ2h0PScxMCc+PHNjcmlwdD5hbGVydCgnb2snKTwvc2NyaXB0PjxsaW5lYXJHcmFkaWVudCBpZD0nZ3JhZGllbnQnPjxzdG9wIG9mZnNldD0nMTAlJyBzdG9wLWNvbG9yPScjRjAwJy8+PHN0b3Agb2Zmc2V0PSc5MCUnIHN0b3AtY29sb3I9JyNmY2MnLz4gPC9saW5lYXJHcmFkaWVudD48cmVjdCBmaWxsPSd1cmwoI2dyYWRpZW50KScgeD0nMCcgeT0nMCcgd2lkdGg9JzEwMCUnIGhlaWdodD0nMTAwJScvPjwvc3ZnPg==\')',
     1098                                'text'     => 'var:preset|color|dark-gray',
     1099                            ),
     1100                            'elements' => array(
     1101                                'link' => array(
     1102                                    'color' => array(
     1103                                        'gradient' => 'url(\'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMCcgaGVpZ2h0PScxMCc+PHNjcmlwdD5hbGVydCgnb2snKTwvc2NyaXB0PjxsaW5lYXJHcmFkaWVudCBpZD0nZ3JhZGllbnQnPjxzdG9wIG9mZnNldD0nMTAlJyBzdG9wLWNvbG9yPScjRjAwJy8+PHN0b3Agb2Zmc2V0PSc5MCUnIHN0b3AtY29sb3I9JyNmY2MnLz4gPC9saW5lYXJHcmFkaWVudD48cmVjdCBmaWxsPSd1cmwoI2dyYWRpZW50KScgeD0nMCcgeT0nMCcgd2lkdGg9JzEwMCUnIGhlaWdodD0nMTAwJScvPjwvc3ZnPg==\')',
     1104                                        'text'     => 'var:preset|color|dark-pink',
     1105                                    ),
     1106                                ),
     1107                            ),
     1108                        ),
     1109                        'invalid/key' => array(
     1110                            'background' => 'green',
     1111                        ),
     1112                    ),
     1113                ),
     1114            )
     1115        );
     1116
     1117        $expected = array(
     1118            'version' => WP_Theme_JSON::LATEST_SCHEMA,
     1119            'styles'  => array(
     1120                'color'    => array(
     1121                    'text' => 'var:preset|color|dark-red',
     1122                ),
     1123                'elements' => array(
     1124                    'link' => array(
     1125                        'color' => array(
     1126                            'text'       => 'var:preset|color|dark-pink',
     1127                            'background' => 'var:preset|color|dark-red',
     1128                        ),
     1129                    ),
     1130                ),
     1131                'blocks'   => array(
     1132                    'core/image' => array(
     1133                        'filter' => array(
     1134                            'duotone' => 'var:preset|duotone|blue-red',
     1135                        ),
     1136                    ),
     1137                    'core/group' => array(
     1138                        'color'    => array(
     1139                            'text' => 'var:preset|color|dark-gray',
     1140                        ),
     1141                        'elements' => array(
     1142                            'link' => array(
     1143                                'color' => array(
     1144                                    'text' => 'var:preset|color|dark-pink',
     1145                                ),
     1146                            ),
     1147                        ),
     1148                    ),
     1149                ),
     1150            ),
     1151        );
     1152        $this->assertEqualSetsWithIndex( $expected, $actual );
     1153    }
     1154
     1155    /**
     1156     * @ticket 54336
     1157     */
     1158    public function test_remove_insecure_properties_removes_unsafe_styles_sub_properties() {
     1159        $actual = WP_Theme_JSON::remove_insecure_properties(
     1160            array(
     1161                'version' => WP_Theme_JSON::LATEST_SCHEMA,
     1162                'styles'  => array(
     1163                    'border'   => array(
     1164                        'radius' => array(
     1165                            'topLeft'     => '6px',
     1166                            'topRight'    => 'var(--top-right, var(--unsafe-fallback))',
     1167                            'bottomRight' => '6px',
     1168                            'bottomLeft'  => '6px',
     1169                        ),
     1170                    ),
     1171                    'spacing'  => array(
     1172                        'padding' => array(
     1173                            'top'    => '1px',
     1174                            'right'  => '1px',
     1175                            'bottom' => 'var(--bottom, var(--unsafe-fallback))',
     1176                            'left'   => '1px',
     1177                        ),
     1178                    ),
     1179                    'elements' => array(
     1180                        'link' => array(
     1181                            'spacing' => array(
     1182                                'padding' => array(
     1183                                    'top'    => '2px',
     1184                                    'right'  => '2px',
     1185                                    'bottom' => 'var(--bottom, var(--unsafe-fallback))',
     1186                                    'left'   => '2px',
     1187                                ),
     1188                            ),
     1189                        ),
     1190                    ),
     1191                    'blocks'   => array(
     1192                        'core/group' => array(
     1193                            'border'   => array(
     1194                                'radius' => array(
     1195                                    'topLeft'     => '5px',
     1196                                    'topRight'    => 'var(--top-right, var(--unsafe-fallback))',
     1197                                    'bottomRight' => '5px',
     1198                                    'bottomLeft'  => '5px',
     1199                                ),
     1200                            ),
     1201                            'spacing'  => array(
     1202                                'padding' => array(
     1203                                    'top'    => '3px',
     1204                                    'right'  => '3px',
     1205                                    'bottom' => 'var(bottom, var(--unsafe-fallback))',
     1206                                    'left'   => '3px',
     1207                                ),
     1208                            ),
     1209                            'elements' => array(
     1210                                'link' => array(
     1211                                    'spacing' => array(
     1212                                        'padding' => array(
     1213                                            'top'    => '4px',
     1214                                            'right'  => '4px',
     1215                                            'bottom' => 'var(--bottom, var(--unsafe-fallback))',
     1216                                            'left'   => '4px',
     1217                                        ),
     1218                                    ),
     1219                                ),
     1220                            ),
     1221                        ),
     1222                    ),
     1223                ),
     1224            ),
     1225            true
     1226        );
     1227
     1228        $expected = array(
     1229            'version' => WP_Theme_JSON::LATEST_SCHEMA,
     1230            'styles'  => array(
     1231                'border'   => array(
     1232                    'radius' => array(
     1233                        'topLeft'     => '6px',
     1234                        'bottomRight' => '6px',
     1235                        'bottomLeft'  => '6px',
     1236                    ),
     1237                ),
     1238                'spacing'  => array(
     1239                    'padding' => array(
     1240                        'top'   => '1px',
     1241                        'right' => '1px',
     1242                        'left'  => '1px',
     1243                    ),
     1244                ),
     1245                'elements' => array(
     1246                    'link' => array(
     1247                        'spacing' => array(
     1248                            'padding' => array(
     1249                                'top'   => '2px',
     1250                                'right' => '2px',
     1251                                'left'  => '2px',
     1252                            ),
     1253                        ),
     1254                    ),
     1255                ),
     1256                'blocks'   => array(
     1257                    'core/group' => array(
     1258                        'border'   => array(
     1259                            'radius' => array(
     1260                                'topLeft'     => '5px',
     1261                                'bottomRight' => '5px',
     1262                                'bottomLeft'  => '5px',
     1263                            ),
     1264                        ),
     1265                        'spacing'  => array(
     1266                            'padding' => array(
     1267                                'top'   => '3px',
     1268                                'right' => '3px',
     1269                                'left'  => '3px',
     1270                            ),
     1271                        ),
     1272                        'elements' => array(
     1273                            'link' => array(
     1274                                'spacing' => array(
     1275                                    'padding' => array(
     1276                                        'top'   => '4px',
     1277                                        'right' => '4px',
     1278                                        'left'  => '4px',
     1279                                    ),
     1280                                ),
     1281                            ),
     1282                        ),
     1283                    ),
     1284                ),
     1285            ),
     1286        );
     1287        $this->assertEqualSetsWithIndex( $expected, $actual );
     1288    }
     1289
     1290    /**
     1291     * @ticket 54336
     1292     */
     1293    public function test_remove_insecure_properties_removes_non_preset_settings() {
     1294        $actual = WP_Theme_JSON::remove_insecure_properties(
     1295            array(
     1296                'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     1297                'settings' => array(
     1298                    'color'   => array(
     1299                        'custom'  => true,
     1300                        'palette' => array(
     1301                            array(
     1302                                'name'  => 'Red',
     1303                                'slug'  => 'red',
     1304                                'color' => '#ff0000',
     1305                            ),
     1306                            array(
     1307                                'name'  => 'Green',
     1308                                'slug'  => 'green',
     1309                                'color' => '#00ff00',
     1310                            ),
     1311                            array(
     1312                                'name'  => 'Blue',
     1313                                'slug'  => 'blue',
     1314                                'color' => '#0000ff',
     1315                            ),
     1316                        ),
     1317                    ),
     1318                    'spacing' => array(
     1319                        'padding' => false,
     1320                    ),
     1321                    'blocks'  => array(
     1322                        'core/group' => array(
     1323                            'color'   => array(
     1324                                'custom'  => true,
     1325                                'palette' => array(
     1326                                    array(
     1327                                        'name'  => 'Yellow',
     1328                                        'slug'  => 'yellow',
     1329                                        'color' => '#ff0000',
     1330                                    ),
     1331                                    array(
     1332                                        'name'  => 'Pink',
     1333                                        'slug'  => 'pink',
     1334                                        'color' => '#00ff00',
     1335                                    ),
     1336                                    array(
     1337                                        'name'  => 'Orange',
     1338                                        'slug'  => 'orange',
     1339                                        'color' => '#0000ff',
     1340                                    ),
     1341                                ),
     1342                            ),
     1343                            'spacing' => array(
     1344                                'padding' => false,
     1345                            ),
     1346                        ),
     1347                    ),
     1348                ),
     1349            )
     1350        );
     1351
     1352        $expected = array(
     1353            'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     1354            'settings' => array(
     1355                'color'  => array(
     1356                    'palette' => array(
     1357                        array(
     1358                            'name'  => 'Red',
     1359                            'slug'  => 'red',
     1360                            'color' => '#ff0000',
     1361                        ),
     1362                        array(
     1363                            'name'  => 'Green',
     1364                            'slug'  => 'green',
     1365                            'color' => '#00ff00',
     1366                        ),
     1367                        array(
     1368                            'name'  => 'Blue',
     1369                            'slug'  => 'blue',
     1370                            'color' => '#0000ff',
     1371                        ),
     1372                    ),
     1373                ),
     1374                'blocks' => array(
     1375                    'core/group' => array(
     1376                        'color' => array(
     1377                            'palette' => array(
     1378                                array(
     1379                                    'name'  => 'Yellow',
     1380                                    'slug'  => 'yellow',
     1381                                    'color' => '#ff0000',
     1382                                ),
     1383                                array(
     1384                                    'name'  => 'Pink',
     1385                                    'slug'  => 'pink',
     1386                                    'color' => '#00ff00',
     1387                                ),
     1388                                array(
     1389                                    'name'  => 'Orange',
     1390                                    'slug'  => 'orange',
     1391                                    'color' => '#0000ff',
     1392                                ),
     1393                            ),
     1394                        ),
     1395                    ),
     1396                ),
     1397            ),
     1398        );
     1399        $this->assertEqualSetsWithIndex( $expected, $actual );
     1400    }
     1401
     1402    /**
     1403     * @ticket 54336
     1404     */
     1405    public function test_remove_insecure_properties_removes_unsafe_preset_settings() {
     1406        $actual = WP_Theme_JSON::remove_insecure_properties(
     1407            array(
     1408                'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     1409                'settings' => array(
     1410                    'color'      => array(
     1411                        'palette' => array(
     1412                            array(
     1413                                'name'  => 'Red/><b>ok</ok>',
     1414                                'slug'  => 'red',
     1415                                'color' => '#ff0000',
     1416                            ),
     1417                            array(
     1418                                'name'  => 'Green',
     1419                                'slug'  => 'a" attr',
     1420                                'color' => '#00ff00',
     1421                            ),
     1422                            array(
     1423                                'name'  => 'Blue',
     1424                                'slug'  => 'blue',
     1425                                'color' => 'var(--color, var(--unsafe-fallback))',
     1426                            ),
     1427                            array(
     1428                                'name'  => 'Pink',
     1429                                'slug'  => 'pink',
     1430                                'color' => '#FFC0CB',
     1431                            ),
     1432                        ),
     1433                    ),
     1434                    'typography' => array(
     1435                        'fontFamilies' => array(
     1436                            array(
     1437                                'name'       => 'Helvetica Arial/><b>test</b>',
     1438                                'slug'       => 'helvetica-arial',
     1439                                'fontFamily' => 'Helvetica Neue, Helvetica, Arial, sans-serif',
     1440                            ),
     1441                            array(
     1442                                'name'       => 'Geneva',
     1443                                'slug'       => 'geneva#asa',
     1444                                'fontFamily' => 'Geneva, Tahoma, Verdana, sans-serif',
     1445                            ),
     1446                            array(
     1447                                'name'       => 'Cambria',
     1448                                'slug'       => 'cambria',
     1449                                'fontFamily' => 'Cambria, Georgia, serif',
     1450                            ),
     1451                            array(
     1452                                'name'       => 'Helvetica Arial',
     1453                                'slug'       => 'helvetica-arial',
     1454                                'fontFamily' => 'var(--fontFamily, var(--unsafe-fallback))',
     1455                            ),
     1456                        ),
     1457                    ),
     1458                    'blocks'     => array(
     1459                        'core/group' => array(
     1460                            'color' => array(
     1461                                'palette' => array(
     1462                                    array(
     1463                                        'name'  => 'Red/><b>ok</ok>',
     1464                                        'slug'  => 'red',
     1465                                        'color' => '#ff0000',
     1466                                    ),
     1467                                    array(
     1468                                        'name'  => 'Green',
     1469                                        'slug'  => 'a" attr',
     1470                                        'color' => '#00ff00',
     1471                                    ),
     1472                                    array(
     1473                                        'name'  => 'Blue',
     1474                                        'slug'  => 'blue',
     1475                                        'color' => 'var(--color, var(--unsafe--fallback))',
     1476                                    ),
     1477                                    array(
     1478                                        'name'  => 'Pink',
     1479                                        'slug'  => 'pink',
     1480                                        'color' => '#FFC0CB',
     1481                                    ),
     1482                                ),
     1483                            ),
     1484                        ),
     1485                    ),
     1486                ),
     1487            )
     1488        );
     1489
     1490        $expected = array(
     1491            'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     1492            'settings' => array(
     1493                'color'      => array(
     1494                    'palette' => array(
     1495                        array(
     1496                            'name'  => 'Pink',
     1497                            'slug'  => 'pink',
     1498                            'color' => '#FFC0CB',
     1499                        ),
     1500                    ),
     1501                ),
     1502                'typography' => array(
     1503                    'fontFamilies' => array(
     1504                        array(
     1505                            'name'       => 'Cambria',
     1506                            'slug'       => 'cambria',
     1507                            'fontFamily' => 'Cambria, Georgia, serif',
     1508                        ),
     1509                    ),
     1510                ),
     1511                'blocks'     => array(
     1512                    'core/group' => array(
     1513                        'color' => array(
     1514                            'palette' => array(
     1515                                array(
     1516                                    'name'  => 'Pink',
     1517                                    'slug'  => 'pink',
     1518                                    'color' => '#FFC0CB',
     1519                                ),
     1520                            ),
     1521                        ),
     1522                    ),
     1523                ),
     1524            ),
     1525        );
     1526        $this->assertEqualSetsWithIndex( $expected, $actual );
     1527    }
     1528
     1529    /**
     1530     * @ticket 54336
     1531     */
     1532    public function test_remove_insecure_properties_applies_safe_styles() {
     1533        $actual = WP_Theme_JSON::remove_insecure_properties(
     1534            array(
     1535                'version' => WP_Theme_JSON::LATEST_SCHEMA,
     1536                'styles'  => array(
     1537                    'color' => array(
     1538                        'text' => '#abcabc ', // Trailing space.
     1539                    ),
     1540                ),
     1541            ),
     1542            true
     1543        );
     1544
     1545        $expected = array(
     1546            'version' => WP_Theme_JSON::LATEST_SCHEMA,
     1547            'styles'  => array(
     1548                'color' => array(
     1549                    'text' => '#abcabc ',
     1550                ),
     1551            ),
     1552        );
     1553        $this->assertEqualSetsWithIndex( $expected, $actual );
     1554    }
     1555
     1556    /**
     1557     * @ticket 54336
     1558     */
     1559    public function test_get_custom_templates() {
     1560        $theme_json = new WP_Theme_JSON(
     1561            array(
     1562                'version'         => 1,
     1563                'customTemplates' => array(
     1564                    array(
     1565                        'name'  => 'page-home',
     1566                        'title' => 'Homepage template',
     1567                    ),
     1568                ),
     1569            )
     1570        );
     1571
     1572        $page_templates = $theme_json->get_custom_templates();
     1573
     1574        $this->assertEqualSetsWithIndex(
     1575            $page_templates,
     1576            array(
     1577                'page-home' => array(
     1578                    'title'     => 'Homepage template',
     1579                    'postTypes' => array( 'page' ),
     1580                ),
     1581            )
     1582        );
     1583    }
     1584
     1585    /**
     1586     * @ticket 54336
     1587     */
     1588    public function test_get_template_parts() {
     1589        $theme_json = new WP_Theme_JSON(
     1590            array(
     1591                'version'       => 1,
     1592                'templateParts' => array(
     1593                    array(
     1594                        'name'  => 'small-header',
     1595                        'title' => 'Small Header',
     1596                        'area'  => 'header',
     1597                    ),
     1598                ),
     1599            )
     1600        );
     1601
     1602        $template_parts = $theme_json->get_template_parts();
     1603
     1604        $this->assertEqualSetsWithIndex(
     1605            $template_parts,
     1606            array(
     1607                'small-header' => array(
     1608                    'title' => 'Small Header',
     1609                    'area'  => 'header',
     1610                ),
     1611            )
     1612        );
    8711613    }
    8721614
     
    9291671                ),
    9301672                'typography' => array(
    931                     'customFontSize'   => false,
    932                     'customLineHeight' => true,
    933                     'fontSizes'        => array(
     1673                    'customFontSize' => false,
     1674                    'lineHeight'    => true,
     1675                    'fontSizes'      => array(
    9341676                        array(
    9351677                            'slug' => 'size-slug',
     
    9491691    /**
    9501692     * @ticket 52991
     1693     * @ticket 54336
    9511694     */
    9521695    public function test_get_editor_settings_no_theme_support() {
     
    9911734                ),
    9921735                'typography' => array(
    993                     'customFontSize'   => true,
    994                     'customLineHeight' => false,
     1736                    'customFontSize' => true,
     1737                    'lineHeight'    => false,
    9951738                ),
    9961739            ),
     
    10041747    /**
    10051748     * @ticket 52991
     1749     * @ticket 54336
    10061750     */
    10071751    public function test_get_editor_settings_blank() {
     
    10121756        $actual   = WP_Theme_JSON::get_from_editor_settings( array() );
    10131757
    1014         $this->assertSameSetsWithIndex( $expected, $actual );
     1758        $this->assertEqualSetsWithIndex( $expected, $actual );
    10151759    }
    10161760
    10171761    /**
    10181762     * @ticket 52991
     1763     * @ticket 54336
    10191764     */
    10201765    public function test_get_editor_settings_custom_units_can_be_disabled() {
    10211766        add_theme_support( 'custom-units', array() );
    1022         $input = get_default_block_editor_settings();
     1767        $actual = WP_Theme_JSON::get_from_editor_settings( get_default_block_editor_settings() );
     1768        remove_theme_support( 'custom-units' );
    10231769
    10241770        $expected = array(
    1025             'units'         => array( array() ),
    1026             'customPadding' => false,
    1027         );
    1028 
    1029         $actual = WP_Theme_JSON::get_from_editor_settings( $input );
    1030 
    1031         $this->assertSameSetsWithIndex( $expected, $actual['settings']['spacing'] );
     1771            'units'   => array( array() ),
     1772            'padding' => false,
     1773        );
     1774
     1775        $this->assertEqualSetsWithIndex( $expected, $actual['settings']['spacing'] );
    10321776    }
    10331777
    10341778    /**
    10351779     * @ticket 52991
     1780     * @ticket 54336
    10361781     */
    10371782    public function test_get_editor_settings_custom_units_can_be_enabled() {
    10381783        add_theme_support( 'custom-units' );
    1039         $input = get_default_block_editor_settings();
     1784        $actual = WP_Theme_JSON::get_from_editor_settings( get_default_block_editor_settings() );
     1785        remove_theme_support( 'custom-units' );
    10401786
    10411787        $expected = array(
    1042             'units'         => array( 'px', 'em', 'rem', 'vh', 'vw', '%' ),
    1043             'customPadding' => false,
    1044         );
    1045 
    1046         $actual = WP_Theme_JSON::get_from_editor_settings( $input );
    1047 
    1048         $this->assertSameSetsWithIndex( $expected, $actual['settings']['spacing'] );
     1788            'units'   => array( 'px', 'em', 'rem', 'vh', 'vw', '%' ),
     1789            'padding' => false,
     1790        );
     1791
     1792        $this->assertEqualSetsWithIndex( $expected, $actual['settings']['spacing'] );
    10491793    }
    10501794
    10511795    /**
    10521796     * @ticket 52991
     1797     * @ticket 54336
    10531798     */
    10541799    public function test_get_editor_settings_custom_units_can_be_filtered() {
    10551800        add_theme_support( 'custom-units', 'rem', 'em' );
    1056         $input = get_default_block_editor_settings();
     1801        $actual = WP_Theme_JSON::get_from_editor_settings( get_default_block_editor_settings() );
     1802        remove_theme_support( 'custom-units' );
    10571803
    10581804        $expected = array(
    1059             'units'         => array( 'rem', 'em' ),
    1060             'customPadding' => false,
    1061         );
    1062 
    1063         $actual = WP_Theme_JSON::get_from_editor_settings( $input );
    1064 
    1065         $this->assertSameSetsWithIndex( $expected, $actual['settings']['spacing'] );
     1805            'units'   => array( 'rem', 'em' ),
     1806            'padding' => false,
     1807        );
     1808        $this->assertEqualSetsWithIndex( $expected, $actual['settings']['spacing'] );
    10661809    }
    10671810
  • trunk/tests/phpunit/tests/theme/wpThemeJsonResolver.php

    r51599 r52049  
    4747    /**
    4848     * @ticket 52991
     49     * @ticket 54336
    4950     */
    5051    public function test_translations_are_applied() {
     
    5354
    5455        switch_theme( 'block-theme' );
    55 
    5656        $actual = WP_Theme_JSON_Resolver::get_theme_data();
    5757
     
    6363            array(
    6464                'color'      => array(
     65                    'custom'         => false,
     66                    'customGradient' => false,
    6567                    'palette'        => array(
    6668                        'theme' => array(
     
    8688                        ),
    8789                    ),
    88                     'custom'         => false,
    89                     'customGradient' => false,
    9090                ),
    9191                'typography' => array(
    92                     'fontSizes'        => array(
     92                    'customFontSize' => false,
     93                    'lineHeight'     => true,
     94                    'fontSizes'      => array(
    9395                        'theme' => array(
    9496                            array(
     
    99101                        ),
    100102                    ),
    101                     'customFontSize'   => false,
    102                     'customLineHeight' => true,
    103103                ),
    104104                'spacing'    => array(
    105                     'units'         => array(
    106                         'rem',
    107                     ),
    108                     'customPadding' => true,
     105                    'units'   => array( 'rem' ),
     106                    'padding' => true,
    109107                ),
    110108                'blocks'     => array(
     
    126124            $actual->get_settings()
    127125        );
     126        $this->assertSame(
     127            $actual->get_custom_templates(),
     128            array(
     129                'page-home' => array(
     130                    'title'     => 'Szablon strony głównej',
     131                    'postTypes' => array( 'page' ),
     132                ),
     133            )
     134        );
     135        $this->assertSame(
     136            $actual->get_template_parts(),
     137            array(
     138                'small-header' => array(
     139                    'title' => 'Mały nagłówek',
     140                    'area'  => 'header',
     141                ),
     142            )
     143        );
    128144    }
    129145
     
    144160    }
    145161
     162    /**
     163     * @ticket 54336
     164     */
     165    function test_add_theme_supports_are_loaded_for_themes_without_theme_json() {
     166        switch_theme( 'default' );
     167        $color_palette = array(
     168            array(
     169                'name'  => 'Primary',
     170                'slug'  => 'primary',
     171                'color' => '#F00',
     172            ),
     173            array(
     174                'name'  => 'Secondary',
     175                'slug'  => 'secondary',
     176                'color' => '#0F0',
     177            ),
     178            array(
     179                'name'  => 'Tertiary',
     180                'slug'  => 'tertiary',
     181                'color' => '#00F',
     182            ),
     183        );
     184        add_theme_support( 'editor-color-palette', $color_palette );
     185        add_theme_support( 'custom-line-height' );
     186
     187        $settings = WP_Theme_JSON_Resolver::get_theme_data()->get_settings();
     188
     189        remove_theme_support( 'custom-line-height' );
     190        remove_theme_support( 'editor-color-palette' );
     191
     192        $this->assertFalse( WP_Theme_JSON_Resolver::theme_has_support() );
     193        $this->assertTrue( $settings['typography']['lineHeight'] );
     194        $this->assertSame( $color_palette, $settings['color']['palette']['theme'] );
     195    }
     196
     197    /**
     198     * Recursively applies ksort to an array.
     199     */
     200    private static function recursive_ksort( &$array ) {
     201        foreach ( $array as &$value ) {
     202            if ( is_array( $value ) ) {
     203                self::recursive_ksort( $value );
     204            }
     205        }
     206        ksort( $array );
     207    }
     208
     209    /**
     210     * @ticket 54336
     211     */
     212    function test_merges_child_theme_json_into_parent_theme_json() {
     213        switch_theme( 'block-theme-child' );
     214
     215        $actual_settings   = WP_Theme_JSON_Resolver::get_theme_data()->get_settings();
     216        $expected_settings = array(
     217            'color'      => array(
     218                'custom'         => false,
     219                'customGradient' => false,
     220                'gradients'      => array(
     221                    'theme' => array(
     222                        array(
     223                            'name'     => 'Custom gradient',
     224                            'gradient' => 'linear-gradient(135deg,rgba(0,0,0) 0%,rgb(0,0,0) 100%)',
     225                            'slug'     => 'custom-gradient',
     226                        ),
     227                    ),
     228                ),
     229                'palette'        => array(
     230                    'theme' => array(
     231                        array(
     232                            'slug'  => 'light',
     233                            'name'  => 'Light',
     234                            'color' => '#f3f4f6',
     235                        ),
     236                        array(
     237                            'slug'  => 'primary',
     238                            'name'  => 'Primary',
     239                            'color' => '#3858e9',
     240                        ),
     241                        array(
     242                            'slug'  => 'dark',
     243                            'name'  => 'Dark',
     244                            'color' => '#111827',
     245                        ),
     246                    ),
     247                ),
     248                'link'           => true,
     249            ),
     250            'typography' => array(
     251                'customFontSize' => false,
     252                'lineHeight'     => true,
     253                'fontSizes'      => array(
     254                    'theme' => array(
     255                        array(
     256                            'name' => 'Custom',
     257                            'slug' => 'custom',
     258                            'size' => '100px',
     259                        ),
     260                    ),
     261                ),
     262            ),
     263            'spacing'    => array(
     264                'units'   => array( 'rem' ),
     265                'padding' => true,
     266            ),
     267            'blocks'     => array(
     268                'core/paragraph'  => array(
     269                    'color' => array(
     270                        'palette' => array(
     271                            'theme' => array(
     272                                array(
     273                                    'slug'  => 'light',
     274                                    'name'  => 'Light',
     275                                    'color' => '#f5f7f9',
     276                                ),
     277                            ),
     278                        ),
     279                    ),
     280                ),
     281                'core/post-title' => array(
     282                    'color' => array(
     283                        'palette' => array(
     284                            'theme' => array(
     285                                array(
     286                                    'slug'  => 'light',
     287                                    'name'  => 'Light',
     288                                    'color' => '#f3f4f6',
     289                                ),
     290                            ),
     291                        ),
     292                    ),
     293                ),
     294            ),
     295        );
     296        self::recursive_ksort( $actual_settings );
     297        self::recursive_ksort( $expected_settings );
     298
     299        // Should merge settings.
     300        $this->assertSame(
     301            $expected_settings,
     302            $actual_settings
     303        );
     304
     305        $this->assertSame(
     306            WP_Theme_JSON_Resolver::get_theme_data()->get_custom_templates(),
     307            array(
     308                'page-home' => array(
     309                    'title'     => 'Homepage',
     310                    'postTypes' => array( 'page' ),
     311                ),
     312            )
     313        );
     314    }
    146315}
Note: See TracChangeset for help on using the changeset viewer.