[ 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       * @since 6.6.0 Added the `dimensions.aspectRatios` and `dimensions.defaultAspectRatios` presets.
 127       *              Updated the 'prevent_override' value for font size presets to use 'typography.defaultFontSizes'
 128       *              and spacing size presets to use `spacing.defaultSpacingSizes`.
 129       * @since 6.9.0 Added `border.radiusSizes`.
 130       * @var array
 131       */
 132      const PRESETS_METADATA = array(
 133          array(
 134              'path'              => array( 'dimensions', 'aspectRatios' ),
 135              'prevent_override'  => array( 'dimensions', 'defaultAspectRatios' ),
 136              'use_default_names' => false,
 137              'value_key'         => 'ratio',
 138              'css_vars'          => '--wp--preset--aspect-ratio--$slug',
 139              'classes'           => array(),
 140              'properties'        => array( 'aspect-ratio' ),
 141          ),
 142          array(
 143              'path'              => array( 'color', 'palette' ),
 144              'prevent_override'  => array( 'color', 'defaultPalette' ),
 145              'use_default_names' => false,
 146              'value_key'         => 'color',
 147              'css_vars'          => '--wp--preset--color--$slug',
 148              'classes'           => array(
 149                  '.has-$slug-color'            => 'color',
 150                  '.has-$slug-background-color' => 'background-color',
 151                  '.has-$slug-border-color'     => 'border-color',
 152              ),
 153              'properties'        => array( 'color', 'background-color', 'border-color' ),
 154          ),
 155          array(
 156              'path'              => array( 'color', 'gradients' ),
 157              'prevent_override'  => array( 'color', 'defaultGradients' ),
 158              'use_default_names' => false,
 159              'value_key'         => 'gradient',
 160              'css_vars'          => '--wp--preset--gradient--$slug',
 161              'classes'           => array( '.has-$slug-gradient-background' => 'background' ),
 162              'properties'        => array( 'background' ),
 163          ),
 164          array(
 165              'path'              => array( 'color', 'duotone' ),
 166              'prevent_override'  => array( 'color', 'defaultDuotone' ),
 167              'use_default_names' => false,
 168              'value_func'        => null, // CSS Custom Properties for duotone are handled by block supports in class-wp-duotone.php.
 169              'css_vars'          => null,
 170              'classes'           => array(),
 171              'properties'        => array( 'filter' ),
 172          ),
 173          array(
 174              'path'              => array( 'typography', 'fontSizes' ),
 175              'prevent_override'  => array( 'typography', 'defaultFontSizes' ),
 176              'use_default_names' => true,
 177              'value_func'        => 'wp_get_typography_font_size_value',
 178              'css_vars'          => '--wp--preset--font-size--$slug',
 179              'classes'           => array( '.has-$slug-font-size' => 'font-size' ),
 180              'properties'        => array( 'font-size' ),
 181          ),
 182          array(
 183              'path'              => array( 'typography', 'fontFamilies' ),
 184              'prevent_override'  => false,
 185              'use_default_names' => false,
 186              'value_key'         => 'fontFamily',
 187              'css_vars'          => '--wp--preset--font-family--$slug',
 188              'classes'           => array( '.has-$slug-font-family' => 'font-family' ),
 189              'properties'        => array( 'font-family' ),
 190          ),
 191          array(
 192              'path'              => array( 'spacing', 'spacingSizes' ),
 193              'prevent_override'  => array( 'spacing', 'defaultSpacingSizes' ),
 194              'use_default_names' => true,
 195              'value_key'         => 'size',
 196              'css_vars'          => '--wp--preset--spacing--$slug',
 197              'classes'           => array(),
 198              'properties'        => array( 'padding', 'margin' ),
 199          ),
 200          array(
 201              'path'              => array( 'shadow', 'presets' ),
 202              'prevent_override'  => array( 'shadow', 'defaultPresets' ),
 203              'use_default_names' => false,
 204              'value_key'         => 'shadow',
 205              'css_vars'          => '--wp--preset--shadow--$slug',
 206              'classes'           => array(),
 207              'properties'        => array( 'box-shadow' ),
 208          ),
 209          array(
 210              'path'              => array( 'border', 'radiusSizes' ),
 211              'prevent_override'  => false,
 212              'use_default_names' => false,
 213              'value_key'         => 'size',
 214              'css_vars'          => '--wp--preset--border-radius--$slug',
 215              'classes'           => array(),
 216              'properties'        => array( 'border-radius' ),
 217          ),
 218          array(
 219              'path'              => array( 'dimensions', 'dimensionSizes' ),
 220              'prevent_override'  => false,
 221              'use_default_names' => false,
 222              'value_key'         => 'size',
 223              'css_vars'          => '--wp--preset--dimension--$slug',
 224              'classes'           => array(),
 225              'properties'        => array( 'width', 'height', 'min-height' ),
 226          ),
 227      );
 228  
 229      /**
 230       * Metadata for style properties.
 231       *
 232       * Each element is a direct mapping from the CSS property name to the
 233       * path to the value in theme.json & block attributes.
 234       *
 235       * @since 5.8.0
 236       * @since 5.9.0 Added the `border-*`, `font-family`, `font-style`, `font-weight`,
 237       *              `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`,
 238       *              `text-decoration`, `text-transform`, and `filter` properties,
 239       *              simplified the metadata structure.
 240       * @since 6.1.0 Added the `border-*-color`, `border-*-width`, `border-*-style`,
 241       *              `--wp--style--root--padding-*`, and `box-shadow` properties,
 242       *              removed the `--wp--style--block-gap` property.
 243       * @since 6.2.0 Added `outline-*`, and `min-height` properties.
 244       * @since 6.3.0 Added `column-count` property.
 245       * @since 6.4.0 Added `writing-mode` property.
 246       * @since 6.5.0 Added `aspect-ratio` property.
 247       * @since 6.6.0 Added `background-[image|position|repeat|size]` properties.
 248       * @since 6.7.0 Added `background-attachment` property.
 249       * @since 7.0.0 Added `dimensions.width` and `dimensions.height`.
 250       *              Added `text-indent` property.
 251       * @since 7.1.0 Added `min-width` and `text-shadow`.
 252       * @var array
 253       */
 254      const PROPERTIES_METADATA = array(
 255          'aspect-ratio'                      => array( 'dimensions', 'aspectRatio' ),
 256          'background'                        => array( 'color', 'gradient' ),
 257          'background-color'                  => array( 'color', 'background' ),
 258          'background-image'                  => array( 'background', 'backgroundImage' ),
 259          'background-position'               => array( 'background', 'backgroundPosition' ),
 260          'background-repeat'                 => array( 'background', 'backgroundRepeat' ),
 261          'background-size'                   => array( 'background', 'backgroundSize' ),
 262          'background-attachment'             => array( 'background', 'backgroundAttachment' ),
 263          'border-radius'                     => array( 'border', 'radius' ),
 264          'border-top-left-radius'            => array( 'border', 'radius', 'topLeft' ),
 265          'border-top-right-radius'           => array( 'border', 'radius', 'topRight' ),
 266          'border-bottom-left-radius'         => array( 'border', 'radius', 'bottomLeft' ),
 267          'border-bottom-right-radius'        => array( 'border', 'radius', 'bottomRight' ),
 268          'border-color'                      => array( 'border', 'color' ),
 269          'border-width'                      => array( 'border', 'width' ),
 270          'border-style'                      => array( 'border', 'style' ),
 271          'border-top-color'                  => array( 'border', 'top', 'color' ),
 272          'border-top-width'                  => array( 'border', 'top', 'width' ),
 273          'border-top-style'                  => array( 'border', 'top', 'style' ),
 274          'border-right-color'                => array( 'border', 'right', 'color' ),
 275          'border-right-width'                => array( 'border', 'right', 'width' ),
 276          'border-right-style'                => array( 'border', 'right', 'style' ),
 277          'border-bottom-color'               => array( 'border', 'bottom', 'color' ),
 278          'border-bottom-width'               => array( 'border', 'bottom', 'width' ),
 279          'border-bottom-style'               => array( 'border', 'bottom', 'style' ),
 280          'border-left-color'                 => array( 'border', 'left', 'color' ),
 281          'border-left-width'                 => array( 'border', 'left', 'width' ),
 282          'border-left-style'                 => array( 'border', 'left', 'style' ),
 283          'color'                             => array( 'color', 'text' ),
 284          'text-align'                        => array( 'typography', 'textAlign' ),
 285          'column-count'                      => array( 'typography', 'textColumns' ),
 286          'font-family'                       => array( 'typography', 'fontFamily' ),
 287          'font-size'                         => array( 'typography', 'fontSize' ),
 288          'font-style'                        => array( 'typography', 'fontStyle' ),
 289          'font-weight'                       => array( 'typography', 'fontWeight' ),
 290          'letter-spacing'                    => array( 'typography', 'letterSpacing' ),
 291          'line-height'                       => array( 'typography', 'lineHeight' ),
 292          'margin'                            => array( 'spacing', 'margin' ),
 293          'margin-top'                        => array( 'spacing', 'margin', 'top' ),
 294          'margin-right'                      => array( 'spacing', 'margin', 'right' ),
 295          'margin-bottom'                     => array( 'spacing', 'margin', 'bottom' ),
 296          'margin-left'                       => array( 'spacing', 'margin', 'left' ),
 297          'min-height'                        => array( 'dimensions', 'minHeight' ),
 298          'min-width'                         => array( 'dimensions', 'minWidth' ),
 299          'outline-color'                     => array( 'outline', 'color' ),
 300          'outline-offset'                    => array( 'outline', 'offset' ),
 301          'outline-style'                     => array( 'outline', 'style' ),
 302          'outline-width'                     => array( 'outline', 'width' ),
 303          'padding'                           => array( 'spacing', 'padding' ),
 304          'padding-top'                       => array( 'spacing', 'padding', 'top' ),
 305          'padding-right'                     => array( 'spacing', 'padding', 'right' ),
 306          'padding-bottom'                    => array( 'spacing', 'padding', 'bottom' ),
 307          'padding-left'                      => array( 'spacing', 'padding', 'left' ),
 308          '--wp--style--root--padding'        => array( 'spacing', 'padding' ),
 309          '--wp--style--root--padding-top'    => array( 'spacing', 'padding', 'top' ),
 310          '--wp--style--root--padding-right'  => array( 'spacing', 'padding', 'right' ),
 311          '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
 312          '--wp--style--root--padding-left'   => array( 'spacing', 'padding', 'left' ),
 313          'text-decoration'                   => array( 'typography', 'textDecoration' ),
 314          'text-shadow'                       => array( 'typography', 'textShadow' ),
 315          'text-transform'                    => array( 'typography', 'textTransform' ),
 316          'text-indent'                       => array( 'typography', 'textIndent' ),
 317          'filter'                            => array( 'filter', 'duotone' ),
 318          'box-shadow'                        => array( 'shadow' ),
 319          'height'                            => array( 'dimensions', 'height' ),
 320          'width'                             => array( 'dimensions', 'width' ),
 321          'writing-mode'                      => array( 'typography', 'writingMode' ),
 322      );
 323  
 324      /**
 325       * Indirect metadata for style properties that are not directly output.
 326       *
 327       * Each element maps from a CSS property name to an array of
 328       * paths to the value in theme.json & block attributes.
 329       *
 330       * Indirect properties are not output directly by `compute_style_properties`,
 331       * but are used elsewhere in the processing of global styles. The indirect
 332       * property is used to validate whether a style value is allowed.
 333       *
 334       * @since 6.2.0
 335       * @since 6.6.0 Added background-image properties.
 336       * @since 7.1.0 Added `background.gradient` to `background-image` paths.
 337       * @var array
 338       */
 339      const INDIRECT_PROPERTIES_METADATA = array(
 340          'gap'              => array(
 341              array( 'spacing', 'blockGap' ),
 342          ),
 343          'column-gap'       => array(
 344              array( 'spacing', 'blockGap', 'left' ),
 345          ),
 346          'row-gap'          => array(
 347              array( 'spacing', 'blockGap', 'top' ),
 348          ),
 349          'max-width'        => array(
 350              array( 'layout', 'contentSize' ),
 351              array( 'layout', 'wideSize' ),
 352          ),
 353          'background-image' => array(
 354              array( 'background', 'backgroundImage', 'url' ),
 355              array( 'background', 'gradient' ),
 356          ),
 357      );
 358  
 359      /**
 360       * Protected style properties.
 361       *
 362       * These style properties are only rendered if a setting enables it
 363       * via a value other than `null`.
 364       *
 365       * Each element maps the style property to the corresponding theme.json
 366       * setting key.
 367       *
 368       * @since 5.9.0
 369       * @var array
 370       */
 371      const PROTECTED_PROPERTIES = array(
 372          'spacing.blockGap' => array( 'spacing', 'blockGap' ),
 373      );
 374  
 375      /**
 376       * The top-level keys a theme.json can have.
 377       *
 378       * @since 5.8.0 As `ALLOWED_TOP_LEVEL_KEYS`.
 379       * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`,
 380       *              added the `customTemplates` and `templateParts` values.
 381       * @since 6.3.0 Added the `description` value.
 382       * @since 6.6.0 Added `blockTypes` to support block style variation theme.json partials.
 383       * @var string[]
 384       */
 385      const VALID_TOP_LEVEL_KEYS = array(
 386          'blockTypes',
 387          'customTemplates',
 388          'description',
 389          'patterns',
 390          'settings',
 391          'slug',
 392          'styles',
 393          'templateParts',
 394          'title',
 395          'version',
 396      );
 397  
 398      /**
 399       * The valid properties under the settings key.
 400       *
 401       * @since 5.8.0 As `ALLOWED_SETTINGS`.
 402       * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`,
 403       *              added new properties for `border`, `color`, `spacing`,
 404       *              and `typography`, and renamed others according to the new schema.
 405       * @since 6.0.0 Added `color.defaultDuotone`.
 406       * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`.
 407       * @since 6.2.0 Added `dimensions.minHeight`, 'shadow.presets', 'shadow.defaultPresets',
 408       *              `position.fixed` and `position.sticky`.
 409       * @since 6.3.0 Added support for `typography.textColumns`, removed `layout.definitions`.
 410       * @since 6.4.0 Added support for `layout.allowEditing`, `background.backgroundImage`,
 411       *              `typography.writingMode`, `lightbox.enabled` and `lightbox.allowEditing`.
 412       * @since 6.5.0 Added support for `layout.allowCustomContentAndWideSize`,
 413       *              `background.backgroundSize` and `dimensions.aspectRatio`.
 414       * @since 6.6.0 Added support for 'dimensions.aspectRatios', 'dimensions.defaultAspectRatios',
 415       *              'typography.defaultFontSizes', and 'spacing.defaultSpacingSizes'.
 416       * @since 6.9.0 Added support for `border.radiusSizes`.
 417       * @since 7.0.0 Added type markers to the schema for boolean values.
 418       *              Added support for `dimensions.width` and `dimensions.height`.
 419       *              Added support for `typography.textIndent`.
 420       * @since 7.1.0 Added `viewport` property.
 421       *              Added support for `background.gradient`, `dimensions.minWidth` and `blockVisibility.allowEditing`.
 422       * @var array
 423       */
 424      const VALID_SETTINGS = array(
 425          'appearanceTools'               => null,
 426          'useRootPaddingAwareAlignments' => null,
 427          'background'                    => array(
 428              'backgroundImage' => null,
 429              'backgroundSize'  => null,
 430              'gradient'        => null,
 431          ),
 432          'border'                        => array(
 433              'color'       => null,
 434              'radius'      => null,
 435              'radiusSizes' => null,
 436              'style'       => null,
 437              'width'       => null,
 438          ),
 439          'color'                         => array(
 440              'background'       => null,
 441              'custom'           => null,
 442              'customDuotone'    => null,
 443              'customGradient'   => null,
 444              'defaultDuotone'   => null,
 445              'defaultGradients' => null,
 446              'defaultPalette'   => null,
 447              'duotone'          => null,
 448              'gradients'        => null,
 449              'link'             => null,
 450              'heading'          => null,
 451              'button'           => null,
 452              'caption'          => null,
 453              'palette'          => null,
 454              'text'             => null,
 455          ),
 456          'custom'                        => null,
 457          'dimensions'                    => array(
 458              'aspectRatio'         => null,
 459              'aspectRatios'        => null,
 460              'defaultAspectRatios' => null,
 461              'dimensionSizes'      => null,
 462              'height'              => null,
 463              'minHeight'           => null,
 464              'minWidth'            => null,
 465              'width'               => null,
 466          ),
 467          'layout'                        => array(
 468              'contentSize'                   => null,
 469              'wideSize'                      => null,
 470              'allowEditing'                  => null,
 471              'allowCustomContentAndWideSize' => null,
 472          ),
 473          'lightbox'                      => array(
 474              'enabled'      => true,
 475              'allowEditing' => true,
 476          ),
 477          'position'                      => array(
 478              'fixed'  => null,
 479              'sticky' => null,
 480          ),
 481          'blockVisibility'               => array(
 482              'allowEditing' => true,
 483          ),
 484          'spacing'                       => array(
 485              'customSpacingSize'   => null,
 486              'defaultSpacingSizes' => null,
 487              'spacingSizes'        => null,
 488              'spacingScale'        => null,
 489              'blockGap'            => null,
 490              'margin'              => null,
 491              'padding'             => null,
 492              'units'               => null,
 493          ),
 494          'shadow'                        => array(
 495              'presets'        => null,
 496              'defaultPresets' => null,
 497          ),
 498          'typography'                    => array(
 499              'fluid'            => null,
 500              'customFontSize'   => null,
 501              'defaultFontSizes' => null,
 502              'dropCap'          => null,
 503              'fontFamilies'     => null,
 504              'fontSizes'        => null,
 505              'fontStyle'        => null,
 506              'fontWeight'       => null,
 507              'letterSpacing'    => null,
 508              'lineHeight'       => null,
 509              'textAlign'        => null,
 510              'textColumns'      => null,
 511              'textDecoration'   => null,
 512              'textIndent'       => null,
 513              'textTransform'    => null,
 514              'writingMode'      => null,
 515          ),
 516          'viewport'                      => array(
 517              'mobile' => null,
 518              'tablet' => null,
 519          ),
 520      );
 521  
 522      /**
 523       * The valid properties for fontFamilies under settings key.
 524       *
 525       * @since 6.5.0
 526       * @var array
 527       */
 528      const FONT_FAMILY_SCHEMA = array(
 529          array(
 530              'fontFamily' => null,
 531              'name'       => null,
 532              'slug'       => null,
 533              'fontFace'   => array(
 534                  array(
 535                      'ascentOverride'        => null,
 536                      'descentOverride'       => null,
 537                      'fontDisplay'           => null,
 538                      'fontFamily'            => null,
 539                      'fontFeatureSettings'   => null,
 540                      'fontStyle'             => null,
 541                      'fontStretch'           => null,
 542                      'fontVariationSettings' => null,
 543                      'fontWeight'            => null,
 544                      'lineGapOverride'       => null,
 545                      'sizeAdjust'            => null,
 546                      'src'                   => null,
 547                      'unicodeRange'          => null,
 548                  ),
 549              ),
 550          ),
 551      );
 552  
 553      /**
 554       * The valid properties under the styles key.
 555       *
 556       * @since 5.8.0 As `ALLOWED_STYLES`.
 557       * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`,
 558       *              added new properties for `border`, `filter`, `spacing`,
 559       *              and `typography`.
 560       * @since 6.1.0 Added new side properties for `border`,
 561       *              added new property `shadow`,
 562       *              updated `blockGap` to be allowed at any level.
 563       * @since 6.2.0 Added `outline`, and `minHeight` properties.
 564       * @since 6.3.0 Added support for `typography.textColumns`.
 565       * @since 6.5.0 Added support for `dimensions.aspectRatio`.
 566       * @since 6.6.0 Added `background` sub properties to top-level only.
 567       * @since 7.0.0 Added support for `dimensions.width` and `dimensions.height`.
 568       * @since 7.1.0 Added support for `background.gradient`,`dimensions.minWidth`,
 569       *              and `typography.textShadow`.
 570       * @var array
 571       */
 572      const VALID_STYLES = array(
 573          'background' => array(
 574              'backgroundImage'      => null,
 575              'backgroundPosition'   => null,
 576              'backgroundRepeat'     => null,
 577              'backgroundSize'       => null,
 578              'backgroundAttachment' => null,
 579              'gradient'             => null,
 580          ),
 581          'border'     => array(
 582              'color'  => null,
 583              'radius' => null,
 584              'style'  => null,
 585              'width'  => null,
 586              'top'    => null,
 587              'right'  => null,
 588              'bottom' => null,
 589              'left'   => null,
 590          ),
 591          'color'      => array(
 592              'background' => null,
 593              'gradient'   => null,
 594              'text'       => null,
 595          ),
 596          'dimensions' => array(
 597              'aspectRatio' => null,
 598              'height'      => null,
 599              'minHeight'   => null,
 600              'minWidth'    => null,
 601              'width'       => null,
 602          ),
 603          'filter'     => array(
 604              'duotone' => null,
 605          ),
 606          'outline'    => array(
 607              'color'  => null,
 608              'offset' => null,
 609              'style'  => null,
 610              'width'  => null,
 611          ),
 612          'shadow'     => null,
 613          'spacing'    => array(
 614              'margin'   => null,
 615              'padding'  => null,
 616              'blockGap' => null,
 617          ),
 618          'typography' => array(
 619              'fontFamily'     => null,
 620              'fontSize'       => null,
 621              'fontStyle'      => null,
 622              'fontWeight'     => null,
 623              'letterSpacing'  => null,
 624              'lineHeight'     => null,
 625              'textAlign'      => null,
 626              'textColumns'    => null,
 627              'textDecoration' => null,
 628              'textIndent'     => null,
 629              'textShadow'     => null,
 630              'textTransform'  => null,
 631              'writingMode'    => null,
 632          ),
 633          'css'        => null,
 634      );
 635  
 636      /**
 637       * Defines which pseudo selectors are enabled for which elements.
 638       *
 639       * The order of the selectors should be: link, any-link, visited, hover, focus, focus-visible, active.
 640       * This is to ensure the user action (hover, focus and active) styles have a higher
 641       * specificity than the visited styles, which in turn have a higher specificity than
 642       * the unvisited styles.
 643       *
 644       * See https://core.trac.wordpress.org/ticket/56928.
 645       * Note: this will affect both top-level and block-level elements.
 646       *
 647       * @since 6.1.0
 648       * @since 6.2.0 Added support for ':link' and ':any-link'.
 649       * @since 6.8.0 Added support for ':focus-visible'.
 650       * @since 6.9.0 Added `textInput` and `select` elements.
 651       * @var array
 652       */
 653      const VALID_ELEMENT_PSEUDO_SELECTORS = array(
 654          'link'   => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
 655          'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
 656      );
 657  
 658      /**
 659       * The valid pseudo-selectors that can be used for blocks.
 660       *
 661       * @since 7.0
 662       * @var array
 663       */
 664      const VALID_BLOCK_PSEUDO_SELECTORS = array(
 665          'core/button'          => array( ':hover', ':focus', ':focus-visible', ':active' ),
 666          'core/navigation-link' => array( ':hover', ':focus', ':focus-visible', ':active' ),
 667      );
 668  
 669      /**
 670       * Custom states for blocks that map to CSS class selectors rather than
 671       * CSS pseudo-selectors. Values use the '-' prefix (e.g. '-current') to
 672       * distinguish them from real CSS pseudo-selectors and breakpoint states.
 673       *
 674       * The CSS selector for each state is defined in the block's block.json
 675       * under `selectors.states`, e.g.:
 676       *
 677       *   "selectors": { "states": { "-current": ".some-css-selector" } }
 678       *
 679       * This constant controls which states are valid in theme.json for a given
 680       * block. Blocks listed here also inherit their VALID_BLOCK_PSEUDO_SELECTORS
 681       * as valid sub-states, producing compound selectors such as
 682       * `.wp-block-navigation-item.current-menu-item:hover`.
 683       *
 684       * @since 7.1.0
 685       * @var array
 686       */
 687      const VALID_BLOCK_CUSTOM_STATES = array(
 688          'core/navigation-link' => array( '-current' ),
 689      );
 690  
 691      /**
 692       * Default viewport breakpoint sizes.
 693       *
 694       * @since 7.1.0
 695       * @var array
 696       */
 697      const DEFAULT_VIEWPORT_BREAKPOINTS = array(
 698          'mobile' => '480px',
 699          'tablet' => '782px',
 700      );
 701  
 702      /**
 703       * Returns CSS media queries for responsive viewport style states.
 704       *
 705       * Breakpoint values are read from `settings.viewport`, sanitized, and
 706       * normalized before the media query strings are generated. By default, the
 707       * returned keys are the theme.json style-state names (`@mobile`, `@tablet`).
 708       * When `$options['include_desktop']` is truthy, `@desktop` is included.
 709       *
 710       * @since 7.1.0
 711       *
 712       * @param mixed $viewport_settings Viewport settings from theme.json.
 713       * @param array $options           {
 714       *     Optional. Options for generating media queries.
 715       *
 716       *     @type bool $include_desktop Whether to include the desktop media query. Default false.
 717       * }
 718       * @return array Responsive media queries.
 719       */
 720  	public static function get_viewport_media_queries( $viewport_settings = null, $options = array() ) {
 721          $breakpoints = self::sanitize_viewport_settings( $viewport_settings );
 722  
 723          $responsive_media_queries = array();
 724  
 725          if ( isset( $breakpoints['mobile'] ) ) {
 726              $responsive_media_queries['@mobile'] = "@media (width <= {$breakpoints['mobile']})";
 727          }
 728  
 729          if ( isset( $breakpoints['tablet'] ) ) {
 730              $responsive_media_queries['@tablet'] = isset( $breakpoints['mobile'] )
 731                  ? sprintf(
 732                      '@media (%s < width <= %s)',
 733                      $breakpoints['mobile'],
 734                      $breakpoints['tablet']
 735                  )
 736                  : "@media (width <= {$breakpoints['tablet']})";
 737          }
 738  
 739          if ( ! empty( $options['include_desktop'] ) ) {
 740              if ( isset( $breakpoints['tablet'] ) ) {
 741                  $desktop_breakpoint = $breakpoints['tablet'];
 742              } else {
 743                  $desktop_breakpoint = $breakpoints['mobile'];
 744              }
 745  
 746              $responsive_media_queries['@desktop'] =
 747                  "@media (width > {$desktop_breakpoint})";
 748          }
 749  
 750          return $responsive_media_queries;
 751      }
 752  
 753      /**
 754       * Checks whether a viewport breakpoint value is a safe CSS length.
 755       *
 756       * Viewport breakpoints are limited to numeric `px`, `em`, and `rem` lengths.
 757       * CSS functions, percentages, and other units are rejected because breakpoint
 758       * values are interpolated into generated media queries.
 759       *
 760       * @since 7.1.0
 761       *
 762       * @param mixed $value Value to check.
 763       * @return bool Whether the value is valid.
 764       */
 765  	private static function is_valid_viewport_breakpoint_size( $value ) {
 766          if ( ! is_string( $value ) ) {
 767              return false;
 768          }
 769  
 770          $value = trim( $value );
 771          if ( '' === $value ) {
 772              return false;
 773          }
 774  
 775          return 1 === preg_match( '/^(?:\d+|\d*\.\d+)(?:px|em|rem)$/', $value );
 776      }
 777  
 778      /**
 779       * Converts a valid viewport breakpoint size to pixels for ordering checks.
 780       *
 781       * Generated media queries keep the original units. This method only
 782       * normalizes values so `mobile` and `tablet` can be compared safely. `em`
 783       * and `rem` lengths use a 16px base for comparison.
 784       *
 785       * @since 7.1.0
 786       *
 787       * @param mixed $value Viewport breakpoint size.
 788       * @return float|null Viewport breakpoint size in pixels, or null when invalid.
 789       */
 790  	private static function get_viewport_breakpoint_value_in_pixels( $value ) {
 791          if ( ! self::is_valid_viewport_breakpoint_size( $value ) ) {
 792              return null;
 793          }
 794  
 795          $value = trim( $value );
 796          $unit  = substr( $value, -3 );
 797          if ( 'rem' === $unit ) {
 798              $number = (float) substr( $value, 0, -3 );
 799          } else {
 800              $unit   = substr( $value, -2 );
 801              $number = (float) substr( $value, 0, -2 );
 802          }
 803  
 804          /*
 805           * Use the most common browser default font size as the base for em/rem
 806           * media query conversions. This pixel value is only used to compare
 807           * breakpoint order; generated media queries keep the original units.
 808           */
 809          return 'px' === $unit ? $number : $number * 16;
 810      }
 811  
 812      /**
 813       * Sanitizes and normalizes viewport breakpoint settings.
 814       *
 815       * Keeps only supported breakpoint keys, trims valid CSS lengths, and returns
 816       * the default breakpoints when no valid custom breakpoint is provided. When
 817       * only one breakpoint is valid, it remains keyed by its configured state and
 818       * uses a single max-width media query. When `tablet` is not larger than
 819       * `mobile`, it is removed.
 820       *
 821       * @since 7.1.0
 822       *
 823       * @param mixed $viewport_settings Viewport settings from theme.json.
 824       * @return array Sanitized viewport breakpoint settings.
 825       */
 826  	private static function sanitize_viewport_settings( $viewport_settings ) {
 827          if ( ! is_array( $viewport_settings ) ) {
 828              return static::DEFAULT_VIEWPORT_BREAKPOINTS;
 829          }
 830  
 831          $breakpoints = array();
 832          foreach ( array_keys( static::DEFAULT_VIEWPORT_BREAKPOINTS ) as $breakpoint ) {
 833              $value = $viewport_settings[ $breakpoint ] ?? null;
 834              $px    = self::get_viewport_breakpoint_value_in_pixels( $value );
 835              if ( null !== $px ) {
 836                  $breakpoints[ $breakpoint ] = array(
 837                      'value' => trim( $value ),
 838                      'px'    => $px,
 839                  );
 840              }
 841          }
 842  
 843          if ( empty( $breakpoints ) ) {
 844              return static::DEFAULT_VIEWPORT_BREAKPOINTS;
 845          }
 846  
 847          if ( 1 === count( $breakpoints ) ) {
 848              $breakpoint = key( $breakpoints );
 849              return array( $breakpoint => $breakpoints[ $breakpoint ]['value'] );
 850          }
 851  
 852          $sanitized = array( 'mobile' => $breakpoints['mobile']['value'] );
 853  
 854          if ( isset( $breakpoints['tablet'] ) && $breakpoints['mobile']['px'] < $breakpoints['tablet']['px'] ) {
 855              $sanitized['tablet'] = $breakpoints['tablet']['value'];
 856          }
 857  
 858          return $sanitized;
 859      }
 860  
 861      /**
 862       * The valid elements that can be found under styles.
 863       *
 864       * @since 5.8.0
 865       * @since 6.1.0 Added `heading`, `button`, and `caption` elements.
 866       * @var string[]
 867       */
 868      const ELEMENTS = array(
 869          'link'      => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity.
 870          'heading'   => 'h1, h2, h3, h4, h5, h6',
 871          'h1'        => 'h1',
 872          'h2'        => 'h2',
 873          'h3'        => 'h3',
 874          'h4'        => 'h4',
 875          'h5'        => 'h5',
 876          'h6'        => 'h6',
 877          // We have the .wp-block-button__link class so that this will target older buttons that have been serialized.
 878          'button'    => '.wp-element-button, .wp-block-button__link',
 879          // The block classes are necessary to target older content that won't use the new class names.
 880          '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',
 881          'cite'      => 'cite',
 882          'textInput' => 'textarea, input:where([type=email],[type=number],[type=password],[type=search],[type=text],[type=tel],[type=url])',
 883          'select'    => 'select',
 884      );
 885  
 886      const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array(
 887          'button'  => 'wp-element-button',
 888          'caption' => 'wp-element-caption',
 889      );
 890  
 891      /**
 892       * List of block support features that can have their related styles
 893       * generated under their own feature level selector rather than the block's.
 894       *
 895       * @since 6.1.0
 896       * @since 7.0.0 Added support for `dimensions`.
 897       * @var string[]
 898       */
 899      const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array(
 900          '__experimentalBorder' => 'border',
 901          'color'                => 'color',
 902          'dimensions'           => 'dimensions',
 903          'spacing'              => 'spacing',
 904          'typography'           => 'typography',
 905      );
 906  
 907      /**
 908       * Return the input schema at the root and per origin.
 909       *
 910       * @since 6.5.0
 911       *
 912       * @param array $schema The base schema.
 913       * @return array The schema at the root and per origin.
 914       *
 915       * Example:
 916       * schema_in_root_and_per_origin(
 917       *   array(
 918       *    'fontFamily' => null,
 919       *    'slug' => null,
 920       *   )
 921       * )
 922       *
 923       * Returns:
 924       * array(
 925       *  'fontFamily' => null,
 926       *  'slug' => null,
 927       *  'default' => array(
 928       *    'fontFamily' => null,
 929       *    'slug' => null,
 930       *  ),
 931       *  'blocks' => array(
 932       *    'fontFamily' => null,
 933       *    'slug' => null,
 934       *  ),
 935       *  'theme' => array(
 936       *     'fontFamily' => null,
 937       *     'slug' => null,
 938       *  ),
 939       *  'custom' => array(
 940       *     'fontFamily' => null,
 941       *     'slug' => null,
 942       *  ),
 943       * )
 944       */
 945  	protected static function schema_in_root_and_per_origin( $schema ) {
 946          $schema_in_root_and_per_origin = $schema;
 947          foreach ( static::VALID_ORIGINS as $origin ) {
 948              $schema_in_root_and_per_origin[ $origin ] = $schema;
 949          }
 950          return $schema_in_root_and_per_origin;
 951      }
 952  
 953  
 954      /**
 955       * Processes pseudo-selectors for any node (block or variation).
 956       *
 957       * @since 7.0.0
 958       *
 959       * @param array      $node            The node data (block or variation).
 960       * @param string     $base_selector   The base selector.
 961       * @param array      $settings        The theme settings.
 962       * @param string     $block_name      The block name.
 963       * @param array|null $block_metadata  Metadata about the block to get styles for.
 964       * @param array|null $style_variation Style variation metadata.
 965       * @return array Array of pseudo-selector declarations.
 966       */
 967  	private function process_pseudo_selectors( $node, $base_selector, $settings, $block_name, $block_metadata = null, $style_variation = null ) {
 968          $pseudo_declarations = array();
 969          $add_declarations    = static function ( $selector, $declarations ) use ( &$pseudo_declarations ) {
 970              if ( empty( $declarations ) ) {
 971                  return;
 972              }
 973  
 974              if ( isset( $pseudo_declarations[ $selector ] ) ) {
 975                  $pseudo_declarations[ $selector ] = array_merge(
 976                      $pseudo_declarations[ $selector ],
 977                      $declarations
 978                  );
 979              } else {
 980                  $pseudo_declarations[ $selector ] = $declarations;
 981              }
 982          };
 983  
 984          if ( ! isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
 985              return $pseudo_declarations;
 986          }
 987  
 988          foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
 989              if ( isset( $node[ $pseudo_selector ] ) ) {
 990                  $pseudo_node = $node[ $pseudo_selector ];
 991  
 992                  if ( is_array( $block_metadata ) ) {
 993                      $feature_declarations = $this->get_feature_declarations_for_node( $block_metadata, $pseudo_node );
 994                      $feature_declarations = self::update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name );
 995                      $feature_declarations = self::update_button_width_declarations( $feature_declarations, $settings );
 996  
 997                      foreach ( $feature_declarations as $feature_selector => $declarations ) {
 998                          $target_selector   = is_array( $style_variation )
 999                              ? static::get_block_style_variation_feature_selector( $style_variation, $feature_selector )
1000                              : $feature_selector;
1001                          $combined_selector = static::append_to_selector( $target_selector, $pseudo_selector );
1002  
1003                          $add_declarations( $combined_selector, $declarations );
1004                      }
1005                  }
1006  
1007                  $combined_selector = static::append_to_selector( $base_selector, $pseudo_selector );
1008                  $declarations      = static::compute_style_properties( $pseudo_node, $settings, null, null );
1009                  $add_declarations( $combined_selector, $declarations );
1010              }
1011          }
1012  
1013          return $pseudo_declarations;
1014      }
1015  
1016  
1017      /**
1018       * Returns a class name by an element name.
1019       *
1020       * @since 6.1.0
1021       *
1022       * @param string $element The name of the element.
1023       * @return string The name of the class.
1024       */
1025  	public static function get_element_class_name( $element ) {
1026          $class_name = '';
1027  
1028          if ( isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ] ) ) {
1029              $class_name = static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ];
1030          }
1031  
1032          return $class_name;
1033      }
1034  
1035      /**
1036       * Options that settings.appearanceTools enables.
1037       *
1038       * @since 6.0.0
1039       * @since 6.2.0 Added `dimensions.minHeight` and `position.sticky`.
1040       * @since 6.4.0 Added `background.backgroundImage`.
1041       * @since 6.5.0 Added `background.backgroundSize` and `dimensions.aspectRatio`.
1042       * @since 7.0.0 Added `dimensions.width` and `dimensions.height`.
1043       * @since 7.1.0 Added `background.gradient`.
1044       *              Added `dimensions.minWidth`.
1045       * @var array
1046       */
1047      const APPEARANCE_TOOLS_OPT_INS = array(
1048          array( 'background', 'backgroundImage' ),
1049          array( 'background', 'backgroundSize' ),
1050          array( 'background', 'gradient' ),
1051          array( 'border', 'color' ),
1052          array( 'border', 'radius' ),
1053          array( 'border', 'style' ),
1054          array( 'border', 'width' ),
1055          array( 'color', 'link' ),
1056          array( 'color', 'heading' ),
1057          array( 'color', 'button' ),
1058          array( 'color', 'caption' ),
1059          array( 'dimensions', 'aspectRatio' ),
1060          array( 'dimensions', 'height' ),
1061          array( 'dimensions', 'minHeight' ),
1062          array( 'dimensions', 'minWidth' ),
1063          array( 'dimensions', 'width' ),
1064          array( 'position', 'sticky' ),
1065          array( 'spacing', 'blockGap' ),
1066          array( 'spacing', 'margin' ),
1067          array( 'spacing', 'padding' ),
1068          array( 'typography', 'lineHeight' ),
1069          array( 'typography', 'textColumns' ),
1070      );
1071  
1072      /**
1073       * The latest version of the schema in use.
1074       *
1075       * @since 5.8.0
1076       * @since 5.9.0 Changed value from 1 to 2.
1077       * @since 6.6.0 Changed value from 2 to 3.
1078       * @var int
1079       */
1080      const LATEST_SCHEMA = 3;
1081  
1082      /**
1083       * Constructor.
1084       *
1085       * @since 5.8.0
1086       * @since 6.6.0 Key spacingScale by origin, and Pre-generate the spacingSizes from spacingScale.
1087       *              Added unwrapping of shared block style variations into block type variations if registered.
1088       *
1089       * @param array  $theme_json A structure that follows the theme.json schema.
1090       * @param string $origin     Optional. What source of data this object represents.
1091       *                           One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
1092       */
1093  	public function __construct( $theme_json = array( 'version' => self::LATEST_SCHEMA ), $origin = 'theme' ) {
1094          if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
1095              $origin = 'theme';
1096          }
1097  
1098          $this->theme_json    = WP_Theme_JSON_Schema::migrate( $theme_json, $origin );
1099          $blocks_metadata     = static::get_blocks_metadata();
1100          $valid_block_names   = array_keys( $blocks_metadata );
1101          $valid_element_names = array_keys( static::ELEMENTS );
1102          $valid_variations    = static::get_valid_block_style_variations( $blocks_metadata );
1103          $this->theme_json    = self::unwrap_shared_block_style_variations( $this->theme_json, $valid_variations );
1104          $this->theme_json    = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations );
1105          $this->theme_json    = static::maybe_opt_in_into_settings( $this->theme_json );
1106  
1107          // Internally, presets are keyed by origin.
1108          $nodes = static::get_setting_nodes( $this->theme_json );
1109          foreach ( $nodes as $node ) {
1110              foreach ( static::PRESETS_METADATA as $preset_metadata ) {
1111                  $path = $node['path'];
1112                  foreach ( $preset_metadata['path'] as $subpath ) {
1113                      $path[] = $subpath;
1114                  }
1115                  $preset = _wp_array_get( $this->theme_json, $path, null );
1116                  if ( null !== $preset ) {
1117                      // If the preset is not already keyed by origin.
1118                      if ( isset( $preset[0] ) || empty( $preset ) ) {
1119                          _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
1120                      }
1121                  }
1122              }
1123          }
1124  
1125          // In addition to presets, spacingScale (which generates presets) is also keyed by origin.
1126          $scale_path    = array( 'settings', 'spacing', 'spacingScale' );
1127          $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null );
1128          if ( null !== $spacing_scale ) {
1129              // If the spacingScale is not already keyed by origin.
1130              if ( empty( array_intersect( array_keys( $spacing_scale ), static::VALID_ORIGINS ) ) ) {
1131                  _wp_array_set( $this->theme_json, $scale_path, array( $origin => $spacing_scale ) );
1132              }
1133          }
1134  
1135          // Pre-generate the spacingSizes from spacingScale.
1136          $scale_path    = array( 'settings', 'spacing', 'spacingScale', $origin );
1137          $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null );
1138          if ( isset( $spacing_scale ) ) {
1139              $sizes_path           = array( 'settings', 'spacing', 'spacingSizes', $origin );
1140              $spacing_sizes        = _wp_array_get( $this->theme_json, $sizes_path, array() );
1141              $spacing_scale_sizes  = self::compute_spacing_sizes( $spacing_scale );
1142              $merged_spacing_sizes = self::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );
1143              _wp_array_set( $this->theme_json, $sizes_path, $merged_spacing_sizes );
1144          }
1145      }
1146  
1147      /**
1148       * Unwraps shared block style variations.
1149       *
1150       * It takes the shared variations (styles.variations.variationName) and
1151       * applies them to all the blocks that have the given variation registered
1152       * (styles.blocks.blockType.variations.variationName).
1153       *
1154       * For example, given the `core/paragraph` and `core/group` blocks have
1155       * registered the `section-a` style variation, and given the following input:
1156       *
1157       * {
1158       *   "styles": {
1159       *     "variations": {
1160       *       "section-a": { "color": { "background": "backgroundColor" } }
1161       *     }
1162       *   }
1163       * }
1164       *
1165       * It returns the following output:
1166       *
1167       * {
1168       *   "styles": {
1169       *     "blocks": {
1170       *       "core/paragraph": {
1171       *         "variations": {
1172       *             "section-a": { "color": { "background": "backgroundColor" } }
1173       *         },
1174       *       },
1175       *       "core/group": {
1176       *         "variations": {
1177       *           "section-a": { "color": { "background": "backgroundColor" } }
1178       *         }
1179       *       }
1180       *     }
1181       *   }
1182       * }
1183       *
1184       * @since 6.6.0
1185       *
1186       * @param array $theme_json       A structure that follows the theme.json schema.
1187       * @param array $valid_variations Valid block style variations.
1188       * @return array Theme json data with shared variation definitions unwrapped under appropriate block types.
1189       */
1190  	private static function unwrap_shared_block_style_variations( $theme_json, $valid_variations ) {
1191          if ( empty( $theme_json['styles']['variations'] ) || empty( $valid_variations ) ) {
1192              return $theme_json;
1193          }
1194  
1195          $new_theme_json = $theme_json;
1196          $variations     = $new_theme_json['styles']['variations'];
1197  
1198          foreach ( $valid_variations as $block_type => $registered_variations ) {
1199              foreach ( $registered_variations as $variation_name ) {
1200                  $block_level_data = $new_theme_json['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array();
1201                  $top_level_data   = $variations[ $variation_name ] ?? array();
1202                  $merged_data      = array_replace_recursive( $top_level_data, $block_level_data );
1203                  if ( ! empty( $merged_data ) ) {
1204                      _wp_array_set( $new_theme_json, array( 'styles', 'blocks', $block_type, 'variations', $variation_name ), $merged_data );
1205                  }
1206              }
1207          }
1208  
1209          unset( $new_theme_json['styles']['variations'] );
1210  
1211          return $new_theme_json;
1212      }
1213  
1214      /**
1215       * Enables some opt-in settings if theme declared support.
1216       *
1217       * @since 5.9.0
1218       *
1219       * @param array $theme_json A theme.json structure to modify.
1220       * @return array The modified theme.json structure.
1221       */
1222  	protected static function maybe_opt_in_into_settings( $theme_json ) {
1223          $new_theme_json = $theme_json;
1224  
1225          if (
1226              isset( $new_theme_json['settings']['appearanceTools'] ) &&
1227              true === $new_theme_json['settings']['appearanceTools']
1228          ) {
1229              static::do_opt_in_into_settings( $new_theme_json['settings'] );
1230          }
1231  
1232          if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) {
1233              foreach ( $new_theme_json['settings']['blocks'] as &$block ) {
1234                  if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) {
1235                      static::do_opt_in_into_settings( $block );
1236                  }
1237              }
1238          }
1239  
1240          return $new_theme_json;
1241      }
1242  
1243      /**
1244       * Enables some settings.
1245       *
1246       * @since 5.9.0
1247       *
1248       * @param array $context The context to which the settings belong.
1249       */
1250  	protected static function do_opt_in_into_settings( &$context ) {
1251          foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) {
1252              /*
1253               * Use "unset prop" as a marker instead of "null" because
1254               * "null" can be a valid value for some props (e.g. blockGap).
1255               */
1256              if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) {
1257                  _wp_array_set( $context, $path, true );
1258              }
1259          }
1260  
1261          unset( $context['appearanceTools'] );
1262      }
1263  
1264      /**
1265       * Sanitizes the input according to the schemas.
1266       *
1267       * @since 5.8.0
1268       * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
1269       * @since 6.3.0 Added the `$valid_variations` parameter.
1270       * @since 6.6.0 Updated schema to allow extended block style variations.
1271       * @since 7.1.1 Updated schema to allow responsive breakpoint states and pseudo-selectors
1272       *              at the top level of `styles` for block style variation partials.
1273       *
1274       * @param array $input               Structure to sanitize.
1275       * @param array $valid_block_names   List of valid block names.
1276       * @param array $valid_element_names List of valid element names.
1277       * @param array $valid_variations    List of valid variations per block.
1278       * @return array The sanitized output.
1279       */
1280  	protected static function sanitize( $input, $valid_block_names, $valid_element_names, $valid_variations ) {
1281          $output = array();
1282  
1283          if ( ! is_array( $input ) ) {
1284              return $output;
1285          }
1286  
1287          // Preserve only the top most level keys.
1288          $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) );
1289  
1290          /*
1291           * Remove any rules that are annotated as "top" in VALID_STYLES constant.
1292           * Some styles are only meant to be available at the top-level (e.g.: blockGap),
1293           * hence, the schema for blocks & elements should not have them.
1294           */
1295          $styles_non_top_level = static::VALID_STYLES;
1296          foreach ( array_keys( $styles_non_top_level ) as $section ) {
1297              // array_key_exists() needs to be used instead of isset() because the value can be null.
1298              if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $section ] ) ) {
1299                  foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) {
1300                      if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) {
1301                          unset( $styles_non_top_level[ $section ][ $prop ] );
1302                      }
1303                  }
1304              }
1305          }
1306  
1307          // Build the schema based on valid block & element names.
1308          $schema                   = array();
1309          $schema_styles_elements   = array();
1310          $responsive_media_queries = static::get_viewport_media_queries( $input['settings']['viewport'] ?? null );
1311  
1312          /*
1313           * Set allowed element pseudo selectors and responsive breakpoint states.
1314           * Target data structure in schema:
1315           * e.g.
1316           * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
1317           * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
1318           * - block responsive elements: `$schema['styles']['blocks']['core/button']['@tablet']['elements']['link'][':hover']`.
1319           */
1320          foreach ( $valid_element_names as $element ) {
1321              $schema_styles_elements[ $element ] = $styles_non_top_level;
1322  
1323              if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
1324                  foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
1325                      $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level;
1326                  }
1327              }
1328  
1329              // Add responsive breakpoint states for elements.
1330              foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1331                  $schema_styles_elements[ $element ][ $breakpoint_state ] = $styles_non_top_level;
1332              }
1333          }
1334  
1335          $schema_styles_blocks   = array();
1336          $schema_settings_blocks = array();
1337          $breakpoint_states      = array_keys( $responsive_media_queries );
1338  
1339          $common_block_settings = static::VALID_SETTINGS;
1340          // `viewport` and `blockVisibility` are global-only settings and cannot be set per block for now.
1341          unset(
1342              $common_block_settings['viewport'],
1343              $common_block_settings['blockVisibility']
1344          );
1345  
1346          /*
1347           * Generate a schema for blocks.
1348           * - Block styles can contain `elements`, `variations`, and responsive breakpoint state definitions.
1349           * - Variations definitions cannot be nested.
1350           * - Variations can contain styles for inner `blocks`, `elements`, and responsive breakpoint states.
1351           * - Variation inner `blocks` styles can contain `elements` and responsive breakpoint states.
1352           *
1353           * As each variation needs both a `blocks` schema and responsive `blocks` schemas
1354           * for further nested inner `blocks`, the overall schema is generated in multiple passes.
1355           *
1356           * All blocks start with the same style schema. Build that common schema
1357           * once, then add block-specific pseudo and custom states below.
1358           */
1359          $responsive_block_schema             = $styles_non_top_level;
1360          $responsive_block_schema['elements'] = $schema_styles_elements;
1361  
1362          $common_block_schema             = $styles_non_top_level;
1363          $common_block_schema['elements'] = $schema_styles_elements;
1364  
1365          foreach ( $breakpoint_states as $breakpoint_state ) {
1366              $common_block_schema[ $breakpoint_state ] = $responsive_block_schema;
1367          }
1368  
1369          foreach ( $valid_block_names as $block ) {
1370              $schema_settings_blocks[ $block ] = $common_block_settings;
1371              $schema_styles_blocks[ $block ]   = $common_block_schema;
1372  
1373              // Add responsive pseudo-selectors only to blocks that support them.
1374              if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1375                  foreach ( $breakpoint_states as $breakpoint_state ) {
1376                      foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1377                          $schema_styles_blocks[ $block ][ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
1378                      }
1379                  }
1380              }
1381  
1382              // Add pseudo-selectors for blocks that support them
1383              if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1384                  foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1385                      $schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level;
1386                  }
1387              }
1388  
1389              // Add custom states for blocks that support them (e.g. '-current' for navigation).
1390              if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $block ] ) ) {
1391                  foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $block ] as $custom_state ) {
1392                      $custom_state_schema = $styles_non_top_level;
1393                      /*
1394                       * The same pseudo-selectors valid for the block at the top level
1395                       * are also valid within each custom state.
1396                       */
1397                      if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1398                          foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo ) {
1399                              $custom_state_schema[ $pseudo ] = $styles_non_top_level;
1400                          }
1401                      }
1402                      $schema_styles_blocks[ $block ][ $custom_state ] = $custom_state_schema;
1403                  }
1404              }
1405          }
1406  
1407          $block_style_variation_styles             = static::VALID_STYLES;
1408          $block_style_variation_styles['blocks']   = $schema_styles_blocks;
1409          $block_style_variation_styles['elements'] = $schema_styles_elements;
1410  
1411          foreach ( $valid_block_names as $block ) {
1412              // Build the schema for each block style variation.
1413              $style_variation_names = array();
1414              if (
1415                  ! empty( $input['styles']['blocks'][ $block ]['variations'] ) &&
1416                  is_array( $input['styles']['blocks'][ $block ]['variations'] ) &&
1417                  isset( $valid_variations[ $block ] )
1418              ) {
1419                  $style_variation_names = array_intersect(
1420                      array_keys( $input['styles']['blocks'][ $block ]['variations'] ),
1421                      $valid_variations[ $block ]
1422                  );
1423              }
1424  
1425              $schema_styles_variations = array();
1426              if ( ! empty( $style_variation_names ) ) {
1427                  foreach ( $style_variation_names as $variation_name ) {
1428                      $variation_schema = $block_style_variation_styles;
1429  
1430                      // Add responsive breakpoint states to block style variations.
1431                      foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1432                          $variation_schema[ $breakpoint_state ]             = $styles_non_top_level;
1433                          $variation_schema[ $breakpoint_state ]['elements'] = $schema_styles_elements;
1434  
1435                          if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1436                              foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1437                                  $variation_schema[ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
1438                              }
1439                          }
1440                      }
1441  
1442                      // Add pseudo-selectors to variations for blocks that support them
1443                      if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1444                          foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
1445                              $variation_schema[ $pseudo_selector ] = $styles_non_top_level;
1446                          }
1447                      }
1448  
1449                      $schema_styles_variations[ $variation_name ] = $variation_schema;
1450                  }
1451              }
1452  
1453              $schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations;
1454          }
1455  
1456          $schema['styles']                                 = static::VALID_STYLES;
1457          $schema['styles']['blocks']                       = $schema_styles_blocks;
1458          $schema['styles']['elements']                     = $schema_styles_elements;
1459          $schema['settings']                               = static::VALID_SETTINGS;
1460          $schema['settings']['blocks']                     = $schema_settings_blocks;
1461          $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA );
1462  
1463          /*
1464           * Add block style variation states to the top-level styles schema.
1465           *
1466           * Block style variations defined in a standalone JSON partial within a
1467           * theme's `styles` directory declare their styles at the root of the
1468           * `styles` object, so they are sanitized against the top-level schema.
1469           * It needs to allow the same states that are allowed for variations
1470           * declared inline in theme.json, otherwise those states are silently
1471           * removed as unknown keys.
1472           *
1473           * The `blockTypes` property is only present on block style variation
1474           * partials, so it both identifies the config as a variation and
1475           * determines which pseudo-selectors are valid for it. Regular
1476           * theme.json files are unaffected.
1477           */
1478          if ( ! empty( $input['blockTypes'] ) && is_array( $input['blockTypes'] ) ) {
1479              $variation_pseudo_selectors = array();
1480              foreach ( $input['blockTypes'] as $variation_block_type ) {
1481                  if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $variation_block_type ] ) ) {
1482                      $variation_pseudo_selectors = array_merge(
1483                          $variation_pseudo_selectors,
1484                          static::VALID_BLOCK_PSEUDO_SELECTORS[ $variation_block_type ]
1485                      );
1486                  }
1487              }
1488              $variation_pseudo_selectors = array_unique( $variation_pseudo_selectors );
1489  
1490              foreach ( array_keys( $responsive_media_queries ) as $breakpoint_state ) {
1491                  $schema['styles'][ $breakpoint_state ]             = $styles_non_top_level;
1492                  $schema['styles'][ $breakpoint_state ]['elements'] = $schema_styles_elements;
1493  
1494                  foreach ( $variation_pseudo_selectors as $pseudo_selector ) {
1495                      $schema['styles'][ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level;
1496                  }
1497              }
1498  
1499              foreach ( $variation_pseudo_selectors as $pseudo_selector ) {
1500                  $schema['styles'][ $pseudo_selector ] = $styles_non_top_level;
1501              }
1502          }
1503  
1504          // Remove anything that's not present in the schema.
1505          foreach ( array( 'styles', 'settings' ) as $subtree ) {
1506              if ( ! isset( $input[ $subtree ] ) ) {
1507                  continue;
1508              }
1509  
1510              if ( ! is_array( $input[ $subtree ] ) ) {
1511                  unset( $output[ $subtree ] );
1512                  continue;
1513              }
1514  
1515              $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
1516  
1517              if ( 'settings' === $subtree && array_key_exists( 'viewport', $input[ $subtree ] ) ) {
1518                  $result['viewport'] = self::sanitize_viewport_settings( $input[ $subtree ]['viewport'] );
1519              }
1520  
1521              if ( empty( $result ) ) {
1522                  unset( $output[ $subtree ] );
1523              } else {
1524                  $output[ $subtree ] = self::resolve_custom_css_format( $result );
1525              }
1526          }
1527  
1528          return $output;
1529      }
1530  
1531      /**
1532       * Appends a sub-selector to an existing one.
1533       *
1534       * Given the compounded $selector "h1, h2, h3"
1535       * and the $to_append selector ".some-class" the result will be
1536       * "h1.some-class, h2.some-class, h3.some-class".
1537       *
1538       * @since 5.8.0
1539       * @since 6.1.0 Added append position.
1540       * @since 6.3.0 Removed append position parameter.
1541       *
1542       * @param string $selector  Original selector.
1543       * @param string $to_append Selector to append.
1544       * @return string The new selector.
1545       */
1546  	protected static function append_to_selector( $selector, $to_append ) {
1547          if ( ! str_contains( $selector, ',' ) ) {
1548              return trim( $selector, " \t\n" ) . $to_append;
1549          }
1550  
1551          /**
1552           * Check for an opportunity to skip the more-costly selector splitting.
1553           * This should be possible if there are no comments, strings, functions,
1554           * URLs, escapes, or comment declaration openers (CDOs).
1555           *
1556           * Note that this means the fast-path will not apply for selectors like
1557           * the following incomplete list:
1558           *
1559           *  - `[class ~= "wide"]`
1560           *  - `.wp-block:is(.is-style-a, .is-style-b)`
1561           *  - `:nth-child(1)`
1562           *
1563           * These syntax forms all present opportunities where a comma may not
1564           * separate selectors. If none of the start characters are present,
1565           * there should be no way for a comma to mean anything other than a
1566           * comma token. The exception are syntax errors, which are not handled here.
1567           *
1568           * @see https://www.w3.org/TR/css-syntax-3/#parse-comma-separated-list-of-component-values
1569           */
1570          if ( strlen( $selector ) === strcspn( $selector, '/\'"(<\\' ) ) {
1571              return preg_replace( '~[ \t\n]*,[ \t\n]*~', "{$to_append}, ", trim( $selector, " \t\n" ) ) . $to_append;
1572          }
1573  
1574          $new_selectors = array();
1575          $selectors     = static::split_selector_list( $selector );
1576          foreach ( $selectors as $sel ) {
1577              $new_selectors[] = $sel . $to_append;
1578          }
1579          return implode( ', ', $new_selectors );
1580      }
1581  
1582      /**
1583       * Prepends a sub-selector to an existing one.
1584       *
1585       * Given the compounded $selector "h1, h2, h3"
1586       * and the $to_prepend selector ".some-class " the result will be
1587       * ".some-class h1, .some-class  h2, .some-class  h3".
1588       *
1589       * @since 6.3.0
1590       *
1591       * @param string $selector   Original selector.
1592       * @param string $to_prepend Selector to prepend.
1593       * @return string The new selector.
1594       */
1595  	protected static function prepend_to_selector( $selector, $to_prepend ) {
1596          if ( ! str_contains( $selector, ',' ) ) {
1597              return $to_prepend . trim( $selector, " \t\n" );
1598          }
1599  
1600          /**
1601           * Check for an opportunity to skip the more-costly selector splitting.
1602           * This should be possible if there are no comments, strings, functions,
1603           * URLs, escapes, or comment declaration openers (CDOs).
1604           *
1605           * Note that this means the fast-path will not apply for selectors like
1606           * the following incomplete list:
1607           *
1608           *  - `[class ~= "wide"]`
1609           *  - `.wp-block:is(.is-style-a, .is-style-b)`
1610           *  - `:nth-child(1)`
1611           *
1612           * These syntax forms all present opportunities where a comma may not
1613           * separate selectors. If none of the start characters are present,
1614           * there should be no way for a comma to mean anything other than a
1615           * comma token. The exception are syntax errors, which are not handled here.
1616           *
1617           * @see https://www.w3.org/TR/css-syntax-3/#parse-comma-separated-list-of-component-values
1618           */
1619          if ( strlen( $selector ) === strcspn( $selector, '/\'"(<\\' ) ) {
1620              return $to_prepend . preg_replace( '~[ \t\n]*,[ \t\n]*~', ", {$to_prepend}", trim( $selector, " \t\n" ) );
1621          }
1622  
1623          $new_selectors = array();
1624          $selectors     = static::split_selector_list( $selector );
1625          foreach ( $selectors as $sel ) {
1626              $new_selectors[] = $to_prepend . $sel;
1627          }
1628          return implode( ', ', $new_selectors );
1629      }
1630  
1631      /**
1632       * Splits a selector list into separate selectors.
1633       *
1634       * While selectors are joined by commas, not all commas separate top-level selectors.
1635       * This method only separates top-level selectors, so some commas may appear inside
1636       * strings, nested selectors, and comments. Leading and trailing CSS whitespace is
1637       * trimmed from the returned list items.
1638       *
1639       * Non-selector content, such as comments, are retained in the list in the same item
1640       * as the selector content they follow.
1641       *
1642       * Example:
1643       *
1644       *     array( '.wp-block' )    === self::split_selector_list( '.wp-block' );
1645       *     array( '.one', '.two' ) === self::split_selector_list( '.one, .two' );
1646       *
1647       *     // Nested selector lists are retained within their containing selector.
1648       *     array( ':is(.a, .b)', 'c' ) === self::split_selector_list( ':is(.a, .b), .c' );
1649       *
1650       *     // Commas within strings do not separate selectors.
1651       *     $selectors   = self::split_selector_list( '[data-label="Save, continue"],.fallback' );
1652       *     $selectors === array( '[data-label="Save, continue"]', '.fallback' )
1653       *
1654       *     array( 'lang(zh, "*-hant")', '.foo' ) === self::split_selector_list( 'lang(zh, "*-hant"), .foo' );
1655       *
1656       *     // Identifiers may contain escaped commas.
1657       *     array( '.foo\,bar', '.baz' ) === self::split_selector_list( '.foo\,bar,.baz' );
1658       *
1659       *     // Comments stay with the selector they follow.
1660       *     array( '.a /* a, the first *\/', '.b' ) === self::split_selector_list( '.a /* a, the first *\/,.b' );
1661       *
1662       * @see https://www.w3.org/TR/selectors/#parse-selector
1663       * @see https://www.w3.org/TR/css-syntax-3/
1664       *
1665       * @since 7.1.0
1666       *
1667       * @param string $selector CSS selector list as a string, e.g. '.wp-block .wp-block-paragraph'.
1668       * @return string[] List of trimmed selectors parsed from input list.
1669       */
1670  	protected static function split_selector_list( $selector ): array {
1671          if ( ! str_contains( $selector, ',' ) ) {
1672              // See note on trimming CSS whitespace in main loop.
1673              return array( trim( $selector, " \t\n" ) );
1674          }
1675  
1676          $selectors         = array();
1677          $selector_length   = strlen( $selector );
1678          $parentheses_depth = 0;
1679          $at                = 0;
1680          $was_at            = 0;
1681  
1682          while ( $at < $selector_length ) {
1683              $next_at = $at + strcspn( $selector, '/,\'"()<-\\', $at );
1684              if ( $next_at >= $selector_length ) {
1685                  break;
1686              }
1687  
1688              $next_cp = $selector[ $next_at ];
1689  
1690              // Escaped syntax characters do not act as delimiters.
1691              if ( '\\' === $next_cp ) {
1692                  $at = min( $next_at + 2, $selector_length );
1693                  continue;
1694              }
1695  
1696              /*
1697               * Start of a parenthesized expression, which maintains a stack of parentheses.
1698               * For the sake of this function, no selector list will be split inside parentheses.
1699               * Therefore it’s possible to jump ahead until this list completes.
1700               */
1701              if ( '(' === $next_cp || ')' === $next_cp ) {
1702                  $parentheses_depth += '(' === $next_cp ? 1 : -1;
1703                  $at                 = $next_at + 1;
1704                  continue;
1705              }
1706  
1707              // Start of a string, which will be incorporated into the selector in which it’s found.
1708              if ( "'" === $next_cp || '"' === $next_cp ) {
1709                  $end_of_string = $next_at + 1;
1710                  while ( $end_of_string < $selector_length ) {
1711                      $end_of_string += strcspn( $selector, "{$next_cp}\\", $end_of_string );
1712                      if ( $end_of_string >= $selector_length ) {
1713                          break;
1714                      }
1715  
1716                      $end_cp = $selector[ $end_of_string ];
1717  
1718                      // Skip escaped characters.
1719                      if ( '\\' === $end_cp ) {
1720                          $end_of_string = $end_of_string + 2;
1721                          continue;
1722                      }
1723  
1724                      if ( $next_cp === $end_cp ) {
1725                          ++$end_of_string;
1726                          break;
1727                      }
1728  
1729                      ++$end_of_string;
1730                  }
1731  
1732                  $at = $end_of_string;
1733                  continue;
1734              }
1735  
1736              // Start of a comment, which will be incorporated into the selector in which it’s found.
1737              if ( '/' === $next_cp && ( $next_at + 1 ) < $selector_length && '*' === $selector[ $next_at + 1 ] ) {
1738                  $comment_end_at = strpos( $selector, '*/', $next_at + 1 );
1739                  $is_terminated  = false !== $comment_end_at;
1740                  $after_comment  = $is_terminated ? $comment_end_at + 2 : strlen( $selector );
1741                  $at             = $after_comment;
1742                  continue;
1743              }
1744  
1745              // Start of a CDO or CDC, which will be incorporated into the selector in which it’s found.
1746              if (
1747                  ( '<' === $next_cp && 0 === substr_compare( $selector, '<!--', $next_at, 4 ) ) ||
1748                  ( '-' === $next_cp && 0 === substr_compare( $selector, '-->', $next_at, 3 ) )
1749              ) {
1750                  $at = $next_at + ( '<' === $next_cp ? 4 : 3 );
1751                  continue;
1752              }
1753  
1754              // Everything else is either a comma token or part of a selector.
1755              if ( ',' === $next_cp && 0 === $parentheses_depth ) {
1756                  /**
1757                   * Trim each selector so that downstream code doesn’t see whitespace
1758                   * as the first character in a selector and get confused.
1759                   *
1760                   * There is inconsistency in this because comments and other syntax
1761                   * are included which are also not part of the selector itself, but
1762                   * a tradeoff is made between removing common syntax which carries
1763                   * no meaning and rarer syntax which leaves auxiliary information.
1764                   *
1765                   * > A newline, U+0009 CHARACTER TABULATION, or U+0020 SPACE.
1766                   * > Note that U+000D CARRIAGE RETURN and U+000C FORM FEED are
1767                   * > not included in this definition, as they are converted
1768                   * > to U+000A LINE FEED during preprocessing.
1769                   *
1770                   * @see https://www.w3.org/TR/css-syntax/#whitespace
1771                   * @see https://www.w3.org/TR/css-syntax/#newline
1772                   */
1773                  $selectors[] = trim( substr( $selector, $was_at, $next_at - $was_at ), " \t\n" );
1774                  $at          = $next_at + 1;
1775                  $was_at      = $at;
1776                  continue;
1777              }
1778  
1779              $at = $next_at + 1;
1780          }
1781  
1782          if ( $was_at < $selector_length ) {
1783              // See note on trimming CSS whitespace in main loop.
1784              $selectors[] = trim( substr( $selector, $was_at ), " \t\n" );
1785          }
1786  
1787          return $selectors;
1788      }
1789  
1790      /**
1791       * Returns the metadata for each block.
1792       *
1793       * Example:
1794       *
1795       *     {
1796       *       'core/paragraph': {
1797       *         'selector': 'p',
1798       *         'elements': {
1799       *           'link' => 'link selector',
1800       *           'etc'  => 'element selector'
1801       *         }
1802       *       },
1803       *       'core/heading': {
1804       *         'selector': 'h1',
1805       *         'elements': {}
1806       *       },
1807       *       'core/image': {
1808       *         'selector': '.wp-block-image',
1809       *         'duotone': 'img',
1810       *         'elements': {}
1811       *       }
1812       *     }
1813       *
1814       * @since 5.8.0
1815       * @since 5.9.0 Added `duotone` key with CSS selector.
1816       * @since 6.1.0 Added `features` key with block support feature level selectors.
1817       * @since 6.3.0 Refactored and stabilized selectors API.
1818       * @since 6.6.0 Updated to include block style variations from the block styles registry.
1819       *
1820       * @return array Block metadata.
1821       */
1822  	protected static function get_blocks_metadata() {
1823          $registry       = WP_Block_Type_Registry::get_instance();
1824          $blocks         = $registry->get_all_registered();
1825          $style_registry = WP_Block_Styles_Registry::get_instance();
1826  
1827          // Is there metadata for all currently registered blocks?
1828          $blocks = array_diff_key( $blocks, static::$blocks_metadata );
1829          if ( empty( $blocks ) ) {
1830              /*
1831               * New block styles may have been registered within WP_Block_Styles_Registry.
1832               * Update block metadata for any new block style variations.
1833               */
1834              $registered_styles = $style_registry->get_all_registered();
1835              foreach ( static::$blocks_metadata as $block_name => $block_metadata ) {
1836                  if ( ! empty( $registered_styles[ $block_name ] ) ) {
1837                      $style_selectors = $block_metadata['styleVariations'] ?? array();
1838  
1839                      foreach ( $registered_styles[ $block_name ] as $block_style ) {
1840                          if ( ! isset( $style_selectors[ $block_style['name'] ] ) ) {
1841                              $style_selectors[ $block_style['name'] ] = static::get_block_style_variation_selector( $block_style['name'], $block_metadata['selector'] );
1842                          }
1843                      }
1844  
1845                      static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
1846                  }
1847              }
1848              return static::$blocks_metadata;
1849          }
1850  
1851          foreach ( $blocks as $block_name => $block_type ) {
1852              $root_selector = wp_get_block_css_selector( $block_type );
1853  
1854              static::$blocks_metadata[ $block_name ]['selector']  = $root_selector;
1855              static::$blocks_metadata[ $block_name ]['selectors'] = static::get_block_selectors( $block_type, $root_selector );
1856  
1857              $elements = static::get_block_element_selectors( $root_selector );
1858              if ( ! empty( $elements ) ) {
1859                  static::$blocks_metadata[ $block_name ]['elements'] = $elements;
1860              }
1861  
1862              // The block may or may not have a duotone selector.
1863              $duotone_selector = wp_get_block_css_selector( $block_type, 'filter.duotone' );
1864  
1865              // Keep backwards compatibility for support.color.__experimentalDuotone.
1866              if ( null === $duotone_selector ) {
1867                  $duotone_support = $block_type->supports['color']['__experimentalDuotone'] ?? null;
1868  
1869                  if ( $duotone_support ) {
1870                      $root_selector    = wp_get_block_css_selector( $block_type );
1871                      $duotone_selector = static::scope_selector( $root_selector, $duotone_support );
1872                  }
1873              }
1874  
1875              if ( null !== $duotone_selector ) {
1876                  static::$blocks_metadata[ $block_name ]['duotone'] = $duotone_selector;
1877              }
1878  
1879              // If the block has style variations, append their selectors to the block metadata.
1880              $style_selectors = array();
1881              if ( ! empty( $block_type->styles ) ) {
1882                  foreach ( $block_type->styles as $style ) {
1883                      $style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
1884                  }
1885              }
1886  
1887              // Block style variations can be registered through the WP_Block_Styles_Registry as well as block.json.
1888              $registered_styles = $style_registry->get_registered_styles_for_block( $block_name );
1889              foreach ( $registered_styles as $style ) {
1890                  $style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
1891              }
1892  
1893              if ( ! empty( $style_selectors ) ) {
1894                  static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
1895              }
1896  
1897              // If the block has custom states defined in block.json, store their selectors.
1898              if ( ! empty( $block_type->selectors['states'] ) && is_array( $block_type->selectors['states'] ) ) {
1899                  static::$blocks_metadata[ $block_name ]['states'] = $block_type->selectors['states'];
1900              }
1901          }
1902  
1903          return static::$blocks_metadata;
1904      }
1905  
1906      /**
1907       * Given a tree, removes the keys that are not present in the schema.
1908       *
1909       * It is recursive and modifies the input in-place.
1910       *
1911       * @since 5.8.0
1912       * @since 7.0.0 Added type validation for boolean values.
1913       *
1914       * @param array $tree   Input to process.
1915       * @param array $schema Schema to adhere to.
1916       * @return array The modified $tree.
1917       */
1918  	protected static function remove_keys_not_in_schema( $tree, $schema ) {
1919          if ( ! is_array( $tree ) ) {
1920              return $tree;
1921          }
1922  
1923          foreach ( $tree as $key => $value ) {
1924              // Remove keys not in the schema or with null/empty values.
1925              if ( ! array_key_exists( $key, $schema ) ) {
1926                  unset( $tree[ $key ] );
1927                  continue;
1928              }
1929  
1930              // Validate type if schema specifies a boolean marker.
1931              if ( is_bool( $schema[ $key ] ) ) {
1932                  // Schema expects a boolean value - validate the input matches.
1933                  if ( ! is_bool( $value ) ) {
1934                      unset( $tree[ $key ] );
1935                      continue;
1936                  }
1937                  // Type matches, keep the value and continue to next key.
1938                  continue;
1939              }
1940  
1941              if ( is_array( $schema[ $key ] ) ) {
1942                  if ( ! is_array( $value ) ) {
1943                      unset( $tree[ $key ] );
1944                  } elseif ( wp_is_numeric_array( $value ) ) {
1945                      // If indexed, process each item in the array.
1946                      foreach ( $value as $item_key => $item_value ) {
1947                          if ( isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] ) ) {
1948                              $tree[ $key ][ $item_key ] = self::remove_keys_not_in_schema( $item_value, $schema[ $key ][0] );
1949                          } else {
1950                              // If the schema does not define a further structure, keep the value as is.
1951                              $tree[ $key ][ $item_key ] = $item_value;
1952                          }
1953                      }
1954                  } else {
1955                      // If associative, process as a single object.
1956                      $tree[ $key ] = self::remove_keys_not_in_schema( $value, $schema[ $key ] );
1957  
1958                      if ( empty( $tree[ $key ] ) ) {
1959                          unset( $tree[ $key ] );
1960                      }
1961                  }
1962              }
1963          }
1964          return $tree;
1965      }
1966  
1967      /**
1968       * Returns the existing settings for each block.
1969       *
1970       * Example:
1971       *
1972       *     {
1973       *       'root': {
1974       *         'color': {
1975       *           'custom': true
1976       *         }
1977       *       },
1978       *       'core/paragraph': {
1979       *         'spacing': {
1980       *           'customPadding': true
1981       *         }
1982       *       }
1983       *     }
1984       *
1985       * @since 5.8.0
1986       *
1987       * @return array Settings per block.
1988       */
1989  	public function get_settings() {
1990          if ( ! isset( $this->theme_json['settings'] ) ) {
1991              return array();
1992          } else {
1993              return $this->theme_json['settings'];
1994          }
1995      }
1996  
1997      /**
1998       * Returns the stylesheet that results of processing
1999       * the theme.json structure this object represents.
2000       *
2001       * @since 5.8.0
2002       * @since 5.9.0 Removed the `$type` parameter, added the `$types` and `$origins` parameters.
2003       * @since 6.3.0 Add fallback layout styles for Post Template when block gap support isn't available.
2004       * @since 6.6.0 Added boolean `skip_root_layout_styles` and `include_block_style_variations` options
2005       *              to control styles output as desired.
2006       * @since 7.0.0 Deprecated 'base-layout-styles' type; added `base_layout_styles` option for classic themes.
2007       *
2008       * @param string[] $types   Types of styles to load. Will load all by default. It accepts:
2009       *                          - `variables`: only the CSS Custom Properties for presets & custom ones.
2010       *                          - `styles`: only the styles section in theme.json.
2011       *                          - `presets`: only the classes for the presets.
2012       *                          - `base-layout-styles`: only the base layout styles. Deprecated in 7.0.0.
2013       *                          - `custom-css`: only the custom CSS.
2014       * @param string[] $origins A list of origins to include. By default it includes VALID_ORIGINS.
2015       * @param array    $options {
2016       *     Optional. An array of options for now used for internal purposes only (may change without notice).
2017       *
2018       *     @type string $scope                           Makes sure all style are scoped to a given selector
2019       *     @type string $root_selector                   Overwrites and forces a given selector to be used on the root node
2020       *     @type bool   $skip_root_layout_styles         Omits root layout styles from the generated stylesheet. Default false.
2021       *     @type bool   $base_layout_styles              When true generates only base layout styles without alignment rules. Default false.
2022       *     @type bool   $include_block_style_variations  Includes styles for block style variations in the generated stylesheet. Default false.
2023       * }
2024       * @return string The resulting stylesheet.
2025       */
2026  	public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) {
2027          if ( null === $origins ) {
2028              $origins = static::VALID_ORIGINS;
2029          }
2030  
2031          if ( is_string( $types ) ) {
2032              // Dispatch error and map old arguments to new ones.
2033              _deprecated_argument( __FUNCTION__, '5.9.0' );
2034              if ( 'block_styles' === $types ) {
2035                  $types = array( 'styles', 'presets' );
2036              } elseif ( 'css_variables' === $types ) {
2037                  $types = array( 'variables' );
2038              } else {
2039                  $types = array( 'variables', 'styles', 'presets' );
2040              }
2041          }
2042  
2043          $blocks_metadata = static::get_blocks_metadata();
2044          $style_nodes     = static::get_style_nodes( $this->theme_json, $blocks_metadata, $options );
2045          $setting_nodes   = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
2046  
2047          $root_style_key    = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true );
2048          $root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true );
2049  
2050          if ( ! empty( $options['scope'] ) ) {
2051              foreach ( $setting_nodes as &$node ) {
2052                  $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
2053              }
2054              foreach ( $style_nodes as &$node ) {
2055                  $node = static::scope_style_node_selectors( $options['scope'], $node );
2056              }
2057              unset( $node );
2058          }
2059  
2060          if ( ! empty( $options['root_selector'] ) ) {
2061              if ( false !== $root_settings_key ) {
2062                  $setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector'];
2063              }
2064              if ( false !== $root_style_key ) {
2065                  $style_nodes[ $root_style_key ]['selector'] = $options['root_selector'];
2066              }
2067          }
2068  
2069          $stylesheet = '';
2070  
2071          if ( in_array( 'variables', $types, true ) ) {
2072              $stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
2073          }
2074  
2075          if ( in_array( 'styles', $types, true ) ) {
2076              if ( false !== $root_style_key && empty( $options['skip_root_layout_styles'] ) ) {
2077                  $stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ], $options );
2078              }
2079              $stylesheet .= $this->get_block_classes( $style_nodes );
2080          }
2081  
2082          if ( in_array( 'presets', $types, true ) ) {
2083              $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
2084          }
2085  
2086          // Load the custom CSS last so it has the highest specificity.
2087          if ( in_array( 'custom-css', $types, true ) ) {
2088              // Add the global styles root CSS.
2089              $stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) );
2090          }
2091  
2092          return $stylesheet;
2093      }
2094  
2095      /**
2096       * Processes the CSS, to apply nesting.
2097       *
2098       * @since 6.2.0
2099       * @since 6.6.0 Enforced 0-1-0 specificity for block custom CSS selectors.
2100       * @since 7.0.0 Made public for use in custom-css block support.
2101       *
2102       * @param string $css      The CSS to process.
2103       * @param string $selector The selector to nest.
2104       * @return string The processed CSS.
2105       */
2106  	public static function process_blocks_custom_css( $css, $selector ) {
2107          $processed_css = '';
2108  
2109          if ( empty( $css ) ) {
2110              return $processed_css;
2111          }
2112  
2113          // Split CSS nested rules.
2114          $parts = explode( '&', $css );
2115          foreach ( $parts as $part ) {
2116              if ( empty( $part ) ) {
2117                  continue;
2118              }
2119              $is_root_css = ( ! str_contains( $part, '{' ) );
2120              if ( $is_root_css ) {
2121                  // If the part doesn't contain braces, it applies to the root level.
2122                  $processed_css .= ':root :where(' . trim( $selector ) . '){' . trim( $part ) . '}';
2123              } else {
2124                  // If the part contains braces, it's a nested CSS rule.
2125                  $part = explode( '{', str_replace( '}', '', $part ) );
2126                  if ( count( $part ) !== 2 ) {
2127                      continue;
2128                  }
2129                  $nested_selector = $part[0];
2130                  $css_value       = $part[1];
2131  
2132                  /*
2133                   * Handle pseudo elements such as ::before, ::after etc. Regex will also
2134                   * capture any leading combinator such as >, +, or ~, as well as spaces.
2135                   * This allows pseudo elements as descendants e.g. `.parent ::before`.
2136                   */
2137                  $matches            = array();
2138                  $has_pseudo_element = preg_match( '/([>+~\s]*::[a-zA-Z-]+)/', $nested_selector, $matches );
2139                  $pseudo_part        = $has_pseudo_element ? $matches[1] : '';
2140                  $nested_selector    = $has_pseudo_element ? str_replace( $pseudo_part, '', $nested_selector ) : $nested_selector;
2141  
2142                  // Finalize selector and re-append pseudo element if required.
2143                  $part_selector  = str_starts_with( $nested_selector, ' ' )
2144                      ? static::scope_selector( $selector, $nested_selector )
2145                      : static::append_to_selector( $selector, $nested_selector );
2146                  $final_selector = ":root :where($part_selector)$pseudo_part";
2147  
2148                  $processed_css .= $final_selector . '{' . trim( $css_value ) . '}';
2149              }
2150          }
2151          return $processed_css;
2152      }
2153  
2154      /**
2155       * Returns the global styles custom CSS.
2156       *
2157       * @since 6.2.0
2158       * @deprecated 6.7.0 Use {@see 'get_stylesheet'} instead.
2159       *
2160       * @return string The global styles custom CSS.
2161       */
2162  	public function get_custom_css() {
2163          _deprecated_function( __METHOD__, '6.7.0', 'get_stylesheet' );
2164          // Add the global styles root CSS.
2165          $stylesheet = $this->theme_json['styles']['css'] ?? '';
2166  
2167          // Add the global styles block CSS.
2168          if ( isset( $this->theme_json['styles']['blocks'] ) ) {
2169              foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) {
2170                  $custom_block_css = $this->theme_json['styles']['blocks'][ $name ]['css'] ?? null;
2171                  if ( $custom_block_css ) {
2172                      $selector    = static::$blocks_metadata[ $name ]['selector'];
2173                      $stylesheet .= $this->process_blocks_custom_css( $custom_block_css, $selector );
2174                  }
2175              }
2176          }
2177  
2178          return $stylesheet;
2179      }
2180  
2181      /**
2182       * Returns the page templates of the active theme.
2183       *
2184       * @since 5.9.0
2185       *
2186       * @return array
2187       */
2188  	public function get_custom_templates() {
2189          $custom_templates = array();
2190          if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $this->theme_json['customTemplates'] ) ) {
2191              return $custom_templates;
2192          }
2193  
2194          foreach ( $this->theme_json['customTemplates'] as $item ) {
2195              if ( isset( $item['name'] ) ) {
2196                  $custom_templates[ $item['name'] ] = array(
2197                      'title'     => $item['title'] ?? '',
2198                      'postTypes' => $item['postTypes'] ?? array( 'page' ),
2199                  );
2200              }
2201          }
2202          return $custom_templates;
2203      }
2204  
2205      /**
2206       * Returns the template part data of active theme.
2207       *
2208       * @since 5.9.0
2209       *
2210       * @return array
2211       */
2212  	public function get_template_parts() {
2213          $template_parts = array();
2214          if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $this->theme_json['templateParts'] ) ) {
2215              return $template_parts;
2216          }
2217  
2218          foreach ( $this->theme_json['templateParts'] as $item ) {
2219              if ( isset( $item['name'] ) ) {
2220                  $template_parts[ $item['name'] ] = array(
2221                      'title' => $item['title'] ?? '',
2222                      'area'  => $item['area'] ?? '',
2223                  );
2224              }
2225          }
2226          return $template_parts;
2227      }
2228  
2229      /**
2230       * Converts each style section into a list of rulesets
2231       * containing the block styles to be appended to the stylesheet.
2232       *
2233       * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
2234       *
2235       * For each section this creates a new ruleset such as:
2236       *
2237       *   block-selector {
2238       *     style-property-one: value;
2239       *   }
2240       *
2241       * @since 5.8.0 As `get_block_styles()`.
2242       * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()`
2243       *              and no longer returns preset classes.
2244       *              Removed the `$setting_nodes` parameter.
2245       * @since 6.1.0 Moved most internal logic to `get_styles_for_block()`.
2246       *
2247       * @param array $style_nodes Nodes with styles.
2248       * @return string The new stylesheet.
2249       */
2250  	protected function get_block_classes( $style_nodes ) {
2251          $block_rules = '';
2252  
2253          foreach ( $style_nodes as $metadata ) {
2254              if ( null === $metadata['selector'] ) {
2255                  continue;
2256              }
2257              $block_rules .= static::get_styles_for_block( $metadata );
2258          }
2259  
2260          return $block_rules;
2261      }
2262  
2263      /**
2264       * Gets the CSS layout rules for a particular block from theme.json layout definitions.
2265       *
2266       * @since 6.1.0
2267       * @since 6.3.0 Reduced specificity for layout margin rules.
2268       * @since 6.5.1 Only output rules referencing content and wide sizes when values exist.
2269       * @since 6.5.3 Add types parameter to check if only base layout styles are needed.
2270       * @since 6.6.0 Updated layout style specificity to be compatible with overall 0-1-0 specificity in global styles.
2271       * @since 7.0.0 Replaced `$types` parameter with `$options` array; base layout styles controlled via `base_layout_styles` option.
2272       *
2273       * @param array $block_metadata Metadata about the block to get styles for.
2274       * @param array $options        Optional. An array of options for now used for internal purposes only.
2275       * @return string Layout styles for the block.
2276       */
2277  	protected function get_layout_styles( $block_metadata, $options = array() ) {
2278          $block_rules = '';
2279          $block_type  = null;
2280  
2281          // Skip outputting layout styles if explicitly disabled.
2282          if ( current_theme_supports( 'disable-layout-styles' ) ) {
2283              return $block_rules;
2284          }
2285  
2286          if ( isset( $block_metadata['name'] ) ) {
2287              $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_metadata['name'] );
2288              if ( ! block_has_support( $block_type, 'layout', false ) && ! block_has_support( $block_type, '__experimentalLayout', false ) ) {
2289                  return $block_rules;
2290              }
2291          }
2292  
2293          $selector                 = $block_metadata['selector'] ?? '';
2294          $has_block_gap_support    = isset( $this->theme_json['settings']['spacing']['blockGap'] );
2295          $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.
2296          $node                     = $options['node'] ?? _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
2297          $layout_definitions       = wp_get_layout_definitions();
2298          $layout_selector_pattern  = '/^[a-zA-Z0-9\-\.\,\ *+>:\(\)]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, child combinator and pseudo class selectors.
2299  
2300          /*
2301           * Gap styles will only be output if the theme has block gap support, or supports a fallback gap.
2302           * Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value.
2303           */
2304          if ( $has_block_gap_support || $has_fallback_gap_support ) {
2305              $block_gap_value = null;
2306              // Use a fallback gap value if block gap support is not available.
2307              if ( ! $has_block_gap_support ) {
2308                  $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null;
2309                  if ( ! empty( $block_type ) ) {
2310                      $block_gap_value = $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? null;
2311                  }
2312              } else {
2313                  $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) );
2314              }
2315  
2316              // Support split row / column values and concatenate to a shorthand value.
2317              if ( is_array( $block_gap_value ) ) {
2318                  if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) {
2319                      $gap_row         = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) );
2320                      $gap_column      = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) );
2321                      $block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column;
2322                  } else {
2323                      // Skip outputting gap value if not all sides are provided.
2324                      $block_gap_value = null;
2325                  }
2326              }
2327  
2328              // If the block should have custom gap, add the gap styles.
2329              if ( null !== $block_gap_value && false !== $block_gap_value && '' !== $block_gap_value ) {
2330                  foreach ( $layout_definitions as $layout_definition_key => $layout_definition ) {
2331                      // Allow outputting fallback gap styles for flex and grid layout types when block gap support isn't available.
2332                      if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key && 'grid' !== $layout_definition_key ) {
2333                          continue;
2334                      }
2335  
2336                      $class_name    = $layout_definition['className'] ?? false;
2337                      $spacing_rules = $layout_definition['spacingStyles'] ?? array();
2338  
2339                      if (
2340                          ! empty( $class_name ) &&
2341                          ! empty( $spacing_rules )
2342                      ) {
2343                          foreach ( $spacing_rules as $spacing_rule ) {
2344                              $declarations = array();
2345                              if (
2346                                  isset( $spacing_rule['selector'] ) &&
2347                                  preg_match( $layout_selector_pattern, $spacing_rule['selector'] ) &&
2348                                  ! empty( $spacing_rule['rules'] )
2349                              ) {
2350                                  // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value.
2351                                  foreach ( $spacing_rule['rules'] as $css_property => $css_value ) {
2352                                      $current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value;
2353                                      if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) {
2354                                          $declarations[] = array(
2355                                              'name'  => $css_property,
2356                                              'value' => $current_css_value,
2357                                          );
2358                                      }
2359                                  }
2360  
2361                                  if ( ! $has_block_gap_support ) {
2362                                      // For fallback gap styles, use lower specificity, to ensure styles do not unintentionally override theme styles.
2363                                      $format          = static::ROOT_BLOCK_SELECTOR === $selector ? ':where(.%2$s%3$s)' : ':where(%1$s.%2$s%3$s)';
2364                                      $layout_selector = sprintf(
2365                                          $format,
2366                                          $selector,
2367                                          $class_name,
2368                                          $spacing_rule['selector']
2369                                      );
2370                                  } else {
2371                                      $format          = static::ROOT_BLOCK_SELECTOR === $selector ? ':root :where(.%2$s)%3$s' : ':root :where(%1$s-%2$s)%3$s';
2372                                      $layout_selector = sprintf(
2373                                          $format,
2374                                          $selector,
2375                                          $class_name,
2376                                          $spacing_rule['selector']
2377                                      );
2378                                  }
2379                                  $block_rules .= static::to_ruleset( $layout_selector, $declarations );
2380                              }
2381                          }
2382                      }
2383                  }
2384              }
2385          }
2386  
2387          // Output base styles.
2388          if (
2389              static::ROOT_BLOCK_SELECTOR === $selector
2390          ) {
2391              $valid_display_modes = array( 'block', 'flex', 'grid' );
2392              foreach ( $layout_definitions as $layout_definition ) {
2393                  $class_name       = $layout_definition['className'] ?? false;
2394                  $base_style_rules = $layout_definition['baseStyles'] ?? array();
2395  
2396                  if (
2397                      ! empty( $class_name ) &&
2398                      is_array( $base_style_rules )
2399                  ) {
2400                      // Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`.
2401                      if (
2402                          ! empty( $layout_definition['displayMode'] ) &&
2403                          is_string( $layout_definition['displayMode'] ) &&
2404                          in_array( $layout_definition['displayMode'], $valid_display_modes, true )
2405                      ) {
2406                          $layout_selector = sprintf(
2407                              '%s .%s',
2408                              $selector,
2409                              $class_name
2410                          );
2411                          $block_rules    .= static::to_ruleset(
2412                              $layout_selector,
2413                              array(
2414                                  array(
2415                                      'name'  => 'display',
2416                                      'value' => $layout_definition['displayMode'],
2417                                  ),
2418                              )
2419                          );
2420                      }
2421  
2422                      foreach ( $base_style_rules as $base_style_rule ) {
2423                          $declarations = array();
2424  
2425                          // Skip outputting base styles for flow and constrained layout types when base_layout_styles is enabled.
2426                          // These themes don't use .wp-site-blocks wrapper, so these layout-specific alignment styles aren't needed.
2427                          if ( ! empty( $options['base_layout_styles'] ) && ( 'default' === $layout_definition['name'] || 'constrained' === $layout_definition['name'] ) ) {
2428                              continue;
2429                          }
2430  
2431                          if (
2432                              isset( $base_style_rule['selector'] ) &&
2433                              preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) &&
2434                              ! empty( $base_style_rule['rules'] )
2435                          ) {
2436                              foreach ( $base_style_rule['rules'] as $css_property => $css_value ) {
2437                                  // Skip rules that reference content size or wide size if they are not defined in the theme.json.
2438                                  if (
2439                                      is_string( $css_value ) &&
2440                                      ( str_contains( $css_value, '--global--content-size' ) || str_contains( $css_value, '--global--wide-size' ) ) &&
2441                                      ! isset( $this->theme_json['settings']['layout']['contentSize'] ) &&
2442                                      ! isset( $this->theme_json['settings']['layout']['wideSize'] )
2443                                  ) {
2444                                      continue;
2445                                  }
2446  
2447                                  if ( static::is_safe_css_declaration( $css_property, $css_value ) ) {
2448                                      $declarations[] = array(
2449                                          'name'  => $css_property,
2450                                          'value' => $css_value,
2451                                      );
2452                                  }
2453                              }
2454  
2455                              $layout_selector = sprintf(
2456                                  '.%s%s',
2457                                  $class_name,
2458                                  $base_style_rule['selector']
2459                              );
2460                              $block_rules    .= static::to_ruleset( $layout_selector, $declarations );
2461                          }
2462                      }
2463                  }
2464              }
2465          }
2466  
2467          if ( ! empty( $options['media_query'] ) && ! empty( $block_rules ) ) {
2468              $block_rules = $options['media_query'] . '{' . $block_rules . '}';
2469          }
2470  
2471          return $block_rules;
2472      }
2473  
2474      /**
2475       * Creates new rulesets as classes for each preset value such as:
2476       *
2477       *   .has-value-color {
2478       *     color: value;
2479       *   }
2480       *
2481       *   .has-value-background-color {
2482       *     background-color: value;
2483       *   }
2484       *
2485       *   .has-value-font-size {
2486       *     font-size: value;
2487       *   }
2488       *
2489       *   .has-value-gradient-background {
2490       *     background: value;
2491       *   }
2492       *
2493       *   :where(p).has-value-gradient-background {
2494       *     background: value;
2495       *   }
2496       *
2497       * @since 5.9.0
2498       *
2499       * @param array    $setting_nodes Nodes with settings.
2500       * @param string[] $origins       List of origins to process presets from.
2501       * @return string The new stylesheet.
2502       */
2503  	protected function get_preset_classes( $setting_nodes, $origins ) {
2504          $preset_rules = '';
2505  
2506          foreach ( $setting_nodes as $metadata ) {
2507              if ( null === $metadata['selector'] ) {
2508                  continue;
2509              }
2510  
2511              $selector      = $metadata['selector'];
2512              $node          = _wp_array_get( $this->theme_json, $metadata['path'], array() );
2513              $preset_rules .= static::compute_preset_classes( $node, $selector, $origins );
2514          }
2515  
2516          return $preset_rules;
2517      }
2518  
2519      /**
2520       * Converts each styles section into a list of rulesets
2521       * to be appended to the stylesheet.
2522       * These rulesets contain all the css variables (custom variables and preset variables).
2523       *
2524       * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
2525       *
2526       * For each section this creates a new ruleset such as:
2527       *
2528       *     block-selector {
2529       *       --wp--preset--category--slug: value;
2530       *       --wp--custom--variable: value;
2531       *     }
2532       *
2533       * @since 5.8.0
2534       * @since 5.9.0 Added the `$origins` parameter.
2535       *
2536       * @param array    $nodes   Nodes with settings.
2537       * @param string[] $origins List of origins to process.
2538       * @return string The new stylesheet.
2539       */
2540  	protected function get_css_variables( $nodes, $origins ) {
2541          $stylesheet = '';
2542          foreach ( $nodes as $metadata ) {
2543              if ( null === $metadata['selector'] ) {
2544                  continue;
2545              }
2546  
2547              $selector          = $metadata['selector'];
2548              $feature_selectors = $metadata['selectors'] ?? array();
2549              $node              = _wp_array_get( $this->theme_json, $metadata['path'], array() );
2550  
2551              /*
2552               * Group preset declarations by selector. Blocks that define
2553               * feature-level selectors need their preset CSS variables
2554               * output under that feature selector instead of the block's
2555               * root selector.
2556               */
2557              $vars_by_selector              = array();
2558              $vars_by_selector[ $selector ] = array();
2559  
2560              foreach ( static::PRESETS_METADATA as $preset_metadata ) {
2561                  if ( empty( $preset_metadata['css_vars'] ) ) {
2562                      continue;
2563                  }
2564  
2565                  $values_by_slug = static::get_settings_values_by_slug( $node, $preset_metadata, $origins );
2566                  if ( empty( $values_by_slug ) ) {
2567                      continue;
2568                  }
2569  
2570                  $target = self::get_feature_selector( $feature_selectors, $preset_metadata['path'][0], $selector );
2571  
2572                  if ( ! isset( $vars_by_selector[ $target ] ) ) {
2573                      $vars_by_selector[ $target ] = array();
2574                  }
2575  
2576                  foreach ( $values_by_slug as $slug => $value ) {
2577                      $vars_by_selector[ $target ][] = array(
2578                          'name'  => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
2579                          'value' => $value,
2580                      );
2581                  }
2582              }
2583  
2584              // Theme vars always use the block's default selector.
2585              foreach ( static::compute_theme_vars( $node ) as $theme_var ) {
2586                  $vars_by_selector[ $selector ][] = $theme_var;
2587              }
2588  
2589              foreach ( $vars_by_selector as $rule_selector => $declarations ) {
2590                  $stylesheet .= static::to_ruleset( $rule_selector, $declarations );
2591              }
2592          }
2593  
2594          return $stylesheet;
2595      }
2596  
2597      /**
2598       * Returns the appropriate selector for a block support feature's
2599       * preset CSS variables.
2600       *
2601       * If the block defines a feature-level selector (as a string or an
2602       * object with a `root` key), that selector is returned. Otherwise,
2603       * the block's default selector is used.
2604       *
2605       * @since 7.1.0
2606       *
2607       * @param array<string, string|array<string, string>> $feature_selectors The block's feature selectors map.
2608       * @param string                                      $feature_key       The feature to look up (e.g. 'dimensions').
2609       * @param string                                      $default_selector  Fallback selector.
2610       * @return string The resolved selector.
2611       */
2612  	private static function get_feature_selector( array $feature_selectors, string $feature_key, string $default_selector ): string {
2613          if ( ! isset( $feature_selectors[ $feature_key ] ) ) {
2614              return $default_selector;
2615          }
2616  
2617          $feature = $feature_selectors[ $feature_key ];
2618  
2619          if ( is_string( $feature ) ) {
2620              return $feature;
2621          }
2622  
2623          if ( isset( $feature['root'] ) && is_string( $feature['root'] ) ) {
2624              return $feature['root'];
2625          }
2626  
2627          return $default_selector;
2628      }
2629  
2630      /**
2631       * Given a selector and a declaration list,
2632       * creates the corresponding ruleset.
2633       *
2634       * @since 5.8.0
2635       * @since 7.1.0 Skip declarations whose value is not a plain string (booleans, arrays, objects, etc.).
2636       *
2637       * @param string $selector     CSS selector.
2638       * @param array  $declarations List of declarations.
2639       * @return string The resulting CSS ruleset.
2640       */
2641  	protected static function to_ruleset( $selector, $declarations ) {
2642          if ( empty( $declarations ) ) {
2643              return '';
2644          }
2645  
2646          $declaration_block = array_reduce(
2647              $declarations,
2648              static function ( $carry, $element ) {
2649                  $value = $element['value'];
2650  
2651                  if ( is_numeric( $value ) ) {
2652                      $value = (string) $value;
2653                  }
2654  
2655                  if ( ! is_string( $value ) ) {
2656                      return $carry;
2657                  }
2658  
2659                  return $carry .= $element['name'] . ': ' . $value . ';';
2660              },
2661              ''
2662          );
2663  
2664          return $selector . '{' . $declaration_block . '}';
2665      }
2666  
2667      /**
2668       * Given a settings array, returns the generated rulesets
2669       * for the preset classes.
2670       *
2671       * @since 5.8.0
2672       * @since 5.9.0 Added the `$origins` parameter.
2673       * @since 6.6.0 Added check for root CSS properties selector.
2674       * @since 7.1.0 Wraps block-level preset classes in `:where()` to match root-level specificity.
2675       *
2676       * @param array    $settings Settings to process.
2677       * @param string   $selector Selector wrapping the classes.
2678       * @param string[] $origins  List of origins to process.
2679       * @return string The result of processing the presets.
2680       */
2681  	protected static function compute_preset_classes( $settings, $selector, $origins ) {
2682          if ( static::ROOT_BLOCK_SELECTOR === $selector || static::ROOT_CSS_PROPERTIES_SELECTOR === $selector ) {
2683              /*
2684               * Classes at the global level do not need any CSS prefixed,
2685               * and we don't want to increase its specificity.
2686               */
2687              $selector = '';
2688          }
2689  
2690          $stylesheet = '';
2691          foreach ( static::PRESETS_METADATA as $preset_metadata ) {
2692              if ( empty( $preset_metadata['classes'] ) ) {
2693                  continue;
2694              }
2695              $slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins );
2696              foreach ( $preset_metadata['classes'] as $class => $property ) {
2697                  foreach ( $slugs as $slug ) {
2698                      $css_var    = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
2699                      $class_name = static::replace_slug_in_string( $class, $slug );
2700  
2701                      /*
2702                       * $selector is often empty (root-level presets), in which case the
2703                       * bare class is used. For block-level presets the block selector is
2704                       * wrapped in `:where()` so the class keeps the same 0-1-0 specificity
2705                       * as a root-level preset. Without this, block-level palette rules
2706                       * (e.g. `p.has-x-color`) out-rank equally-important rules that also
2707                       * target the same property at 0-1-0, such as per-instance responsive
2708                       * state styles.
2709                       */
2710                      $new_selector = '' === $selector ? $class_name : ':where(' . $selector . ')' . $class_name;
2711                      $stylesheet  .= static::to_ruleset(
2712                          $new_selector,
2713                          array(
2714                              array(
2715                                  'name'  => $property,
2716                                  'value' => 'var(' . $css_var . ') !important',
2717                              ),
2718                          )
2719                      );
2720                  }
2721              }
2722          }
2723  
2724          return $stylesheet;
2725      }
2726  
2727      /**
2728       * Function that scopes a selector with another one. This works a bit like
2729       * SCSS nesting except the `&` operator isn't supported.
2730       *
2731       * <code>
2732       * $scope = '.a, .b .c';
2733       * $selector = '> .x, .y';
2734       * $merged = scope_selector( $scope, $selector );
2735       * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
2736       * </code>
2737       *
2738       * @since 5.9.0
2739       * @since 6.6.0 Added early return if missing scope or selector.
2740       *
2741       * @param string $scope    Selector to scope to.
2742       * @param string $selector Original selector.
2743       * @return string Scoped selector.
2744       */
2745  	public static function scope_selector( $scope, $selector ) {
2746          if ( ! $scope || ! $selector ) {
2747              return $selector;
2748          }
2749  
2750          $scopes    = static::split_selector_list( $scope );
2751          $selectors = static::split_selector_list( $selector );
2752  
2753          $selectors_scoped = array();
2754          foreach ( $scopes as $outer ) {
2755              foreach ( $selectors as $inner ) {
2756                  if ( ! empty( $outer ) && ! empty( $inner ) ) {
2757                      $selectors_scoped[] = $outer . ' ' . $inner;
2758                  } elseif ( empty( $outer ) ) {
2759                      $selectors_scoped[] = $inner;
2760                  } elseif ( empty( $inner ) ) {
2761                      $selectors_scoped[] = $outer;
2762                  }
2763              }
2764          }
2765  
2766          $result = implode( ', ', $selectors_scoped );
2767          return $result;
2768      }
2769  
2770      /**
2771       * Scopes the selectors for a given style node.
2772       *
2773       * This includes the primary selector, i.e. `$node['selector']`, as well as any custom
2774       * selectors for features and subfeatures, e.g. `$node['selectors']['border']` etc.
2775       *
2776       * @since 6.6.0
2777       *
2778       * @param string $scope Selector to scope to.
2779       * @param array  $node  Style node with selectors to scope.
2780       * @return array Node with updated selectors.
2781       */
2782  	protected static function scope_style_node_selectors( $scope, $node ) {
2783          $node['selector'] = static::scope_selector( $scope, $node['selector'] );
2784  
2785          if ( empty( $node['selectors'] ) ) {
2786              return $node;
2787          }
2788  
2789          foreach ( $node['selectors'] as $feature => $selector ) {
2790              if ( is_string( $selector ) ) {
2791                  $node['selectors'][ $feature ] = static::scope_selector( $scope, $selector );
2792              }
2793              if ( is_array( $selector ) ) {
2794                  foreach ( $selector as $subfeature => $subfeature_selector ) {
2795                      $node['selectors'][ $feature ][ $subfeature ] = static::scope_selector( $scope, $subfeature_selector );
2796                  }
2797              }
2798          }
2799  
2800          return $node;
2801      }
2802  
2803      /**
2804       * Gets preset values keyed by slugs based on settings and metadata.
2805       *
2806       * <code>
2807       * $settings = array(
2808       *     'typography' => array(
2809       *         'fontFamilies' => array(
2810       *             array(
2811       *                 'slug'       => 'sansSerif',
2812       *                 'fontFamily' => '"Helvetica Neue", sans-serif',
2813       *             ),
2814       *             array(
2815       *                 'slug'   => 'serif',
2816       *                 'colors' => 'Georgia, serif',
2817       *             )
2818       *         ),
2819       *     ),
2820       * );
2821       * $meta = array(
2822       *    'path'      => array( 'typography', 'fontFamilies' ),
2823       *    'value_key' => 'fontFamily',
2824       * );
2825       * $values_by_slug = get_settings_values_by_slug();
2826       * // $values_by_slug === array(
2827       * //   'sans-serif' => '"Helvetica Neue", sans-serif',
2828       * //   'serif'      => 'Georgia, serif',
2829       * // );
2830       * </code>
2831       *
2832       * @since 5.9.0
2833       * @since 6.6.0 Passing $settings to the callbacks defined in static::PRESETS_METADATA.
2834       *
2835       * @param array    $settings        Settings to process.
2836       * @param array    $preset_metadata One of the PRESETS_METADATA values.
2837       * @param string[] $origins         List of origins to process.
2838       * @return array Array of presets where each key is a slug and each value is the preset value.
2839       */
2840  	protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) {
2841          $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
2842  
2843          $result = array();
2844          foreach ( $origins as $origin ) {
2845              if ( ! isset( $preset_per_origin[ $origin ] ) ) {
2846                  continue;
2847              }
2848              foreach ( $preset_per_origin[ $origin ] as $preset ) {
2849                  $slug = _wp_to_kebab_case( $preset['slug'] );
2850  
2851                  $value = '';
2852                  if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
2853                      $value_key = $preset_metadata['value_key'];
2854                      $value     = $preset[ $value_key ];
2855                  } elseif (
2856                      isset( $preset_metadata['value_func'] ) &&
2857                      is_callable( $preset_metadata['value_func'] )
2858                  ) {
2859                      $value_func = $preset_metadata['value_func'];
2860                      $value      = call_user_func( $value_func, $preset, $settings );
2861                  } else {
2862                      // If we don't have a value, then don't add it to the result.
2863                      continue;
2864                  }
2865  
2866                  $result[ $slug ] = $value;
2867              }
2868          }
2869          return $result;
2870      }
2871  
2872      /**
2873       * Similar to get_settings_values_by_slug, but doesn't compute the value.
2874       *
2875       * @since 5.9.0
2876       *
2877       * @param array    $settings        Settings to process.
2878       * @param array    $preset_metadata One of the PRESETS_METADATA values.
2879       * @param string[] $origins         List of origins to process.
2880       * @return array Array of presets where the key and value are both the slug.
2881       */
2882  	protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) {
2883          if ( null === $origins ) {
2884              $origins = static::VALID_ORIGINS;
2885          }
2886  
2887          $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
2888  
2889          $result = array();
2890          foreach ( $origins as $origin ) {
2891              if ( ! isset( $preset_per_origin[ $origin ] ) ) {
2892                  continue;
2893              }
2894              foreach ( $preset_per_origin[ $origin ] as $preset ) {
2895                  $slug = _wp_to_kebab_case( $preset['slug'] );
2896  
2897                  // Use the array as a set so we don't get duplicates.
2898                  $result[ $slug ] = $slug;
2899              }
2900          }
2901          return $result;
2902      }
2903  
2904      /**
2905       * Transforms a slug into a CSS Custom Property.
2906       *
2907       * @since 5.9.0
2908       *
2909       * @param string $input String to replace.
2910       * @param string $slug  The slug value to use to generate the custom property.
2911       * @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`.
2912       */
2913  	protected static function replace_slug_in_string( $input, $slug ) {
2914          return strtr( $input, array( '$slug' => $slug ) );
2915      }
2916  
2917      /**
2918       * Given the block settings, extracts the CSS Custom Properties
2919       * for the presets and adds them to the $declarations array
2920       * following the format:
2921       *
2922       *     array(
2923       *       'name'  => 'property_name',
2924       *       'value' => 'property_value,
2925       *     )
2926       *
2927       * @since 5.8.0
2928       * @since 5.9.0 Added the `$origins` parameter.
2929       *
2930       * @param array    $settings Settings to process.
2931       * @param string[] $origins  List of origins to process.
2932       * @return array The modified $declarations.
2933       */
2934  	protected static function compute_preset_vars( $settings, $origins ) {
2935          $declarations = array();
2936          foreach ( static::PRESETS_METADATA as $preset_metadata ) {
2937              if ( empty( $preset_metadata['css_vars'] ) ) {
2938                  continue;
2939              }
2940              $values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins );
2941              foreach ( $values_by_slug as $slug => $value ) {
2942                  $declarations[] = array(
2943                      'name'  => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
2944                      'value' => $value,
2945                  );
2946              }
2947          }
2948  
2949          return $declarations;
2950      }
2951  
2952      /**
2953       * Given an array of settings, extracts the CSS Custom Properties
2954       * for the custom values and adds them to the $declarations
2955       * array following the format:
2956       *
2957       *     array(
2958       *       'name'  => 'property_name',
2959       *       'value' => 'property_value,
2960       *     )
2961       *
2962       * @since 5.8.0
2963       *
2964       * @param array $settings Settings to process.
2965       * @return array The modified $declarations.
2966       */
2967  	protected static function compute_theme_vars( $settings ) {
2968          $declarations  = array();
2969          $custom_values = $settings['custom'] ?? array();
2970          $css_vars      = static::flatten_tree( $custom_values );
2971          foreach ( $css_vars as $key => $value ) {
2972              $declarations[] = array(
2973                  'name'  => '--wp--custom--' . $key,
2974                  'value' => $value,
2975              );
2976          }
2977  
2978          return $declarations;
2979      }
2980  
2981      /**
2982       * Given a tree, it creates a flattened one
2983       * by merging the keys and binding the leaf values
2984       * to the new keys.
2985       *
2986       * It also transforms camelCase names into kebab-case
2987       * and substitutes '/' by '-'.
2988       *
2989       * This is thought to be useful to generate
2990       * CSS Custom Properties from a tree,
2991       * although there's nothing in the implementation
2992       * of this function that requires that format.
2993       *
2994       * For example, assuming the given prefix is '--wp'
2995       * and the token is '--', for this input tree:
2996       *
2997       *     {
2998       *       'some/property': 'value',
2999       *       'nestedProperty': {
3000       *         'sub-property': 'value'
3001       *       }
3002       *     }
3003       *
3004       * it'll return this output:
3005       *
3006       *     {
3007       *       '--wp--some-property': 'value',
3008       *       '--wp--nested-property--sub-property': 'value'
3009       *     }
3010       *
3011       * @since 5.8.0
3012       *
3013       * @param array  $tree   Input tree to process.
3014       * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string.
3015       * @param string $token  Optional. Token to use between levels. Default '--'.
3016       * @return array The flattened tree.
3017       */
3018  	protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
3019          $result = array();
3020          foreach ( $tree as $property => $value ) {
3021              $new_key = $prefix . str_replace(
3022                  '/',
3023                  '-',
3024                  strtolower( _wp_to_kebab_case( $property ) )
3025              );
3026  
3027              if ( is_array( $value ) ) {
3028                  $new_prefix        = $new_key . $token;
3029                  $flattened_subtree = static::flatten_tree( $value, $new_prefix, $token );
3030                  foreach ( $flattened_subtree as $subtree_key => $subtree_value ) {
3031                      $result[ $subtree_key ] = $subtree_value;
3032                  }
3033              } else {
3034                  $result[ $new_key ] = $value;
3035              }
3036          }
3037          return $result;
3038      }
3039  
3040      /**
3041       * Given a styles array, it extracts the style properties
3042       * and adds them to the $declarations array following the format:
3043       *
3044       *     array(
3045       *       'name'  => 'property_name',
3046       *       'value' => 'property_value',
3047       *     )
3048       *
3049       * @since 5.8.0
3050       * @since 5.9.0 Added the `$settings` and `$properties` parameters.
3051       * @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters.
3052       * @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set.
3053       * @since 6.6.0 Pass current theme JSON settings to wp_get_typography_font_size_value(), and process background properties.
3054       * @since 6.7.0 `ref` resolution of background properties, and assigning custom default values.
3055       *
3056       * @param array  $styles           Styles to process.
3057       * @param array  $settings         Theme settings.
3058       * @param array  $properties       Properties metadata.
3059       * @param array  $theme_json       Theme JSON array.
3060       * @param string $selector         The style block selector.
3061       * @param bool   $use_root_padding Whether to add custom properties at root level.
3062       * @return array Returns the modified $declarations.
3063       */
3064  	protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
3065          if ( empty( $styles ) ) {
3066              return array();
3067          }
3068  
3069          if ( null === $properties ) {
3070              $properties = static::PROPERTIES_METADATA;
3071          }
3072          $declarations             = array();
3073          $root_variable_duplicates = array();
3074          $root_style_length        = strlen( '--wp--style--root--' );
3075  
3076          foreach ( $properties as $css_property => $value_path ) {
3077              if ( ! is_array( $value_path ) ) {
3078                  continue;
3079              }
3080  
3081              $is_root_style = str_starts_with( $css_property, '--wp--style--root--' );
3082              if ( $is_root_style && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) {
3083                  continue;
3084              }
3085  
3086              $value = static::get_property_value( $styles, $value_path, $theme_json );
3087  
3088              /*
3089               * Root-level padding styles don't currently support strings with CSS shorthand values.
3090               * This may change: https://github.com/WordPress/gutenberg/issues/40132.
3091               */
3092              if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) {
3093                  continue;
3094              }
3095  
3096              if ( $is_root_style && $use_root_padding ) {
3097                  $root_variable_duplicates[] = substr( $css_property, $root_style_length );
3098              }
3099  
3100              /*
3101               * Processes background image styles.
3102               * If the value is a URL, it will be converted to a CSS `url()` value.
3103               * For uploaded image (images with a database ID), apply size and position defaults,
3104               * equal to those applied in block supports in lib/background.php.
3105               */
3106              if ( 'background-image' === $css_property ) {
3107                  $background_image_input = array();
3108                  if ( ! empty( $value ) ) {
3109                      $background_image_input['backgroundImage'] = $value;
3110                  }
3111                  $gradient_value = $styles['background']['gradient'] ?? null;
3112                  if ( ! empty( $gradient_value ) ) {
3113                      $background_image_input['gradient'] = $gradient_value;
3114                  }
3115                  if ( ! empty( $background_image_input ) ) {
3116                      $background_styles = wp_style_engine_get_styles(
3117                          array( 'background' => $background_image_input )
3118                      );
3119                      $value             = $background_styles['declarations'][ $css_property ] ?? null;
3120                  }
3121              }
3122              if ( empty( $value ) && static::ROOT_BLOCK_SELECTOR !== $selector && ! empty( $styles['background']['backgroundImage']['id'] ) ) {
3123                  if ( 'background-size' === $css_property ) {
3124                      $value = 'cover';
3125                  }
3126                  // If the background size is set to `contain` and no position is set, set the position to `center`.
3127                  if ( 'background-position' === $css_property ) {
3128                      $background_size = $styles['background']['backgroundSize'] ?? null;
3129                      $value           = 'contain' === $background_size ? '50% 50%' : null;
3130                  }
3131              }
3132  
3133              // Skip if empty and not "0" or value represents array of longhand values.
3134              $has_missing_value = empty( $value ) && ! is_numeric( $value );
3135              if ( $has_missing_value || is_array( $value ) ) {
3136                  continue;
3137              }
3138  
3139              // Calculates fluid typography rules where available.
3140              if ( 'font-size' === $css_property ) {
3141                  /*
3142                   * wp_get_typography_font_size_value() will check
3143                   * if fluid typography has been activated and also
3144                   * whether the incoming value can be converted to a fluid value.
3145                   * Values that already have a clamp() function will not pass the test,
3146                   * and therefore the original $value will be returned.
3147                   * Pass the current theme_json settings to override any global settings.
3148                   */
3149                  $value = wp_get_typography_font_size_value( array( 'size' => $value ), $settings );
3150              }
3151  
3152              if ( 'aspect-ratio' === $css_property ) {
3153                  // For aspect ratio to work, other dimensions rules must be unset.
3154                  // This ensures that a fixed height does not override the aspect ratio.
3155                  $declarations[] = array(
3156                      'name'  => 'min-height',
3157                      'value' => 'unset',
3158                  );
3159              }
3160  
3161              $declarations[] = array(
3162                  'name'  => $css_property,
3163                  'value' => $value,
3164              );
3165          }
3166  
3167          // If a variable value is added to the root, the corresponding property should be removed.
3168          foreach ( $root_variable_duplicates as $duplicate ) {
3169              $discard = array_search( $duplicate, array_column( $declarations, 'name' ), true );
3170              if ( is_numeric( $discard ) ) {
3171                  array_splice( $declarations, $discard, 1 );
3172              }
3173          }
3174  
3175          return $declarations;
3176      }
3177  
3178      /**
3179       * Returns the style property for the given path.
3180       *
3181       * It also converts references to a path to the value
3182       * stored at that location, e.g.
3183       * { "ref": "style.color.background" } => "#fff".
3184       *
3185       * @since 5.8.0
3186       * @since 5.9.0 Added support for values of array type, which are returned as is.
3187       * @since 6.1.0 Added the `$theme_json` parameter.
3188       * @since 6.3.0 It no longer converts the internal format "var:preset|color|secondary"
3189       *              to the standard form "--wp--preset--color--secondary".
3190       *              This is already done by the sanitize method,
3191       *              so every property will be in the standard form.
3192       * @since 6.7.0 Added support for background image refs.
3193       *
3194       * @param array $styles     Styles subtree.
3195       * @param array $path       Which property to process.
3196       * @param array $theme_json Theme JSON array.
3197       * @return string|array Style property value.
3198       */
3199  	protected static function get_property_value( $styles, $path, $theme_json = null ) {
3200          $value = _wp_array_get( $styles, $path, '' );
3201  
3202          if ( '' === $value || null === $value ) {
3203              // No need to process the value further.
3204              return '';
3205          }
3206  
3207          /*
3208           * This converts references to a path to the value at that path
3209           * where the value is an array with a "ref" key, pointing to a path.
3210           * For example: { "ref": "style.color.background" } => "#fff".
3211           * In the case of backgroundImage, if both a ref and a URL are present in the value,
3212           * the URL takes precedence and the ref is ignored.
3213           */
3214          if ( is_array( $value ) && isset( $value['ref'] ) ) {
3215              $value_path = explode( '.', $value['ref'] );
3216              $ref_value  = _wp_array_get( $theme_json, $value_path );
3217              // Background Image refs can refer to a string or an array containing a URL string.
3218              $ref_value_url = $ref_value['url'] ?? null;
3219              // Only use the ref value if we find anything.
3220              if ( ! empty( $ref_value ) && ( is_string( $ref_value ) || is_string( $ref_value_url ) ) ) {
3221                  $value = $ref_value;
3222              }
3223  
3224              if ( is_array( $ref_value ) && isset( $ref_value['ref'] ) ) {
3225                  $path_string      = json_encode( $path );
3226                  $ref_value_string = json_encode( $ref_value );
3227                  _doing_it_wrong(
3228                      'get_property_value',
3229                      sprintf(
3230                          /* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */
3231                          __( '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.' ),
3232                          'theme.json',
3233                          $ref_value_string,
3234                          $path_string,
3235                          $ref_value['ref']
3236                      ),
3237                      '6.1.0'
3238                  );
3239              }
3240          }
3241  
3242          return $value;
3243      }
3244  
3245      /**
3246       * Builds metadata for the setting nodes, which returns in the form of:
3247       *
3248       *     [
3249       *       [
3250       *         'path'     => ['path', 'to', 'some', 'node' ],
3251       *         'selector' => 'CSS selector for some node'
3252       *       ],
3253       *       [
3254       *         'path'     => [ 'path', 'to', 'other', 'node' ],
3255       *         'selector' => 'CSS selector for other node'
3256       *       ],
3257       *     ]
3258       *
3259       * @since 5.8.0
3260       *
3261       * @param array $theme_json The tree to extract setting nodes from.
3262       * @param array $selectors  List of selectors per block.
3263       * @return array An array of setting nodes metadata.
3264       */
3265  	protected static function get_setting_nodes( $theme_json, $selectors = array() ) {
3266          $nodes = array();
3267          if ( ! isset( $theme_json['settings'] ) ) {
3268              return $nodes;
3269          }
3270  
3271          // Top-level.
3272          $nodes[] = array(
3273              'path'     => array( 'settings' ),
3274              'selector' => static::ROOT_CSS_PROPERTIES_SELECTOR,
3275          );
3276  
3277          // Calculate paths for blocks.
3278          if ( ! isset( $theme_json['settings']['blocks'] ) ) {
3279              return $nodes;
3280          }
3281  
3282          foreach ( $theme_json['settings']['blocks'] as $name => $node ) {
3283              $selector = null;
3284              if ( isset( $selectors[ $name ]['selector'] ) ) {
3285                  $selector = $selectors[ $name ]['selector'];
3286              }
3287  
3288              $nodes[] = array(
3289                  'path'      => array( 'settings', 'blocks', $name ),
3290                  'selector'  => $selector,
3291                  'selectors' => $selectors[ $name ]['selectors'] ?? array(),
3292              );
3293          }
3294  
3295          return $nodes;
3296      }
3297  
3298      /**
3299       * Builds metadata for the style nodes, which returns in the form of:
3300       *
3301       *     [
3302       *       [
3303       *         'path'     => [ 'path', 'to', 'some', 'node' ],
3304       *         'selector' => 'CSS selector for some node',
3305       *         'duotone'  => 'CSS selector for duotone for some node'
3306       *       ],
3307       *       [
3308       *         'path'     => ['path', 'to', 'other', 'node' ],
3309       *         'selector' => 'CSS selector for other node',
3310       *         'duotone'  => null
3311       *       ],
3312       *     ]
3313       *
3314       * @since 5.8.0
3315       * @since 6.6.0 Added options array for modifying generated nodes.
3316       *
3317       * @param array $theme_json The tree to extract style nodes from.
3318       * @param array $selectors  List of selectors per block.
3319       * @param array $options {
3320       *     Optional. An array of options for now used for internal purposes only (may change without notice).
3321       *
3322       *     @type bool $include_block_style_variations Includes style nodes for block style variations. Default false.
3323       * }
3324       * @return array An array of style nodes metadata.
3325       */
3326  	protected static function get_style_nodes( $theme_json, $selectors = array(), $options = array() ) {
3327          $nodes = array();
3328          if ( ! isset( $theme_json['styles'] ) ) {
3329              return $nodes;
3330          }
3331  
3332          // Top-level.
3333          $nodes[] = array(
3334              'path'     => array( 'styles' ),
3335              'selector' => static::ROOT_BLOCK_SELECTOR,
3336          );
3337  
3338          if ( isset( $theme_json['styles']['elements'] ) ) {
3339              foreach ( self::ELEMENTS as $element => $selector ) {
3340                  if ( ! isset( $theme_json['styles']['elements'][ $element ] ) ) {
3341                      continue;
3342                  }
3343                  $nodes[] = array(
3344                      'path'     => array( 'styles', 'elements', $element ),
3345                      'selector' => static::ELEMENTS[ $element ],
3346                  );
3347  
3348                  // Handle any pseudo selectors for the element.
3349                  if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
3350                      foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
3351  
3352                          if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) {
3353                              $nodes[] = array(
3354                                  'path'     => array( 'styles', 'elements', $element ),
3355                                  'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ),
3356                              );
3357                          }
3358                      }
3359                  }
3360              }
3361          }
3362  
3363          // Blocks.
3364          if ( ! isset( $theme_json['styles']['blocks'] ) ) {
3365              return $nodes;
3366          }
3367  
3368          $block_nodes = self::get_block_nodes( $theme_json, $selectors, $options );
3369          foreach ( $block_nodes as $block_node ) {
3370              $nodes[] = $block_node;
3371          }
3372  
3373          /**
3374           * Filters the list of style nodes with metadata.
3375           *
3376           * This allows for things like loading block CSS independently.
3377           *
3378           * @since 6.1.0
3379           *
3380           * @param array $nodes Style nodes with metadata.
3381           */
3382          return apply_filters( 'wp_theme_json_get_style_nodes', $nodes );
3383      }
3384  
3385      /**
3386       * A public helper to get the block nodes from a theme.json file.
3387       *
3388       * @since 6.1.0
3389       *
3390       * @return array The block nodes in theme.json.
3391       */
3392  	public function get_styles_block_nodes() {
3393          return self::get_block_nodes( $this->theme_json );
3394      }
3395  
3396      /**
3397       * Returns a filtered declarations array if there is a separator block with only a background
3398       * style defined in theme.json by adding a color attribute to reflect the changes in the front.
3399       *
3400       * @since 6.1.1
3401       *
3402       * @param array $declarations List of declarations.
3403       * @return array $declarations List of declarations filtered.
3404       */
3405  	private static function update_separator_declarations( $declarations ) {
3406          $background_color     = '';
3407          $border_color_matches = false;
3408          $text_color_matches   = false;
3409  
3410          foreach ( $declarations as $declaration ) {
3411              if ( 'background-color' === $declaration['name'] && ! $background_color && isset( $declaration['value'] ) ) {
3412                  $background_color = $declaration['value'];
3413              } elseif ( 'border-color' === $declaration['name'] ) {
3414                  $border_color_matches = true;
3415              } elseif ( 'color' === $declaration['name'] ) {
3416                  $text_color_matches = true;
3417              }
3418  
3419              if ( $background_color && $border_color_matches && $text_color_matches ) {
3420                  break;
3421              }
3422          }
3423  
3424          if ( $background_color && ! $border_color_matches && ! $text_color_matches ) {
3425              $declarations[] = array(
3426                  'name'  => 'color',
3427                  'value' => $background_color,
3428              );
3429          }
3430  
3431          return $declarations;
3432      }
3433  
3434      /**
3435       * Updates the text indent selector for paragraph blocks based on the textIndent setting.
3436       *
3437       * The textIndent setting can be 'subsequent' (default), 'all', or false.
3438       * When set to 'all', the selector should be '.wp-block-paragraph' instead of
3439       * '.wp-block-paragraph + .wp-block-paragraph' to apply indent to all paragraphs.
3440       *
3441       * @since 7.0.0
3442       *
3443       * @param array  $feature_declarations The feature declarations keyed by selector.
3444       * @param array  $settings             The theme.json settings.
3445       * @param string $block_name           The block name being processed.
3446       * @return array The updated feature declarations.
3447       */
3448  	private static function update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name ) {
3449          if ( 'core/paragraph' !== $block_name ) {
3450              return $feature_declarations;
3451          }
3452  
3453          // Check block-level settings first, then fall back to global settings.
3454          $block_settings      = $settings['blocks']['core/paragraph'] ?? null;
3455          $text_indent_setting = $block_settings['typography']['textIndent']
3456              ?? $settings['typography']['textIndent']
3457              ?? 'subsequent';
3458  
3459          if ( 'all' !== $text_indent_setting ) {
3460              return $feature_declarations;
3461          }
3462  
3463          // Look for the text indent selector and replace it.
3464          $old_selector = '.wp-block-paragraph + .wp-block-paragraph';
3465          $new_selector = '.wp-block-paragraph';
3466  
3467          if ( isset( $feature_declarations[ $old_selector ] ) ) {
3468              $declarations = $feature_declarations[ $old_selector ];
3469              unset( $feature_declarations[ $old_selector ] );
3470              $feature_declarations[ $new_selector ] = $declarations;
3471          }
3472  
3473          return $feature_declarations;
3474      }
3475  
3476      /**
3477       * Updates button width declarations to use a calc() formula for percentage values.
3478       *
3479       * When a percentage width is set on the Button block via Global Styles, the
3480       * resulting CSS needs to account for block gap spacing so that buttons tile
3481       * correctly on a row (e.g. 4 buttons at 25% width all fit on one row).
3482       *
3483       * This mirrors the dynamic calc() formula applied at the block instance level
3484       * in the button block's stylesheet (style.scss).
3485       *
3486       * @since 7.1.0
3487       *
3488       * @param array $feature_declarations The feature declarations keyed by selector.
3489       * @param array $settings             The theme.json settings.
3490       * @return array The updated feature declarations.
3491       */
3492  	private static function update_button_width_declarations( $feature_declarations, $settings ) {
3493          if ( ! isset( $feature_declarations['.wp-block-button'] ) ) {
3494              return $feature_declarations;
3495          }
3496  
3497          foreach ( $feature_declarations['.wp-block-button'] as &$declaration ) {
3498              if ( 'width' !== $declaration['name'] || ! isset( $declaration['value'] ) ) {
3499                  continue;
3500              }
3501  
3502              $value      = $declaration['value'];
3503              $percentage = null;
3504  
3505              // Case 1: Direct percentage value e.g. "25%".
3506              if ( is_string( $value ) && str_ends_with( $value, '%' ) ) {
3507                  $percentage = (float) $value;
3508              }
3509  
3510              // Case 2: Preset CSS var e.g. "var(--wp--preset--dimension--50)".
3511              if ( null === $percentage && is_string( $value ) && str_starts_with( $value, 'var(--wp--preset--dimension--' ) ) {
3512                  // Extract the slug from the var name.
3513                  $slug = substr( $value, strlen( 'var(--wp--preset--dimension--' ), -1 );
3514  
3515                  /*
3516                   * Look up the preset size across all origins.
3517                   * Check block-level settings first (core/button), then top-level settings.
3518                   */
3519                  $dimension_sizes = ( $settings['blocks']['core/button']['dimensions']['dimensionSizes'] ?? array() )
3520                      + ( $settings['dimensions']['dimensionSizes'] ?? array() );
3521                  foreach ( $dimension_sizes as $origin_sizes ) {
3522                      if ( ! is_array( $origin_sizes ) ) {
3523                          continue;
3524                      }
3525                      foreach ( $origin_sizes as $preset ) {
3526                          if ( isset( $preset['slug'] ) && $slug === $preset['slug'] && isset( $preset['size'] ) ) {
3527                              $size = $preset['size'];
3528                              if ( is_string( $size ) && str_ends_with( $size, '%' ) ) {
3529                                  $percentage = (float) $size;
3530                              }
3531                              break 2;
3532                          }
3533                      }
3534                  }
3535              }
3536  
3537              if ( null === $percentage ) {
3538                  continue;
3539              }
3540  
3541              /*
3542               * Apply the same calc() formula as the block instance level (style.scss).
3543               * The numeric percentage value is used as a unitless number:
3544               * - Multiplied by 1% to get the percentage width.
3545               * - Divided by 100 to calculate the gap adjustment proportion.
3546               */
3547              $declaration['value'] = sprintf(
3548                  'calc(%s * 1%% - (var(--wp--style--block-gap, 0.5em) * (1 - %s / 100)))',
3549                  $percentage,
3550                  $percentage
3551              );
3552          }
3553          unset( $declaration );
3554  
3555          return $feature_declarations;
3556      }
3557  
3558      /**
3559       * An internal method to get the block nodes from a theme.json file.
3560       *
3561       * @since 6.1.0
3562       * @since 6.3.0 Refactored and stabilized selectors API.
3563       * @since 6.6.0 Added optional selectors and options for generating block nodes.
3564       * @since 6.7.0 Added $include_node_paths_only option.
3565       * @since 7.1.0 Added responsive block nodes for breakpoint-based styles.
3566       *
3567       * @param array $theme_json The theme.json converted to an array.
3568       * @param array $selectors  Optional list of selectors per block.
3569       * @param array $options    {
3570       *     Optional. An array of options for now used for internal purposes only (may change without notice).
3571       *
3572       *     @type bool $include_block_style_variations Include nodes for block style variations. Default false.
3573       *     @type bool $include_node_paths_only        Return only block nodes node paths. Default false.
3574       * }
3575       * @return array The block nodes in theme.json.
3576       */
3577  	private static function get_block_nodes( $theme_json, $selectors = array(), $options = array() ) {
3578          $nodes = array();
3579  
3580          if ( ! isset( $theme_json['styles']['blocks'] ) ) {
3581              return $nodes;
3582          }
3583  
3584          $include_variations       = $options['include_block_style_variations'] ?? false;
3585          $include_node_paths_only  = $options['include_node_paths_only'] ?? false;
3586          $responsive_media_queries = static::get_viewport_media_queries( $theme_json['settings']['viewport'] ?? null );
3587  
3588          // If only node paths are to be returned, skip selector assignment.
3589          if ( ! $include_node_paths_only ) {
3590              $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
3591          }
3592  
3593          foreach ( $theme_json['styles']['blocks'] as $name => $node ) {
3594              $node_path = array( 'styles', 'blocks', $name );
3595              if ( $include_node_paths_only ) {
3596                  $variation_paths = array();
3597                  if ( $include_variations && isset( $node['variations'] ) ) {
3598                      foreach ( $node['variations'] as $variation => $variation_node ) {
3599                          $variation_paths[] = array(
3600                              'path' => array( 'styles', 'blocks', $name, 'variations', $variation ),
3601                          );
3602                      }
3603                  }
3604                  $node = array(
3605                      'path' => $node_path,
3606                  );
3607                  if ( ! empty( $variation_paths ) ) {
3608                      $node['variations'] = $variation_paths;
3609                  }
3610                  $nodes[] = $node;
3611              } else {
3612                  $selector = null;
3613                  if ( isset( $selectors[ $name ]['selector'] ) ) {
3614                      $selector = $selectors[ $name ]['selector'];
3615                  }
3616  
3617                  $duotone_selector = null;
3618                  if ( isset( $selectors[ $name ]['duotone'] ) ) {
3619                      $duotone_selector = $selectors[ $name ]['duotone'];
3620                  }
3621  
3622                  $feature_selectors = null;
3623                  if ( isset( $selectors[ $name ]['selectors'] ) ) {
3624                      $feature_selectors = $selectors[ $name ]['selectors'];
3625                  }
3626  
3627                  $variation_selectors = array();
3628  
3629                  if ( $include_variations && isset( $node['variations'] ) ) {
3630                      foreach ( $node['variations'] as $variation => $node ) {
3631                          $variation_selectors[] = array(
3632                              'name'     => $variation,
3633                              'path'     => array( 'styles', 'blocks', $name, 'variations', $variation ),
3634                              'selector' => $selectors[ $name ]['styleVariations'][ $variation ],
3635                          );
3636                      }
3637                  }
3638  
3639                  $nodes[] = array(
3640                      'name'       => $name,
3641                      'path'       => $node_path,
3642                      'selector'   => $selector,
3643                      'selectors'  => $feature_selectors,
3644                      'elements'   => $selectors[ $name ]['elements'] ?? array(),
3645                      'duotone'    => $duotone_selector,
3646                      'variations' => $variation_selectors,
3647                      'css'        => $selector,
3648                  );
3649  
3650                  // Responsive block nodes: emit one node per breakpoint that has styles.
3651                  // These are rendered immediately after the base block node so that
3652                  // the cascade order is: .block{} → @media{.block{}}
3653                  foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3654                      if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ] ) ) {
3655                          $nodes[] = array(
3656                              'name'        => $name,
3657                              'path'        => array( 'styles', 'blocks', $name, $breakpoint ),
3658                              'media_query' => $responsive_media_queries[ $breakpoint ],
3659                              'selector'    => $selector,
3660                              'selectors'   => $feature_selectors,
3661                              'elements'    => $selectors[ $name ]['elements'] ?? array(),
3662                              'variations'  => $variation_selectors,
3663                              'css'         => $selector,
3664                          );
3665                      }
3666                  }
3667  
3668                  // Handle any pseudo selectors for the block.
3669                  if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
3670                      foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) {
3671                          $has_pseudo            = isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] );
3672                          $has_responsive_pseudo = false;
3673                          foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3674                              if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
3675                                  $has_responsive_pseudo = true;
3676                                  break;
3677                              }
3678                          }
3679  
3680                          if ( ! $has_pseudo && ! $has_responsive_pseudo ) {
3681                              continue;
3682                          }
3683  
3684                          /*
3685                           * Append the pseudo-selector to each feature selector so that
3686                           * get_feature_declarations_for_node generates CSS scoped to the
3687                           * pseudo-state (e.g. '.wp-block-button:hover') rather than the
3688                           * default state (e.g. '.wp-block-button').
3689                           */
3690                          $pseudo_feature_selectors = array();
3691                          foreach ( $feature_selectors ?? array() as $feature => $feature_selector ) {
3692                              if ( is_array( $feature_selector ) ) {
3693                                  $pseudo_feature_selectors[ $feature ] = array();
3694                                  foreach ( $feature_selector as $subfeature => $subfeature_selector ) {
3695                                      $pseudo_feature_selectors[ $feature ][ $subfeature ] = static::append_to_selector( $subfeature_selector, $pseudo_selector );
3696                                  }
3697                              } else {
3698                                  $pseudo_feature_selectors[ $feature ] = static::append_to_selector( $feature_selector, $pseudo_selector );
3699                              }
3700                          }
3701  
3702                          if ( $has_pseudo ) {
3703                              $nodes[] = array(
3704                                  'name'       => $name,
3705                                  'path'       => array( 'styles', 'blocks', $name, $pseudo_selector ),
3706                                  'selector'   => static::append_to_selector( $selector, $pseudo_selector ),
3707                                  'selectors'  => $pseudo_feature_selectors,
3708                                  'elements'   => $selectors[ $name ]['elements'] ?? array(),
3709                                  'duotone'    => $duotone_selector,
3710                                  'variations' => $variation_selectors,
3711                                  'css'        => static::append_to_selector( $selector, $pseudo_selector ),
3712                              );
3713                          }
3714  
3715                          // Responsive pseudo nodes: emit one node per breakpoint that has
3716                          // this pseudo state, immediately after the default pseudo node.
3717                          // Cascade order: .block:hover{} → @media{.block:hover{}}
3718                          foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3719                              if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
3720                                  $nodes[] = array(
3721                                      'name'        => $name,
3722                                      'path'        => array( 'styles', 'blocks', $name, $breakpoint, $pseudo_selector ),
3723                                      'media_query' => $responsive_media_queries[ $breakpoint ],
3724                                      'selector'    => static::append_to_selector( $selector, $pseudo_selector ),
3725                                      'selectors'   => $pseudo_feature_selectors,
3726                                      'elements'    => $selectors[ $name ]['elements'] ?? array(),
3727                                      'variations'  => $variation_selectors,
3728                                      'css'         => static::append_to_selector( $selector, $pseudo_selector ),
3729                                  );
3730                              }
3731                          }
3732                      }
3733                  }
3734  
3735                  // Handle custom states (e.g. '-current' for navigation).
3736                  if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $name ] ) ) {
3737                      foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $name ] as $custom_state ) {
3738                          if (
3739                              isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ] ) &&
3740                              isset( $selectors[ $name ]['states'][ $custom_state ] )
3741                          ) {
3742                              $custom_css_selector = $selectors[ $name ]['states'][ $custom_state ];
3743                              $nodes[]             = array(
3744                                  'name'       => $name,
3745                                  'path'       => array( 'styles', 'blocks', $name, $custom_state ),
3746                                  'selector'   => $custom_css_selector,
3747                                  'selectors'  => $feature_selectors,
3748                                  'elements'   => $selectors[ $name ]['elements'] ?? array(),
3749                                  'duotone'    => $duotone_selector,
3750                                  'variations' => $variation_selectors,
3751                                  'css'        => $custom_css_selector,
3752                              );
3753  
3754                              // Sub-pseudo-selectors within the custom state.
3755                              if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
3756                                  foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo ) {
3757                                      if ( isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ][ $pseudo ] ) ) {
3758                                          $compound_css_selector = static::append_to_selector( $custom_css_selector, $pseudo );
3759                                          $nodes[]               = array(
3760                                              'name'       => $name,
3761                                              'path'       => array( 'styles', 'blocks', $name, $custom_state, $pseudo ),
3762                                              'selector'   => $compound_css_selector,
3763                                              'selectors'  => $feature_selectors,
3764                                              'elements'   => $selectors[ $name ]['elements'] ?? array(),
3765                                              'duotone'    => $duotone_selector,
3766                                              'variations' => $variation_selectors,
3767                                              'css'        => $compound_css_selector,
3768                                          );
3769                                      }
3770                                  }
3771                              }
3772                          }
3773                      }
3774                  }
3775              }
3776              /*
3777               * Elements can be styled outside any breakpoint, inside one, or both,
3778               * so collect the names from all of those places before looping. An
3779               * element styled only inside a breakpoint still needs a node.
3780               */
3781              $block_node    = $theme_json['styles']['blocks'][ $name ] ?? array();
3782              $element_names = array_keys( $block_node['elements'] ?? array() );
3783              foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3784                  $element_names = array_merge(
3785                      $element_names,
3786                      array_keys( $block_node[ $breakpoint ]['elements'] ?? array() )
3787                  );
3788              }
3789              $element_names = array_unique( $element_names );
3790  
3791              if ( ! empty( $element_names ) ) {
3792                  foreach ( $element_names as $element ) {
3793                      $element_path = array( 'styles', 'blocks', $name, 'elements', $element );
3794                      if ( $include_node_paths_only ) {
3795                          if ( isset( $block_node['elements'][ $element ] ) ) {
3796                              $nodes[] = array(
3797                                  'path' => $element_path,
3798                              );
3799                          }
3800                          continue;
3801                      }
3802  
3803                      if ( ! isset( $selectors[ $name ]['elements'][ $element ] ) ) {
3804                          continue;
3805                      }
3806  
3807                      $element_selector = $selectors[ $name ]['elements'][ $element ];
3808  
3809                      if ( isset( $block_node['elements'][ $element ] ) ) {
3810                          $nodes[] = array(
3811                              'path'     => $element_path,
3812                              'selector' => $element_selector,
3813                          );
3814                      }
3815  
3816                      // Responsive element nodes: one node per breakpoint that has
3817                      // styles for this element. Cascade: a{} → @media{a{}}
3818                      foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3819                          if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ] ) ) {
3820                              $nodes[] = array(
3821                                  'path'        => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3822                                  'selector'    => $element_selector,
3823                                  'media_query' => $responsive_media_queries[ $breakpoint ],
3824                              );
3825                          }
3826                      }
3827  
3828                      // Handle any pseudo selectors for the element.
3829                      if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
3830                          foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
3831                              // Emit the default pseudo node only when the default state styles
3832                              // the pseudo. Otherwise get_styles_for_block() falls back to the
3833                              // element's base styles, outputting a rule the theme never defined.
3834                              if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3835                                  $nodes[] = array(
3836                                      'path'     => array( 'styles', 'blocks', $name, 'elements', $element ),
3837                                      'selector' => static::append_to_selector( $element_selector, $pseudo_selector ),
3838                                  );
3839                              }
3840  
3841                              // Responsive element pseudo nodes: one node per breakpoint
3842                              // that has this pseudo state for this element.
3843                              // Cascade: a:hover{} → @media{a:hover{}}
3844                              foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3845                                  if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ][ $pseudo_selector ] ) ) {
3846                                      $nodes[] = array(
3847                                          'path'        => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
3848                                          'selector'    => static::append_to_selector( $element_selector, $pseudo_selector ),
3849                                          'media_query' => $responsive_media_queries[ $breakpoint ],
3850                                      );
3851                                  }
3852                              }
3853                          }
3854                      }
3855                  }
3856              }
3857          }
3858  
3859          return $nodes;
3860      }
3861  
3862      /**
3863       * Gets the CSS rules for a particular block from theme.json.
3864       *
3865       * @since 6.1.0
3866       * @since 6.6.0 Setting a min-height of HTML when root styles have a background gradient or image.
3867       *              Updated general global styles specificity to 0-1-0.
3868       *              Fixed custom CSS output in block style variations.
3869       *
3870       * @param array $block_metadata Metadata about the block to get styles for.
3871       * @return string Styles for the block.
3872       */
3873  	public function get_styles_for_block( $block_metadata ) {
3874          $node                     = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
3875          $use_root_padding         = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
3876          $selector                 = $block_metadata['selector'];
3877          $settings                 = $this->theme_json['settings'] ?? array();
3878          $feature_declarations     = static::get_feature_declarations_for_node( $block_metadata, $node );
3879          $is_root_selector         = static::ROOT_BLOCK_SELECTOR === $selector;
3880          $media_query              = $block_metadata['media_query'] ?? null;
3881          $responsive_media_queries = static::get_viewport_media_queries( $settings['viewport'] ?? null );
3882  
3883          // Update text indent selector for paragraph blocks based on the textIndent setting.
3884          $block_name           = $block_metadata['name'] ?? null;
3885          $feature_declarations = self::update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name );
3886          $block_elements       = $block_metadata['elements'] ?? array();
3887  
3888          // Update button width declarations for percentage values to use calc() with block gap.
3889          $feature_declarations = self::update_button_width_declarations( $feature_declarations, $settings );
3890  
3891          // If there are style variations, generate the declarations for them, including any feature selectors the block may have.
3892          $style_variation_declarations          = array();
3893          $style_variation_custom_css            = array();
3894          $style_variation_responsive_css        = array();
3895          $style_variation_responsive_pseudo_css = array();
3896          $style_variation_layout_metadata       = array();
3897          if ( ! $media_query && ! empty( $block_metadata['variations'] ) ) {
3898              foreach ( $block_metadata['variations'] as $style_variation ) {
3899                  $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
3900  
3901                  // Generate any feature/subfeature style declarations for the current style variation.
3902                  $variation_declarations = static::get_feature_declarations_for_node( $block_metadata, $style_variation_node );
3903  
3904                  // Update text indent selector for paragraph blocks based on the textIndent setting.
3905                  $variation_declarations = self::update_paragraph_text_indent_selector( $variation_declarations, $settings, $block_name );
3906  
3907                  // Update button width declarations for percentage values to use calc() with block gap.
3908                  $variation_declarations = self::update_button_width_declarations( $variation_declarations, $settings );
3909  
3910                  // Combine selectors with style variation's selector and add to overall style variation declarations.
3911                  foreach ( $variation_declarations as $current_selector => $new_declarations ) {
3912                      $combined_selectors = static::get_block_style_variation_feature_selector( $style_variation, $current_selector );
3913  
3914                      // Add the new declarations to the overall results under the modified selector.
3915                      $style_variation_declarations[ $combined_selectors ] = $new_declarations;
3916                  }
3917  
3918                  // Compute declarations for remaining styles not covered by feature level selectors.
3919                  $style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json );
3920  
3921                  // Process pseudo-selectors for this variation (e.g., :hover, :focus)
3922                  if ( isset( $block_metadata['name'] ) ) {
3923                      $block_name = $block_metadata['name'];
3924                  } elseif ( in_array( 'blocks', $block_metadata['path'], true ) && count( $block_metadata['path'] ) >= 3 ) {
3925                      $block_name = self::get_block_name_from_metadata_path( $block_metadata );
3926                  } else {
3927                      $block_name = null;
3928                  }
3929                  $variation_pseudo_declarations = $this->process_pseudo_selectors( $style_variation_node, $style_variation['selector'], $settings, $block_name, $block_metadata, $style_variation );
3930                  $style_variation_declarations  = array_merge( $style_variation_declarations, $variation_pseudo_declarations );
3931  
3932                  // Store custom CSS for the style variation.
3933                  if ( isset( $style_variation_node['css'] ) ) {
3934                      $style_variation_custom_css[ $style_variation['selector'] ] = $this->process_blocks_custom_css( $style_variation_node['css'], $style_variation['selector'] );
3935                  }
3936  
3937                  // Store variation metadata and node for layout styles generation.
3938                  // Only store if the variation has blockGap defined.
3939                  if ( isset( $style_variation_node['spacing']['blockGap'] ) ) {
3940                      // Append block selector to the variation selector for proper targeting.
3941                      $variation_metadata_with_selector             = $style_variation;
3942                      $variation_metadata_with_selector['selector'] = $style_variation['selector'] . $block_metadata['css'];
3943  
3944                      /*
3945                       * `get_layout_styles()` reads `name` as a block name, to check that the block
3946                       * supports layout at all. A variation node's `name` is the variation slug,
3947                       * which is never a registered block, so the check fails and every variation
3948                       * gap rule is discarded. Pass the block the variation belongs to, so the
3949                       * support check answers the question it is actually asking.
3950                       */
3951                      $variation_metadata_with_selector['name'] = $block_name;
3952  
3953                      $style_variation_layout_metadata[ $style_variation['selector'] ] = array(
3954                          'metadata' => $variation_metadata_with_selector,
3955                          'node'     => $style_variation_node,
3956                      );
3957                  }
3958  
3959                  // Store responsive breakpoint CSS for the style variation.
3960                  // This includes both base properties and feature-level selectors.
3961                  $variation_responsive_css        = '';
3962                  $variation_responsive_pseudo_css = '';
3963  
3964                  foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
3965                      if ( ! isset( $style_variation_node[ $breakpoint ] ) ) {
3966                          continue;
3967                      }
3968  
3969                      $breakpoint_node  = $style_variation_node[ $breakpoint ];
3970                      $breakpoint_media = $responsive_media_queries[ $breakpoint ];
3971                      // Process feature-level declarations for this breakpoint.
3972                      $breakpoint_feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $breakpoint_node );
3973                      $breakpoint_feature_declarations = self::update_paragraph_text_indent_selector( $breakpoint_feature_declarations, $settings, $block_name );
3974                      $breakpoint_feature_declarations = self::update_button_width_declarations( $breakpoint_feature_declarations, $settings );
3975                      foreach ( $breakpoint_feature_declarations as $feature_selector => $feature_decl ) {
3976                          $combined_selectors = static::get_block_style_variation_feature_selector( $style_variation, $feature_selector );
3977  
3978                          $feature_ruleset           = static::to_ruleset( ':root :where(' . $combined_selectors . ')', $feature_decl );
3979                          $variation_responsive_css .= $breakpoint_media . '{' . $feature_ruleset . '}';
3980                      }
3981  
3982                      // Process base properties for this breakpoint.
3983                      $breakpoint_declarations = static::compute_style_properties( $breakpoint_node, $settings, null, $this->theme_json );
3984                      if ( ! empty( $breakpoint_declarations ) ) {
3985                          $base_ruleset              = static::to_ruleset( ':root :where(' . $style_variation['selector'] . ')', $breakpoint_declarations );
3986                          $variation_responsive_css .= $breakpoint_media . '{' . $base_ruleset . '}';
3987                      }
3988  
3989                      $breakpoint_pseudo_declarations = $this->process_pseudo_selectors( $breakpoint_node, $style_variation['selector'], $settings, $block_name, $block_metadata, $style_variation );
3990                      foreach ( $breakpoint_pseudo_declarations as $pseudo_selector => $pseudo_declarations ) {
3991                          if ( empty( $pseudo_declarations ) ) {
3992                              continue;
3993                          }
3994                          $pseudo_ruleset                   = static::to_ruleset( ':root :where(' . $pseudo_selector . ')', $pseudo_declarations );
3995                          $variation_responsive_pseudo_css .= $breakpoint_media . '{' . $pseudo_ruleset . '}';
3996                      }
3997  
3998                      // Process custom CSS for this breakpoint.
3999                      if ( isset( $breakpoint_node['css'] ) ) {
4000                          $breakpoint_custom_css     = static::process_blocks_custom_css( $breakpoint_node['css'], $style_variation['selector'] );
4001                          $variation_responsive_css .= $breakpoint_media . '{' . $breakpoint_custom_css . '}';
4002                      }
4003  
4004                      // Process blockGap responsive layout styles for this variation.
4005                      if ( isset( $breakpoint_node['spacing']['blockGap'] ) ) {
4006                          $variation_layout_metadata             = $style_variation;
4007                          $variation_layout_metadata['selector'] = $style_variation['selector'] . $block_metadata['css'];
4008  
4009                          // The variation slug is not a block name here either. See above.
4010                          $variation_layout_metadata['name'] = $block_name;
4011  
4012                          $variation_responsive_css .= $this->get_layout_styles(
4013                              $variation_layout_metadata,
4014                              array(
4015                                  'node'        => $breakpoint_node,
4016                                  'media_query' => $breakpoint_media,
4017                              )
4018                          );
4019                      }
4020  
4021                      // Process nested element styles for this breakpoint state.
4022                      if ( isset( $breakpoint_node['elements'] ) && ! empty( $block_elements ) ) {
4023                          foreach ( $breakpoint_node['elements'] as $element_name => $element_node ) {
4024                              if ( ! isset( $block_elements[ $element_name ] ) ) {
4025                                  continue;
4026                              }
4027  
4028                              $variation_element_selector = static::get_block_style_variation_feature_selector( $style_variation, $block_elements[ $element_name ] );
4029  
4030                              $element_declarations = static::compute_style_properties( $element_node, $settings, null, $this->theme_json );
4031                              if ( ! empty( $element_declarations ) ) {
4032                                  $element_ruleset           = static::to_ruleset( ':root :where(' . $variation_element_selector . ')', $element_declarations );
4033                                  $variation_responsive_css .= $breakpoint_media . '{' . $element_ruleset . '}';
4034                              }
4035  
4036                              if ( isset( $element_node['css'] ) ) {
4037                                  $element_custom_css        = static::process_blocks_custom_css( $element_node['css'], $variation_element_selector );
4038                                  $variation_responsive_css .= $breakpoint_media . '{' . $element_custom_css . '}';
4039                              }
4040  
4041                              if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
4042                                  foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
4043                                      if ( ! isset( $element_node[ $pseudo_selector ] ) ) {
4044                                          continue;
4045                                      }
4046  
4047                                      $pseudo_declarations = static::compute_style_properties( $element_node[ $pseudo_selector ], $settings, null, $this->theme_json );
4048                                      if ( empty( $pseudo_declarations ) ) {
4049                                          continue;
4050                                      }
4051  
4052                                      $pseudo_selector_ruleset          = static::to_ruleset( ':root :where(' . static::append_to_selector( $variation_element_selector, $pseudo_selector ) . ')', $pseudo_declarations );
4053                                      $variation_responsive_pseudo_css .= $breakpoint_media . '{' . $pseudo_selector_ruleset . '}';
4054                                  }
4055                              }
4056                          }
4057                      }
4058                  }
4059  
4060                  if ( ! empty( $variation_responsive_css ) ) {
4061                      $style_variation_responsive_css[ $style_variation['selector'] ] = $variation_responsive_css;
4062                  }
4063                  if ( ! empty( $variation_responsive_pseudo_css ) ) {
4064                      $style_variation_responsive_pseudo_css[ $style_variation['selector'] ] = $variation_responsive_pseudo_css;
4065                  }
4066              }
4067          }
4068          /*
4069           * Get a reference to element name from path.
4070           * $block_metadata['path'] = array( 'styles','elements','link' );
4071           * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ].
4072           * Skip non-element paths like just ['styles'].
4073           */
4074          $is_processing_element = in_array( 'elements', $block_metadata['path'], true );
4075  
4076          $current_element = $is_processing_element ? array_last( $block_metadata['path'] ) : null;
4077  
4078          $element_pseudo_allowed = array();
4079  
4080          if ( isset( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) {
4081              $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ];
4082          }
4083  
4084          /*
4085           * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover").
4086           * This also resets the array keys.
4087           */
4088          $pseudo_matches = array_values(
4089              array_filter(
4090                  $element_pseudo_allowed,
4091                  static function ( $pseudo_selector ) use ( $selector ) {
4092                      /*
4093                       * Check if the pseudo selector is in the current selector,
4094                       * ensuring it is not followed by a dash (e.g., :focus should not match :focus-visible).
4095                       */
4096                      return preg_match( '/' . preg_quote( $pseudo_selector, '/' ) . '(?!-)/', $selector ) === 1;
4097                  }
4098              )
4099          );
4100  
4101          $pseudo_selector = $pseudo_matches[0] ?? null;
4102  
4103          /*
4104           * If the current selector is a pseudo selector that's defined in the allow list for the current
4105           * element then compute the style properties for it.
4106           * Otherwise just compute the styles for the default selector as normal.
4107           */
4108          if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) &&
4109              isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] )
4110              && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true )
4111          ) {
4112              $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding );
4113          } else {
4114              /*
4115               * For block pseudo-selector nodes (e.g. ':hover'), $node has already had any
4116               * feature-selector properties (e.g. writingMode) removed by get_feature_declarations_for_node,
4117               * so those properties are not output twice.
4118               */
4119              $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
4120          }
4121  
4122          $block_rules = '';
4123  
4124          /*
4125           * 1. Bespoke declaration modifiers:
4126           * - 'filter': Separate the declarations that use the general selector
4127           * from the ones using the duotone selector.
4128           * - 'background|background-image': set the html min-height to 100%
4129           * to ensure the background covers the entire viewport.
4130           */
4131          $declarations_duotone       = array();
4132          $should_set_root_min_height = false;
4133  
4134          foreach ( $declarations as $index => $declaration ) {
4135              if ( 'filter' === $declaration['name'] ) {
4136                  /*
4137                   * 'unset' filters happen when a filter is unset
4138                   * in the site-editor UI. Because the 'unset' value
4139                   * in the user origin overrides the value in the
4140                   * theme origin, we can skip rendering anything
4141                   * here as no filter needs to be applied anymore.
4142                   * So only add declarations to with values other
4143                   * than 'unset'.
4144                   */
4145                  if ( 'unset' !== $declaration['value'] ) {
4146                      $declarations_duotone[] = $declaration;
4147                  }
4148                  unset( $declarations[ $index ] );
4149              }
4150  
4151              if ( $is_root_selector && ( 'background-image' === $declaration['name'] || 'background' === $declaration['name'] ) ) {
4152                  $should_set_root_min_height = true;
4153              }
4154          }
4155  
4156          /*
4157           * If root styles has a background-image or a background (gradient) set,
4158           * set the min-height to '100%'. Minus `--wp-admin--admin-bar--height` for logged-in view.
4159           * Setting the CSS rule on the HTML tag ensures background gradients and images behave similarly,
4160           * and matches the behavior of the site editor.
4161           */
4162          if ( $should_set_root_min_height ) {
4163              $block_rules .= static::to_ruleset(
4164                  'html',
4165                  array(
4166                      array(
4167                          'name'  => 'min-height',
4168                          'value' => 'calc(100% - var(--wp-admin--admin-bar--height, 0px))',
4169                      ),
4170                  )
4171              );
4172          }
4173  
4174          // Update declarations if there are separators with only background color defined.
4175          if ( '.wp-block-separator' === $selector ) {
4176              $declarations = self::update_separator_declarations( $declarations );
4177          }
4178  
4179          /*
4180           * Root selector (body) styles should not be wrapped in `:root where()` to keep
4181           * specificity at (0,0,1) and maintain backwards compatibility.
4182           *
4183           * Top-level element styles using element-only specificity selectors should
4184           * not get wrapped in `:root :where()` to maintain backwards compatibility.
4185           *
4186           * Pseudo classes, e.g. :hover, :focus etc., are a class-level selector so
4187           * still need to be wrapped in `:root :where` to cap specificity for nested
4188           * variations etc. Pseudo selectors won't match the ELEMENTS selector exactly.
4189           */
4190          $element_only_selector = $is_root_selector || (
4191              $current_element &&
4192              isset( static::ELEMENTS[ $current_element ] ) &&
4193              // buttons, captions etc. still need `:root :where()` as they are class based selectors.
4194              ! isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $current_element ] ) &&
4195              static::ELEMENTS[ $current_element ] === $selector
4196          );
4197  
4198          // 2. Generate and append the rules that use the general selector.
4199          $general_selector = $element_only_selector ? $selector : ":root :where($selector)";
4200          $block_rules     .= static::to_ruleset( $general_selector, $declarations );
4201  
4202          // 3. Generate and append the rules that use the duotone selector.
4203          if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
4204              $block_rules .= static::to_ruleset( $block_metadata['duotone'], $declarations_duotone );
4205          }
4206  
4207          // 4. Generate Layout block gap styles.
4208          if (
4209              ! $is_root_selector &&
4210              ! empty( $block_metadata['name'] )
4211          ) {
4212              $block_rules .= $this->get_layout_styles( $block_metadata );
4213          }
4214  
4215          // 5. Generate and append the feature level rulesets.
4216          foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) {
4217              $block_rules .= static::to_ruleset( ":root :where($feature_selector)", $individual_feature_declarations );
4218          }
4219  
4220          // 6. Generate and append the style variation rulesets.
4221          foreach ( $style_variation_declarations as $style_variation_selector => $individual_style_variation_declarations ) {
4222              $block_rules .= static::to_ruleset( ":root :where($style_variation_selector)", $individual_style_variation_declarations );
4223              if ( isset( $style_variation_layout_metadata[ $style_variation_selector ] ) ) {
4224                  $variation_data = $style_variation_layout_metadata[ $style_variation_selector ];
4225                  $block_rules   .= $this->get_layout_styles( $variation_data['metadata'], array( 'node' => $variation_data['node'] ) );
4226              }
4227              if ( isset( $style_variation_custom_css[ $style_variation_selector ] ) ) {
4228                  $block_rules .= $style_variation_custom_css[ $style_variation_selector ];
4229              }
4230              if ( isset( $style_variation_responsive_css[ $style_variation_selector ] ) ) {
4231                  $block_rules .= $style_variation_responsive_css[ $style_variation_selector ];
4232              }
4233          }
4234          /*
4235           * Responsive pseudo styles must be output after default pseudo styles
4236           * so viewport state styles win in the cascade.
4237           */
4238          foreach ( $style_variation_responsive_pseudo_css as $responsive_pseudo_css ) {
4239              $block_rules .= $responsive_pseudo_css;
4240          }
4241  
4242          // 7. Generate and append any custom CSS rules.
4243          if ( isset( $node['css'] ) && ! $is_root_selector ) {
4244              $css_feature_selector = $block_metadata['selectors']['css'] ?? null;
4245              if ( is_array( $css_feature_selector ) ) {
4246                  $css_feature_selector = $css_feature_selector['root'] ?? null;
4247              }
4248              $css_selector = is_string( $css_feature_selector ) ? $css_feature_selector : $selector;
4249              $block_rules .= $this->process_blocks_custom_css( $node['css'], $css_selector );
4250          }
4251  
4252          // 8. Wrap the entire block output in a media query if this is a responsive node.
4253          // Responsive nodes are created by get_block_nodes() for each breakpoint and carry
4254          // a 'media_query' key.
4255          if ( $media_query && ! empty( $block_rules ) ) {
4256              $block_rules = $media_query . '{' . $block_rules . '}';
4257          }
4258  
4259          return $block_rules;
4260      }
4261  
4262      /**
4263       * Outputs the CSS for layout rules on the root.
4264       *
4265       * @since 6.1.0
4266       * @since 6.6.0 Use `ROOT_CSS_PROPERTIES_SELECTOR` for CSS custom properties and improved consistency of root padding rules.
4267       *              Updated specificity of body margin reset and first/last child selectors.
4268       * @since 7.0.0 Added `$options` parameter to control alignment styles output for classic themes.
4269       *
4270       * @param string $selector       The root node selector.
4271       * @param array  $block_metadata The metadata for the root block.
4272       * @param array  $options        Optional. An array of options for now used for internal purposes only.
4273       * @return string The additional root rules CSS.
4274       */
4275  	public function get_root_layout_rules( $selector, $block_metadata, $options = array() ) {
4276          $css              = '';
4277          $settings         = $this->theme_json['settings'] ?? array();
4278          $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
4279  
4280          /*
4281           * If there are content and wide widths in theme.json, output them
4282           * as custom properties on the body element so all blocks can use them.
4283           */
4284          if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) {
4285              $content_size = $settings['layout']['contentSize'] ?? $settings['layout']['wideSize'];
4286              $content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial';
4287              $wide_size    = $settings['layout']['wideSize'] ?? $settings['layout']['contentSize'];
4288              $wide_size    = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial';
4289              $css         .= static::ROOT_CSS_PROPERTIES_SELECTOR . ' { --wp--style--global--content-size: ' . $content_size . ';';
4290              $css         .= '--wp--style--global--wide-size: ' . $wide_size . '; }';
4291          }
4292  
4293          /*
4294           * Reset default browser margin on the body element.
4295           * This is set on the body selector **before** generating the ruleset
4296           * from the `theme.json`. This is to ensure that if the `theme.json` declares
4297           * `margin` in its `spacing` declaration for the `body` element then these
4298           * user-generated values take precedence in the CSS cascade.
4299           * @link https://github.com/WordPress/gutenberg/issues/36147.
4300           */
4301          $css .= ':where(body) { margin: 0; }';
4302  
4303          if ( $use_root_padding ) {
4304              // Top and bottom padding are applied to the outer block container.
4305              $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }';
4306              // Right and left padding are applied to the first container with `.has-global-padding` class.
4307              $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
4308              // Alignfull children of the container with left and right padding have negative margins so they can still be full width.
4309              $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); }';
4310              // Nested children of the container with left and right padding that are not full aligned do not get padding, unless they are direct children of an alignfull flow container.
4311              $css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) { padding-right: 0; padding-left: 0; }';
4312              // Alignfull direct children of the containers that are targeted by the rule above do not need negative margins.
4313              $css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) > .alignfull { margin-left: 0; margin-right: 0; }';
4314          }
4315  
4316          // Skip outputting alignment styles when base_layout_styles is enabled.
4317          // These styles target .wp-site-blocks which is only used by block themes.
4318          if ( empty( $options['base_layout_styles'] ) ) {
4319              $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
4320              $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
4321              $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
4322          }
4323  
4324          // Block gap styles will be output unless explicitly set to `null`.
4325          if ( isset( $this->theme_json['settings']['spacing']['blockGap'] ) ) {
4326              $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) );
4327              $css            .= ":where(.wp-site-blocks) > * { margin-block-start: $block_gap_value; margin-block-end: 0; }";
4328              $css            .= ':where(.wp-site-blocks) > :first-child { margin-block-start: 0; }';
4329              $css            .= ':where(.wp-site-blocks) > :last-child { margin-block-end: 0; }';
4330  
4331              // For backwards compatibility, ensure the legacy block gap CSS variable is still available.
4332              $css .= static::ROOT_CSS_PROPERTIES_SELECTOR . " { --wp--style--block-gap: $block_gap_value; }";
4333          }
4334          $css .= $this->get_layout_styles( $block_metadata, $options );
4335  
4336          return $css;
4337      }
4338  
4339      /**
4340       * For metadata values that can either be booleans or paths to booleans, gets the value.
4341       *
4342       *     $data = array(
4343       *       'color' => array(
4344       *         'defaultPalette' => true
4345       *       )
4346       *     );
4347       *
4348       *     static::get_metadata_boolean( $data, false );
4349       *     // => false
4350       *
4351       *     static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) );
4352       *     // => true
4353       *
4354       * @since 6.0.0
4355       *
4356       * @param array      $data          The data to inspect.
4357       * @param bool|array $path          Boolean or path to a boolean.
4358       * @param bool       $default_value Default value if the referenced path is missing.
4359       *                                  Default false.
4360       * @return bool Value of boolean metadata.
4361       */
4362  	protected static function get_metadata_boolean( $data, $path, $default_value = false ) {
4363          if ( is_bool( $path ) ) {
4364              return $path;
4365          }
4366  
4367          if ( is_array( $path ) ) {
4368              $value = _wp_array_get( $data, $path );
4369              if ( null !== $value ) {
4370                  return $value;
4371              }
4372          }
4373  
4374          return $default_value;
4375      }
4376  
4377      /**
4378       * Merges new incoming data.
4379       *
4380       * @since 5.8.0
4381       * @since 5.9.0 Duotone preset also has origins.
4382       * @since 6.7.0 Replace background image objects during merge.
4383       *
4384       * @param WP_Theme_JSON $incoming Data to merge.
4385       */
4386  	public function merge( $incoming ) {
4387          $incoming_data    = $incoming->get_raw_data();
4388          $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
4389  
4390          /*
4391           * Recompute all the spacing sizes based on the new hierarchy of data. In the constructor
4392           * spacingScale and spacingSizes are both keyed by origin and VALID_ORIGINS is ordered, so
4393           * we can allow partial spacingScale data to inherit missing data from earlier layers when
4394           * computing the spacing sizes.
4395           *
4396           * This happens before the presets are merged to ensure that default spacing sizes can be
4397           * removed from the theme origin if $prevent_override is true.
4398           */
4399          $flattened_spacing_scale = array();
4400          foreach ( static::VALID_ORIGINS as $origin ) {
4401              $scale_path = array( 'settings', 'spacing', 'spacingScale', $origin );
4402  
4403              // Apply the base spacing scale to the current layer.
4404              $base_spacing_scale      = _wp_array_get( $this->theme_json, $scale_path, array() );
4405              $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $base_spacing_scale );
4406  
4407              $spacing_scale = _wp_array_get( $incoming_data, $scale_path, null );
4408              if ( ! isset( $spacing_scale ) ) {
4409                  continue;
4410              }
4411  
4412              // Allow partial scale settings by merging with lower layers.
4413              $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $spacing_scale );
4414  
4415              // Generate and merge the scales for this layer.
4416              $sizes_path           = array( 'settings', 'spacing', 'spacingSizes', $origin );
4417              $spacing_sizes        = _wp_array_get( $incoming_data, $sizes_path, array() );
4418              $spacing_scale_sizes  = self::compute_spacing_sizes( $flattened_spacing_scale );
4419              $merged_spacing_sizes = self::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );
4420  
4421              _wp_array_set( $incoming_data, $sizes_path, $merged_spacing_sizes );
4422          }
4423  
4424          /*
4425           * The array_replace_recursive algorithm merges at the leaf level,
4426           * but we don't want leaf arrays to be merged, so we overwrite it.
4427           *
4428           * For leaf values that are sequential arrays it will use the numeric indexes for replacement.
4429           * We rather replace the existing with the incoming value, if it exists.
4430           * This is the case of spacing.units.
4431           *
4432           * For leaf values that are associative arrays it will merge them as expected.
4433           * This is also not the behavior we want for the current associative arrays (presets).
4434           * We rather replace the existing with the incoming value, if it exists.
4435           * This happens, for example, when we merge data from theme.json upon existing
4436           * theme supports or when we merge anything coming from the same source twice.
4437           * This is the case of color.palette, color.gradients, color.duotone,
4438           * typography.fontSizes, or typography.fontFamilies.
4439           *
4440           * Additionally, for some preset types, we also want to make sure the
4441           * values they introduce don't conflict with default values. We do so
4442           * by checking the incoming slugs for theme presets and compare them
4443           * with the equivalent default presets: if a slug is present as a default
4444           * we remove it from the theme presets.
4445           */
4446          $nodes        = static::get_setting_nodes( $incoming_data );
4447          $slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) );
4448          foreach ( $nodes as $node ) {
4449              // Replace the spacing.units.
4450              $path   = $node['path'];
4451              $path[] = 'spacing';
4452              $path[] = 'units';
4453  
4454              $content = _wp_array_get( $incoming_data, $path, null );
4455              if ( isset( $content ) ) {
4456                  _wp_array_set( $this->theme_json, $path, $content );
4457              }
4458  
4459              // Replace the presets.
4460              foreach ( static::PRESETS_METADATA as $preset_metadata ) {
4461                  $prevent_override = $preset_metadata['prevent_override'];
4462                  if ( is_array( $prevent_override ) ) {
4463                      $global_path  = array_merge( array( 'settings' ), $prevent_override );
4464                      $global_value = _wp_array_get( $this->theme_json, $global_path, null );
4465  
4466                      $node_level_path  = array_merge( $node['path'], $prevent_override );
4467                      $prevent_override = _wp_array_get( $this->theme_json, $node_level_path, $global_value );
4468                  }
4469  
4470                  foreach ( static::VALID_ORIGINS as $origin ) {
4471                      $base_path = $node['path'];
4472                      foreach ( $preset_metadata['path'] as $leaf ) {
4473                          $base_path[] = $leaf;
4474                      }
4475  
4476                      $path   = $base_path;
4477                      $path[] = $origin;
4478  
4479                      $content = _wp_array_get( $incoming_data, $path, null );
4480                      if ( ! isset( $content ) ) {
4481                          continue;
4482                      }
4483  
4484                      // Set names for theme presets based on the slug if they are not set and can use default names.
4485                      if ( 'theme' === $origin && $preset_metadata['use_default_names'] ) {
4486                          foreach ( $content as $key => $item ) {
4487                              if ( ! isset( $item['name'] ) ) {
4488                                  $name = static::get_name_from_defaults( $item['slug'], $base_path );
4489                                  if ( null !== $name ) {
4490                                      $content[ $key ]['name'] = $name;
4491                                  }
4492                              }
4493                          }
4494                      }
4495  
4496                      // Filter out default slugs from theme presets when defaults should not be overridden.
4497                      if ( 'theme' === $origin && $prevent_override ) {
4498                          $slugs_node    = static::get_default_slugs( $this->theme_json, $node['path'] );
4499                          $preset_global = _wp_array_get( $slugs_global, $preset_metadata['path'], array() );
4500                          $preset_node   = _wp_array_get( $slugs_node, $preset_metadata['path'], array() );
4501                          $preset_slugs  = array_merge_recursive( $preset_global, $preset_node );
4502  
4503                          $content = static::filter_slugs( $content, $preset_slugs );
4504                      }
4505  
4506                      _wp_array_set( $this->theme_json, $path, $content );
4507                  }
4508              }
4509          }
4510  
4511          /*
4512           * Style values are merged at the leaf level, however
4513           * some values provide exceptions, namely style values that are
4514           * objects and represent unique definitions for the style.
4515           */
4516          $style_nodes = self::get_block_nodes(
4517              $this->theme_json,
4518              array(),
4519              array( 'include_node_paths_only' => true )
4520          );
4521  
4522          // Add top-level styles.
4523          $style_nodes[] = array( 'path' => array( 'styles' ) );
4524  
4525          foreach ( $style_nodes as $style_node ) {
4526              $path = $style_node['path'];
4527              /*
4528               * Background image styles should be replaced, not merged,
4529               * as they themselves are specific object definitions for the style.
4530               */
4531              $background_image_path = array_merge( $path, static::PROPERTIES_METADATA['background-image'] );
4532              $content               = _wp_array_get( $incoming_data, $background_image_path, null );
4533              if ( isset( $content ) ) {
4534                  _wp_array_set( $this->theme_json, $background_image_path, $content );
4535              }
4536          }
4537      }
4538  
4539      /**
4540       * Converts all filter (duotone) presets into SVGs.
4541       *
4542       * @since 5.9.1
4543       *
4544       * @param array $origins List of origins to process.
4545       * @return string SVG filters.
4546       */
4547  	public function get_svg_filters( $origins ) {
4548          $blocks_metadata = static::get_blocks_metadata();
4549          $setting_nodes   = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
4550  
4551          $filters = '';
4552          foreach ( $setting_nodes as $metadata ) {
4553              $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
4554              if ( empty( $node['color']['duotone'] ) ) {
4555                  continue;
4556              }
4557  
4558              $duotone_presets = $node['color']['duotone'];
4559  
4560              foreach ( $origins as $origin ) {
4561                  if ( ! isset( $duotone_presets[ $origin ] ) ) {
4562                      continue;
4563                  }
4564                  foreach ( $duotone_presets[ $origin ] as $duotone_preset ) {
4565                      $filters .= WP_Duotone::get_filter_svg_from_preset( $duotone_preset );
4566                  }
4567              }
4568          }
4569  
4570          return $filters;
4571      }
4572  
4573      /**
4574       * Determines whether a presets should be overridden or not.
4575       *
4576       * @since 5.9.0
4577       * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead.
4578       *
4579       * @param array      $theme_json The theme.json like structure to inspect.
4580       * @param array      $path       Path to inspect.
4581       * @param bool|array $override   Data to compute whether to override the preset.
4582       * @return bool|null True if the preset should override the defaults, false if not. Null if the override parameter is invalid.
4583       */
4584  	protected static function should_override_preset( $theme_json, $path, $override ) {
4585          _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' );
4586  
4587          if ( is_bool( $override ) ) {
4588              return $override;
4589          }
4590  
4591          /*
4592           * The relationship between whether to override the defaults
4593           * and whether the defaults are enabled is inverse:
4594           *
4595           * - If defaults are enabled  => theme presets should not be overridden
4596           * - If defaults are disabled => theme presets should be overridden
4597           *
4598           * For example, a theme sets defaultPalette to false,
4599           * making the default palette hidden from the user.
4600           * In that case, we want all the theme presets to be present,
4601           * so they should override the defaults.
4602           */
4603          if ( is_array( $override ) ) {
4604              $value = _wp_array_get( $theme_json, array_merge( $path, $override ) );
4605              if ( isset( $value ) ) {
4606                  return ! $value;
4607              }
4608  
4609              // Search the top-level key if none was found for this node.
4610              $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) );
4611              if ( isset( $value ) ) {
4612                  return ! $value;
4613              }
4614  
4615              return true;
4616          }
4617  
4618          return null;
4619      }
4620  
4621      /**
4622       * Returns the default slugs for all the presets in an associative array
4623       * whose keys are the preset paths and the leaves is the list of slugs.
4624       *
4625       * For example:
4626       *
4627       *     array(
4628       *       'color' => array(
4629       *         'palette'   => array( 'slug-1', 'slug-2' ),
4630       *         'gradients' => array( 'slug-3', 'slug-4' ),
4631       *       ),
4632       *     )
4633       *
4634       * @since 5.9.0
4635       *
4636       * @param array $data      A theme.json like structure.
4637       * @param array $node_path The path to inspect. It's 'settings' by default.
4638       * @return array
4639       */
4640  	protected static function get_default_slugs( $data, $node_path ) {
4641          $slugs = array();
4642  
4643          foreach ( static::PRESETS_METADATA as $metadata ) {
4644              $path = $node_path;
4645              foreach ( $metadata['path'] as $leaf ) {
4646                  $path[] = $leaf;
4647              }
4648              $path[] = 'default';
4649  
4650              $preset = _wp_array_get( $data, $path, null );
4651              if ( ! isset( $preset ) ) {
4652                  continue;
4653              }
4654  
4655              $slugs_for_preset = array();
4656              foreach ( $preset as $item ) {
4657                  if ( isset( $item['slug'] ) ) {
4658                      $slugs_for_preset[] = $item['slug'];
4659                  }
4660              }
4661  
4662              _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset );
4663          }
4664  
4665          return $slugs;
4666      }
4667  
4668      /**
4669       * Gets a `default`'s preset name by a provided slug.
4670       *
4671       * @since 5.9.0
4672       *
4673       * @param string $slug      The slug we want to find a match from default presets.
4674       * @param array  $base_path The path to inspect. It's 'settings' by default.
4675       * @return string|null
4676       */
4677  	protected function get_name_from_defaults( $slug, $base_path ) {
4678          $path            = $base_path;
4679          $path[]          = 'default';
4680          $default_content = _wp_array_get( $this->theme_json, $path, null );
4681          if ( ! $default_content ) {
4682              return null;
4683          }
4684          foreach ( $default_content as $item ) {
4685              if ( $slug === $item['slug'] ) {
4686                  return $item['name'];
4687              }
4688          }
4689          return null;
4690      }
4691  
4692      /**
4693       * Removes the preset values whose slug is equal to any of given slugs.
4694       *
4695       * @since 5.9.0
4696       *
4697       * @param array $node  The node with the presets to validate.
4698       * @param array $slugs The slugs that should not be overridden.
4699       * @return array The new node.
4700       */
4701  	protected static function filter_slugs( $node, $slugs ) {
4702          if ( empty( $slugs ) ) {
4703              return $node;
4704          }
4705  
4706          $new_node = array();
4707          foreach ( $node as $value ) {
4708              if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) {
4709                  $new_node[] = $value;
4710              }
4711          }
4712  
4713          return $new_node;
4714      }
4715  
4716      /**
4717       * Removes insecure data from theme.json.
4718       *
4719       * @since 5.9.0
4720       * @since 6.3.2 Preserves global styles block variations when securing styles.
4721       * @since 6.6.0 Updated to allow variation element styles and $origin parameter.
4722       *
4723       * @param array  $theme_json Structure to sanitize.
4724       * @param string $origin     Optional. What source of data this object represents.
4725       *                           One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
4726       * @return array Sanitized structure.
4727       */
4728  	public static function remove_insecure_properties( $theme_json, $origin = 'theme' ) {
4729          if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
4730              $origin = 'theme';
4731          }
4732  
4733          $sanitized = array();
4734  
4735          $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json, $origin );
4736  
4737          $blocks_metadata     = static::get_blocks_metadata();
4738          $valid_block_names   = array_keys( $blocks_metadata );
4739          $valid_element_names = array_keys( static::ELEMENTS );
4740          $valid_variations    = static::get_valid_block_style_variations( $blocks_metadata );
4741  
4742          $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations );
4743  
4744          $blocks_metadata          = static::get_blocks_metadata();
4745          $style_options            = array( 'include_block_style_variations' => true ); // Allow variations data.
4746          $style_nodes              = static::get_style_nodes( $theme_json, $blocks_metadata, $style_options );
4747          $responsive_media_queries = static::get_viewport_media_queries( $theme_json['settings']['viewport'] ?? null );
4748  
4749          foreach ( $style_nodes as $metadata ) {
4750              $input = _wp_array_get( $theme_json, $metadata['path'], array() );
4751              if ( empty( $input ) ) {
4752                  continue;
4753              }
4754  
4755              $block_name = in_array( 'blocks', $metadata['path'], true )
4756                  ? self::get_block_name_from_metadata_path( $metadata )
4757                  : null;
4758  
4759              // The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability.
4760              if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) {
4761                  $output = $input;
4762              } else {
4763                  $output = static::remove_insecure_styles( $input );
4764              }
4765  
4766              /*
4767               * Get a reference to element name from path.
4768               * $metadata['path'] = array( 'styles', 'elements', 'link' );
4769               */
4770              $current_element = array_last( $metadata['path'] );
4771  
4772              /*
4773               * $output is stripped of pseudo selectors. Re-add and process them
4774               * or insecure styles here.
4775               */
4776              if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) {
4777                  foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) {
4778                      if ( isset( $input[ $pseudo_selector ] ) ) {
4779                          $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] );
4780                      }
4781                  }
4782              }
4783  
4784              // Re-add and process responsive breakpoint styles.
4785              foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4786                  if ( isset( $input[ $breakpoint ] ) ) {
4787                      $output[ $breakpoint ] = static::remove_insecure_styles( $input[ $breakpoint ] );
4788  
4789                      if ( isset( $input[ $breakpoint ]['elements'] ) ) {
4790                          $output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $input[ $breakpoint ]['elements'], $responsive_media_queries );
4791                      }
4792  
4793                      if ( isset( $input[ $breakpoint ]['blocks'] ) ) {
4794                          $output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $input[ $breakpoint ]['blocks'], $responsive_media_queries );
4795                      }
4796  
4797                      if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
4798                          foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
4799                              if ( isset( $input[ $breakpoint ][ $pseudo_selector ] ) ) {
4800                                  $output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $input[ $breakpoint ][ $pseudo_selector ] );
4801                              }
4802                          }
4803                      }
4804  
4805                      // Responsive custom CSS is allowed for users with 'edit_css' capability.
4806                      if ( isset( $input[ $breakpoint ]['css'] ) && current_user_can( 'edit_css' ) ) {
4807                          $output[ $breakpoint ]['css'] = $input[ $breakpoint ]['css'];
4808                      }
4809                  }
4810              }
4811  
4812              if ( ! empty( $output ) ) {
4813                  _wp_array_set( $sanitized, $metadata['path'], $output );
4814              }
4815  
4816              if ( isset( $metadata['variations'] ) ) {
4817                  foreach ( $metadata['variations'] as $variation ) {
4818                      $variation_input = _wp_array_get( $theme_json, $variation['path'], array() );
4819                      if ( empty( $variation_input ) ) {
4820                          continue;
4821                      }
4822  
4823                      $variation_output = static::remove_insecure_styles( $variation_input );
4824  
4825                      if ( isset( $variation_input['blocks'] ) ) {
4826                          $variation_output['blocks'] = static::remove_insecure_inner_block_styles( $variation_input['blocks'], $responsive_media_queries );
4827                      }
4828  
4829                      if ( isset( $variation_input['elements'] ) ) {
4830                          $variation_output['elements'] = static::remove_insecure_element_styles( $variation_input['elements'], $responsive_media_queries );
4831                      }
4832  
4833                      // Re-add and process responsive breakpoint styles for variations.
4834                      foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4835                          if ( isset( $variation_input[ $breakpoint ] ) ) {
4836                              $variation_output[ $breakpoint ] = static::remove_insecure_styles( $variation_input[ $breakpoint ] );
4837  
4838                              if ( isset( $variation_input[ $breakpoint ]['elements'] ) ) {
4839                                  $variation_output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $variation_input[ $breakpoint ]['elements'], $responsive_media_queries );
4840                              }
4841  
4842                              if ( isset( $variation_input[ $breakpoint ]['blocks'] ) ) {
4843                                  $variation_output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $variation_input[ $breakpoint ]['blocks'], $responsive_media_queries );
4844                              }
4845  
4846                              if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
4847                                  foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
4848                                      if ( isset( $variation_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4849                                          $variation_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $variation_input[ $breakpoint ][ $pseudo_selector ] );
4850                                      }
4851                                  }
4852                              }
4853  
4854                              // Responsive custom CSS is allowed for users with 'edit_css' capability.
4855                              if ( isset( $variation_input[ $breakpoint ]['css'] ) && current_user_can( 'edit_css' ) ) {
4856                                  $variation_output[ $breakpoint ]['css'] = $variation_input[ $breakpoint ]['css'];
4857                              }
4858                          }
4859                      }
4860  
4861                      if ( ! empty( $variation_output ) ) {
4862                          _wp_array_set( $sanitized, $variation['path'], $variation_output );
4863                      }
4864                  }
4865              }
4866          }
4867  
4868          $setting_nodes = static::get_setting_nodes( $theme_json );
4869          foreach ( $setting_nodes as $metadata ) {
4870              $input = _wp_array_get( $theme_json, $metadata['path'], array() );
4871              if ( empty( $input ) ) {
4872                  continue;
4873              }
4874  
4875              $output = static::remove_insecure_settings( $input, array( 'settings' ) === $metadata['path'] );
4876              if ( ! empty( $output ) ) {
4877                  _wp_array_set( $sanitized, $metadata['path'], $output );
4878              }
4879          }
4880  
4881          if ( empty( $sanitized['styles'] ) ) {
4882              unset( $theme_json['styles'] );
4883          } else {
4884              $theme_json['styles'] = $sanitized['styles'];
4885          }
4886  
4887          if ( empty( $sanitized['settings'] ) ) {
4888              unset( $theme_json['settings'] );
4889          } else {
4890              $theme_json['settings'] = $sanitized['settings'];
4891          }
4892  
4893          return $theme_json;
4894      }
4895  
4896      /**
4897       * Remove insecure element styles within a variation or block.
4898       *
4899       *  * When responsive media queries are provided, nested responsive state styles
4900       * matching those viewport state keys are re-added after the base sanitization pass.
4901       *
4902       * @since 6.8.0
4903       * @since 7.1.0 Added the `$responsive_media_queries` parameter.
4904       *
4905       * @param array      $elements                 The elements to process.
4906       * @param array|null $responsive_media_queries Optional. Media queries whose keys define allowed
4907       *                                             viewport states. Default null.
4908       * @return array The sanitized elements styles.
4909       */
4910  	protected static function remove_insecure_element_styles( $elements, $responsive_media_queries = null ) {
4911          $sanitized           = array();
4912          $valid_element_names = array_keys( static::ELEMENTS );
4913  
4914          foreach ( $valid_element_names as $element_name ) {
4915              $element_input = $elements[ $element_name ] ?? null;
4916              if ( $element_input ) {
4917                  $element_output = static::remove_insecure_styles( $element_input );
4918  
4919                  if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
4920                      foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
4921                          if ( isset( $element_input[ $pseudo_selector ] ) ) {
4922                              $element_output[ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $pseudo_selector ] );
4923                          }
4924                      }
4925                  }
4926  
4927                  if ( null !== $responsive_media_queries ) {
4928                      // Re-add and process responsive breakpoint styles for elements.
4929                      foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4930                          if ( isset( $element_input[ $breakpoint ] ) ) {
4931                              $element_output[ $breakpoint ] = static::remove_insecure_styles( $element_input[ $breakpoint ] );
4932  
4933                              if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
4934                                  foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
4935                                      if ( isset( $element_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4936                                          $element_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $breakpoint ][ $pseudo_selector ] );
4937                                      }
4938                                  }
4939                              }
4940                          }
4941                      }
4942                  }
4943  
4944                  $sanitized[ $element_name ] = $element_output;
4945              }
4946          }
4947          return $sanitized;
4948      }
4949  
4950      /**
4951       * Remove insecure styles from inner blocks and their elements.
4952       *
4953       * When responsive media queries are provided, nested responsive state styles
4954       * for those media-query keys are re-added after the base sanitization pass.
4955       *
4956       * @since 6.8.0
4957       * @since 7.1.0 Added the `$responsive_media_queries` parameter.
4958       *
4959       * @param array      $blocks                   The block styles to process.
4960       * @param array|null $responsive_media_queries Optional. Media queries whose keys define allowed
4961       *                                             viewport states. Default null.
4962       * @return array Sanitized block type styles.
4963       */
4964  	protected static function remove_insecure_inner_block_styles( $blocks, $responsive_media_queries = null ) {
4965          $sanitized = array();
4966          foreach ( $blocks as $block_type => $block_input ) {
4967              $block_output = static::remove_insecure_styles( $block_input );
4968  
4969              if ( isset( $block_input['elements'] ) ) {
4970                  $block_output['elements'] = static::remove_insecure_element_styles( $block_input['elements'], $responsive_media_queries );
4971              }
4972  
4973              if ( null !== $responsive_media_queries ) {
4974                  // Re-add and process responsive breakpoint styles for inner blocks.
4975                  foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
4976                      if ( isset( $block_input[ $breakpoint ] ) ) {
4977                          $block_output[ $breakpoint ] = static::remove_insecure_styles( $block_input[ $breakpoint ] );
4978  
4979                          if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] ) ) {
4980                              foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] as $pseudo_selector ) {
4981                                  if ( isset( $block_input[ $breakpoint ][ $pseudo_selector ] ) ) {
4982                                      $block_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $block_input[ $breakpoint ][ $pseudo_selector ] );
4983                                  }
4984                              }
4985                          }
4986                      }
4987                  }
4988              }
4989  
4990              $sanitized[ $block_type ] = $block_output;
4991          }
4992          return $sanitized;
4993      }
4994  
4995      /**
4996       * Preserves valid typed settings from input to output based on type markers in schema.
4997       *
4998       * Recursively iterates through the schema and validates/preserves settings
4999       * that have type markers (e.g., boolean) in VALID_SETTINGS.
5000       *
5001       * @since 7.0.0
5002       *
5003       * @param array             $input  Input settings to process.
5004       * @param array             $output Output settings array (passed by reference).
5005       * @param array             $schema Schema to validate against (typically VALID_SETTINGS).
5006       * @param array<string|int> $path   Current path in the schema (for recursive calls).
5007       */
5008  	private static function preserve_valid_typed_settings( $input, &$output, $schema, $path = array() ) {
5009          foreach ( $schema as $key => $schema_value ) {
5010              $current_path = array_merge( $path, array( $key ) );
5011  
5012              // Validate boolean type markers.
5013              if ( is_bool( $schema_value ) ) {
5014                  $value = _wp_array_get( $input, $current_path, null );
5015                  if ( is_bool( $value ) ) {
5016                      _wp_array_set( $output, $current_path, $value ); // Preserve boolean value.
5017                  }
5018              } elseif ( is_array( $schema_value ) ) {
5019                  self::preserve_valid_typed_settings( $input, $output, $schema_value, $current_path ); // Recurse into nested structure.
5020              }
5021          }
5022      }
5023  
5024      /**
5025       * Processes a setting node and returns the same node
5026       * without the insecure settings.
5027       *
5028       * @since 5.9.0
5029       * @since 7.1.0 Added the `$is_root` parameter.
5030       *
5031       * @param array $input   Node to process.
5032       * @param bool  $is_root Optional. Whether the node is the root settings node. Default false.
5033       * @return array
5034       */
5035  	protected static function remove_insecure_settings( $input, $is_root = false ) {
5036          $output = array();
5037          foreach ( static::PRESETS_METADATA as $preset_metadata ) {
5038              foreach ( static::VALID_ORIGINS as $origin ) {
5039                  $path_with_origin   = $preset_metadata['path'];
5040                  $path_with_origin[] = $origin;
5041                  $presets            = _wp_array_get( $input, $path_with_origin, null );
5042                  if ( null === $presets ) {
5043                      continue;
5044                  }
5045  
5046                  $escaped_preset = array();
5047                  foreach ( $presets as $preset ) {
5048                      if (
5049                          esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] &&
5050                          sanitize_html_class( $preset['slug'] ) === $preset['slug']
5051                      ) {
5052                          $value = null;
5053                          if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
5054                              $value = $preset[ $preset_metadata['value_key'] ];
5055                          } elseif (
5056                              isset( $preset_metadata['value_func'] ) &&
5057                              is_callable( $preset_metadata['value_func'] )
5058                          ) {
5059                              $value = call_user_func( $preset_metadata['value_func'], $preset );
5060                          }
5061  
5062                          $preset_is_valid = true;
5063                          foreach ( $preset_metadata['properties'] as $property ) {
5064                              if ( ! static::is_safe_css_declaration( $property, $value ) ) {
5065                                  $preset_is_valid = false;
5066                                  break;
5067                              }
5068                          }
5069  
5070                          if ( $preset_is_valid ) {
5071                              $escaped_preset[] = $preset;
5072                          }
5073                      }
5074                  }
5075  
5076                  if ( ! empty( $escaped_preset ) ) {
5077                      _wp_array_set( $output, $path_with_origin, $escaped_preset );
5078                  }
5079              }
5080          }
5081  
5082          // Ensure indirect properties not included in any `PRESETS_METADATA` value are allowed.
5083          self::remove_indirect_properties( $input, $output );
5084  
5085          // Preserve all valid settings that have type markers in VALID_SETTINGS.
5086          self::preserve_valid_typed_settings( $input, $output, static::VALID_SETTINGS );
5087  
5088          if ( $is_root && array_key_exists( 'viewport', $input ) ) {
5089              $output['viewport'] = self::sanitize_viewport_settings( $input['viewport'] );
5090          }
5091  
5092          return $output;
5093      }
5094  
5095      /**
5096       * Processes a style node and returns the same node
5097       * without the insecure styles.
5098       *
5099       * @since 5.9.0
5100       *
5101       * @param array $input Node to process.
5102       * @return array
5103       */
5104  	protected static function remove_insecure_styles( $input ) {
5105          $output       = array();
5106          $declarations = static::compute_style_properties( $input );
5107  
5108          foreach ( $declarations as $declaration ) {
5109              if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) {
5110                  $path = static::PROPERTIES_METADATA[ $declaration['name'] ];
5111  
5112                  /*
5113                   * Check the value isn't an array before adding so as to not
5114                   * double up shorthand and longhand styles.
5115                   */
5116                  $value = _wp_array_get( $input, $path, array() );
5117                  if ( ! is_array( $value ) ) {
5118                      _wp_array_set( $output, $path, $value );
5119                  }
5120              }
5121          }
5122  
5123          // Ensure indirect properties not handled by `compute_style_properties` are allowed.
5124          self::remove_indirect_properties( $input, $output );
5125  
5126          return $output;
5127      }
5128  
5129      /**
5130       * Checks that a declaration provided by the user is safe.
5131       *
5132       * @since 5.9.0
5133       *
5134       * @param string $property_name  Property name in a CSS declaration, i.e. the `color` in `color: red`.
5135       * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`.
5136       * @return bool
5137       */
5138  	protected static function is_safe_css_declaration( $property_name, $property_value ) {
5139          $style_to_validate = $property_name . ': ' . $property_value;
5140          $filtered          = esc_html( safecss_filter_attr( $style_to_validate ) );
5141          return ! empty( trim( $filtered ) );
5142      }
5143  
5144      /**
5145       * Removes indirect properties from the given input node and
5146       * sets in the given output node.
5147       *
5148       * @since 6.2.0
5149       *
5150       * @param array $input  Node to process.
5151       * @param array $output The processed node. Passed by reference.
5152       */
5153  	private static function remove_indirect_properties( $input, &$output ) {
5154          foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) {
5155              foreach ( $paths as $path ) {
5156                  $value = _wp_array_get( $input, $path );
5157                  if (
5158                      is_string( $value ) &&
5159                      static::is_safe_css_declaration( $property, $value )
5160                  ) {
5161                      _wp_array_set( $output, $path, $value );
5162                  }
5163              }
5164          }
5165      }
5166  
5167      /**
5168       * Returns the raw data.
5169       *
5170       * @since 5.8.0
5171       *
5172       * @return array Raw data.
5173       */
5174  	public function get_raw_data() {
5175          return $this->theme_json;
5176      }
5177  
5178      /**
5179       * Transforms the given editor settings according the
5180       * add_theme_support format to the theme.json format.
5181       *
5182       * @since 5.8.0
5183       *
5184       * @param array $settings Existing editor settings.
5185       * @return array Config that adheres to the theme.json schema.
5186       */
5187  	public static function get_from_editor_settings( $settings ) {
5188          $theme_settings = array(
5189              'version'  => static::LATEST_SCHEMA,
5190              'settings' => array(),
5191          );
5192  
5193          // Deprecated theme supports.
5194          if ( isset( $settings['disableCustomColors'] ) ) {
5195              $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors'];
5196          }
5197  
5198          if ( isset( $settings['disableCustomGradients'] ) ) {
5199              $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients'];
5200          }
5201  
5202          if ( isset( $settings['disableCustomFontSizes'] ) ) {
5203              $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes'];
5204          }
5205  
5206          if ( isset( $settings['enableCustomLineHeight'] ) ) {
5207              $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight'];
5208          }
5209  
5210          if ( isset( $settings['enableCustomUnits'] ) ) {
5211              $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ?
5212                  array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) :
5213                  $settings['enableCustomUnits'];
5214          }
5215  
5216          if ( isset( $settings['colors'] ) ) {
5217              $theme_settings['settings']['color']['palette'] = $settings['colors'];
5218          }
5219  
5220          if ( isset( $settings['gradients'] ) ) {
5221              $theme_settings['settings']['color']['gradients'] = $settings['gradients'];
5222          }
5223  
5224          if ( isset( $settings['fontSizes'] ) ) {
5225              $font_sizes = $settings['fontSizes'];
5226              // Back-compatibility for presets without units.
5227              foreach ( $font_sizes as $key => $font_size ) {
5228                  if ( is_numeric( $font_size['size'] ) ) {
5229                      $font_sizes[ $key ]['size'] = $font_size['size'] . 'px';
5230                  }
5231              }
5232              $theme_settings['settings']['typography']['fontSizes'] = $font_sizes;
5233          }
5234  
5235          if ( isset( $settings['enableCustomSpacing'] ) ) {
5236              $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing'];
5237          }
5238  
5239          if ( isset( $settings['spacingSizes'] ) ) {
5240              $theme_settings['settings']['spacing']['spacingSizes'] = $settings['spacingSizes'];
5241          }
5242  
5243          return $theme_settings;
5244      }
5245  
5246      /**
5247       * Returns the current theme's wanted patterns(slugs) to be
5248       * registered from Pattern Directory.
5249       *
5250       * @since 6.0.0
5251       *
5252       * @return string[]
5253       */
5254  	public function get_patterns() {
5255          if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) {
5256              return $this->theme_json['patterns'];
5257          }
5258          return array();
5259      }
5260  
5261      /**
5262       * Returns a valid theme.json as provided by a theme.
5263       *
5264       * Unlike get_raw_data() this returns the presets flattened, as provided by a theme.
5265       * This also uses appearanceTools instead of their opt-ins if all of them are true.
5266       *
5267       * @since 6.0.0
5268       *
5269       * @return array
5270       */
5271  	public function get_data() {
5272          $output = $this->theme_json;
5273          $nodes  = static::get_setting_nodes( $output );
5274  
5275          /**
5276           * Flatten the theme & custom origins into a single one.
5277           *
5278           * For example, the following:
5279           *
5280           * {
5281           *   "settings": {
5282           *     "color": {
5283           *       "palette": {
5284           *         "theme": [ {} ],
5285           *         "custom": [ {} ]
5286           *       }
5287           *     }
5288           *   }
5289           * }
5290           *
5291           * will be converted to:
5292           *
5293           * {
5294           *   "settings": {
5295           *     "color": {
5296           *       "palette": [ {} ]
5297           *     }
5298           *   }
5299           * }
5300           */
5301          foreach ( $nodes as $node ) {
5302              foreach ( static::PRESETS_METADATA as $preset_metadata ) {
5303                  $path = $node['path'];
5304                  foreach ( $preset_metadata['path'] as $preset_metadata_path ) {
5305                      $path[] = $preset_metadata_path;
5306                  }
5307                  $preset = _wp_array_get( $output, $path, null );
5308                  if ( null === $preset ) {
5309                      continue;
5310                  }
5311  
5312                  $items = array();
5313                  if ( isset( $preset['theme'] ) ) {
5314                      foreach ( $preset['theme'] as $item ) {
5315                          $slug = $item['slug'];
5316                          unset( $item['slug'] );
5317                          $items[ $slug ] = $item;
5318                      }
5319                  }
5320                  if ( isset( $preset['custom'] ) ) {
5321                      foreach ( $preset['custom'] as $item ) {
5322                          $slug = $item['slug'];
5323                          unset( $item['slug'] );
5324                          $items[ $slug ] = $item;
5325                      }
5326                  }
5327                  $flattened_preset = array();
5328                  foreach ( $items as $slug => $value ) {
5329                      $flattened_preset[] = array_merge( array( 'slug' => (string) $slug ), $value );
5330                  }
5331                  _wp_array_set( $output, $path, $flattened_preset );
5332              }
5333          }
5334  
5335          /*
5336           * If all of the static::APPEARANCE_TOOLS_OPT_INS are true,
5337           * this code unsets them and sets 'appearanceTools' instead.
5338           */
5339          foreach ( $nodes as $node ) {
5340              $all_opt_ins_are_set = true;
5341              foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
5342                  $full_path = $node['path'];
5343                  foreach ( $opt_in_path as $opt_in_path_item ) {
5344                      $full_path[] = $opt_in_path_item;
5345                  }
5346                  /*
5347                   * Use "unset prop" as a marker instead of "null" because
5348                   * "null" can be a valid value for some props (e.g. blockGap).
5349                   */
5350                  $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
5351                  if ( 'unset prop' === $opt_in_value ) {
5352                      $all_opt_ins_are_set = false;
5353                      break;
5354                  }
5355              }
5356  
5357              if ( $all_opt_ins_are_set ) {
5358                  $node_path_with_appearance_tools   = $node['path'];
5359                  $node_path_with_appearance_tools[] = 'appearanceTools';
5360                  _wp_array_set( $output, $node_path_with_appearance_tools, true );
5361                  foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
5362                      $full_path = $node['path'];
5363                      foreach ( $opt_in_path as $opt_in_path_item ) {
5364                          $full_path[] = $opt_in_path_item;
5365                      }
5366                      /*
5367                       * Use "unset prop" as a marker instead of "null" because
5368                       * "null" can be a valid value for some props (e.g. blockGap).
5369                       */
5370                      $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' );
5371                      if ( true !== $opt_in_value ) {
5372                          continue;
5373                      }
5374  
5375                      /*
5376                       * The following could be improved to be path independent.
5377                       * At the moment it relies on a couple of assumptions:
5378                       *
5379                       * - all opt-ins having a path of size 2.
5380                       * - there's two sources of settings: the top-level and the block-level.
5381                       */
5382                      if (
5383                          ( 1 === count( $node['path'] ) ) &&
5384                          ( 'settings' === $node['path'][0] )
5385                      ) {
5386                          // Top-level settings.
5387                          unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] );
5388                          if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) {
5389                              unset( $output['settings'][ $opt_in_path[0] ] );
5390                          }
5391                      } elseif (
5392                          ( 3 === count( $node['path'] ) ) &&
5393                          ( 'settings' === $node['path'][0] ) &&
5394                          ( 'blocks' === $node['path'][1] )
5395                      ) {
5396                          // Block-level settings.
5397                          $block_name = $node['path'][2];
5398                          unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] );
5399                          if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) {
5400                              unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] );
5401                          }
5402                      }
5403                  }
5404              }
5405          }
5406  
5407          wp_recursive_ksort( $output );
5408  
5409          return $output;
5410      }
5411  
5412      /**
5413       * Sets the spacingSizes array based on the spacingScale values from theme.json.
5414       *
5415       * @since 6.1.0
5416       * @deprecated 6.6.0 No longer used as the spacingSizes are automatically
5417       *                   generated in the constructor and merge methods instead
5418       *                   of manually after instantiation.
5419       *
5420       * @return void
5421       */
5422  	public function set_spacing_sizes() {
5423          _deprecated_function( __METHOD__, '6.6.0' );
5424  
5425          $spacing_scale = $this->theme_json['settings']['spacing']['spacingScale'] ?? array();
5426  
5427          if ( ! isset( $spacing_scale['steps'] )
5428              || ! is_numeric( $spacing_scale['steps'] )
5429              || ! isset( $spacing_scale['mediumStep'] )
5430              || ! isset( $spacing_scale['unit'] )
5431              || ! isset( $spacing_scale['operator'] )
5432              || ! isset( $spacing_scale['increment'] )
5433              || ! isset( $spacing_scale['steps'] )
5434              || ! is_numeric( $spacing_scale['increment'] )
5435              || ! is_numeric( $spacing_scale['mediumStep'] )
5436              || ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ) {
5437              if ( ! empty( $spacing_scale ) ) {
5438                  wp_trigger_error(
5439                      __METHOD__,
5440                      sprintf(
5441                          /* translators: 1: theme.json, 2: settings.spacing.spacingScale */
5442                          __( 'Some of the %1$s %2$s values are invalid' ),
5443                          'theme.json',
5444                          'settings.spacing.spacingScale'
5445                      ),
5446                      E_USER_NOTICE
5447                  );
5448              }
5449              return;
5450          }
5451  
5452          // If theme authors want to prevent the generation of the core spacing scale they can set their theme.json spacingScale.steps to 0.
5453          if ( 0 === $spacing_scale['steps'] ) {
5454              return;
5455          }
5456  
5457          $spacing_sizes = self::compute_spacing_sizes( $spacing_scale );
5458  
5459          // If there are 7 or fewer steps in the scale revert to numbers for labels instead of t-shirt sizes.
5460          if ( $spacing_scale['steps'] <= 7 ) {
5461              for ( $spacing_sizes_count = 0, $spacing_sizes_length = count( $spacing_sizes ); $spacing_sizes_count < $spacing_sizes_length; $spacing_sizes_count++ ) {
5462                  $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 );
5463              }
5464          }
5465  
5466          _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes );
5467      }
5468  
5469      /**
5470       * Merges two sets of spacing size presets.
5471       *
5472       * @since 6.6.0
5473       *
5474       * @param array $base     The base set of spacing sizes.
5475       * @param array $incoming The set of spacing sizes to merge with the base. Duplicate slugs will override the base values.
5476       * @return array The merged set of spacing sizes.
5477       */
5478  	private static function merge_spacing_sizes( $base, $incoming ) {
5479          // Preserve the order if there are no base (spacingScale) values.
5480          if ( empty( $base ) ) {
5481              return $incoming;
5482          }
5483          $merged = array();
5484          foreach ( $base as $item ) {
5485              $merged[ $item['slug'] ] = $item;
5486          }
5487          foreach ( $incoming as $item ) {
5488              $merged[ $item['slug'] ] = $item;
5489          }
5490          ksort( $merged, SORT_NUMERIC );
5491          return array_values( $merged );
5492      }
5493  
5494      /**
5495       * Generates a set of spacing sizes by starting with a medium size and
5496       * applying an operator with an increment value to generate the rest of the
5497       * sizes outward from the medium size. The medium slug is '50' with the rest
5498       * of the slugs being 10 apart. The generated names use t-shirt sizing.
5499       *
5500       * Example:
5501       *
5502       *     $spacing_scale = array(
5503       *         'steps'      => 4,
5504       *         'mediumStep' => 16,
5505       *         'unit'       => 'px',
5506       *         'operator'   => '+',
5507       *         'increment'  => 2,
5508       *     );
5509       *     $spacing_sizes = self::compute_spacing_sizes( $spacing_scale );
5510       *     // -> array(
5511       *     //        array( 'name' => 'Small',   'slug' => '40', 'size' => '14px' ),
5512       *     //        array( 'name' => 'Medium',  'slug' => '50', 'size' => '16px' ),
5513       *     //        array( 'name' => 'Large',   'slug' => '60', 'size' => '18px' ),
5514       *     //        array( 'name' => 'X-Large', 'slug' => '70', 'size' => '20px' ),
5515       *     //    )
5516       *
5517       * @since 6.6.0
5518       *
5519       * @param array $spacing_scale {
5520       *      The spacing scale values. All are required.
5521       *
5522       *      @type int    $steps      The number of steps in the scale. (up to 10 steps are supported.)
5523       *      @type float  $mediumStep The middle value that gets the slug '50'. (For even number of steps, this becomes the first middle value.)
5524       *      @type string $unit       The CSS unit to use for the sizes.
5525       *      @type string $operator   The mathematical operator to apply to generate the other sizes. Either '+' or '*'.
5526       *      @type float  $increment  The value used with the operator to generate the other sizes.
5527       * }
5528       * @return array The spacing sizes presets or an empty array if some spacing scale values are missing or invalid.
5529       */
5530  	private static function compute_spacing_sizes( $spacing_scale ) {
5531          /*
5532           * This condition is intentionally missing some checks on ranges for the values in order to
5533           * keep backwards compatibility with the previous implementation.
5534           */
5535          if (
5536              ! isset( $spacing_scale['steps'] ) ||
5537              ! is_numeric( $spacing_scale['steps'] ) ||
5538              0 === $spacing_scale['steps'] ||
5539              ! isset( $spacing_scale['mediumStep'] ) ||
5540              ! is_numeric( $spacing_scale['mediumStep'] ) ||
5541              ! isset( $spacing_scale['unit'] ) ||
5542              ! isset( $spacing_scale['operator'] ) ||
5543              ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ||
5544              ! isset( $spacing_scale['increment'] ) ||
5545              ! is_numeric( $spacing_scale['increment'] )
5546          ) {
5547              return array();
5548          }
5549  
5550          $unit            = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] );
5551          $current_step    = $spacing_scale['mediumStep'];
5552          $steps_mid_point = round( $spacing_scale['steps'] / 2, 0 );
5553          $x_small_count   = null;
5554          $below_sizes     = array();
5555          $slug            = 40;
5556          $remainder       = 0;
5557  
5558          for ( $below_midpoint_count = $steps_mid_point - 1; $spacing_scale['steps'] > 1 && $slug > 0 && $below_midpoint_count > 0; $below_midpoint_count-- ) {
5559              if ( '+' === $spacing_scale['operator'] ) {
5560                  $current_step -= $spacing_scale['increment'];
5561              } elseif ( $spacing_scale['increment'] > 1 ) {
5562                  $current_step /= $spacing_scale['increment'];
5563              } else {
5564                  $current_step *= $spacing_scale['increment'];
5565              }
5566  
5567              if ( $current_step <= 0 ) {
5568                  $remainder = $below_midpoint_count;
5569                  break;
5570              }
5571  
5572              $below_sizes[] = array(
5573                  /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Small. */
5574                  'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small' ) : sprintf( __( '%sX-Small' ), (string) $x_small_count ),
5575                  'slug' => (string) $slug,
5576                  'size' => round( $current_step, 2 ) . $unit,
5577              );
5578  
5579              if ( $below_midpoint_count === $steps_mid_point - 2 ) {
5580                  $x_small_count = 2;
5581              }
5582  
5583              if ( $below_midpoint_count < $steps_mid_point - 2 ) {
5584                  ++$x_small_count;
5585              }
5586  
5587              $slug -= 10;
5588          }
5589  
5590          $below_sizes = array_reverse( $below_sizes );
5591  
5592          $below_sizes[] = array(
5593              'name' => __( 'Medium' ),
5594              'slug' => '50',
5595              'size' => $spacing_scale['mediumStep'] . $unit,
5596          );
5597  
5598          $current_step  = $spacing_scale['mediumStep'];
5599          $x_large_count = null;
5600          $above_sizes   = array();
5601          $slug          = 60;
5602          $steps_above   = ( $spacing_scale['steps'] - $steps_mid_point ) + $remainder;
5603  
5604          for ( $above_midpoint_count = 0; $above_midpoint_count < $steps_above; $above_midpoint_count++ ) {
5605              $current_step = '+' === $spacing_scale['operator']
5606                  ? $current_step + $spacing_scale['increment']
5607                  : ( $spacing_scale['increment'] >= 1 ? $current_step * $spacing_scale['increment'] : $current_step / $spacing_scale['increment'] );
5608  
5609              $above_sizes[] = array(
5610                  /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Large. */
5611                  'name' => 0 === $above_midpoint_count ? __( 'Large' ) : sprintf( __( '%sX-Large' ), (string) $x_large_count ),
5612                  'slug' => (string) $slug,
5613                  'size' => round( $current_step, 2 ) . $unit,
5614              );
5615  
5616              if ( 1 === $above_midpoint_count ) {
5617                  $x_large_count = 2;
5618              }
5619  
5620              if ( $above_midpoint_count > 1 ) {
5621                  ++$x_large_count;
5622              }
5623  
5624              $slug += 10;
5625          }
5626  
5627          $spacing_sizes = $below_sizes;
5628          foreach ( $above_sizes as $above_sizes_item ) {
5629              $spacing_sizes[] = $above_sizes_item;
5630          }
5631  
5632          return $spacing_sizes;
5633      }
5634  
5635      /**
5636       * This is used to convert the internal representation of variables to the CSS representation.
5637       * For example, `var:preset|color|vivid-green-cyan` becomes `var(--wp--preset--color--vivid-green-cyan)`.
5638       *
5639       * @since 6.3.0
5640       * @since 7.2.0 Preset reference slugs are kebab-cased to match the generated custom properties.
5641       *
5642       * @param string $value The variable such as var:preset|color|vivid-green-cyan to convert.
5643       * @return string The converted variable.
5644       */
5645  	private static function convert_custom_properties( $value ) {
5646          $prefix     = 'var:';
5647          $prefix_len = strlen( $prefix );
5648          $token_in   = '|';
5649          $token_out  = '--';
5650          if ( str_starts_with( $value, $prefix ) ) {
5651              $parts = explode( $token_in, substr( $value, $prefix_len ) );
5652  
5653              /*
5654               * The slug of a preset reference is kebab-cased so the resulting
5655               * custom property matches the one generated from the preset,
5656               * whose slug is also kebab-cased (see `get_settings_values_by_slug()`).
5657               * For slugs that are not already kebab-cased (e.g. `n27`), a verbatim
5658               * conversion produces a reference to a custom property that does
5659               * not exist (`--wp--preset--font-family--n27` instead of the
5660               * generated `--wp--preset--font-family--n-27`).
5661               *
5662               * Duotone is the exception: its custom properties are generated by
5663               * `WP_Duotone` from the presets it registers in
5664               * `get_all_global_styles_presets()`. Duotone references are
5665               * kebab-cased all the same: the editor and the JS style engine
5666               * kebab-case the references of every preset type, and
5667               * `WP_Duotone` looks up presets by kebab-cased filter ID.
5668               */
5669              if ( 3 === count( $parts ) && 'preset' === $parts[0] ) {
5670                  $parts[2] = _wp_to_kebab_case( $parts[2] );
5671              }
5672  
5673              $value = 'var(--wp--' . implode( $token_out, $parts ) . ')';
5674          }
5675  
5676          return $value;
5677      }
5678  
5679      /**
5680       * Given a tree, converts the internal representation of variables to the CSS representation.
5681       * It is recursive and modifies the input in-place.
5682       *
5683       * @since 6.3.0
5684       *
5685       * @param array $tree Input to process.
5686       * @return array The modified $tree.
5687       */
5688  	private static function resolve_custom_css_format( $tree ) {
5689          $prefix = 'var:';
5690  
5691          foreach ( $tree as $key => $data ) {
5692              if ( is_string( $data ) && str_starts_with( $data, $prefix ) ) {
5693                  $tree[ $key ] = self::convert_custom_properties( $data );
5694              } elseif ( is_array( $data ) ) {
5695                  $tree[ $key ] = self::resolve_custom_css_format( $data );
5696              }
5697          }
5698  
5699          return $tree;
5700      }
5701  
5702      /**
5703       * Returns the selectors metadata for a block.
5704       *
5705       * @since 6.3.0
5706       *
5707       * @param object $block_type    The block type.
5708       * @param string $root_selector The block's root selector.
5709       * @return array The custom selectors set by the block.
5710       */
5711  	protected static function get_block_selectors( $block_type, $root_selector ) {
5712          if ( ! empty( $block_type->selectors ) ) {
5713              return $block_type->selectors;
5714          }
5715  
5716          $selectors = array( 'root' => $root_selector );
5717          foreach ( static::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS as $key => $feature ) {
5718              $feature_selector = wp_get_block_css_selector( $block_type, $key );
5719              if ( null !== $feature_selector ) {
5720                  $selectors[ $feature ] = array( 'root' => $feature_selector );
5721              }
5722          }
5723  
5724          return $selectors;
5725      }
5726  
5727      /**
5728       * Generates all the element selectors for a block.
5729       *
5730       * @since 6.3.0
5731       *
5732       * @param string $root_selector The block's root CSS selector.
5733       * @return array The block's element selectors.
5734       */
5735  	protected static function get_block_element_selectors( $root_selector ) {
5736          /*
5737           * Assign defaults, then override those that the block sets by itself.
5738           * If the block selector is compounded, will append the element to each
5739           * individual block selector.
5740           */
5741          $block_selectors   = explode( ',', $root_selector );
5742          $element_selectors = array();
5743          foreach ( static::ELEMENTS as $el_name => $el_selector ) {
5744              $element_selector = array();
5745              foreach ( $block_selectors as $selector ) {
5746                  if ( $selector === $el_selector ) {
5747                      $element_selector = array( $el_selector );
5748                      break;
5749                  }
5750                  $element_selector[] = static::prepend_to_selector( $el_selector, $selector . ' ' );
5751              }
5752              $element_selectors[ $el_name ] = implode( ',', $element_selector );
5753          }
5754  
5755          return $element_selectors;
5756      }
5757  
5758      /**
5759       * Generates style declarations for a node's features e.g., color, border,
5760       * typography etc. that have custom selectors in their related block's
5761       * metadata.
5762       *
5763       * @since 6.3.0
5764       *
5765       * @param array $metadata The related block metadata containing selectors.
5766       * @param array $node     A merged theme.json node for block or variation. Features
5767       *                        promoted to their own selector are removed from it.
5768       * @return array The style declarations for the node's features with custom
5769       *               selectors.
5770       */
5771  	protected function get_feature_declarations_for_node( $metadata, &$node ) {
5772          $declarations = array();
5773  
5774          if ( ! isset( $metadata['selectors'] ) ) {
5775              return $declarations;
5776          }
5777  
5778          $settings = $this->theme_json['settings'] ?? array();
5779  
5780          foreach ( $metadata['selectors'] as $feature => $feature_selectors ) {
5781              /*
5782               * Skip if this is the block's root selector, the custom CSS
5783               * selector, or the block doesn't have any styles for the feature.
5784               */
5785              if ( 'root' === $feature || 'css' === $feature || empty( $node[ $feature ] ) ) {
5786                  continue;
5787              }
5788  
5789              if ( is_array( $feature_selectors ) ) {
5790                  foreach ( $feature_selectors as $subfeature => $subfeature_selector ) {
5791                      if ( 'root' === $subfeature || empty( $node[ $feature ][ $subfeature ] ) ) {
5792                          continue;
5793                      }
5794  
5795                      /*
5796                       * Create temporary node containing only the subfeature data
5797                       * to leverage existing `compute_style_properties` function.
5798                       */
5799                      $subfeature_node = array(
5800                          $feature => array(
5801                              $subfeature => $node[ $feature ][ $subfeature ],
5802                          ),
5803                      );
5804  
5805                      // Generate style declarations.
5806                      $new_declarations = static::compute_style_properties( $subfeature_node, $settings, null, $this->theme_json );
5807  
5808                      // Merge subfeature declarations into feature declarations.
5809                      if ( isset( $declarations[ $subfeature_selector ] ) ) {
5810                          foreach ( $new_declarations as $new_declaration ) {
5811                              $declarations[ $subfeature_selector ][] = $new_declaration;
5812                          }
5813                      } else {
5814                          $declarations[ $subfeature_selector ] = $new_declarations;
5815                      }
5816  
5817                      /*
5818                       * Remove the subfeature from the block's node now its
5819                       * styles will be included under its own selector not the
5820                       * block's.
5821                       */
5822                      unset( $node[ $feature ][ $subfeature ] );
5823                  }
5824              }
5825  
5826              /*
5827               * Now subfeatures have been processed and removed we can process
5828               * feature root selector or simple string selector.
5829               */
5830              if (
5831                  is_string( $feature_selectors ) ||
5832                  ( isset( $feature_selectors['root'] ) && $feature_selectors['root'] )
5833              ) {
5834                  $feature_selector = is_string( $feature_selectors ) ? $feature_selectors : $feature_selectors['root'];
5835  
5836                  /*
5837                   * Create temporary node containing only the feature data
5838                   * to leverage existing `compute_style_properties` function.
5839                   */
5840                  $feature_node = array( $feature => $node[ $feature ] );
5841  
5842                  // Generate the style declarations.
5843                  $new_declarations = static::compute_style_properties( $feature_node, $settings, null, $this->theme_json );
5844  
5845                  /*
5846                   * Merge new declarations with any that already exist for
5847                   * the feature selector. This may occur when multiple block
5848                   * support features use the same custom selector.
5849                   */
5850                  if ( isset( $declarations[ $feature_selector ] ) ) {
5851                      foreach ( $new_declarations as $new_declaration ) {
5852                          $declarations[ $feature_selector ][] = $new_declaration;
5853                      }
5854                  } else {
5855                      $declarations[ $feature_selector ] = $new_declarations;
5856                  }
5857  
5858                  /*
5859                   * Remove the feature from the block's node now its styles
5860                   * will be included under its own selector not the block's.
5861                   */
5862                  unset( $node[ $feature ] );
5863              }
5864          }
5865  
5866          return $declarations;
5867      }
5868  
5869      /**
5870       * Replaces CSS variables with their values in place.
5871       *
5872       * @since 6.3.0
5873       * @since 6.5.0 Check for empty style before processing its value.
5874       *
5875       * @param array $styles CSS declarations to convert.
5876       * @param array $values key => value pairs to use for replacement.
5877       * @return array
5878       */
5879  	private static function convert_variables_to_value( $styles, $values ) {
5880          foreach ( $styles as $key => $style ) {
5881              if ( empty( $style ) ) {
5882                  continue;
5883              }
5884  
5885              if ( is_array( $style ) ) {
5886                  $styles[ $key ] = self::convert_variables_to_value( $style, $values );
5887                  continue;
5888              }
5889  
5890              if ( 0 <= strpos( $style, 'var(' ) ) {
5891                  // find all the variables in the string in the form of var(--variable-name, fallback), with fallback in the second capture group.
5892  
5893                  $has_matches = preg_match_all( '/var\(([^),]+)?,?\s?(\S+)?\)/', $style, $var_parts );
5894  
5895                  if ( $has_matches ) {
5896                      $resolved_style = $styles[ $key ];
5897                      foreach ( $var_parts[1] as $index => $var_part ) {
5898                          $key_in_values   = 'var(' . $var_part . ')';
5899                          $rule_to_replace = $var_parts[0][ $index ]; // the css rule to replace e.g. var(--wp--preset--color--vivid-green-cyan).
5900                          $fallback        = $var_parts[2][ $index ]; // the fallback value.
5901                          $resolved_style  = str_replace(
5902                              array(
5903                                  $rule_to_replace,
5904                                  $fallback,
5905                              ),
5906                              array(
5907                                  $values[ $key_in_values ] ?? $rule_to_replace,
5908                                  $values[ $fallback ] ?? $fallback,
5909                              ),
5910                              $resolved_style
5911                          );
5912                      }
5913                      $styles[ $key ] = $resolved_style;
5914                  }
5915              }
5916          }
5917  
5918          return $styles;
5919      }
5920  
5921      /**
5922       * Resolves the values of CSS variables in the given styles.
5923       *
5924       * @since 6.3.0
5925       *
5926       * @param WP_Theme_JSON $theme_json The theme json resolver.
5927       * @return WP_Theme_JSON The $theme_json with resolved variables.
5928       */
5929  	public static function resolve_variables( $theme_json ) {
5930          $settings    = $theme_json->get_settings();
5931          $styles      = $theme_json->get_raw_data()['styles'];
5932          $preset_vars = static::compute_preset_vars( $settings, static::VALID_ORIGINS );
5933          $theme_vars  = static::compute_theme_vars( $settings );
5934          $vars        = array_reduce(
5935              array_merge( $preset_vars, $theme_vars ),
5936              function ( $carry, $item ) {
5937                  $name                    = $item['name'];
5938                  $carry[ "var({$name})" ] = $item['value'];
5939                  return $carry;
5940              },
5941              array()
5942          );
5943  
5944          $theme_json->theme_json['styles'] = self::convert_variables_to_value( $styles, $vars );
5945          return $theme_json;
5946      }
5947  
5948      /**
5949       * Generates a selector for a block style variation.
5950       *
5951       * @since 6.5.0
5952       *
5953       * @param string $variation_name Name of the block style variation.
5954       * @param string $block_selector CSS selector for the block.
5955       * @return string Block selector with block style variation selector added to it.
5956       */
5957  	protected static function get_block_style_variation_selector( $variation_name, $block_selector ) {
5958          $variation_class = ".is-style-$variation_name";
5959  
5960          if ( ! $block_selector ) {
5961              return $variation_class;
5962          }
5963  
5964          $limit          = 1;
5965          $selector_parts = static::split_selector_list( $block_selector );
5966          $result         = array();
5967  
5968          /*
5969           * Append the variation class to each selector's ancestor: the first
5970           * run of characters before any combinator (whitespace) or pseudo-class
5971           * (`:`). Only the first match is replaced.
5972           *
5973           * Examples ("custom" variation):
5974           * - `.wp-block`              => `.wp-block.is-style-custom`
5975           * - `.wp-block .inner`       => `.wp-block.is-style-custom .inner`
5976           * - `.wp-block:where(.a .b)` => `.wp-block.is-style-custom:where(.a .b)`
5977           * - `:where(.outer .inner)`  => `:where(.outer.is-style-custom .inner)`
5978           */
5979          foreach ( $selector_parts as $part ) {
5980              $result[] = preg_replace_callback(
5981                  '/[^\s:]+/',
5982                  function ( $matches ) use ( $variation_class ) {
5983                      return $matches[0] . $variation_class;
5984                  },
5985                  $part,
5986                  $limit
5987              );
5988          }
5989  
5990          return implode( ', ', $result );
5991      }
5992  
5993      /**
5994       * Applies a block style variation class to a feature selector.
5995       *
5996       * Feature selectors can target a different element than the block's root
5997       * selector. For example, the Button block's root selector targets the inner
5998       * link, while its dimensions width selector targets the outer wrapper. Apply
5999       * the variation class directly to the selector that will receive the
6000       * declarations instead of deriving it by subtracting the root selector from
6001       * the feature selector.
6002       *
6003       * @since 7.0.0
6004       *
6005       * @param array  $style_variation  Style variation metadata.
6006       * @param string $feature_selector CSS selector for the feature.
6007       * @return string Feature selector with block style variation selector added.
6008       */
6009  	protected static function get_block_style_variation_feature_selector( $style_variation, $feature_selector ) {
6010          $variation_path = $style_variation['path'] ?? array();
6011          $variation_name = $style_variation['name'] ?? ( is_array( $variation_path ) ? end( $variation_path ) : null );
6012  
6013          if ( ! $variation_name ) {
6014              return $style_variation['selector'] ?? $feature_selector;
6015          }
6016  
6017          $variation_class = ".is-style-$variation_name";
6018          $selector_parts  = static::split_selector_list( $feature_selector );
6019          $selector_parts  = array_map(
6020              static function ( $selector ) use ( $variation_class ) {
6021                  $prefix = $variation_class . ' ';
6022  
6023                  if ( str_starts_with( $selector, $prefix ) ) {
6024                      return substr( $selector, strlen( $prefix ) );
6025                  }
6026  
6027                  return $selector;
6028              },
6029              $selector_parts
6030          );
6031  
6032          return static::get_block_style_variation_selector(
6033              $variation_name,
6034              implode( ', ', $selector_parts )
6035          );
6036      }
6037  
6038      /**
6039       * Collects valid block style variations keyed by block type.
6040       *
6041       * @since 6.6.0
6042       * @since 6.8.0 Added the `$blocks_metadata` parameter.
6043       *
6044       * @param array $blocks_metadata Optional. List of metadata per block. Default is the metadata for all blocks.
6045       * @return array Valid block style variations by block type.
6046       */
6047  	protected static function get_valid_block_style_variations( $blocks_metadata = array() ) {
6048          $valid_variations = array();
6049          $blocks_metadata  = empty( $blocks_metadata ) ? static::get_blocks_metadata() : $blocks_metadata;
6050          foreach ( $blocks_metadata as $block_name => $block_meta ) {
6051              if ( ! isset( $block_meta['styleVariations'] ) ) {
6052                  continue;
6053              }
6054              $valid_variations[ $block_name ] = array_keys( $block_meta['styleVariations'] );
6055          }
6056  
6057          return $valid_variations;
6058      }
6059  
6060      /**
6061       * Extracts the block name from the block metadata path.
6062       *
6063       * @since 7.1.0
6064       *
6065       * @param array $block_metadata Block metadata.
6066       * @return string|null The block name or null if not found.
6067       */
6068  	private static function get_block_name_from_metadata_path( $block_metadata ) {
6069          return $block_metadata['path'][2] ?? null;
6070      }
6071  }


Generated : Wed Sep 23 08:20:35 2026 Cross-referenced by PHPXref