[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> class-wp-theme-json.php (source)

   1  <?php
   2  /**
   3   * WP_Theme_JSON class
   4   *
   5   * @package WordPress
   6   * @subpackage Theme
   7   * @since 5.8.0
   8   */
   9  
  10  /**
  11   * Class that encapsulates the processing of structures that adhere to the theme.json spec.
  12   *
  13   * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes).
  14   * This is a low-level API that may need to do breaking changes. Please,
  15   * use get_global_settings, get_global_styles, and get_global_stylesheet instead.
  16   *
  17   * @access private
  18   */
  19  #[AllowDynamicProperties]
  20  class WP_Theme_JSON {
  21  
  22      /**
  23       * Container of data in theme.json format.
  24       *
  25       * @since 5.8.0
  26       * @var array
  27       */
  28      protected $theme_json = null;
  29  
  30      /**
  31       * Holds block metadata extracted from block.json
  32       * to be shared among all instances so we don't
  33       * process it twice.
  34       *
  35       * @since 5.8.0
  36       * @since 6.1.0 Initialize as an empty array.
  37       * @var array
  38       */
  39      protected static $blocks_metadata = array();
  40  
  41      /**
  42       * The CSS selector for the top-level styles.
  43       *
  44       * @since 5.8.0
  45       * @var string
  46       */
  47      const ROOT_BLOCK_SELECTOR = 'body';
  48  
  49      /**
  50       * The sources of data this object can represent.
  51       *
  52       * @since 5.8.0
  53       * @since 6.1.0 Added 'blocks'.
  54       * @var string[]
  55       */
  56      const VALID_ORIGINS = array(
  57          'default',
  58          'blocks',
  59          'theme',
  60          'custom',
  61      );
  62  
  63      /**
  64       * Presets are a set of values that serve
  65       * to bootstrap some styles: colors, font sizes, etc.
  66       *
  67       * They are a unkeyed array of values such as:
  68       *
  69       *     array(
  70       *       array(
  71       *         'slug'      => 'unique-name-within-the-set',
  72       *         'name'      => 'Name for the UI',
  73       *         <value_key> => 'value'
  74       *       ),
  75       *     )
  76       *
  77       * This contains the necessary metadata to process them:
  78       *
  79       * - path             => Where to find the preset within the settings section.
  80       * - prevent_override => Disables override of default presets by theme presets.
  81       *                       The relationship between whether to override the defaults
  82       *                       and whether the defaults are enabled is inverse:
  83       *                         - If defaults are enabled  => theme presets should not be overriden
  84       *                         - If defaults are disabled => theme presets should be overriden
  85       *                       For example, a theme sets defaultPalette to false,
  86       *                       making the default palette hidden from the user.
  87       *                       In that case, we want all the theme presets to be present,
  88       *                       so they should override the defaults by setting this false.
  89       * - use_default_names => whether to use the default names
  90       * - value_key        => the key that represents the value
  91       * - value_func       => optionally, instead of value_key, a function to generate
  92       *                       the value that takes a preset as an argument
  93       *                       (either value_key or value_func should be present)
  94       * - css_vars         => template string to use in generating the CSS Custom Property.
  95       *                       Example output: "--wp--preset--duotone--blue: <value>" will generate as many CSS Custom Properties as presets defined
  96       *                       substituting the $slug for the slug's value for each preset value.
  97       * - classes          => array containing a structure with the classes to
  98       *                       generate for the presets, where for each array item
  99       *                       the key is the class name and the value the property name.
 100       *                       The "$slug" substring will be replaced by the slug of each preset.
 101       *                       For example:
 102       *                       'classes' => array(
 103       *                         '.has-$slug-color'            => 'color',
 104       *                         '.has-$slug-background-color' => 'background-color',
 105       *                         '.has-$slug-border-color'     => 'border-color',
 106       *                       )
 107       * - properties       => array of CSS properties to be used by kses to
 108       *                       validate the content of each preset
 109       *                       by means of the remove_insecure_properties method.
 110       *
 111       * @since 5.8.0
 112       * @since 5.9.0 Added the `color.duotone` and `typography.fontFamilies` presets,
 113       *              `use_default_names` preset key, and simplified the metadata structure.
 114       * @since 6.0.0 Replaced `override` with `prevent_override` and updated the
 115       *              `prevent_override` value for `color.duotone` to use `color.defaultDuotone`.
 116       * @since 6.2.0 Added 'shadow' presets.
 117       * @var array
 118       */
 119      const PRESETS_METADATA = array(
 120          array(
 121              'path'              => array( 'color', 'palette' ),
 122              'prevent_override'  => array( 'color', 'defaultPalette' ),
 123              'use_default_names' => false,
 124              'value_key'         => 'color',
 125              'css_vars'          => '--wp--preset--color--$slug',
 126              'classes'           => array(
 127                  '.has-$slug-color'            => 'color',
 128                  '.has-$slug-background-color' => 'background-color',
 129                  '.has-$slug-border-color'     => 'border-color',
 130              ),
 131              'properties'        => array( 'color', 'background-color', 'border-color' ),
 132          ),
 133          array(
 134              'path'              => array( 'color', 'gradients' ),
 135              'prevent_override'  => array( 'color', 'defaultGradients' ),
 136              'use_default_names' => false,
 137              'value_key'         => 'gradient',
 138              'css_vars'          => '--wp--preset--gradient--$slug',
 139              'classes'           => array( '.has-$slug-gradient-background' => 'background' ),
 140              'properties'        => array( 'background' ),
 141          ),
 142          array(
 143              'path'              => array( 'color', 'duotone' ),
 144              'prevent_override'  => array( 'color', 'defaultDuotone' ),
 145              'use_default_names' => false,
 146              'value_func'        => 'wp_get_duotone_filter_property',
 147              'css_vars'          => '--wp--preset--duotone--$slug',
 148              'classes'           => array(),
 149              'properties'        => array( 'filter' ),
 150          ),
 151          array(
 152              'path'              => array( 'typography', 'fontSizes' ),
 153              'prevent_override'  => false,
 154              'use_default_names' => true,
 155              'value_func'        => 'wp_get_typography_font_size_value',
 156              'css_vars'          => '--wp--preset--font-size--$slug',
 157              'classes'           => array( '.has-$slug-font-size' => 'font-size' ),
 158              'properties'        => array( 'font-size' ),
 159          ),
 160          array(
 161              'path'              => array( 'typography', 'fontFamilies' ),
 162              'prevent_override'  => false,
 163              'use_default_names' => false,
 164              'value_key'         => 'fontFamily',
 165              'css_vars'          => '--wp--preset--font-family--$slug',
 166              'classes'           => array( '.has-$slug-font-family' => 'font-family' ),
 167              'properties'        => array( 'font-family' ),
 168          ),
 169          array(
 170              'path'              => array( 'spacing', 'spacingSizes' ),
 171              'prevent_override'  => false,
 172              'use_default_names' => true,
 173              'value_key'         => 'size',
 174              'css_vars'          => '--wp--preset--spacing--$slug',
 175              'classes'           => array(),
 176              'properties'        => array( 'padding', 'margin' ),
 177          ),
 178          array(
 179              'path'              => array( 'shadow', 'presets' ),
 180              'prevent_override'  => array( 'shadow', 'defaultPresets' ),
 181              'use_default_names' => false,
 182              'value_key'         => 'shadow',
 183              'css_vars'          => '--wp--preset--shadow--$slug',
 184              'classes'           => array(),
 185              'properties'        => array( 'box-shadow' ),
 186          ),
 187      );
 188  
 189      /**
 190       * Metadata for style properties.
 191       *
 192       * Each element is a direct mapping from the CSS property name to the
 193       * path to the value in theme.json & block attributes.
 194       *
 195       * @since 5.8.0
 196       * @since 5.9.0 Added the `border-*`, `font-family`, `font-style`, `font-weight`,
 197       *              `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`,
 198       *              `text-decoration`, `text-transform`, and `filter` properties,
 199       *              simplified the metadata structure.
 200       * @since 6.1.0 Added the `border-*-color`, `border-*-width`, `border-*-style`,
 201       *              `--wp--style--root--padding-*`, and `box-shadow` properties,
 202       *              removed the `--wp--style--block-gap` property.
 203       * @since 6.2.0 Added `outline-*`, and `min-height` properties.
 204       *
 205       * @var array
 206       */
 207      const PROPERTIES_METADATA = array(
 208          'background'                        => array( 'color', 'gradient' ),
 209          'background-color'                  => array( 'color', 'background' ),
 210          'border-radius'                     => array( 'border', 'radius' ),
 211          'border-top-left-radius'            => array( 'border', 'radius', 'topLeft' ),
 212          'border-top-right-radius'           => array( 'border', 'radius', 'topRight' ),
 213          'border-bottom-left-radius'         => array( 'border', 'radius', 'bottomLeft' ),
 214          'border-bottom-right-radius'        => array( 'border', 'radius', 'bottomRight' ),
 215          'border-color'                      => array( 'border', 'color' ),
 216          'border-width'                      => array( 'border', 'width' ),
 217          'border-style'                      => array( 'border', 'style' ),
 218          'border-top-color'                  => array( 'border', 'top', 'color' ),
 219          'border-top-width'                  => array( 'border', 'top', 'width' ),
 220          'border-top-style'                  => array( 'border', 'top', 'style' ),
 221          'border-right-color'                => array( 'border', 'right', 'color' ),
 222          'border-right-width'                => array( 'border', 'right', 'width' ),
 223          'border-right-style'                => array( 'border', 'right', 'style' ),
 224          'border-bottom-color'               => array( 'border', 'bottom', 'color' ),
 225          'border-bottom-width'               => array( 'border', 'bottom', 'width' ),
 226          'border-bottom-style'               => array( 'border', 'bottom', 'style' ),
 227          'border-left-color'                 => array( 'border', 'left', 'color' ),
 228          'border-left-width'                 => array( 'border', 'left', 'width' ),
 229          'border-left-style'                 => array( 'border', 'left', 'style' ),
 230          'color'                             => array( 'color', 'text' ),
 231          'font-family'                       => array( 'typography', 'fontFamily' ),
 232          'font-size'                         => array( 'typography', 'fontSize' ),
 233          'font-style'                        => array( 'typography', 'fontStyle' ),
 234          'font-weight'                       => array( 'typography', 'fontWeight' ),
 235          'letter-spacing'                    => array( 'typography', 'letterSpacing' ),
 236          'line-height'                       => array( 'typography', 'lineHeight' ),
 237          'margin'                            => array( 'spacing', 'margin' ),
 238          'margin-top'                        => array( 'spacing', 'margin', 'top' ),
 239          'margin-right'                      => array( 'spacing', 'margin', 'right' ),
 240          'margin-bottom'                     => array( 'spacing', 'margin', 'bottom' ),
 241          'margin-left'                       => array( 'spacing', 'margin', 'left' ),
 242          'min-height'                        => array( 'dimensions', 'minHeight' ),
 243          'outline-color'                     => array( 'outline', 'color' ),
 244          'outline-offset'                    => array( 'outline', 'offset' ),
 245          'outline-style'                     => array( 'outline', 'style' ),
 246          'outline-width'                     => array( 'outline', 'width' ),
 247          'padding'                           => array( 'spacing', 'padding' ),
 248          'padding-top'                       => array( 'spacing', 'padding', 'top' ),
 249          'padding-right'                     => array( 'spacing', 'padding', 'right' ),
 250          'padding-bottom'                    => array( 'spacing', 'padding', 'bottom' ),
 251          'padding-left'                      => array( 'spacing', 'padding', 'left' ),
 252          '--wp--style--root--padding'        => array( 'spacing', 'padding' ),
 253          '--wp--style--root--padding-top'    => array( 'spacing', 'padding', 'top' ),
 254          '--wp--style--root--padding-right'  => array( 'spacing', 'padding', 'right' ),
 255          '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
 256          '--wp--style--root--padding-left'   => array( 'spacing', 'padding', 'left' ),
 257          'text-decoration'                   => array( 'typography', 'textDecoration' ),
 258          'text-transform'                    => array( 'typography', 'textTransform' ),
 259          'filter'                            => array( 'filter', 'duotone' ),
 260          'box-shadow'                        => array( 'shadow' ),
 261      );
 262  
 263      /**
 264       * Indirect metadata for style properties that are not directly output.
 265       *
 266       * Each element maps from a CSS property name to an array of
 267       * paths to the value in theme.json & block attributes.
 268       *
 269       * Indirect properties are not output directly by `compute_style_properties`,
 270       * but are used elsewhere in the processing of global styles. The indirect
 271       * property is used to validate whether or not a style value is allowed.
 272       *
 273       * @since 6.2.0
 274       *
 275       * @var array
 276       */
 277      const INDIRECT_PROPERTIES_METADATA = array(
 278          'gap'        => array(
 279              array( 'spacing', 'blockGap' ),
 280          ),
 281          'column-gap' => array(
 282              array( 'spacing', 'blockGap', 'left' ),
 283          ),
 284          'row-gap'    => array(
 285              array( 'spacing', 'blockGap', 'top' ),
 286          ),
 287          'max-width'  => array(
 288              array( 'layout', 'contentSize' ),
 289              array( 'layout', 'wideSize' ),
 290          ),
 291      );
 292  
 293      /**
 294       * Protected style properties.
 295       *
 296       * These style properties are only rendered if a setting enables it
 297       * via a value other than `null`.
 298       *
 299       * Each element maps the style property to the corresponding theme.json
 300       * setting key.
 301       *
 302       * @since 5.9.0
 303       */
 304      const PROTECTED_PROPERTIES = array(
 305          'spacing.blockGap' => array( 'spacing', 'blockGap' ),
 306      );
 307  
 308      /**
 309       * The top-level keys a theme.json can have.
 310       *
 311       * @since 5.8.0 As `ALLOWED_TOP_LEVEL_KEYS`.
 312       * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`,
 313       *              added the `customTemplates` and `templateParts` values.
 314       * @var string[]
 315       */
 316      const VALID_TOP_LEVEL_KEYS = array(
 317          'customTemplates',
 318          'patterns',
 319          'settings',
 320          'styles',
 321          'templateParts',
 322          'title',
 323          'version',
 324      );
 325  
 326      /**
 327       * The valid properties under the settings key.
 328       *
 329       * @since 5.8.0 As `ALLOWED_SETTINGS`.
 330       * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`,
 331       *              added new properties for `border`, `color`, `spacing`,
 332       *              and `typography`, and renamed others according to the new schema.
 333       * @since 6.0.0 Added `color.defaultDuotone`.
 334       * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`.
 335       * @since 6.2.0 Added `dimensions.minHeight`, 'shadow.presets', 'shadow.defaultPresets',
 336       *              `position.fixed` and `position.sticky`.
 337       * @var array
 338       */
 339      const VALID_SETTINGS = array(
 340          'appearanceTools'               => null,
 341          'useRootPaddingAwareAlignments' => null,
 342          'border'                        => array(
 343              'color'  => null,
 344              'radius' => null,
 345              'style'  => null,
 346              'width'  => null,
 347          ),
 348          'color'                         => array(
 349              'background'       => null,
 350              'custom'           => null,
 351              'customDuotone'    => null,
 352              'customGradient'   => null,
 353              'defaultDuotone'   => null,
 354              'defaultGradients' => null,
 355              'defaultPalette'   => null,
 356              'duotone'          => null,
 357              'gradients'        => null,
 358              'link'             => null,
 359              'palette'          => null,
 360              'text'             => null,
 361          ),
 362          'custom'                        => null,
 363          'dimensions'                    => array(
 364              'minHeight' => null,
 365          ),
 366          'layout'                        => array(
 367              'contentSize' => null,
 368              'definitions' => null,
 369              'wideSize'    => null,
 370          ),
 371          'position'                      => array(
 372              'fixed'  => null,
 373              'sticky' => null,
 374          ),
 375          'spacing'                       => array(
 376              'customSpacingSize' => null,
 377              'spacingSizes'      => null,
 378              'spacingScale'      => null,
 379              'blockGap'          => null,
 380              'margin'            => null,
 381              'padding'           => null,
 382              'units'             => null,
 383          ),
 384          'shadow'                        => array(
 385              'presets'        => null,
 386              'defaultPresets' => null,
 387          ),
 388          'typography'                    => array(
 389              'fluid'          => null,
 390              'customFontSize' => null,
 391              'dropCap'        => null,
 392              'fontFamilies'   => null,
 393              'fontSizes'      => null,
 394              'fontStyle'      => null,
 395              'fontWeight'     => null,
 396              'letterSpacing'  => null,
 397              'lineHeight'     => null,
 398              'textDecoration' => null,
 399              'textTransform'  => null,
 400          ),
 401      );
 402  
 403      /**
 404       * The valid properties under the styles key.
 405       *
 406       * @since 5.8.0 As `ALLOWED_STYLES`.
 407       * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`,
 408       *              added new properties for `border`, `filter`, `spacing`,
 409       *              and `typography`.
 410       * @since 6.1.0 Added new side properties for `border`,
 411       *              added new property `shadow`,
 412       *              updated `blockGap` to be allowed at any level.
 413       * @since 6.2.0 Added `outline`, and `minHeight` properties.
 414       *
 415       * @var array
 416       */
 417      const VALID_STYLES = array(
 418          'border'     => array(
 419              'color'  => null,
 420              'radius' => null,
 421              'style'  => null,
 422              'width'  => null,
 423              'top'    => null,
 424              'right'  => null,
 425              'bottom' => null,
 426              'left'   => null,
 427          ),
 428          'color'      => array(
 429              'background' => null,
 430              'gradient'   => null,
 431              'text'       => null,
 432          ),
 433          'dimensions' => array(
 434              'minHeight' => null,
 435          ),
 436          'filter'     => array(
 437              'duotone' => null,
 438          ),
 439          'outline'    => array(
 440              'color'  => null,
 441              'offset' => null,
 442              'style'  => null,
 443              'width'  => null,
 444          ),
 445          'shadow'     => null,
 446          'spacing'    => array(
 447              'margin'   => null,
 448              'padding'  => null,
 449              'blockGap' => null,
 450          ),
 451          'typography' => array(
 452              'fontFamily'     => null,
 453              'fontSize'       => null,
 454              'fontStyle'      => null,
 455              'fontWeight'     => null,
 456              'letterSpacing'  => null,
 457              'lineHeight'     => null,
 458              'textDecoration' => null,
 459              'textTransform'  => null,
 460          ),
 461          'css'        => null,
 462      );
 463  
 464      /**
 465       * Defines which pseudo selectors are enabled for which elements.
 466       *
 467       * The order of the selectors should be: link, any-link, visited, hover, focus, active.
 468       * This is to ensure the user action (hover, focus and active) styles have a higher
 469       * specificity than the visited styles, which in turn have a higher specificity than
 470       * the unvisited styles.
 471       *
 472       * See https://core.trac.wordpress.org/ticket/56928.
 473       * Note: this will affect both top-level and block-level elements.
 474       *
 475       * @since 6.1.0
 476       * @since 6.2.0 Added support for ':link' and ':any-link'.
 477       */
 478      const VALID_ELEMENT_PSEUDO_SELECTORS = array(
 479          'link'   => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ),
 480          'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ),
 481      );
 482  
 483      /**
 484       * The valid elements that can be found under styles.
 485       *
 486       * @since 5.8.0
 487       * @since 6.1.0 Added `heading`, `button`, and `caption` elements.
 488       * @var string[]
 489       */
 490      const ELEMENTS = array(
 491          'link'    => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity.
 492          'heading' => 'h1, h2, h3, h4, h5, h6',
 493          'h1'      => 'h1',
 494          'h2'      => 'h2',
 495          'h3'      => 'h3',
 496          'h4'      => 'h4',
 497          'h5'      => 'h5',
 498          'h6'      => 'h6',
 499          // We have the .wp-block-button__link class so that this will target older buttons that have been serialized.
 500          'button'  => '.wp-element-button, .wp-block-button__link',
 501          // The block classes are necessary to target older content that won't use the new class names.
 502          'caption' => '.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption',
 503          'cite'    => 'cite',
 504      );
 505  
 506      const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array(
 507          'button'  => 'wp-element-button',
 508          'caption' => 'wp-element-caption',
 509      );
 510  
 511      /**
 512       * List of block support features that can have their related styles
 513       * generated under their own feature level selector rather than the block's.
 514       *
 515       * @since 6.1.0
 516       * @var string[]
 517       */
 518      const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array(
 519          '__experimentalBorder' => 'border',
 520          'color'                => 'color',
 521          'spacing'              => 'spacing',
 522          'typography'           => 'typography',
 523      );
 524  
 525      /**
 526       * Returns a class name by an element name.
 527       *
 528       * @since 6.1.0
 529       *
 530       * @param string $element The name of the element.
 531       * @return string The name of the class.
 532       */
 533  	public static function get_element_class_name( $element ) {
 534          $class_name = '';
 535  
 536          // TODO: Replace array_key_exists() with isset() check once WordPress drops
 537          // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
 538          if ( array_key_exists( $element, static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES ) ) {
 539              $class_name = static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ];
 540          }
 541  
 542          return $class_name;
 543      }
 544  
 545      /**
 546       * Options that settings.appearanceTools enables.
 547       *
 548       * @since 6.0.0
 549       * @since 6.2.0 Added `dimensions.minHeight` and `position.sticky`.
 550       * @var array
 551       */
 552      const APPEARANCE_TOOLS_OPT_INS = array(
 553          array( 'border', 'color' ),
 554          array( 'border', 'radius' ),
 555          array( 'border', 'style' ),
 556          array( 'border', 'width' ),
 557          array( 'color', 'link' ),
 558          array( 'dimensions', 'minHeight' ),
 559          array( 'position', 'sticky' ),
 560          array( 'spacing', 'blockGap' ),
 561          array( 'spacing', 'margin' ),
 562          array( 'spacing', 'padding' ),
 563          array( 'typography', 'lineHeight' ),
 564      );
 565  
 566      /**
 567       * The latest version of the schema in use.
 568       *
 569       * @since 5.8.0
 570       * @since 5.9.0 Changed value from 1 to 2.
 571       * @var int
 572       */
 573      const LATEST_SCHEMA = 2;
 574  
 575      /**
 576       * Constructor.
 577       *
 578       * @since 5.8.0
 579       *
 580       * @param array  $theme_json A structure that follows the theme.json schema.
 581       * @param string $origin     Optional. What source of data this object represents.
 582       *                           One of 'default', 'theme', or 'custom'. Default 'theme'.
 583       */
 584  	public function __construct( $theme_json = array(), $origin = 'theme' ) {
 585          if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
 586              $origin = 'theme';
 587          }
 588  
 589          $this->theme_json    = WP_Theme_JSON_Schema::migrate( $theme_json );
 590          $valid_block_names   = array_keys( static::get_blocks_metadata() );
 591          $valid_element_names = array_keys( static::ELEMENTS );
 592          $theme_json          = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names );
 593          $this->theme_json    = static::maybe_opt_in_into_settings( $theme_json );
 594  
 595          // Internally, presets are keyed by origin.
 596          $nodes = static::get_setting_nodes( $this->theme_json );
 597          foreach ( $nodes as $node ) {
 598              foreach ( static::PRESETS_METADATA as $preset_metadata ) {
 599                  $path = $node['path'];
 600                  foreach ( $preset_metadata['path'] as $subpath ) {
 601                      $path[] = $subpath;
 602                  }
 603                  $preset = _wp_array_get( $this->theme_json, $path, null );
 604                  if ( null !== $preset ) {
 605                      // If the preset is not already keyed by origin.
 606                      if ( isset( $preset[0] ) || empty( $preset ) ) {
 607                          _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
 608                      }
 609                  }
 610              }
 611          }
 612      }
 613  
 614      /**
 615       * Enables some opt-in settings if theme declared support.
 616       *
 617       * @since 5.9.0
 618       *
 619       * @param array $theme_json A theme.json structure to modify.
 620       * @return array The modified theme.json structure.
 621       */
 622  	protected static function maybe_opt_in_into_settings( $theme_json ) {
 623          $new_theme_json = $theme_json;
 624  
 625          if (
 626              isset( $new_theme_json['settings']['appearanceTools'] ) &&
 627              true === $new_theme_json['settings']['appearanceTools']
 628          ) {
 629              static::do_opt_in_into_settings( $new_theme_json['settings'] );
 630          }
 631  
 632          if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) {
 633              foreach ( $new_theme_json['settings']['blocks'] as &$block ) {
 634                  if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) {
 635                      static::do_opt_in_into_settings( $block );
 636                  }
 637              }
 638          }
 639  
 640          return $new_theme_json;
 641      }
 642  
 643      /**
 644       * Enables some settings.
 645       *
 646       * @since 5.9.0
 647       *
 648       * @param array $context The context to which the settings belong.
 649       */
 650  	protected static function do_opt_in_into_settings( &$context ) {
 651          foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) {
 652              // Use "unset prop" as a marker instead of "null" because
 653              // "null" can be a valid value for some props (e.g. blockGap).
 654              if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) {
 655                  _wp_array_set( $context, $path, true );
 656              }
 657          }
 658  
 659          unset( $context['appearanceTools'] );
 660      }
 661  
 662      /**
 663       * Sanitizes the input according to the schemas.
 664       *
 665       * @since 5.8.0
 666       * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
 667       *
 668       * @param array $input               Structure to sanitize.
 669       * @param array $valid_block_names   List of valid block names.
 670       * @param array $valid_element_names List of valid element names.
 671       * @return array The sanitized output.
 672       */
 673  	protected static function sanitize( $input, $valid_block_names, $valid_element_names ) {
 674  
 675          $output = array();
 676  
 677          if ( ! is_array( $input ) ) {
 678              return $output;
 679          }
 680  
 681          // Preserve only the top most level keys.
 682          $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) );
 683  
 684          /*
 685           * Remove any rules that are annotated as "top" in VALID_STYLES constant.
 686           * Some styles are only meant to be available at the top-level (e.g.: blockGap),
 687           * hence, the schema for blocks & elements should not have them.
 688           */
 689          $styles_non_top_level = static::VALID_STYLES;
 690          foreach ( array_keys( $styles_non_top_level ) as $section ) {
 691              // array_key_exists() needs to be used instead of isset() because the value can be null.
 692              if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $section ] ) ) {
 693                  foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) {
 694                      if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) {
 695                          unset( $styles_non_top_level[ $section ][ $prop ] );
 696                      }
 697                  }
 698              }
 699          }
 700  
 701          // Build the schema based on valid block & element names.
 702          $schema                 = array();
 703          $schema_styles_elements = array();
 704  
 705          /*
 706           * Set allowed element pseudo selectors based on per element allow list.
 707           * Target data structure in schema:
 708           * e.g.
 709           * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
 710           * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
 711           */
 712          foreach ( $valid_element_names as $element ) {
 713              $schema_styles_elements[ $element ] = $styles_non_top_level;
 714  
 715              // TODO: Replace array_key_exists() with isset() check once WordPress drops
 716              // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
 717              if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
 718                  foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
 719                      $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level;
 720                  }
 721              }
 722          }
 723  
 724          $schema_styles_blocks   = array();
 725          $schema_settings_blocks = array();
 726          foreach ( $valid_block_names as $block ) {
 727              // Build the schema for each block style variation.
 728              $style_variation_names = array();
 729              if (
 730                  ! empty( $input['styles']['blocks'][ $block ]['variations'] ) &&
 731                  is_array( $input['styles']['blocks'][ $block ]['variations'] )
 732              ) {
 733                  $style_variation_names = array_keys( $input['styles']['blocks'][ $block ]['variations'] );
 734              }
 735  
 736              $schema_styles_variations = array();
 737              if ( ! empty( $style_variation_names ) ) {
 738                  $schema_styles_variations = array_fill_keys( $style_variation_names, $styles_non_top_level );
 739              }
 740  
 741              $schema_settings_blocks[ $block ]             = static::VALID_SETTINGS;
 742              $schema_styles_blocks[ $block ]               = $styles_non_top_level;
 743              $schema_styles_blocks[ $block ]['elements']   = $schema_styles_elements;
 744              $schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations;
 745          }
 746  
 747          $schema['styles']             = static::VALID_STYLES;
 748          $schema['styles']['blocks']   = $schema_styles_blocks;
 749          $schema['styles']['elements'] = $schema_styles_elements;
 750          $schema['settings']           = static::VALID_SETTINGS;
 751          $schema['settings']['blocks'] = $schema_settings_blocks;
 752  
 753          // Remove anything that's not present in the schema.
 754          foreach ( array( 'styles', 'settings' ) as $subtree ) {
 755              if ( ! isset( $input[ $subtree ] ) ) {
 756                  continue;
 757              }
 758  
 759              if ( ! is_array( $input[ $subtree ] ) ) {
 760                  unset( $output[ $subtree ] );
 761                  continue;
 762              }
 763  
 764              $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
 765  
 766              if ( empty( $result ) ) {
 767                  unset( $output[ $subtree ] );
 768              } else {
 769                  $output[ $subtree ] = $result;
 770              }
 771          }
 772  
 773          return $output;
 774      }
 775  
 776      /**
 777       * Appends a sub-selector to an existing one.
 778       *
 779       * Given the compounded $selector "h1, h2, h3"
 780       * and the $to_append selector ".some-class" the result will be
 781       * "h1.some-class, h2.some-class, h3.some-class".
 782       *
 783       * @since 5.8.0
 784       * @since 6.1.0 Added append position.
 785       *
 786       * @param string $selector  Original selector.
 787       * @param string $to_append Selector to append.
 788       * @param string $position  A position sub-selector should be appended. Default 'right'.
 789       * @return string The new selector.
 790       */
 791  	protected static function append_to_selector( $selector, $to_append, $position = 'right' ) {
 792          $new_selectors = array();
 793          $selectors     = explode( ',', $selector );
 794          foreach ( $selectors as $sel ) {
 795              $new_selectors[] = 'right' === $position ? $sel . $to_append : $to_append . $sel;
 796          }
 797          return implode( ',', $new_selectors );
 798      }
 799  
 800      /**
 801       * Returns the metadata for each block.
 802       *
 803       * Example:
 804       *
 805       *     {
 806       *       'core/paragraph': {
 807       *         'selector': 'p',
 808       *         'elements': {
 809       *           'link' => 'link selector',
 810       *           'etc'  => 'element selector'
 811       *         }
 812       *       },
 813       *       'core/heading': {
 814       *         'selector': 'h1',
 815       *         'elements': {}
 816       *       },
 817       *       'core/image': {
 818       *         'selector': '.wp-block-image',
 819       *         'duotone': 'img',
 820       *         'elements': {}
 821       *       }
 822       *     }
 823       *
 824       * @since 5.8.0
 825       * @since 5.9.0 Added `duotone` key with CSS selector.
 826       * @since 6.1.0 Added `features` key with block support feature level selectors.
 827       *
 828       * @return array Block metadata.
 829       */
 830  	protected static function get_blocks_metadata() {
 831          $registry = WP_Block_Type_Registry::get_instance();
 832          $blocks   = $registry->get_all_registered();
 833  
 834          // Is there metadata for all currently registered blocks?
 835          $blocks = array_diff_key( $blocks, static::$blocks_metadata );
 836          if ( empty( $blocks ) ) {
 837              return static::$blocks_metadata;
 838          }
 839  
 840          foreach ( $blocks as $block_name => $block_type ) {
 841              if (
 842                  isset( $block_type->supports['__experimentalSelector'] ) &&
 843                  is_string( $block_type->supports['__experimentalSelector'] )
 844              ) {
 845                  static::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector'];
 846              } else {
 847                  static::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) );
 848              }
 849  
 850              if (
 851                  isset( $block_type->supports['color']['__experimentalDuotone'] ) &&
 852                  is_string( $block_type->supports['color']['__experimentalDuotone'] )
 853              ) {
 854                  static::$blocks_metadata[ $block_name ]['duotone'] = $block_type->supports['color']['__experimentalDuotone'];
 855              }
 856  
 857              // Generate block support feature level selectors if opted into
 858              // for the current block.
 859              $features = array();
 860              foreach ( static::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS as $key => $feature ) {
 861                  if (
 862                      isset( $block_type->supports[ $key ]['__experimentalSelector'] ) &&
 863                      $block_type->supports[ $key ]['__experimentalSelector']
 864                  ) {
 865                      $features[ $feature ] = static::scope_selector(
 866                          static::$blocks_metadata[ $block_name ]['selector'],
 867                          $block_type->supports[ $key ]['__experimentalSelector']
 868                      );
 869                  }
 870              }
 871  
 872              if ( ! empty( $features ) ) {
 873                  static::$blocks_metadata[ $block_name ]['features'] = $features;
 874              }
 875  
 876              // Assign defaults, then overwrite those that the block sets by itself.
 877              // If the block selector is compounded, will append the element to each
 878              // individual block selector.
 879              $block_selectors = explode( ',', static::$blocks_metadata[ $block_name ]['selector'] );
 880              foreach ( static::ELEMENTS as $el_name => $el_selector ) {
 881                  $element_selector = array();
 882                  foreach ( $block_selectors as $selector ) {
 883                      if ( $selector === $el_selector ) {
 884                          $element_selector = array( $el_selector );
 885                          break;
 886                      }
 887                      $element_selector[] = static::append_to_selector( $el_selector, $selector . ' ', 'left' );
 888                  }
 889                  static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector );
 890              }
 891              // If the block has style variations, append their selectors to the block metadata.
 892              if ( ! empty( $block_type->styles ) ) {
 893                  $style_selectors = array();
 894                  foreach ( $block_type->styles as $style ) {
 895                      // The style variation classname is duplicated in the selector to ensure that it overrides core block styles.
 896                      $style_selectors[ $style['name'] ] = static::append_to_selector( '.is-style-' . $style['name'] . '.is-style-' . $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
 897                  }
 898                  static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
 899              }
 900          }
 901  
 902          return static::$blocks_metadata;
 903      }
 904  
 905      /**
 906       * Given a tree, removes the keys that are not present in the schema.
 907       *
 908       * It is recursive and modifies the input in-place.
 909       *
 910       * @since 5.8.0
 911       *
 912       * @param array $tree   Input to process.
 913       * @param array $schema Schema to adhere to.
 914       * @return array The modified $tree.
 915       */
 916  	protected static function remove_keys_not_in_schema( $tree, $schema ) {
 917          $tree = array_intersect_key( $tree, $schema );
 918  
 919          foreach ( $schema as $key => $data ) {
 920              if ( ! isset( $tree[ $key ] ) ) {
 921                  continue;
 922              }
 923  
 924              if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) {
 925                  $tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] );
 926  
 927                  if ( empty( $tree[ $key ] ) ) {
 928                      unset( $tree[ $key ] );
 929                  }
 930              } elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) {
 931                  unset( $tree[ $key ] );
 932              }
 933          }
 934  
 935          return $tree;
 936      }
 937  
 938      /**
 939       * Returns the existing settings for each block.
 940       *
 941       * Example:
 942       *
 943       *     {
 944       *       'root': {
 945       *         'color': {
 946       *           'custom': true
 947       *         }
 948       *       },
 949       *       'core/paragraph': {
 950       *         'spacing': {
 951       *           'customPadding': true
 952       *         }
 953       *       }
 954       *     }
 955       *
 956       * @since 5.8.0
 957       *
 958       * @return array Settings per block.
 959       */
 960  	public function get_settings() {
 961          if ( ! isset( $this->theme_json['settings'] ) ) {
 962              return array();
 963          } else {
 964              return $this->theme_json['settings'];
 965          }
 966      }
 967  
 968      /**
 969       * Returns the stylesheet that results of processing
 970       * the theme.json structure this object represents.
 971       *
 972       * @since 5.8.0
 973       * @since 5.9.0 Removed the `$type` parameter, added the `$types` and `$origins` parameters.
 974       *
 975       * @param string[] $types   Types of styles to load. Will load all by default. It accepts:
 976       *                          - `variables`: only the CSS Custom Properties for presets & custom ones.
 977       *                          - `styles`: only the styles section in theme.json.
 978       *                          - `presets`: only the classes for the presets.
 979       * @param string[] $origins A list of origins to include. By default it includes VALID_ORIGINS.
 980       * @param array    $options An array of options for now used for internal purposes only (may change without notice).
 981       *                          The options currently supported are 'scope' that makes sure all style are scoped to a
 982       *                          given selector, and root_selector which overwrites and forces a given selector to be
 983       *                          used on the root node.
 984       * @return string The resulting stylesheet.
 985       */
 986  	public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) {
 987          if ( null === $origins ) {
 988              $origins = static::VALID_ORIGINS;
 989          }
 990  
 991          if ( is_string( $types ) ) {
 992              // Dispatch error and map old arguments to new ones.
 993              _deprecated_argument( __FUNCTION__, '5.9.0' );
 994              if ( 'block_styles' === $types ) {
 995                  $types = array( 'styles', 'presets' );
 996              } elseif ( 'css_variables' === $types ) {
 997                  $types = array( 'variables' );
 998              } else {
 999                  $types = array( 'variables', 'styles', 'presets' );
1000              }
1001          }
1002  
1003          $blocks_metadata = static::get_blocks_metadata();
1004          $style_nodes     = static::get_style_nodes( $this->theme_json, $blocks_metadata );
1005          $setting_nodes   = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
1006  
1007          $root_style_key    = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true );
1008          $root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true );
1009  
1010          if ( ! empty( $options['scope'] ) ) {
1011              foreach ( $setting_nodes as &$node ) {
1012                  $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
1013              }
1014              foreach ( $style_nodes as &$node ) {
1015                  $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
1016              }
1017          }
1018  
1019          if ( ! empty( $options['root_selector'] ) ) {
1020              if ( false !== $root_settings_key ) {
1021                  $setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector'];
1022              }
1023              if ( false !== $root_style_key ) {
1024                  $setting_nodes[ $root_style_key ]['selector'] = $options['root_selector'];
1025              }
1026          }
1027  
1028          $stylesheet = '';
1029  
1030          if ( in_array( 'variables', $types, true ) ) {
1031              $stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
1032          }
1033  
1034          if ( in_array( 'styles', $types, true ) ) {
1035              if ( false !== $root_style_key ) {
1036                  $stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] );
1037              }
1038              $stylesheet .= $this->get_block_classes( $style_nodes );
1039          } elseif ( in_array( 'base-layout-styles', $types, true ) ) {
1040              $root_selector    = static::ROOT_BLOCK_SELECTOR;
1041              $columns_selector = '.wp-block-columns';
1042              if ( ! empty( $options['scope'] ) ) {
1043                  $root_selector    = static::scope_selector( $options['scope'], $root_selector );
1044                  $columns_selector = static::scope_selector( $options['scope'], $columns_selector );
1045              }
1046              if ( ! empty( $options['root_selector'] ) ) {
1047                  $root_selector = $options['root_selector'];
1048              }
1049              // Base layout styles are provided as part of `styles`, so only output separately if explicitly requested.
1050              // For backwards compatibility, the Columns block is explicitly included, to support a different default gap value.
1051              $base_styles_nodes = array(
1052                  array(
1053                      'path'     => array( 'styles' ),
1054                      'selector' => $root_selector,
1055                  ),
1056                  array(
1057                      'path'     => array( 'styles', 'blocks', 'core/columns' ),
1058                      'selector' => $columns_selector,
1059                      'name'     => 'core/columns',
1060                  ),
1061              );
1062  
1063              foreach ( $base_styles_nodes as $base_style_node ) {
1064                  $stylesheet .= $this->get_layout_styles( $base_style_node );
1065              }
1066          }
1067  
1068          if ( in_array( 'presets', $types, true ) ) {
1069              $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
1070          }
1071  
1072          return $stylesheet;
1073      }
1074  
1075      /**
1076       * Processes the CSS, to apply nesting.
1077       *
1078       * @since 6.2.0
1079       *
1080       * @param string $css      The CSS to process.
1081       * @param string $selector The selector to nest.
1082       * @return string The processed CSS.
1083       */
1084  	protected function process_blocks_custom_css( $css, $selector ) {
1085          $processed_css = '';
1086  
1087          // Split CSS nested rules.
1088          $parts = explode( '&', $css );
1089          foreach ( $parts as $part ) {
1090              $processed_css .= ( ! str_contains( $part, '{' ) )
1091                  ? trim( $selector ) . '{' . trim( $part ) . '}' // If the part doesn't contain braces, it applies to the root level.
1092                  : trim( $selector . $part ); // Prepend the selector, which effectively replaces the "&" character.
1093          }
1094          return $processed_css;
1095      }
1096  
1097      /**
1098       * Returns the global styles custom CSS.
1099       *
1100       * @since 6.2.0
1101       *
1102       * @return string The global styles custom CSS.
1103       */
1104  	public function get_custom_css() {
1105          // Add the global styles root CSS.
1106          $stylesheet = _wp_array_get( $this->theme_json, array( 'styles', 'css' ), '' );
1107  
1108          // Add the global styles block CSS.
1109          if ( isset( $this->theme_json['styles']['blocks'] ) ) {
1110              foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) {
1111                  $custom_block_css = _wp_array_get( $this->theme_json, array( 'styles', 'blocks', $name, 'css' ) );
1112                  if ( $custom_block_css ) {
1113                      $selector    = static::$blocks_metadata[ $name ]['selector'];
1114                      $stylesheet .= $this->process_blocks_custom_css( $custom_block_css, $selector );
1115                  }
1116              }
1117          }
1118  
1119          return $stylesheet;
1120      }
1121  
1122      /**
1123       * Returns the page templates of the active theme.
1124       *
1125       * @since 5.9.0
1126       *
1127       * @return array
1128       */
1129  	public function get_custom_templates() {
1130          $custom_templates = array();
1131          if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $this->theme_json['customTemplates'] ) ) {
1132              return $custom_templates;
1133          }
1134  
1135          foreach ( $this->theme_json['customTemplates'] as $item ) {
1136              if ( isset( $item['name'] ) ) {
1137                  $custom_templates[ $item['name'] ] = array(
1138                      'title'     => isset( $item['title'] ) ? $item['title'] : '',
1139                      'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ),
1140                  );
1141              }
1142          }
1143          return $custom_templates;
1144      }
1145  
1146      /**
1147       * Returns the template part data of active theme.
1148       *
1149       * @since 5.9.0
1150       *
1151       * @return array
1152       */
1153  	public function get_template_parts() {
1154          $template_parts = array();
1155          if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $this->theme_json['templateParts'] ) ) {
1156              return $template_parts;
1157          }
1158  
1159          foreach ( $this->theme_json['templateParts'] as $item ) {
1160              if ( isset( $item['name'] ) ) {
1161                  $template_parts[ $item['name'] ] = array(
1162                      'title' => isset( $item['title'] ) ? $item['title'] : '',
1163                      'area'  => isset( $item['area'] ) ? $item['area'] : '',
1164                  );
1165              }
1166          }
1167          return $template_parts;
1168      }
1169  
1170      /**
1171       * Converts each style section into a list of rulesets
1172       * containing the block styles to be appended to the stylesheet.
1173       *
1174       * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
1175       *
1176       * For each section this creates a new ruleset such as:
1177       *
1178       *   block-selector {
1179       *     style-property-one: value;
1180       *   }
1181       *
1182       * @since 5.8.0 As `get_block_styles()`.
1183       * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()`
1184       *              and no longer returns preset classes.
1185       *              Removed the `$setting_nodes` parameter.
1186       * @since 6.1.0 Moved most internal logic to `get_styles_for_block()`.
1187       *
1188       * @param array $style_nodes Nodes with styles.
1189       * @return string The new stylesheet.
1190       */
1191  	protected function get_block_classes( $style_nodes ) {
1192          $block_rules = '';
1193  
1194          foreach ( $style_nodes as $metadata ) {
1195              if ( null === $metadata['selector'] ) {
1196                  continue;
1197              }
1198              $block_rules .= static::get_styles_for_block( $metadata );
1199          }
1200  
1201          return $block_rules;
1202      }
1203  
1204      /**
1205       * Gets the CSS layout rules for a particular block from theme.json layout definitions.
1206       *
1207       * @since 6.1.0
1208       *
1209       * @param array $block_metadata Metadata about the block to get styles for.
1210       * @return string Layout styles for the block.
1211       */
1212  	protected function get_layout_styles( $block_metadata ) {
1213          $block_rules = '';
1214          $block_type  = null;
1215  
1216          // Skip outputting layout styles if explicitly disabled.
1217          if ( current_theme_supports( 'disable-layout-styles' ) ) {
1218              return $block_rules;
1219          }
1220  
1221          if ( isset( $block_metadata['name'] ) ) {
1222              $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_metadata['name'] );
1223              if ( ! block_has_support( $block_type, array( '__experimentalLayout' ), false ) ) {
1224                  return $block_rules;
1225              }
1226          }
1227  
1228          $selector                 = isset( $block_metadata['selector'] ) ? $block_metadata['selector'] : '';
1229          $has_block_gap_support    = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
1230          $has_fallback_gap_support = ! $has_block_gap_support; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback gap styles support.
1231          $node                     = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
1232          $layout_definitions       = _wp_array_get( $this->theme_json, array( 'settings', 'layout', 'definitions' ), array() );
1233          $layout_selector_pattern  = '/^[a-zA-Z0-9\-\.\ *+>:\(\)]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, child combinator and pseudo class selectors.
1234  
1235          // Gap styles will only be output if the theme has block gap support, or supports a fallback gap.
1236          // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value.
1237          if ( $has_block_gap_support || $has_fallback_gap_support ) {
1238              $block_gap_value = null;
1239              // Use a fallback gap value if block gap support is not available.
1240              if ( ! $has_block_gap_support ) {
1241                  $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null;
1242                  if ( ! empty( $block_type ) ) {
1243                      $block_gap_value = _wp_array_get( $block_type->supports, array( 'spacing', 'blockGap', '__experimentalDefault' ), null );
1244                  }
1245              } else {
1246                  $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) );
1247              }
1248  
1249              // Support split row / column values and concatenate to a shorthand value.
1250              if ( is_array( $block_gap_value ) ) {
1251                  if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) {
1252                      $gap_row         = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) );
1253                      $gap_column      = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) );
1254                      $block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column;
1255                  } else {
1256                      // Skip outputting gap value if not all sides are provided.
1257                      $block_gap_value = null;
1258                  }
1259              }
1260  
1261              // If the block should have custom gap, add the gap styles.
1262              if ( null !== $block_gap_value && false !== $block_gap_value && '' !== $block_gap_value ) {
1263                  foreach ( $layout_definitions as $layout_definition_key => $layout_definition ) {
1264                      // Allow outputting fallback gap styles for flex layout type when block gap support isn't available.
1265                      if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key ) {
1266                          continue;
1267                      }
1268  
1269                      $class_name    = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) );
1270                      $spacing_rules = _wp_array_get( $layout_definition, array( 'spacingStyles' ), array() );
1271  
1272                      if (
1273                          ! empty( $class_name ) &&
1274                          ! empty( $spacing_rules )
1275                      ) {
1276                          foreach ( $spacing_rules as $spacing_rule ) {
1277                              $declarations = array();
1278                              if (
1279                                  isset( $spacing_rule['selector'] ) &&
1280                                  preg_match( $layout_selector_pattern, $spacing_rule['selector'] ) &&
1281                                  ! empty( $spacing_rule['rules'] )
1282                              ) {
1283                                  // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value.
1284                                  foreach ( $spacing_rule['rules'] as $css_property => $css_value ) {
1285                                      $current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value;
1286                                      if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) {
1287                                          $declarations[] = array(
1288                                              'name'  => $css_property,
1289                                              'value' => $current_css_value,
1290                                          );
1291                                      }
1292                                  }
1293  
1294                                  if ( ! $has_block_gap_support ) {
1295                                      // For fallback gap styles, use lower specificity, to ensure styles do not unintentionally override theme styles.
1296                                      $format          = static::ROOT_BLOCK_SELECTOR === $selector ? ':where(.%2$s%3$s)' : ':where(%1$s.%2$s%3$s)';
1297                                      $layout_selector = sprintf(
1298                                          $format,
1299                                          $selector,
1300                                          $class_name,
1301                                          $spacing_rule['selector']
1302                                      );
1303                                  } else {
1304                                      $format          = static::ROOT_BLOCK_SELECTOR === $selector ? '%s .%s%s' : '%s.%s%s';
1305                                      $layout_selector = sprintf(
1306                                          $format,
1307                                          $selector,
1308                                          $class_name,
1309                                          $spacing_rule['selector']
1310                                      );
1311                                  }
1312                                  $block_rules .= static::to_ruleset( $layout_selector, $declarations );
1313                              }
1314                          }
1315                      }
1316                  }
1317              }
1318          }
1319  
1320          // Output base styles.
1321          if (
1322              static::ROOT_BLOCK_SELECTOR === $selector
1323          ) {
1324              $valid_display_modes = array( 'block', 'flex', 'grid' );
1325              foreach ( $layout_definitions as $layout_definition ) {
1326                  $class_name       = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) );
1327                  $base_style_rules = _wp_array_get( $layout_definition, array( 'baseStyles' ), array() );
1328  
1329                  if (
1330                      ! empty( $class_name ) &&
1331                      ! empty( $base_style_rules )
1332                  ) {
1333                      // Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`.
1334                      if (
1335                          ! empty( $layout_definition['displayMode'] ) &&
1336                          is_string( $layout_definition['displayMode'] ) &&
1337                          in_array( $layout_definition['displayMode'], $valid_display_modes, true )
1338                      ) {
1339                          $layout_selector = sprintf(
1340                              '%s .%s',
1341                              $selector,
1342                              $class_name
1343                          );
1344                          $block_rules    .= static::to_ruleset(
1345                              $layout_selector,
1346                              array(
1347                                  array(
1348                                      'name'  => 'display',
1349                                      'value' => $layout_definition['displayMode'],
1350                                  ),
1351                              )
1352                          );
1353                      }
1354  
1355                      foreach ( $base_style_rules as $base_style_rule ) {
1356                          $declarations = array();
1357  
1358                          if (
1359                              isset( $base_style_rule['selector'] ) &&
1360                              preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) &&
1361                              ! empty( $base_style_rule['rules'] )
1362                          ) {
1363                              foreach ( $base_style_rule['rules'] as $css_property => $css_value ) {
1364                                  if ( static::is_safe_css_declaration( $css_property, $css_value ) ) {
1365                                      $declarations[] = array(
1366                                          'name'  => $css_property,
1367                                          'value' => $css_value,
1368                                      );
1369                                  }
1370                              }
1371  
1372                              $layout_selector = sprintf(
1373                                  '%s .%s%s',
1374                                  $selector,
1375                                  $class_name,
1376                                  $base_style_rule['selector']
1377                              );
1378                              $block_rules    .= static::to_ruleset( $layout_selector, $declarations );
1379                          }
1380                      }
1381                  }
1382              }
1383          }
1384          return $block_rules;
1385      }
1386  
1387      /**
1388       * Creates new rulesets as classes for each preset value such as:
1389       *
1390       *   .has-value-color {
1391       *     color: value;
1392       *   }
1393       *
1394       *   .has-value-background-color {
1395       *     background-color: value;
1396       *   }
1397       *
1398       *   .has-value-font-size {
1399       *     font-size: value;
1400       *   }
1401       *
1402       *   .has-value-gradient-background {
1403       *     background: value;
1404       *   }
1405       *
1406       *   p.has-value-gradient-background {
1407       *     background: value;
1408       *   }
1409       *
1410       * @since 5.9.0
1411       *
1412       * @param array    $setting_nodes Nodes with settings.
1413       * @param string[] $origins       List of origins to process presets from.
1414       * @return string The new stylesheet.
1415       */
1416  	protected function get_preset_classes( $setting_nodes, $origins ) {
1417          $preset_rules = '';
1418  
1419          foreach ( $setting_nodes as $metadata ) {
1420              if ( null === $metadata['selector'] ) {
1421                  continue;
1422              }
1423  
1424              $selector      = $metadata['selector'];
1425              $node          = _wp_array_get( $this->theme_json, $metadata['path'], array() );
1426              $preset_rules .= static::compute_preset_classes( $node, $selector, $origins );
1427          }
1428  
1429          return $preset_rules;
1430      }
1431  
1432      /**
1433       * Converts each styles section into a list of rulesets
1434       * to be appended to the stylesheet.
1435       * These rulesets contain all the css variables (custom variables and preset variables).
1436       *
1437       * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
1438       *
1439       * For each section this creates a new ruleset such as:
1440       *
1441       *     block-selector {
1442       *       --wp--preset--category--slug: value;
1443       *       --wp--custom--variable: value;
1444       *     }
1445       *
1446       * @since 5.8.0
1447       * @since 5.9.0 Added the `$origins` parameter.
1448       *
1449       * @param array    $nodes   Nodes with settings.
1450       * @param string[] $origins List of origins to process.
1451       * @return string The new stylesheet.
1452       */
1453  	protected function get_css_variables( $nodes, $origins ) {
1454          $stylesheet = '';
1455          foreach ( $nodes as $metadata ) {
1456              if ( null === $metadata['selector'] ) {
1457                  continue;
1458              }
1459  
1460              $selector = $metadata['selector'];
1461  
1462              $node                    = _wp_array_get( $this->theme_json, $metadata['path'], array() );
1463              $declarations            = static::compute_preset_vars( $node, $origins );
1464              $theme_vars_declarations = static::compute_theme_vars( $node );
1465              foreach ( $theme_vars_declarations as $theme_vars_declaration ) {
1466                  $declarations[] = $theme_vars_declaration;
1467              }
1468  
1469              $stylesheet .= static::to_ruleset( $selector, $declarations );
1470          }
1471  
1472          return $stylesheet;
1473      }
1474  
1475      /**
1476       * Given a selector and a declaration list,
1477       * creates the corresponding ruleset.
1478       *
1479       * @since 5.8.0
1480       *
1481       * @param string $selector     CSS selector.
1482       * @param array  $declarations List of declarations.
1483       * @return string The resulting CSS ruleset.
1484       */
1485  	protected static function to_ruleset( $selector, $declarations ) {
1486          if ( empty( $declarations ) ) {
1487              return '';
1488          }
1489  
1490          $declaration_block = array_reduce(
1491              $declarations,
1492              static function ( $carry, $element ) {
1493                  return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
1494              ''
1495          );
1496  
1497          return $selector . '{' . $declaration_block . '}';
1498      }
1499  
1500      /**
1501       * Given a settings array, returns the generated rulesets
1502       * for the preset classes.
1503       *
1504       * @since 5.8.0
1505       * @since 5.9.0 Added the `$origins` parameter.
1506       *
1507       * @param array    $settings Settings to process.
1508       * @param string   $selector Selector wrapping the classes.
1509       * @param string[] $origins  List of origins to process.
1510       * @return string The result of processing the presets.
1511       */
1512  	protected static function compute_preset_classes( $settings, $selector, $origins ) {
1513          if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
1514              // Classes at the global level do not need any CSS prefixed,
1515              // and we don't want to increase its specificity.
1516              $selector = '';
1517          }
1518  
1519          $stylesheet = '';
1520          foreach ( static::PRESETS_METADATA as $preset_metadata ) {
1521              $slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins );
1522              foreach ( $preset_metadata['classes'] as $class => $property ) {
1523                  foreach ( $slugs as $slug ) {
1524                      $css_var     = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
1525                      $class_name  = static::replace_slug_in_string( $class, $slug );
1526                      $stylesheet .= static::to_ruleset(
1527                          static::append_to_selector( $selector, $class_name ),
1528                          array(
1529                              array(
1530                                  'name'  => $property,
1531                                  'value' => 'var(' . $css_var . ') !important',
1532                              ),
1533                          )
1534                      );
1535                  }
1536              }
1537          }
1538  
1539          return $stylesheet;
1540      }
1541  
1542      /**
1543       * Function that scopes a selector with another one. This works a bit like
1544       * SCSS nesting except the `&` operator isn't supported.
1545       *
1546       * <code>
1547       * $scope = '.a, .b .c';
1548       * $selector = '> .x, .y';
1549       * $merged = scope_selector( $scope, $selector );
1550       * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
1551       * </code>
1552       *
1553       * @since 5.9.0
1554       *
1555       * @param string $scope    Selector to scope to.
1556       * @param string $selector Original selector.
1557       * @return string Scoped selector.
1558       */
1559  	public static function scope_selector( $scope, $selector ) {
1560          $scopes    = explode( ',', $scope );
1561          $selectors = explode( ',', $selector );
1562  
1563          $selectors_scoped = array();
1564          foreach ( $scopes as $outer ) {
1565              foreach ( $selectors as $inner ) {
1566                  $outer = trim( $outer );
1567                  $inner = trim( $inner );
1568                  if ( ! empty( $outer ) && ! empty( $inner ) ) {
1569                      $selectors_scoped[] = $outer . ' ' . $inner;
1570                  } elseif ( empty( $outer ) ) {
1571                      $selectors_scoped[] = $inner;
1572                  } elseif ( empty( $inner ) ) {
1573                      $selectors_scoped[] = $outer;
1574                  }
1575              }
1576          }
1577  
1578          $result = implode( ', ', $selectors_scoped );
1579          return $result;
1580      }
1581  
1582      /**
1583       * Gets preset values keyed by slugs based on settings and metadata.
1584       *
1585       * <code>
1586       * $settings = array(
1587       *     'typography' => array(
1588       *         'fontFamilies' => array(
1589       *             array(
1590       *                 'slug'       => 'sansSerif',
1591       *                 'fontFamily' => '"Helvetica Neue", sans-serif',
1592       *             ),
1593       *             array(
1594       *                 'slug'   => 'serif',
1595       *                 'colors' => 'Georgia, serif',
1596       *             )
1597       *         ),
1598       *     ),
1599       * );
1600       * $meta = array(
1601       *    'path'      => array( 'typography', 'fontFamilies' ),
1602       *    'value_key' => 'fontFamily',
1603       * );
1604       * $values_by_slug = get_settings_values_by_slug();
1605       * // $values_by_slug === array(
1606       * //   'sans-serif' => '"Helvetica Neue", sans-serif',
1607       * //   'serif'      => 'Georgia, serif',
1608       * // );
1609       * </code>
1610       *
1611       * @since 5.9.0
1612       *
1613       * @param array    $settings        Settings to process.
1614       * @param array    $preset_metadata One of the PRESETS_METADATA values.
1615       * @param string[] $origins         List of origins to process.
1616       * @return array Array of presets where each key is a slug and each value is the preset value.
1617       */
1618  	protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) {
1619          $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
1620  
1621          $result = array();
1622          foreach ( $origins as $origin ) {
1623              if ( ! isset( $preset_per_origin[ $origin ] ) ) {
1624                  continue;
1625              }
1626              foreach ( $preset_per_origin[ $origin ] as $preset ) {
1627                  $slug = _wp_to_kebab_case( $preset['slug'] );
1628  
1629                  $value = '';
1630                  if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
1631                      $value_key = $preset_metadata['value_key'];
1632                      $value     = $preset[ $value_key ];
1633                  } elseif (
1634                      isset( $preset_metadata['value_func'] ) &&
1635                      is_callable( $preset_metadata['value_func'] )
1636                  ) {
1637                      $value_func = $preset_metadata['value_func'];
1638                      $value      = call_user_func( $value_func, $preset );
1639                  } else {
1640                      // If we don't have a value, then don't add it to the result.
1641                      continue;
1642                  }
1643  
1644                  $result[ $slug ] = $value;
1645              }
1646          }
1647          return $result;
1648      }
1649  
1650      /**
1651       * Similar to get_settings_values_by_slug, but doesn't compute the value.
1652       *
1653       * @since 5.9.0
1654       *
1655       * @param array    $settings        Settings to process.
1656       * @param array    $preset_metadata One of the PRESETS_METADATA values.
1657       * @param string[] $origins         List of origins to process.
1658       * @return array Array of presets where the key and value are both the slug.
1659       */
1660  	protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) {
1661          if ( null === $origins ) {
1662              $origins = static::VALID_ORIGINS;
1663          }
1664  
1665          $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
1666  
1667          $result = array();
1668          foreach ( $origins as $origin ) {
1669              if ( ! isset( $preset_per_origin[ $origin ] ) ) {
1670                  continue;
1671              }
1672              foreach ( $preset_per_origin[ $origin ] as $preset ) {
1673                  $slug = _wp_to_kebab_case( $preset['slug'] );
1674  
1675                  // Use the array as a set so we don't get duplicates.
1676                  $result[ $slug ] = $slug;
1677              }
1678          }
1679          return $result;
1680      }
1681  
1682      /**
1683       * Transforms a slug into a CSS Custom Property.
1684       *
1685       * @since 5.9.0
1686       *
1687       * @param string $input String to replace.
1688       * @param string $slug  The slug value to use to generate the custom property.
1689       * @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`.
1690       */
1691  	protected static function replace_slug_in_string( $input, $slug ) {
1692          return strtr( $input, array( '$slug' => $slug ) );
1693      }
1694  
1695      /**
1696       * Given the block settings, extracts the CSS Custom Properties
1697       * for the presets and adds them to the $declarations array
1698       * following the format:
1699       *
1700       *     array(
1701       *       'name'  => 'property_name',
1702       *       'value' => 'property_value,
1703       *     )
1704       *
1705       * @since 5.8.0
1706       * @since 5.9.0 Added the `$origins` parameter.
1707       *
1708       * @param array    $settings Settings to process.
1709       * @param string[] $origins  List of origins to process.
1710       * @return array The modified $declarations.
1711       */
1712  	protected static function compute_preset_vars( $settings, $origins ) {
1713          $declarations = array();
1714          foreach ( static::PRESETS_METADATA as $preset_metadata ) {
1715              $values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins );
1716              foreach ( $values_by_slug as $slug => $value ) {
1717                  $declarations[] = array(
1718                      'name'  => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
1719                      'value' => $value,
1720                  );
1721              }
1722          }
1723  
1724          return $declarations;
1725      }
1726  
1727      /**
1728       * Given an array of settings, extracts the CSS Custom Properties
1729       * for the custom values and adds them to the $declarations
1730       * array following the format:
1731       *
1732       *     array(
1733       *       'name'  => 'property_name',
1734       *       'value' => 'property_value,
1735       *     )
1736       *
1737       * @since 5.8.0
1738       *
1739       * @param array $settings Settings to process.
1740       * @return array The modified $declarations.
1741       */
1742  	protected static function compute_theme_vars( $settings ) {
1743          $declarations  = array();
1744          $custom_values = _wp_array_get( $settings, array( 'custom' ), array() );
1745          $css_vars      = static::flatten_tree( $custom_values );
1746          foreach ( $css_vars as $key => $value ) {
1747              $declarations[] = array(
1748                  'name'  => '--wp--custom--' . $key,
1749                  'value' => $value,
1750              );
1751          }
1752  
1753          return $declarations;
1754      }
1755  
1756      /**
1757       * Given a tree, it creates a flattened one
1758       * by merging the keys and binding the leaf values
1759       * to the new keys.
1760       *
1761       * It also transforms camelCase names into kebab-case
1762       * and substitutes '/' by '-'.
1763       *
1764       * This is thought to be useful to generate
1765       * CSS Custom Properties from a tree,
1766       * although there's nothing in the implementation
1767       * of this function that requires that format.
1768       *
1769       * For example, assuming the given prefix is '--wp'
1770       * and the token is '--', for this input tree:
1771       *
1772       *     {
1773       *       'some/property': 'value',
1774       *       'nestedProperty': {
1775       *         'sub-property': 'value'
1776       *       }
1777       *     }
1778       *
1779       * it'll return this output:
1780       *
1781       *     {
1782       *       '--wp--some-property': 'value',
1783       *       '--wp--nested-property--sub-property': 'value'
1784       *     }
1785       *
1786       * @since 5.8.0
1787       *
1788       * @param array  $tree   Input tree to process.
1789       * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string.
1790       * @param string $token  Optional. Token to use between levels. Default '--'.
1791       * @return array The flattened tree.
1792       */
1793  	protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
1794          $result = array();
1795          foreach ( $tree as $property => $value ) {
1796              $new_key = $prefix . str_replace(
1797                  '/',
1798                  '-',
1799                  strtolower( _wp_to_kebab_case( $property ) )
1800              );
1801  
1802              if ( is_array( $value ) ) {
1803                  $new_prefix        = $new_key . $token;
1804                  $flattened_subtree = static::flatten_tree( $value, $new_prefix, $token );
1805                  foreach ( $flattened_subtree as $subtree_key => $subtree_value ) {
1806                      $result[ $subtree_key ] = $subtree_value;
1807                  }
1808              } else {
1809                  $result[ $new_key ] = $value;
1810              }
1811          }
1812          return $result;
1813      }
1814  
1815      /**
1816       * Given a styles array, it extracts the style properties
1817       * and adds them to the $declarations array following the format:
1818       *
1819       *     array(
1820       *       'name'  => 'property_name',
1821       *       'value' => 'property_value,
1822       *     )
1823       *
1824       * @since 5.8.0
1825       * @since 5.9.0 Added the `$settings` and `$properties` parameters.
1826       * @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters.
1827       *
1828       * @param array   $styles Styles to process.
1829       * @param array   $settings Theme settings.
1830       * @param array   $properties Properties metadata.
1831       * @param array   $theme_json Theme JSON array.
1832       * @param string  $selector The style block selector.
1833       * @param boolean $use_root_padding Whether to add custom properties at root level.
1834       * @return array  Returns the modified $declarations.
1835       */
1836  	protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
1837          if ( null === $properties ) {
1838              $properties = static::PROPERTIES_METADATA;
1839          }
1840  
1841          $declarations = array();
1842          if ( empty( $styles ) ) {
1843              return $declarations;
1844          }
1845  
1846          $root_variable_duplicates = array();
1847  
1848          foreach ( $properties as $css_property => $value_path ) {
1849              $value = static::get_property_value( $styles, $value_path, $theme_json );
1850  
1851              if ( str_starts_with( $css_property, '--wp--style--root--' ) && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) {
1852                  continue;
1853              }
1854              // Root-level padding styles don't currently support strings with CSS shorthand values.
1855              // This may change: https://github.com/WordPress/gutenberg/issues/40132.
1856              if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) {
1857                  continue;
1858              }
1859  
1860              if ( str_starts_with( $css_property, '--wp--style--root--' ) && $use_root_padding ) {
1861                  $root_variable_duplicates[] = substr( $css_property, strlen( '--wp--style--root--' ) );
1862              }
1863  
1864              // Look up protected properties, keyed by value path.
1865              // Skip protected properties that are explicitly set to `null`.
1866              if ( is_array( $value_path ) ) {
1867                  $path_string = implode( '.', $value_path );
1868                  if (
1869                      // TODO: Replace array_key_exists() with isset() check once WordPress drops
1870                      // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
1871                      array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) &&
1872                      _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null
1873                  ) {
1874                      continue;
1875                  }
1876              }
1877  
1878              // Skip if empty and not "0" or value represents array of longhand values.
1879              $has_missing_value = empty( $value ) && ! is_numeric( $value );
1880              if ( $has_missing_value || is_array( $value ) ) {
1881                  continue;
1882              }
1883  
1884              // Calculates fluid typography rules where available.
1885              if ( 'font-size' === $css_property ) {
1886                  /*
1887                   * wp_get_typography_font_size_value() will check
1888                   * if fluid typography has been activated and also
1889                   * whether the incoming value can be converted to a fluid value.
1890                   * Values that already have a clamp() function will not pass the test,
1891                   * and therefore the original $value will be returned.
1892                   */
1893                  $value = wp_get_typography_font_size_value( array( 'size' => $value ) );
1894              }
1895  
1896              $declarations[] = array(
1897                  'name'  => $css_property,
1898                  'value' => $value,
1899              );
1900          }
1901  
1902          // If a variable value is added to the root, the corresponding property should be removed.
1903          foreach ( $root_variable_duplicates as $duplicate ) {
1904              $discard = array_search( $duplicate, array_column( $declarations, 'name' ), true );
1905              if ( is_numeric( $discard ) ) {
1906                  array_splice( $declarations, $discard, 1 );
1907              }
1908          }
1909  
1910          return $declarations;
1911      }
1912  
1913      /**
1914       * Returns the style property for the given path.
1915       *
1916       * It also converts CSS Custom Property stored as
1917       * "var:preset|color|secondary" to the form
1918       * "--wp--preset--color--secondary".
1919       *
1920       * It also converts references to a path to the value
1921       * stored at that location, e.g.
1922       * { "ref": "style.color.background" } => "#fff".
1923       *
1924       * @since 5.8.0
1925       * @since 5.9.0 Added support for values of array type, which are returned as is.
1926       * @since 6.1.0 Added the `$theme_json` parameter.
1927       *
1928       * @param array $styles Styles subtree.
1929       * @param array $path   Which property to process.
1930       * @param array $theme_json Theme JSON array.
1931       * @return string|array Style property value.
1932       */
1933  	protected static function get_property_value( $styles, $path, $theme_json = null ) {
1934          $value = _wp_array_get( $styles, $path, '' );
1935  
1936          if ( '' === $value || null === $value ) {
1937              // No need to process the value further.
1938              return '';
1939          }
1940  
1941          /*
1942           * This converts references to a path to the value at that path
1943           * where the values is an array with a "ref" key, pointing to a path.
1944           * For example: { "ref": "style.color.background" } => "#fff".
1945           */
1946          if ( is_array( $value ) && isset( $value['ref'] ) ) {
1947              $value_path = explode( '.', $value['ref'] );
1948              $ref_value  = _wp_array_get( $theme_json, $value_path );
1949              // Only use the ref value if we find anything.
1950              if ( ! empty( $ref_value ) && is_string( $ref_value ) ) {
1951                  $value = $ref_value;
1952              }
1953  
1954              if ( is_array( $ref_value ) && isset( $ref_value['ref'] ) ) {
1955                  $path_string      = json_encode( $path );
1956                  $ref_value_string = json_encode( $ref_value );
1957                  _doing_it_wrong(
1958                      'get_property_value',
1959                      sprintf(
1960                          /* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */
1961                          __( 'Your %1$s file uses a dynamic value (%2$s) for the path at %3$s. However, the value at %3$s is also a dynamic value (pointing to %4$s) and pointing to another dynamic value is not supported. Please update %3$s to point directly to %4$s.' ),
1962                          'theme.json',
1963                          $ref_value_string,
1964                          $path_string,
1965                          $ref_value['ref']
1966                      ),
1967                      '6.1.0'
1968                  );
1969              }
1970          }
1971  
1972          if ( is_array( $value ) ) {
1973              return $value;
1974          }
1975  
1976          // Convert custom CSS properties.
1977          $prefix     = 'var:';
1978          $prefix_len = strlen( $prefix );
1979          $token_in   = '|';
1980          $token_out  = '--';
1981          if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) {
1982              $unwrapped_name = str_replace(
1983                  $token_in,
1984                  $token_out,
1985                  substr( $value, $prefix_len )
1986              );
1987              $value          = "var(--wp--$unwrapped_name)";
1988          }
1989  
1990          return $value;
1991      }
1992  
1993      /**
1994       * Builds metadata for the setting nodes, which returns in the form of:
1995       *
1996       *     [
1997       *       [
1998       *         'path'     => ['path', 'to', 'some', 'node' ],
1999       *         'selector' => 'CSS selector for some node'
2000       *       ],
2001       *       [
2002       *         'path'     => [ 'path', 'to', 'other', 'node' ],
2003       *         'selector' => 'CSS selector for other node'
2004       *       ],
2005       *     ]
2006       *
2007       * @since 5.8.0
2008       *
2009       * @param array $theme_json The tree to extract setting nodes from.
2010       * @param array $selectors  List of selectors per block.
2011       * @return array An array of setting nodes metadata.
2012       */
2013  	protected static function get_setting_nodes( $theme_json, $selectors = array() ) {
2014          $nodes = array();
2015          if ( ! isset( $theme_json['settings'] ) ) {
2016              return $nodes;
2017          }
2018  
2019          // Top-level.
2020          $nodes[] = array(
2021              'path'     => array( 'settings' ),
2022              'selector' => static::ROOT_BLOCK_SELECTOR,
2023          );
2024  
2025          // Calculate paths for blocks.
2026          if ( ! isset( $theme_json['settings']['blocks'] ) ) {
2027              return $nodes;
2028          }
2029  
2030          foreach ( $theme_json['settings']['blocks'] as $name => $node ) {
2031              $selector = null;
2032              if ( isset( $selectors[ $name ]['selector'] ) ) {
2033                  $selector = $selectors[ $name ]['selector'];
2034              }
2035  
2036              $nodes[] = array(
2037                  'path'     => array( 'settings', 'blocks', $name ),
2038                  'selector' => $selector,
2039              );
2040          }
2041  
2042          return $nodes;
2043      }
2044  
2045      /**
2046       * Builds metadata for the style nodes, which returns in the form of:
2047       *
2048       *     [
2049       *       [
2050       *         'path'     => [ 'path', 'to', 'some', 'node' ],
2051       *         'selector' => 'CSS selector for some node',
2052       *         'duotone'  => 'CSS selector for duotone for some node'
2053       *       ],
2054       *       [
2055       *         'path'     => ['path', 'to', 'other', 'node' ],
2056       *         'selector' => 'CSS selector for other node',
2057       *         'duotone'  => null
2058       *       ],
2059       *     ]
2060       *
2061       * @since 5.8.0
2062       *
2063       * @param array $theme_json The tree to extract style nodes from.
2064       * @param array $selectors  List of selectors per block.
2065       * @return array An array of style nodes metadata.
2066       */
2067  	protected static function get_style_nodes( $theme_json, $selectors = array() ) {
2068          $nodes = array();
2069          if ( ! isset( $theme_json['styles'] ) ) {
2070              return $nodes;
2071          }
2072  
2073          // Top-level.
2074          $nodes[] = array(
2075              'path'     => array( 'styles' ),
2076              'selector' => static::ROOT_BLOCK_SELECTOR,
2077          );
2078  
2079          if ( isset( $theme_json['styles']['elements'] ) ) {
2080              foreach ( self::ELEMENTS as $element => $selector ) {
2081                  if ( ! isset( $theme_json['styles']['elements'][ $element ] ) ) {
2082                      continue;
2083                  }
2084                  $nodes[] = array(
2085                      'path'     => array( 'styles', 'elements', $element ),
2086                      'selector' => static::ELEMENTS[ $element ],
2087                  );
2088  
2089                  // Handle any pseudo selectors for the element.
2090                  // TODO: Replace array_key_exists() with isset() check once WordPress drops
2091                  // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
2092                  if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
2093                      foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
2094  
2095                          if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) {
2096                              $nodes[] = array(
2097                                  'path'     => array( 'styles', 'elements', $element ),
2098                                  'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ),
2099                              );
2100                          }
2101                      }
2102                  }
2103              }
2104          }
2105  
2106          // Blocks.
2107          if ( ! isset( $theme_json['styles']['blocks'] ) ) {
2108              return $nodes;
2109          }
2110  
2111          $block_nodes = static::get_block_nodes( $theme_json );
2112          foreach ( $block_nodes as $block_node ) {
2113              $nodes[] = $block_node;
2114          }
2115  
2116          /**
2117           * Filters the list of style nodes with metadata.
2118           *
2119           * This allows for things like loading block CSS independently.
2120           *
2121           * @since 6.1.0
2122           *
2123           * @param array $nodes Style nodes with metadata.
2124           */
2125          return apply_filters( 'wp_theme_json_get_style_nodes', $nodes );
2126      }
2127  
2128      /**
2129       * A public helper to get the block nodes from a theme.json file.
2130       *
2131       * @since 6.1.0
2132       *
2133       * @return array The block nodes in theme.json.
2134       */
2135  	public function get_styles_block_nodes() {
2136          return static::get_block_nodes( $this->theme_json );
2137      }
2138  
2139      /**
2140       * Returns a filtered declarations array if there is a separator block with only a background
2141       * style defined in theme.json by adding a color attribute to reflect the changes in the front.
2142       *
2143       * @since 6.1.1
2144       *
2145       * @param array $declarations List of declarations.
2146       * @return array $declarations List of declarations filtered.
2147       */
2148  	private static function update_separator_declarations( $declarations ) {
2149          $background_color     = '';
2150          $border_color_matches = false;
2151          $text_color_matches   = false;
2152  
2153          foreach ( $declarations as $declaration ) {
2154              if ( 'background-color' === $declaration['name'] && ! $background_color && isset( $declaration['value'] ) ) {
2155                  $background_color = $declaration['value'];
2156              } elseif ( 'border-color' === $declaration['name'] ) {
2157                  $border_color_matches = true;
2158              } elseif ( 'color' === $declaration['name'] ) {
2159                  $text_color_matches = true;
2160              }
2161  
2162              if ( $background_color && $border_color_matches && $text_color_matches ) {
2163                  break;
2164              }
2165          }
2166  
2167          if ( $background_color && ! $border_color_matches && ! $text_color_matches ) {
2168              $declarations[] = array(
2169                  'name'  => 'color',
2170                  'value' => $background_color,
2171              );
2172          }
2173  
2174          return $declarations;
2175      }
2176  
2177      /**
2178       * An internal method to get the block nodes from a theme.json file.
2179       *
2180       * @since 6.1.0
2181       *
2182       * @param array $theme_json The theme.json converted to an array.
2183       * @return array The block nodes in theme.json.
2184       */
2185  	private static function get_block_nodes( $theme_json ) {
2186          $selectors = static::get_blocks_metadata();
2187          $nodes     = array();
2188          if ( ! isset( $theme_json['styles'] ) ) {
2189              return $nodes;
2190          }
2191  
2192          // Blocks.
2193          if ( ! isset( $theme_json['styles']['blocks'] ) ) {
2194              return $nodes;
2195          }
2196  
2197          foreach ( $theme_json['styles']['blocks'] as $name => $node ) {
2198              $selector = null;
2199              if ( isset( $selectors[ $name ]['selector'] ) ) {
2200                  $selector = $selectors[ $name ]['selector'];
2201              }
2202  
2203              $duotone_selector = null;
2204              if ( isset( $selectors[ $name ]['duotone'] ) ) {
2205                  $duotone_selector = $selectors[ $name ]['duotone'];
2206              }
2207  
2208              $feature_selectors = null;
2209              if ( isset( $selectors[ $name ]['features'] ) ) {
2210                  $feature_selectors = $selectors[ $name ]['features'];
2211              }
2212  
2213              $variation_selectors = array();
2214              if ( isset( $node['variations'] ) ) {
2215                  foreach ( $node['variations'] as $variation => $node ) {
2216                      $variation_selectors[] = array(
2217                          'path'     => array( 'styles', 'blocks', $name, 'variations', $variation ),
2218                          'selector' => $selectors[ $name ]['styleVariations'][ $variation ],
2219                      );
2220                  }
2221              }
2222  
2223              $nodes[] = array(
2224                  'name'       => $name,
2225                  'path'       => array( 'styles', 'blocks', $name ),
2226                  'selector'   => $selector,
2227                  'duotone'    => $duotone_selector,
2228                  'features'   => $feature_selectors,
2229                  'variations' => $variation_selectors,
2230              );
2231  
2232              if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
2233                  foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
2234                      $nodes[] = array(
2235                          'path'     => array( 'styles', 'blocks', $name, 'elements', $element ),
2236                          'selector' => $selectors[ $name ]['elements'][ $element ],
2237                      );
2238  
2239                      // Handle any pseudo selectors for the element.
2240                      // TODO: Replace array_key_exists() with isset() check once WordPress drops
2241                      // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
2242                      if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
2243                          foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
2244                              if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
2245                                  $nodes[] = array(
2246                                      'path'     => array( 'styles', 'blocks', $name, 'elements', $element ),
2247                                      'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ),
2248                                  );
2249                              }
2250                          }
2251                      }
2252                  }
2253              }
2254          }
2255  
2256          return $nodes;
2257      }
2258  
2259      /**
2260       * Gets the CSS rules for a particular block from theme.json.
2261       *
2262       * @since 6.1.0
2263       *
2264       * @param array $block_metadata Metadata about the block to get styles for.
2265       *
2266       * @return string Styles for the block.
2267       */
2268  	public function get_styles_for_block( $block_metadata ) {
2269          $node             = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
2270          $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
2271          $selector         = $block_metadata['selector'];
2272          $settings         = _wp_array_get( $this->theme_json, array( 'settings' ) );
2273  
2274          /*
2275           * Process style declarations for block support features the current
2276           * block contains selectors for. Values for a feature with a custom
2277           * selector are filtered from the theme.json node before it is
2278           * processed as normal.
2279          */
2280          $feature_declarations = array();
2281  
2282          if ( ! empty( $block_metadata['features'] ) ) {
2283              foreach ( $block_metadata['features'] as $feature_name => $feature_selector ) {
2284                  if ( ! empty( $node[ $feature_name ] ) ) {
2285                      // Create temporary node containing only the feature data
2286                      // to leverage existing `compute_style_properties` function.
2287                      $feature = array( $feature_name => $node[ $feature_name ] );
2288                      // Generate the feature's declarations only.
2289                      $new_feature_declarations = static::compute_style_properties( $feature, $settings, null, $this->theme_json );
2290  
2291                      // Merge new declarations with any that already exist for
2292                      // the feature selector. This may occur when multiple block
2293                      // support features use the same custom selector.
2294                      if ( isset( $feature_declarations[ $feature_selector ] ) ) {
2295                          foreach ( $new_feature_declarations as $new_feature_declaration ) {
2296                              $feature_declarations[ $feature_selector ][] = $new_feature_declaration;
2297                          }
2298                      } else {
2299                          $feature_declarations[ $feature_selector ] = $new_feature_declarations;
2300                      }
2301  
2302                      // Remove the feature from the block's node now the
2303                      // styles will be included under the feature level selector.
2304                      unset( $node[ $feature_name ] );
2305                  }
2306              }
2307          }
2308  
2309          // If there are style variations, generate the declarations for them, including any feature selectors the block may have.
2310          $style_variation_declarations = array();
2311          if ( ! empty( $block_metadata['variations'] ) ) {
2312              foreach ( $block_metadata['variations'] as $style_variation ) {
2313                  $style_variation_node     = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
2314                  $style_variation_selector = $style_variation['selector'];
2315  
2316                  // If the block has feature selectors, generate the declarations for them within the current style variation.
2317                  if ( ! empty( $block_metadata['features'] ) ) {
2318                      $clean_style_variation_selector = trim( $style_variation_selector );
2319                      foreach ( $block_metadata['features'] as $feature_name => $feature_selector ) {
2320                          if ( empty( $style_variation_node[ $feature_name ] ) ) {
2321                              continue;
2322                          }
2323                          // Prepend the variation selector to the feature selector.
2324                          $split_feature_selectors    = explode( ',', $feature_selector );
2325                          $feature_selectors          = array_map(
2326                              static function( $split_feature_selector ) use ( $clean_style_variation_selector ) {
2327                                  return $clean_style_variation_selector . trim( $split_feature_selector );
2328                              },
2329                              $split_feature_selectors
2330                          );
2331                          $combined_feature_selectors = implode( ',', $feature_selectors );
2332  
2333                          // Compute declarations for the feature.
2334                          $new_feature_declarations = static::compute_style_properties( array( $feature_name => $style_variation_node[ $feature_name ] ), $settings, null, $this->theme_json );
2335  
2336                          /*
2337                           * Merge new declarations with any that already exist for
2338                           * the feature selector. This may occur when multiple block
2339                           * support features use the same custom selector.
2340                           */
2341                          if ( isset( $style_variation_declarations[ $combined_feature_selectors ] ) ) {
2342                              $style_variation_declarations[ $combined_feature_selectors ] = array_merge( $style_variation_declarations[ $combined_feature_selectors ], $new_feature_declarations );
2343                          } else {
2344                              $style_variation_declarations[ $combined_feature_selectors ] = $new_feature_declarations;
2345                          }
2346                          /*
2347                           * Remove the feature from the variation's node now the
2348                           * styles will be included under the feature level selector.
2349                           */
2350                          unset( $style_variation_node[ $feature_name ] );
2351                      }
2352                  }
2353                  // Compute declarations for remaining styles not covered by feature level selectors.
2354                  $style_variation_declarations[ $style_variation_selector ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json );
2355              }
2356          }
2357          /*
2358           * Get a reference to element name from path.
2359           * $block_metadata['path'] = array( 'styles','elements','link' );
2360           * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ].
2361           * Skip non-element paths like just ['styles'].
2362           */
2363          $is_processing_element = in_array( 'elements', $block_metadata['path'], true );
2364  
2365          $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null;
2366  
2367          $element_pseudo_allowed = array();
2368  
2369          // TODO: Replace array_key_exists() with isset() check once WordPress drops
2370          // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
2371          if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
2372              $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ];
2373          }
2374  
2375          /*
2376           * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover").
2377           * This also resets the array keys.
2378           */
2379          $pseudo_matches = array_values(
2380              array_filter(
2381                  $element_pseudo_allowed,
2382                  function( $pseudo_selector ) use ( $selector ) {
2383                      return str_contains( $selector, $pseudo_selector );
2384                  }
2385              )
2386          );
2387  
2388          $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null;
2389  
2390          /*
2391           * If the current selector is a pseudo selector that's defined in the allow list for the current
2392           * element then compute the style properties for it.
2393           * Otherwise just compute the styles for the default selector as normal.
2394           */
2395          if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) &&
2396              // TODO: Replace array_key_exists() with isset() check once WordPress drops
2397              // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
2398              array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS )
2399              && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true )
2400          ) {
2401              $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding );
2402          } else {
2403              $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
2404          }
2405  
2406          $block_rules = '';
2407  
2408          /*
2409           * 1. Separate the declarations that use the general selector
2410           * from the ones using the duotone selector.
2411           */
2412          $declarations_duotone = array();
2413          foreach ( $declarations as $index => $declaration ) {
2414              if ( 'filter' === $declaration['name'] ) {
2415                  unset( $declarations[ $index ] );
2416                  $declarations_duotone[] = $declaration;
2417              }
2418          }
2419  
2420          // Update declarations if there are separators with only background color defined.
2421          if ( '.wp-block-separator' === $selector ) {
2422              $declarations = static::update_separator_declarations( $declarations );
2423          }
2424  
2425          // 2. Generate and append the rules that use the general selector.
2426          $block_rules .= static::to_ruleset( $selector, $declarations );
2427  
2428          // 3. Generate and append the rules that use the duotone selector.
2429          if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
2430              $selector_duotone = static::scope_selector( $block_metadata['selector'], $block_metadata['duotone'] );
2431              $block_rules     .= static::to_ruleset( $selector_duotone, $declarations_duotone );
2432          }
2433  
2434          // 4. Generate Layout block gap styles.
2435          if (
2436              static::ROOT_BLOCK_SELECTOR !== $selector &&
2437              ! empty( $block_metadata['name'] )
2438          ) {
2439              $block_rules .= $this->get_layout_styles( $block_metadata );
2440          }
2441  
2442          // 5. Generate and append the feature level rulesets.
2443          foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) {
2444              $block_rules .= static::to_ruleset( $feature_selector, $individual_feature_declarations );
2445          }
2446  
2447          // 6. Generate and append the style variation rulesets.
2448          foreach ( $style_variation_declarations as $style_variation_selector => $individual_style_variation_declarations ) {
2449              $block_rules .= static::to_ruleset( $style_variation_selector, $individual_style_variation_declarations );
2450          }
2451  
2452          return $block_rules;
2453      }
2454  
2455      /**
2456       * Outputs the CSS for layout rules on the root.
2457       *
2458       * @since 6.1.0
2459       *
2460       * @param string $selector The root node selector.
2461       * @param array  $block_metadata The metadata for the root block.
2462       * @return string The additional root rules CSS.
2463       */
2464  	public function get_root_layout_rules( $selector, $block_metadata ) {
2465          $css              = '';
2466          $settings         = _wp_array_get( $this->theme_json, array( 'settings' ) );
2467          $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
2468  
2469          /*
2470          * Reset default browser margin on the root body element.
2471          * This is set on the root selector **before** generating the ruleset
2472          * from the `theme.json`. This is to ensure that if the `theme.json` declares
2473          * `margin` in its `spacing` declaration for the `body` element then these
2474          * user-generated values take precedence in the CSS cascade.
2475          * @link https://github.com/WordPress/gutenberg/issues/36147.
2476          */
2477          $css .= 'body { margin: 0;';
2478  
2479          /*
2480          * If there are content and wide widths in theme.json, output them
2481          * as custom properties on the body element so all blocks can use them.
2482          */
2483          if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) {
2484              $content_size = isset( $settings['layout']['contentSize'] ) ? $settings['layout']['contentSize'] : $settings['layout']['wideSize'];
2485              $content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial';
2486              $wide_size    = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize'];
2487              $wide_size    = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial';
2488              $css         .= '--wp--style--global--content-size: ' . $content_size . ';';
2489              $css         .= '--wp--style--global--wide-size: ' . $wide_size . ';';
2490          }
2491  
2492          $css .= ' }';
2493  
2494          if ( $use_root_padding ) {
2495              // Top and bottom padding are applied to the outer block container.
2496              $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }';
2497              // Right and left padding are applied to the first container with `.has-global-padding` class.
2498              $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
2499              // Nested containers with `.has-global-padding` class do not get padding.
2500              $css .= '.has-global-padding :where(.has-global-padding) { padding-right: 0; padding-left: 0; }';
2501              // Alignfull children of the container with left and right padding have negative margins so they can still be full width.
2502              $css .= '.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }';
2503              // The above rule is negated for alignfull children of nested containers.
2504              $css .= '.has-global-padding :where(.has-global-padding) > .alignfull { margin-right: 0; margin-left: 0; }';
2505              // Some of the children of alignfull blocks without content width should also get padding: text blocks and non-alignfull container blocks.
2506              $css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
2507              // The above rule also has to be negated for blocks inside nested `.has-global-padding` blocks.
2508              $css .= '.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }';
2509          }
2510  
2511          $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
2512          $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
2513          $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
2514  
2515          $block_gap_value       = _wp_array_get( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ), '0.5em' );
2516          $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
2517          if ( $has_block_gap_support ) {
2518              $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) );
2519              $css            .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }';
2520              $css            .= ".wp-site-blocks > * + * { margin-block-start: $block_gap_value; }";
2521  
2522              // For backwards compatibility, ensure the legacy block gap CSS variable is still available.
2523              $css .= "$selector { --wp--style--block-gap: $block_gap_value; }";
2524          }
2525          $css .= $this->get_layout_styles( $block_metadata );
2526  
2527          return $css;
2528      }
2529  
2530      /**
2531       * For metadata values that can either be booleans or paths to booleans, gets the value.
2532       *
2533       *     $data = array(
2534       *       'color' => array(
2535       *         'defaultPalette' => true
2536       *       )
2537       *     );
2538       *
2539       *     static::get_metadata_boolean( $data, false );
2540       *     // => false
2541       *
2542       *     static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) );
2543       *     // => true
2544       *
2545       * @since 6.0.0
2546       *
2547       * @param array      $data          The data to inspect.
2548       * @param bool|array $path          Boolean or path to a boolean.
2549       * @param bool       $default_value Default value if the referenced path is missing.
2550       *                                  Default false.
2551       * @return bool Value of boolean metadata.
2552       */
2553  	protected static function get_metadata_boolean( $data, $path, $default_value = false ) {
2554          if ( is_bool( $path ) ) {
2555              return $path;
2556          }
2557  
2558          if ( is_array( $path ) ) {
2559              $value = _wp_array_get( $data, $path );
2560              if ( null !== $value ) {
2561                  return $value;
2562              }
2563          }
2564  
2565          return $default_value;
2566      }
2567  
2568      /**
2569       * Merges new incoming data.
2570       *
2571       * @since 5.8.0
2572       * @since 5.9.0 Duotone preset also has origins.
2573       *
2574       * @param WP_Theme_JSON $incoming Data to merge.
2575       */
2576  	public function merge( $incoming ) {
2577          $incoming_data    = $incoming->get_raw_data();
2578          $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
2579  
2580          /*
2581           * The array_replace_recursive algorithm merges at the leaf level,
2582           * but we don't want leaf arrays to be merged, so we overwrite it.
2583           *
2584           * For leaf values that are sequential arrays it will use the numeric indexes for replacement.
2585           * We rather replace the existing with the incoming value, if it exists.
2586           * This is the case of spacing.units.
2587           *
2588           * For leaf values that are associative arrays it will merge them as expected.
2589           * This is also not the behavior we want for the current associative arrays (presets).
2590           * We rather replace the existing with the incoming value, if it exists.
2591           * This happens, for example, when we merge data from theme.json upon existing
2592           * theme supports or when we merge anything coming from the same source twice.
2593           * This is the case of color.palette, color.gradients, color.duotone,
2594           * typography.fontSizes, or typography.fontFamilies.
2595           *
2596           * Additionally, for some preset types, we also want to make sure the
2597           * values they introduce don't conflict with default values. We do so
2598           * by checking the incoming slugs for theme presets and compare them
2599           * with the equivalent default presets: if a slug is present as a default
2600           * we remove it from the theme presets.
2601           */
2602          $nodes        = static::get_setting_nodes( $incoming_data );
2603          $slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) );
2604          foreach ( $nodes as $node ) {
2605              // Replace the spacing.units.
2606              $path   = $node['path'];
2607              $path[] = 'spacing';
2608              $path[] = 'units';
2609  
2610              $content = _wp_array_get( $incoming_data, $path, null );
2611              if ( isset( $content ) ) {
2612                  _wp_array_set( $this->theme_json, $path, $content );
2613              }
2614  
2615              // Replace the presets.
2616              foreach ( static::PRESETS_METADATA as $preset ) {
2617                  $override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true );
2618  
2619                  foreach ( static::VALID_ORIGINS as $origin ) {
2620                      $base_path = $node['path'];
2621                      foreach ( $preset['path'] as $leaf ) {
2622                          $base_path[] = $leaf;
2623                      }
2624  
2625                      $path   = $base_path;
2626                      $path[] = $origin;
2627  
2628                      $content = _wp_array_get( $incoming_data, $path, null );
2629                      if ( ! isset( $content ) ) {
2630                          continue;
2631                      }
2632  
2633                      if ( 'theme' === $origin && $preset['use_default_names'] ) {
2634                          foreach ( $content as $key => $item ) {
2635                              if ( ! isset( $item['name'] ) ) {
2636                                  $name = static::get_name_from_defaults( $item['slug'], $base_path );
2637                                  if ( null !== $name ) {
2638                                      $content[ $key ]['name'] = $name;
2639                                  }
2640                              }
2641                          }
2642                      }
2643  
2644                      if (
2645                          ( 'theme' !== $origin ) ||
2646                          ( 'theme' === $origin && $override_preset )
2647                      ) {
2648                          _wp_array_set( $this->theme_json, $path, $content );
2649                      } else {
2650                          $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] );
2651                          $slugs      = array_merge_recursive( $slugs_global, $slugs_node );
2652  
2653                          $slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() );
2654                          $content          = static::filter_slugs( $content, $slugs_for_preset );
2655                          _wp_array_set( $this->theme_json, $path, $content );
2656                      }
2657                  }
2658              }
2659          }
2660      }
2661  
2662      /**
2663       * Converts all filter (duotone) presets into SVGs.
2664       *
2665       * @since 5.9.1
2666       *
2667       * @param array $origins List of origins to process.
2668       * @return string SVG filters.
2669       */
2670  	public function get_svg_filters( $origins ) {
2671          $blocks_metadata = static::get_blocks_metadata();
2672          $setting_nodes   = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
2673  
2674          $filters = '';
2675          foreach ( $setting_nodes as $metadata ) {
2676              $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
2677              if ( empty( $node['color']['duotone'] ) ) {
2678                  continue;
2679              }
2680  
2681              $duotone_presets = $node['color']['duotone'];
2682  
2683              foreach ( $origins as $origin ) {
2684                  if ( ! isset( $duotone_presets[ $origin ] ) ) {
2685                      continue;
2686                  }
2687                  foreach ( $duotone_presets[ $origin ] as $duotone_preset ) {
2688                      $filters .= wp_get_duotone_filter_svg( $duotone_preset );
2689                  }
2690              }
2691          }
2692  
2693          return $filters;
2694      }
2695  
2696      /**
2697       * Determines whether a presets should be overridden or not.
2698       *
2699       * @since 5.9.0
2700       * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead.
2701       *
2702       * @param array      $theme_json The theme.json like structure to inspect.
2703       * @param array      $path       Path to inspect.
2704       * @param bool|array $override   Data to compute whether to override the preset.
2705       * @return boolean
2706       */
2707  	protected static function should_override_preset( $theme_json, $path, $override ) {
2708          _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' );
2709  
2710          if ( is_bool( $override ) ) {
2711              return $override;
2712          }
2713  
2714          /*
2715           * The relationship between whether to override the defaults
2716           * and whether the defaults are enabled is inverse:
2717           *
2718           * - If defaults are enabled  => theme presets should not be overridden
2719           * - If defaults are disabled => theme presets should be overridden
2720           *
2721           * For example, a theme sets defaultPalette to false,
2722           * making the default palette hidden from the user.
2723           * In that case, we want all the theme presets to be present,
2724           * so they should override the defaults.
2725           */
2726          if ( is_array( $override ) ) {
2727              $value = _wp_array_get( $theme_json, array_merge( $path, $override ) );
2728              if ( isset( $value ) ) {
2729                  return ! $value;
2730              }
2731  
2732              // Search the top-level key if none was found for this node.
2733              $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) );
2734              if ( isset( $value ) ) {
2735                  return ! $value;
2736              }
2737  
2738              return true;
2739          }
2740      }
2741  
2742      /**
2743       * Returns the default slugs for all the presets in an associative array
2744       * whose keys are the preset paths and the leafs is the list of slugs.
2745       *
2746       * For example:
2747       *
2748       *     array(
2749       *       'color' => array(
2750       *         'palette'   => array( 'slug-1', 'slug-2' ),
2751       *         'gradients' => array( 'slug-3', 'slug-4' ),
2752       *       ),
2753       *     )
2754       *
2755       * @since 5.9.0
2756       *
2757       * @param array $data      A theme.json like structure.
2758       * @param array $node_path The path to inspect. It's 'settings' by default.
2759       * @return array
2760       */
2761  	protected static function get_default_slugs( $data, $node_path ) {
2762          $slugs = array();
2763  
2764          foreach ( static::PRESETS_METADATA as $metadata ) {
2765              $path = $node_path;
2766              foreach ( $metadata['path'] as $leaf ) {
2767                  $path[] = $leaf;
2768              }
2769              $path[] = 'default';
2770  
2771              $preset = _wp_array_get( $data, $path, null );
2772              if ( ! isset( $preset ) ) {
2773                  continue;
2774              }
2775  
2776              $slugs_for_preset = array();
2777              foreach ( $preset as $item ) {
2778                  if ( isset( $item['slug'] ) ) {
2779                      $slugs_for_preset[] = $item['slug'];
2780                  }
2781              }
2782  
2783              _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset );
2784          }
2785  
2786          return $slugs;
2787      }
2788  
2789      /**
2790       * Gets a `default`'s preset name by a provided slug.
2791       *
2792       * @since 5.9.0
2793       *
2794       * @param string $slug The slug we want to find a match from default presets.
2795       * @param array  $base_path The path to inspect. It's 'settings' by default.
2796       * @return string|null
2797       */
2798  	protected function get_name_from_defaults( $slug, $base_path ) {
2799          $path            = $base_path;
2800          $path[]          = 'default';
2801          $default_content = _wp_array_get( $this->theme_json, $path, null );
2802          if ( ! $default_content ) {
2803              return null;
2804          }
2805          foreach ( $default_content as $item ) {
2806              if ( $slug === $item['slug'] ) {
2807                  return $item['name'];
2808              }
2809          }
2810          return null;
2811      }
2812  
2813      /**
2814       * Removes the preset values whose slug is equal to any of given slugs.
2815       *
2816       * @since 5.9.0
2817       *
2818       * @param array $node  The node with the presets to validate.
2819       * @param array $slugs The slugs that should not be overridden.
2820       * @return array The new node.
2821       */
2822  	protected static function filter_slugs( $node, $slugs ) {
2823          if ( empty( $slugs ) ) {
2824              return $node;
2825          }
2826  
2827          $new_node = array();
2828          foreach ( $node as $value ) {
2829              if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) {
2830                  $new_node[] = $value;
2831              }
2832          }
2833  
2834          return $new_node;
2835      }
2836  
2837      /**
2838       * Removes insecure data from theme.json.
2839       *
2840       * @since 5.9.0
2841       *
2842       * @param array $theme_json Structure to sanitize.
2843       * @return array Sanitized structure.
2844       */
2845  	public static function remove_insecure_properties( $theme_json ) {
2846          $sanitized = array();
2847  
2848          $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json );
2849  
2850          $valid_block_names   = array_keys( static::get_blocks_metadata() );
2851          $valid_element_names = array_keys( static::ELEMENTS );
2852  
2853          $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names );
2854  
2855          $blocks_metadata = static::get_blocks_metadata();
2856          $style_nodes     = static::get_style_nodes( $theme_json, $blocks_metadata );
2857  
2858          foreach ( $style_nodes as $metadata ) {
2859              $input = _wp_array_get( $theme_json, $metadata['path'], array() );
2860              if ( empty( $input ) ) {
2861                  continue;
2862              }
2863  
2864              // The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability.
2865              if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) {
2866                  $output = $input;
2867              } else {
2868                  $output = static::remove_insecure_styles( $input );
2869              }
2870  
2871              /*
2872               * Get a reference to element name from path.
2873               * $metadata['path'] = array( 'styles', 'elements', 'link' );
2874               */
2875              $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ];
2876  
2877              /*
2878               * $output is stripped of pseudo selectors. Re-add and process them
2879               * or insecure styles here.
2880               */
2881              // TODO: Replace array_key_exists() with isset() check once WordPress drops
2882              // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
2883              if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
2884                  foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) {
2885                      if ( isset( $input[ $pseudo_selector ] ) ) {
2886                          $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] );
2887                      }
2888                  }
2889              }
2890  
2891              if ( ! empty( $output ) ) {
2892                  _wp_array_set( $sanitized, $metadata['path'], $output );
2893              }
2894          }
2895  
2896          $setting_nodes = static::get_setting_nodes( $theme_json );
2897          foreach ( $setting_nodes as $metadata ) {
2898              $input = _wp_array_get( $theme_json, $metadata['path'], array() );
2899              if ( empty( $input ) ) {
2900                  continue;
2901              }
2902  
2903              $output = static::remove_insecure_settings( $input );
2904              if ( ! empty( $output ) ) {
2905                  _wp_array_set( $sanitized, $metadata['path'], $output );
2906              }
2907          }
2908  
2909          if ( empty( $sanitized['styles'] ) ) {
2910              unset( $theme_json['styles'] );
2911          } else {
2912              $theme_json['styles'] = $sanitized['styles'];
2913          }
2914  
2915          if ( empty( $sanitized['settings'] ) ) {
2916              unset( $theme_json['settings'] );
2917          } else {
2918              $theme_json['settings'] = $sanitized['settings'];
2919          }
2920  
2921          return $theme_json;
2922      }
2923  
2924      /**
2925       * Processes a setting node and returns the same node
2926       * without the insecure settings.
2927       *
2928       * @since 5.9.0
2929       *
2930       * @param array $input Node to process.
2931       * @return array
2932       */
2933  	protected static function remove_insecure_settings( $input ) {
2934          $output = array();
2935          foreach ( static::PRESETS_METADATA as $preset_metadata ) {
2936              foreach ( static::VALID_ORIGINS as $origin ) {
2937                  $path_with_origin   = $preset_metadata['path'];
2938                  $path_with_origin[] = $origin;
2939                  $presets            = _wp_array_get( $input, $path_with_origin, null );
2940                  if ( null === $presets ) {
2941                      continue;
2942                  }
2943  
2944                  $escaped_preset = array();
2945                  foreach ( $presets as $preset ) {
2946                      if (
2947                          esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] &&
2948                          sanitize_html_class( $preset['slug'] ) === $preset['slug']
2949                      ) {
2950                          $value = null;
2951                          if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
2952                              $value = $preset[ $preset_metadata['value_key'] ];
2953                          } elseif (
2954                              isset( $preset_metadata['value_func'] ) &&
2955                              is_callable( $preset_metadata['value_func'] )
2956                          ) {
2957                              $value = call_user_func( $preset_metadata['value_func'], $preset );
2958                          }
2959  
2960                          $preset_is_valid = true;
2961                          foreach ( $preset_metadata['properties'] as $property ) {
2962                              if ( ! static::is_safe_css_declaration( $property, $value ) ) {
2963                                  $preset_is_valid = false;
2964                                  break;
2965                              }
2966                          }
2967  
2968                          if ( $preset_is_valid ) {
2969                              $escaped_preset[] = $preset;
2970                          }
2971                      }
2972                  }
2973  
2974                  if ( ! empty( $escaped_preset ) ) {
2975                      _wp_array_set( $output, $path_with_origin, $escaped_preset );
2976                  }
2977              }
2978          }
2979  
2980          // Ensure indirect properties not included in any `PRESETS_METADATA` value are allowed.
2981          static::remove_indirect_properties( $input, $output );
2982  
2983          return $output;
2984      }
2985  
2986      /**
2987       * Processes a style node and returns the same node
2988       * without the insecure styles.
2989       *
2990       * @since 5.9.0
2991       *
2992       * @param array $input Node to process.
2993       * @return array
2994       */
2995  	protected static function remove_insecure_styles( $input ) {
2996          $output       = array();
2997          $declarations = static::compute_style_properties( $input );
2998  
2999          foreach ( $declarations as $declaration ) {
3000              if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) {
3001                  $path = static::PROPERTIES_METADATA[ $declaration['name'] ];
3002  
3003                  // Check the value isn't an array before adding so as to not
3004                  // double up shorthand and longhand styles.
3005                  $value = _wp_array_get( $input, $path, array() );
3006                  if ( ! is_array( $value ) ) {
3007                      _wp_array_set( $output, $path, $value );
3008                  }
3009              }
3010          }
3011  
3012          // Ensure indirect properties not handled by `compute_style_properties` are allowed.
3013          static::remove_indirect_properties( $input, $output );
3014  
3015          return $output;
3016      }
3017  
3018      /**
3019       * Checks that a declaration provided by the user is safe.
3020       *
3021       * @since 5.9.0
3022       *
3023       * @param string $property_name  Property name in a CSS declaration, i.e. the `color` in `color: red`.
3024       * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`.
3025       * @return bool
3026       */
3027  	protected static function is_safe_css_declaration( $property_name, $property_value ) {
3028          $style_to_validate = $property_name . ': ' . $property_value;
3029          $filtered          = esc_html( safecss_filter_attr( $style_to_validate ) );
3030          return ! empty( trim( $filtered ) );
3031      }
3032  
3033      /**
3034       * Removes indirect properties from the given input node and
3035       * sets in the given output node.
3036       *
3037       * @since 6.2.0
3038       *
3039       * @param array $input  Node to process.
3040       * @param array $output The processed node. Passed by reference.
3041       */
3042  	private static function remove_indirect_properties( $input, &$output ) {
3043          foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) {
3044              foreach ( $paths as $path ) {
3045                  $value = _wp_array_get( $input, $path );
3046                  if (
3047                      is_string( $value ) &&
3048                      static::is_safe_css_declaration( $property, $value )
3049                  ) {
3050                      _wp_array_set( $output, $path, $value );
3051                  }
3052              }
3053          }
3054      }
3055  
3056      /**
3057       * Returns the raw data.
3058       *
3059       * @since 5.8.0
3060       *
3061       * @return array Raw data.
3062       */
3063  	public function get_raw_data() {
3064          return $this->theme_json;
3065      }
3066  
3067      /**
3068       * Transforms the given editor settings according the
3069       * add_theme_support format to the theme.json format.
3070       *
3071       * @since 5.8.0
3072       *
3073       * @param array $settings Existing editor settings.
3074       * @return array Config that adheres to the theme.json schema.
3075       */
3076  	public static function get_from_editor_settings( $settings ) {
3077          $theme_settings = array(
3078              'version'  => static::LATEST_SCHEMA,
3079              'settings' => array(),
3080          );
3081  
3082          // Deprecated theme supports.
3083          if ( isset( $settings['disableCustomColors'] ) ) {
3084              if ( ! isset( $theme_settings['settings']['color'] ) ) {
3085                  $theme_settings['settings']['color'] = array();
3086              }
3087              $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors'];
3088          }
3089  
3090          if ( isset( $settings['disableCustomGradients'] ) ) {
3091              if ( ! isset( $theme_settings['settings']['color'] ) ) {
3092                  $theme_settings['settings']['color'] = array();
3093              }
3094              $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients'];
3095          }
3096  
3097          if ( isset( $settings['disableCustomFontSizes'] ) ) {
3098              if ( ! isset( $theme_settings['settings']['typography'] ) ) {
3099                  $theme_settings['settings']['typography'] = array();
3100              }
3101              $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes'];
3102          }
3103  
3104          if ( isset( $settings['enableCustomLineHeight'] ) ) {
3105              if ( ! isset( $theme_settings['settings']['typography'] ) ) {
3106                  $theme_settings['settings']['typography'] = array();
3107              }
3108              $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight'];
3109          }
3110  
3111          if ( isset( $settings['enableCustomUnits'] ) ) {
3112              if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
3113                  $theme_settings['settings']['spacing'] = array();
3114              }
3115              $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ?
3116                  array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) :
3117                  $settings['enableCustomUnits'];
3118          }
3119  
3120          if ( isset( $settings['colors'] ) ) {
3121              if ( ! isset( $theme_settings['settings']['color'] ) ) {
3122                  $theme_settings['settings']['color'] = array();
3123              }
3124              $theme_settings['settings']['color']['palette'] = $settings['colors'];
3125          }
3126  
3127          if ( isset( $settings['gradients'] ) ) {
3128              if ( ! isset( $theme_settings['settings']['color'] ) ) {
3129                  $theme_settings['settings']['color'] = array();
3130              }
3131              $theme_settings['settings']['color']['gradients'] = $settings['gradients'];
3132          }
3133  
3134          if ( isset( $settings['fontSizes'] ) ) {
3135              $font_sizes = $settings['fontSizes'];
3136              // Back-compatibility for presets without units.
3137              foreach ( $font_sizes as $key => $font_size ) {
3138                  if ( is_numeric( $font_size['size'] ) ) {
3139                      $font_sizes[ $key ]['size'] = $font_size['size'] . 'px';
3140                  }
3141              }
3142              if ( ! isset( $theme_settings['settings']['typography'] ) ) {
3143                  $theme_settings['settings']['typography'] = array();
3144              }
3145              $theme_settings['settings']['typography']['fontSizes'] = $font_sizes;
3146          }
3147  
3148          if ( isset( $settings['enableCustomSpacing'] ) ) {
3149              if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
3150                  $theme_settings['settings']['spacing'] = array();
3151              }
3152              $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing'];
3153          }
3154  
3155          return $theme_settings;
3156      }
3157  
3158      /**
3159       * Returns the current theme's wanted patterns(slugs) to be
3160       * registered from Pattern Directory.
3161       *
3162       * @since 6.0.0
3163       *
3164       * @return string[]
3165       */
3166  	public function get_patterns() {
3167          if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) {
3168              return $this->theme_json['patterns'];
3169          }
3170          return array();
3171      }
3172  
3173      /**
3174       * Returns a valid theme.json as provided by a theme.
3175       *
3176       * Unlike get_raw_data() this returns the presets flattened, as provided by a theme.
3177       * This also uses appearanceTools instead of their opt-ins if all of them are true.
3178       *
3179       * @since 6.0.0
3180       *
3181       * @return array
3182       */
3183  	public function get_data() {
3184          $output = $this->theme_json;
3185          $nodes  = static::get_setting_nodes( $output );
3186  
3187          /**
3188           * Flatten the theme & custom origins into a single one.
3189           *
3190           * For example, the following:
3191           *
3192           * {
3193           *   "settings": {
3194           *     "color": {
3195           *       "palette": {
3196           *         "theme": [ {} ],
3197           *         "custom": [ {} ]
3198           *       }
3199           *     }
3200           *   }
3201           * }
3202           *
3203           * will be converted to:
3204           *
3205           * {
3206           *   "settings": {
3207           *     "color": {
3208           *       "palette": [ {} ]
3209           *     }
3210           *   }
3211           * }
3212           */
3213          foreach ( $nodes as $node ) {
3214              foreach ( static::PRESETS_METADATA as $preset_metadata ) {
3215                  $path = $node['path'];
3216                  foreach ( $preset_metadata['path'] as $preset_metadata_path ) {
3217                      $path[] = $preset_metadata_path;
3218                  }
3219                  $preset = _wp_array_get( $output, $path, null );
3220                  if ( null === $preset ) {
3221                      continue;
3222                  }
3223  
3224                  $items = array();
3225                  if ( isset( $preset['theme'] ) ) {
3226                      foreach ( $preset['theme'] as $item ) {
3227                          $slug = $item['slug'];
3228                          unset( $item['slug'] );
3229                          $items[ $slug ] = $item;
3230                      }
3231                  }
3232                  if ( isset( $preset['custom'] ) ) {
3233                      foreach ( $preset['custom'] as $item ) {
3234                          $slug = $item['slug'];
3235                          unset( $item['slug'] );
3236                          $items[ $slug ] = $item;
3237                      }
3238                  }
3239                  $flattened_preset = array();
3240                  foreach ( $items as $slug => $value ) {
3241                      $flattened_preset[] = array_merge( array( 'slug' => (string) $slug ), $value );
3242                  }
3243                  _wp_array_set( $output, $path, $flattened_preset );
3244              }
3245          }
3246  
3247          // If all of the static::APPEARANCE_TOOLS_OPT_INS are true,
3248          // this code unsets them and sets 'appearanceTools' instead.
3249          foreach ( $nodes as $node ) {
3250              $all_opt_ins_are_set = true;
3251              foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
3252                  $full_path = $node['path'];
3253                  foreach ( $opt_in_path as $opt_in_path_item ) {
3254                      $full_path[] = $opt_in_path_item;
3255                  }
3256                  // Use "unset prop" as a marker instead of "null" because
3257                  // "null" can be a valid value for some props (e.g. blockGap).
3258                  $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
3259                  if ( 'unset prop' === $opt_in_value ) {
3260                      $all_opt_ins_are_set = false;
3261                      break;
3262                  }
3263              }
3264  
3265              if ( $all_opt_ins_are_set ) {
3266                  $node_path_with_appearance_tools   = $node['path'];
3267                  $node_path_with_appearance_tools[] = 'appearanceTools';
3268                  _wp_array_set( $output, $node_path_with_appearance_tools, true );
3269                  foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
3270                      $full_path = $node['path'];
3271                      foreach ( $opt_in_path as $opt_in_path_item ) {
3272                          $full_path[] = $opt_in_path_item;
3273                      }
3274                      // Use "unset prop" as a marker instead of "null" because
3275                      // "null" can be a valid value for some props (e.g. blockGap).
3276                      $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
3277                      if ( true !== $opt_in_value ) {
3278                          continue;
3279                      }
3280  
3281                      // The following could be improved to be path independent.
3282                      // At the moment it relies on a couple of assumptions:
3283                      //
3284                      // - all opt-ins having a path of size 2.
3285                      // - there's two sources of settings: the top-level and the block-level.
3286                      if (
3287                          ( 1 === count( $node['path'] ) ) &&
3288                          ( 'settings' === $node['path'][0] )
3289                      ) {
3290                          // Top-level settings.
3291                          unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] );
3292                          if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) {
3293                              unset( $output['settings'][ $opt_in_path[0] ] );
3294                          }
3295                      } elseif (
3296                          ( 3 === count( $node['path'] ) ) &&
3297                          ( 'settings' === $node['path'][0] ) &&
3298                          ( 'blocks' === $node['path'][1] )
3299                      ) {
3300                          // Block-level settings.
3301                          $block_name = $node['path'][2];
3302                          unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] );
3303                          if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) {
3304                              unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] );
3305                          }
3306                      }
3307                  }
3308              }
3309          }
3310  
3311          wp_recursive_ksort( $output );
3312  
3313          return $output;
3314      }
3315  
3316      /**
3317       * Sets the spacingSizes array based on the spacingScale values from theme.json.
3318       *
3319       * @since 6.1.0
3320       *
3321       * @return null|void
3322       */
3323  	public function set_spacing_sizes() {
3324          $spacing_scale = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'spacingScale' ), array() );
3325  
3326          if ( ! isset( $spacing_scale['steps'] )
3327              || ! is_numeric( $spacing_scale['steps'] )
3328              || ! isset( $spacing_scale['mediumStep'] )
3329              || ! isset( $spacing_scale['unit'] )
3330              || ! isset( $spacing_scale['operator'] )
3331              || ! isset( $spacing_scale['increment'] )
3332              || ! isset( $spacing_scale['steps'] )
3333              || ! is_numeric( $spacing_scale['increment'] )
3334              || ! is_numeric( $spacing_scale['mediumStep'] )
3335              || ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ) {
3336              if ( ! empty( $spacing_scale ) ) {
3337                  trigger_error( __( 'Some of the theme.json settings.spacing.spacingScale values are invalid' ), E_USER_NOTICE );
3338              }
3339              return null;
3340          }
3341  
3342          // If theme authors want to prevent the generation of the core spacing scale they can set their theme.json spacingScale.steps to 0.
3343          if ( 0 === $spacing_scale['steps'] ) {
3344              return null;
3345          }
3346  
3347          $unit            = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] );
3348          $current_step    = $spacing_scale['mediumStep'];
3349          $steps_mid_point = round( $spacing_scale['steps'] / 2, 0 );
3350          $x_small_count   = null;
3351          $below_sizes     = array();
3352          $slug            = 40;
3353          $remainder       = 0;
3354  
3355          for ( $below_midpoint_count = $steps_mid_point - 1; $spacing_scale['steps'] > 1 && $slug > 0 && $below_midpoint_count > 0; $below_midpoint_count-- ) {
3356              if ( '+' === $spacing_scale['operator'] ) {
3357                  $current_step -= $spacing_scale['increment'];
3358              } elseif ( $spacing_scale['increment'] > 1 ) {
3359                  $current_step /= $spacing_scale['increment'];
3360              } else {
3361                  $current_step *= $spacing_scale['increment'];
3362              }
3363  
3364              if ( $current_step <= 0 ) {
3365                  $remainder = $below_midpoint_count;
3366                  break;
3367              }
3368  
3369              $below_sizes[] = array(
3370                  /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Small. */
3371                  'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small' ) : sprintf( __( '%sX-Small' ), (string) $x_small_count ),
3372                  'slug' => (string) $slug,
3373                  'size' => round( $current_step, 2 ) . $unit,
3374              );
3375  
3376              if ( $below_midpoint_count === $steps_mid_point - 2 ) {
3377                  $x_small_count = 2;
3378              }
3379  
3380              if ( $below_midpoint_count < $steps_mid_point - 2 ) {
3381                  $x_small_count++;
3382              }
3383  
3384              $slug -= 10;
3385          }
3386  
3387          $below_sizes = array_reverse( $below_sizes );
3388  
3389          $below_sizes[] = array(
3390              'name' => __( 'Medium' ),
3391              'slug' => '50',
3392              'size' => $spacing_scale['mediumStep'] . $unit,
3393          );
3394  
3395          $current_step  = $spacing_scale['mediumStep'];
3396          $x_large_count = null;
3397          $above_sizes   = array();
3398          $slug          = 60;
3399          $steps_above   = ( $spacing_scale['steps'] - $steps_mid_point ) + $remainder;
3400  
3401          for ( $above_midpoint_count = 0; $above_midpoint_count < $steps_above; $above_midpoint_count++ ) {
3402              $current_step = '+' === $spacing_scale['operator']
3403                  ? $current_step + $spacing_scale['increment']
3404                  : ( $spacing_scale['increment'] >= 1 ? $current_step * $spacing_scale['increment'] : $current_step / $spacing_scale['increment'] );
3405  
3406              $above_sizes[] = array(
3407                  /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Large. */
3408                  'name' => 0 === $above_midpoint_count ? __( 'Large' ) : sprintf( __( '%sX-Large' ), (string) $x_large_count ),
3409                  'slug' => (string) $slug,
3410                  'size' => round( $current_step, 2 ) . $unit,
3411              );
3412  
3413              if ( 1 === $above_midpoint_count ) {
3414                  $x_large_count = 2;
3415              }
3416  
3417              if ( $above_midpoint_count > 1 ) {
3418                  $x_large_count++;
3419              }
3420  
3421              $slug += 10;
3422          }
3423  
3424          $spacing_sizes = $below_sizes;
3425          foreach ( $above_sizes as $above_sizes_item ) {
3426              $spacing_sizes[] = $above_sizes_item;
3427          }
3428  
3429          // If there are 7 or fewer steps in the scale revert to numbers for labels instead of t-shirt sizes.
3430          if ( $spacing_scale['steps'] <= 7 ) {
3431              for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) {
3432                  $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 );
3433              }
3434          }
3435  
3436          _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes );
3437      }
3438  }


Generated : Sun Jun 4 08:20:02 2023 Cross-referenced by PHPXref