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


Generated : Thu May 9 08:20:02 2024 Cross-referenced by PHPXref