wp/wp-includes/class-wp-theme-json.php
changeset 19 3d72ae0968f4
parent 18 be944660c56a
child 21 48c4eec2b7e6
equal deleted inserted replaced
18:be944660c56a 19:3d72ae0968f4
     8  */
     8  */
     9 
     9 
    10 /**
    10 /**
    11  * Class that encapsulates the processing of structures that adhere to the theme.json spec.
    11  * Class that encapsulates the processing of structures that adhere to the theme.json spec.
    12  *
    12  *
       
    13  * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes).
       
    14  * This is a low-level API that may need to do breaking changes. Please,
       
    15  * use get_global_settings, get_global_styles, and get_global_stylesheet instead.
       
    16  *
    13  * @access private
    17  * @access private
    14  */
    18  */
    15 class WP_Theme_JSON {
    19 class WP_Theme_JSON {
    16 
    20 
    17 	/**
    21 	/**
    18 	 * Container of data in theme.json format.
    22 	 * Container of data in theme.json format.
    19 	 *
    23 	 *
    20 	 * @since 5.8.0
    24 	 * @since 5.8.0
    21 	 * @var array
    25 	 * @var array
    22 	 */
    26 	 */
    23 	private $theme_json = null;
    27 	protected $theme_json = null;
    24 
    28 
    25 	/**
    29 	/**
    26 	 * Holds block metadata extracted from block.json
    30 	 * Holds block metadata extracted from block.json
    27 	 * to be shared among all instances so we don't
    31 	 * to be shared among all instances so we don't
    28 	 * process it twice.
    32 	 * process it twice.
    29 	 *
    33 	 *
    30 	 * @since 5.8.0
    34 	 * @since 5.8.0
    31 	 * @var array
    35 	 * @var array
    32 	 */
    36 	 */
    33 	private static $blocks_metadata = null;
    37 	protected static $blocks_metadata = null;
    34 
    38 
    35 	/**
    39 	/**
    36 	 * The CSS selector for the top-level styles.
    40 	 * The CSS selector for the top-level styles.
    37 	 *
    41 	 *
    38 	 * @since 5.8.0
    42 	 * @since 5.8.0
    42 
    46 
    43 	/**
    47 	/**
    44 	 * The sources of data this object can represent.
    48 	 * The sources of data this object can represent.
    45 	 *
    49 	 *
    46 	 * @since 5.8.0
    50 	 * @since 5.8.0
    47 	 * @var array
    51 	 * @var string[]
    48 	 */
    52 	 */
    49 	const VALID_ORIGINS = array(
    53 	const VALID_ORIGINS = array(
    50 		'core',
    54 		'default',
    51 		'theme',
    55 		'theme',
    52 		'user',
    56 		'custom',
    53 	);
    57 	);
    54 
    58 
    55 	/**
    59 	/**
    56 	 * Presets are a set of values that serve
    60 	 * Presets are a set of values that serve
    57 	 * to bootstrap some styles: colors, font sizes, etc.
    61 	 * to bootstrap some styles: colors, font sizes, etc.
    68 	 * )
    72 	 * )
    69 	 * ```
    73 	 * ```
    70 	 *
    74 	 *
    71 	 * This contains the necessary metadata to process them:
    75 	 * This contains the necessary metadata to process them:
    72 	 *
    76 	 *
    73 	 * - path          => where to find the preset within the settings section
    77 	 * - path             => Where to find the preset within the settings section.
    74 	 *
    78 	 * - prevent_override => Disables override of default presets by theme presets.
    75 	 * - value_key     => the key that represents the value
    79 	 *                       The relationship between whether to override the defaults
    76 	 *
    80 	 *                       and whether the defaults are enabled is inverse:
    77 	 * - css_var_infix => infix to use in generating the CSS Custom Property. Example:
    81 	 *                         - If defaults are enabled  => theme presets should not be overriden
    78 	 *                   --wp--preset--<preset_infix>--<slug>: <preset_value>
    82 	 *                         - If defaults are disabled => theme presets should be overriden
    79 	 *
    83 	 *                       For example, a theme sets defaultPalette to false,
    80 	 * - classes      => array containing a structure with the classes to
    84 	 *                       making the default palette hidden from the user.
    81 	 *                   generate for the presets. Each class should have
    85 	 *                       In that case, we want all the theme presets to be present,
    82 	 *                   the class suffix and the property name. Example:
    86 	 *                       so they should override the defaults by setting this false.
    83 	 *
    87 	 * - use_default_names => whether to use the default names
    84 	 *                   .has-<slug>-<class_suffix> {
    88 	 * - value_key        => the key that represents the value
    85 	 *                       <property_name>: <preset_value>
    89 	 * - value_func       => optionally, instead of value_key, a function to generate
    86 	 *                   }
    90 	 *                       the value that takes a preset as an argument
    87 	 *
    91 	 *                       (either value_key or value_func should be present)
    88 	 * @since 5.8.0
    92 	 * - css_vars         => template string to use in generating the CSS Custom Property.
       
    93 	 *                       Example output: "--wp--preset--duotone--blue: <value>" will generate as many CSS Custom Properties as presets defined
       
    94 	 *                       substituting the $slug for the slug's value for each preset value.
       
    95 	 * - classes          => array containing a structure with the classes to
       
    96 	 *                       generate for the presets, where for each array item
       
    97 	 *                       the key is the class name and the value the property name.
       
    98 	 *                       The "$slug" substring will be replaced by the slug of each preset.
       
    99 	 *                       For example:
       
   100 	 *                       'classes' => array(
       
   101 	 *                         '.has-$slug-color'            => 'color',
       
   102 	 *                         '.has-$slug-background-color' => 'background-color',
       
   103 	 *                         '.has-$slug-border-color'     => 'border-color',
       
   104 	 *                       )
       
   105 	 * - properties       => array of CSS properties to be used by kses to
       
   106 	 *                       validate the content of each preset
       
   107 	 *                       by means of the remove_insecure_properties method.
       
   108 	 *
       
   109 	 * @since 5.8.0
       
   110 	 * @since 5.9.0 Added the `color.duotone` and `typography.fontFamilies` presets,
       
   111 	 *              `use_default_names` preset key, and simplified the metadata structure.
       
   112 	 * @since 6.0.0 Replaced `override` with `prevent_override` and updated the
       
   113 	 *              `prevent_overried` value for `color.duotone` to use `color.defaultDuotone`.
    89 	 * @var array
   114 	 * @var array
    90 	 */
   115 	 */
    91 	const PRESETS_METADATA = array(
   116 	const PRESETS_METADATA = array(
    92 		array(
   117 		array(
    93 			'path'          => array( 'color', 'palette' ),
   118 			'path'              => array( 'color', 'palette' ),
    94 			'value_key'     => 'color',
   119 			'prevent_override'  => array( 'color', 'defaultPalette' ),
    95 			'css_var_infix' => 'color',
   120 			'use_default_names' => false,
    96 			'classes'       => array(
   121 			'value_key'         => 'color',
    97 				array(
   122 			'css_vars'          => '--wp--preset--color--$slug',
    98 					'class_suffix'  => 'color',
   123 			'classes'           => array(
    99 					'property_name' => 'color',
   124 				'.has-$slug-color'            => 'color',
   100 				),
   125 				'.has-$slug-background-color' => 'background-color',
   101 				array(
   126 				'.has-$slug-border-color'     => 'border-color',
   102 					'class_suffix'  => 'background-color',
       
   103 					'property_name' => 'background-color',
       
   104 				),
       
   105 			),
   127 			),
       
   128 			'properties'        => array( 'color', 'background-color', 'border-color' ),
   106 		),
   129 		),
   107 		array(
   130 		array(
   108 			'path'          => array( 'color', 'gradients' ),
   131 			'path'              => array( 'color', 'gradients' ),
   109 			'value_key'     => 'gradient',
   132 			'prevent_override'  => array( 'color', 'defaultGradients' ),
   110 			'css_var_infix' => 'gradient',
   133 			'use_default_names' => false,
   111 			'classes'       => array(
   134 			'value_key'         => 'gradient',
   112 				array(
   135 			'css_vars'          => '--wp--preset--gradient--$slug',
   113 					'class_suffix'  => 'gradient-background',
   136 			'classes'           => array( '.has-$slug-gradient-background' => 'background' ),
   114 					'property_name' => 'background',
   137 			'properties'        => array( 'background' ),
   115 				),
       
   116 			),
       
   117 		),
   138 		),
   118 		array(
   139 		array(
   119 			'path'          => array( 'typography', 'fontSizes' ),
   140 			'path'              => array( 'color', 'duotone' ),
   120 			'value_key'     => 'size',
   141 			'prevent_override'  => array( 'color', 'defaultDuotone' ),
   121 			'css_var_infix' => 'font-size',
   142 			'use_default_names' => false,
   122 			'classes'       => array(
   143 			'value_func'        => 'wp_get_duotone_filter_property',
   123 				array(
   144 			'css_vars'          => '--wp--preset--duotone--$slug',
   124 					'class_suffix'  => 'font-size',
   145 			'classes'           => array(),
   125 					'property_name' => 'font-size',
   146 			'properties'        => array( 'filter' ),
   126 				),
   147 		),
   127 			),
   148 		array(
       
   149 			'path'              => array( 'typography', 'fontSizes' ),
       
   150 			'prevent_override'  => false,
       
   151 			'use_default_names' => true,
       
   152 			'value_key'         => 'size',
       
   153 			'css_vars'          => '--wp--preset--font-size--$slug',
       
   154 			'classes'           => array( '.has-$slug-font-size' => 'font-size' ),
       
   155 			'properties'        => array( 'font-size' ),
       
   156 		),
       
   157 		array(
       
   158 			'path'              => array( 'typography', 'fontFamilies' ),
       
   159 			'prevent_override'  => false,
       
   160 			'use_default_names' => false,
       
   161 			'value_key'         => 'fontFamily',
       
   162 			'css_vars'          => '--wp--preset--font-family--$slug',
       
   163 			'classes'           => array( '.has-$slug-font-family' => 'font-family' ),
       
   164 			'properties'        => array( 'font-family' ),
   128 		),
   165 		),
   129 	);
   166 	);
   130 
   167 
   131 	/**
   168 	/**
   132 	 * Metadata for style properties.
   169 	 * Metadata for style properties.
   133 	 *
   170 	 *
   134 	 * Each property declares:
   171 	 * Each element is a direct mapping from the CSS property name to the
   135 	 *
   172 	 * path to the value in theme.json & block attributes.
   136 	 * - 'value': path to the value in theme.json and block attributes.
   173 	 *
   137 	 *
   174 	 * @since 5.8.0
   138 	 * @since 5.8.0
   175 	 * @since 5.9.0 Added the `border-*`, `font-family`, `font-style`, `font-weight`,
       
   176 	 *              `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`,
       
   177 	 *              `text-decoration`, `text-transform`, and `filter` properties,
       
   178 	 *              simplified the metadata structure.
   139 	 * @var array
   179 	 * @var array
   140 	 */
   180 	 */
   141 	const PROPERTIES_METADATA = array(
   181 	const PROPERTIES_METADATA = array(
   142 		'background'       => array(
   182 		'background'                 => array( 'color', 'gradient' ),
   143 			'value' => array( 'color', 'gradient' ),
   183 		'background-color'           => array( 'color', 'background' ),
   144 		),
   184 		'border-radius'              => array( 'border', 'radius' ),
   145 		'background-color' => array(
   185 		'border-top-left-radius'     => array( 'border', 'radius', 'topLeft' ),
   146 			'value' => array( 'color', 'background' ),
   186 		'border-top-right-radius'    => array( 'border', 'radius', 'topRight' ),
   147 		),
   187 		'border-bottom-left-radius'  => array( 'border', 'radius', 'bottomLeft' ),
   148 		'color'            => array(
   188 		'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ),
   149 			'value' => array( 'color', 'text' ),
   189 		'border-color'               => array( 'border', 'color' ),
   150 		),
   190 		'border-width'               => array( 'border', 'width' ),
   151 		'font-size'        => array(
   191 		'border-style'               => array( 'border', 'style' ),
   152 			'value' => array( 'typography', 'fontSize' ),
   192 		'color'                      => array( 'color', 'text' ),
   153 		),
   193 		'font-family'                => array( 'typography', 'fontFamily' ),
   154 		'line-height'      => array(
   194 		'font-size'                  => array( 'typography', 'fontSize' ),
   155 			'value' => array( 'typography', 'lineHeight' ),
   195 		'font-style'                 => array( 'typography', 'fontStyle' ),
   156 		),
   196 		'font-weight'                => array( 'typography', 'fontWeight' ),
   157 		'margin'           => array(
   197 		'letter-spacing'             => array( 'typography', 'letterSpacing' ),
   158 			'value'      => array( 'spacing', 'margin' ),
   198 		'line-height'                => array( 'typography', 'lineHeight' ),
   159 			'properties' => array( 'top', 'right', 'bottom', 'left' ),
   199 		'margin'                     => array( 'spacing', 'margin' ),
   160 		),
   200 		'margin-top'                 => array( 'spacing', 'margin', 'top' ),
   161 		'padding'          => array(
   201 		'margin-right'               => array( 'spacing', 'margin', 'right' ),
   162 			'value'      => array( 'spacing', 'padding' ),
   202 		'margin-bottom'              => array( 'spacing', 'margin', 'bottom' ),
   163 			'properties' => array( 'top', 'right', 'bottom', 'left' ),
   203 		'margin-left'                => array( 'spacing', 'margin', 'left' ),
   164 		),
   204 		'padding'                    => array( 'spacing', 'padding' ),
       
   205 		'padding-top'                => array( 'spacing', 'padding', 'top' ),
       
   206 		'padding-right'              => array( 'spacing', 'padding', 'right' ),
       
   207 		'padding-bottom'             => array( 'spacing', 'padding', 'bottom' ),
       
   208 		'padding-left'               => array( 'spacing', 'padding', 'left' ),
       
   209 		'--wp--style--block-gap'     => array( 'spacing', 'blockGap' ),
       
   210 		'text-decoration'            => array( 'typography', 'textDecoration' ),
       
   211 		'text-transform'             => array( 'typography', 'textTransform' ),
       
   212 		'filter'                     => array( 'filter', 'duotone' ),
   165 	);
   213 	);
   166 
   214 
   167 	/**
   215 	/**
   168 	 * @since 5.8.0
   216 	 * Protected style properties.
   169 	 * @var array
   217 	 *
   170 	 */
   218 	 * These style properties are only rendered if a setting enables it
   171 	const ALLOWED_TOP_LEVEL_KEYS = array(
   219 	 * via a value other than `null`.
       
   220 	 *
       
   221 	 * Each element maps the style property to the corresponding theme.json
       
   222 	 * setting key.
       
   223 	 *
       
   224 	 * @since 5.9.0
       
   225 	 */
       
   226 	const PROTECTED_PROPERTIES = array(
       
   227 		'spacing.blockGap' => array( 'spacing', 'blockGap' ),
       
   228 	);
       
   229 
       
   230 	/**
       
   231 	 * The top-level keys a theme.json can have.
       
   232 	 *
       
   233 	 * @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`,
       
   235 	 *              added the `customTemplates` and `templateParts` values.
       
   236 	 * @var string[]
       
   237 	 */
       
   238 	const VALID_TOP_LEVEL_KEYS = array(
       
   239 		'customTemplates',
       
   240 		'patterns',
   172 		'settings',
   241 		'settings',
   173 		'styles',
   242 		'styles',
       
   243 		'templateParts',
   174 		'version',
   244 		'version',
       
   245 		'title',
   175 	);
   246 	);
   176 
   247 
   177 	/**
   248 	/**
   178 	 * @since 5.8.0
   249 	 * The valid properties under the settings key.
       
   250 	 *
       
   251 	 * @since 5.8.0 As `ALLOWED_SETTINGS`.
       
   252 	 * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`,
       
   253 	 *              added new properties for `border`, `color`, `spacing`,
       
   254 	 *              and `typography`, and renamed others according to the new schema.
       
   255 	 * @since 6.0.0 Added `color.defaultDuotone`.
   179 	 * @var array
   256 	 * @var array
   180 	 */
   257 	 */
   181 	const ALLOWED_SETTINGS = array(
   258 	const VALID_SETTINGS = array(
   182 		'border'     => array(
   259 		'appearanceTools' => null,
   183 			'customRadius' => null,
   260 		'border'          => array(
       
   261 			'color'  => null,
       
   262 			'radius' => null,
       
   263 			'style'  => null,
       
   264 			'width'  => null,
   184 		),
   265 		),
   185 		'color'      => array(
   266 		'color'           => array(
   186 			'custom'         => null,
   267 			'background'       => null,
   187 			'customDuotone'  => null,
   268 			'custom'           => null,
   188 			'customGradient' => null,
   269 			'customDuotone'    => null,
   189 			'duotone'        => null,
   270 			'customGradient'   => null,
   190 			'gradients'      => null,
   271 			'defaultDuotone'   => null,
   191 			'link'           => null,
   272 			'defaultGradients' => null,
   192 			'palette'        => null,
   273 			'defaultPalette'   => null,
       
   274 			'duotone'          => null,
       
   275 			'gradients'        => null,
       
   276 			'link'             => null,
       
   277 			'palette'          => null,
       
   278 			'text'             => null,
   193 		),
   279 		),
   194 		'custom'     => null,
   280 		'custom'          => null,
   195 		'layout'     => array(
   281 		'layout'          => array(
   196 			'contentSize' => null,
   282 			'contentSize' => null,
   197 			'wideSize'    => null,
   283 			'wideSize'    => null,
   198 		),
   284 		),
   199 		'spacing'    => array(
   285 		'spacing'         => array(
   200 			'customMargin'  => null,
   286 			'blockGap' => null,
   201 			'customPadding' => null,
   287 			'margin'   => null,
   202 			'units'         => null,
   288 			'padding'  => null,
       
   289 			'units'    => null,
   203 		),
   290 		),
   204 		'typography' => array(
   291 		'typography'      => array(
   205 			'customFontSize'   => null,
   292 			'customFontSize' => null,
   206 			'customLineHeight' => null,
   293 			'dropCap'        => null,
   207 			'dropCap'          => null,
   294 			'fontFamilies'   => null,
   208 			'fontSizes'        => null,
   295 			'fontSizes'      => null,
       
   296 			'fontStyle'      => null,
       
   297 			'fontWeight'     => null,
       
   298 			'letterSpacing'  => null,
       
   299 			'lineHeight'     => null,
       
   300 			'textDecoration' => null,
       
   301 			'textTransform'  => null,
   209 		),
   302 		),
   210 	);
   303 	);
   211 
   304 
   212 	/**
   305 	/**
   213 	 * @since 5.8.0
   306 	 * The valid properties under the styles key.
       
   307 	 *
       
   308 	 * @since 5.8.0 As `ALLOWED_STYLES`.
       
   309 	 * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`,
       
   310 	 *              added new properties for `border`, `filter`, `spacing`,
       
   311 	 *              and `typography`.
   214 	 * @var array
   312 	 * @var array
   215 	 */
   313 	 */
   216 	const ALLOWED_STYLES = array(
   314 	const VALID_STYLES = array(
   217 		'border'     => array(
   315 		'border'     => array(
       
   316 			'color'  => null,
   218 			'radius' => null,
   317 			'radius' => null,
       
   318 			'style'  => null,
       
   319 			'width'  => null,
   219 		),
   320 		),
   220 		'color'      => array(
   321 		'color'      => array(
   221 			'background' => null,
   322 			'background' => null,
   222 			'gradient'   => null,
   323 			'gradient'   => null,
   223 			'text'       => null,
   324 			'text'       => null,
   224 		),
   325 		),
       
   326 		'filter'     => array(
       
   327 			'duotone' => null,
       
   328 		),
   225 		'spacing'    => array(
   329 		'spacing'    => array(
   226 			'margin'  => array(
   330 			'margin'   => null,
   227 				'top'    => null,
   331 			'padding'  => null,
   228 				'right'  => null,
   332 			'blockGap' => 'top',
   229 				'bottom' => null,
       
   230 				'left'   => null,
       
   231 			),
       
   232 			'padding' => array(
       
   233 				'bottom' => null,
       
   234 				'left'   => null,
       
   235 				'right'  => null,
       
   236 				'top'    => null,
       
   237 			),
       
   238 		),
   333 		),
   239 		'typography' => array(
   334 		'typography' => array(
   240 			'fontSize'   => null,
   335 			'fontFamily'     => null,
   241 			'lineHeight' => null,
   336 			'fontSize'       => null,
       
   337 			'fontStyle'      => null,
       
   338 			'fontWeight'     => null,
       
   339 			'letterSpacing'  => null,
       
   340 			'lineHeight'     => null,
       
   341 			'textDecoration' => null,
       
   342 			'textTransform'  => null,
   242 		),
   343 		),
   243 	);
   344 	);
   244 
   345 
   245 	/**
   346 	/**
   246 	 * @since 5.8.0
   347 	 * The valid elements that can be found under styles.
   247 	 * @var array
   348 	 *
       
   349 	 * @since 5.8.0
       
   350 	 * @var string[]
   248 	 */
   351 	 */
   249 	const ELEMENTS = array(
   352 	const ELEMENTS = array(
   250 		'link' => 'a',
   353 		'link' => 'a',
   251 		'h1'   => 'h1',
   354 		'h1'   => 'h1',
   252 		'h2'   => 'h2',
   355 		'h2'   => 'h2',
   255 		'h5'   => 'h5',
   358 		'h5'   => 'h5',
   256 		'h6'   => 'h6',
   359 		'h6'   => 'h6',
   257 	);
   360 	);
   258 
   361 
   259 	/**
   362 	/**
   260 	 * @since 5.8.0
   363 	 * Options that settings.appearanceTools enables.
       
   364 	 *
       
   365 	 * @since 6.0.0
       
   366 	 * @var array
       
   367 	 */
       
   368 	const APPEARANCE_TOOLS_OPT_INS = array(
       
   369 		array( 'border', 'color' ),
       
   370 		array( 'border', 'radius' ),
       
   371 		array( 'border', 'style' ),
       
   372 		array( 'border', 'width' ),
       
   373 		array( 'color', 'link' ),
       
   374 		array( 'spacing', 'blockGap' ),
       
   375 		array( 'spacing', 'margin' ),
       
   376 		array( 'spacing', 'padding' ),
       
   377 		array( 'typography', 'lineHeight' ),
       
   378 	);
       
   379 
       
   380 	/**
       
   381 	 * The latest version of the schema in use.
       
   382 	 *
       
   383 	 * @since 5.8.0
       
   384 	 * @since 5.9.0 Changed value from 1 to 2.
   261 	 * @var int
   385 	 * @var int
   262 	 */
   386 	 */
   263 	const LATEST_SCHEMA = 1;
   387 	const LATEST_SCHEMA = 2;
   264 
   388 
   265 	/**
   389 	/**
   266 	 * Constructor.
   390 	 * Constructor.
   267 	 *
   391 	 *
   268 	 * @since 5.8.0
   392 	 * @since 5.8.0
   269 	 *
   393 	 *
   270 	 * @param array $theme_json A structure that follows the theme.json schema.
   394 	 * @param array  $theme_json A structure that follows the theme.json schema.
   271 	 * @param string $origin    Optional. What source of data this object represents.
   395 	 * @param string $origin     Optional. What source of data this object represents.
   272 	 *                          One of 'core', 'theme', or 'user'. Default 'theme'.
   396 	 *                           One of 'default', 'theme', or 'custom'. Default 'theme'.
   273 	 */
   397 	 */
   274 	public function __construct( $theme_json = array(), $origin = 'theme' ) {
   398 	public function __construct( $theme_json = array(), $origin = 'theme' ) {
   275 		if ( ! in_array( $origin, self::VALID_ORIGINS, true ) ) {
   399 		if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
   276 			$origin = 'theme';
   400 			$origin = 'theme';
   277 		}
   401 		}
   278 
   402 
   279 		if ( ! isset( $theme_json['version'] ) || self::LATEST_SCHEMA !== $theme_json['version'] ) {
   403 		$this->theme_json    = WP_Theme_JSON_Schema::migrate( $theme_json );
   280 			$this->theme_json = array();
   404 		$valid_block_names   = array_keys( static::get_blocks_metadata() );
   281 			return;
   405 		$valid_element_names = array_keys( static::ELEMENTS );
   282 		}
   406 		$theme_json          = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names );
   283 
   407 		$this->theme_json    = static::maybe_opt_in_into_settings( $theme_json );
   284 		$this->theme_json = self::sanitize( $theme_json );
       
   285 
   408 
   286 		// Internally, presets are keyed by origin.
   409 		// Internally, presets are keyed by origin.
   287 		$nodes = self::get_setting_nodes( $this->theme_json );
   410 		$nodes = static::get_setting_nodes( $this->theme_json );
   288 		foreach ( $nodes as $node ) {
   411 		foreach ( $nodes as $node ) {
   289 			foreach ( self::PRESETS_METADATA as $preset ) {
   412 			foreach ( static::PRESETS_METADATA as $preset_metadata ) {
   290 				$path   = array_merge( $node['path'], $preset['path'] );
   413 				$path   = array_merge( $node['path'], $preset_metadata['path'] );
   291 				$preset = _wp_array_get( $this->theme_json, $path, null );
   414 				$preset = _wp_array_get( $this->theme_json, $path, null );
   292 				if ( null !== $preset ) {
   415 				if ( null !== $preset ) {
   293 					_wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
   416 					// If the preset is not already keyed by origin.
   294 				}
   417 					if ( isset( $preset[0] ) || empty( $preset ) ) {
   295 			}
   418 						_wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
   296 		}
   419 					}
       
   420 				}
       
   421 			}
       
   422 		}
       
   423 	}
       
   424 
       
   425 	/**
       
   426 	 * Enables some opt-in settings if theme declared support.
       
   427 	 *
       
   428 	 * @since 5.9.0
       
   429 	 *
       
   430 	 * @param array $theme_json A theme.json structure to modify.
       
   431 	 * @return array The modified theme.json structure.
       
   432 	 */
       
   433 	protected static function maybe_opt_in_into_settings( $theme_json ) {
       
   434 		$new_theme_json = $theme_json;
       
   435 
       
   436 		if (
       
   437 			isset( $new_theme_json['settings']['appearanceTools'] ) &&
       
   438 			true === $new_theme_json['settings']['appearanceTools']
       
   439 		) {
       
   440 			static::do_opt_in_into_settings( $new_theme_json['settings'] );
       
   441 		}
       
   442 
       
   443 		if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) {
       
   444 			foreach ( $new_theme_json['settings']['blocks'] as &$block ) {
       
   445 				if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) {
       
   446 					static::do_opt_in_into_settings( $block );
       
   447 				}
       
   448 			}
       
   449 		}
       
   450 
       
   451 		return $new_theme_json;
       
   452 	}
       
   453 
       
   454 	/**
       
   455 	 * Enables some settings.
       
   456 	 *
       
   457 	 * @since 5.9.0
       
   458 	 *
       
   459 	 * @param array $context The context to which the settings belong.
       
   460 	 */
       
   461 	protected static function do_opt_in_into_settings( &$context ) {
       
   462 		foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) {
       
   463 			// Use "unset prop" as a marker instead of "null" because
       
   464 			// "null" can be a valid value for some props (e.g. blockGap).
       
   465 			if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) {
       
   466 				_wp_array_set( $context, $path, true );
       
   467 			}
       
   468 		}
       
   469 
       
   470 		unset( $context['appearanceTools'] );
   297 	}
   471 	}
   298 
   472 
   299 	/**
   473 	/**
   300 	 * Sanitizes the input according to the schemas.
   474 	 * Sanitizes the input according to the schemas.
   301 	 *
   475 	 *
   302 	 * @since 5.8.0
   476 	 * @since 5.8.0
   303 	 *
   477 	 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
   304 	 * @param array $input Structure to sanitize.
   478 	 *
       
   479 	 * @param array $input               Structure to sanitize.
       
   480 	 * @param array $valid_block_names   List of valid block names.
       
   481 	 * @param array $valid_element_names List of valid element names.
   305 	 * @return array The sanitized output.
   482 	 * @return array The sanitized output.
   306 	 */
   483 	 */
   307 	private static function sanitize( $input ) {
   484 	protected static function sanitize( $input, $valid_block_names, $valid_element_names ) {
   308 		$output = array();
   485 		$output = array();
   309 
   486 
   310 		if ( ! is_array( $input ) ) {
   487 		if ( ! is_array( $input ) ) {
   311 			return $output;
   488 			return $output;
   312 		}
   489 		}
   313 
   490 
   314 		$allowed_top_level_keys = self::ALLOWED_TOP_LEVEL_KEYS;
   491 		$output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) );
   315 		$allowed_settings       = self::ALLOWED_SETTINGS;
   492 
   316 		$allowed_styles         = self::ALLOWED_STYLES;
   493 		// Some styles are only meant to be available at the top-level (e.g.: blockGap),
   317 		$allowed_blocks         = array_keys( self::get_blocks_metadata() );
   494 		// hence, the schema for blocks & elements should not have them.
   318 		$allowed_elements       = array_keys( self::ELEMENTS );
   495 		$styles_non_top_level = static::VALID_STYLES;
   319 
   496 		foreach ( array_keys( $styles_non_top_level ) as $section ) {
   320 		$output = array_intersect_key( $input, array_flip( $allowed_top_level_keys ) );
   497 			foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) {
   321 
   498 				if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) {
   322 		// Build the schema.
   499 					unset( $styles_non_top_level[ $section ][ $prop ] );
       
   500 				}
       
   501 			}
       
   502 		}
       
   503 
       
   504 		// Build the schema based on valid block & element names.
   323 		$schema                 = array();
   505 		$schema                 = array();
   324 		$schema_styles_elements = array();
   506 		$schema_styles_elements = array();
   325 		foreach ( $allowed_elements as $element ) {
   507 		foreach ( $valid_element_names as $element ) {
   326 			$schema_styles_elements[ $element ] = $allowed_styles;
   508 			$schema_styles_elements[ $element ] = $styles_non_top_level;
   327 		}
   509 		}
   328 		$schema_styles_blocks   = array();
   510 		$schema_styles_blocks   = array();
   329 		$schema_settings_blocks = array();
   511 		$schema_settings_blocks = array();
   330 		foreach ( $allowed_blocks as $block ) {
   512 		foreach ( $valid_block_names as $block ) {
   331 			$schema_settings_blocks[ $block ]           = $allowed_settings;
   513 			$schema_settings_blocks[ $block ]           = static::VALID_SETTINGS;
   332 			$schema_styles_blocks[ $block ]             = $allowed_styles;
   514 			$schema_styles_blocks[ $block ]             = $styles_non_top_level;
   333 			$schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
   515 			$schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
   334 		}
   516 		}
   335 		$schema['styles']             = $allowed_styles;
   517 		$schema['styles']             = static::VALID_STYLES;
   336 		$schema['styles']['blocks']   = $schema_styles_blocks;
   518 		$schema['styles']['blocks']   = $schema_styles_blocks;
   337 		$schema['styles']['elements'] = $schema_styles_elements;
   519 		$schema['styles']['elements'] = $schema_styles_elements;
   338 		$schema['settings']           = $allowed_settings;
   520 		$schema['settings']           = static::VALID_SETTINGS;
   339 		$schema['settings']['blocks'] = $schema_settings_blocks;
   521 		$schema['settings']['blocks'] = $schema_settings_blocks;
   340 
   522 
   341 		// Remove anything that's not present in the schema.
   523 		// Remove anything that's not present in the schema.
   342 		foreach ( array( 'styles', 'settings' ) as $subtree ) {
   524 		foreach ( array( 'styles', 'settings' ) as $subtree ) {
   343 			if ( ! isset( $input[ $subtree ] ) ) {
   525 			if ( ! isset( $input[ $subtree ] ) ) {
   347 			if ( ! is_array( $input[ $subtree ] ) ) {
   529 			if ( ! is_array( $input[ $subtree ] ) ) {
   348 				unset( $output[ $subtree ] );
   530 				unset( $output[ $subtree ] );
   349 				continue;
   531 				continue;
   350 			}
   532 			}
   351 
   533 
   352 			$result = self::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
   534 			$result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
   353 
   535 
   354 			if ( empty( $result ) ) {
   536 			if ( empty( $result ) ) {
   355 				unset( $output[ $subtree ] );
   537 				unset( $output[ $subtree ] );
   356 			} else {
   538 			} else {
   357 				$output[ $subtree ] = $result;
   539 				$output[ $subtree ] = $result;
   375 	 *         }
   557 	 *         }
   376 	 *       },
   558 	 *       },
   377 	 *       'core/heading': {
   559 	 *       'core/heading': {
   378 	 *         'selector': 'h1',
   560 	 *         'selector': 'h1',
   379 	 *         'elements': {}
   561 	 *         'elements': {}
   380 	 *       }
   562 	 *       },
   381 	 *       'core/group': {
   563 	 *       'core/image': {
   382 	 *         'selector': '.wp-block-group',
   564 	 *         'selector': '.wp-block-image',
       
   565 	 *         'duotone': 'img',
   383 	 *         'elements': {}
   566 	 *         'elements': {}
   384 	 *       }
   567 	 *       }
   385 	 *     }
   568 	 *     }
   386 	 *
   569 	 *
   387 	 * @since 5.8.0
   570 	 * @since 5.8.0
       
   571 	 * @since 5.9.0 Added `duotone` key with CSS selector.
   388 	 *
   572 	 *
   389 	 * @return array Block metadata.
   573 	 * @return array Block metadata.
   390 	 */
   574 	 */
   391 	private static function get_blocks_metadata() {
   575 	protected static function get_blocks_metadata() {
   392 		if ( null !== self::$blocks_metadata ) {
   576 		if ( null !== static::$blocks_metadata ) {
   393 			return self::$blocks_metadata;
   577 			return static::$blocks_metadata;
   394 		}
   578 		}
   395 
   579 
   396 		self::$blocks_metadata = array();
   580 		static::$blocks_metadata = array();
   397 
   581 
   398 		$registry = WP_Block_Type_Registry::get_instance();
   582 		$registry = WP_Block_Type_Registry::get_instance();
   399 		$blocks   = $registry->get_all_registered();
   583 		$blocks   = $registry->get_all_registered();
   400 		foreach ( $blocks as $block_name => $block_type ) {
   584 		foreach ( $blocks as $block_name => $block_type ) {
   401 			if (
   585 			if (
   402 				isset( $block_type->supports['__experimentalSelector'] ) &&
   586 				isset( $block_type->supports['__experimentalSelector'] ) &&
   403 				is_string( $block_type->supports['__experimentalSelector'] )
   587 				is_string( $block_type->supports['__experimentalSelector'] )
   404 			) {
   588 			) {
   405 				self::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector'];
   589 				static::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector'];
   406 			} else {
   590 			} else {
   407 				self::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) );
   591 				static::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) );
   408 			}
   592 			}
   409 
   593 
   410 			/*
   594 			if (
   411 			 * Assign defaults, then overwrite those that the block sets by itself.
   595 				isset( $block_type->supports['color']['__experimentalDuotone'] ) &&
   412 			 * If the block selector is compounded, will append the element to each
   596 				is_string( $block_type->supports['color']['__experimentalDuotone'] )
   413 			 * individual block selector.
   597 			) {
   414 			 */
   598 				static::$blocks_metadata[ $block_name ]['duotone'] = $block_type->supports['color']['__experimentalDuotone'];
   415 			$block_selectors = explode( ',', self::$blocks_metadata[ $block_name ]['selector'] );
   599 			}
   416 			foreach ( self::ELEMENTS as $el_name => $el_selector ) {
   600 
       
   601 			// Assign defaults, then overwrite those that the block sets by itself.
       
   602 			// If the block selector is compounded, will append the element to each
       
   603 			// individual block selector.
       
   604 			$block_selectors = explode( ',', static::$blocks_metadata[ $block_name ]['selector'] );
       
   605 			foreach ( static::ELEMENTS as $el_name => $el_selector ) {
   417 				$element_selector = array();
   606 				$element_selector = array();
   418 				foreach ( $block_selectors as $selector ) {
   607 				foreach ( $block_selectors as $selector ) {
   419 					$element_selector[] = $selector . ' ' . $el_selector;
   608 					$element_selector[] = $selector . ' ' . $el_selector;
   420 				}
   609 				}
   421 				self::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector );
   610 				static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector );
   422 			}
   611 			}
   423 		}
   612 		}
   424 
   613 
   425 		return self::$blocks_metadata;
   614 		return static::$blocks_metadata;
   426 	}
   615 	}
   427 
   616 
   428 	/**
   617 	/**
   429 	 * Given a tree, removes the keys that are not present in the schema.
   618 	 * Given a tree, removes the keys that are not present in the schema.
   430 	 *
   619 	 *
   434 	 *
   623 	 *
   435 	 * @param array $tree   Input to process.
   624 	 * @param array $tree   Input to process.
   436 	 * @param array $schema Schema to adhere to.
   625 	 * @param array $schema Schema to adhere to.
   437 	 * @return array Returns the modified $tree.
   626 	 * @return array Returns the modified $tree.
   438 	 */
   627 	 */
   439 	private static function remove_keys_not_in_schema( $tree, $schema ) {
   628 	protected static function remove_keys_not_in_schema( $tree, $schema ) {
   440 		$tree = array_intersect_key( $tree, $schema );
   629 		$tree = array_intersect_key( $tree, $schema );
   441 
   630 
   442 		foreach ( $schema as $key => $data ) {
   631 		foreach ( $schema as $key => $data ) {
   443 			if ( ! isset( $tree[ $key ] ) ) {
   632 			if ( ! isset( $tree[ $key ] ) ) {
   444 				continue;
   633 				continue;
   445 			}
   634 			}
   446 
   635 
   447 			if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) {
   636 			if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) {
   448 				$tree[ $key ] = self::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] );
   637 				$tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] );
   449 
   638 
   450 				if ( empty( $tree[ $key ] ) ) {
   639 				if ( empty( $tree[ $key ] ) ) {
   451 					unset( $tree[ $key ] );
   640 					unset( $tree[ $key ] );
   452 				}
   641 				}
   453 			} elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) {
   642 			} elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) {
   491 	/**
   680 	/**
   492 	 * Returns the stylesheet that results of processing
   681 	 * Returns the stylesheet that results of processing
   493 	 * the theme.json structure this object represents.
   682 	 * the theme.json structure this object represents.
   494 	 *
   683 	 *
   495 	 * @since 5.8.0
   684 	 * @since 5.8.0
   496 	 *
   685 	 * @since 5.9.0 Removed the `$type` parameter`, added the `$types` and `$origins` parameters.
   497 	 * @param string $type Optional. Type of stylesheet we want. Accepts 'all',
   686 	 *
   498 	 *                     'block_styles', and 'css_variables'. Default 'all'.
   687 	 * @param array $types   Types of styles to load. Will load all by default. It accepts:
       
   688 	 *                       - `variables`: only the CSS Custom Properties for presets & custom ones.
       
   689 	 *                       - `styles`: only the styles section in theme.json.
       
   690 	 *                       - `presets`: only the classes for the presets.
       
   691 	 * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS.
   499 	 * @return string Stylesheet.
   692 	 * @return string Stylesheet.
   500 	 */
   693 	 */
   501 	public function get_stylesheet( $type = 'all' ) {
   694 	public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null ) {
   502 		$blocks_metadata = self::get_blocks_metadata();
   695 		if ( null === $origins ) {
   503 		$style_nodes     = self::get_style_nodes( $this->theme_json, $blocks_metadata );
   696 			$origins = static::VALID_ORIGINS;
   504 		$setting_nodes   = self::get_setting_nodes( $this->theme_json, $blocks_metadata );
   697 		}
   505 
   698 
   506 		switch ( $type ) {
   699 		if ( is_string( $types ) ) {
   507 			case 'block_styles':
   700 			// Dispatch error and map old arguments to new ones.
   508 				return $this->get_block_styles( $style_nodes, $setting_nodes );
   701 			_deprecated_argument( __FUNCTION__, '5.9.0' );
   509 			case 'css_variables':
   702 			if ( 'block_styles' === $types ) {
   510 				return $this->get_css_variables( $setting_nodes );
   703 				$types = array( 'styles', 'presets' );
   511 			default:
   704 			} elseif ( 'css_variables' === $types ) {
   512 				return $this->get_css_variables( $setting_nodes ) . $this->get_block_styles( $style_nodes, $setting_nodes );
   705 				$types = array( 'variables' );
   513 		}
   706 			} else {
   514 
   707 				$types = array( 'variables', 'styles', 'presets' );
       
   708 			}
       
   709 		}
       
   710 
       
   711 		$blocks_metadata = static::get_blocks_metadata();
       
   712 		$style_nodes     = static::get_style_nodes( $this->theme_json, $blocks_metadata );
       
   713 		$setting_nodes   = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
       
   714 
       
   715 		$stylesheet = '';
       
   716 
       
   717 		if ( in_array( 'variables', $types, true ) ) {
       
   718 			$stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
       
   719 		}
       
   720 
       
   721 		if ( in_array( 'styles', $types, true ) ) {
       
   722 			$stylesheet .= $this->get_block_classes( $style_nodes );
       
   723 		}
       
   724 
       
   725 		if ( in_array( 'presets', $types, true ) ) {
       
   726 			$stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
       
   727 		}
       
   728 
       
   729 		return $stylesheet;
       
   730 	}
       
   731 
       
   732 	/**
       
   733 	 * Returns the page templates of the active theme.
       
   734 	 *
       
   735 	 * @since 5.9.0
       
   736 	 *
       
   737 	 * @return array
       
   738 	 */
       
   739 	public function get_custom_templates() {
       
   740 		$custom_templates = array();
       
   741 		if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $this->theme_json['customTemplates'] ) ) {
       
   742 			return $custom_templates;
       
   743 		}
       
   744 
       
   745 		foreach ( $this->theme_json['customTemplates'] as $item ) {
       
   746 			if ( isset( $item['name'] ) ) {
       
   747 				$custom_templates[ $item['name'] ] = array(
       
   748 					'title'     => isset( $item['title'] ) ? $item['title'] : '',
       
   749 					'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ),
       
   750 				);
       
   751 			}
       
   752 		}
       
   753 		return $custom_templates;
       
   754 	}
       
   755 
       
   756 	/**
       
   757 	 * Returns the template part data of active theme.
       
   758 	 *
       
   759 	 * @since 5.9.0
       
   760 	 *
       
   761 	 * @return array
       
   762 	 */
       
   763 	public function get_template_parts() {
       
   764 		$template_parts = array();
       
   765 		if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $this->theme_json['templateParts'] ) ) {
       
   766 			return $template_parts;
       
   767 		}
       
   768 
       
   769 		foreach ( $this->theme_json['templateParts'] as $item ) {
       
   770 			if ( isset( $item['name'] ) ) {
       
   771 				$template_parts[ $item['name'] ] = array(
       
   772 					'title' => isset( $item['title'] ) ? $item['title'] : '',
       
   773 					'area'  => isset( $item['area'] ) ? $item['area'] : '',
       
   774 				);
       
   775 			}
       
   776 		}
       
   777 		return $template_parts;
   515 	}
   778 	}
   516 
   779 
   517 	/**
   780 	/**
   518 	 * Converts each style section into a list of rulesets
   781 	 * Converts each style section into a list of rulesets
   519 	 * containing the block styles to be appended to the stylesheet.
   782 	 * containing the block styles to be appended to the stylesheet.
   524 	 *
   787 	 *
   525 	 *   block-selector {
   788 	 *   block-selector {
   526 	 *     style-property-one: value;
   789 	 *     style-property-one: value;
   527 	 *   }
   790 	 *   }
   528 	 *
   791 	 *
   529 	 * Additionally, it'll also create new rulesets
   792 	 * @since 5.8.0 As `get_block_styles()`.
   530 	 * as classes for each preset value such as:
   793 	 * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()`
   531 	 *
   794 	 *              and no longer returns preset classes.
   532 	 *     .has-value-color {
   795 	 *              Removed the `$setting_nodes` parameter.
   533 	 *       color: value;
   796 	 *
   534 	 *     }
   797 	 * @param array $style_nodes Nodes with styles.
   535 	 *
       
   536 	 *     .has-value-background-color {
       
   537 	 *       background-color: value;
       
   538 	 *     }
       
   539 	 *
       
   540 	 *     .has-value-font-size {
       
   541 	 *       font-size: value;
       
   542 	 *     }
       
   543 	 *
       
   544 	 *     .has-value-gradient-background {
       
   545 	 *       background: value;
       
   546 	 *     }
       
   547 	 *
       
   548 	 *     p.has-value-gradient-background {
       
   549 	 *       background: value;
       
   550 	 *     }
       
   551 	 *
       
   552 	 * @since 5.8.0
       
   553 	 *
       
   554 	 * @param array $style_nodes   Nodes with styles.
       
   555 	 * @param array $setting_nodes Nodes with settings.
       
   556 	 * @return string The new stylesheet.
   798 	 * @return string The new stylesheet.
   557 	 */
   799 	 */
   558 	private function get_block_styles( $style_nodes, $setting_nodes ) {
   800 	protected function get_block_classes( $style_nodes ) {
   559 		$block_rules = '';
   801 		$block_rules = '';
       
   802 
   560 		foreach ( $style_nodes as $metadata ) {
   803 		foreach ( $style_nodes as $metadata ) {
   561 			if ( null === $metadata['selector'] ) {
   804 			if ( null === $metadata['selector'] ) {
   562 				continue;
   805 				continue;
   563 			}
   806 			}
   564 
   807 
   565 			$node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
   808 			$node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
   566 			$selector     = $metadata['selector'];
   809 			$selector     = $metadata['selector'];
   567 			$declarations = self::compute_style_properties( $node );
   810 			$settings     = _wp_array_get( $this->theme_json, array( 'settings' ) );
   568 			$block_rules .= self::to_ruleset( $selector, $declarations );
   811 			$declarations = static::compute_style_properties( $node, $settings );
   569 		}
   812 
   570 
   813 			// 1. Separate the ones who use the general selector
       
   814 			// and the ones who use the duotone selector.
       
   815 			$declarations_duotone = array();
       
   816 			foreach ( $declarations as $index => $declaration ) {
       
   817 				if ( 'filter' === $declaration['name'] ) {
       
   818 					unset( $declarations[ $index ] );
       
   819 					$declarations_duotone[] = $declaration;
       
   820 				}
       
   821 			}
       
   822 
       
   823 			/*
       
   824 			 * Reset default browser margin on the root body element.
       
   825 			 * This is set on the root selector **before** generating the ruleset
       
   826 			 * from the `theme.json`. This is to ensure that if the `theme.json` declares
       
   827 			 * `margin` in its `spacing` declaration for the `body` element then these
       
   828 			 * user-generated values take precedence in the CSS cascade.
       
   829 			 * @link https://github.com/WordPress/gutenberg/issues/36147.
       
   830 			 */
       
   831 			if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
       
   832 				$block_rules .= 'body { margin: 0; }';
       
   833 			}
       
   834 
       
   835 			// 2. Generate the rules that use the general selector.
       
   836 			$block_rules .= static::to_ruleset( $selector, $declarations );
       
   837 
       
   838 			// 3. Generate the rules that use the duotone selector.
       
   839 			if ( isset( $metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
       
   840 				$selector_duotone = static::scope_selector( $metadata['selector'], $metadata['duotone'] );
       
   841 				$block_rules     .= static::to_ruleset( $selector_duotone, $declarations_duotone );
       
   842 			}
       
   843 
       
   844 			if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
       
   845 				$block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
       
   846 				$block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
       
   847 				$block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
       
   848 
       
   849 				$has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
       
   850 				if ( $has_block_gap_support ) {
       
   851 					$block_rules .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }';
       
   852 					$block_rules .= '.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }';
       
   853 				}
       
   854 			}
       
   855 		}
       
   856 
       
   857 		return $block_rules;
       
   858 	}
       
   859 
       
   860 	/**
       
   861 	 * Creates new rulesets as classes for each preset value such as:
       
   862 	 *
       
   863 	 *   .has-value-color {
       
   864 	 *     color: value;
       
   865 	 *   }
       
   866 	 *
       
   867 	 *   .has-value-background-color {
       
   868 	 *     background-color: value;
       
   869 	 *   }
       
   870 	 *
       
   871 	 *   .has-value-font-size {
       
   872 	 *     font-size: value;
       
   873 	 *   }
       
   874 	 *
       
   875 	 *   .has-value-gradient-background {
       
   876 	 *     background: value;
       
   877 	 *   }
       
   878 	 *
       
   879 	 *   p.has-value-gradient-background {
       
   880 	 *     background: value;
       
   881 	 *   }
       
   882 	 *
       
   883 	 * @since 5.9.0
       
   884 	 *
       
   885 	 * @param array $setting_nodes Nodes with settings.
       
   886 	 * @param array $origins       List of origins to process presets from.
       
   887 	 * @return string The new stylesheet.
       
   888 	 */
       
   889 	protected function get_preset_classes( $setting_nodes, $origins ) {
   571 		$preset_rules = '';
   890 		$preset_rules = '';
       
   891 
   572 		foreach ( $setting_nodes as $metadata ) {
   892 		foreach ( $setting_nodes as $metadata ) {
   573 			if ( null === $metadata['selector'] ) {
   893 			if ( null === $metadata['selector'] ) {
   574 				continue;
   894 				continue;
   575 			}
   895 			}
   576 
   896 
   577 			$selector      = $metadata['selector'];
   897 			$selector      = $metadata['selector'];
   578 			$node          = _wp_array_get( $this->theme_json, $metadata['path'], array() );
   898 			$node          = _wp_array_get( $this->theme_json, $metadata['path'], array() );
   579 			$preset_rules .= self::compute_preset_classes( $node, $selector );
   899 			$preset_rules .= static::compute_preset_classes( $node, $selector, $origins );
   580 		}
   900 		}
   581 
   901 
   582 		return $block_rules . $preset_rules;
   902 		return $preset_rules;
   583 	}
   903 	}
   584 
   904 
   585 	/**
   905 	/**
   586 	 * Converts each styles section into a list of rulesets
   906 	 * Converts each styles section into a list of rulesets
   587 	 * to be appended to the stylesheet.
   907 	 * to be appended to the stylesheet.
   595 	 *       --wp--preset--category--slug: value;
   915 	 *       --wp--preset--category--slug: value;
   596 	 *       --wp--custom--variable: value;
   916 	 *       --wp--custom--variable: value;
   597 	 *     }
   917 	 *     }
   598 	 *
   918 	 *
   599 	 * @since 5.8.0
   919 	 * @since 5.8.0
   600 	 *
   920 	 * @since 5.9.0 Added the `$origins` parameter.
   601 	 * @param array $nodes Nodes with settings.
   921 	 *
       
   922 	 * @param array $nodes   Nodes with settings.
       
   923 	 * @param array $origins List of origins to process.
   602 	 * @return string The new stylesheet.
   924 	 * @return string The new stylesheet.
   603 	 */
   925 	 */
   604 	private function get_css_variables( $nodes ) {
   926 	protected function get_css_variables( $nodes, $origins ) {
   605 		$stylesheet = '';
   927 		$stylesheet = '';
   606 		foreach ( $nodes as $metadata ) {
   928 		foreach ( $nodes as $metadata ) {
   607 			if ( null === $metadata['selector'] ) {
   929 			if ( null === $metadata['selector'] ) {
   608 				continue;
   930 				continue;
   609 			}
   931 			}
   610 
   932 
   611 			$selector = $metadata['selector'];
   933 			$selector = $metadata['selector'];
   612 
   934 
   613 			$node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
   935 			$node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
   614 			$declarations = array_merge( self::compute_preset_vars( $node ), self::compute_theme_vars( $node ) );
   936 			$declarations = array_merge( static::compute_preset_vars( $node, $origins ), static::compute_theme_vars( $node ) );
   615 
   937 
   616 			$stylesheet .= self::to_ruleset( $selector, $declarations );
   938 			$stylesheet .= static::to_ruleset( $selector, $declarations );
   617 		}
   939 		}
   618 
   940 
   619 		return $stylesheet;
   941 		return $stylesheet;
   620 	}
   942 	}
   621 
   943 
   627 	 *
   949 	 *
   628 	 * @param string $selector     CSS selector.
   950 	 * @param string $selector     CSS selector.
   629 	 * @param array  $declarations List of declarations.
   951 	 * @param array  $declarations List of declarations.
   630 	 * @return string CSS ruleset.
   952 	 * @return string CSS ruleset.
   631 	 */
   953 	 */
   632 	private static function to_ruleset( $selector, $declarations ) {
   954 	protected static function to_ruleset( $selector, $declarations ) {
   633 		if ( empty( $declarations ) ) {
   955 		if ( empty( $declarations ) ) {
   634 			return '';
   956 			return '';
   635 		}
   957 		}
   636 
   958 
   637 		$declaration_block = array_reduce(
   959 		$declaration_block = array_reduce(
   638 			$declarations,
   960 			$declarations,
   639 			function ( $carry, $element ) {
   961 			static function ( $carry, $element ) {
   640 				return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
   962 				return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
   641 			''
   963 			''
   642 		);
   964 		);
   643 
   965 
   644 		return $selector . '{' . $declaration_block . '}';
   966 		return $selector . '{' . $declaration_block . '}';
   655 	 *
   977 	 *
   656 	 * @param string $selector  Original selector.
   978 	 * @param string $selector  Original selector.
   657 	 * @param string $to_append Selector to append.
   979 	 * @param string $to_append Selector to append.
   658 	 * @return string
   980 	 * @return string
   659 	 */
   981 	 */
   660 	private static function append_to_selector( $selector, $to_append ) {
   982 	protected static function append_to_selector( $selector, $to_append ) {
   661 		$new_selectors = array();
   983 		$new_selectors = array();
   662 		$selectors     = explode( ',', $selector );
   984 		$selectors     = explode( ',', $selector );
   663 		foreach ( $selectors as $sel ) {
   985 		foreach ( $selectors as $sel ) {
   664 			$new_selectors[] = $sel . $to_append;
   986 			$new_selectors[] = $sel . $to_append;
   665 		}
   987 		}
   666 
   988 
   667 		return implode( ',', $new_selectors );
   989 		return implode( ',', $new_selectors );
   668 	}
   990 	}
   669 
   991 
   670 	/**
   992 	/**
   671 	 * Given an array of presets keyed by origin and the value key of the preset,
       
   672 	 * it returns an array where each key is the preset slug and each value the preset value.
       
   673 	 *
       
   674 	 * @since 5.8.0
       
   675 	 *
       
   676 	 * @param array  $preset_per_origin Array of presets keyed by origin.
       
   677 	 * @param string $value_key         The property of the preset that contains its value.
       
   678 	 * @return array Array of presets where each key is a slug and each value is the preset value.
       
   679 	 */
       
   680 	private static function get_merged_preset_by_slug( $preset_per_origin, $value_key ) {
       
   681 		$result = array();
       
   682 		foreach ( self::VALID_ORIGINS as $origin ) {
       
   683 			if ( ! isset( $preset_per_origin[ $origin ] ) ) {
       
   684 				continue;
       
   685 			}
       
   686 			foreach ( $preset_per_origin[ $origin ] as $preset ) {
       
   687 				/*
       
   688 				 * We don't want to use kebabCase here,
       
   689 				 * see https://github.com/WordPress/gutenberg/issues/32347
       
   690 				 * However, we need to make sure the generated class or CSS variable
       
   691 				 * doesn't contain spaces.
       
   692 				 */
       
   693 				$result[ preg_replace( '/\s+/', '-', $preset['slug'] ) ] = $preset[ $value_key ];
       
   694 			}
       
   695 		}
       
   696 		return $result;
       
   697 	}
       
   698 
       
   699 	/**
       
   700 	 * Given a settings array, it returns the generated rulesets
   993 	 * Given a settings array, it returns the generated rulesets
   701 	 * for the preset classes.
   994 	 * for the preset classes.
   702 	 *
   995 	 *
   703 	 * @since 5.8.0
   996 	 * @since 5.8.0
       
   997 	 * @since 5.9.0 Added the `$origins` parameter.
   704 	 *
   998 	 *
   705 	 * @param array  $settings Settings to process.
   999 	 * @param array  $settings Settings to process.
   706 	 * @param string $selector Selector wrapping the classes.
  1000 	 * @param string $selector Selector wrapping the classes.
       
  1001 	 * @param array  $origins  List of origins to process.
   707 	 * @return string The result of processing the presets.
  1002 	 * @return string The result of processing the presets.
   708 	 */
  1003 	 */
   709 	private static function compute_preset_classes( $settings, $selector ) {
  1004 	protected static function compute_preset_classes( $settings, $selector, $origins ) {
   710 		if ( self::ROOT_BLOCK_SELECTOR === $selector ) {
  1005 		if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
   711 			// Classes at the global level do not need any CSS prefixed,
  1006 			// Classes at the global level do not need any CSS prefixed,
   712 			// and we don't want to increase its specificity.
  1007 			// and we don't want to increase its specificity.
   713 			$selector = '';
  1008 			$selector = '';
   714 		}
  1009 		}
   715 
  1010 
   716 		$stylesheet = '';
  1011 		$stylesheet = '';
   717 		foreach ( self::PRESETS_METADATA as $preset ) {
  1012 		foreach ( static::PRESETS_METADATA as $preset_metadata ) {
   718 			$preset_per_origin = _wp_array_get( $settings, $preset['path'], array() );
  1013 			$slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins );
   719 			$preset_by_slug    = self::get_merged_preset_by_slug( $preset_per_origin, $preset['value_key'] );
  1014 			foreach ( $preset_metadata['classes'] as $class => $property ) {
   720 			foreach ( $preset['classes'] as $class ) {
  1015 				foreach ( $slugs as $slug ) {
   721 				foreach ( $preset_by_slug as $slug => $value ) {
  1016 					$css_var     = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
   722 					$stylesheet .= self::to_ruleset(
  1017 					$class_name  = static::replace_slug_in_string( $class, $slug );
   723 						self::append_to_selector( $selector, '.has-' . _wp_to_kebab_case( $slug ) . '-' . $class['class_suffix'] ),
  1018 					$stylesheet .= static::to_ruleset(
       
  1019 						static::append_to_selector( $selector, $class_name ),
   724 						array(
  1020 						array(
   725 							array(
  1021 							array(
   726 								'name'  => $class['property_name'],
  1022 								'name'  => $property,
   727 								'value' => 'var(--wp--preset--' . $preset['css_var_infix'] . '--' . _wp_to_kebab_case( $slug ) . ') !important',
  1023 								'value' => 'var(' . $css_var . ') !important',
   728 							),
  1024 							),
   729 						)
  1025 						)
   730 					);
  1026 					);
   731 				}
  1027 				}
   732 			}
  1028 			}
   733 		}
  1029 		}
   734 
  1030 
   735 		return $stylesheet;
  1031 		return $stylesheet;
       
  1032 	}
       
  1033 
       
  1034 	/**
       
  1035 	 * Function that scopes a selector with another one. This works a bit like
       
  1036 	 * SCSS nesting except the `&` operator isn't supported.
       
  1037 	 *
       
  1038 	 * <code>
       
  1039 	 * $scope = '.a, .b .c';
       
  1040 	 * $selector = '> .x, .y';
       
  1041 	 * $merged = scope_selector( $scope, $selector );
       
  1042 	 * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
       
  1043 	 * </code>
       
  1044 	 *
       
  1045 	 * @since 5.9.0
       
  1046 	 *
       
  1047 	 * @param string $scope    Selector to scope to.
       
  1048 	 * @param string $selector Original selector.
       
  1049 	 * @return string Scoped selector.
       
  1050 	 */
       
  1051 	protected static function scope_selector( $scope, $selector ) {
       
  1052 		$scopes    = explode( ',', $scope );
       
  1053 		$selectors = explode( ',', $selector );
       
  1054 
       
  1055 		$selectors_scoped = array();
       
  1056 		foreach ( $scopes as $outer ) {
       
  1057 			foreach ( $selectors as $inner ) {
       
  1058 				$selectors_scoped[] = trim( $outer ) . ' ' . trim( $inner );
       
  1059 			}
       
  1060 		}
       
  1061 
       
  1062 		return implode( ', ', $selectors_scoped );
       
  1063 	}
       
  1064 
       
  1065 	/**
       
  1066 	 * Gets preset values keyed by slugs based on settings and metadata.
       
  1067 	 *
       
  1068 	 * <code>
       
  1069 	 * $settings = array(
       
  1070 	 *     'typography' => array(
       
  1071 	 *         'fontFamilies' => array(
       
  1072 	 *             array(
       
  1073 	 *                 'slug'       => 'sansSerif',
       
  1074 	 *                 'fontFamily' => '"Helvetica Neue", sans-serif',
       
  1075 	 *             ),
       
  1076 	 *             array(
       
  1077 	 *                 'slug'   => 'serif',
       
  1078 	 *                 'colors' => 'Georgia, serif',
       
  1079 	 *             )
       
  1080 	 *         ),
       
  1081 	 *     ),
       
  1082 	 * );
       
  1083 	 * $meta = array(
       
  1084 	 *    'path'      => array( 'typography', 'fontFamilies' ),
       
  1085 	 *    'value_key' => 'fontFamily',
       
  1086 	 * );
       
  1087 	 * $values_by_slug = get_settings_values_by_slug();
       
  1088 	 * // $values_by_slug === array(
       
  1089 	 * //   'sans-serif' => '"Helvetica Neue", sans-serif',
       
  1090 	 * //   'serif'      => 'Georgia, serif',
       
  1091 	 * // );
       
  1092 	 * </code>
       
  1093 	 *
       
  1094 	 * @since 5.9.0
       
  1095 	 *
       
  1096 	 * @param array $settings        Settings to process.
       
  1097 	 * @param array $preset_metadata One of the PRESETS_METADATA values.
       
  1098 	 * @param array $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.
       
  1100 	 */
       
  1101 	protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) {
       
  1102 		$preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
       
  1103 
       
  1104 		$result = array();
       
  1105 		foreach ( $origins as $origin ) {
       
  1106 			if ( ! isset( $preset_per_origin[ $origin ] ) ) {
       
  1107 				continue;
       
  1108 			}
       
  1109 			foreach ( $preset_per_origin[ $origin ] as $preset ) {
       
  1110 				$slug = _wp_to_kebab_case( $preset['slug'] );
       
  1111 
       
  1112 				$value = '';
       
  1113 				if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
       
  1114 					$value_key = $preset_metadata['value_key'];
       
  1115 					$value     = $preset[ $value_key ];
       
  1116 				} elseif (
       
  1117 					isset( $preset_metadata['value_func'] ) &&
       
  1118 					is_callable( $preset_metadata['value_func'] )
       
  1119 				) {
       
  1120 					$value_func = $preset_metadata['value_func'];
       
  1121 					$value      = call_user_func( $value_func, $preset );
       
  1122 				} else {
       
  1123 					// If we don't have a value, then don't add it to the result.
       
  1124 					continue;
       
  1125 				}
       
  1126 
       
  1127 				$result[ $slug ] = $value;
       
  1128 			}
       
  1129 		}
       
  1130 		return $result;
       
  1131 	}
       
  1132 
       
  1133 	/**
       
  1134 	 * Similar to get_settings_values_by_slug, but doesn't compute the value.
       
  1135 	 *
       
  1136 	 * @since 5.9.0
       
  1137 	 *
       
  1138 	 * @param array $settings        Settings to process.
       
  1139 	 * @param array $preset_metadata One of the PRESETS_METADATA values.
       
  1140 	 * @param array $origins         List of origins to process.
       
  1141 	 * @return array Array of presets where the key and value are both the slug.
       
  1142 	 */
       
  1143 	protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) {
       
  1144 		if ( null === $origins ) {
       
  1145 			$origins = static::VALID_ORIGINS;
       
  1146 		}
       
  1147 
       
  1148 		$preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
       
  1149 
       
  1150 		$result = array();
       
  1151 		foreach ( $origins as $origin ) {
       
  1152 			if ( ! isset( $preset_per_origin[ $origin ] ) ) {
       
  1153 				continue;
       
  1154 			}
       
  1155 			foreach ( $preset_per_origin[ $origin ] as $preset ) {
       
  1156 				$slug = _wp_to_kebab_case( $preset['slug'] );
       
  1157 
       
  1158 				// Use the array as a set so we don't get duplicates.
       
  1159 				$result[ $slug ] = $slug;
       
  1160 			}
       
  1161 		}
       
  1162 		return $result;
       
  1163 	}
       
  1164 
       
  1165 	/**
       
  1166 	 * Transform a slug into a CSS Custom Property.
       
  1167 	 *
       
  1168 	 * @since 5.9.0
       
  1169 	 *
       
  1170 	 * @param string $input String to replace.
       
  1171 	 * @param string $slug  The slug value to use to generate the custom property.
       
  1172 	 * @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`.
       
  1173 	 */
       
  1174 	protected static function replace_slug_in_string( $input, $slug ) {
       
  1175 		return strtr( $input, array( '$slug' => $slug ) );
   736 	}
  1176 	}
   737 
  1177 
   738 	/**
  1178 	/**
   739 	 * Given the block settings, it extracts the CSS Custom Properties
  1179 	 * Given the block settings, it extracts the CSS Custom Properties
   740 	 * for the presets and adds them to the $declarations array
  1180 	 * for the presets and adds them to the $declarations array
   744 	 *       'name'  => 'property_name',
  1184 	 *       'name'  => 'property_name',
   745 	 *       'value' => 'property_value,
  1185 	 *       'value' => 'property_value,
   746 	 *     )
  1186 	 *     )
   747 	 *
  1187 	 *
   748 	 * @since 5.8.0
  1188 	 * @since 5.8.0
       
  1189 	 * @since 5.9.0 Added the `$origins` parameter.
   749 	 *
  1190 	 *
   750 	 * @param array $settings Settings to process.
  1191 	 * @param array $settings Settings to process.
       
  1192 	 * @param array $origins  List of origins to process.
   751 	 * @return array Returns the modified $declarations.
  1193 	 * @return array Returns the modified $declarations.
   752 	 */
  1194 	 */
   753 	private static function compute_preset_vars( $settings ) {
  1195 	protected static function compute_preset_vars( $settings, $origins ) {
   754 		$declarations = array();
  1196 		$declarations = array();
   755 		foreach ( self::PRESETS_METADATA as $preset ) {
  1197 		foreach ( static::PRESETS_METADATA as $preset_metadata ) {
   756 			$preset_per_origin = _wp_array_get( $settings, $preset['path'], array() );
  1198 			$values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins );
   757 			$preset_by_slug    = self::get_merged_preset_by_slug( $preset_per_origin, $preset['value_key'] );
  1199 			foreach ( $values_by_slug as $slug => $value ) {
   758 			foreach ( $preset_by_slug as $slug => $value ) {
       
   759 				$declarations[] = array(
  1200 				$declarations[] = array(
   760 					'name'  => '--wp--preset--' . $preset['css_var_infix'] . '--' . _wp_to_kebab_case( $slug ),
  1201 					'name'  => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
   761 					'value' => $value,
  1202 					'value' => $value,
   762 				);
  1203 				);
   763 			}
  1204 			}
   764 		}
  1205 		}
   765 
  1206 
   779 	 * @since 5.8.0
  1220 	 * @since 5.8.0
   780 	 *
  1221 	 *
   781 	 * @param array $settings Settings to process.
  1222 	 * @param array $settings Settings to process.
   782 	 * @return array Returns the modified $declarations.
  1223 	 * @return array Returns the modified $declarations.
   783 	 */
  1224 	 */
   784 	private static function compute_theme_vars( $settings ) {
  1225 	protected static function compute_theme_vars( $settings ) {
   785 		$declarations  = array();
  1226 		$declarations  = array();
   786 		$custom_values = _wp_array_get( $settings, array( 'custom' ), array() );
  1227 		$custom_values = _wp_array_get( $settings, array( 'custom' ), array() );
   787 		$css_vars      = self::flatten_tree( $custom_values );
  1228 		$css_vars      = static::flatten_tree( $custom_values );
   788 		foreach ( $css_vars as $key => $value ) {
  1229 		foreach ( $css_vars as $key => $value ) {
   789 			$declarations[] = array(
  1230 			$declarations[] = array(
   790 				'name'  => '--wp--custom--' . $key,
  1231 				'name'  => '--wp--custom--' . $key,
   791 				'value' => $value,
  1232 				'value' => $value,
   792 			);
  1233 			);
   830 	 * @param array  $tree   Input tree to process.
  1271 	 * @param array  $tree   Input tree to process.
   831 	 * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string.
  1272 	 * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string.
   832 	 * @param string $token  Optional. Token to use between levels. Default '--'.
  1273 	 * @param string $token  Optional. Token to use between levels. Default '--'.
   833 	 * @return array The flattened tree.
  1274 	 * @return array The flattened tree.
   834 	 */
  1275 	 */
   835 	private static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
  1276 	protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
   836 		$result = array();
  1277 		$result = array();
   837 		foreach ( $tree as $property => $value ) {
  1278 		foreach ( $tree as $property => $value ) {
   838 			$new_key = $prefix . str_replace(
  1279 			$new_key = $prefix . str_replace(
   839 				'/',
  1280 				'/',
   840 				'-',
  1281 				'-',
   841 				strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $property ) ) // CamelCase to kebab-case.
  1282 				strtolower( _wp_to_kebab_case( $property ) )
   842 			);
  1283 			);
   843 
  1284 
   844 			if ( is_array( $value ) ) {
  1285 			if ( is_array( $value ) ) {
   845 				$new_prefix = $new_key . $token;
  1286 				$new_prefix = $new_key . $token;
   846 				$result     = array_merge(
  1287 				$result     = array_merge(
   847 					$result,
  1288 					$result,
   848 					self::flatten_tree( $value, $new_prefix, $token )
  1289 					static::flatten_tree( $value, $new_prefix, $token )
   849 				);
  1290 				);
   850 			} else {
  1291 			} else {
   851 				$result[ $new_key ] = $value;
  1292 				$result[ $new_key ] = $value;
   852 			}
  1293 			}
   853 		}
  1294 		}
   862 	 *       'name'  => 'property_name',
  1303 	 *       'name'  => 'property_name',
   863 	 *       'value' => 'property_value,
  1304 	 *       'value' => 'property_value,
   864 	 *     )
  1305 	 *     )
   865 	 *
  1306 	 *
   866 	 * @since 5.8.0
  1307 	 * @since 5.8.0
   867 	 *
  1308 	 * @since 5.9.0 Added the `$settings` and `$properties` parameters.
   868 	 * @param array $styles Styles to process.
  1309 	 *
       
  1310 	 * @param array $styles    Styles to process.
       
  1311 	 * @param array $settings  Theme settings.
       
  1312 	 * @param array $properties Properties metadata.
   869 	 * @return array Returns the modified $declarations.
  1313 	 * @return array Returns the modified $declarations.
   870 	 */
  1314 	 */
   871 	private static function compute_style_properties( $styles ) {
  1315 	protected static function compute_style_properties( $styles, $settings = array(), $properties = null ) {
       
  1316 		if ( null === $properties ) {
       
  1317 			$properties = static::PROPERTIES_METADATA;
       
  1318 		}
       
  1319 
   872 		$declarations = array();
  1320 		$declarations = array();
   873 		if ( empty( $styles ) ) {
  1321 		if ( empty( $styles ) ) {
   874 			return $declarations;
  1322 			return $declarations;
   875 		}
  1323 		}
   876 
  1324 
   877 		$properties = array();
  1325 		foreach ( $properties as $css_property => $value_path ) {
   878 		foreach ( self::PROPERTIES_METADATA as $name => $metadata ) {
  1326 			$value = static::get_property_value( $styles, $value_path );
   879 			/*
  1327 
   880 			 * Some properties can be shorthand properties, meaning that
  1328 			// Look up protected properties, keyed by value path.
   881 			 * they contain multiple values instead of a single one.
  1329 			// Skip protected properties that are explicitly set to `null`.
   882 			 * An example of this is the padding property.
  1330 			if ( is_array( $value_path ) ) {
   883 			 */
  1331 				$path_string = implode( '.', $value_path );
   884 			if ( self::has_properties( $metadata ) ) {
  1332 				if (
   885 				foreach ( $metadata['properties'] as $property ) {
  1333 					array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) &&
   886 					$properties[] = array(
  1334 					_wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null
   887 						'name'  => $name . '-' . $property,
  1335 				) {
   888 						'value' => array_merge( $metadata['value'], array( $property ) ),
  1336 					continue;
   889 					);
  1337 				}
   890 				}
  1338 			}
   891 			} else {
  1339 
   892 				$properties[] = array(
  1340 			// Skip if empty and not "0" or value represents array of longhand values.
   893 					'name'  => $name,
  1341 			$has_missing_value = empty( $value ) && ! is_numeric( $value );
   894 					'value' => $metadata['value'],
  1342 			if ( $has_missing_value || is_array( $value ) ) {
   895 				);
       
   896 			}
       
   897 		}
       
   898 
       
   899 		foreach ( $properties as $prop ) {
       
   900 			$value = self::get_property_value( $styles, $prop['value'] );
       
   901 			if ( empty( $value ) ) {
       
   902 				continue;
  1343 				continue;
   903 			}
  1344 			}
   904 
  1345 
   905 			$declarations[] = array(
  1346 			$declarations[] = array(
   906 				'name'  => $prop['name'],
  1347 				'name'  => $css_property,
   907 				'value' => $value,
  1348 				'value' => $value,
   908 			);
  1349 			);
   909 		}
  1350 		}
   910 
  1351 
   911 		return $declarations;
  1352 		return $declarations;
   912 	}
       
   913 
       
   914 	/**
       
   915 	 * Whether the metadata contains a key named properties.
       
   916 	 *
       
   917 	 * @since 5.8.0
       
   918 	 *
       
   919 	 * @param array $metadata Description of the style property.
       
   920 	 * @return bool True if properties exists, false otherwise.
       
   921 	 */
       
   922 	private static function has_properties( $metadata ) {
       
   923 		if ( array_key_exists( 'properties', $metadata ) ) {
       
   924 			return true;
       
   925 		}
       
   926 
       
   927 		return false;
       
   928 	}
  1353 	}
   929 
  1354 
   930 	/**
  1355 	/**
   931 	 * Returns the style property for the given path.
  1356 	 * Returns the style property for the given path.
   932 	 *
  1357 	 *
   933 	 * It also converts CSS Custom Property stored as
  1358 	 * It also converts CSS Custom Property stored as
   934 	 * "var:preset|color|secondary" to the form
  1359 	 * "var:preset|color|secondary" to the form
   935 	 * "--wp--preset--color--secondary".
  1360 	 * "--wp--preset--color--secondary".
   936 	 *
  1361 	 *
   937 	 * @since 5.8.0
  1362 	 * @since 5.8.0
       
  1363 	 * @since 5.9.0 Added support for values of array type, which are returned as is.
   938 	 *
  1364 	 *
   939 	 * @param array $styles Styles subtree.
  1365 	 * @param array $styles Styles subtree.
   940 	 * @param array $path   Which property to process.
  1366 	 * @param array $path   Which property to process.
   941 	 * @return string Style property value.
  1367 	 * @return string|array Style property value.
   942 	 */
  1368 	 */
   943 	private static function get_property_value( $styles, $path ) {
  1369 	protected static function get_property_value( $styles, $path ) {
   944 		$value = _wp_array_get( $styles, $path, '' );
  1370 		$value = _wp_array_get( $styles, $path, '' );
   945 
  1371 
   946 		if ( '' === $value ) {
  1372 		if ( '' === $value || is_array( $value ) ) {
   947 			return $value;
  1373 			return $value;
   948 		}
  1374 		}
   949 
  1375 
   950 		$prefix     = 'var:';
  1376 		$prefix     = 'var:';
   951 		$prefix_len = strlen( $prefix );
  1377 		$prefix_len = strlen( $prefix );
   981 	 *
  1407 	 *
   982 	 * @param array $theme_json The tree to extract setting nodes from.
  1408 	 * @param array $theme_json The tree to extract setting nodes from.
   983 	 * @param array $selectors  List of selectors per block.
  1409 	 * @param array $selectors  List of selectors per block.
   984 	 * @return array
  1410 	 * @return array
   985 	 */
  1411 	 */
   986 	private static function get_setting_nodes( $theme_json, $selectors = array() ) {
  1412 	protected static function get_setting_nodes( $theme_json, $selectors = array() ) {
   987 		$nodes = array();
  1413 		$nodes = array();
   988 		if ( ! isset( $theme_json['settings'] ) ) {
  1414 		if ( ! isset( $theme_json['settings'] ) ) {
   989 			return $nodes;
  1415 			return $nodes;
   990 		}
  1416 		}
   991 
  1417 
   992 		// Top-level.
  1418 		// Top-level.
   993 		$nodes[] = array(
  1419 		$nodes[] = array(
   994 			'path'     => array( 'settings' ),
  1420 			'path'     => array( 'settings' ),
   995 			'selector' => self::ROOT_BLOCK_SELECTOR,
  1421 			'selector' => static::ROOT_BLOCK_SELECTOR,
   996 		);
  1422 		);
   997 
  1423 
   998 		// Calculate paths for blocks.
  1424 		// Calculate paths for blocks.
   999 		if ( ! isset( $theme_json['settings']['blocks'] ) ) {
  1425 		if ( ! isset( $theme_json['settings']['blocks'] ) ) {
  1000 			return $nodes;
  1426 			return $nodes;
  1013 		}
  1439 		}
  1014 
  1440 
  1015 		return $nodes;
  1441 		return $nodes;
  1016 	}
  1442 	}
  1017 
  1443 
  1018 
       
  1019 	/**
  1444 	/**
  1020 	 * Builds metadata for the style nodes, which returns in the form of:
  1445 	 * Builds metadata for the style nodes, which returns in the form of:
  1021 	 *
  1446 	 *
  1022 	 *     [
  1447 	 *     [
  1023 	 *       [
  1448 	 *       [
  1024 	 *         'path'     => [ 'path', 'to', 'some', 'node' ],
  1449 	 *         'path'     => [ 'path', 'to', 'some', 'node' ],
  1025 	 *         'selector' => 'CSS selector for some node'
  1450 	 *         'selector' => 'CSS selector for some node',
       
  1451 	 *         'duotone'  => 'CSS selector for duotone for some node'
  1026 	 *       ],
  1452 	 *       ],
  1027 	 *       [
  1453 	 *       [
  1028 	 *         'path'     => ['path', 'to', 'other', 'node' ],
  1454 	 *         'path'     => ['path', 'to', 'other', 'node' ],
  1029 	 *         'selector' => 'CSS selector for other node'
  1455 	 *         'selector' => 'CSS selector for other node',
       
  1456 	 *         'duotone'  => null
  1030 	 *       ],
  1457 	 *       ],
  1031 	 *     ]
  1458 	 *     ]
  1032 	 *
  1459 	 *
  1033 	 * @since 5.8.0
  1460 	 * @since 5.8.0
  1034 	 *
  1461 	 *
  1035 	 * @param array $theme_json The tree to extract style nodes from.
  1462 	 * @param array $theme_json The tree to extract style nodes from.
  1036 	 * @param array $selectors  List of selectors per block.
  1463 	 * @param array $selectors  List of selectors per block.
  1037 	 * @return array
  1464 	 * @return array
  1038 	 */
  1465 	 */
  1039 	private static function get_style_nodes( $theme_json, $selectors = array() ) {
  1466 	protected static function get_style_nodes( $theme_json, $selectors = array() ) {
  1040 		$nodes = array();
  1467 		$nodes = array();
  1041 		if ( ! isset( $theme_json['styles'] ) ) {
  1468 		if ( ! isset( $theme_json['styles'] ) ) {
  1042 			return $nodes;
  1469 			return $nodes;
  1043 		}
  1470 		}
  1044 
  1471 
  1045 		// Top-level.
  1472 		// Top-level.
  1046 		$nodes[] = array(
  1473 		$nodes[] = array(
  1047 			'path'     => array( 'styles' ),
  1474 			'path'     => array( 'styles' ),
  1048 			'selector' => self::ROOT_BLOCK_SELECTOR,
  1475 			'selector' => static::ROOT_BLOCK_SELECTOR,
  1049 		);
  1476 		);
  1050 
  1477 
  1051 		if ( isset( $theme_json['styles']['elements'] ) ) {
  1478 		if ( isset( $theme_json['styles']['elements'] ) ) {
  1052 			foreach ( $theme_json['styles']['elements'] as $element => $node ) {
  1479 			foreach ( $theme_json['styles']['elements'] as $element => $node ) {
  1053 				$nodes[] = array(
  1480 				$nodes[] = array(
  1054 					'path'     => array( 'styles', 'elements', $element ),
  1481 					'path'     => array( 'styles', 'elements', $element ),
  1055 					'selector' => self::ELEMENTS[ $element ],
  1482 					'selector' => static::ELEMENTS[ $element ],
  1056 				);
  1483 				);
  1057 			}
  1484 			}
  1058 		}
  1485 		}
  1059 
  1486 
  1060 		// Blocks.
  1487 		// Blocks.
  1066 			$selector = null;
  1493 			$selector = null;
  1067 			if ( isset( $selectors[ $name ]['selector'] ) ) {
  1494 			if ( isset( $selectors[ $name ]['selector'] ) ) {
  1068 				$selector = $selectors[ $name ]['selector'];
  1495 				$selector = $selectors[ $name ]['selector'];
  1069 			}
  1496 			}
  1070 
  1497 
       
  1498 			$duotone_selector = null;
       
  1499 			if ( isset( $selectors[ $name ]['duotone'] ) ) {
       
  1500 				$duotone_selector = $selectors[ $name ]['duotone'];
       
  1501 			}
       
  1502 
  1071 			$nodes[] = array(
  1503 			$nodes[] = array(
  1072 				'path'     => array( 'styles', 'blocks', $name ),
  1504 				'path'     => array( 'styles', 'blocks', $name ),
  1073 				'selector' => $selector,
  1505 				'selector' => $selector,
       
  1506 				'duotone'  => $duotone_selector,
  1074 			);
  1507 			);
  1075 
  1508 
  1076 			if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
  1509 			if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
  1077 				foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
  1510 				foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
  1078 					$nodes[] = array(
  1511 					$nodes[] = array(
  1085 
  1518 
  1086 		return $nodes;
  1519 		return $nodes;
  1087 	}
  1520 	}
  1088 
  1521 
  1089 	/**
  1522 	/**
       
  1523 	 * For metadata values that can either be booleans or paths to booleans, gets the value.
       
  1524 	 *
       
  1525 	 * ```php
       
  1526 	 * $data = array(
       
  1527 	 *   'color' => array(
       
  1528 	 *     'defaultPalette' => true
       
  1529 	 *   )
       
  1530 	 * );
       
  1531 	 *
       
  1532 	 * static::get_metadata_boolean( $data, false );
       
  1533 	 * // => false
       
  1534 	 *
       
  1535 	 * static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) );
       
  1536 	 * // => true
       
  1537 	 * ```
       
  1538 	 *
       
  1539 	 * @since 6.0.0
       
  1540 	 *
       
  1541 	 * @param array      $data    The data to inspect.
       
  1542 	 * @param bool|array $path    Boolean or path to a boolean.
       
  1543 	 * @param bool       $default Default value if the referenced path is missing.
       
  1544 	 *                            Default false.
       
  1545 	 * @return bool Value of boolean metadata.
       
  1546 	 */
       
  1547 	protected static function get_metadata_boolean( $data, $path, $default = false ) {
       
  1548 		if ( is_bool( $path ) ) {
       
  1549 			return $path;
       
  1550 		}
       
  1551 
       
  1552 		if ( is_array( $path ) ) {
       
  1553 			$value = _wp_array_get( $data, $path );
       
  1554 			if ( null !== $value ) {
       
  1555 				return $value;
       
  1556 			}
       
  1557 		}
       
  1558 
       
  1559 		return $default;
       
  1560 	}
       
  1561 
       
  1562 	/**
  1090 	 * Merge new incoming data.
  1563 	 * Merge new incoming data.
  1091 	 *
  1564 	 *
  1092 	 * @since 5.8.0
  1565 	 * @since 5.8.0
       
  1566 	 * @since 5.9.0 Duotone preset also has origins.
  1093 	 *
  1567 	 *
  1094 	 * @param WP_Theme_JSON $incoming Data to merge.
  1568 	 * @param WP_Theme_JSON $incoming Data to merge.
  1095 	 */
  1569 	 */
  1096 	public function merge( $incoming ) {
  1570 	public function merge( $incoming ) {
  1097 		$incoming_data    = $incoming->get_raw_data();
  1571 		$incoming_data    = $incoming->get_raw_data();
  1098 		$this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
  1572 		$this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
  1099 
  1573 
  1100 		/*
  1574 		/*
  1101 		 * The array_replace_recursive() algorithm merges at the leaf level.
  1575 		 * The array_replace_recursive algorithm merges at the leaf level,
  1102 		 * For leaf values that are arrays it will use the numeric indexes for replacement.
  1576 		 * but we don't want leaf arrays to be merged, so we overwrite it.
  1103 		 * In those cases, we want to replace the existing with the incoming value, if it exists.
  1577 		 *
       
  1578 		 * For leaf values that are sequential arrays it will use the numeric indexes for replacement.
       
  1579 		 * We rather replace the existing with the incoming value, if it exists.
       
  1580 		 * This is the case of spacing.units.
       
  1581 		 *
       
  1582 		 * For leaf values that are associative arrays it will merge them as expected.
       
  1583 		 * This is also not the behavior we want for the current associative arrays (presets).
       
  1584 		 * We rather replace the existing with the incoming value, if it exists.
       
  1585 		 * This happens, for example, when we merge data from theme.json upon existing
       
  1586 		 * theme supports or when we merge anything coming from the same source twice.
       
  1587 		 * This is the case of color.palette, color.gradients, color.duotone,
       
  1588 		 * typography.fontSizes, or typography.fontFamilies.
       
  1589 		 *
       
  1590 		 * Additionally, for some preset types, we also want to make sure the
       
  1591 		 * values they introduce don't conflict with default values. We do so
       
  1592 		 * by checking the incoming slugs for theme presets and compare them
       
  1593 		 * with the equivalent default presets: if a slug is present as a default
       
  1594 		 * we remove it from the theme presets.
  1104 		 */
  1595 		 */
  1105 		$to_replace   = array();
  1596 		$nodes        = static::get_setting_nodes( $incoming_data );
  1106 		$to_replace[] = array( 'spacing', 'units' );
  1597 		$slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) );
  1107 		$to_replace[] = array( 'color', 'duotone' );
  1598 		foreach ( $nodes as $node ) {
  1108 		foreach ( self::VALID_ORIGINS as $origin ) {
  1599 			$slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] );
  1109 			$to_replace[] = array( 'color', 'palette', $origin );
  1600 			$slugs      = array_merge_recursive( $slugs_global, $slugs_node );
  1110 			$to_replace[] = array( 'color', 'gradients', $origin );
  1601 
  1111 			$to_replace[] = array( 'typography', 'fontSizes', $origin );
  1602 			// Replace the spacing.units.
  1112 			$to_replace[] = array( 'typography', 'fontFamilies', $origin );
  1603 			$path    = array_merge( $node['path'], array( 'spacing', 'units' ) );
  1113 		}
  1604 			$content = _wp_array_get( $incoming_data, $path, null );
  1114 
  1605 			if ( isset( $content ) ) {
  1115 		$nodes = self::get_setting_nodes( $this->theme_json );
  1606 				_wp_array_set( $this->theme_json, $path, $content );
  1116 		foreach ( $nodes as $metadata ) {
  1607 			}
  1117 			foreach ( $to_replace as $property_path ) {
  1608 
  1118 				$path = array_merge( $metadata['path'], $property_path );
  1609 			// Replace the presets.
  1119 				$node = _wp_array_get( $incoming_data, $path, null );
  1610 			foreach ( static::PRESETS_METADATA as $preset ) {
  1120 				if ( isset( $node ) ) {
  1611 				$override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true );
  1121 					_wp_array_set( $this->theme_json, $path, $node );
  1612 
  1122 				}
  1613 				foreach ( static::VALID_ORIGINS as $origin ) {
  1123 			}
  1614 					$base_path = array_merge( $node['path'], $preset['path'] );
  1124 		}
  1615 					$path      = array_merge( $base_path, array( $origin ) );
       
  1616 					$content   = _wp_array_get( $incoming_data, $path, null );
       
  1617 					if ( ! isset( $content ) ) {
       
  1618 						continue;
       
  1619 					}
       
  1620 
       
  1621 					if ( 'theme' === $origin && $preset['use_default_names'] ) {
       
  1622 						foreach ( $content as &$item ) {
       
  1623 							if ( ! array_key_exists( 'name', $item ) ) {
       
  1624 								$name = static::get_name_from_defaults( $item['slug'], $base_path );
       
  1625 								if ( null !== $name ) {
       
  1626 									$item['name'] = $name;
       
  1627 								}
       
  1628 							}
       
  1629 						}
       
  1630 					}
       
  1631 
       
  1632 					if (
       
  1633 						( 'theme' !== $origin ) ||
       
  1634 						( 'theme' === $origin && $override_preset )
       
  1635 					) {
       
  1636 						_wp_array_set( $this->theme_json, $path, $content );
       
  1637 					} else {
       
  1638 						$slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() );
       
  1639 						$content          = static::filter_slugs( $content, $slugs_for_preset );
       
  1640 						_wp_array_set( $this->theme_json, $path, $content );
       
  1641 					}
       
  1642 				}
       
  1643 			}
       
  1644 		}
       
  1645 	}
       
  1646 
       
  1647 	/**
       
  1648 	 * Converts all filter (duotone) presets into SVGs.
       
  1649 	 *
       
  1650 	 * @since 5.9.1
       
  1651 	 *
       
  1652 	 * @param array $origins List of origins to process.
       
  1653 	 * @return string SVG filters.
       
  1654 	 */
       
  1655 	public function get_svg_filters( $origins ) {
       
  1656 		$blocks_metadata = static::get_blocks_metadata();
       
  1657 		$setting_nodes   = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
       
  1658 
       
  1659 		$filters = '';
       
  1660 		foreach ( $setting_nodes as $metadata ) {
       
  1661 			$node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
       
  1662 			if ( empty( $node['color']['duotone'] ) ) {
       
  1663 				continue;
       
  1664 			}
       
  1665 
       
  1666 			$duotone_presets = $node['color']['duotone'];
       
  1667 
       
  1668 			foreach ( $origins as $origin ) {
       
  1669 				if ( ! isset( $duotone_presets[ $origin ] ) ) {
       
  1670 					continue;
       
  1671 				}
       
  1672 				foreach ( $duotone_presets[ $origin ] as $duotone_preset ) {
       
  1673 					$filters .= wp_get_duotone_filter_svg( $duotone_preset );
       
  1674 				}
       
  1675 			}
       
  1676 		}
       
  1677 
       
  1678 		return $filters;
       
  1679 	}
       
  1680 
       
  1681 	/**
       
  1682 	 * Returns whether a presets should be overridden or not.
       
  1683 	 *
       
  1684 	 * @since 5.9.0
       
  1685 	 * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead.
       
  1686 	 *
       
  1687 	 * @param array      $theme_json The theme.json like structure to inspect.
       
  1688 	 * @param array      $path       Path to inspect.
       
  1689 	 * @param bool|array $override   Data to compute whether to override the preset.
       
  1690 	 * @return boolean
       
  1691 	 */
       
  1692 	protected static function should_override_preset( $theme_json, $path, $override ) {
       
  1693 		_deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' );
       
  1694 
       
  1695 		if ( is_bool( $override ) ) {
       
  1696 			return $override;
       
  1697 		}
       
  1698 
       
  1699 		/*
       
  1700 		 * The relationship between whether to override the defaults
       
  1701 		 * and whether the defaults are enabled is inverse:
       
  1702 		 *
       
  1703 		 * - If defaults are enabled  => theme presets should not be overridden
       
  1704 		 * - If defaults are disabled => theme presets should be overridden
       
  1705 		 *
       
  1706 		 * For example, a theme sets defaultPalette to false,
       
  1707 		 * making the default palette hidden from the user.
       
  1708 		 * In that case, we want all the theme presets to be present,
       
  1709 		 * so they should override the defaults.
       
  1710 		 */
       
  1711 		if ( is_array( $override ) ) {
       
  1712 			$value = _wp_array_get( $theme_json, array_merge( $path, $override ) );
       
  1713 			if ( isset( $value ) ) {
       
  1714 				return ! $value;
       
  1715 			}
       
  1716 
       
  1717 			// Search the top-level key if none was found for this node.
       
  1718 			$value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) );
       
  1719 			if ( isset( $value ) ) {
       
  1720 				return ! $value;
       
  1721 			}
       
  1722 
       
  1723 			return true;
       
  1724 		}
       
  1725 	}
       
  1726 
       
  1727 	/**
       
  1728 	 * 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.
       
  1730 	 *
       
  1731 	 * For example:
       
  1732 	 *
       
  1733 	 *  array(
       
  1734 	 *   'color' => array(
       
  1735 	 *     'palette'   => array( 'slug-1', 'slug-2' ),
       
  1736 	 *     'gradients' => array( 'slug-3', 'slug-4' ),
       
  1737 	 *   ),
       
  1738 	 * )
       
  1739 	 *
       
  1740 	 * @since 5.9.0
       
  1741 	 *
       
  1742 	 * @param array $data      A theme.json like structure.
       
  1743 	 * @param array $node_path The path to inspect. It's 'settings' by default.
       
  1744 	 * @return array
       
  1745 	 */
       
  1746 	protected static function get_default_slugs( $data, $node_path ) {
       
  1747 		$slugs = array();
       
  1748 
       
  1749 		foreach ( static::PRESETS_METADATA as $metadata ) {
       
  1750 			$path   = array_merge( $node_path, $metadata['path'], array( 'default' ) );
       
  1751 			$preset = _wp_array_get( $data, $path, null );
       
  1752 			if ( ! isset( $preset ) ) {
       
  1753 				continue;
       
  1754 			}
       
  1755 
       
  1756 			$slugs_for_preset = array();
       
  1757 			$slugs_for_preset = array_map(
       
  1758 				static function( $value ) {
       
  1759 					return isset( $value['slug'] ) ? $value['slug'] : null;
       
  1760 				},
       
  1761 				$preset
       
  1762 			);
       
  1763 			_wp_array_set( $slugs, $metadata['path'], $slugs_for_preset );
       
  1764 		}
       
  1765 
       
  1766 		return $slugs;
       
  1767 	}
       
  1768 
       
  1769 	/**
       
  1770 	 * Get a `default`'s preset name by a provided slug.
       
  1771 	 *
       
  1772 	 * @since 5.9.0
       
  1773 	 *
       
  1774 	 * @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.
       
  1776 	 * @return string|null
       
  1777 	 */
       
  1778 	protected function get_name_from_defaults( $slug, $base_path ) {
       
  1779 		$path            = array_merge( $base_path, array( 'default' ) );
       
  1780 		$default_content = _wp_array_get( $this->theme_json, $path, null );
       
  1781 		if ( ! $default_content ) {
       
  1782 			return null;
       
  1783 		}
       
  1784 		foreach ( $default_content as $item ) {
       
  1785 			if ( $slug === $item['slug'] ) {
       
  1786 				return $item['name'];
       
  1787 			}
       
  1788 		}
       
  1789 		return null;
       
  1790 	}
       
  1791 
       
  1792 	/**
       
  1793 	 * Removes the preset values whose slug is equal to any of given slugs.
       
  1794 	 *
       
  1795 	 * @since 5.9.0
       
  1796 	 *
       
  1797 	 * @param array $node  The node with the presets to validate.
       
  1798 	 * @param array $slugs The slugs that should not be overridden.
       
  1799 	 * @return array The new node.
       
  1800 	 */
       
  1801 	protected static function filter_slugs( $node, $slugs ) {
       
  1802 		if ( empty( $slugs ) ) {
       
  1803 			return $node;
       
  1804 		}
       
  1805 
       
  1806 		$new_node = array();
       
  1807 		foreach ( $node as $value ) {
       
  1808 			if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) {
       
  1809 				$new_node[] = $value;
       
  1810 			}
       
  1811 		}
       
  1812 
       
  1813 		return $new_node;
       
  1814 	}
       
  1815 
       
  1816 	/**
       
  1817 	 * Removes insecure data from theme.json.
       
  1818 	 *
       
  1819 	 * @since 5.9.0
       
  1820 	 *
       
  1821 	 * @param array $theme_json Structure to sanitize.
       
  1822 	 * @return array Sanitized structure.
       
  1823 	 */
       
  1824 	public static function remove_insecure_properties( $theme_json ) {
       
  1825 		$sanitized = array();
       
  1826 
       
  1827 		$theme_json = WP_Theme_JSON_Schema::migrate( $theme_json );
       
  1828 
       
  1829 		$valid_block_names   = array_keys( static::get_blocks_metadata() );
       
  1830 		$valid_element_names = array_keys( static::ELEMENTS );
       
  1831 		$theme_json          = static::sanitize( $theme_json, $valid_block_names, $valid_element_names );
       
  1832 
       
  1833 		$blocks_metadata = static::get_blocks_metadata();
       
  1834 		$style_nodes     = static::get_style_nodes( $theme_json, $blocks_metadata );
       
  1835 		foreach ( $style_nodes as $metadata ) {
       
  1836 			$input = _wp_array_get( $theme_json, $metadata['path'], array() );
       
  1837 			if ( empty( $input ) ) {
       
  1838 				continue;
       
  1839 			}
       
  1840 
       
  1841 			$output = static::remove_insecure_styles( $input );
       
  1842 			if ( ! empty( $output ) ) {
       
  1843 				_wp_array_set( $sanitized, $metadata['path'], $output );
       
  1844 			}
       
  1845 		}
       
  1846 
       
  1847 		$setting_nodes = static::get_setting_nodes( $theme_json );
       
  1848 		foreach ( $setting_nodes as $metadata ) {
       
  1849 			$input = _wp_array_get( $theme_json, $metadata['path'], array() );
       
  1850 			if ( empty( $input ) ) {
       
  1851 				continue;
       
  1852 			}
       
  1853 
       
  1854 			$output = static::remove_insecure_settings( $input );
       
  1855 			if ( ! empty( $output ) ) {
       
  1856 				_wp_array_set( $sanitized, $metadata['path'], $output );
       
  1857 			}
       
  1858 		}
       
  1859 
       
  1860 		if ( empty( $sanitized['styles'] ) ) {
       
  1861 			unset( $theme_json['styles'] );
       
  1862 		} else {
       
  1863 			$theme_json['styles'] = $sanitized['styles'];
       
  1864 		}
       
  1865 
       
  1866 		if ( empty( $sanitized['settings'] ) ) {
       
  1867 			unset( $theme_json['settings'] );
       
  1868 		} else {
       
  1869 			$theme_json['settings'] = $sanitized['settings'];
       
  1870 		}
       
  1871 
       
  1872 		return $theme_json;
       
  1873 	}
       
  1874 
       
  1875 	/**
       
  1876 	 * Processes a setting node and returns the same node
       
  1877 	 * without the insecure settings.
       
  1878 	 *
       
  1879 	 * @since 5.9.0
       
  1880 	 *
       
  1881 	 * @param array $input Node to process.
       
  1882 	 * @return array
       
  1883 	 */
       
  1884 	protected static function remove_insecure_settings( $input ) {
       
  1885 		$output = array();
       
  1886 		foreach ( static::PRESETS_METADATA as $preset_metadata ) {
       
  1887 			foreach ( static::VALID_ORIGINS as $origin ) {
       
  1888 				$path_with_origin = array_merge( $preset_metadata['path'], array( $origin ) );
       
  1889 				$presets          = _wp_array_get( $input, $path_with_origin, null );
       
  1890 				if ( null === $presets ) {
       
  1891 					continue;
       
  1892 				}
       
  1893 
       
  1894 				$escaped_preset = array();
       
  1895 				foreach ( $presets as $preset ) {
       
  1896 					if (
       
  1897 						esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] &&
       
  1898 						sanitize_html_class( $preset['slug'] ) === $preset['slug']
       
  1899 					) {
       
  1900 						$value = null;
       
  1901 						if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
       
  1902 							$value = $preset[ $preset_metadata['value_key'] ];
       
  1903 						} elseif (
       
  1904 							isset( $preset_metadata['value_func'] ) &&
       
  1905 							is_callable( $preset_metadata['value_func'] )
       
  1906 						) {
       
  1907 							$value = call_user_func( $preset_metadata['value_func'], $preset );
       
  1908 						}
       
  1909 
       
  1910 						$preset_is_valid = true;
       
  1911 						foreach ( $preset_metadata['properties'] as $property ) {
       
  1912 							if ( ! static::is_safe_css_declaration( $property, $value ) ) {
       
  1913 								$preset_is_valid = false;
       
  1914 								break;
       
  1915 							}
       
  1916 						}
       
  1917 
       
  1918 						if ( $preset_is_valid ) {
       
  1919 							$escaped_preset[] = $preset;
       
  1920 						}
       
  1921 					}
       
  1922 				}
       
  1923 
       
  1924 				if ( ! empty( $escaped_preset ) ) {
       
  1925 					_wp_array_set( $output, $path_with_origin, $escaped_preset );
       
  1926 				}
       
  1927 			}
       
  1928 		}
       
  1929 		return $output;
       
  1930 	}
       
  1931 
       
  1932 	/**
       
  1933 	 * Processes a style node and returns the same node
       
  1934 	 * without the insecure styles.
       
  1935 	 *
       
  1936 	 * @since 5.9.0
       
  1937 	 *
       
  1938 	 * @param array $input Node to process.
       
  1939 	 * @return array
       
  1940 	 */
       
  1941 	protected static function remove_insecure_styles( $input ) {
       
  1942 		$output       = array();
       
  1943 		$declarations = static::compute_style_properties( $input );
       
  1944 
       
  1945 		foreach ( $declarations as $declaration ) {
       
  1946 			if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) {
       
  1947 				$path = static::PROPERTIES_METADATA[ $declaration['name'] ];
       
  1948 
       
  1949 				// Check the value isn't an array before adding so as to not
       
  1950 				// double up shorthand and longhand styles.
       
  1951 				$value = _wp_array_get( $input, $path, array() );
       
  1952 				if ( ! is_array( $value ) ) {
       
  1953 					_wp_array_set( $output, $path, $value );
       
  1954 				}
       
  1955 			}
       
  1956 		}
       
  1957 		return $output;
       
  1958 	}
       
  1959 
       
  1960 	/**
       
  1961 	 * Checks that a declaration provided by the user is safe.
       
  1962 	 *
       
  1963 	 * @since 5.9.0
       
  1964 	 *
       
  1965 	 * @param string $property_name  Property name in a CSS declaration, i.e. the `color` in `color: red`.
       
  1966 	 * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`.
       
  1967 	 * @return bool
       
  1968 	 */
       
  1969 	protected static function is_safe_css_declaration( $property_name, $property_value ) {
       
  1970 		$style_to_validate = $property_name . ': ' . $property_value;
       
  1971 		$filtered          = esc_html( safecss_filter_attr( $style_to_validate ) );
       
  1972 		return ! empty( trim( $filtered ) );
  1125 	}
  1973 	}
  1126 
  1974 
  1127 	/**
  1975 	/**
  1128 	 * Returns the raw data.
  1976 	 * Returns the raw data.
  1129 	 *
  1977 	 *
  1144 	 * @param array $settings Existing editor settings.
  1992 	 * @param array $settings Existing editor settings.
  1145 	 * @return array Config that adheres to the theme.json schema.
  1993 	 * @return array Config that adheres to the theme.json schema.
  1146 	 */
  1994 	 */
  1147 	public static function get_from_editor_settings( $settings ) {
  1995 	public static function get_from_editor_settings( $settings ) {
  1148 		$theme_settings = array(
  1996 		$theme_settings = array(
  1149 			'version'  => self::LATEST_SCHEMA,
  1997 			'version'  => static::LATEST_SCHEMA,
  1150 			'settings' => array(),
  1998 			'settings' => array(),
  1151 		);
  1999 		);
  1152 
  2000 
  1153 		// Deprecated theme supports.
  2001 		// Deprecated theme supports.
  1154 		if ( isset( $settings['disableCustomColors'] ) ) {
  2002 		if ( isset( $settings['disableCustomColors'] ) ) {
  1174 
  2022 
  1175 		if ( isset( $settings['enableCustomLineHeight'] ) ) {
  2023 		if ( isset( $settings['enableCustomLineHeight'] ) ) {
  1176 			if ( ! isset( $theme_settings['settings']['typography'] ) ) {
  2024 			if ( ! isset( $theme_settings['settings']['typography'] ) ) {
  1177 				$theme_settings['settings']['typography'] = array();
  2025 				$theme_settings['settings']['typography'] = array();
  1178 			}
  2026 			}
  1179 			$theme_settings['settings']['typography']['customLineHeight'] = $settings['enableCustomLineHeight'];
  2027 			$theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight'];
  1180 		}
  2028 		}
  1181 
  2029 
  1182 		if ( isset( $settings['enableCustomUnits'] ) ) {
  2030 		if ( isset( $settings['enableCustomUnits'] ) ) {
  1183 			if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
  2031 			if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
  1184 				$theme_settings['settings']['spacing'] = array();
  2032 				$theme_settings['settings']['spacing'] = array();
  1218 
  2066 
  1219 		if ( isset( $settings['enableCustomSpacing'] ) ) {
  2067 		if ( isset( $settings['enableCustomSpacing'] ) ) {
  1220 			if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
  2068 			if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
  1221 				$theme_settings['settings']['spacing'] = array();
  2069 				$theme_settings['settings']['spacing'] = array();
  1222 			}
  2070 			}
  1223 			$theme_settings['settings']['spacing']['customPadding'] = $settings['enableCustomSpacing'];
  2071 			$theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing'];
  1224 		}
       
  1225 
       
  1226 		// Things that didn't land in core yet, so didn't have a setting assigned.
       
  1227 		if ( current( (array) get_theme_support( 'experimental-link-color' ) ) ) {
       
  1228 			if ( ! isset( $theme_settings['settings']['color'] ) ) {
       
  1229 				$theme_settings['settings']['color'] = array();
       
  1230 			}
       
  1231 			$theme_settings['settings']['color']['link'] = true;
       
  1232 		}
  2072 		}
  1233 
  2073 
  1234 		return $theme_settings;
  2074 		return $theme_settings;
  1235 	}
  2075 	}
  1236 
  2076 
       
  2077 	/**
       
  2078 	 * Returns the current theme's wanted patterns(slugs) to be
       
  2079 	 * registered from Pattern Directory.
       
  2080 	 *
       
  2081 	 * @since 6.0.0
       
  2082 	 *
       
  2083 	 * @return string[]
       
  2084 	 */
       
  2085 	public function get_patterns() {
       
  2086 		if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) {
       
  2087 			return $this->theme_json['patterns'];
       
  2088 		}
       
  2089 		return array();
       
  2090 	}
       
  2091 
       
  2092 	/**
       
  2093 	 * Returns a valid theme.json as provided by a theme.
       
  2094 	 *
       
  2095 	 * Unlike get_raw_data() this returns the presets flattened, as provided by a theme.
       
  2096 	 * This also uses appearanceTools instead of their opt-ins if all of them are true.
       
  2097 	 *
       
  2098 	 * @since 6.0.0
       
  2099 	 *
       
  2100 	 * @return array
       
  2101 	 */
       
  2102 	public function get_data() {
       
  2103 		$output = $this->theme_json;
       
  2104 		$nodes  = static::get_setting_nodes( $output );
       
  2105 
       
  2106 		/**
       
  2107 		 * Flatten the theme & custom origins into a single one.
       
  2108 		 *
       
  2109 		 * For example, the following:
       
  2110 		 *
       
  2111 		 * {
       
  2112 		 *   "settings": {
       
  2113 		 *     "color": {
       
  2114 		 *       "palette": {
       
  2115 		 *         "theme": [ {} ],
       
  2116 		 *         "custom": [ {} ]
       
  2117 		 *       }
       
  2118 		 *     }
       
  2119 		 *   }
       
  2120 		 * }
       
  2121 		 *
       
  2122 		 * will be converted to:
       
  2123 		 *
       
  2124 		 * {
       
  2125 		 *   "settings": {
       
  2126 		 *     "color": {
       
  2127 		 *       "palette": [ {} ]
       
  2128 		 *     }
       
  2129 		 *   }
       
  2130 		 * }
       
  2131 		 */
       
  2132 		foreach ( $nodes as $node ) {
       
  2133 			foreach ( static::PRESETS_METADATA as $preset_metadata ) {
       
  2134 				$path   = array_merge( $node['path'], $preset_metadata['path'] );
       
  2135 				$preset = _wp_array_get( $output, $path, null );
       
  2136 				if ( null === $preset ) {
       
  2137 					continue;
       
  2138 				}
       
  2139 
       
  2140 				$items = array();
       
  2141 				if ( isset( $preset['theme'] ) ) {
       
  2142 					foreach ( $preset['theme'] as $item ) {
       
  2143 						$slug = $item['slug'];
       
  2144 						unset( $item['slug'] );
       
  2145 						$items[ $slug ] = $item;
       
  2146 					}
       
  2147 				}
       
  2148 				if ( isset( $preset['custom'] ) ) {
       
  2149 					foreach ( $preset['custom'] as $item ) {
       
  2150 						$slug = $item['slug'];
       
  2151 						unset( $item['slug'] );
       
  2152 						$items[ $slug ] = $item;
       
  2153 					}
       
  2154 				}
       
  2155 				$flattened_preset = array();
       
  2156 				foreach ( $items as $slug => $value ) {
       
  2157 					$flattened_preset[] = array_merge( array( 'slug' => $slug ), $value );
       
  2158 				}
       
  2159 				_wp_array_set( $output, $path, $flattened_preset );
       
  2160 			}
       
  2161 		}
       
  2162 
       
  2163 		// If all of the static::APPEARANCE_TOOLS_OPT_INS are true,
       
  2164 		// this code unsets them and sets 'appearanceTools' instead.
       
  2165 		foreach ( $nodes as $node ) {
       
  2166 			$all_opt_ins_are_set = true;
       
  2167 			foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
       
  2168 				$full_path = array_merge( $node['path'], $opt_in_path );
       
  2169 				// Use "unset prop" as a marker instead of "null" because
       
  2170 				// "null" can be a valid value for some props (e.g. blockGap).
       
  2171 				$opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
       
  2172 				if ( 'unset prop' === $opt_in_value ) {
       
  2173 					$all_opt_ins_are_set = false;
       
  2174 					break;
       
  2175 				}
       
  2176 			}
       
  2177 
       
  2178 			if ( $all_opt_ins_are_set ) {
       
  2179 				_wp_array_set( $output, array_merge( $node['path'], array( 'appearanceTools' ) ), true );
       
  2180 				foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
       
  2181 					$full_path = array_merge( $node['path'], $opt_in_path );
       
  2182 					// Use "unset prop" as a marker instead of "null" because
       
  2183 					// "null" can be a valid value for some props (e.g. blockGap).
       
  2184 					$opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
       
  2185 					if ( true !== $opt_in_value ) {
       
  2186 						continue;
       
  2187 					}
       
  2188 
       
  2189 					// The following could be improved to be path independent.
       
  2190 					// At the moment it relies on a couple of assumptions:
       
  2191 					//
       
  2192 					// - all opt-ins having a path of size 2.
       
  2193 					// - there's two sources of settings: the top-level and the block-level.
       
  2194 					if (
       
  2195 						( 1 === count( $node['path'] ) ) &&
       
  2196 						( 'settings' === $node['path'][0] )
       
  2197 					) {
       
  2198 						// Top-level settings.
       
  2199 						unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] );
       
  2200 						if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) {
       
  2201 							unset( $output['settings'][ $opt_in_path[0] ] );
       
  2202 						}
       
  2203 					} elseif (
       
  2204 						( 3 === count( $node['path'] ) ) &&
       
  2205 						( 'settings' === $node['path'][0] ) &&
       
  2206 						( 'blocks' === $node['path'][1] )
       
  2207 					) {
       
  2208 						// Block-level settings.
       
  2209 						$block_name = $node['path'][2];
       
  2210 						unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] );
       
  2211 						if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) {
       
  2212 							unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] );
       
  2213 						}
       
  2214 					}
       
  2215 				}
       
  2216 			}
       
  2217 		}
       
  2218 
       
  2219 		wp_recursive_ksort( $output );
       
  2220 
       
  2221 		return $output;
       
  2222 	}
       
  2223 
  1237 }
  2224 }