wp/wp-includes/class-wp-theme-json.php
changeset 21 48c4eec2b7e6
parent 19 3d72ae0968f4
child 22 8c2e4d02f4ef
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
    14  * This is a low-level API that may need to do breaking changes. Please,
    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.
    15  * use get_global_settings, get_global_styles, and get_global_stylesheet instead.
    16  *
    16  *
    17  * @access private
    17  * @access private
    18  */
    18  */
       
    19 #[AllowDynamicProperties]
    19 class WP_Theme_JSON {
    20 class WP_Theme_JSON {
    20 
    21 
    21 	/**
    22 	/**
    22 	 * Container of data in theme.json format.
    23 	 * Container of data in theme.json format.
    23 	 *
    24 	 *
    30 	 * Holds block metadata extracted from block.json
    31 	 * Holds block metadata extracted from block.json
    31 	 * to be shared among all instances so we don't
    32 	 * to be shared among all instances so we don't
    32 	 * process it twice.
    33 	 * process it twice.
    33 	 *
    34 	 *
    34 	 * @since 5.8.0
    35 	 * @since 5.8.0
       
    36 	 * @since 6.1.0 Initialize as an empty array.
    35 	 * @var array
    37 	 * @var array
    36 	 */
    38 	 */
    37 	protected static $blocks_metadata = null;
    39 	protected static $blocks_metadata = array();
       
    40 
       
    41 	/**
       
    42 	 * The CSS selector for the top-level preset settings.
       
    43 	 *
       
    44 	 * @since 6.6.0
       
    45 	 * @var string
       
    46 	 */
       
    47 	const ROOT_CSS_PROPERTIES_SELECTOR = ':root';
    38 
    48 
    39 	/**
    49 	/**
    40 	 * The CSS selector for the top-level styles.
    50 	 * The CSS selector for the top-level styles.
    41 	 *
    51 	 *
    42 	 * @since 5.8.0
    52 	 * @since 5.8.0
    46 
    56 
    47 	/**
    57 	/**
    48 	 * The sources of data this object can represent.
    58 	 * The sources of data this object can represent.
    49 	 *
    59 	 *
    50 	 * @since 5.8.0
    60 	 * @since 5.8.0
       
    61 	 * @since 6.1.0 Added 'blocks'.
    51 	 * @var string[]
    62 	 * @var string[]
    52 	 */
    63 	 */
    53 	const VALID_ORIGINS = array(
    64 	const VALID_ORIGINS = array(
    54 		'default',
    65 		'default',
       
    66 		'blocks',
    55 		'theme',
    67 		'theme',
    56 		'custom',
    68 		'custom',
    57 	);
    69 	);
    58 
    70 
    59 	/**
    71 	/**
    60 	 * Presets are a set of values that serve
    72 	 * Presets are a set of values that serve
    61 	 * to bootstrap some styles: colors, font sizes, etc.
    73 	 * to bootstrap some styles: colors, font sizes, etc.
    62 	 *
    74 	 *
    63 	 * They are a unkeyed array of values such as:
    75 	 * They are a unkeyed array of values such as:
    64 	 *
    76 	 *
    65 	 * ```php
    77 	 *     array(
    66 	 * array(
    78 	 *       array(
    67 	 *   array(
    79 	 *         'slug'      => 'unique-name-within-the-set',
    68 	 *     'slug'      => 'unique-name-within-the-set',
    80 	 *         'name'      => 'Name for the UI',
    69 	 *     'name'      => 'Name for the UI',
    81 	 *         <value_key> => 'value'
    70 	 *     <value_key> => 'value'
    82 	 *       ),
    71 	 *   ),
    83 	 *     )
    72 	 * )
       
    73 	 * ```
       
    74 	 *
    84 	 *
    75 	 * This contains the necessary metadata to process them:
    85 	 * This contains the necessary metadata to process them:
    76 	 *
    86 	 *
    77 	 * - path             => Where to find the preset within the settings section.
    87 	 * - path             => Where to find the preset within the settings section.
    78 	 * - prevent_override => Disables override of default presets by theme presets.
    88 	 * - prevent_override => Disables override of default presets by theme presets.
    79 	 *                       The relationship between whether to override the defaults
    89 	 *                       The relationship between whether to override the defaults
    80 	 *                       and whether the defaults are enabled is inverse:
    90 	 *                       and whether the defaults are enabled is inverse:
    81 	 *                         - If defaults are enabled  => theme presets should not be overriden
    91 	 *                         - If defaults are enabled  => theme presets should not be overridden
    82 	 *                         - If defaults are disabled => theme presets should be overriden
    92 	 *                         - If defaults are disabled => theme presets should be overridden
    83 	 *                       For example, a theme sets defaultPalette to false,
    93 	 *                       For example, a theme sets defaultPalette to false,
    84 	 *                       making the default palette hidden from the user.
    94 	 *                       making the default palette hidden from the user.
    85 	 *                       In that case, we want all the theme presets to be present,
    95 	 *                       In that case, we want all the theme presets to be present,
    86 	 *                       so they should override the defaults by setting this false.
    96 	 *                       so they should override the defaults by setting this false.
    87 	 * - use_default_names => whether to use the default names
    97 	 * - use_default_names => whether to use the default names
   108 	 *
   118 	 *
   109 	 * @since 5.8.0
   119 	 * @since 5.8.0
   110 	 * @since 5.9.0 Added the `color.duotone` and `typography.fontFamilies` presets,
   120 	 * @since 5.9.0 Added the `color.duotone` and `typography.fontFamilies` presets,
   111 	 *              `use_default_names` preset key, and simplified the metadata structure.
   121 	 *              `use_default_names` preset key, and simplified the metadata structure.
   112 	 * @since 6.0.0 Replaced `override` with `prevent_override` and updated the
   122 	 * @since 6.0.0 Replaced `override` with `prevent_override` and updated the
   113 	 *              `prevent_overried` value for `color.duotone` to use `color.defaultDuotone`.
   123 	 *              `prevent_override` value for `color.duotone` to use `color.defaultDuotone`.
       
   124 	 * @since 6.2.0 Added 'shadow' presets.
       
   125 	 * @since 6.3.0 Replaced value_func for duotone with `null`. Custom properties are handled by class-wp-duotone.php.
       
   126 	 * @since 6.6.0 Added the `dimensions.aspectRatios` and `dimensions.defaultAspectRatios` presets.
       
   127 	 *              Updated the 'prevent_override' value for font size presets to use 'typography.defaultFontSizes'
       
   128 	 *              and spacing size presets to use `spacing.defaultSpacingSizes`.
   114 	 * @var array
   129 	 * @var array
   115 	 */
   130 	 */
   116 	const PRESETS_METADATA = array(
   131 	const PRESETS_METADATA = array(
       
   132 		array(
       
   133 			'path'              => array( 'dimensions', 'aspectRatios' ),
       
   134 			'prevent_override'  => array( 'dimensions', 'defaultAspectRatios' ),
       
   135 			'use_default_names' => false,
       
   136 			'value_key'         => 'ratio',
       
   137 			'css_vars'          => '--wp--preset--aspect-ratio--$slug',
       
   138 			'classes'           => array(),
       
   139 			'properties'        => array( 'aspect-ratio' ),
       
   140 		),
   117 		array(
   141 		array(
   118 			'path'              => array( 'color', 'palette' ),
   142 			'path'              => array( 'color', 'palette' ),
   119 			'prevent_override'  => array( 'color', 'defaultPalette' ),
   143 			'prevent_override'  => array( 'color', 'defaultPalette' ),
   120 			'use_default_names' => false,
   144 			'use_default_names' => false,
   121 			'value_key'         => 'color',
   145 			'value_key'         => 'color',
   138 		),
   162 		),
   139 		array(
   163 		array(
   140 			'path'              => array( 'color', 'duotone' ),
   164 			'path'              => array( 'color', 'duotone' ),
   141 			'prevent_override'  => array( 'color', 'defaultDuotone' ),
   165 			'prevent_override'  => array( 'color', 'defaultDuotone' ),
   142 			'use_default_names' => false,
   166 			'use_default_names' => false,
   143 			'value_func'        => 'wp_get_duotone_filter_property',
   167 			'value_func'        => null, // CSS Custom Properties for duotone are handled by block supports in class-wp-duotone.php.
   144 			'css_vars'          => '--wp--preset--duotone--$slug',
   168 			'css_vars'          => null,
   145 			'classes'           => array(),
   169 			'classes'           => array(),
   146 			'properties'        => array( 'filter' ),
   170 			'properties'        => array( 'filter' ),
   147 		),
   171 		),
   148 		array(
   172 		array(
   149 			'path'              => array( 'typography', 'fontSizes' ),
   173 			'path'              => array( 'typography', 'fontSizes' ),
   150 			'prevent_override'  => false,
   174 			'prevent_override'  => array( 'typography', 'defaultFontSizes' ),
   151 			'use_default_names' => true,
   175 			'use_default_names' => true,
   152 			'value_key'         => 'size',
   176 			'value_func'        => 'wp_get_typography_font_size_value',
   153 			'css_vars'          => '--wp--preset--font-size--$slug',
   177 			'css_vars'          => '--wp--preset--font-size--$slug',
   154 			'classes'           => array( '.has-$slug-font-size' => 'font-size' ),
   178 			'classes'           => array( '.has-$slug-font-size' => 'font-size' ),
   155 			'properties'        => array( 'font-size' ),
   179 			'properties'        => array( 'font-size' ),
   156 		),
   180 		),
   157 		array(
   181 		array(
   161 			'value_key'         => 'fontFamily',
   185 			'value_key'         => 'fontFamily',
   162 			'css_vars'          => '--wp--preset--font-family--$slug',
   186 			'css_vars'          => '--wp--preset--font-family--$slug',
   163 			'classes'           => array( '.has-$slug-font-family' => 'font-family' ),
   187 			'classes'           => array( '.has-$slug-font-family' => 'font-family' ),
   164 			'properties'        => array( 'font-family' ),
   188 			'properties'        => array( 'font-family' ),
   165 		),
   189 		),
       
   190 		array(
       
   191 			'path'              => array( 'spacing', 'spacingSizes' ),
       
   192 			'prevent_override'  => array( 'spacing', 'defaultSpacingSizes' ),
       
   193 			'use_default_names' => true,
       
   194 			'value_key'         => 'size',
       
   195 			'css_vars'          => '--wp--preset--spacing--$slug',
       
   196 			'classes'           => array(),
       
   197 			'properties'        => array( 'padding', 'margin' ),
       
   198 		),
       
   199 		array(
       
   200 			'path'              => array( 'shadow', 'presets' ),
       
   201 			'prevent_override'  => array( 'shadow', 'defaultPresets' ),
       
   202 			'use_default_names' => false,
       
   203 			'value_key'         => 'shadow',
       
   204 			'css_vars'          => '--wp--preset--shadow--$slug',
       
   205 			'classes'           => array(),
       
   206 			'properties'        => array( 'box-shadow' ),
       
   207 		),
   166 	);
   208 	);
   167 
   209 
   168 	/**
   210 	/**
   169 	 * Metadata for style properties.
   211 	 * Metadata for style properties.
   170 	 *
   212 	 *
   174 	 * @since 5.8.0
   216 	 * @since 5.8.0
   175 	 * @since 5.9.0 Added the `border-*`, `font-family`, `font-style`, `font-weight`,
   217 	 * @since 5.9.0 Added the `border-*`, `font-family`, `font-style`, `font-weight`,
   176 	 *              `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`,
   218 	 *              `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`,
   177 	 *              `text-decoration`, `text-transform`, and `filter` properties,
   219 	 *              `text-decoration`, `text-transform`, and `filter` properties,
   178 	 *              simplified the metadata structure.
   220 	 *              simplified the metadata structure.
       
   221 	 * @since 6.1.0 Added the `border-*-color`, `border-*-width`, `border-*-style`,
       
   222 	 *              `--wp--style--root--padding-*`, and `box-shadow` properties,
       
   223 	 *              removed the `--wp--style--block-gap` property.
       
   224 	 * @since 6.2.0 Added `outline-*`, and `min-height` properties.
       
   225 	 * @since 6.3.0 Added `column-count` property.
       
   226 	 * @since 6.4.0 Added `writing-mode` property.
       
   227 	 * @since 6.5.0 Added `aspect-ratio` property.
       
   228 	 * @since 6.6.0 Added `background-[image|position|repeat|size]` properties.
       
   229 	 *
   179 	 * @var array
   230 	 * @var array
   180 	 */
   231 	 */
   181 	const PROPERTIES_METADATA = array(
   232 	const PROPERTIES_METADATA = array(
   182 		'background'                 => array( 'color', 'gradient' ),
   233 		'aspect-ratio'                      => array( 'dimensions', 'aspectRatio' ),
   183 		'background-color'           => array( 'color', 'background' ),
   234 		'background'                        => array( 'color', 'gradient' ),
   184 		'border-radius'              => array( 'border', 'radius' ),
   235 		'background-color'                  => array( 'color', 'background' ),
   185 		'border-top-left-radius'     => array( 'border', 'radius', 'topLeft' ),
   236 		'background-image'                  => array( 'background', 'backgroundImage' ),
   186 		'border-top-right-radius'    => array( 'border', 'radius', 'topRight' ),
   237 		'background-position'               => array( 'background', 'backgroundPosition' ),
   187 		'border-bottom-left-radius'  => array( 'border', 'radius', 'bottomLeft' ),
   238 		'background-repeat'                 => array( 'background', 'backgroundRepeat' ),
   188 		'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ),
   239 		'background-size'                   => array( 'background', 'backgroundSize' ),
   189 		'border-color'               => array( 'border', 'color' ),
   240 		'border-radius'                     => array( 'border', 'radius' ),
   190 		'border-width'               => array( 'border', 'width' ),
   241 		'border-top-left-radius'            => array( 'border', 'radius', 'topLeft' ),
   191 		'border-style'               => array( 'border', 'style' ),
   242 		'border-top-right-radius'           => array( 'border', 'radius', 'topRight' ),
   192 		'color'                      => array( 'color', 'text' ),
   243 		'border-bottom-left-radius'         => array( 'border', 'radius', 'bottomLeft' ),
   193 		'font-family'                => array( 'typography', 'fontFamily' ),
   244 		'border-bottom-right-radius'        => array( 'border', 'radius', 'bottomRight' ),
   194 		'font-size'                  => array( 'typography', 'fontSize' ),
   245 		'border-color'                      => array( 'border', 'color' ),
   195 		'font-style'                 => array( 'typography', 'fontStyle' ),
   246 		'border-width'                      => array( 'border', 'width' ),
   196 		'font-weight'                => array( 'typography', 'fontWeight' ),
   247 		'border-style'                      => array( 'border', 'style' ),
   197 		'letter-spacing'             => array( 'typography', 'letterSpacing' ),
   248 		'border-top-color'                  => array( 'border', 'top', 'color' ),
   198 		'line-height'                => array( 'typography', 'lineHeight' ),
   249 		'border-top-width'                  => array( 'border', 'top', 'width' ),
   199 		'margin'                     => array( 'spacing', 'margin' ),
   250 		'border-top-style'                  => array( 'border', 'top', 'style' ),
   200 		'margin-top'                 => array( 'spacing', 'margin', 'top' ),
   251 		'border-right-color'                => array( 'border', 'right', 'color' ),
   201 		'margin-right'               => array( 'spacing', 'margin', 'right' ),
   252 		'border-right-width'                => array( 'border', 'right', 'width' ),
   202 		'margin-bottom'              => array( 'spacing', 'margin', 'bottom' ),
   253 		'border-right-style'                => array( 'border', 'right', 'style' ),
   203 		'margin-left'                => array( 'spacing', 'margin', 'left' ),
   254 		'border-bottom-color'               => array( 'border', 'bottom', 'color' ),
   204 		'padding'                    => array( 'spacing', 'padding' ),
   255 		'border-bottom-width'               => array( 'border', 'bottom', 'width' ),
   205 		'padding-top'                => array( 'spacing', 'padding', 'top' ),
   256 		'border-bottom-style'               => array( 'border', 'bottom', 'style' ),
   206 		'padding-right'              => array( 'spacing', 'padding', 'right' ),
   257 		'border-left-color'                 => array( 'border', 'left', 'color' ),
   207 		'padding-bottom'             => array( 'spacing', 'padding', 'bottom' ),
   258 		'border-left-width'                 => array( 'border', 'left', 'width' ),
   208 		'padding-left'               => array( 'spacing', 'padding', 'left' ),
   259 		'border-left-style'                 => array( 'border', 'left', 'style' ),
   209 		'--wp--style--block-gap'     => array( 'spacing', 'blockGap' ),
   260 		'color'                             => array( 'color', 'text' ),
   210 		'text-decoration'            => array( 'typography', 'textDecoration' ),
   261 		'text-align'                        => array( 'typography', 'textAlign' ),
   211 		'text-transform'             => array( 'typography', 'textTransform' ),
   262 		'column-count'                      => array( 'typography', 'textColumns' ),
   212 		'filter'                     => array( 'filter', 'duotone' ),
   263 		'font-family'                       => array( 'typography', 'fontFamily' ),
       
   264 		'font-size'                         => array( 'typography', 'fontSize' ),
       
   265 		'font-style'                        => array( 'typography', 'fontStyle' ),
       
   266 		'font-weight'                       => array( 'typography', 'fontWeight' ),
       
   267 		'letter-spacing'                    => array( 'typography', 'letterSpacing' ),
       
   268 		'line-height'                       => array( 'typography', 'lineHeight' ),
       
   269 		'margin'                            => array( 'spacing', 'margin' ),
       
   270 		'margin-top'                        => array( 'spacing', 'margin', 'top' ),
       
   271 		'margin-right'                      => array( 'spacing', 'margin', 'right' ),
       
   272 		'margin-bottom'                     => array( 'spacing', 'margin', 'bottom' ),
       
   273 		'margin-left'                       => array( 'spacing', 'margin', 'left' ),
       
   274 		'min-height'                        => array( 'dimensions', 'minHeight' ),
       
   275 		'outline-color'                     => array( 'outline', 'color' ),
       
   276 		'outline-offset'                    => array( 'outline', 'offset' ),
       
   277 		'outline-style'                     => array( 'outline', 'style' ),
       
   278 		'outline-width'                     => array( 'outline', 'width' ),
       
   279 		'padding'                           => array( 'spacing', 'padding' ),
       
   280 		'padding-top'                       => array( 'spacing', 'padding', 'top' ),
       
   281 		'padding-right'                     => array( 'spacing', 'padding', 'right' ),
       
   282 		'padding-bottom'                    => array( 'spacing', 'padding', 'bottom' ),
       
   283 		'padding-left'                      => array( 'spacing', 'padding', 'left' ),
       
   284 		'--wp--style--root--padding'        => array( 'spacing', 'padding' ),
       
   285 		'--wp--style--root--padding-top'    => array( 'spacing', 'padding', 'top' ),
       
   286 		'--wp--style--root--padding-right'  => array( 'spacing', 'padding', 'right' ),
       
   287 		'--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
       
   288 		'--wp--style--root--padding-left'   => array( 'spacing', 'padding', 'left' ),
       
   289 		'text-decoration'                   => array( 'typography', 'textDecoration' ),
       
   290 		'text-transform'                    => array( 'typography', 'textTransform' ),
       
   291 		'filter'                            => array( 'filter', 'duotone' ),
       
   292 		'box-shadow'                        => array( 'shadow' ),
       
   293 		'writing-mode'                      => array( 'typography', 'writingMode' ),
       
   294 	);
       
   295 
       
   296 	/**
       
   297 	 * Indirect metadata for style properties that are not directly output.
       
   298 	 *
       
   299 	 * Each element maps from a CSS property name to an array of
       
   300 	 * paths to the value in theme.json & block attributes.
       
   301 	 *
       
   302 	 * Indirect properties are not output directly by `compute_style_properties`,
       
   303 	 * but are used elsewhere in the processing of global styles. The indirect
       
   304 	 * property is used to validate whether a style value is allowed.
       
   305 	 *
       
   306 	 * @since 6.2.0
       
   307 	 * @since 6.6.0 Added background-image properties.
       
   308 	 *
       
   309 	 * @var array
       
   310 	 */
       
   311 	const INDIRECT_PROPERTIES_METADATA = array(
       
   312 		'gap'              => array(
       
   313 			array( 'spacing', 'blockGap' ),
       
   314 		),
       
   315 		'column-gap'       => array(
       
   316 			array( 'spacing', 'blockGap', 'left' ),
       
   317 		),
       
   318 		'row-gap'          => array(
       
   319 			array( 'spacing', 'blockGap', 'top' ),
       
   320 		),
       
   321 		'max-width'        => array(
       
   322 			array( 'layout', 'contentSize' ),
       
   323 			array( 'layout', 'wideSize' ),
       
   324 		),
       
   325 		'background-image' => array(
       
   326 			array( 'background', 'backgroundImage', 'url' ),
       
   327 		),
   213 	);
   328 	);
   214 
   329 
   215 	/**
   330 	/**
   216 	 * Protected style properties.
   331 	 * Protected style properties.
   217 	 *
   332 	 *
   231 	 * The top-level keys a theme.json can have.
   346 	 * The top-level keys a theme.json can have.
   232 	 *
   347 	 *
   233 	 * @since 5.8.0 As `ALLOWED_TOP_LEVEL_KEYS`.
   348 	 * @since 5.8.0 As `ALLOWED_TOP_LEVEL_KEYS`.
   234 	 * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`,
   349 	 * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`,
   235 	 *              added the `customTemplates` and `templateParts` values.
   350 	 *              added the `customTemplates` and `templateParts` values.
       
   351 	 * @since 6.3.0 Added the `description` value.
       
   352 	 * @since 6.6.0 Added `blockTypes` to support block style variation theme.json partials.
   236 	 * @var string[]
   353 	 * @var string[]
   237 	 */
   354 	 */
   238 	const VALID_TOP_LEVEL_KEYS = array(
   355 	const VALID_TOP_LEVEL_KEYS = array(
       
   356 		'blockTypes',
   239 		'customTemplates',
   357 		'customTemplates',
       
   358 		'description',
   240 		'patterns',
   359 		'patterns',
   241 		'settings',
   360 		'settings',
       
   361 		'slug',
   242 		'styles',
   362 		'styles',
   243 		'templateParts',
   363 		'templateParts',
       
   364 		'title',
   244 		'version',
   365 		'version',
   245 		'title',
       
   246 	);
   366 	);
   247 
   367 
   248 	/**
   368 	/**
   249 	 * The valid properties under the settings key.
   369 	 * The valid properties under the settings key.
   250 	 *
   370 	 *
   251 	 * @since 5.8.0 As `ALLOWED_SETTINGS`.
   371 	 * @since 5.8.0 As `ALLOWED_SETTINGS`.
   252 	 * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`,
   372 	 * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`,
   253 	 *              added new properties for `border`, `color`, `spacing`,
   373 	 *              added new properties for `border`, `color`, `spacing`,
   254 	 *              and `typography`, and renamed others according to the new schema.
   374 	 *              and `typography`, and renamed others according to the new schema.
   255 	 * @since 6.0.0 Added `color.defaultDuotone`.
   375 	 * @since 6.0.0 Added `color.defaultDuotone`.
       
   376 	 * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`.
       
   377 	 * @since 6.2.0 Added `dimensions.minHeight`, 'shadow.presets', 'shadow.defaultPresets',
       
   378 	 *              `position.fixed` and `position.sticky`.
       
   379 	 * @since 6.3.0 Added support for `typography.textColumns`, removed `layout.definitions`.
       
   380 	 * @since 6.4.0 Added support for `layout.allowEditing`, `background.backgroundImage`,
       
   381 	 *              `typography.writingMode`, `lightbox.enabled` and `lightbox.allowEditing`.
       
   382 	 * @since 6.5.0 Added support for `layout.allowCustomContentAndWideSize`,
       
   383 	 *              `background.backgroundSize` and `dimensions.aspectRatio`.
       
   384 	 * @since 6.6.0 Added support for 'dimensions.aspectRatios', 'dimensions.defaultAspectRatios',
       
   385 	 *              'typography.defaultFontSizes', and 'spacing.defaultSpacingSizes'.
   256 	 * @var array
   386 	 * @var array
   257 	 */
   387 	 */
   258 	const VALID_SETTINGS = array(
   388 	const VALID_SETTINGS = array(
   259 		'appearanceTools' => null,
   389 		'appearanceTools'               => null,
   260 		'border'          => array(
   390 		'useRootPaddingAwareAlignments' => null,
       
   391 		'background'                    => array(
       
   392 			'backgroundImage' => null,
       
   393 			'backgroundSize'  => null,
       
   394 		),
       
   395 		'border'                        => array(
   261 			'color'  => null,
   396 			'color'  => null,
   262 			'radius' => null,
   397 			'radius' => null,
   263 			'style'  => null,
   398 			'style'  => null,
   264 			'width'  => null,
   399 			'width'  => null,
   265 		),
   400 		),
   266 		'color'           => array(
   401 		'color'                         => array(
   267 			'background'       => null,
   402 			'background'       => null,
   268 			'custom'           => null,
   403 			'custom'           => null,
   269 			'customDuotone'    => null,
   404 			'customDuotone'    => null,
   270 			'customGradient'   => null,
   405 			'customGradient'   => null,
   271 			'defaultDuotone'   => null,
   406 			'defaultDuotone'   => null,
   272 			'defaultGradients' => null,
   407 			'defaultGradients' => null,
   273 			'defaultPalette'   => null,
   408 			'defaultPalette'   => null,
   274 			'duotone'          => null,
   409 			'duotone'          => null,
   275 			'gradients'        => null,
   410 			'gradients'        => null,
   276 			'link'             => null,
   411 			'link'             => null,
       
   412 			'heading'          => null,
       
   413 			'button'           => null,
       
   414 			'caption'          => null,
   277 			'palette'          => null,
   415 			'palette'          => null,
   278 			'text'             => null,
   416 			'text'             => null,
   279 		),
   417 		),
   280 		'custom'          => null,
   418 		'custom'                        => null,
   281 		'layout'          => array(
   419 		'dimensions'                    => array(
   282 			'contentSize' => null,
   420 			'aspectRatio'         => null,
   283 			'wideSize'    => null,
   421 			'aspectRatios'        => null,
       
   422 			'defaultAspectRatios' => null,
       
   423 			'minHeight'           => null,
   284 		),
   424 		),
   285 		'spacing'         => array(
   425 		'layout'                        => array(
   286 			'blockGap' => null,
   426 			'contentSize'                   => null,
   287 			'margin'   => null,
   427 			'wideSize'                      => null,
   288 			'padding'  => null,
   428 			'allowEditing'                  => null,
   289 			'units'    => null,
   429 			'allowCustomContentAndWideSize' => null,
   290 		),
   430 		),
   291 		'typography'      => array(
   431 		'lightbox'                      => array(
   292 			'customFontSize' => null,
   432 			'enabled'      => null,
   293 			'dropCap'        => null,
   433 			'allowEditing' => null,
   294 			'fontFamilies'   => null,
   434 		),
   295 			'fontSizes'      => null,
   435 		'position'                      => array(
   296 			'fontStyle'      => null,
   436 			'fixed'  => null,
   297 			'fontWeight'     => null,
   437 			'sticky' => null,
   298 			'letterSpacing'  => null,
   438 		),
   299 			'lineHeight'     => null,
   439 		'spacing'                       => array(
   300 			'textDecoration' => null,
   440 			'customSpacingSize'   => null,
   301 			'textTransform'  => null,
   441 			'defaultSpacingSizes' => null,
       
   442 			'spacingSizes'        => null,
       
   443 			'spacingScale'        => null,
       
   444 			'blockGap'            => null,
       
   445 			'margin'              => null,
       
   446 			'padding'             => null,
       
   447 			'units'               => null,
       
   448 		),
       
   449 		'shadow'                        => array(
       
   450 			'presets'        => null,
       
   451 			'defaultPresets' => null,
       
   452 		),
       
   453 		'typography'                    => array(
       
   454 			'fluid'            => null,
       
   455 			'customFontSize'   => null,
       
   456 			'defaultFontSizes' => null,
       
   457 			'dropCap'          => null,
       
   458 			'fontFamilies'     => null,
       
   459 			'fontSizes'        => null,
       
   460 			'fontStyle'        => null,
       
   461 			'fontWeight'       => null,
       
   462 			'letterSpacing'    => null,
       
   463 			'lineHeight'       => null,
       
   464 			'textAlign'        => null,
       
   465 			'textColumns'      => null,
       
   466 			'textDecoration'   => null,
       
   467 			'textTransform'    => null,
       
   468 			'writingMode'      => null,
       
   469 		),
       
   470 	);
       
   471 
       
   472 	/*
       
   473 	 * The valid properties for fontFamilies under settings key.
       
   474 	 *
       
   475 	 * @since 6.5.0
       
   476 	 *
       
   477 	 * @var array
       
   478 	 */
       
   479 	const FONT_FAMILY_SCHEMA = array(
       
   480 		array(
       
   481 			'fontFamily' => null,
       
   482 			'name'       => null,
       
   483 			'slug'       => null,
       
   484 			'fontFace'   => array(
       
   485 				array(
       
   486 					'ascentOverride'        => null,
       
   487 					'descentOverride'       => null,
       
   488 					'fontDisplay'           => null,
       
   489 					'fontFamily'            => null,
       
   490 					'fontFeatureSettings'   => null,
       
   491 					'fontStyle'             => null,
       
   492 					'fontStretch'           => null,
       
   493 					'fontVariationSettings' => null,
       
   494 					'fontWeight'            => null,
       
   495 					'lineGapOverride'       => null,
       
   496 					'sizeAdjust'            => null,
       
   497 					'src'                   => null,
       
   498 					'unicodeRange'          => null,
       
   499 				),
       
   500 			),
   302 		),
   501 		),
   303 	);
   502 	);
   304 
   503 
   305 	/**
   504 	/**
   306 	 * The valid properties under the styles key.
   505 	 * The valid properties under the styles key.
   307 	 *
   506 	 *
   308 	 * @since 5.8.0 As `ALLOWED_STYLES`.
   507 	 * @since 5.8.0 As `ALLOWED_STYLES`.
   309 	 * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`,
   508 	 * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`,
   310 	 *              added new properties for `border`, `filter`, `spacing`,
   509 	 *              added new properties for `border`, `filter`, `spacing`,
   311 	 *              and `typography`.
   510 	 *              and `typography`.
       
   511 	 * @since 6.1.0 Added new side properties for `border`,
       
   512 	 *              added new property `shadow`,
       
   513 	 *              updated `blockGap` to be allowed at any level.
       
   514 	 * @since 6.2.0 Added `outline`, and `minHeight` properties.
       
   515 	 * @since 6.3.0 Added support for `typography.textColumns`.
       
   516 	 * @since 6.5.0 Added support for `dimensions.aspectRatio`.
       
   517 	 * @since 6.6.0 Added `background` sub properties to top-level only.
       
   518 	 *
   312 	 * @var array
   519 	 * @var array
   313 	 */
   520 	 */
   314 	const VALID_STYLES = array(
   521 	const VALID_STYLES = array(
       
   522 		'background' => array(
       
   523 			'backgroundImage'    => 'top',
       
   524 			'backgroundPosition' => 'top',
       
   525 			'backgroundRepeat'   => 'top',
       
   526 			'backgroundSize'     => 'top',
       
   527 		),
   315 		'border'     => array(
   528 		'border'     => array(
   316 			'color'  => null,
   529 			'color'  => null,
   317 			'radius' => null,
   530 			'radius' => null,
   318 			'style'  => null,
   531 			'style'  => null,
   319 			'width'  => null,
   532 			'width'  => null,
       
   533 			'top'    => null,
       
   534 			'right'  => null,
       
   535 			'bottom' => null,
       
   536 			'left'   => null,
   320 		),
   537 		),
   321 		'color'      => array(
   538 		'color'      => array(
   322 			'background' => null,
   539 			'background' => null,
   323 			'gradient'   => null,
   540 			'gradient'   => null,
   324 			'text'       => null,
   541 			'text'       => null,
   325 		),
   542 		),
       
   543 		'dimensions' => array(
       
   544 			'aspectRatio' => null,
       
   545 			'minHeight'   => null,
       
   546 		),
   326 		'filter'     => array(
   547 		'filter'     => array(
   327 			'duotone' => null,
   548 			'duotone' => null,
   328 		),
   549 		),
       
   550 		'outline'    => array(
       
   551 			'color'  => null,
       
   552 			'offset' => null,
       
   553 			'style'  => null,
       
   554 			'width'  => null,
       
   555 		),
       
   556 		'shadow'     => null,
   329 		'spacing'    => array(
   557 		'spacing'    => array(
   330 			'margin'   => null,
   558 			'margin'   => null,
   331 			'padding'  => null,
   559 			'padding'  => null,
   332 			'blockGap' => 'top',
   560 			'blockGap' => null,
   333 		),
   561 		),
   334 		'typography' => array(
   562 		'typography' => array(
   335 			'fontFamily'     => null,
   563 			'fontFamily'     => null,
   336 			'fontSize'       => null,
   564 			'fontSize'       => null,
   337 			'fontStyle'      => null,
   565 			'fontStyle'      => null,
   338 			'fontWeight'     => null,
   566 			'fontWeight'     => null,
   339 			'letterSpacing'  => null,
   567 			'letterSpacing'  => null,
   340 			'lineHeight'     => null,
   568 			'lineHeight'     => null,
       
   569 			'textAlign'      => null,
       
   570 			'textColumns'    => null,
   341 			'textDecoration' => null,
   571 			'textDecoration' => null,
   342 			'textTransform'  => null,
   572 			'textTransform'  => null,
       
   573 			'writingMode'    => null,
   343 		),
   574 		),
       
   575 		'css'        => null,
   344 	);
   576 	);
   345 
   577 
   346 	/**
   578 	/**
       
   579 	 * Defines which pseudo selectors are enabled for which elements.
       
   580 	 *
       
   581 	 * The order of the selectors should be: link, any-link, visited, hover, focus, active.
       
   582 	 * This is to ensure the user action (hover, focus and active) styles have a higher
       
   583 	 * specificity than the visited styles, which in turn have a higher specificity than
       
   584 	 * the unvisited styles.
       
   585 	 *
       
   586 	 * See https://core.trac.wordpress.org/ticket/56928.
       
   587 	 * Note: this will affect both top-level and block-level elements.
       
   588 	 *
       
   589 	 * @since 6.1.0
       
   590 	 * @since 6.2.0 Added support for ':link' and ':any-link'.
       
   591 	 */
       
   592 	const VALID_ELEMENT_PSEUDO_SELECTORS = array(
       
   593 		'link'   => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ),
       
   594 		'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ),
       
   595 	);
       
   596 
       
   597 	/**
   347 	 * The valid elements that can be found under styles.
   598 	 * The valid elements that can be found under styles.
   348 	 *
   599 	 *
   349 	 * @since 5.8.0
   600 	 * @since 5.8.0
       
   601 	 * @since 6.1.0 Added `heading`, `button`, and `caption` elements.
   350 	 * @var string[]
   602 	 * @var string[]
   351 	 */
   603 	 */
   352 	const ELEMENTS = array(
   604 	const ELEMENTS = array(
   353 		'link' => 'a',
   605 		'link'    => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity.
   354 		'h1'   => 'h1',
   606 		'heading' => 'h1, h2, h3, h4, h5, h6',
   355 		'h2'   => 'h2',
   607 		'h1'      => 'h1',
   356 		'h3'   => 'h3',
   608 		'h2'      => 'h2',
   357 		'h4'   => 'h4',
   609 		'h3'      => 'h3',
   358 		'h5'   => 'h5',
   610 		'h4'      => 'h4',
   359 		'h6'   => 'h6',
   611 		'h5'      => 'h5',
       
   612 		'h6'      => 'h6',
       
   613 		// We have the .wp-block-button__link class so that this will target older buttons that have been serialized.
       
   614 		'button'  => '.wp-element-button, .wp-block-button__link',
       
   615 		// The block classes are necessary to target older content that won't use the new class names.
       
   616 		'caption' => '.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption',
       
   617 		'cite'    => 'cite',
   360 	);
   618 	);
   361 
   619 
       
   620 	const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array(
       
   621 		'button'  => 'wp-element-button',
       
   622 		'caption' => 'wp-element-caption',
       
   623 	);
       
   624 
       
   625 	/**
       
   626 	 * List of block support features that can have their related styles
       
   627 	 * generated under their own feature level selector rather than the block's.
       
   628 	 *
       
   629 	 * @since 6.1.0
       
   630 	 * @var string[]
       
   631 	 */
       
   632 	const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array(
       
   633 		'__experimentalBorder' => 'border',
       
   634 		'color'                => 'color',
       
   635 		'spacing'              => 'spacing',
       
   636 		'typography'           => 'typography',
       
   637 	);
       
   638 
       
   639 	/**
       
   640 	 * Return the input schema at the root and per origin.
       
   641 	 *
       
   642 	 * @since 6.5.0
       
   643 	 *
       
   644 	 * @param array $schema The base schema.
       
   645 	 * @return array The schema at the root and per origin.
       
   646 	 *
       
   647 	 * Example:
       
   648 	 * schema_in_root_and_per_origin(
       
   649 	 *   array(
       
   650 	 *    'fontFamily' => null,
       
   651 	 *    'slug' => null,
       
   652 	 *   )
       
   653 	 * )
       
   654 	 *
       
   655 	 * Returns:
       
   656 	 * array(
       
   657 	 *  'fontFamily' => null,
       
   658 	 *  'slug' => null,
       
   659 	 *  'default' => array(
       
   660 	 *    'fontFamily' => null,
       
   661 	 *    'slug' => null,
       
   662 	 *  ),
       
   663 	 *  'blocks' => array(
       
   664 	 *    'fontFamily' => null,
       
   665 	 *    'slug' => null,
       
   666 	 *  ),
       
   667 	 *  'theme' => array(
       
   668 	 *     'fontFamily' => null,
       
   669 	 *     'slug' => null,
       
   670 	 *  ),
       
   671 	 *  'custom' => array(
       
   672 	 *     'fontFamily' => null,
       
   673 	 *     'slug' => null,
       
   674 	 *  ),
       
   675 	 * )
       
   676 	 */
       
   677 	protected static function schema_in_root_and_per_origin( $schema ) {
       
   678 		$schema_in_root_and_per_origin = $schema;
       
   679 		foreach ( static::VALID_ORIGINS as $origin ) {
       
   680 			$schema_in_root_and_per_origin[ $origin ] = $schema;
       
   681 		}
       
   682 		return $schema_in_root_and_per_origin;
       
   683 	}
       
   684 
       
   685 	/**
       
   686 	 * Returns a class name by an element name.
       
   687 	 *
       
   688 	 * @since 6.1.0
       
   689 	 *
       
   690 	 * @param string $element The name of the element.
       
   691 	 * @return string The name of the class.
       
   692 	 */
       
   693 	public static function get_element_class_name( $element ) {
       
   694 		$class_name = '';
       
   695 
       
   696 		if ( isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ] ) ) {
       
   697 			$class_name = static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ];
       
   698 		}
       
   699 
       
   700 		return $class_name;
       
   701 	}
       
   702 
   362 	/**
   703 	/**
   363 	 * Options that settings.appearanceTools enables.
   704 	 * Options that settings.appearanceTools enables.
   364 	 *
   705 	 *
   365 	 * @since 6.0.0
   706 	 * @since 6.0.0
       
   707 	 * @since 6.2.0 Added `dimensions.minHeight` and `position.sticky`.
       
   708 	 * @since 6.4.0 Added `background.backgroundImage`.
       
   709 	 * @since 6.5.0 Added `background.backgroundSize` and `dimensions.aspectRatio`.
   366 	 * @var array
   710 	 * @var array
   367 	 */
   711 	 */
   368 	const APPEARANCE_TOOLS_OPT_INS = array(
   712 	const APPEARANCE_TOOLS_OPT_INS = array(
       
   713 		array( 'background', 'backgroundImage' ),
       
   714 		array( 'background', 'backgroundSize' ),
   369 		array( 'border', 'color' ),
   715 		array( 'border', 'color' ),
   370 		array( 'border', 'radius' ),
   716 		array( 'border', 'radius' ),
   371 		array( 'border', 'style' ),
   717 		array( 'border', 'style' ),
   372 		array( 'border', 'width' ),
   718 		array( 'border', 'width' ),
   373 		array( 'color', 'link' ),
   719 		array( 'color', 'link' ),
       
   720 		array( 'color', 'heading' ),
       
   721 		array( 'color', 'button' ),
       
   722 		array( 'color', 'caption' ),
       
   723 		array( 'dimensions', 'aspectRatio' ),
       
   724 		array( 'dimensions', 'minHeight' ),
       
   725 		array( 'position', 'sticky' ),
   374 		array( 'spacing', 'blockGap' ),
   726 		array( 'spacing', 'blockGap' ),
   375 		array( 'spacing', 'margin' ),
   727 		array( 'spacing', 'margin' ),
   376 		array( 'spacing', 'padding' ),
   728 		array( 'spacing', 'padding' ),
   377 		array( 'typography', 'lineHeight' ),
   729 		array( 'typography', 'lineHeight' ),
   378 	);
   730 	);
   380 	/**
   732 	/**
   381 	 * The latest version of the schema in use.
   733 	 * The latest version of the schema in use.
   382 	 *
   734 	 *
   383 	 * @since 5.8.0
   735 	 * @since 5.8.0
   384 	 * @since 5.9.0 Changed value from 1 to 2.
   736 	 * @since 5.9.0 Changed value from 1 to 2.
       
   737 	 * @since 6.6.0 Changed value from 2 to 3.
   385 	 * @var int
   738 	 * @var int
   386 	 */
   739 	 */
   387 	const LATEST_SCHEMA = 2;
   740 	const LATEST_SCHEMA = 3;
   388 
   741 
   389 	/**
   742 	/**
   390 	 * Constructor.
   743 	 * Constructor.
   391 	 *
   744 	 *
   392 	 * @since 5.8.0
   745 	 * @since 5.8.0
       
   746 	 * @since 6.6.0 Key spacingScale by origin, and Pre-generate the spacingSizes from spacingScale.
       
   747 	 *              Added unwrapping of shared block style variations into block type variations if registered.
   393 	 *
   748 	 *
   394 	 * @param array  $theme_json A structure that follows the theme.json schema.
   749 	 * @param array  $theme_json A structure that follows the theme.json schema.
   395 	 * @param string $origin     Optional. What source of data this object represents.
   750 	 * @param string $origin     Optional. What source of data this object represents.
   396 	 *                           One of 'default', 'theme', or 'custom'. Default 'theme'.
   751 	 *                           One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
   397 	 */
   752 	 */
   398 	public function __construct( $theme_json = array(), $origin = 'theme' ) {
   753 	public function __construct( $theme_json = array( 'version' => self::LATEST_SCHEMA ), $origin = 'theme' ) {
   399 		if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
   754 		if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
   400 			$origin = 'theme';
   755 			$origin = 'theme';
   401 		}
   756 		}
   402 
   757 
   403 		$this->theme_json    = WP_Theme_JSON_Schema::migrate( $theme_json );
   758 		$this->theme_json    = WP_Theme_JSON_Schema::migrate( $theme_json, $origin );
   404 		$valid_block_names   = array_keys( static::get_blocks_metadata() );
   759 		$valid_block_names   = array_keys( static::get_blocks_metadata() );
   405 		$valid_element_names = array_keys( static::ELEMENTS );
   760 		$valid_element_names = array_keys( static::ELEMENTS );
   406 		$theme_json          = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names );
   761 		$valid_variations    = static::get_valid_block_style_variations();
   407 		$this->theme_json    = static::maybe_opt_in_into_settings( $theme_json );
   762 		$this->theme_json    = static::unwrap_shared_block_style_variations( $this->theme_json, $valid_variations );
       
   763 		$this->theme_json    = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations );
       
   764 		$this->theme_json    = static::maybe_opt_in_into_settings( $this->theme_json );
   408 
   765 
   409 		// Internally, presets are keyed by origin.
   766 		// Internally, presets are keyed by origin.
   410 		$nodes = static::get_setting_nodes( $this->theme_json );
   767 		$nodes = static::get_setting_nodes( $this->theme_json );
   411 		foreach ( $nodes as $node ) {
   768 		foreach ( $nodes as $node ) {
   412 			foreach ( static::PRESETS_METADATA as $preset_metadata ) {
   769 			foreach ( static::PRESETS_METADATA as $preset_metadata ) {
   413 				$path   = array_merge( $node['path'], $preset_metadata['path'] );
   770 				$path = $node['path'];
       
   771 				foreach ( $preset_metadata['path'] as $subpath ) {
       
   772 					$path[] = $subpath;
       
   773 				}
   414 				$preset = _wp_array_get( $this->theme_json, $path, null );
   774 				$preset = _wp_array_get( $this->theme_json, $path, null );
   415 				if ( null !== $preset ) {
   775 				if ( null !== $preset ) {
   416 					// If the preset is not already keyed by origin.
   776 					// If the preset is not already keyed by origin.
   417 					if ( isset( $preset[0] ) || empty( $preset ) ) {
   777 					if ( isset( $preset[0] ) || empty( $preset ) ) {
   418 						_wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
   778 						_wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
   419 					}
   779 					}
   420 				}
   780 				}
   421 			}
   781 			}
   422 		}
   782 		}
       
   783 
       
   784 		// In addition to presets, spacingScale (which generates presets) is also keyed by origin.
       
   785 		$scale_path    = array( 'settings', 'spacing', 'spacingScale' );
       
   786 		$spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null );
       
   787 		if ( null !== $spacing_scale ) {
       
   788 			// If the spacingScale is not already keyed by origin.
       
   789 			if ( empty( array_intersect( array_keys( $spacing_scale ), static::VALID_ORIGINS ) ) ) {
       
   790 				_wp_array_set( $this->theme_json, $scale_path, array( $origin => $spacing_scale ) );
       
   791 			}
       
   792 		}
       
   793 
       
   794 		// Pre-generate the spacingSizes from spacingScale.
       
   795 		$scale_path    = array( 'settings', 'spacing', 'spacingScale', $origin );
       
   796 		$spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null );
       
   797 		if ( isset( $spacing_scale ) ) {
       
   798 			$sizes_path           = array( 'settings', 'spacing', 'spacingSizes', $origin );
       
   799 			$spacing_sizes        = _wp_array_get( $this->theme_json, $sizes_path, array() );
       
   800 			$spacing_scale_sizes  = static::compute_spacing_sizes( $spacing_scale );
       
   801 			$merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );
       
   802 			_wp_array_set( $this->theme_json, $sizes_path, $merged_spacing_sizes );
       
   803 		}
       
   804 	}
       
   805 
       
   806 	/**
       
   807 	 * Unwraps shared block style variations.
       
   808 	 *
       
   809 	 * It takes the shared variations (styles.variations.variationName) and
       
   810 	 * applies them to all the blocks that have the given variation registered
       
   811 	 * (styles.blocks.blockType.variations.variationName).
       
   812 	 *
       
   813 	 * For example, given the `core/paragraph` and `core/group` blocks have
       
   814 	 * registered the `section-a` style variation, and given the following input:
       
   815 	 *
       
   816 	 * {
       
   817 	 *   "styles": {
       
   818 	 *     "variations": {
       
   819 	 *       "section-a": { "color": { "background": "backgroundColor" } }
       
   820 	 *     }
       
   821 	 *   }
       
   822 	 * }
       
   823 	 *
       
   824 	 * It returns the following output:
       
   825 	 *
       
   826 	 * {
       
   827 	 *   "styles": {
       
   828 	 *     "blocks": {
       
   829 	 *       "core/paragraph": {
       
   830 	 *         "variations": {
       
   831 	 *             "section-a": { "color": { "background": "backgroundColor" } }
       
   832 	 *         },
       
   833 	 *       },
       
   834 	 *       "core/group": {
       
   835 	 *         "variations": {
       
   836 	 *           "section-a": { "color": { "background": "backgroundColor" } }
       
   837 	 *         }
       
   838 	 *       }
       
   839 	 *     }
       
   840 	 *   }
       
   841 	 * }
       
   842 	 *
       
   843 	 * @since 6.6.0
       
   844 	 *
       
   845 	 * @param array $theme_json       A structure that follows the theme.json schema.
       
   846 	 * @param array $valid_variations Valid block style variations.
       
   847 	 * @return array Theme json data with shared variation definitions unwrapped under appropriate block types.
       
   848 	 */
       
   849 	private static function unwrap_shared_block_style_variations( $theme_json, $valid_variations ) {
       
   850 		if ( empty( $theme_json['styles']['variations'] ) || empty( $valid_variations ) ) {
       
   851 			return $theme_json;
       
   852 		}
       
   853 
       
   854 		$new_theme_json = $theme_json;
       
   855 		$variations     = $new_theme_json['styles']['variations'];
       
   856 
       
   857 		foreach ( $valid_variations as $block_type => $registered_variations ) {
       
   858 			foreach ( $registered_variations as $variation_name ) {
       
   859 				$block_level_data = $new_theme_json['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array();
       
   860 				$top_level_data   = $variations[ $variation_name ] ?? array();
       
   861 				$merged_data      = array_replace_recursive( $top_level_data, $block_level_data );
       
   862 				if ( ! empty( $merged_data ) ) {
       
   863 					_wp_array_set( $new_theme_json, array( 'styles', 'blocks', $block_type, 'variations', $variation_name ), $merged_data );
       
   864 				}
       
   865 			}
       
   866 		}
       
   867 
       
   868 		unset( $new_theme_json['styles']['variations'] );
       
   869 
       
   870 		return $new_theme_json;
   423 	}
   871 	}
   424 
   872 
   425 	/**
   873 	/**
   426 	 * Enables some opt-in settings if theme declared support.
   874 	 * Enables some opt-in settings if theme declared support.
   427 	 *
   875 	 *
   458 	 *
   906 	 *
   459 	 * @param array $context The context to which the settings belong.
   907 	 * @param array $context The context to which the settings belong.
   460 	 */
   908 	 */
   461 	protected static function do_opt_in_into_settings( &$context ) {
   909 	protected static function do_opt_in_into_settings( &$context ) {
   462 		foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) {
   910 		foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) {
   463 			// Use "unset prop" as a marker instead of "null" because
   911 			/*
   464 			// "null" can be a valid value for some props (e.g. blockGap).
   912 			 * Use "unset prop" as a marker instead of "null" because
       
   913 			 * "null" can be a valid value for some props (e.g. blockGap).
       
   914 			 */
   465 			if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) {
   915 			if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) {
   466 				_wp_array_set( $context, $path, true );
   916 				_wp_array_set( $context, $path, true );
   467 			}
   917 			}
   468 		}
   918 		}
   469 
   919 
   473 	/**
   923 	/**
   474 	 * Sanitizes the input according to the schemas.
   924 	 * Sanitizes the input according to the schemas.
   475 	 *
   925 	 *
   476 	 * @since 5.8.0
   926 	 * @since 5.8.0
   477 	 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
   927 	 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
       
   928 	 * @since 6.3.0 Added the `$valid_variations` parameter.
       
   929 	 * @since 6.6.0 Updated schema to allow extended block style variations.
   478 	 *
   930 	 *
   479 	 * @param array $input               Structure to sanitize.
   931 	 * @param array $input               Structure to sanitize.
   480 	 * @param array $valid_block_names   List of valid block names.
   932 	 * @param array $valid_block_names   List of valid block names.
   481 	 * @param array $valid_element_names List of valid element names.
   933 	 * @param array $valid_element_names List of valid element names.
       
   934 	 * @param array $valid_variations    List of valid variations per block.
   482 	 * @return array The sanitized output.
   935 	 * @return array The sanitized output.
   483 	 */
   936 	 */
   484 	protected static function sanitize( $input, $valid_block_names, $valid_element_names ) {
   937 	protected static function sanitize( $input, $valid_block_names, $valid_element_names, $valid_variations ) {
       
   938 
   485 		$output = array();
   939 		$output = array();
   486 
   940 
   487 		if ( ! is_array( $input ) ) {
   941 		if ( ! is_array( $input ) ) {
   488 			return $output;
   942 			return $output;
   489 		}
   943 		}
   490 
   944 
       
   945 		// Preserve only the top most level keys.
   491 		$output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) );
   946 		$output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) );
   492 
   947 
   493 		// Some styles are only meant to be available at the top-level (e.g.: blockGap),
   948 		/*
   494 		// hence, the schema for blocks & elements should not have them.
   949 		 * Remove any rules that are annotated as "top" in VALID_STYLES constant.
       
   950 		 * Some styles are only meant to be available at the top-level (e.g.: blockGap),
       
   951 		 * hence, the schema for blocks & elements should not have them.
       
   952 		 */
   495 		$styles_non_top_level = static::VALID_STYLES;
   953 		$styles_non_top_level = static::VALID_STYLES;
   496 		foreach ( array_keys( $styles_non_top_level ) as $section ) {
   954 		foreach ( array_keys( $styles_non_top_level ) as $section ) {
   497 			foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) {
   955 			// array_key_exists() needs to be used instead of isset() because the value can be null.
   498 				if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) {
   956 			if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $section ] ) ) {
   499 					unset( $styles_non_top_level[ $section ][ $prop ] );
   957 				foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) {
       
   958 					if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) {
       
   959 						unset( $styles_non_top_level[ $section ][ $prop ] );
       
   960 					}
   500 				}
   961 				}
   501 			}
   962 			}
   502 		}
   963 		}
   503 
   964 
   504 		// Build the schema based on valid block & element names.
   965 		// Build the schema based on valid block & element names.
   505 		$schema                 = array();
   966 		$schema                 = array();
   506 		$schema_styles_elements = array();
   967 		$schema_styles_elements = array();
       
   968 
       
   969 		/*
       
   970 		 * Set allowed element pseudo selectors based on per element allow list.
       
   971 		 * Target data structure in schema:
       
   972 		 * e.g.
       
   973 		 * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
       
   974 		 * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
       
   975 		 */
   507 		foreach ( $valid_element_names as $element ) {
   976 		foreach ( $valid_element_names as $element ) {
   508 			$schema_styles_elements[ $element ] = $styles_non_top_level;
   977 			$schema_styles_elements[ $element ] = $styles_non_top_level;
   509 		}
   978 
       
   979 			if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
       
   980 				foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
       
   981 					$schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level;
       
   982 				}
       
   983 			}
       
   984 		}
       
   985 
   510 		$schema_styles_blocks   = array();
   986 		$schema_styles_blocks   = array();
   511 		$schema_settings_blocks = array();
   987 		$schema_settings_blocks = array();
       
   988 
       
   989 		/*
       
   990 		 * Generate a schema for blocks.
       
   991 		 * - Block styles can contain `elements` & `variations` definitions.
       
   992 		 * - Variations definitions cannot be nested.
       
   993 		 * - Variations can contain styles for inner `blocks`.
       
   994 		 * - Variation inner `blocks` styles can contain `elements`.
       
   995 		 *
       
   996 		 * As each variation needs a `blocks` schema but further nested
       
   997 		 * inner `blocks`, the overall schema will be generated in multiple passes.
       
   998 		 */
   512 		foreach ( $valid_block_names as $block ) {
   999 		foreach ( $valid_block_names as $block ) {
   513 			$schema_settings_blocks[ $block ]           = static::VALID_SETTINGS;
  1000 			$schema_settings_blocks[ $block ]           = static::VALID_SETTINGS;
   514 			$schema_styles_blocks[ $block ]             = $styles_non_top_level;
  1001 			$schema_styles_blocks[ $block ]             = $styles_non_top_level;
   515 			$schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
  1002 			$schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
   516 		}
  1003 		}
   517 		$schema['styles']             = static::VALID_STYLES;
  1004 
   518 		$schema['styles']['blocks']   = $schema_styles_blocks;
  1005 		$block_style_variation_styles             = static::VALID_STYLES;
   519 		$schema['styles']['elements'] = $schema_styles_elements;
  1006 		$block_style_variation_styles['blocks']   = $schema_styles_blocks;
   520 		$schema['settings']           = static::VALID_SETTINGS;
  1007 		$block_style_variation_styles['elements'] = $schema_styles_elements;
   521 		$schema['settings']['blocks'] = $schema_settings_blocks;
  1008 
       
  1009 		foreach ( $valid_block_names as $block ) {
       
  1010 			// Build the schema for each block style variation.
       
  1011 			$style_variation_names = array();
       
  1012 			if (
       
  1013 				! empty( $input['styles']['blocks'][ $block ]['variations'] ) &&
       
  1014 				is_array( $input['styles']['blocks'][ $block ]['variations'] ) &&
       
  1015 				isset( $valid_variations[ $block ] )
       
  1016 			) {
       
  1017 				$style_variation_names = array_intersect(
       
  1018 					array_keys( $input['styles']['blocks'][ $block ]['variations'] ),
       
  1019 					$valid_variations[ $block ]
       
  1020 				);
       
  1021 			}
       
  1022 
       
  1023 			$schema_styles_variations = array();
       
  1024 			if ( ! empty( $style_variation_names ) ) {
       
  1025 				$schema_styles_variations = array_fill_keys( $style_variation_names, $block_style_variation_styles );
       
  1026 			}
       
  1027 
       
  1028 			$schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations;
       
  1029 		}
       
  1030 
       
  1031 		$schema['styles']                                 = static::VALID_STYLES;
       
  1032 		$schema['styles']['blocks']                       = $schema_styles_blocks;
       
  1033 		$schema['styles']['elements']                     = $schema_styles_elements;
       
  1034 		$schema['settings']                               = static::VALID_SETTINGS;
       
  1035 		$schema['settings']['blocks']                     = $schema_settings_blocks;
       
  1036 		$schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA );
   522 
  1037 
   523 		// Remove anything that's not present in the schema.
  1038 		// Remove anything that's not present in the schema.
   524 		foreach ( array( 'styles', 'settings' ) as $subtree ) {
  1039 		foreach ( array( 'styles', 'settings' ) as $subtree ) {
   525 			if ( ! isset( $input[ $subtree ] ) ) {
  1040 			if ( ! isset( $input[ $subtree ] ) ) {
   526 				continue;
  1041 				continue;
   534 			$result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
  1049 			$result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
   535 
  1050 
   536 			if ( empty( $result ) ) {
  1051 			if ( empty( $result ) ) {
   537 				unset( $output[ $subtree ] );
  1052 				unset( $output[ $subtree ] );
   538 			} else {
  1053 			} else {
   539 				$output[ $subtree ] = $result;
  1054 				$output[ $subtree ] = static::resolve_custom_css_format( $result );
   540 			}
  1055 			}
   541 		}
  1056 		}
   542 
  1057 
   543 		return $output;
  1058 		return $output;
       
  1059 	}
       
  1060 
       
  1061 	/**
       
  1062 	 * Appends a sub-selector to an existing one.
       
  1063 	 *
       
  1064 	 * Given the compounded $selector "h1, h2, h3"
       
  1065 	 * and the $to_append selector ".some-class" the result will be
       
  1066 	 * "h1.some-class, h2.some-class, h3.some-class".
       
  1067 	 *
       
  1068 	 * @since 5.8.0
       
  1069 	 * @since 6.1.0 Added append position.
       
  1070 	 * @since 6.3.0 Removed append position parameter.
       
  1071 	 *
       
  1072 	 * @param string $selector  Original selector.
       
  1073 	 * @param string $to_append Selector to append.
       
  1074 	 * @return string The new selector.
       
  1075 	 */
       
  1076 	protected static function append_to_selector( $selector, $to_append ) {
       
  1077 		if ( ! str_contains( $selector, ',' ) ) {
       
  1078 			return $selector . $to_append;
       
  1079 		}
       
  1080 		$new_selectors = array();
       
  1081 		$selectors     = explode( ',', $selector );
       
  1082 		foreach ( $selectors as $sel ) {
       
  1083 			$new_selectors[] = $sel . $to_append;
       
  1084 		}
       
  1085 		return implode( ',', $new_selectors );
       
  1086 	}
       
  1087 
       
  1088 	/**
       
  1089 	 * Prepends a sub-selector to an existing one.
       
  1090 	 *
       
  1091 	 * Given the compounded $selector "h1, h2, h3"
       
  1092 	 * and the $to_prepend selector ".some-class " the result will be
       
  1093 	 * ".some-class h1, .some-class  h2, .some-class  h3".
       
  1094 	 *
       
  1095 	 * @since 6.3.0
       
  1096 	 *
       
  1097 	 * @param string $selector   Original selector.
       
  1098 	 * @param string $to_prepend Selector to prepend.
       
  1099 	 * @return string The new selector.
       
  1100 	 */
       
  1101 	protected static function prepend_to_selector( $selector, $to_prepend ) {
       
  1102 		if ( ! str_contains( $selector, ',' ) ) {
       
  1103 			return $to_prepend . $selector;
       
  1104 		}
       
  1105 		$new_selectors = array();
       
  1106 		$selectors     = explode( ',', $selector );
       
  1107 		foreach ( $selectors as $sel ) {
       
  1108 			$new_selectors[] = $to_prepend . $sel;
       
  1109 		}
       
  1110 		return implode( ',', $new_selectors );
   544 	}
  1111 	}
   545 
  1112 
   546 	/**
  1113 	/**
   547 	 * Returns the metadata for each block.
  1114 	 * Returns the metadata for each block.
   548 	 *
  1115 	 *
   567 	 *       }
  1134 	 *       }
   568 	 *     }
  1135 	 *     }
   569 	 *
  1136 	 *
   570 	 * @since 5.8.0
  1137 	 * @since 5.8.0
   571 	 * @since 5.9.0 Added `duotone` key with CSS selector.
  1138 	 * @since 5.9.0 Added `duotone` key with CSS selector.
       
  1139 	 * @since 6.1.0 Added `features` key with block support feature level selectors.
       
  1140 	 * @since 6.3.0 Refactored and stabilized selectors API.
       
  1141 	 * @since 6.6.0 Updated to include block style variations from the block styles registry.
   572 	 *
  1142 	 *
   573 	 * @return array Block metadata.
  1143 	 * @return array Block metadata.
   574 	 */
  1144 	 */
   575 	protected static function get_blocks_metadata() {
  1145 	protected static function get_blocks_metadata() {
   576 		if ( null !== static::$blocks_metadata ) {
  1146 		$registry       = WP_Block_Type_Registry::get_instance();
       
  1147 		$blocks         = $registry->get_all_registered();
       
  1148 		$style_registry = WP_Block_Styles_Registry::get_instance();
       
  1149 
       
  1150 		// Is there metadata for all currently registered blocks?
       
  1151 		$blocks = array_diff_key( $blocks, static::$blocks_metadata );
       
  1152 		if ( empty( $blocks ) ) {
       
  1153 			/*
       
  1154 			 * New block styles may have been registered within WP_Block_Styles_Registry.
       
  1155 			 * Update block metadata for any new block style variations.
       
  1156 			 */
       
  1157 			$registered_styles = $style_registry->get_all_registered();
       
  1158 			foreach ( static::$blocks_metadata as $block_name => $block_metadata ) {
       
  1159 				if ( ! empty( $registered_styles[ $block_name ] ) ) {
       
  1160 					$style_selectors = $block_metadata['styleVariations'] ?? array();
       
  1161 
       
  1162 					foreach ( $registered_styles[ $block_name ] as $block_style ) {
       
  1163 						if ( ! isset( $style_selectors[ $block_style['name'] ] ) ) {
       
  1164 							$style_selectors[ $block_style['name'] ] = static::get_block_style_variation_selector( $block_style['name'], $block_metadata['selector'] );
       
  1165 						}
       
  1166 					}
       
  1167 
       
  1168 					static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
       
  1169 				}
       
  1170 			}
   577 			return static::$blocks_metadata;
  1171 			return static::$blocks_metadata;
   578 		}
  1172 		}
   579 
  1173 
   580 		static::$blocks_metadata = array();
       
   581 
       
   582 		$registry = WP_Block_Type_Registry::get_instance();
       
   583 		$blocks   = $registry->get_all_registered();
       
   584 		foreach ( $blocks as $block_name => $block_type ) {
  1174 		foreach ( $blocks as $block_name => $block_type ) {
   585 			if (
  1175 			$root_selector = wp_get_block_css_selector( $block_type );
   586 				isset( $block_type->supports['__experimentalSelector'] ) &&
  1176 
   587 				is_string( $block_type->supports['__experimentalSelector'] )
  1177 			static::$blocks_metadata[ $block_name ]['selector']  = $root_selector;
   588 			) {
  1178 			static::$blocks_metadata[ $block_name ]['selectors'] = static::get_block_selectors( $block_type, $root_selector );
   589 				static::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector'];
  1179 
   590 			} else {
  1180 			$elements = static::get_block_element_selectors( $root_selector );
   591 				static::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) );
  1181 			if ( ! empty( $elements ) ) {
   592 			}
  1182 				static::$blocks_metadata[ $block_name ]['elements'] = $elements;
   593 
  1183 			}
   594 			if (
  1184 
   595 				isset( $block_type->supports['color']['__experimentalDuotone'] ) &&
  1185 			// The block may or may not have a duotone selector.
   596 				is_string( $block_type->supports['color']['__experimentalDuotone'] )
  1186 			$duotone_selector = wp_get_block_css_selector( $block_type, 'filter.duotone' );
   597 			) {
  1187 
   598 				static::$blocks_metadata[ $block_name ]['duotone'] = $block_type->supports['color']['__experimentalDuotone'];
  1188 			// Keep backwards compatibility for support.color.__experimentalDuotone.
   599 			}
  1189 			if ( null === $duotone_selector ) {
   600 
  1190 				$duotone_support = isset( $block_type->supports['color']['__experimentalDuotone'] )
   601 			// Assign defaults, then overwrite those that the block sets by itself.
  1191 					? $block_type->supports['color']['__experimentalDuotone']
   602 			// If the block selector is compounded, will append the element to each
  1192 					: null;
   603 			// individual block selector.
  1193 
   604 			$block_selectors = explode( ',', static::$blocks_metadata[ $block_name ]['selector'] );
  1194 				if ( $duotone_support ) {
   605 			foreach ( static::ELEMENTS as $el_name => $el_selector ) {
  1195 					$root_selector    = wp_get_block_css_selector( $block_type );
   606 				$element_selector = array();
  1196 					$duotone_selector = static::scope_selector( $root_selector, $duotone_support );
   607 				foreach ( $block_selectors as $selector ) {
  1197 				}
   608 					$element_selector[] = $selector . ' ' . $el_selector;
  1198 			}
   609 				}
  1199 
   610 				static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector );
  1200 			if ( null !== $duotone_selector ) {
       
  1201 				static::$blocks_metadata[ $block_name ]['duotone'] = $duotone_selector;
       
  1202 			}
       
  1203 
       
  1204 			// If the block has style variations, append their selectors to the block metadata.
       
  1205 			$style_selectors = array();
       
  1206 			if ( ! empty( $block_type->styles ) ) {
       
  1207 				foreach ( $block_type->styles as $style ) {
       
  1208 					$style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
       
  1209 				}
       
  1210 			}
       
  1211 
       
  1212 			// Block style variations can be registered through the WP_Block_Styles_Registry as well as block.json.
       
  1213 			$registered_styles = $style_registry->get_registered_styles_for_block( $block_name );
       
  1214 			foreach ( $registered_styles as $style ) {
       
  1215 				$style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
       
  1216 			}
       
  1217 
       
  1218 			if ( ! empty( $style_selectors ) ) {
       
  1219 				static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
   611 			}
  1220 			}
   612 		}
  1221 		}
   613 
  1222 
   614 		return static::$blocks_metadata;
  1223 		return static::$blocks_metadata;
   615 	}
  1224 	}
   621 	 *
  1230 	 *
   622 	 * @since 5.8.0
  1231 	 * @since 5.8.0
   623 	 *
  1232 	 *
   624 	 * @param array $tree   Input to process.
  1233 	 * @param array $tree   Input to process.
   625 	 * @param array $schema Schema to adhere to.
  1234 	 * @param array $schema Schema to adhere to.
   626 	 * @return array Returns the modified $tree.
  1235 	 * @return array The modified $tree.
   627 	 */
  1236 	 */
   628 	protected static function remove_keys_not_in_schema( $tree, $schema ) {
  1237 	protected static function remove_keys_not_in_schema( $tree, $schema ) {
   629 		$tree = array_intersect_key( $tree, $schema );
  1238 		if ( ! is_array( $tree ) ) {
   630 
  1239 			return $tree;
   631 		foreach ( $schema as $key => $data ) {
  1240 		}
   632 			if ( ! isset( $tree[ $key ] ) ) {
  1241 
       
  1242 		foreach ( $tree as $key => $value ) {
       
  1243 			// Remove keys not in the schema or with null/empty values.
       
  1244 			if ( ! array_key_exists( $key, $schema ) ) {
       
  1245 				unset( $tree[ $key ] );
   633 				continue;
  1246 				continue;
   634 			}
  1247 			}
   635 
  1248 
   636 			if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) {
  1249 			if ( is_array( $schema[ $key ] ) ) {
   637 				$tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] );
  1250 				if ( ! is_array( $value ) ) {
   638 
       
   639 				if ( empty( $tree[ $key ] ) ) {
       
   640 					unset( $tree[ $key ] );
  1251 					unset( $tree[ $key ] );
   641 				}
  1252 				} elseif ( wp_is_numeric_array( $value ) ) {
   642 			} elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) {
  1253 					// If indexed, process each item in the array.
   643 				unset( $tree[ $key ] );
  1254 					foreach ( $value as $item_key => $item_value ) {
   644 			}
  1255 						if ( isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] ) ) {
   645 		}
  1256 							$tree[ $key ][ $item_key ] = self::remove_keys_not_in_schema( $item_value, $schema[ $key ][0] );
   646 
  1257 						} else {
       
  1258 							// If the schema does not define a further structure, keep the value as is.
       
  1259 							$tree[ $key ][ $item_key ] = $item_value;
       
  1260 						}
       
  1261 					}
       
  1262 				} else {
       
  1263 					// If associative, process as a single object.
       
  1264 					$tree[ $key ] = self::remove_keys_not_in_schema( $value, $schema[ $key ] );
       
  1265 
       
  1266 					if ( empty( $tree[ $key ] ) ) {
       
  1267 						unset( $tree[ $key ] );
       
  1268 					}
       
  1269 				}
       
  1270 			}
       
  1271 		}
   647 		return $tree;
  1272 		return $tree;
   648 	}
  1273 	}
   649 
  1274 
   650 	/**
  1275 	/**
   651 	 * Returns the existing settings for each block.
  1276 	 * Returns the existing settings for each block.
   680 	/**
  1305 	/**
   681 	 * Returns the stylesheet that results of processing
  1306 	 * Returns the stylesheet that results of processing
   682 	 * the theme.json structure this object represents.
  1307 	 * the theme.json structure this object represents.
   683 	 *
  1308 	 *
   684 	 * @since 5.8.0
  1309 	 * @since 5.8.0
   685 	 * @since 5.9.0 Removed the `$type` parameter`, added the `$types` and `$origins` parameters.
  1310 	 * @since 5.9.0 Removed the `$type` parameter, added the `$types` and `$origins` parameters.
   686 	 *
  1311 	 * @since 6.3.0 Add fallback layout styles for Post Template when block gap support isn't available.
   687 	 * @param array $types   Types of styles to load. Will load all by default. It accepts:
  1312 	 * @since 6.6.0 Added boolean `skip_root_layout_styles` and `include_block_style_variations` options
   688 	 *                       - `variables`: only the CSS Custom Properties for presets & custom ones.
  1313 	 *              to control styles output as desired.
   689 	 *                       - `styles`: only the styles section in theme.json.
  1314 	 *
   690 	 *                       - `presets`: only the classes for the presets.
  1315 	 * @param string[] $types   Types of styles to load. Will load all by default. It accepts:
   691 	 * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS.
  1316 	 *                          - `variables`: only the CSS Custom Properties for presets & custom ones.
   692 	 * @return string Stylesheet.
  1317 	 *                          - `styles`: only the styles section in theme.json.
   693 	 */
  1318 	 *                          - `presets`: only the classes for the presets.
   694 	public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null ) {
  1319 	 * @param string[] $origins A list of origins to include. By default it includes VALID_ORIGINS.
       
  1320 	 * @param array    $options {
       
  1321 	 *     Optional. An array of options for now used for internal purposes only (may change without notice).
       
  1322 	 *
       
  1323 	 *     @type string $scope                           Makes sure all style are scoped to a given selector
       
  1324 	 *     @type string $root_selector                   Overwrites and forces a given selector to be used on the root node
       
  1325 	 *     @type bool   $skip_root_layout_styles         Omits root layout styles from the generated stylesheet. Default false.
       
  1326 	 *     @type bool   $include_block_style_variations  Includes styles for block style variations in the generated stylesheet. Default false.
       
  1327 	 * }
       
  1328 	 * @return string The resulting stylesheet.
       
  1329 	 */
       
  1330 	public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) {
   695 		if ( null === $origins ) {
  1331 		if ( null === $origins ) {
   696 			$origins = static::VALID_ORIGINS;
  1332 			$origins = static::VALID_ORIGINS;
   697 		}
  1333 		}
   698 
  1334 
   699 		if ( is_string( $types ) ) {
  1335 		if ( is_string( $types ) ) {
   707 				$types = array( 'variables', 'styles', 'presets' );
  1343 				$types = array( 'variables', 'styles', 'presets' );
   708 			}
  1344 			}
   709 		}
  1345 		}
   710 
  1346 
   711 		$blocks_metadata = static::get_blocks_metadata();
  1347 		$blocks_metadata = static::get_blocks_metadata();
   712 		$style_nodes     = static::get_style_nodes( $this->theme_json, $blocks_metadata );
  1348 		$style_nodes     = static::get_style_nodes( $this->theme_json, $blocks_metadata, $options );
   713 		$setting_nodes   = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
  1349 		$setting_nodes   = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
       
  1350 
       
  1351 		$root_style_key    = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true );
       
  1352 		$root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true );
       
  1353 
       
  1354 		if ( ! empty( $options['scope'] ) ) {
       
  1355 			foreach ( $setting_nodes as &$node ) {
       
  1356 				$node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
       
  1357 			}
       
  1358 			foreach ( $style_nodes as &$node ) {
       
  1359 				$node = static::scope_style_node_selectors( $options['scope'], $node );
       
  1360 			}
       
  1361 			unset( $node );
       
  1362 		}
       
  1363 
       
  1364 		if ( ! empty( $options['root_selector'] ) ) {
       
  1365 			if ( false !== $root_settings_key ) {
       
  1366 				$setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector'];
       
  1367 			}
       
  1368 			if ( false !== $root_style_key ) {
       
  1369 				$style_nodes[ $root_style_key ]['selector'] = $options['root_selector'];
       
  1370 			}
       
  1371 		}
   714 
  1372 
   715 		$stylesheet = '';
  1373 		$stylesheet = '';
   716 
  1374 
   717 		if ( in_array( 'variables', $types, true ) ) {
  1375 		if ( in_array( 'variables', $types, true ) ) {
   718 			$stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
  1376 			$stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
   719 		}
  1377 		}
   720 
  1378 
   721 		if ( in_array( 'styles', $types, true ) ) {
  1379 		if ( in_array( 'styles', $types, true ) ) {
       
  1380 			if ( false !== $root_style_key && empty( $options['skip_root_layout_styles'] ) ) {
       
  1381 				$stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] );
       
  1382 			}
   722 			$stylesheet .= $this->get_block_classes( $style_nodes );
  1383 			$stylesheet .= $this->get_block_classes( $style_nodes );
       
  1384 		} elseif ( in_array( 'base-layout-styles', $types, true ) ) {
       
  1385 			$root_selector          = static::ROOT_BLOCK_SELECTOR;
       
  1386 			$columns_selector       = '.wp-block-columns';
       
  1387 			$post_template_selector = '.wp-block-post-template';
       
  1388 			if ( ! empty( $options['scope'] ) ) {
       
  1389 				$root_selector          = static::scope_selector( $options['scope'], $root_selector );
       
  1390 				$columns_selector       = static::scope_selector( $options['scope'], $columns_selector );
       
  1391 				$post_template_selector = static::scope_selector( $options['scope'], $post_template_selector );
       
  1392 			}
       
  1393 			if ( ! empty( $options['root_selector'] ) ) {
       
  1394 				$root_selector = $options['root_selector'];
       
  1395 			}
       
  1396 			/*
       
  1397 			 * Base layout styles are provided as part of `styles`, so only output separately if explicitly requested.
       
  1398 			 * For backwards compatibility, the Columns block is explicitly included, to support a different default gap value.
       
  1399 			 */
       
  1400 			$base_styles_nodes = array(
       
  1401 				array(
       
  1402 					'path'     => array( 'styles' ),
       
  1403 					'selector' => $root_selector,
       
  1404 				),
       
  1405 				array(
       
  1406 					'path'     => array( 'styles', 'blocks', 'core/columns' ),
       
  1407 					'selector' => $columns_selector,
       
  1408 					'name'     => 'core/columns',
       
  1409 				),
       
  1410 				array(
       
  1411 					'path'     => array( 'styles', 'blocks', 'core/post-template' ),
       
  1412 					'selector' => $post_template_selector,
       
  1413 					'name'     => 'core/post-template',
       
  1414 				),
       
  1415 			);
       
  1416 
       
  1417 			foreach ( $base_styles_nodes as $base_style_node ) {
       
  1418 				$stylesheet .= $this->get_layout_styles( $base_style_node, $types );
       
  1419 			}
   723 		}
  1420 		}
   724 
  1421 
   725 		if ( in_array( 'presets', $types, true ) ) {
  1422 		if ( in_array( 'presets', $types, true ) ) {
   726 			$stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
  1423 			$stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
       
  1424 		}
       
  1425 
       
  1426 		return $stylesheet;
       
  1427 	}
       
  1428 
       
  1429 	/**
       
  1430 	 * Processes the CSS, to apply nesting.
       
  1431 	 *
       
  1432 	 * @since 6.2.0
       
  1433 	 * @since 6.6.0 Enforced 0-1-0 specificity for block custom CSS selectors.
       
  1434 	 *
       
  1435 	 * @param string $css      The CSS to process.
       
  1436 	 * @param string $selector The selector to nest.
       
  1437 	 * @return string The processed CSS.
       
  1438 	 */
       
  1439 	protected function process_blocks_custom_css( $css, $selector ) {
       
  1440 		$processed_css = '';
       
  1441 
       
  1442 		if ( empty( $css ) ) {
       
  1443 			return $processed_css;
       
  1444 		}
       
  1445 
       
  1446 		// Split CSS nested rules.
       
  1447 		$parts = explode( '&', $css );
       
  1448 		foreach ( $parts as $part ) {
       
  1449 			if ( empty( $part ) ) {
       
  1450 				continue;
       
  1451 			}
       
  1452 			$is_root_css = ( ! str_contains( $part, '{' ) );
       
  1453 			if ( $is_root_css ) {
       
  1454 				// If the part doesn't contain braces, it applies to the root level.
       
  1455 				$processed_css .= ':root :where(' . trim( $selector ) . '){' . trim( $part ) . '}';
       
  1456 			} else {
       
  1457 				// If the part contains braces, it's a nested CSS rule.
       
  1458 				$part = explode( '{', str_replace( '}', '', $part ) );
       
  1459 				if ( count( $part ) !== 2 ) {
       
  1460 					continue;
       
  1461 				}
       
  1462 				$nested_selector = $part[0];
       
  1463 				$css_value       = $part[1];
       
  1464 
       
  1465 				/*
       
  1466 				 * Handle pseudo elements such as ::before, ::after etc. Regex will also
       
  1467 				 * capture any leading combinator such as >, +, or ~, as well as spaces.
       
  1468 				 * This allows pseudo elements as descendants e.g. `.parent ::before`.
       
  1469 				 */
       
  1470 				$matches            = array();
       
  1471 				$has_pseudo_element = preg_match( '/([>+~\s]*::[a-zA-Z-]+)/', $nested_selector, $matches );
       
  1472 				$pseudo_part        = $has_pseudo_element ? $matches[1] : '';
       
  1473 				$nested_selector    = $has_pseudo_element ? str_replace( $pseudo_part, '', $nested_selector ) : $nested_selector;
       
  1474 
       
  1475 				// Finalize selector and re-append pseudo element if required.
       
  1476 				$part_selector  = str_starts_with( $nested_selector, ' ' )
       
  1477 					? static::scope_selector( $selector, $nested_selector )
       
  1478 					: static::append_to_selector( $selector, $nested_selector );
       
  1479 				$final_selector = ":root :where($part_selector)$pseudo_part";
       
  1480 
       
  1481 				$processed_css .= $final_selector . '{' . trim( $css_value ) . '}';
       
  1482 			}
       
  1483 		}
       
  1484 		return $processed_css;
       
  1485 	}
       
  1486 
       
  1487 	/**
       
  1488 	 * Returns the global styles custom CSS.
       
  1489 	 *
       
  1490 	 * @since 6.2.0
       
  1491 	 *
       
  1492 	 * @return string The global styles custom CSS.
       
  1493 	 */
       
  1494 	public function get_custom_css() {
       
  1495 		// Add the global styles root CSS.
       
  1496 		$stylesheet = isset( $this->theme_json['styles']['css'] ) ? $this->theme_json['styles']['css'] : '';
       
  1497 
       
  1498 		// Add the global styles block CSS.
       
  1499 		if ( isset( $this->theme_json['styles']['blocks'] ) ) {
       
  1500 			foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) {
       
  1501 				$custom_block_css = isset( $this->theme_json['styles']['blocks'][ $name ]['css'] )
       
  1502 					? $this->theme_json['styles']['blocks'][ $name ]['css']
       
  1503 					: null;
       
  1504 				if ( $custom_block_css ) {
       
  1505 					$selector    = static::$blocks_metadata[ $name ]['selector'];
       
  1506 					$stylesheet .= $this->process_blocks_custom_css( $custom_block_css, $selector );
       
  1507 				}
       
  1508 			}
   727 		}
  1509 		}
   728 
  1510 
   729 		return $stylesheet;
  1511 		return $stylesheet;
   730 	}
  1512 	}
   731 
  1513 
   791 	 *
  1573 	 *
   792 	 * @since 5.8.0 As `get_block_styles()`.
  1574 	 * @since 5.8.0 As `get_block_styles()`.
   793 	 * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()`
  1575 	 * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()`
   794 	 *              and no longer returns preset classes.
  1576 	 *              and no longer returns preset classes.
   795 	 *              Removed the `$setting_nodes` parameter.
  1577 	 *              Removed the `$setting_nodes` parameter.
       
  1578 	 * @since 6.1.0 Moved most internal logic to `get_styles_for_block()`.
   796 	 *
  1579 	 *
   797 	 * @param array $style_nodes Nodes with styles.
  1580 	 * @param array $style_nodes Nodes with styles.
   798 	 * @return string The new stylesheet.
  1581 	 * @return string The new stylesheet.
   799 	 */
  1582 	 */
   800 	protected function get_block_classes( $style_nodes ) {
  1583 	protected function get_block_classes( $style_nodes ) {
   802 
  1585 
   803 		foreach ( $style_nodes as $metadata ) {
  1586 		foreach ( $style_nodes as $metadata ) {
   804 			if ( null === $metadata['selector'] ) {
  1587 			if ( null === $metadata['selector'] ) {
   805 				continue;
  1588 				continue;
   806 			}
  1589 			}
   807 
  1590 			$block_rules .= static::get_styles_for_block( $metadata );
   808 			$node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
  1591 		}
   809 			$selector     = $metadata['selector'];
  1592 
   810 			$settings     = _wp_array_get( $this->theme_json, array( 'settings' ) );
  1593 		return $block_rules;
   811 			$declarations = static::compute_style_properties( $node, $settings );
  1594 	}
   812 
  1595 
   813 			// 1. Separate the ones who use the general selector
  1596 	/**
   814 			// and the ones who use the duotone selector.
  1597 	 * Gets the CSS layout rules for a particular block from theme.json layout definitions.
   815 			$declarations_duotone = array();
  1598 	 *
   816 			foreach ( $declarations as $index => $declaration ) {
  1599 	 * @since 6.1.0
   817 				if ( 'filter' === $declaration['name'] ) {
  1600 	 * @since 6.3.0 Reduced specificity for layout margin rules.
   818 					unset( $declarations[ $index ] );
  1601 	 * @since 6.5.1 Only output rules referencing content and wide sizes when values exist.
   819 					$declarations_duotone[] = $declaration;
  1602 	 * @since 6.5.3 Add types parameter to check if only base layout styles are needed.
   820 				}
  1603 	 * @since 6.6.0 Updated layout style specificity to be compatible with overall 0-1-0 specificity in global styles.
   821 			}
  1604 	 *
   822 
  1605 	 * @param array $block_metadata Metadata about the block to get styles for.
   823 			/*
  1606 	 * @param array $types          Optional. Types of styles to output. If empty, all styles will be output.
   824 			 * Reset default browser margin on the root body element.
  1607 	 * @return string Layout styles for the block.
   825 			 * This is set on the root selector **before** generating the ruleset
  1608 	 */
   826 			 * from the `theme.json`. This is to ensure that if the `theme.json` declares
  1609 	protected function get_layout_styles( $block_metadata, $types = array() ) {
   827 			 * `margin` in its `spacing` declaration for the `body` element then these
  1610 		$block_rules = '';
   828 			 * user-generated values take precedence in the CSS cascade.
  1611 		$block_type  = null;
   829 			 * @link https://github.com/WordPress/gutenberg/issues/36147.
  1612 
   830 			 */
  1613 		// Skip outputting layout styles if explicitly disabled.
   831 			if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
  1614 		if ( current_theme_supports( 'disable-layout-styles' ) ) {
   832 				$block_rules .= 'body { margin: 0; }';
  1615 			return $block_rules;
   833 			}
  1616 		}
   834 
  1617 
   835 			// 2. Generate the rules that use the general selector.
  1618 		if ( isset( $block_metadata['name'] ) ) {
   836 			$block_rules .= static::to_ruleset( $selector, $declarations );
  1619 			$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_metadata['name'] );
   837 
  1620 			if ( ! block_has_support( $block_type, 'layout', false ) && ! block_has_support( $block_type, '__experimentalLayout', false ) ) {
   838 			// 3. Generate the rules that use the duotone selector.
  1621 				return $block_rules;
   839 			if ( isset( $metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
  1622 			}
   840 				$selector_duotone = static::scope_selector( $metadata['selector'], $metadata['duotone'] );
  1623 		}
   841 				$block_rules     .= static::to_ruleset( $selector_duotone, $declarations_duotone );
  1624 
   842 			}
  1625 		$selector                 = isset( $block_metadata['selector'] ) ? $block_metadata['selector'] : '';
   843 
  1626 		$has_block_gap_support    = isset( $this->theme_json['settings']['spacing']['blockGap'] );
   844 			if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
  1627 		$has_fallback_gap_support = ! $has_block_gap_support; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback gap styles support.
   845 				$block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
  1628 		$node                     = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
   846 				$block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
  1629 		$layout_definitions       = wp_get_layout_definitions();
   847 				$block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
  1630 		$layout_selector_pattern  = '/^[a-zA-Z0-9\-\.\,\ *+>:\(\)]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, child combinator and pseudo class selectors.
   848 
  1631 
   849 				$has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
  1632 		/*
   850 				if ( $has_block_gap_support ) {
  1633 		 * Gap styles will only be output if the theme has block gap support, or supports a fallback gap.
   851 					$block_rules .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }';
  1634 		 * Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value.
   852 					$block_rules .= '.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }';
  1635 		 */
   853 				}
  1636 		if ( $has_block_gap_support || $has_fallback_gap_support ) {
   854 			}
  1637 			$block_gap_value = null;
   855 		}
  1638 			// Use a fallback gap value if block gap support is not available.
   856 
  1639 			if ( ! $has_block_gap_support ) {
       
  1640 				$block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null;
       
  1641 				if ( ! empty( $block_type ) ) {
       
  1642 					$block_gap_value = isset( $block_type->supports['spacing']['blockGap']['__experimentalDefault'] )
       
  1643 						? $block_type->supports['spacing']['blockGap']['__experimentalDefault']
       
  1644 						: null;
       
  1645 				}
       
  1646 			} else {
       
  1647 				$block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) );
       
  1648 			}
       
  1649 
       
  1650 			// Support split row / column values and concatenate to a shorthand value.
       
  1651 			if ( is_array( $block_gap_value ) ) {
       
  1652 				if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) {
       
  1653 					$gap_row         = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) );
       
  1654 					$gap_column      = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) );
       
  1655 					$block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column;
       
  1656 				} else {
       
  1657 					// Skip outputting gap value if not all sides are provided.
       
  1658 					$block_gap_value = null;
       
  1659 				}
       
  1660 			}
       
  1661 
       
  1662 			// If the block should have custom gap, add the gap styles.
       
  1663 			if ( null !== $block_gap_value && false !== $block_gap_value && '' !== $block_gap_value ) {
       
  1664 				foreach ( $layout_definitions as $layout_definition_key => $layout_definition ) {
       
  1665 					// Allow outputting fallback gap styles for flex and grid layout types when block gap support isn't available.
       
  1666 					if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key && 'grid' !== $layout_definition_key ) {
       
  1667 						continue;
       
  1668 					}
       
  1669 
       
  1670 					$class_name    = isset( $layout_definition['className'] ) ? $layout_definition['className'] : false;
       
  1671 					$spacing_rules = isset( $layout_definition['spacingStyles'] ) ? $layout_definition['spacingStyles'] : array();
       
  1672 
       
  1673 					if (
       
  1674 						! empty( $class_name ) &&
       
  1675 						! empty( $spacing_rules )
       
  1676 					) {
       
  1677 						foreach ( $spacing_rules as $spacing_rule ) {
       
  1678 							$declarations = array();
       
  1679 							if (
       
  1680 								isset( $spacing_rule['selector'] ) &&
       
  1681 								preg_match( $layout_selector_pattern, $spacing_rule['selector'] ) &&
       
  1682 								! empty( $spacing_rule['rules'] )
       
  1683 							) {
       
  1684 								// Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value.
       
  1685 								foreach ( $spacing_rule['rules'] as $css_property => $css_value ) {
       
  1686 									$current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value;
       
  1687 									if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) {
       
  1688 										$declarations[] = array(
       
  1689 											'name'  => $css_property,
       
  1690 											'value' => $current_css_value,
       
  1691 										);
       
  1692 									}
       
  1693 								}
       
  1694 
       
  1695 								if ( ! $has_block_gap_support ) {
       
  1696 									// For fallback gap styles, use lower specificity, to ensure styles do not unintentionally override theme styles.
       
  1697 									$format          = static::ROOT_BLOCK_SELECTOR === $selector ? ':where(.%2$s%3$s)' : ':where(%1$s.%2$s%3$s)';
       
  1698 									$layout_selector = sprintf(
       
  1699 										$format,
       
  1700 										$selector,
       
  1701 										$class_name,
       
  1702 										$spacing_rule['selector']
       
  1703 									);
       
  1704 								} else {
       
  1705 									$format          = static::ROOT_BLOCK_SELECTOR === $selector ? ':root :where(.%2$s)%3$s' : ':root :where(%1$s-%2$s)%3$s';
       
  1706 									$layout_selector = sprintf(
       
  1707 										$format,
       
  1708 										$selector,
       
  1709 										$class_name,
       
  1710 										$spacing_rule['selector']
       
  1711 									);
       
  1712 								}
       
  1713 								$block_rules .= static::to_ruleset( $layout_selector, $declarations );
       
  1714 							}
       
  1715 						}
       
  1716 					}
       
  1717 				}
       
  1718 			}
       
  1719 		}
       
  1720 
       
  1721 		// Output base styles.
       
  1722 		if (
       
  1723 			static::ROOT_BLOCK_SELECTOR === $selector
       
  1724 		) {
       
  1725 			$valid_display_modes = array( 'block', 'flex', 'grid' );
       
  1726 			foreach ( $layout_definitions as $layout_definition ) {
       
  1727 				$class_name       = isset( $layout_definition['className'] ) ? $layout_definition['className'] : false;
       
  1728 				$base_style_rules = isset( $layout_definition['baseStyles'] ) ? $layout_definition['baseStyles'] : array();
       
  1729 
       
  1730 				if (
       
  1731 					! empty( $class_name ) &&
       
  1732 					is_array( $base_style_rules )
       
  1733 				) {
       
  1734 					// Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`.
       
  1735 					if (
       
  1736 						! empty( $layout_definition['displayMode'] ) &&
       
  1737 						is_string( $layout_definition['displayMode'] ) &&
       
  1738 						in_array( $layout_definition['displayMode'], $valid_display_modes, true )
       
  1739 					) {
       
  1740 						$layout_selector = sprintf(
       
  1741 							'%s .%s',
       
  1742 							$selector,
       
  1743 							$class_name
       
  1744 						);
       
  1745 						$block_rules    .= static::to_ruleset(
       
  1746 							$layout_selector,
       
  1747 							array(
       
  1748 								array(
       
  1749 									'name'  => 'display',
       
  1750 									'value' => $layout_definition['displayMode'],
       
  1751 								),
       
  1752 							)
       
  1753 						);
       
  1754 					}
       
  1755 
       
  1756 					foreach ( $base_style_rules as $base_style_rule ) {
       
  1757 						$declarations = array();
       
  1758 
       
  1759 						// Skip outputting base styles for flow and constrained layout types if theme doesn't support theme.json. The 'base-layout-styles' type flags this.
       
  1760 						if ( in_array( 'base-layout-styles', $types, true ) && ( 'default' === $layout_definition['name'] || 'constrained' === $layout_definition['name'] ) ) {
       
  1761 							continue;
       
  1762 						}
       
  1763 
       
  1764 						if (
       
  1765 							isset( $base_style_rule['selector'] ) &&
       
  1766 							preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) &&
       
  1767 							! empty( $base_style_rule['rules'] )
       
  1768 						) {
       
  1769 							foreach ( $base_style_rule['rules'] as $css_property => $css_value ) {
       
  1770 								// Skip rules that reference content size or wide size if they are not defined in the theme.json.
       
  1771 								if (
       
  1772 									is_string( $css_value ) &&
       
  1773 									( str_contains( $css_value, '--global--content-size' ) || str_contains( $css_value, '--global--wide-size' ) ) &&
       
  1774 									! isset( $this->theme_json['settings']['layout']['contentSize'] ) &&
       
  1775 									! isset( $this->theme_json['settings']['layout']['wideSize'] )
       
  1776 								) {
       
  1777 									continue;
       
  1778 								}
       
  1779 
       
  1780 								if ( static::is_safe_css_declaration( $css_property, $css_value ) ) {
       
  1781 									$declarations[] = array(
       
  1782 										'name'  => $css_property,
       
  1783 										'value' => $css_value,
       
  1784 									);
       
  1785 								}
       
  1786 							}
       
  1787 
       
  1788 							$layout_selector = sprintf(
       
  1789 								'.%s%s',
       
  1790 								$class_name,
       
  1791 								$base_style_rule['selector']
       
  1792 							);
       
  1793 							$block_rules    .= static::to_ruleset( $layout_selector, $declarations );
       
  1794 						}
       
  1795 					}
       
  1796 				}
       
  1797 			}
       
  1798 		}
   857 		return $block_rules;
  1799 		return $block_rules;
   858 	}
  1800 	}
   859 
  1801 
   860 	/**
  1802 	/**
   861 	 * Creates new rulesets as classes for each preset value such as:
  1803 	 * Creates new rulesets as classes for each preset value such as:
   880 	 *     background: value;
  1822 	 *     background: value;
   881 	 *   }
  1823 	 *   }
   882 	 *
  1824 	 *
   883 	 * @since 5.9.0
  1825 	 * @since 5.9.0
   884 	 *
  1826 	 *
   885 	 * @param array $setting_nodes Nodes with settings.
  1827 	 * @param array    $setting_nodes Nodes with settings.
   886 	 * @param array $origins       List of origins to process presets from.
  1828 	 * @param string[] $origins       List of origins to process presets from.
   887 	 * @return string The new stylesheet.
  1829 	 * @return string The new stylesheet.
   888 	 */
  1830 	 */
   889 	protected function get_preset_classes( $setting_nodes, $origins ) {
  1831 	protected function get_preset_classes( $setting_nodes, $origins ) {
   890 		$preset_rules = '';
  1832 		$preset_rules = '';
   891 
  1833 
   917 	 *     }
  1859 	 *     }
   918 	 *
  1860 	 *
   919 	 * @since 5.8.0
  1861 	 * @since 5.8.0
   920 	 * @since 5.9.0 Added the `$origins` parameter.
  1862 	 * @since 5.9.0 Added the `$origins` parameter.
   921 	 *
  1863 	 *
   922 	 * @param array $nodes   Nodes with settings.
  1864 	 * @param array    $nodes   Nodes with settings.
   923 	 * @param array $origins List of origins to process.
  1865 	 * @param string[] $origins List of origins to process.
   924 	 * @return string The new stylesheet.
  1866 	 * @return string The new stylesheet.
   925 	 */
  1867 	 */
   926 	protected function get_css_variables( $nodes, $origins ) {
  1868 	protected function get_css_variables( $nodes, $origins ) {
   927 		$stylesheet = '';
  1869 		$stylesheet = '';
   928 		foreach ( $nodes as $metadata ) {
  1870 		foreach ( $nodes as $metadata ) {
   930 				continue;
  1872 				continue;
   931 			}
  1873 			}
   932 
  1874 
   933 			$selector = $metadata['selector'];
  1875 			$selector = $metadata['selector'];
   934 
  1876 
   935 			$node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
  1877 			$node                    = _wp_array_get( $this->theme_json, $metadata['path'], array() );
   936 			$declarations = array_merge( static::compute_preset_vars( $node, $origins ), static::compute_theme_vars( $node ) );
  1878 			$declarations            = static::compute_preset_vars( $node, $origins );
       
  1879 			$theme_vars_declarations = static::compute_theme_vars( $node );
       
  1880 			foreach ( $theme_vars_declarations as $theme_vars_declaration ) {
       
  1881 				$declarations[] = $theme_vars_declaration;
       
  1882 			}
   937 
  1883 
   938 			$stylesheet .= static::to_ruleset( $selector, $declarations );
  1884 			$stylesheet .= static::to_ruleset( $selector, $declarations );
   939 		}
  1885 		}
   940 
  1886 
   941 		return $stylesheet;
  1887 		return $stylesheet;
   947 	 *
  1893 	 *
   948 	 * @since 5.8.0
  1894 	 * @since 5.8.0
   949 	 *
  1895 	 *
   950 	 * @param string $selector     CSS selector.
  1896 	 * @param string $selector     CSS selector.
   951 	 * @param array  $declarations List of declarations.
  1897 	 * @param array  $declarations List of declarations.
   952 	 * @return string CSS ruleset.
  1898 	 * @return string The resulting CSS ruleset.
   953 	 */
  1899 	 */
   954 	protected static function to_ruleset( $selector, $declarations ) {
  1900 	protected static function to_ruleset( $selector, $declarations ) {
   955 		if ( empty( $declarations ) ) {
  1901 		if ( empty( $declarations ) ) {
   956 			return '';
  1902 			return '';
   957 		}
  1903 		}
   965 
  1911 
   966 		return $selector . '{' . $declaration_block . '}';
  1912 		return $selector . '{' . $declaration_block . '}';
   967 	}
  1913 	}
   968 
  1914 
   969 	/**
  1915 	/**
   970 	 * Function that appends a sub-selector to a existing one.
  1916 	 * Given a settings array, returns the generated rulesets
   971 	 *
       
   972 	 * Given the compounded $selector "h1, h2, h3"
       
   973 	 * and the $to_append selector ".some-class" the result will be
       
   974 	 * "h1.some-class, h2.some-class, h3.some-class".
       
   975 	 *
       
   976 	 * @since 5.8.0
       
   977 	 *
       
   978 	 * @param string $selector  Original selector.
       
   979 	 * @param string $to_append Selector to append.
       
   980 	 * @return string
       
   981 	 */
       
   982 	protected static function append_to_selector( $selector, $to_append ) {
       
   983 		$new_selectors = array();
       
   984 		$selectors     = explode( ',', $selector );
       
   985 		foreach ( $selectors as $sel ) {
       
   986 			$new_selectors[] = $sel . $to_append;
       
   987 		}
       
   988 
       
   989 		return implode( ',', $new_selectors );
       
   990 	}
       
   991 
       
   992 	/**
       
   993 	 * Given a settings array, it returns the generated rulesets
       
   994 	 * for the preset classes.
  1917 	 * for the preset classes.
   995 	 *
  1918 	 *
   996 	 * @since 5.8.0
  1919 	 * @since 5.8.0
   997 	 * @since 5.9.0 Added the `$origins` parameter.
  1920 	 * @since 5.9.0 Added the `$origins` parameter.
   998 	 *
  1921 	 * @since 6.6.0 Added check for root CSS properties selector.
   999 	 * @param array  $settings Settings to process.
  1922 	 *
  1000 	 * @param string $selector Selector wrapping the classes.
  1923 	 * @param array    $settings Settings to process.
  1001 	 * @param array  $origins  List of origins to process.
  1924 	 * @param string   $selector Selector wrapping the classes.
       
  1925 	 * @param string[] $origins  List of origins to process.
  1002 	 * @return string The result of processing the presets.
  1926 	 * @return string The result of processing the presets.
  1003 	 */
  1927 	 */
  1004 	protected static function compute_preset_classes( $settings, $selector, $origins ) {
  1928 	protected static function compute_preset_classes( $settings, $selector, $origins ) {
  1005 		if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
  1929 		if ( static::ROOT_BLOCK_SELECTOR === $selector || static::ROOT_CSS_PROPERTIES_SELECTOR === $selector ) {
  1006 			// Classes at the global level do not need any CSS prefixed,
  1930 			/*
  1007 			// and we don't want to increase its specificity.
  1931 			 * Classes at the global level do not need any CSS prefixed,
       
  1932 			 * and we don't want to increase its specificity.
       
  1933 			 */
  1008 			$selector = '';
  1934 			$selector = '';
  1009 		}
  1935 		}
  1010 
  1936 
  1011 		$stylesheet = '';
  1937 		$stylesheet = '';
  1012 		foreach ( static::PRESETS_METADATA as $preset_metadata ) {
  1938 		foreach ( static::PRESETS_METADATA as $preset_metadata ) {
       
  1939 			if ( empty( $preset_metadata['classes'] ) ) {
       
  1940 				continue;
       
  1941 			}
  1013 			$slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins );
  1942 			$slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins );
  1014 			foreach ( $preset_metadata['classes'] as $class => $property ) {
  1943 			foreach ( $preset_metadata['classes'] as $class => $property ) {
  1015 				foreach ( $slugs as $slug ) {
  1944 				foreach ( $slugs as $slug ) {
  1016 					$css_var     = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
  1945 					$css_var    = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
  1017 					$class_name  = static::replace_slug_in_string( $class, $slug );
  1946 					$class_name = static::replace_slug_in_string( $class, $slug );
  1018 					$stylesheet .= static::to_ruleset(
  1947 
  1019 						static::append_to_selector( $selector, $class_name ),
  1948 					// $selector is often empty, so we can save ourselves the `append_to_selector()` call then.
       
  1949 					$new_selector = '' === $selector ? $class_name : static::append_to_selector( $selector, $class_name );
       
  1950 					$stylesheet  .= static::to_ruleset(
       
  1951 						$new_selector,
  1020 						array(
  1952 						array(
  1021 							array(
  1953 							array(
  1022 								'name'  => $property,
  1954 								'name'  => $property,
  1023 								'value' => 'var(' . $css_var . ') !important',
  1955 								'value' => 'var(' . $css_var . ') !important',
  1024 							),
  1956 							),
  1041 	 * $merged = scope_selector( $scope, $selector );
  1973 	 * $merged = scope_selector( $scope, $selector );
  1042 	 * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
  1974 	 * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
  1043 	 * </code>
  1975 	 * </code>
  1044 	 *
  1976 	 *
  1045 	 * @since 5.9.0
  1977 	 * @since 5.9.0
       
  1978 	 * @since 6.6.0 Added early return if missing scope or selector.
  1046 	 *
  1979 	 *
  1047 	 * @param string $scope    Selector to scope to.
  1980 	 * @param string $scope    Selector to scope to.
  1048 	 * @param string $selector Original selector.
  1981 	 * @param string $selector Original selector.
  1049 	 * @return string Scoped selector.
  1982 	 * @return string Scoped selector.
  1050 	 */
  1983 	 */
  1051 	protected static function scope_selector( $scope, $selector ) {
  1984 	public static function scope_selector( $scope, $selector ) {
       
  1985 		if ( ! $scope || ! $selector ) {
       
  1986 			return $selector;
       
  1987 		}
       
  1988 
  1052 		$scopes    = explode( ',', $scope );
  1989 		$scopes    = explode( ',', $scope );
  1053 		$selectors = explode( ',', $selector );
  1990 		$selectors = explode( ',', $selector );
  1054 
  1991 
  1055 		$selectors_scoped = array();
  1992 		$selectors_scoped = array();
  1056 		foreach ( $scopes as $outer ) {
  1993 		foreach ( $scopes as $outer ) {
  1057 			foreach ( $selectors as $inner ) {
  1994 			foreach ( $selectors as $inner ) {
  1058 				$selectors_scoped[] = trim( $outer ) . ' ' . trim( $inner );
  1995 				$outer = trim( $outer );
  1059 			}
  1996 				$inner = trim( $inner );
  1060 		}
  1997 				if ( ! empty( $outer ) && ! empty( $inner ) ) {
  1061 
  1998 					$selectors_scoped[] = $outer . ' ' . $inner;
  1062 		return implode( ', ', $selectors_scoped );
  1999 				} elseif ( empty( $outer ) ) {
       
  2000 					$selectors_scoped[] = $inner;
       
  2001 				} elseif ( empty( $inner ) ) {
       
  2002 					$selectors_scoped[] = $outer;
       
  2003 				}
       
  2004 			}
       
  2005 		}
       
  2006 
       
  2007 		$result = implode( ', ', $selectors_scoped );
       
  2008 		return $result;
       
  2009 	}
       
  2010 
       
  2011 	/**
       
  2012 	 * Scopes the selectors for a given style node.
       
  2013 	 *
       
  2014 	 * This includes the primary selector, i.e. `$node['selector']`, as well as any custom
       
  2015 	 * selectors for features and subfeatures, e.g. `$node['selectors']['border']` etc.
       
  2016 	 *
       
  2017 	 * @since 6.6.0
       
  2018 	 *
       
  2019 	 * @param string $scope Selector to scope to.
       
  2020 	 * @param array  $node  Style node with selectors to scope.
       
  2021 	 * @return array Node with updated selectors.
       
  2022 	 */
       
  2023 	protected static function scope_style_node_selectors( $scope, $node ) {
       
  2024 		$node['selector'] = static::scope_selector( $scope, $node['selector'] );
       
  2025 
       
  2026 		if ( empty( $node['selectors'] ) ) {
       
  2027 			return $node;
       
  2028 		}
       
  2029 
       
  2030 		foreach ( $node['selectors'] as $feature => $selector ) {
       
  2031 			if ( is_string( $selector ) ) {
       
  2032 				$node['selectors'][ $feature ] = static::scope_selector( $scope, $selector );
       
  2033 			}
       
  2034 			if ( is_array( $selector ) ) {
       
  2035 				foreach ( $selector as $subfeature => $subfeature_selector ) {
       
  2036 					$node['selectors'][ $feature ][ $subfeature ] = static::scope_selector( $scope, $subfeature_selector );
       
  2037 				}
       
  2038 			}
       
  2039 		}
       
  2040 
       
  2041 		return $node;
  1063 	}
  2042 	}
  1064 
  2043 
  1065 	/**
  2044 	/**
  1066 	 * Gets preset values keyed by slugs based on settings and metadata.
  2045 	 * Gets preset values keyed by slugs based on settings and metadata.
  1067 	 *
  2046 	 *
  1090 	 * //   'serif'      => 'Georgia, serif',
  2069 	 * //   'serif'      => 'Georgia, serif',
  1091 	 * // );
  2070 	 * // );
  1092 	 * </code>
  2071 	 * </code>
  1093 	 *
  2072 	 *
  1094 	 * @since 5.9.0
  2073 	 * @since 5.9.0
  1095 	 *
  2074 	 * @since 6.6.0 Passing $settings to the callbacks defined in static::PRESETS_METADATA.
  1096 	 * @param array $settings        Settings to process.
  2075 	 *
  1097 	 * @param array $preset_metadata One of the PRESETS_METADATA values.
  2076 	 * @param array    $settings        Settings to process.
  1098 	 * @param array $origins         List of origins to process.
  2077 	 * @param array    $preset_metadata One of the PRESETS_METADATA values.
       
  2078 	 * @param string[] $origins         List of origins to process.
  1099 	 * @return array Array of presets where each key is a slug and each value is the preset value.
  2079 	 * @return array Array of presets where each key is a slug and each value is the preset value.
  1100 	 */
  2080 	 */
  1101 	protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) {
  2081 	protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) {
  1102 		$preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
  2082 		$preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
  1103 
  2083 
  1116 				} elseif (
  2096 				} elseif (
  1117 					isset( $preset_metadata['value_func'] ) &&
  2097 					isset( $preset_metadata['value_func'] ) &&
  1118 					is_callable( $preset_metadata['value_func'] )
  2098 					is_callable( $preset_metadata['value_func'] )
  1119 				) {
  2099 				) {
  1120 					$value_func = $preset_metadata['value_func'];
  2100 					$value_func = $preset_metadata['value_func'];
  1121 					$value      = call_user_func( $value_func, $preset );
  2101 					$value      = call_user_func( $value_func, $preset, $settings );
  1122 				} else {
  2102 				} else {
  1123 					// If we don't have a value, then don't add it to the result.
  2103 					// If we don't have a value, then don't add it to the result.
  1124 					continue;
  2104 					continue;
  1125 				}
  2105 				}
  1126 
  2106 
  1133 	/**
  2113 	/**
  1134 	 * Similar to get_settings_values_by_slug, but doesn't compute the value.
  2114 	 * Similar to get_settings_values_by_slug, but doesn't compute the value.
  1135 	 *
  2115 	 *
  1136 	 * @since 5.9.0
  2116 	 * @since 5.9.0
  1137 	 *
  2117 	 *
  1138 	 * @param array $settings        Settings to process.
  2118 	 * @param array    $settings        Settings to process.
  1139 	 * @param array $preset_metadata One of the PRESETS_METADATA values.
  2119 	 * @param array    $preset_metadata One of the PRESETS_METADATA values.
  1140 	 * @param array $origins         List of origins to process.
  2120 	 * @param string[] $origins         List of origins to process.
  1141 	 * @return array Array of presets where the key and value are both the slug.
  2121 	 * @return array Array of presets where the key and value are both the slug.
  1142 	 */
  2122 	 */
  1143 	protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) {
  2123 	protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) {
  1144 		if ( null === $origins ) {
  2124 		if ( null === $origins ) {
  1145 			$origins = static::VALID_ORIGINS;
  2125 			$origins = static::VALID_ORIGINS;
  1161 		}
  2141 		}
  1162 		return $result;
  2142 		return $result;
  1163 	}
  2143 	}
  1164 
  2144 
  1165 	/**
  2145 	/**
  1166 	 * Transform a slug into a CSS Custom Property.
  2146 	 * Transforms a slug into a CSS Custom Property.
  1167 	 *
  2147 	 *
  1168 	 * @since 5.9.0
  2148 	 * @since 5.9.0
  1169 	 *
  2149 	 *
  1170 	 * @param string $input String to replace.
  2150 	 * @param string $input String to replace.
  1171 	 * @param string $slug  The slug value to use to generate the custom property.
  2151 	 * @param string $slug  The slug value to use to generate the custom property.
  1174 	protected static function replace_slug_in_string( $input, $slug ) {
  2154 	protected static function replace_slug_in_string( $input, $slug ) {
  1175 		return strtr( $input, array( '$slug' => $slug ) );
  2155 		return strtr( $input, array( '$slug' => $slug ) );
  1176 	}
  2156 	}
  1177 
  2157 
  1178 	/**
  2158 	/**
  1179 	 * Given the block settings, it extracts the CSS Custom Properties
  2159 	 * Given the block settings, extracts the CSS Custom Properties
  1180 	 * for the presets and adds them to the $declarations array
  2160 	 * for the presets and adds them to the $declarations array
  1181 	 * following the format:
  2161 	 * following the format:
  1182 	 *
  2162 	 *
  1183 	 *     array(
  2163 	 *     array(
  1184 	 *       'name'  => 'property_name',
  2164 	 *       'name'  => 'property_name',
  1186 	 *     )
  2166 	 *     )
  1187 	 *
  2167 	 *
  1188 	 * @since 5.8.0
  2168 	 * @since 5.8.0
  1189 	 * @since 5.9.0 Added the `$origins` parameter.
  2169 	 * @since 5.9.0 Added the `$origins` parameter.
  1190 	 *
  2170 	 *
  1191 	 * @param array $settings Settings to process.
  2171 	 * @param array    $settings Settings to process.
  1192 	 * @param array $origins  List of origins to process.
  2172 	 * @param string[] $origins  List of origins to process.
  1193 	 * @return array Returns the modified $declarations.
  2173 	 * @return array The modified $declarations.
  1194 	 */
  2174 	 */
  1195 	protected static function compute_preset_vars( $settings, $origins ) {
  2175 	protected static function compute_preset_vars( $settings, $origins ) {
  1196 		$declarations = array();
  2176 		$declarations = array();
  1197 		foreach ( static::PRESETS_METADATA as $preset_metadata ) {
  2177 		foreach ( static::PRESETS_METADATA as $preset_metadata ) {
       
  2178 			if ( empty( $preset_metadata['css_vars'] ) ) {
       
  2179 				continue;
       
  2180 			}
  1198 			$values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins );
  2181 			$values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins );
  1199 			foreach ( $values_by_slug as $slug => $value ) {
  2182 			foreach ( $values_by_slug as $slug => $value ) {
  1200 				$declarations[] = array(
  2183 				$declarations[] = array(
  1201 					'name'  => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
  2184 					'name'  => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
  1202 					'value' => $value,
  2185 					'value' => $value,
  1206 
  2189 
  1207 		return $declarations;
  2190 		return $declarations;
  1208 	}
  2191 	}
  1209 
  2192 
  1210 	/**
  2193 	/**
  1211 	 * Given an array of settings, it extracts the CSS Custom Properties
  2194 	 * Given an array of settings, extracts the CSS Custom Properties
  1212 	 * for the custom values and adds them to the $declarations
  2195 	 * for the custom values and adds them to the $declarations
  1213 	 * array following the format:
  2196 	 * array following the format:
  1214 	 *
  2197 	 *
  1215 	 *     array(
  2198 	 *     array(
  1216 	 *       'name'  => 'property_name',
  2199 	 *       'name'  => 'property_name',
  1218 	 *     )
  2201 	 *     )
  1219 	 *
  2202 	 *
  1220 	 * @since 5.8.0
  2203 	 * @since 5.8.0
  1221 	 *
  2204 	 *
  1222 	 * @param array $settings Settings to process.
  2205 	 * @param array $settings Settings to process.
  1223 	 * @return array Returns the modified $declarations.
  2206 	 * @return array The modified $declarations.
  1224 	 */
  2207 	 */
  1225 	protected static function compute_theme_vars( $settings ) {
  2208 	protected static function compute_theme_vars( $settings ) {
  1226 		$declarations  = array();
  2209 		$declarations  = array();
  1227 		$custom_values = _wp_array_get( $settings, array( 'custom' ), array() );
  2210 		$custom_values = isset( $settings['custom'] ) ? $settings['custom'] : array();
  1228 		$css_vars      = static::flatten_tree( $custom_values );
  2211 		$css_vars      = static::flatten_tree( $custom_values );
  1229 		foreach ( $css_vars as $key => $value ) {
  2212 		foreach ( $css_vars as $key => $value ) {
  1230 			$declarations[] = array(
  2213 			$declarations[] = array(
  1231 				'name'  => '--wp--custom--' . $key,
  2214 				'name'  => '--wp--custom--' . $key,
  1232 				'value' => $value,
  2215 				'value' => $value,
  1281 				'-',
  2264 				'-',
  1282 				strtolower( _wp_to_kebab_case( $property ) )
  2265 				strtolower( _wp_to_kebab_case( $property ) )
  1283 			);
  2266 			);
  1284 
  2267 
  1285 			if ( is_array( $value ) ) {
  2268 			if ( is_array( $value ) ) {
  1286 				$new_prefix = $new_key . $token;
  2269 				$new_prefix        = $new_key . $token;
  1287 				$result     = array_merge(
  2270 				$flattened_subtree = static::flatten_tree( $value, $new_prefix, $token );
  1288 					$result,
  2271 				foreach ( $flattened_subtree as $subtree_key => $subtree_value ) {
  1289 					static::flatten_tree( $value, $new_prefix, $token )
  2272 					$result[ $subtree_key ] = $subtree_value;
  1290 				);
  2273 				}
  1291 			} else {
  2274 			} else {
  1292 				$result[ $new_key ] = $value;
  2275 				$result[ $new_key ] = $value;
  1293 			}
  2276 			}
  1294 		}
  2277 		}
  1295 		return $result;
  2278 		return $result;
  1304 	 *       'value' => 'property_value,
  2287 	 *       'value' => 'property_value,
  1305 	 *     )
  2288 	 *     )
  1306 	 *
  2289 	 *
  1307 	 * @since 5.8.0
  2290 	 * @since 5.8.0
  1308 	 * @since 5.9.0 Added the `$settings` and `$properties` parameters.
  2291 	 * @since 5.9.0 Added the `$settings` and `$properties` parameters.
  1309 	 *
  2292 	 * @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters.
  1310 	 * @param array $styles    Styles to process.
  2293 	 * @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set.
  1311 	 * @param array $settings  Theme settings.
  2294 	 * @since 6.6.0 Pass current theme JSON settings to wp_get_typography_font_size_value(), and process background properties.
  1312 	 * @param array $properties Properties metadata.
  2295 	 *
  1313 	 * @return array Returns the modified $declarations.
  2296 	 * @param array   $styles Styles to process.
  1314 	 */
  2297 	 * @param array   $settings Theme settings.
  1315 	protected static function compute_style_properties( $styles, $settings = array(), $properties = null ) {
  2298 	 * @param array   $properties Properties metadata.
       
  2299 	 * @param array   $theme_json Theme JSON array.
       
  2300 	 * @param string  $selector The style block selector.
       
  2301 	 * @param boolean $use_root_padding Whether to add custom properties at root level.
       
  2302 	 * @return array  Returns the modified $declarations.
       
  2303 	 */
       
  2304 	protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
  1316 		if ( null === $properties ) {
  2305 		if ( null === $properties ) {
  1317 			$properties = static::PROPERTIES_METADATA;
  2306 			$properties = static::PROPERTIES_METADATA;
  1318 		}
  2307 		}
  1319 
  2308 
  1320 		$declarations = array();
  2309 		$declarations = array();
  1321 		if ( empty( $styles ) ) {
  2310 		if ( empty( $styles ) ) {
  1322 			return $declarations;
  2311 			return $declarations;
  1323 		}
  2312 		}
  1324 
  2313 
       
  2314 		$root_variable_duplicates = array();
       
  2315 
  1325 		foreach ( $properties as $css_property => $value_path ) {
  2316 		foreach ( $properties as $css_property => $value_path ) {
  1326 			$value = static::get_property_value( $styles, $value_path );
  2317 			$value = static::get_property_value( $styles, $value_path, $theme_json );
  1327 
  2318 
  1328 			// Look up protected properties, keyed by value path.
  2319 			if ( str_starts_with( $css_property, '--wp--style--root--' ) && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) {
  1329 			// Skip protected properties that are explicitly set to `null`.
  2320 				continue;
       
  2321 			}
       
  2322 			/*
       
  2323 			 * Root-level padding styles don't currently support strings with CSS shorthand values.
       
  2324 			 * This may change: https://github.com/WordPress/gutenberg/issues/40132.
       
  2325 			 */
       
  2326 			if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) {
       
  2327 				continue;
       
  2328 			}
       
  2329 
       
  2330 			if ( str_starts_with( $css_property, '--wp--style--root--' ) && $use_root_padding ) {
       
  2331 				$root_variable_duplicates[] = substr( $css_property, strlen( '--wp--style--root--' ) );
       
  2332 			}
       
  2333 
       
  2334 			/*
       
  2335 			 * Look up protected properties, keyed by value path.
       
  2336 			 * Skip protected properties that are explicitly set to `null`.
       
  2337 			 */
  1330 			if ( is_array( $value_path ) ) {
  2338 			if ( is_array( $value_path ) ) {
  1331 				$path_string = implode( '.', $value_path );
  2339 				$path_string = implode( '.', $value_path );
  1332 				if (
  2340 				if (
  1333 					array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) &&
  2341 					isset( static::PROTECTED_PROPERTIES[ $path_string ] ) &&
  1334 					_wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null
  2342 					_wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null
  1335 				) {
  2343 				) {
  1336 					continue;
  2344 					continue;
  1337 				}
  2345 				}
       
  2346 			}
       
  2347 
       
  2348 			// Processes background styles.
       
  2349 			if ( 'background' === $value_path[0] && isset( $styles['background'] ) ) {
       
  2350 				$background_styles = wp_style_engine_get_styles( array( 'background' => $styles['background'] ) );
       
  2351 				$value             = isset( $background_styles['declarations'][ $css_property ] ) ? $background_styles['declarations'][ $css_property ] : $value;
  1338 			}
  2352 			}
  1339 
  2353 
  1340 			// Skip if empty and not "0" or value represents array of longhand values.
  2354 			// Skip if empty and not "0" or value represents array of longhand values.
  1341 			$has_missing_value = empty( $value ) && ! is_numeric( $value );
  2355 			$has_missing_value = empty( $value ) && ! is_numeric( $value );
  1342 			if ( $has_missing_value || is_array( $value ) ) {
  2356 			if ( $has_missing_value || is_array( $value ) ) {
  1343 				continue;
  2357 				continue;
  1344 			}
  2358 			}
  1345 
  2359 
       
  2360 			// Calculates fluid typography rules where available.
       
  2361 			if ( 'font-size' === $css_property ) {
       
  2362 				/*
       
  2363 				 * wp_get_typography_font_size_value() will check
       
  2364 				 * if fluid typography has been activated and also
       
  2365 				 * whether the incoming value can be converted to a fluid value.
       
  2366 				 * Values that already have a clamp() function will not pass the test,
       
  2367 				 * and therefore the original $value will be returned.
       
  2368 				 * Pass the current theme_json settings to override any global settings.
       
  2369 				 */
       
  2370 				$value = wp_get_typography_font_size_value( array( 'size' => $value ), $settings );
       
  2371 			}
       
  2372 
       
  2373 			if ( 'aspect-ratio' === $css_property ) {
       
  2374 				// For aspect ratio to work, other dimensions rules must be unset.
       
  2375 				// This ensures that a fixed height does not override the aspect ratio.
       
  2376 				$declarations[] = array(
       
  2377 					'name'  => 'min-height',
       
  2378 					'value' => 'unset',
       
  2379 				);
       
  2380 			}
       
  2381 
  1346 			$declarations[] = array(
  2382 			$declarations[] = array(
  1347 				'name'  => $css_property,
  2383 				'name'  => $css_property,
  1348 				'value' => $value,
  2384 				'value' => $value,
  1349 			);
  2385 			);
  1350 		}
  2386 		}
  1351 
  2387 
       
  2388 		// If a variable value is added to the root, the corresponding property should be removed.
       
  2389 		foreach ( $root_variable_duplicates as $duplicate ) {
       
  2390 			$discard = array_search( $duplicate, array_column( $declarations, 'name' ), true );
       
  2391 			if ( is_numeric( $discard ) ) {
       
  2392 				array_splice( $declarations, $discard, 1 );
       
  2393 			}
       
  2394 		}
       
  2395 
  1352 		return $declarations;
  2396 		return $declarations;
  1353 	}
  2397 	}
  1354 
  2398 
  1355 	/**
  2399 	/**
  1356 	 * Returns the style property for the given path.
  2400 	 * Returns the style property for the given path.
  1357 	 *
  2401 	 *
  1358 	 * It also converts CSS Custom Property stored as
  2402 	 * It also converts references to a path to the value
  1359 	 * "var:preset|color|secondary" to the form
  2403 	 * stored at that location, e.g.
  1360 	 * "--wp--preset--color--secondary".
  2404 	 * { "ref": "style.color.background" } => "#fff".
  1361 	 *
  2405 	 *
  1362 	 * @since 5.8.0
  2406 	 * @since 5.8.0
  1363 	 * @since 5.9.0 Added support for values of array type, which are returned as is.
  2407 	 * @since 5.9.0 Added support for values of array type, which are returned as is.
       
  2408 	 * @since 6.1.0 Added the `$theme_json` parameter.
       
  2409 	 * @since 6.3.0 It no longer converts the internal format "var:preset|color|secondary"
       
  2410 	 *              to the standard form "--wp--preset--color--secondary".
       
  2411 	 *              This is already done by the sanitize method,
       
  2412 	 *              so every property will be in the standard form.
  1364 	 *
  2413 	 *
  1365 	 * @param array $styles Styles subtree.
  2414 	 * @param array $styles Styles subtree.
  1366 	 * @param array $path   Which property to process.
  2415 	 * @param array $path   Which property to process.
       
  2416 	 * @param array $theme_json Theme JSON array.
  1367 	 * @return string|array Style property value.
  2417 	 * @return string|array Style property value.
  1368 	 */
  2418 	 */
  1369 	protected static function get_property_value( $styles, $path ) {
  2419 	protected static function get_property_value( $styles, $path, $theme_json = null ) {
  1370 		$value = _wp_array_get( $styles, $path, '' );
  2420 		$value = _wp_array_get( $styles, $path, '' );
  1371 
  2421 
  1372 		if ( '' === $value || is_array( $value ) ) {
  2422 		if ( '' === $value || null === $value ) {
       
  2423 			// No need to process the value further.
       
  2424 			return '';
       
  2425 		}
       
  2426 
       
  2427 		/*
       
  2428 		 * This converts references to a path to the value at that path
       
  2429 		 * where the values is an array with a "ref" key, pointing to a path.
       
  2430 		 * For example: { "ref": "style.color.background" } => "#fff".
       
  2431 		 */
       
  2432 		if ( is_array( $value ) && isset( $value['ref'] ) ) {
       
  2433 			$value_path = explode( '.', $value['ref'] );
       
  2434 			$ref_value  = _wp_array_get( $theme_json, $value_path );
       
  2435 			// Only use the ref value if we find anything.
       
  2436 			if ( ! empty( $ref_value ) && is_string( $ref_value ) ) {
       
  2437 				$value = $ref_value;
       
  2438 			}
       
  2439 
       
  2440 			if ( is_array( $ref_value ) && isset( $ref_value['ref'] ) ) {
       
  2441 				$path_string      = json_encode( $path );
       
  2442 				$ref_value_string = json_encode( $ref_value );
       
  2443 				_doing_it_wrong(
       
  2444 					'get_property_value',
       
  2445 					sprintf(
       
  2446 						/* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */
       
  2447 						__( 'Your %1$s file uses a dynamic value (%2$s) for the path at %3$s. However, the value at %3$s is also a dynamic value (pointing to %4$s) and pointing to another dynamic value is not supported. Please update %3$s to point directly to %4$s.' ),
       
  2448 						'theme.json',
       
  2449 						$ref_value_string,
       
  2450 						$path_string,
       
  2451 						$ref_value['ref']
       
  2452 					),
       
  2453 					'6.1.0'
       
  2454 				);
       
  2455 			}
       
  2456 		}
       
  2457 
       
  2458 		if ( is_array( $value ) ) {
  1373 			return $value;
  2459 			return $value;
  1374 		}
       
  1375 
       
  1376 		$prefix     = 'var:';
       
  1377 		$prefix_len = strlen( $prefix );
       
  1378 		$token_in   = '|';
       
  1379 		$token_out  = '--';
       
  1380 		if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) {
       
  1381 			$unwrapped_name = str_replace(
       
  1382 				$token_in,
       
  1383 				$token_out,
       
  1384 				substr( $value, $prefix_len )
       
  1385 			);
       
  1386 			$value          = "var(--wp--$unwrapped_name)";
       
  1387 		}
  2460 		}
  1388 
  2461 
  1389 		return $value;
  2462 		return $value;
  1390 	}
  2463 	}
  1391 
  2464 
  1405 	 *
  2478 	 *
  1406 	 * @since 5.8.0
  2479 	 * @since 5.8.0
  1407 	 *
  2480 	 *
  1408 	 * @param array $theme_json The tree to extract setting nodes from.
  2481 	 * @param array $theme_json The tree to extract setting nodes from.
  1409 	 * @param array $selectors  List of selectors per block.
  2482 	 * @param array $selectors  List of selectors per block.
  1410 	 * @return array
  2483 	 * @return array An array of setting nodes metadata.
  1411 	 */
  2484 	 */
  1412 	protected static function get_setting_nodes( $theme_json, $selectors = array() ) {
  2485 	protected static function get_setting_nodes( $theme_json, $selectors = array() ) {
  1413 		$nodes = array();
  2486 		$nodes = array();
  1414 		if ( ! isset( $theme_json['settings'] ) ) {
  2487 		if ( ! isset( $theme_json['settings'] ) ) {
  1415 			return $nodes;
  2488 			return $nodes;
  1416 		}
  2489 		}
  1417 
  2490 
  1418 		// Top-level.
  2491 		// Top-level.
  1419 		$nodes[] = array(
  2492 		$nodes[] = array(
  1420 			'path'     => array( 'settings' ),
  2493 			'path'     => array( 'settings' ),
  1421 			'selector' => static::ROOT_BLOCK_SELECTOR,
  2494 			'selector' => static::ROOT_CSS_PROPERTIES_SELECTOR,
  1422 		);
  2495 		);
  1423 
  2496 
  1424 		// Calculate paths for blocks.
  2497 		// Calculate paths for blocks.
  1425 		if ( ! isset( $theme_json['settings']['blocks'] ) ) {
  2498 		if ( ! isset( $theme_json['settings']['blocks'] ) ) {
  1426 			return $nodes;
  2499 			return $nodes;
  1456 	 *         'duotone'  => null
  2529 	 *         'duotone'  => null
  1457 	 *       ],
  2530 	 *       ],
  1458 	 *     ]
  2531 	 *     ]
  1459 	 *
  2532 	 *
  1460 	 * @since 5.8.0
  2533 	 * @since 5.8.0
       
  2534 	 * @since 6.6.0 Added options array for modifying generated nodes.
  1461 	 *
  2535 	 *
  1462 	 * @param array $theme_json The tree to extract style nodes from.
  2536 	 * @param array $theme_json The tree to extract style nodes from.
  1463 	 * @param array $selectors  List of selectors per block.
  2537 	 * @param array $selectors  List of selectors per block.
  1464 	 * @return array
  2538 	 * @param array $options {
  1465 	 */
  2539 	 *     Optional. An array of options for now used for internal purposes only (may change without notice).
  1466 	protected static function get_style_nodes( $theme_json, $selectors = array() ) {
  2540 	 *
       
  2541 	 *     @type bool $include_block_style_variations Includes style nodes for block style variations. Default false.
       
  2542 	 * }
       
  2543 	 * @return array An array of style nodes metadata.
       
  2544 	 */
       
  2545 	protected static function get_style_nodes( $theme_json, $selectors = array(), $options = array() ) {
  1467 		$nodes = array();
  2546 		$nodes = array();
  1468 		if ( ! isset( $theme_json['styles'] ) ) {
  2547 		if ( ! isset( $theme_json['styles'] ) ) {
  1469 			return $nodes;
  2548 			return $nodes;
  1470 		}
  2549 		}
  1471 
  2550 
  1474 			'path'     => array( 'styles' ),
  2553 			'path'     => array( 'styles' ),
  1475 			'selector' => static::ROOT_BLOCK_SELECTOR,
  2554 			'selector' => static::ROOT_BLOCK_SELECTOR,
  1476 		);
  2555 		);
  1477 
  2556 
  1478 		if ( isset( $theme_json['styles']['elements'] ) ) {
  2557 		if ( isset( $theme_json['styles']['elements'] ) ) {
  1479 			foreach ( $theme_json['styles']['elements'] as $element => $node ) {
  2558 			foreach ( self::ELEMENTS as $element => $selector ) {
       
  2559 				if ( ! isset( $theme_json['styles']['elements'][ $element ] ) ) {
       
  2560 					continue;
       
  2561 				}
  1480 				$nodes[] = array(
  2562 				$nodes[] = array(
  1481 					'path'     => array( 'styles', 'elements', $element ),
  2563 					'path'     => array( 'styles', 'elements', $element ),
  1482 					'selector' => static::ELEMENTS[ $element ],
  2564 					'selector' => static::ELEMENTS[ $element ],
  1483 				);
  2565 				);
  1484 			}
  2566 
       
  2567 				// Handle any pseudo selectors for the element.
       
  2568 				if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
       
  2569 					foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
       
  2570 
       
  2571 						if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) {
       
  2572 							$nodes[] = array(
       
  2573 								'path'     => array( 'styles', 'elements', $element ),
       
  2574 								'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ),
       
  2575 							);
       
  2576 						}
       
  2577 					}
       
  2578 				}
       
  2579 			}
       
  2580 		}
       
  2581 
       
  2582 		// Blocks.
       
  2583 		if ( ! isset( $theme_json['styles']['blocks'] ) ) {
       
  2584 			return $nodes;
       
  2585 		}
       
  2586 
       
  2587 		$block_nodes = static::get_block_nodes( $theme_json, $selectors, $options );
       
  2588 		foreach ( $block_nodes as $block_node ) {
       
  2589 			$nodes[] = $block_node;
       
  2590 		}
       
  2591 
       
  2592 		/**
       
  2593 		 * Filters the list of style nodes with metadata.
       
  2594 		 *
       
  2595 		 * This allows for things like loading block CSS independently.
       
  2596 		 *
       
  2597 		 * @since 6.1.0
       
  2598 		 *
       
  2599 		 * @param array $nodes Style nodes with metadata.
       
  2600 		 */
       
  2601 		return apply_filters( 'wp_theme_json_get_style_nodes', $nodes );
       
  2602 	}
       
  2603 
       
  2604 	/**
       
  2605 	 * A public helper to get the block nodes from a theme.json file.
       
  2606 	 *
       
  2607 	 * @since 6.1.0
       
  2608 	 *
       
  2609 	 * @return array The block nodes in theme.json.
       
  2610 	 */
       
  2611 	public function get_styles_block_nodes() {
       
  2612 		return static::get_block_nodes( $this->theme_json );
       
  2613 	}
       
  2614 
       
  2615 	/**
       
  2616 	 * Returns a filtered declarations array if there is a separator block with only a background
       
  2617 	 * style defined in theme.json by adding a color attribute to reflect the changes in the front.
       
  2618 	 *
       
  2619 	 * @since 6.1.1
       
  2620 	 *
       
  2621 	 * @param array $declarations List of declarations.
       
  2622 	 * @return array $declarations List of declarations filtered.
       
  2623 	 */
       
  2624 	private static function update_separator_declarations( $declarations ) {
       
  2625 		$background_color     = '';
       
  2626 		$border_color_matches = false;
       
  2627 		$text_color_matches   = false;
       
  2628 
       
  2629 		foreach ( $declarations as $declaration ) {
       
  2630 			if ( 'background-color' === $declaration['name'] && ! $background_color && isset( $declaration['value'] ) ) {
       
  2631 				$background_color = $declaration['value'];
       
  2632 			} elseif ( 'border-color' === $declaration['name'] ) {
       
  2633 				$border_color_matches = true;
       
  2634 			} elseif ( 'color' === $declaration['name'] ) {
       
  2635 				$text_color_matches = true;
       
  2636 			}
       
  2637 
       
  2638 			if ( $background_color && $border_color_matches && $text_color_matches ) {
       
  2639 				break;
       
  2640 			}
       
  2641 		}
       
  2642 
       
  2643 		if ( $background_color && ! $border_color_matches && ! $text_color_matches ) {
       
  2644 			$declarations[] = array(
       
  2645 				'name'  => 'color',
       
  2646 				'value' => $background_color,
       
  2647 			);
       
  2648 		}
       
  2649 
       
  2650 		return $declarations;
       
  2651 	}
       
  2652 
       
  2653 	/**
       
  2654 	 * An internal method to get the block nodes from a theme.json file.
       
  2655 	 *
       
  2656 	 * @since 6.1.0
       
  2657 	 * @since 6.3.0 Refactored and stabilized selectors API.
       
  2658 	 * @since 6.6.0 Added optional selectors and options for generating block nodes.
       
  2659 	 *
       
  2660 	 * @param array $theme_json The theme.json converted to an array.
       
  2661 	 * @param array $selectors  Optional list of selectors per block.
       
  2662 	 * @param array $options {
       
  2663 	 *     Optional. An array of options for now used for internal purposes only (may change without notice).
       
  2664 	 *
       
  2665 	 *     @type bool   $include_block_style_variations  Includes nodes for block style variations. Default false.
       
  2666 	 * }
       
  2667 	 * @return array The block nodes in theme.json.
       
  2668 	 */
       
  2669 	private static function get_block_nodes( $theme_json, $selectors = array(), $options = array() ) {
       
  2670 		$selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
       
  2671 		$nodes     = array();
       
  2672 		if ( ! isset( $theme_json['styles'] ) ) {
       
  2673 			return $nodes;
  1485 		}
  2674 		}
  1486 
  2675 
  1487 		// Blocks.
  2676 		// Blocks.
  1488 		if ( ! isset( $theme_json['styles']['blocks'] ) ) {
  2677 		if ( ! isset( $theme_json['styles']['blocks'] ) ) {
  1489 			return $nodes;
  2678 			return $nodes;
  1498 			$duotone_selector = null;
  2687 			$duotone_selector = null;
  1499 			if ( isset( $selectors[ $name ]['duotone'] ) ) {
  2688 			if ( isset( $selectors[ $name ]['duotone'] ) ) {
  1500 				$duotone_selector = $selectors[ $name ]['duotone'];
  2689 				$duotone_selector = $selectors[ $name ]['duotone'];
  1501 			}
  2690 			}
  1502 
  2691 
       
  2692 			$feature_selectors = null;
       
  2693 			if ( isset( $selectors[ $name ]['selectors'] ) ) {
       
  2694 				$feature_selectors = $selectors[ $name ]['selectors'];
       
  2695 			}
       
  2696 
       
  2697 			$variation_selectors = array();
       
  2698 			$include_variations  = $options['include_block_style_variations'] ?? false;
       
  2699 			if ( $include_variations && isset( $node['variations'] ) ) {
       
  2700 				foreach ( $node['variations'] as $variation => $node ) {
       
  2701 					$variation_selectors[] = array(
       
  2702 						'path'     => array( 'styles', 'blocks', $name, 'variations', $variation ),
       
  2703 						'selector' => $selectors[ $name ]['styleVariations'][ $variation ],
       
  2704 					);
       
  2705 				}
       
  2706 			}
       
  2707 
  1503 			$nodes[] = array(
  2708 			$nodes[] = array(
  1504 				'path'     => array( 'styles', 'blocks', $name ),
  2709 				'name'       => $name,
  1505 				'selector' => $selector,
  2710 				'path'       => array( 'styles', 'blocks', $name ),
  1506 				'duotone'  => $duotone_selector,
  2711 				'selector'   => $selector,
       
  2712 				'selectors'  => $feature_selectors,
       
  2713 				'duotone'    => $duotone_selector,
       
  2714 				'features'   => $feature_selectors,
       
  2715 				'variations' => $variation_selectors,
  1507 			);
  2716 			);
  1508 
  2717 
  1509 			if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
  2718 			if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
  1510 				foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
  2719 				foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
  1511 					$nodes[] = array(
  2720 					$nodes[] = array(
  1512 						'path'     => array( 'styles', 'blocks', $name, 'elements', $element ),
  2721 						'path'     => array( 'styles', 'blocks', $name, 'elements', $element ),
  1513 						'selector' => $selectors[ $name ]['elements'][ $element ],
  2722 						'selector' => $selectors[ $name ]['elements'][ $element ],
  1514 					);
  2723 					);
       
  2724 
       
  2725 					// Handle any pseudo selectors for the element.
       
  2726 					if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
       
  2727 						foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
       
  2728 							if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
       
  2729 								$nodes[] = array(
       
  2730 									'path'     => array( 'styles', 'blocks', $name, 'elements', $element ),
       
  2731 									'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ),
       
  2732 								);
       
  2733 							}
       
  2734 						}
       
  2735 					}
  1515 				}
  2736 				}
  1516 			}
  2737 			}
  1517 		}
  2738 		}
  1518 
  2739 
  1519 		return $nodes;
  2740 		return $nodes;
  1520 	}
  2741 	}
  1521 
  2742 
  1522 	/**
  2743 	/**
       
  2744 	 * Gets the CSS rules for a particular block from theme.json.
       
  2745 	 *
       
  2746 	 * @since 6.1.0
       
  2747 	 * @since 6.6.0 Setting a min-height of HTML when root styles have a background gradient or image.
       
  2748 	 *              Updated general global styles specificity to 0-1-0.
       
  2749 	 *              Fixed custom CSS output in block style variations.
       
  2750 	 *
       
  2751 	 * @param array $block_metadata Metadata about the block to get styles for.
       
  2752 	 *
       
  2753 	 * @return string Styles for the block.
       
  2754 	 */
       
  2755 	public function get_styles_for_block( $block_metadata ) {
       
  2756 		$node                 = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
       
  2757 		$use_root_padding     = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
       
  2758 		$selector             = $block_metadata['selector'];
       
  2759 		$settings             = isset( $this->theme_json['settings'] ) ? $this->theme_json['settings'] : array();
       
  2760 		$feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node );
       
  2761 		$is_root_selector     = static::ROOT_BLOCK_SELECTOR === $selector;
       
  2762 
       
  2763 		// If there are style variations, generate the declarations for them, including any feature selectors the block may have.
       
  2764 		$style_variation_declarations = array();
       
  2765 		$style_variation_custom_css   = array();
       
  2766 		if ( ! empty( $block_metadata['variations'] ) ) {
       
  2767 			foreach ( $block_metadata['variations'] as $style_variation ) {
       
  2768 				$style_variation_node           = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
       
  2769 				$clean_style_variation_selector = trim( $style_variation['selector'] );
       
  2770 
       
  2771 				// Generate any feature/subfeature style declarations for the current style variation.
       
  2772 				$variation_declarations = static::get_feature_declarations_for_node( $block_metadata, $style_variation_node );
       
  2773 
       
  2774 				// Combine selectors with style variation's selector and add to overall style variation declarations.
       
  2775 				foreach ( $variation_declarations as $current_selector => $new_declarations ) {
       
  2776 					// If current selector includes block classname, remove it but leave the whitespace in.
       
  2777 					$shortened_selector = str_replace( $block_metadata['selector'] . ' ', ' ', $current_selector );
       
  2778 
       
  2779 					// Prepend the variation selector to the current selector.
       
  2780 					$split_selectors    = explode( ',', $shortened_selector );
       
  2781 					$updated_selectors  = array_map(
       
  2782 						static function ( $split_selector ) use ( $clean_style_variation_selector ) {
       
  2783 							return $clean_style_variation_selector . $split_selector;
       
  2784 						},
       
  2785 						$split_selectors
       
  2786 					);
       
  2787 					$combined_selectors = implode( ',', $updated_selectors );
       
  2788 
       
  2789 					// Add the new declarations to the overall results under the modified selector.
       
  2790 					$style_variation_declarations[ $combined_selectors ] = $new_declarations;
       
  2791 				}
       
  2792 
       
  2793 				// Compute declarations for remaining styles not covered by feature level selectors.
       
  2794 				$style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json );
       
  2795 				// Store custom CSS for the style variation.
       
  2796 				if ( isset( $style_variation_node['css'] ) ) {
       
  2797 					$style_variation_custom_css[ $style_variation['selector'] ] = $this->process_blocks_custom_css( $style_variation_node['css'], $style_variation['selector'] );
       
  2798 				}
       
  2799 			}
       
  2800 		}
       
  2801 		/*
       
  2802 		 * Get a reference to element name from path.
       
  2803 		 * $block_metadata['path'] = array( 'styles','elements','link' );
       
  2804 		 * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ].
       
  2805 		 * Skip non-element paths like just ['styles'].
       
  2806 		 */
       
  2807 		$is_processing_element = in_array( 'elements', $block_metadata['path'], true );
       
  2808 
       
  2809 		$current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null;
       
  2810 
       
  2811 		$element_pseudo_allowed = array();
       
  2812 
       
  2813 		if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) {
       
  2814 			$element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ];
       
  2815 		}
       
  2816 
       
  2817 		/*
       
  2818 		 * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover").
       
  2819 		 * This also resets the array keys.
       
  2820 		 */
       
  2821 		$pseudo_matches = array_values(
       
  2822 			array_filter(
       
  2823 				$element_pseudo_allowed,
       
  2824 				static function ( $pseudo_selector ) use ( $selector ) {
       
  2825 					return str_contains( $selector, $pseudo_selector );
       
  2826 				}
       
  2827 			)
       
  2828 		);
       
  2829 
       
  2830 		$pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null;
       
  2831 
       
  2832 		/*
       
  2833 		 * If the current selector is a pseudo selector that's defined in the allow list for the current
       
  2834 		 * element then compute the style properties for it.
       
  2835 		 * Otherwise just compute the styles for the default selector as normal.
       
  2836 		 */
       
  2837 		if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) &&
       
  2838 			isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] )
       
  2839 			&& in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true )
       
  2840 		) {
       
  2841 			$declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding );
       
  2842 		} else {
       
  2843 			$declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
       
  2844 		}
       
  2845 
       
  2846 		$block_rules = '';
       
  2847 
       
  2848 		/*
       
  2849 		 * 1. Bespoke declaration modifiers:
       
  2850 		 * - 'filter': Separate the declarations that use the general selector
       
  2851 		 * from the ones using the duotone selector.
       
  2852 		 * - 'background|background-image': set the html min-height to 100%
       
  2853 		 * to ensure the background covers the entire viewport.
       
  2854 		 */
       
  2855 		$declarations_duotone       = array();
       
  2856 		$should_set_root_min_height = false;
       
  2857 
       
  2858 		foreach ( $declarations as $index => $declaration ) {
       
  2859 			if ( 'filter' === $declaration['name'] ) {
       
  2860 				/*
       
  2861 				 * 'unset' filters happen when a filter is unset
       
  2862 				 * in the site-editor UI. Because the 'unset' value
       
  2863 				 * in the user origin overrides the value in the
       
  2864 				 * theme origin, we can skip rendering anything
       
  2865 				 * here as no filter needs to be applied anymore.
       
  2866 				 * So only add declarations to with values other
       
  2867 				 * than 'unset'.
       
  2868 				 */
       
  2869 				if ( 'unset' !== $declaration['value'] ) {
       
  2870 					$declarations_duotone[] = $declaration;
       
  2871 				}
       
  2872 				unset( $declarations[ $index ] );
       
  2873 			}
       
  2874 
       
  2875 			if ( $is_root_selector && ( 'background-image' === $declaration['name'] || 'background' === $declaration['name'] ) ) {
       
  2876 				$should_set_root_min_height = true;
       
  2877 			}
       
  2878 		}
       
  2879 
       
  2880 		/*
       
  2881 		 * If root styles has a background-image or a background (gradient) set,
       
  2882 		 * set the min-height to '100%'. Minus `--wp-admin--admin-bar--height` for logged-in view.
       
  2883 		 * Setting the CSS rule on the HTML tag ensures background gradients and images behave similarly,
       
  2884 		 * and matches the behavior of the site editor.
       
  2885 		 */
       
  2886 		if ( $should_set_root_min_height ) {
       
  2887 			$block_rules .= static::to_ruleset(
       
  2888 				'html',
       
  2889 				array(
       
  2890 					array(
       
  2891 						'name'  => 'min-height',
       
  2892 						'value' => 'calc(100% - var(--wp-admin--admin-bar--height, 0px))',
       
  2893 					),
       
  2894 				)
       
  2895 			);
       
  2896 		}
       
  2897 
       
  2898 		// Update declarations if there are separators with only background color defined.
       
  2899 		if ( '.wp-block-separator' === $selector ) {
       
  2900 			$declarations = static::update_separator_declarations( $declarations );
       
  2901 		}
       
  2902 
       
  2903 		/*
       
  2904 		 * Root selector (body) styles should not be wrapped in `:root where()` to keep
       
  2905 		 * specificity at (0,0,1) and maintain backwards compatibility.
       
  2906 		 *
       
  2907 		 * Top-level element styles using element-only specificity selectors should
       
  2908 		 * not get wrapped in `:root :where()` to maintain backwards compatibility.
       
  2909 		 *
       
  2910 		 * Pseudo classes, e.g. :hover, :focus etc., are a class-level selector so
       
  2911 		 * still need to be wrapped in `:root :where` to cap specificity for nested
       
  2912 		 * variations etc. Pseudo selectors won't match the ELEMENTS selector exactly.
       
  2913 		 */
       
  2914 		$element_only_selector = $is_root_selector || (
       
  2915 			$current_element &&
       
  2916 			isset( static::ELEMENTS[ $current_element ] ) &&
       
  2917 			// buttons, captions etc. still need `:root :where()` as they are class based selectors.
       
  2918 			! isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $current_element ] ) &&
       
  2919 			static::ELEMENTS[ $current_element ] === $selector
       
  2920 		);
       
  2921 
       
  2922 		// 2. Generate and append the rules that use the general selector.
       
  2923 		$general_selector = $element_only_selector ? $selector : ":root :where($selector)";
       
  2924 		$block_rules     .= static::to_ruleset( $general_selector, $declarations );
       
  2925 
       
  2926 		// 3. Generate and append the rules that use the duotone selector.
       
  2927 		if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
       
  2928 			$block_rules .= static::to_ruleset( $block_metadata['duotone'], $declarations_duotone );
       
  2929 		}
       
  2930 
       
  2931 		// 4. Generate Layout block gap styles.
       
  2932 		if (
       
  2933 			! $is_root_selector &&
       
  2934 			! empty( $block_metadata['name'] )
       
  2935 		) {
       
  2936 			$block_rules .= $this->get_layout_styles( $block_metadata );
       
  2937 		}
       
  2938 
       
  2939 		// 5. Generate and append the feature level rulesets.
       
  2940 		foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) {
       
  2941 			$block_rules .= static::to_ruleset( ":root :where($feature_selector)", $individual_feature_declarations );
       
  2942 		}
       
  2943 
       
  2944 		// 6. Generate and append the style variation rulesets.
       
  2945 		foreach ( $style_variation_declarations as $style_variation_selector => $individual_style_variation_declarations ) {
       
  2946 			$block_rules .= static::to_ruleset( ":root :where($style_variation_selector)", $individual_style_variation_declarations );
       
  2947 			if ( isset( $style_variation_custom_css[ $style_variation_selector ] ) ) {
       
  2948 				$block_rules .= $style_variation_custom_css[ $style_variation_selector ];
       
  2949 			}
       
  2950 		}
       
  2951 
       
  2952 		// 7. Generate and append any custom CSS rules pertaining to nested block style variations.
       
  2953 		if ( isset( $node['css'] ) && ! $is_root_selector ) {
       
  2954 			$block_rules .= $this->process_blocks_custom_css( $node['css'], $selector );
       
  2955 		}
       
  2956 
       
  2957 		return $block_rules;
       
  2958 	}
       
  2959 
       
  2960 	/**
       
  2961 	 * Outputs the CSS for layout rules on the root.
       
  2962 	 *
       
  2963 	 * @since 6.1.0
       
  2964 	 * @since 6.6.0 Use `ROOT_CSS_PROPERTIES_SELECTOR` for CSS custom properties and improved consistency of root padding rules.
       
  2965 	 *              Updated specificity of body margin reset and first/last child selectors.
       
  2966 	 *
       
  2967 	 * @param string $selector The root node selector.
       
  2968 	 * @param array  $block_metadata The metadata for the root block.
       
  2969 	 * @return string The additional root rules CSS.
       
  2970 	 */
       
  2971 	public function get_root_layout_rules( $selector, $block_metadata ) {
       
  2972 		$css              = '';
       
  2973 		$settings         = isset( $this->theme_json['settings'] ) ? $this->theme_json['settings'] : array();
       
  2974 		$use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
       
  2975 
       
  2976 		/*
       
  2977 		* If there are content and wide widths in theme.json, output them
       
  2978 		* as custom properties on the body element so all blocks can use them.
       
  2979 		*/
       
  2980 		if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) {
       
  2981 			$content_size = isset( $settings['layout']['contentSize'] ) ? $settings['layout']['contentSize'] : $settings['layout']['wideSize'];
       
  2982 			$content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial';
       
  2983 			$wide_size    = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize'];
       
  2984 			$wide_size    = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial';
       
  2985 			$css         .= static::ROOT_CSS_PROPERTIES_SELECTOR . ' { --wp--style--global--content-size: ' . $content_size . ';';
       
  2986 			$css         .= '--wp--style--global--wide-size: ' . $wide_size . '; }';
       
  2987 		}
       
  2988 
       
  2989 		/*
       
  2990 		* Reset default browser margin on the body element.
       
  2991 		* This is set on the body selector **before** generating the ruleset
       
  2992 		* from the `theme.json`. This is to ensure that if the `theme.json` declares
       
  2993 		* `margin` in its `spacing` declaration for the `body` element then these
       
  2994 		* user-generated values take precedence in the CSS cascade.
       
  2995 		* @link https://github.com/WordPress/gutenberg/issues/36147.
       
  2996 		*/
       
  2997 		$css .= ':where(body) { margin: 0; }';
       
  2998 
       
  2999 		if ( $use_root_padding ) {
       
  3000 			// Top and bottom padding are applied to the outer block container.
       
  3001 			$css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }';
       
  3002 			// Right and left padding are applied to the first container with `.has-global-padding` class.
       
  3003 			$css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
       
  3004 			// Alignfull children of the container with left and right padding have negative margins so they can still be full width.
       
  3005 			$css .= '.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }';
       
  3006 			// Nested children of the container with left and right padding that are not full aligned do not get padding, unless they are direct children of an alignfull flow container.
       
  3007 			$css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) { padding-right: 0; padding-left: 0; }';
       
  3008 			// Alignfull direct children of the containers that are targeted by the rule above do not need negative margins.
       
  3009 			$css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) > .alignfull { margin-left: 0; margin-right: 0; }';
       
  3010 		}
       
  3011 
       
  3012 		$css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
       
  3013 		$css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
       
  3014 		$css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
       
  3015 
       
  3016 		// Block gap styles will be output unless explicitly set to `null`. See static::PROTECTED_PROPERTIES.
       
  3017 		if ( isset( $this->theme_json['settings']['spacing']['blockGap'] ) ) {
       
  3018 			$block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) );
       
  3019 			$css            .= ":where(.wp-site-blocks) > * { margin-block-start: $block_gap_value; margin-block-end: 0; }";
       
  3020 			$css            .= ':where(.wp-site-blocks) > :first-child { margin-block-start: 0; }';
       
  3021 			$css            .= ':where(.wp-site-blocks) > :last-child { margin-block-end: 0; }';
       
  3022 
       
  3023 			// For backwards compatibility, ensure the legacy block gap CSS variable is still available.
       
  3024 			$css .= static::ROOT_CSS_PROPERTIES_SELECTOR . " { --wp--style--block-gap: $block_gap_value; }";
       
  3025 		}
       
  3026 		$css .= $this->get_layout_styles( $block_metadata );
       
  3027 
       
  3028 		return $css;
       
  3029 	}
       
  3030 
       
  3031 	/**
  1523 	 * For metadata values that can either be booleans or paths to booleans, gets the value.
  3032 	 * For metadata values that can either be booleans or paths to booleans, gets the value.
  1524 	 *
  3033 	 *
  1525 	 * ```php
  3034 	 *     $data = array(
  1526 	 * $data = array(
  3035 	 *       'color' => array(
  1527 	 *   'color' => array(
  3036 	 *         'defaultPalette' => true
  1528 	 *     'defaultPalette' => true
  3037 	 *       )
  1529 	 *   )
  3038 	 *     );
  1530 	 * );
  3039 	 *
  1531 	 *
  3040 	 *     static::get_metadata_boolean( $data, false );
  1532 	 * static::get_metadata_boolean( $data, false );
  3041 	 *     // => false
  1533 	 * // => false
  3042 	 *
  1534 	 *
  3043 	 *     static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) );
  1535 	 * static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) );
  3044 	 *     // => true
  1536 	 * // => true
       
  1537 	 * ```
       
  1538 	 *
  3045 	 *
  1539 	 * @since 6.0.0
  3046 	 * @since 6.0.0
  1540 	 *
  3047 	 *
  1541 	 * @param array      $data    The data to inspect.
  3048 	 * @param array      $data          The data to inspect.
  1542 	 * @param bool|array $path    Boolean or path to a boolean.
  3049 	 * @param bool|array $path          Boolean or path to a boolean.
  1543 	 * @param bool       $default Default value if the referenced path is missing.
  3050 	 * @param bool       $default_value Default value if the referenced path is missing.
  1544 	 *                            Default false.
  3051 	 *                                  Default false.
  1545 	 * @return bool Value of boolean metadata.
  3052 	 * @return bool Value of boolean metadata.
  1546 	 */
  3053 	 */
  1547 	protected static function get_metadata_boolean( $data, $path, $default = false ) {
  3054 	protected static function get_metadata_boolean( $data, $path, $default_value = false ) {
  1548 		if ( is_bool( $path ) ) {
  3055 		if ( is_bool( $path ) ) {
  1549 			return $path;
  3056 			return $path;
  1550 		}
  3057 		}
  1551 
  3058 
  1552 		if ( is_array( $path ) ) {
  3059 		if ( is_array( $path ) ) {
  1554 			if ( null !== $value ) {
  3061 			if ( null !== $value ) {
  1555 				return $value;
  3062 				return $value;
  1556 			}
  3063 			}
  1557 		}
  3064 		}
  1558 
  3065 
  1559 		return $default;
  3066 		return $default_value;
  1560 	}
  3067 	}
  1561 
  3068 
  1562 	/**
  3069 	/**
  1563 	 * Merge new incoming data.
  3070 	 * Merges new incoming data.
  1564 	 *
  3071 	 *
  1565 	 * @since 5.8.0
  3072 	 * @since 5.8.0
  1566 	 * @since 5.9.0 Duotone preset also has origins.
  3073 	 * @since 5.9.0 Duotone preset also has origins.
  1567 	 *
  3074 	 *
  1568 	 * @param WP_Theme_JSON $incoming Data to merge.
  3075 	 * @param WP_Theme_JSON $incoming Data to merge.
  1569 	 */
  3076 	 */
  1570 	public function merge( $incoming ) {
  3077 	public function merge( $incoming ) {
  1571 		$incoming_data    = $incoming->get_raw_data();
  3078 		$incoming_data    = $incoming->get_raw_data();
  1572 		$this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
  3079 		$this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
       
  3080 
       
  3081 		/*
       
  3082 		 * Recompute all the spacing sizes based on the new hierarchy of data. In the constructor
       
  3083 		 * spacingScale and spacingSizes are both keyed by origin and VALID_ORIGINS is ordered, so
       
  3084 		 * we can allow partial spacingScale data to inherit missing data from earlier layers when
       
  3085 		 * computing the spacing sizes.
       
  3086 		 *
       
  3087 		 * This happens before the presets are merged to ensure that default spacing sizes can be
       
  3088 		 * removed from the theme origin if $prevent_override is true.
       
  3089 		 */
       
  3090 		$flattened_spacing_scale = array();
       
  3091 		foreach ( static::VALID_ORIGINS as $origin ) {
       
  3092 			$scale_path = array( 'settings', 'spacing', 'spacingScale', $origin );
       
  3093 
       
  3094 			// Apply the base spacing scale to the current layer.
       
  3095 			$base_spacing_scale      = _wp_array_get( $this->theme_json, $scale_path, array() );
       
  3096 			$flattened_spacing_scale = array_replace( $flattened_spacing_scale, $base_spacing_scale );
       
  3097 
       
  3098 			$spacing_scale = _wp_array_get( $incoming_data, $scale_path, null );
       
  3099 			if ( ! isset( $spacing_scale ) ) {
       
  3100 				continue;
       
  3101 			}
       
  3102 
       
  3103 			// Allow partial scale settings by merging with lower layers.
       
  3104 			$flattened_spacing_scale = array_replace( $flattened_spacing_scale, $spacing_scale );
       
  3105 
       
  3106 			// Generate and merge the scales for this layer.
       
  3107 			$sizes_path           = array( 'settings', 'spacing', 'spacingSizes', $origin );
       
  3108 			$spacing_sizes        = _wp_array_get( $incoming_data, $sizes_path, array() );
       
  3109 			$spacing_scale_sizes  = static::compute_spacing_sizes( $flattened_spacing_scale );
       
  3110 			$merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );
       
  3111 
       
  3112 			_wp_array_set( $incoming_data, $sizes_path, $merged_spacing_sizes );
       
  3113 		}
  1573 
  3114 
  1574 		/*
  3115 		/*
  1575 		 * The array_replace_recursive algorithm merges at the leaf level,
  3116 		 * The array_replace_recursive algorithm merges at the leaf level,
  1576 		 * but we don't want leaf arrays to be merged, so we overwrite it.
  3117 		 * but we don't want leaf arrays to be merged, so we overwrite it.
  1577 		 *
  3118 		 *
  1594 		 * we remove it from the theme presets.
  3135 		 * we remove it from the theme presets.
  1595 		 */
  3136 		 */
  1596 		$nodes        = static::get_setting_nodes( $incoming_data );
  3137 		$nodes        = static::get_setting_nodes( $incoming_data );
  1597 		$slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) );
  3138 		$slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) );
  1598 		foreach ( $nodes as $node ) {
  3139 		foreach ( $nodes as $node ) {
  1599 			$slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] );
       
  1600 			$slugs      = array_merge_recursive( $slugs_global, $slugs_node );
       
  1601 
       
  1602 			// Replace the spacing.units.
  3140 			// Replace the spacing.units.
  1603 			$path    = array_merge( $node['path'], array( 'spacing', 'units' ) );
  3141 			$path   = $node['path'];
       
  3142 			$path[] = 'spacing';
       
  3143 			$path[] = 'units';
       
  3144 
  1604 			$content = _wp_array_get( $incoming_data, $path, null );
  3145 			$content = _wp_array_get( $incoming_data, $path, null );
  1605 			if ( isset( $content ) ) {
  3146 			if ( isset( $content ) ) {
  1606 				_wp_array_set( $this->theme_json, $path, $content );
  3147 				_wp_array_set( $this->theme_json, $path, $content );
  1607 			}
  3148 			}
  1608 
  3149 
  1609 			// Replace the presets.
  3150 			// Replace the presets.
  1610 			foreach ( static::PRESETS_METADATA as $preset ) {
  3151 			foreach ( static::PRESETS_METADATA as $preset_metadata ) {
  1611 				$override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true );
  3152 				$prevent_override = $preset_metadata['prevent_override'];
       
  3153 				if ( is_array( $prevent_override ) ) {
       
  3154 					$prevent_override = _wp_array_get( $this->theme_json['settings'], $preset_metadata['prevent_override'] );
       
  3155 				}
  1612 
  3156 
  1613 				foreach ( static::VALID_ORIGINS as $origin ) {
  3157 				foreach ( static::VALID_ORIGINS as $origin ) {
  1614 					$base_path = array_merge( $node['path'], $preset['path'] );
  3158 					$base_path = $node['path'];
  1615 					$path      = array_merge( $base_path, array( $origin ) );
  3159 					foreach ( $preset_metadata['path'] as $leaf ) {
  1616 					$content   = _wp_array_get( $incoming_data, $path, null );
  3160 						$base_path[] = $leaf;
       
  3161 					}
       
  3162 
       
  3163 					$path   = $base_path;
       
  3164 					$path[] = $origin;
       
  3165 
       
  3166 					$content = _wp_array_get( $incoming_data, $path, null );
  1617 					if ( ! isset( $content ) ) {
  3167 					if ( ! isset( $content ) ) {
  1618 						continue;
  3168 						continue;
  1619 					}
  3169 					}
  1620 
  3170 
  1621 					if ( 'theme' === $origin && $preset['use_default_names'] ) {
  3171 					// Set names for theme presets based on the slug if they are not set and can use default names.
  1622 						foreach ( $content as &$item ) {
  3172 					if ( 'theme' === $origin && $preset_metadata['use_default_names'] ) {
  1623 							if ( ! array_key_exists( 'name', $item ) ) {
  3173 						foreach ( $content as $key => $item ) {
       
  3174 							if ( ! isset( $item['name'] ) ) {
  1624 								$name = static::get_name_from_defaults( $item['slug'], $base_path );
  3175 								$name = static::get_name_from_defaults( $item['slug'], $base_path );
  1625 								if ( null !== $name ) {
  3176 								if ( null !== $name ) {
  1626 									$item['name'] = $name;
  3177 									$content[ $key ]['name'] = $name;
  1627 								}
  3178 								}
  1628 							}
  3179 							}
  1629 						}
  3180 						}
  1630 					}
  3181 					}
  1631 
  3182 
  1632 					if (
  3183 					// Filter out default slugs from theme presets when defaults should not be overridden.
  1633 						( 'theme' !== $origin ) ||
  3184 					if ( 'theme' === $origin && $prevent_override ) {
  1634 						( 'theme' === $origin && $override_preset )
  3185 						$slugs_node    = static::get_default_slugs( $this->theme_json, $node['path'] );
  1635 					) {
  3186 						$preset_global = _wp_array_get( $slugs_global, $preset_metadata['path'], array() );
  1636 						_wp_array_set( $this->theme_json, $path, $content );
  3187 						$preset_node   = _wp_array_get( $slugs_node, $preset_metadata['path'], array() );
  1637 					} else {
  3188 						$preset_slugs  = array_merge_recursive( $preset_global, $preset_node );
  1638 						$slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() );
  3189 
  1639 						$content          = static::filter_slugs( $content, $slugs_for_preset );
  3190 						$content = static::filter_slugs( $content, $preset_slugs );
  1640 						_wp_array_set( $this->theme_json, $path, $content );
       
  1641 					}
  3191 					}
       
  3192 
       
  3193 					_wp_array_set( $this->theme_json, $path, $content );
  1642 				}
  3194 				}
  1643 			}
  3195 			}
  1644 		}
  3196 		}
  1645 	}
  3197 	}
  1646 
  3198 
  1677 
  3229 
  1678 		return $filters;
  3230 		return $filters;
  1679 	}
  3231 	}
  1680 
  3232 
  1681 	/**
  3233 	/**
  1682 	 * Returns whether a presets should be overridden or not.
  3234 	 * Determines whether a presets should be overridden or not.
  1683 	 *
  3235 	 *
  1684 	 * @since 5.9.0
  3236 	 * @since 5.9.0
  1685 	 * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead.
  3237 	 * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead.
  1686 	 *
  3238 	 *
  1687 	 * @param array      $theme_json The theme.json like structure to inspect.
  3239 	 * @param array      $theme_json The theme.json like structure to inspect.
  1688 	 * @param array      $path       Path to inspect.
  3240 	 * @param array      $path       Path to inspect.
  1689 	 * @param bool|array $override   Data to compute whether to override the preset.
  3241 	 * @param bool|array $override   Data to compute whether to override the preset.
  1690 	 * @return boolean
  3242 	 * @return bool
  1691 	 */
  3243 	 */
  1692 	protected static function should_override_preset( $theme_json, $path, $override ) {
  3244 	protected static function should_override_preset( $theme_json, $path, $override ) {
  1693 		_deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' );
  3245 		_deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' );
  1694 
  3246 
  1695 		if ( is_bool( $override ) ) {
  3247 		if ( is_bool( $override ) ) {
  1724 		}
  3276 		}
  1725 	}
  3277 	}
  1726 
  3278 
  1727 	/**
  3279 	/**
  1728 	 * Returns the default slugs for all the presets in an associative array
  3280 	 * Returns the default slugs for all the presets in an associative array
  1729 	 * whose keys are the preset paths and the leafs is the list of slugs.
  3281 	 * whose keys are the preset paths and the leaves is the list of slugs.
  1730 	 *
  3282 	 *
  1731 	 * For example:
  3283 	 * For example:
  1732 	 *
  3284 	 *
  1733 	 *  array(
  3285 	 *     array(
  1734 	 *   'color' => array(
  3286 	 *       'color' => array(
  1735 	 *     'palette'   => array( 'slug-1', 'slug-2' ),
  3287 	 *         'palette'   => array( 'slug-1', 'slug-2' ),
  1736 	 *     'gradients' => array( 'slug-3', 'slug-4' ),
  3288 	 *         'gradients' => array( 'slug-3', 'slug-4' ),
  1737 	 *   ),
  3289 	 *       ),
  1738 	 * )
  3290 	 *     )
  1739 	 *
  3291 	 *
  1740 	 * @since 5.9.0
  3292 	 * @since 5.9.0
  1741 	 *
  3293 	 *
  1742 	 * @param array $data      A theme.json like structure.
  3294 	 * @param array $data      A theme.json like structure.
  1743 	 * @param array $node_path The path to inspect. It's 'settings' by default.
  3295 	 * @param array $node_path The path to inspect. It's 'settings' by default.
  1745 	 */
  3297 	 */
  1746 	protected static function get_default_slugs( $data, $node_path ) {
  3298 	protected static function get_default_slugs( $data, $node_path ) {
  1747 		$slugs = array();
  3299 		$slugs = array();
  1748 
  3300 
  1749 		foreach ( static::PRESETS_METADATA as $metadata ) {
  3301 		foreach ( static::PRESETS_METADATA as $metadata ) {
  1750 			$path   = array_merge( $node_path, $metadata['path'], array( 'default' ) );
  3302 			$path = $node_path;
       
  3303 			foreach ( $metadata['path'] as $leaf ) {
       
  3304 				$path[] = $leaf;
       
  3305 			}
       
  3306 			$path[] = 'default';
       
  3307 
  1751 			$preset = _wp_array_get( $data, $path, null );
  3308 			$preset = _wp_array_get( $data, $path, null );
  1752 			if ( ! isset( $preset ) ) {
  3309 			if ( ! isset( $preset ) ) {
  1753 				continue;
  3310 				continue;
  1754 			}
  3311 			}
  1755 
  3312 
  1756 			$slugs_for_preset = array();
  3313 			$slugs_for_preset = array();
  1757 			$slugs_for_preset = array_map(
  3314 			foreach ( $preset as $item ) {
  1758 				static function( $value ) {
  3315 				if ( isset( $item['slug'] ) ) {
  1759 					return isset( $value['slug'] ) ? $value['slug'] : null;
  3316 					$slugs_for_preset[] = $item['slug'];
  1760 				},
  3317 				}
  1761 				$preset
  3318 			}
  1762 			);
  3319 
  1763 			_wp_array_set( $slugs, $metadata['path'], $slugs_for_preset );
  3320 			_wp_array_set( $slugs, $metadata['path'], $slugs_for_preset );
  1764 		}
  3321 		}
  1765 
  3322 
  1766 		return $slugs;
  3323 		return $slugs;
  1767 	}
  3324 	}
  1768 
  3325 
  1769 	/**
  3326 	/**
  1770 	 * Get a `default`'s preset name by a provided slug.
  3327 	 * Gets a `default`'s preset name by a provided slug.
  1771 	 *
  3328 	 *
  1772 	 * @since 5.9.0
  3329 	 * @since 5.9.0
  1773 	 *
  3330 	 *
  1774 	 * @param string $slug The slug we want to find a match from default presets.
  3331 	 * @param string $slug The slug we want to find a match from default presets.
  1775 	 * @param array  $base_path The path to inspect. It's 'settings' by default.
  3332 	 * @param array  $base_path The path to inspect. It's 'settings' by default.
  1776 	 * @return string|null
  3333 	 * @return string|null
  1777 	 */
  3334 	 */
  1778 	protected function get_name_from_defaults( $slug, $base_path ) {
  3335 	protected function get_name_from_defaults( $slug, $base_path ) {
  1779 		$path            = array_merge( $base_path, array( 'default' ) );
  3336 		$path            = $base_path;
       
  3337 		$path[]          = 'default';
  1780 		$default_content = _wp_array_get( $this->theme_json, $path, null );
  3338 		$default_content = _wp_array_get( $this->theme_json, $path, null );
  1781 		if ( ! $default_content ) {
  3339 		if ( ! $default_content ) {
  1782 			return null;
  3340 			return null;
  1783 		}
  3341 		}
  1784 		foreach ( $default_content as $item ) {
  3342 		foreach ( $default_content as $item ) {
  1815 
  3373 
  1816 	/**
  3374 	/**
  1817 	 * Removes insecure data from theme.json.
  3375 	 * Removes insecure data from theme.json.
  1818 	 *
  3376 	 *
  1819 	 * @since 5.9.0
  3377 	 * @since 5.9.0
  1820 	 *
  3378 	 * @since 6.3.2 Preserves global styles block variations when securing styles.
  1821 	 * @param array $theme_json Structure to sanitize.
  3379 	 * @since 6.6.0 Updated to allow variation element styles and $origin parameter.
       
  3380 	 *
       
  3381 	 * @param array  $theme_json Structure to sanitize.
       
  3382 	 * @param string $origin    Optional. What source of data this object represents.
       
  3383 	 *                          One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
  1822 	 * @return array Sanitized structure.
  3384 	 * @return array Sanitized structure.
  1823 	 */
  3385 	 */
  1824 	public static function remove_insecure_properties( $theme_json ) {
  3386 	public static function remove_insecure_properties( $theme_json, $origin = 'theme' ) {
       
  3387 		if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
       
  3388 			$origin = 'theme';
       
  3389 		}
       
  3390 
  1825 		$sanitized = array();
  3391 		$sanitized = array();
  1826 
  3392 
  1827 		$theme_json = WP_Theme_JSON_Schema::migrate( $theme_json );
  3393 		$theme_json = WP_Theme_JSON_Schema::migrate( $theme_json, $origin );
  1828 
  3394 
  1829 		$valid_block_names   = array_keys( static::get_blocks_metadata() );
  3395 		$valid_block_names   = array_keys( static::get_blocks_metadata() );
  1830 		$valid_element_names = array_keys( static::ELEMENTS );
  3396 		$valid_element_names = array_keys( static::ELEMENTS );
  1831 		$theme_json          = static::sanitize( $theme_json, $valid_block_names, $valid_element_names );
  3397 		$valid_variations    = static::get_valid_block_style_variations();
       
  3398 
       
  3399 		$theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations );
  1832 
  3400 
  1833 		$blocks_metadata = static::get_blocks_metadata();
  3401 		$blocks_metadata = static::get_blocks_metadata();
  1834 		$style_nodes     = static::get_style_nodes( $theme_json, $blocks_metadata );
  3402 		$style_options   = array( 'include_block_style_variations' => true ); // Allow variations data.
       
  3403 		$style_nodes     = static::get_style_nodes( $theme_json, $blocks_metadata, $style_options );
       
  3404 
  1835 		foreach ( $style_nodes as $metadata ) {
  3405 		foreach ( $style_nodes as $metadata ) {
  1836 			$input = _wp_array_get( $theme_json, $metadata['path'], array() );
  3406 			$input = _wp_array_get( $theme_json, $metadata['path'], array() );
  1837 			if ( empty( $input ) ) {
  3407 			if ( empty( $input ) ) {
  1838 				continue;
  3408 				continue;
  1839 			}
  3409 			}
  1840 
  3410 
  1841 			$output = static::remove_insecure_styles( $input );
  3411 			// The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability.
       
  3412 			if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) {
       
  3413 				$output = $input;
       
  3414 			} else {
       
  3415 				$output = static::remove_insecure_styles( $input );
       
  3416 			}
       
  3417 
       
  3418 			/*
       
  3419 			 * Get a reference to element name from path.
       
  3420 			 * $metadata['path'] = array( 'styles', 'elements', 'link' );
       
  3421 			 */
       
  3422 			$current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ];
       
  3423 
       
  3424 			/*
       
  3425 			 * $output is stripped of pseudo selectors. Re-add and process them
       
  3426 			 * or insecure styles here.
       
  3427 			 */
       
  3428 			if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) {
       
  3429 				foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) {
       
  3430 					if ( isset( $input[ $pseudo_selector ] ) ) {
       
  3431 						$output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] );
       
  3432 					}
       
  3433 				}
       
  3434 			}
       
  3435 
  1842 			if ( ! empty( $output ) ) {
  3436 			if ( ! empty( $output ) ) {
  1843 				_wp_array_set( $sanitized, $metadata['path'], $output );
  3437 				_wp_array_set( $sanitized, $metadata['path'], $output );
       
  3438 			}
       
  3439 
       
  3440 			if ( isset( $metadata['variations'] ) ) {
       
  3441 				foreach ( $metadata['variations'] as $variation ) {
       
  3442 					$variation_input = _wp_array_get( $theme_json, $variation['path'], array() );
       
  3443 					if ( empty( $variation_input ) ) {
       
  3444 						continue;
       
  3445 					}
       
  3446 
       
  3447 					$variation_output = static::remove_insecure_styles( $variation_input );
       
  3448 
       
  3449 					// Process a variation's elements and element pseudo selector styles.
       
  3450 					if ( isset( $variation_input['elements'] ) ) {
       
  3451 						foreach ( $valid_element_names as $element_name ) {
       
  3452 							$element_input = $variation_input['elements'][ $element_name ] ?? null;
       
  3453 							if ( $element_input ) {
       
  3454 								$element_output = static::remove_insecure_styles( $element_input );
       
  3455 
       
  3456 								if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
       
  3457 									foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
       
  3458 										if ( isset( $element_input[ $pseudo_selector ] ) ) {
       
  3459 											$element_output[ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $pseudo_selector ] );
       
  3460 										}
       
  3461 									}
       
  3462 								}
       
  3463 
       
  3464 								if ( ! empty( $element_output ) ) {
       
  3465 									_wp_array_set( $variation_output, array( 'elements', $element_name ), $element_output );
       
  3466 								}
       
  3467 							}
       
  3468 						}
       
  3469 					}
       
  3470 
       
  3471 					if ( ! empty( $variation_output ) ) {
       
  3472 						_wp_array_set( $sanitized, $variation['path'], $variation_output );
       
  3473 					}
       
  3474 				}
  1844 			}
  3475 			}
  1845 		}
  3476 		}
  1846 
  3477 
  1847 		$setting_nodes = static::get_setting_nodes( $theme_json );
  3478 		$setting_nodes = static::get_setting_nodes( $theme_json );
  1848 		foreach ( $setting_nodes as $metadata ) {
  3479 		foreach ( $setting_nodes as $metadata ) {
  1883 	 */
  3514 	 */
  1884 	protected static function remove_insecure_settings( $input ) {
  3515 	protected static function remove_insecure_settings( $input ) {
  1885 		$output = array();
  3516 		$output = array();
  1886 		foreach ( static::PRESETS_METADATA as $preset_metadata ) {
  3517 		foreach ( static::PRESETS_METADATA as $preset_metadata ) {
  1887 			foreach ( static::VALID_ORIGINS as $origin ) {
  3518 			foreach ( static::VALID_ORIGINS as $origin ) {
  1888 				$path_with_origin = array_merge( $preset_metadata['path'], array( $origin ) );
  3519 				$path_with_origin   = $preset_metadata['path'];
  1889 				$presets          = _wp_array_get( $input, $path_with_origin, null );
  3520 				$path_with_origin[] = $origin;
       
  3521 				$presets            = _wp_array_get( $input, $path_with_origin, null );
  1890 				if ( null === $presets ) {
  3522 				if ( null === $presets ) {
  1891 					continue;
  3523 					continue;
  1892 				}
  3524 				}
  1893 
  3525 
  1894 				$escaped_preset = array();
  3526 				$escaped_preset = array();
  1924 				if ( ! empty( $escaped_preset ) ) {
  3556 				if ( ! empty( $escaped_preset ) ) {
  1925 					_wp_array_set( $output, $path_with_origin, $escaped_preset );
  3557 					_wp_array_set( $output, $path_with_origin, $escaped_preset );
  1926 				}
  3558 				}
  1927 			}
  3559 			}
  1928 		}
  3560 		}
       
  3561 
       
  3562 		// Ensure indirect properties not included in any `PRESETS_METADATA` value are allowed.
       
  3563 		static::remove_indirect_properties( $input, $output );
       
  3564 
  1929 		return $output;
  3565 		return $output;
  1930 	}
  3566 	}
  1931 
  3567 
  1932 	/**
  3568 	/**
  1933 	 * Processes a style node and returns the same node
  3569 	 * Processes a style node and returns the same node
  1944 
  3580 
  1945 		foreach ( $declarations as $declaration ) {
  3581 		foreach ( $declarations as $declaration ) {
  1946 			if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) {
  3582 			if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) {
  1947 				$path = static::PROPERTIES_METADATA[ $declaration['name'] ];
  3583 				$path = static::PROPERTIES_METADATA[ $declaration['name'] ];
  1948 
  3584 
  1949 				// Check the value isn't an array before adding so as to not
  3585 				/*
  1950 				// double up shorthand and longhand styles.
  3586 				 * Check the value isn't an array before adding so as to not
       
  3587 				 * double up shorthand and longhand styles.
       
  3588 				 */
  1951 				$value = _wp_array_get( $input, $path, array() );
  3589 				$value = _wp_array_get( $input, $path, array() );
  1952 				if ( ! is_array( $value ) ) {
  3590 				if ( ! is_array( $value ) ) {
  1953 					_wp_array_set( $output, $path, $value );
  3591 					_wp_array_set( $output, $path, $value );
  1954 				}
  3592 				}
  1955 			}
  3593 			}
  1956 		}
  3594 		}
       
  3595 
       
  3596 		// Ensure indirect properties not handled by `compute_style_properties` are allowed.
       
  3597 		static::remove_indirect_properties( $input, $output );
       
  3598 
  1957 		return $output;
  3599 		return $output;
  1958 	}
  3600 	}
  1959 
  3601 
  1960 	/**
  3602 	/**
  1961 	 * Checks that a declaration provided by the user is safe.
  3603 	 * Checks that a declaration provided by the user is safe.
  1968 	 */
  3610 	 */
  1969 	protected static function is_safe_css_declaration( $property_name, $property_value ) {
  3611 	protected static function is_safe_css_declaration( $property_name, $property_value ) {
  1970 		$style_to_validate = $property_name . ': ' . $property_value;
  3612 		$style_to_validate = $property_name . ': ' . $property_value;
  1971 		$filtered          = esc_html( safecss_filter_attr( $style_to_validate ) );
  3613 		$filtered          = esc_html( safecss_filter_attr( $style_to_validate ) );
  1972 		return ! empty( trim( $filtered ) );
  3614 		return ! empty( trim( $filtered ) );
       
  3615 	}
       
  3616 
       
  3617 	/**
       
  3618 	 * Removes indirect properties from the given input node and
       
  3619 	 * sets in the given output node.
       
  3620 	 *
       
  3621 	 * @since 6.2.0
       
  3622 	 *
       
  3623 	 * @param array $input  Node to process.
       
  3624 	 * @param array $output The processed node. Passed by reference.
       
  3625 	 */
       
  3626 	private static function remove_indirect_properties( $input, &$output ) {
       
  3627 		foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) {
       
  3628 			foreach ( $paths as $path ) {
       
  3629 				$value = _wp_array_get( $input, $path );
       
  3630 				if (
       
  3631 					is_string( $value ) &&
       
  3632 					static::is_safe_css_declaration( $property, $value )
       
  3633 				) {
       
  3634 					_wp_array_set( $output, $path, $value );
       
  3635 				}
       
  3636 			}
       
  3637 		}
  1973 	}
  3638 	}
  1974 
  3639 
  1975 	/**
  3640 	/**
  1976 	 * Returns the raw data.
  3641 	 * Returns the raw data.
  1977 	 *
  3642 	 *
  1998 			'settings' => array(),
  3663 			'settings' => array(),
  1999 		);
  3664 		);
  2000 
  3665 
  2001 		// Deprecated theme supports.
  3666 		// Deprecated theme supports.
  2002 		if ( isset( $settings['disableCustomColors'] ) ) {
  3667 		if ( isset( $settings['disableCustomColors'] ) ) {
  2003 			if ( ! isset( $theme_settings['settings']['color'] ) ) {
       
  2004 				$theme_settings['settings']['color'] = array();
       
  2005 			}
       
  2006 			$theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors'];
  3668 			$theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors'];
  2007 		}
  3669 		}
  2008 
  3670 
  2009 		if ( isset( $settings['disableCustomGradients'] ) ) {
  3671 		if ( isset( $settings['disableCustomGradients'] ) ) {
  2010 			if ( ! isset( $theme_settings['settings']['color'] ) ) {
       
  2011 				$theme_settings['settings']['color'] = array();
       
  2012 			}
       
  2013 			$theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients'];
  3672 			$theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients'];
  2014 		}
  3673 		}
  2015 
  3674 
  2016 		if ( isset( $settings['disableCustomFontSizes'] ) ) {
  3675 		if ( isset( $settings['disableCustomFontSizes'] ) ) {
  2017 			if ( ! isset( $theme_settings['settings']['typography'] ) ) {
       
  2018 				$theme_settings['settings']['typography'] = array();
       
  2019 			}
       
  2020 			$theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes'];
  3676 			$theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes'];
  2021 		}
  3677 		}
  2022 
  3678 
  2023 		if ( isset( $settings['enableCustomLineHeight'] ) ) {
  3679 		if ( isset( $settings['enableCustomLineHeight'] ) ) {
  2024 			if ( ! isset( $theme_settings['settings']['typography'] ) ) {
       
  2025 				$theme_settings['settings']['typography'] = array();
       
  2026 			}
       
  2027 			$theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight'];
  3680 			$theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight'];
  2028 		}
  3681 		}
  2029 
  3682 
  2030 		if ( isset( $settings['enableCustomUnits'] ) ) {
  3683 		if ( isset( $settings['enableCustomUnits'] ) ) {
  2031 			if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
       
  2032 				$theme_settings['settings']['spacing'] = array();
       
  2033 			}
       
  2034 			$theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ?
  3684 			$theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ?
  2035 				array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) :
  3685 				array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) :
  2036 				$settings['enableCustomUnits'];
  3686 				$settings['enableCustomUnits'];
  2037 		}
  3687 		}
  2038 
  3688 
  2039 		if ( isset( $settings['colors'] ) ) {
  3689 		if ( isset( $settings['colors'] ) ) {
  2040 			if ( ! isset( $theme_settings['settings']['color'] ) ) {
       
  2041 				$theme_settings['settings']['color'] = array();
       
  2042 			}
       
  2043 			$theme_settings['settings']['color']['palette'] = $settings['colors'];
  3690 			$theme_settings['settings']['color']['palette'] = $settings['colors'];
  2044 		}
  3691 		}
  2045 
  3692 
  2046 		if ( isset( $settings['gradients'] ) ) {
  3693 		if ( isset( $settings['gradients'] ) ) {
  2047 			if ( ! isset( $theme_settings['settings']['color'] ) ) {
       
  2048 				$theme_settings['settings']['color'] = array();
       
  2049 			}
       
  2050 			$theme_settings['settings']['color']['gradients'] = $settings['gradients'];
  3694 			$theme_settings['settings']['color']['gradients'] = $settings['gradients'];
  2051 		}
  3695 		}
  2052 
  3696 
  2053 		if ( isset( $settings['fontSizes'] ) ) {
  3697 		if ( isset( $settings['fontSizes'] ) ) {
  2054 			$font_sizes = $settings['fontSizes'];
  3698 			$font_sizes = $settings['fontSizes'];
  2056 			foreach ( $font_sizes as $key => $font_size ) {
  3700 			foreach ( $font_sizes as $key => $font_size ) {
  2057 				if ( is_numeric( $font_size['size'] ) ) {
  3701 				if ( is_numeric( $font_size['size'] ) ) {
  2058 					$font_sizes[ $key ]['size'] = $font_size['size'] . 'px';
  3702 					$font_sizes[ $key ]['size'] = $font_size['size'] . 'px';
  2059 				}
  3703 				}
  2060 			}
  3704 			}
  2061 			if ( ! isset( $theme_settings['settings']['typography'] ) ) {
       
  2062 				$theme_settings['settings']['typography'] = array();
       
  2063 			}
       
  2064 			$theme_settings['settings']['typography']['fontSizes'] = $font_sizes;
  3705 			$theme_settings['settings']['typography']['fontSizes'] = $font_sizes;
  2065 		}
  3706 		}
  2066 
  3707 
  2067 		if ( isset( $settings['enableCustomSpacing'] ) ) {
  3708 		if ( isset( $settings['enableCustomSpacing'] ) ) {
  2068 			if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
       
  2069 				$theme_settings['settings']['spacing'] = array();
       
  2070 			}
       
  2071 			$theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing'];
  3709 			$theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing'];
       
  3710 		}
       
  3711 
       
  3712 		if ( isset( $settings['spacingSizes'] ) ) {
       
  3713 			$theme_settings['settings']['spacing']['spacingSizes'] = $settings['spacingSizes'];
  2072 		}
  3714 		}
  2073 
  3715 
  2074 		return $theme_settings;
  3716 		return $theme_settings;
  2075 	}
  3717 	}
  2076 
  3718 
  2129 		 *   }
  3771 		 *   }
  2130 		 * }
  3772 		 * }
  2131 		 */
  3773 		 */
  2132 		foreach ( $nodes as $node ) {
  3774 		foreach ( $nodes as $node ) {
  2133 			foreach ( static::PRESETS_METADATA as $preset_metadata ) {
  3775 			foreach ( static::PRESETS_METADATA as $preset_metadata ) {
  2134 				$path   = array_merge( $node['path'], $preset_metadata['path'] );
  3776 				$path = $node['path'];
       
  3777 				foreach ( $preset_metadata['path'] as $preset_metadata_path ) {
       
  3778 					$path[] = $preset_metadata_path;
       
  3779 				}
  2135 				$preset = _wp_array_get( $output, $path, null );
  3780 				$preset = _wp_array_get( $output, $path, null );
  2136 				if ( null === $preset ) {
  3781 				if ( null === $preset ) {
  2137 					continue;
  3782 					continue;
  2138 				}
  3783 				}
  2139 
  3784 
  2152 						$items[ $slug ] = $item;
  3797 						$items[ $slug ] = $item;
  2153 					}
  3798 					}
  2154 				}
  3799 				}
  2155 				$flattened_preset = array();
  3800 				$flattened_preset = array();
  2156 				foreach ( $items as $slug => $value ) {
  3801 				foreach ( $items as $slug => $value ) {
  2157 					$flattened_preset[] = array_merge( array( 'slug' => $slug ), $value );
  3802 					$flattened_preset[] = array_merge( array( 'slug' => (string) $slug ), $value );
  2158 				}
  3803 				}
  2159 				_wp_array_set( $output, $path, $flattened_preset );
  3804 				_wp_array_set( $output, $path, $flattened_preset );
  2160 			}
  3805 			}
  2161 		}
  3806 		}
  2162 
  3807 
  2163 		// If all of the static::APPEARANCE_TOOLS_OPT_INS are true,
  3808 		/*
  2164 		// this code unsets them and sets 'appearanceTools' instead.
  3809 		 * If all of the static::APPEARANCE_TOOLS_OPT_INS are true,
       
  3810 		 * this code unsets them and sets 'appearanceTools' instead.
       
  3811 		 */
  2165 		foreach ( $nodes as $node ) {
  3812 		foreach ( $nodes as $node ) {
  2166 			$all_opt_ins_are_set = true;
  3813 			$all_opt_ins_are_set = true;
  2167 			foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
  3814 			foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
  2168 				$full_path = array_merge( $node['path'], $opt_in_path );
  3815 				$full_path = $node['path'];
  2169 				// Use "unset prop" as a marker instead of "null" because
  3816 				foreach ( $opt_in_path as $opt_in_path_item ) {
  2170 				// "null" can be a valid value for some props (e.g. blockGap).
  3817 					$full_path[] = $opt_in_path_item;
       
  3818 				}
       
  3819 				/*
       
  3820 				 * Use "unset prop" as a marker instead of "null" because
       
  3821 				 * "null" can be a valid value for some props (e.g. blockGap).
       
  3822 				 */
  2171 				$opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
  3823 				$opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
  2172 				if ( 'unset prop' === $opt_in_value ) {
  3824 				if ( 'unset prop' === $opt_in_value ) {
  2173 					$all_opt_ins_are_set = false;
  3825 					$all_opt_ins_are_set = false;
  2174 					break;
  3826 					break;
  2175 				}
  3827 				}
  2176 			}
  3828 			}
  2177 
  3829 
  2178 			if ( $all_opt_ins_are_set ) {
  3830 			if ( $all_opt_ins_are_set ) {
  2179 				_wp_array_set( $output, array_merge( $node['path'], array( 'appearanceTools' ) ), true );
  3831 				$node_path_with_appearance_tools   = $node['path'];
       
  3832 				$node_path_with_appearance_tools[] = 'appearanceTools';
       
  3833 				_wp_array_set( $output, $node_path_with_appearance_tools, true );
  2180 				foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
  3834 				foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
  2181 					$full_path = array_merge( $node['path'], $opt_in_path );
  3835 					$full_path = $node['path'];
  2182 					// Use "unset prop" as a marker instead of "null" because
  3836 					foreach ( $opt_in_path as $opt_in_path_item ) {
  2183 					// "null" can be a valid value for some props (e.g. blockGap).
  3837 						$full_path[] = $opt_in_path_item;
       
  3838 					}
       
  3839 					/*
       
  3840 					 * Use "unset prop" as a marker instead of "null" because
       
  3841 					 * "null" can be a valid value for some props (e.g. blockGap).
       
  3842 					 */
  2184 					$opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
  3843 					$opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
  2185 					if ( true !== $opt_in_value ) {
  3844 					if ( true !== $opt_in_value ) {
  2186 						continue;
  3845 						continue;
  2187 					}
  3846 					}
  2188 
  3847 
  2189 					// The following could be improved to be path independent.
  3848 					/*
  2190 					// At the moment it relies on a couple of assumptions:
  3849 					 * The following could be improved to be path independent.
  2191 					//
  3850 					 * At the moment it relies on a couple of assumptions:
  2192 					// - all opt-ins having a path of size 2.
  3851 					 *
  2193 					// - there's two sources of settings: the top-level and the block-level.
  3852 					 * - all opt-ins having a path of size 2.
       
  3853 					 * - there's two sources of settings: the top-level and the block-level.
       
  3854 					 */
  2194 					if (
  3855 					if (
  2195 						( 1 === count( $node['path'] ) ) &&
  3856 						( 1 === count( $node['path'] ) ) &&
  2196 						( 'settings' === $node['path'][0] )
  3857 						( 'settings' === $node['path'][0] )
  2197 					) {
  3858 					) {
  2198 						// Top-level settings.
  3859 						// Top-level settings.
  2219 		wp_recursive_ksort( $output );
  3880 		wp_recursive_ksort( $output );
  2220 
  3881 
  2221 		return $output;
  3882 		return $output;
  2222 	}
  3883 	}
  2223 
  3884 
       
  3885 	/**
       
  3886 	 * Sets the spacingSizes array based on the spacingScale values from theme.json.
       
  3887 	 *
       
  3888 	 * @since 6.1.0
       
  3889 	 * @deprecated 6.6.0 No longer used as the spacingSizes are automatically
       
  3890 	 *                   generated in the constructor and merge methods instead
       
  3891 	 *                   of manually after instantiation.
       
  3892 	 *
       
  3893 	 * @return null|void
       
  3894 	 */
       
  3895 	public function set_spacing_sizes() {
       
  3896 		_deprecated_function( __METHOD__, '6.6.0' );
       
  3897 
       
  3898 		$spacing_scale = isset( $this->theme_json['settings']['spacing']['spacingScale'] )
       
  3899 			? $this->theme_json['settings']['spacing']['spacingScale']
       
  3900 			: array();
       
  3901 
       
  3902 		if ( ! isset( $spacing_scale['steps'] )
       
  3903 			|| ! is_numeric( $spacing_scale['steps'] )
       
  3904 			|| ! isset( $spacing_scale['mediumStep'] )
       
  3905 			|| ! isset( $spacing_scale['unit'] )
       
  3906 			|| ! isset( $spacing_scale['operator'] )
       
  3907 			|| ! isset( $spacing_scale['increment'] )
       
  3908 			|| ! isset( $spacing_scale['steps'] )
       
  3909 			|| ! is_numeric( $spacing_scale['increment'] )
       
  3910 			|| ! is_numeric( $spacing_scale['mediumStep'] )
       
  3911 			|| ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ) {
       
  3912 			if ( ! empty( $spacing_scale ) ) {
       
  3913 				wp_trigger_error(
       
  3914 					__METHOD__,
       
  3915 					sprintf(
       
  3916 						/* translators: 1: theme.json, 2: settings.spacing.spacingScale */
       
  3917 						__( 'Some of the %1$s %2$s values are invalid' ),
       
  3918 						'theme.json',
       
  3919 						'settings.spacing.spacingScale'
       
  3920 					),
       
  3921 					E_USER_NOTICE
       
  3922 				);
       
  3923 			}
       
  3924 			return null;
       
  3925 		}
       
  3926 
       
  3927 		// If theme authors want to prevent the generation of the core spacing scale they can set their theme.json spacingScale.steps to 0.
       
  3928 		if ( 0 === $spacing_scale['steps'] ) {
       
  3929 			return null;
       
  3930 		}
       
  3931 
       
  3932 		$spacing_sizes = static::compute_spacing_sizes( $spacing_scale );
       
  3933 
       
  3934 		// If there are 7 or fewer steps in the scale revert to numbers for labels instead of t-shirt sizes.
       
  3935 		if ( $spacing_scale['steps'] <= 7 ) {
       
  3936 			for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) {
       
  3937 				$spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 );
       
  3938 			}
       
  3939 		}
       
  3940 
       
  3941 		_wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes );
       
  3942 	}
       
  3943 
       
  3944 	/**
       
  3945 	 * Merges two sets of spacing size presets.
       
  3946 	 *
       
  3947 	 * @since 6.6.0
       
  3948 	 *
       
  3949 	 * @param array $base     The base set of spacing sizes.
       
  3950 	 * @param array $incoming The set of spacing sizes to merge with the base. Duplicate slugs will override the base values.
       
  3951 	 * @return array The merged set of spacing sizes.
       
  3952 	 */
       
  3953 	private static function merge_spacing_sizes( $base, $incoming ) {
       
  3954 		// Preserve the order if there are no base (spacingScale) values.
       
  3955 		if ( empty( $base ) ) {
       
  3956 			return $incoming;
       
  3957 		}
       
  3958 		$merged = array();
       
  3959 		foreach ( $base as $item ) {
       
  3960 			$merged[ $item['slug'] ] = $item;
       
  3961 		}
       
  3962 		foreach ( $incoming as $item ) {
       
  3963 			$merged[ $item['slug'] ] = $item;
       
  3964 		}
       
  3965 		ksort( $merged, SORT_NUMERIC );
       
  3966 		return array_values( $merged );
       
  3967 	}
       
  3968 
       
  3969 	/**
       
  3970 	 * Generates a set of spacing sizes by starting with a medium size and
       
  3971 	 * applying an operator with an increment value to generate the rest of the
       
  3972 	 * sizes outward from the medium size. The medium slug is '50' with the rest
       
  3973 	 * of the slugs being 10 apart. The generated names use t-shirt sizing.
       
  3974 	 *
       
  3975 	 * Example:
       
  3976 	 *
       
  3977 	 *     $spacing_scale = array(
       
  3978 	 *         'steps'      => 4,
       
  3979 	 *         'mediumStep' => 16,
       
  3980 	 *         'unit'       => 'px',
       
  3981 	 *         'operator'   => '+',
       
  3982 	 *         'increment'  => 2,
       
  3983 	 *     );
       
  3984 	 *     $spacing_sizes = static::compute_spacing_sizes( $spacing_scale );
       
  3985 	 *     // -> array(
       
  3986 	 *     //        array( 'name' => 'Small',   'slug' => '40', 'size' => '14px' ),
       
  3987 	 *     //        array( 'name' => 'Medium',  'slug' => '50', 'size' => '16px' ),
       
  3988 	 *     //        array( 'name' => 'Large',   'slug' => '60', 'size' => '18px' ),
       
  3989 	 *     //        array( 'name' => 'X-Large', 'slug' => '70', 'size' => '20px' ),
       
  3990 	 *     //    )
       
  3991 	 *
       
  3992 	 * @since 6.6.0
       
  3993 	 *
       
  3994 	 * @param array $spacing_scale {
       
  3995 	 *      The spacing scale values. All are required.
       
  3996 	 *
       
  3997 	 *      @type int    $steps      The number of steps in the scale. (up to 10 steps are supported.)
       
  3998 	 *      @type float  $mediumStep The middle value that gets the slug '50'. (For even number of steps, this becomes the first middle value.)
       
  3999 	 *      @type string $unit       The CSS unit to use for the sizes.
       
  4000 	 *      @type string $operator   The mathematical operator to apply to generate the other sizes. Either '+' or '*'.
       
  4001 	 *      @type float  $increment  The value used with the operator to generate the other sizes.
       
  4002 	 * }
       
  4003 	 * @return array The spacing sizes presets or an empty array if some spacing scale values are missing or invalid.
       
  4004 	 */
       
  4005 	private static function compute_spacing_sizes( $spacing_scale ) {
       
  4006 		/*
       
  4007 		 * This condition is intentionally missing some checks on ranges for the values in order to
       
  4008 		 * keep backwards compatibility with the previous implementation.
       
  4009 		 */
       
  4010 		if (
       
  4011 			! isset( $spacing_scale['steps'] ) ||
       
  4012 			! is_numeric( $spacing_scale['steps'] ) ||
       
  4013 			0 === $spacing_scale['steps'] ||
       
  4014 			! isset( $spacing_scale['mediumStep'] ) ||
       
  4015 			! is_numeric( $spacing_scale['mediumStep'] ) ||
       
  4016 			! isset( $spacing_scale['unit'] ) ||
       
  4017 			! isset( $spacing_scale['operator'] ) ||
       
  4018 			( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ||
       
  4019 			! isset( $spacing_scale['increment'] ) ||
       
  4020 			! is_numeric( $spacing_scale['increment'] )
       
  4021 		) {
       
  4022 			return array();
       
  4023 		}
       
  4024 
       
  4025 		$unit            = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] );
       
  4026 		$current_step    = $spacing_scale['mediumStep'];
       
  4027 		$steps_mid_point = round( $spacing_scale['steps'] / 2, 0 );
       
  4028 		$x_small_count   = null;
       
  4029 		$below_sizes     = array();
       
  4030 		$slug            = 40;
       
  4031 		$remainder       = 0;
       
  4032 
       
  4033 		for ( $below_midpoint_count = $steps_mid_point - 1; $spacing_scale['steps'] > 1 && $slug > 0 && $below_midpoint_count > 0; $below_midpoint_count-- ) {
       
  4034 			if ( '+' === $spacing_scale['operator'] ) {
       
  4035 				$current_step -= $spacing_scale['increment'];
       
  4036 			} elseif ( $spacing_scale['increment'] > 1 ) {
       
  4037 				$current_step /= $spacing_scale['increment'];
       
  4038 			} else {
       
  4039 				$current_step *= $spacing_scale['increment'];
       
  4040 			}
       
  4041 
       
  4042 			if ( $current_step <= 0 ) {
       
  4043 				$remainder = $below_midpoint_count;
       
  4044 				break;
       
  4045 			}
       
  4046 
       
  4047 			$below_sizes[] = array(
       
  4048 				/* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Small. */
       
  4049 				'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small' ) : sprintf( __( '%sX-Small' ), (string) $x_small_count ),
       
  4050 				'slug' => (string) $slug,
       
  4051 				'size' => round( $current_step, 2 ) . $unit,
       
  4052 			);
       
  4053 
       
  4054 			if ( $below_midpoint_count === $steps_mid_point - 2 ) {
       
  4055 				$x_small_count = 2;
       
  4056 			}
       
  4057 
       
  4058 			if ( $below_midpoint_count < $steps_mid_point - 2 ) {
       
  4059 				++$x_small_count;
       
  4060 			}
       
  4061 
       
  4062 			$slug -= 10;
       
  4063 		}
       
  4064 
       
  4065 		$below_sizes = array_reverse( $below_sizes );
       
  4066 
       
  4067 		$below_sizes[] = array(
       
  4068 			'name' => __( 'Medium' ),
       
  4069 			'slug' => '50',
       
  4070 			'size' => $spacing_scale['mediumStep'] . $unit,
       
  4071 		);
       
  4072 
       
  4073 		$current_step  = $spacing_scale['mediumStep'];
       
  4074 		$x_large_count = null;
       
  4075 		$above_sizes   = array();
       
  4076 		$slug          = 60;
       
  4077 		$steps_above   = ( $spacing_scale['steps'] - $steps_mid_point ) + $remainder;
       
  4078 
       
  4079 		for ( $above_midpoint_count = 0; $above_midpoint_count < $steps_above; $above_midpoint_count++ ) {
       
  4080 			$current_step = '+' === $spacing_scale['operator']
       
  4081 				? $current_step + $spacing_scale['increment']
       
  4082 				: ( $spacing_scale['increment'] >= 1 ? $current_step * $spacing_scale['increment'] : $current_step / $spacing_scale['increment'] );
       
  4083 
       
  4084 			$above_sizes[] = array(
       
  4085 				/* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Large. */
       
  4086 				'name' => 0 === $above_midpoint_count ? __( 'Large' ) : sprintf( __( '%sX-Large' ), (string) $x_large_count ),
       
  4087 				'slug' => (string) $slug,
       
  4088 				'size' => round( $current_step, 2 ) . $unit,
       
  4089 			);
       
  4090 
       
  4091 			if ( 1 === $above_midpoint_count ) {
       
  4092 				$x_large_count = 2;
       
  4093 			}
       
  4094 
       
  4095 			if ( $above_midpoint_count > 1 ) {
       
  4096 				++$x_large_count;
       
  4097 			}
       
  4098 
       
  4099 			$slug += 10;
       
  4100 		}
       
  4101 
       
  4102 		$spacing_sizes = $below_sizes;
       
  4103 		foreach ( $above_sizes as $above_sizes_item ) {
       
  4104 			$spacing_sizes[] = $above_sizes_item;
       
  4105 		}
       
  4106 
       
  4107 		return $spacing_sizes;
       
  4108 	}
       
  4109 
       
  4110 	/**
       
  4111 	 * This is used to convert the internal representation of variables to the CSS representation.
       
  4112 	 * For example, `var:preset|color|vivid-green-cyan` becomes `var(--wp--preset--color--vivid-green-cyan)`.
       
  4113 	 *
       
  4114 	 * @since 6.3.0
       
  4115 	 * @param string $value The variable such as var:preset|color|vivid-green-cyan to convert.
       
  4116 	 * @return string The converted variable.
       
  4117 	 */
       
  4118 	private static function convert_custom_properties( $value ) {
       
  4119 		$prefix     = 'var:';
       
  4120 		$prefix_len = strlen( $prefix );
       
  4121 		$token_in   = '|';
       
  4122 		$token_out  = '--';
       
  4123 		if ( str_starts_with( $value, $prefix ) ) {
       
  4124 			$unwrapped_name = str_replace(
       
  4125 				$token_in,
       
  4126 				$token_out,
       
  4127 				substr( $value, $prefix_len )
       
  4128 			);
       
  4129 			$value          = "var(--wp--$unwrapped_name)";
       
  4130 		}
       
  4131 
       
  4132 		return $value;
       
  4133 	}
       
  4134 
       
  4135 	/**
       
  4136 	 * Given a tree, converts the internal representation of variables to the CSS representation.
       
  4137 	 * It is recursive and modifies the input in-place.
       
  4138 	 *
       
  4139 	 * @since 6.3.0
       
  4140 	 * @param array $tree   Input to process.
       
  4141 	 * @return array The modified $tree.
       
  4142 	 */
       
  4143 	private static function resolve_custom_css_format( $tree ) {
       
  4144 		$prefix = 'var:';
       
  4145 
       
  4146 		foreach ( $tree as $key => $data ) {
       
  4147 			if ( is_string( $data ) && str_starts_with( $data, $prefix ) ) {
       
  4148 				$tree[ $key ] = self::convert_custom_properties( $data );
       
  4149 			} elseif ( is_array( $data ) ) {
       
  4150 				$tree[ $key ] = self::resolve_custom_css_format( $data );
       
  4151 			}
       
  4152 		}
       
  4153 
       
  4154 		return $tree;
       
  4155 	}
       
  4156 
       
  4157 	/**
       
  4158 	 * Returns the selectors metadata for a block.
       
  4159 	 *
       
  4160 	 * @since 6.3.0
       
  4161 	 *
       
  4162 	 * @param object $block_type    The block type.
       
  4163 	 * @param string $root_selector The block's root selector.
       
  4164 	 *
       
  4165 	 * @return array The custom selectors set by the block.
       
  4166 	 */
       
  4167 	protected static function get_block_selectors( $block_type, $root_selector ) {
       
  4168 		if ( ! empty( $block_type->selectors ) ) {
       
  4169 			return $block_type->selectors;
       
  4170 		}
       
  4171 
       
  4172 		$selectors = array( 'root' => $root_selector );
       
  4173 		foreach ( static::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS as $key => $feature ) {
       
  4174 			$feature_selector = wp_get_block_css_selector( $block_type, $key );
       
  4175 			if ( null !== $feature_selector ) {
       
  4176 				$selectors[ $feature ] = array( 'root' => $feature_selector );
       
  4177 			}
       
  4178 		}
       
  4179 
       
  4180 		return $selectors;
       
  4181 	}
       
  4182 
       
  4183 	/**
       
  4184 	 * Generates all the element selectors for a block.
       
  4185 	 *
       
  4186 	 * @since 6.3.0
       
  4187 	 *
       
  4188 	 * @param string $root_selector The block's root CSS selector.
       
  4189 	 * @return array The block's element selectors.
       
  4190 	 */
       
  4191 	protected static function get_block_element_selectors( $root_selector ) {
       
  4192 		/*
       
  4193 		 * Assign defaults, then override those that the block sets by itself.
       
  4194 		 * If the block selector is compounded, will append the element to each
       
  4195 		 * individual block selector.
       
  4196 		 */
       
  4197 		$block_selectors   = explode( ',', $root_selector );
       
  4198 		$element_selectors = array();
       
  4199 		foreach ( static::ELEMENTS as $el_name => $el_selector ) {
       
  4200 			$element_selector = array();
       
  4201 			foreach ( $block_selectors as $selector ) {
       
  4202 				if ( $selector === $el_selector ) {
       
  4203 					$element_selector = array( $el_selector );
       
  4204 					break;
       
  4205 				}
       
  4206 				$element_selector[] = static::prepend_to_selector( $el_selector, $selector . ' ' );
       
  4207 			}
       
  4208 			$element_selectors[ $el_name ] = implode( ',', $element_selector );
       
  4209 		}
       
  4210 
       
  4211 		return $element_selectors;
       
  4212 	}
       
  4213 
       
  4214 	/**
       
  4215 	 * Generates style declarations for a node's features e.g., color, border,
       
  4216 	 * typography etc. that have custom selectors in their related block's
       
  4217 	 * metadata.
       
  4218 	 *
       
  4219 	 * @since 6.3.0
       
  4220 	 *
       
  4221 	 * @param object $metadata The related block metadata containing selectors.
       
  4222 	 * @param object $node     A merged theme.json node for block or variation.
       
  4223 	 *
       
  4224 	 * @return array The style declarations for the node's features with custom
       
  4225 	 * selectors.
       
  4226 	 */
       
  4227 	protected function get_feature_declarations_for_node( $metadata, &$node ) {
       
  4228 		$declarations = array();
       
  4229 
       
  4230 		if ( ! isset( $metadata['selectors'] ) ) {
       
  4231 			return $declarations;
       
  4232 		}
       
  4233 
       
  4234 		$settings = isset( $this->theme_json['settings'] )
       
  4235 			? $this->theme_json['settings']
       
  4236 			: array();
       
  4237 
       
  4238 		foreach ( $metadata['selectors'] as $feature => $feature_selectors ) {
       
  4239 			/*
       
  4240 			 * Skip if this is the block's root selector or the block doesn't
       
  4241 			 * have any styles for the feature.
       
  4242 			 */
       
  4243 			if ( 'root' === $feature || empty( $node[ $feature ] ) ) {
       
  4244 				continue;
       
  4245 			}
       
  4246 
       
  4247 			if ( is_array( $feature_selectors ) ) {
       
  4248 				foreach ( $feature_selectors as $subfeature => $subfeature_selector ) {
       
  4249 					if ( 'root' === $subfeature || empty( $node[ $feature ][ $subfeature ] ) ) {
       
  4250 						continue;
       
  4251 					}
       
  4252 
       
  4253 					/*
       
  4254 					 * Create temporary node containing only the subfeature data
       
  4255 					 * to leverage existing `compute_style_properties` function.
       
  4256 					 */
       
  4257 					$subfeature_node = array(
       
  4258 						$feature => array(
       
  4259 							$subfeature => $node[ $feature ][ $subfeature ],
       
  4260 						),
       
  4261 					);
       
  4262 
       
  4263 					// Generate style declarations.
       
  4264 					$new_declarations = static::compute_style_properties( $subfeature_node, $settings, null, $this->theme_json );
       
  4265 
       
  4266 					// Merge subfeature declarations into feature declarations.
       
  4267 					if ( isset( $declarations[ $subfeature_selector ] ) ) {
       
  4268 						foreach ( $new_declarations as $new_declaration ) {
       
  4269 							$declarations[ $subfeature_selector ][] = $new_declaration;
       
  4270 						}
       
  4271 					} else {
       
  4272 						$declarations[ $subfeature_selector ] = $new_declarations;
       
  4273 					}
       
  4274 
       
  4275 					/*
       
  4276 					 * Remove the subfeature from the block's node now its
       
  4277 					 * styles will be included under its own selector not the
       
  4278 					 * block's.
       
  4279 					 */
       
  4280 					unset( $node[ $feature ][ $subfeature ] );
       
  4281 				}
       
  4282 			}
       
  4283 
       
  4284 			/*
       
  4285 			 * Now subfeatures have been processed and removed we can process
       
  4286 			 * feature root selector or simple string selector.
       
  4287 			 */
       
  4288 			if (
       
  4289 				is_string( $feature_selectors ) ||
       
  4290 				( isset( $feature_selectors['root'] ) && $feature_selectors['root'] )
       
  4291 			) {
       
  4292 				$feature_selector = is_string( $feature_selectors ) ? $feature_selectors : $feature_selectors['root'];
       
  4293 
       
  4294 				/*
       
  4295 				 * Create temporary node containing only the feature data
       
  4296 				 * to leverage existing `compute_style_properties` function.
       
  4297 				 */
       
  4298 				$feature_node = array( $feature => $node[ $feature ] );
       
  4299 
       
  4300 				// Generate the style declarations.
       
  4301 				$new_declarations = static::compute_style_properties( $feature_node, $settings, null, $this->theme_json );
       
  4302 
       
  4303 				/*
       
  4304 				 * Merge new declarations with any that already exist for
       
  4305 				 * the feature selector. This may occur when multiple block
       
  4306 				 * support features use the same custom selector.
       
  4307 				 */
       
  4308 				if ( isset( $declarations[ $feature_selector ] ) ) {
       
  4309 					foreach ( $new_declarations as $new_declaration ) {
       
  4310 						$declarations[ $feature_selector ][] = $new_declaration;
       
  4311 					}
       
  4312 				} else {
       
  4313 					$declarations[ $feature_selector ] = $new_declarations;
       
  4314 				}
       
  4315 
       
  4316 				/*
       
  4317 				 * Remove the feature from the block's node now its styles
       
  4318 				 * will be included under its own selector not the block's.
       
  4319 				 */
       
  4320 				unset( $node[ $feature ] );
       
  4321 			}
       
  4322 		}
       
  4323 
       
  4324 		return $declarations;
       
  4325 	}
       
  4326 
       
  4327 	/**
       
  4328 	 * Replaces CSS variables with their values in place.
       
  4329 	 *
       
  4330 	 * @since 6.3.0
       
  4331 	 * @since 6.5.0 Check for empty style before processing its value.
       
  4332 	 *
       
  4333 	 * @param array $styles CSS declarations to convert.
       
  4334 	 * @param array $values key => value pairs to use for replacement.
       
  4335 	 * @return array
       
  4336 	 */
       
  4337 	private static function convert_variables_to_value( $styles, $values ) {
       
  4338 		foreach ( $styles as $key => $style ) {
       
  4339 			if ( empty( $style ) ) {
       
  4340 				continue;
       
  4341 			}
       
  4342 
       
  4343 			if ( is_array( $style ) ) {
       
  4344 				$styles[ $key ] = self::convert_variables_to_value( $style, $values );
       
  4345 				continue;
       
  4346 			}
       
  4347 
       
  4348 			if ( 0 <= strpos( $style, 'var(' ) ) {
       
  4349 				// find all the variables in the string in the form of var(--variable-name, fallback), with fallback in the second capture group.
       
  4350 
       
  4351 				$has_matches = preg_match_all( '/var\(([^),]+)?,?\s?(\S+)?\)/', $style, $var_parts );
       
  4352 
       
  4353 				if ( $has_matches ) {
       
  4354 					$resolved_style = $styles[ $key ];
       
  4355 					foreach ( $var_parts[1] as $index => $var_part ) {
       
  4356 						$key_in_values   = 'var(' . $var_part . ')';
       
  4357 						$rule_to_replace = $var_parts[0][ $index ]; // the css rule to replace e.g. var(--wp--preset--color--vivid-green-cyan).
       
  4358 						$fallback        = $var_parts[2][ $index ]; // the fallback value.
       
  4359 						$resolved_style  = str_replace(
       
  4360 							array(
       
  4361 								$rule_to_replace,
       
  4362 								$fallback,
       
  4363 							),
       
  4364 							array(
       
  4365 								isset( $values[ $key_in_values ] ) ? $values[ $key_in_values ] : $rule_to_replace,
       
  4366 								isset( $values[ $fallback ] ) ? $values[ $fallback ] : $fallback,
       
  4367 							),
       
  4368 							$resolved_style
       
  4369 						);
       
  4370 					}
       
  4371 					$styles[ $key ] = $resolved_style;
       
  4372 				}
       
  4373 			}
       
  4374 		}
       
  4375 
       
  4376 		return $styles;
       
  4377 	}
       
  4378 
       
  4379 	/**
       
  4380 	 * Resolves the values of CSS variables in the given styles.
       
  4381 	 *
       
  4382 	 * @since 6.3.0
       
  4383 	 * @param WP_Theme_JSON $theme_json The theme json resolver.
       
  4384 	 *
       
  4385 	 * @return WP_Theme_JSON The $theme_json with resolved variables.
       
  4386 	 */
       
  4387 	public static function resolve_variables( $theme_json ) {
       
  4388 		$settings    = $theme_json->get_settings();
       
  4389 		$styles      = $theme_json->get_raw_data()['styles'];
       
  4390 		$preset_vars = static::compute_preset_vars( $settings, static::VALID_ORIGINS );
       
  4391 		$theme_vars  = static::compute_theme_vars( $settings );
       
  4392 		$vars        = array_reduce(
       
  4393 			array_merge( $preset_vars, $theme_vars ),
       
  4394 			function ( $carry, $item ) {
       
  4395 				$name                    = $item['name'];
       
  4396 				$carry[ "var({$name})" ] = $item['value'];
       
  4397 				return $carry;
       
  4398 			},
       
  4399 			array()
       
  4400 		);
       
  4401 
       
  4402 		$theme_json->theme_json['styles'] = self::convert_variables_to_value( $styles, $vars );
       
  4403 		return $theme_json;
       
  4404 	}
       
  4405 
       
  4406 	/**
       
  4407 	 * Generates a selector for a block style variation.
       
  4408 	 *
       
  4409 	 * @since 6.5.0
       
  4410 	 *
       
  4411 	 * @param string $variation_name Name of the block style variation.
       
  4412 	 * @param string $block_selector CSS selector for the block.
       
  4413 	 * @return string Block selector with block style variation selector added to it.
       
  4414 	 */
       
  4415 	protected static function get_block_style_variation_selector( $variation_name, $block_selector ) {
       
  4416 		$variation_class = ".is-style-$variation_name";
       
  4417 
       
  4418 		if ( ! $block_selector ) {
       
  4419 			return $variation_class;
       
  4420 		}
       
  4421 
       
  4422 		$limit          = 1;
       
  4423 		$selector_parts = explode( ',', $block_selector );
       
  4424 		$result         = array();
       
  4425 
       
  4426 		foreach ( $selector_parts as $part ) {
       
  4427 			$result[] = preg_replace_callback(
       
  4428 				'/((?::\([^)]+\))?\s*)([^\s:]+)/',
       
  4429 				function ( $matches ) use ( $variation_class ) {
       
  4430 					return $matches[1] . $matches[2] . $variation_class;
       
  4431 				},
       
  4432 				$part,
       
  4433 				$limit
       
  4434 			);
       
  4435 		}
       
  4436 
       
  4437 		return implode( ',', $result );
       
  4438 	}
       
  4439 
       
  4440 	/**
       
  4441 	 * Collects valid block style variations keyed by block type.
       
  4442 	 *
       
  4443 	 * @since 6.6.0
       
  4444 	 *
       
  4445 	 * @return array Valid block style variations by block type.
       
  4446 	 */
       
  4447 	protected static function get_valid_block_style_variations() {
       
  4448 		$valid_variations = array();
       
  4449 		foreach ( self::get_blocks_metadata() as $block_name => $block_meta ) {
       
  4450 			if ( ! isset( $block_meta['styleVariations'] ) ) {
       
  4451 				continue;
       
  4452 			}
       
  4453 			$valid_variations[ $block_name ] = array_keys( $block_meta['styleVariations'] );
       
  4454 		}
       
  4455 
       
  4456 		return $valid_variations;
       
  4457 	}
  2224 }
  4458 }