+ * $scope = '.a, .b .c';
+ * $selector = '> .x, .y';
+ * $merged = scope_selector( $scope, $selector );
+ * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
+ *
+ *
+ * @since 5.9.0
+ *
+ * @param string $scope Selector to scope to.
+ * @param string $selector Original selector.
+ * @return string Scoped selector.
+ */
+ protected static function scope_selector( $scope, $selector ) {
+ $scopes = explode( ',', $scope );
+ $selectors = explode( ',', $selector );
+
+ $selectors_scoped = array();
+ foreach ( $scopes as $outer ) {
+ foreach ( $selectors as $inner ) {
+ $outer = trim( $outer );
+ $inner = trim( $inner );
+ if ( ! empty( $outer ) && ! empty( $inner ) ) {
+ $selectors_scoped[] = $outer . ' ' . $inner;
+ } elseif ( empty( $outer ) ) {
+ $selectors_scoped[] = $inner;
+ } elseif ( empty( $inner ) ) {
+ $selectors_scoped[] = $outer;
+ }
+ }
+ }
+
+ $result = implode( ', ', $selectors_scoped );
+ return $result;
+ }
+
+ /**
+ * Gets preset values keyed by slugs based on settings and metadata.
+ *
+ *
+ * $settings = array(
+ * 'typography' => array(
+ * 'fontFamilies' => array(
+ * array(
+ * 'slug' => 'sansSerif',
+ * 'fontFamily' => '"Helvetica Neue", sans-serif',
+ * ),
+ * array(
+ * 'slug' => 'serif',
+ * 'colors' => 'Georgia, serif',
+ * )
+ * ),
+ * ),
+ * );
+ * $meta = array(
+ * 'path' => array( 'typography', 'fontFamilies' ),
+ * 'value_key' => 'fontFamily',
+ * );
+ * $values_by_slug = get_settings_values_by_slug();
+ * // $values_by_slug === array(
+ * // 'sans-serif' => '"Helvetica Neue", sans-serif',
+ * // 'serif' => 'Georgia, serif',
+ * // );
+ *
+ *
+ * @since 5.9.0
+ *
+ * @param array $settings Settings to process.
+ * @param array $preset_metadata One of the PRESETS_METADATA values.
+ * @param array $origins List of origins to process.
+ * @return array Array of presets where each key is a slug and each value is the preset value.
+ */
+ protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) {
+ $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
+
+ $result = array();
+ foreach ( $origins as $origin ) {
+ if ( ! isset( $preset_per_origin[ $origin ] ) ) {
+ continue;
+ }
+ foreach ( $preset_per_origin[ $origin ] as $preset ) {
+ $slug = _wp_to_kebab_case( $preset['slug'] );
+
+ $value = '';
+ if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
+ $value_key = $preset_metadata['value_key'];
+ $value = $preset[ $value_key ];
+ } elseif (
+ isset( $preset_metadata['value_func'] ) &&
+ is_callable( $preset_metadata['value_func'] )
+ ) {
+ $value_func = $preset_metadata['value_func'];
+ $value = call_user_func( $value_func, $preset );
+ } else {
+ // If we don't have a value, then don't add it to the result.
+ continue;
+ }
+
+ $result[ $slug ] = $value;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Similar to get_settings_values_by_slug, but doesn't compute the value.
+ *
+ * @since 5.9.0
+ *
+ * @param array $settings Settings to process.
+ * @param array $preset_metadata One of the PRESETS_METADATA values.
+ * @param array $origins List of origins to process.
+ * @return array Array of presets where the key and value are both the slug.
+ */
+ protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) {
+ if ( null === $origins ) {
+ $origins = static::VALID_ORIGINS;
+ }
+
+ $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
+
+ $result = array();
+ foreach ( $origins as $origin ) {
+ if ( ! isset( $preset_per_origin[ $origin ] ) ) {
+ continue;
+ }
+ foreach ( $preset_per_origin[ $origin ] as $preset ) {
+ $slug = _wp_to_kebab_case( $preset['slug'] );
+
+ // Use the array as a set so we don't get duplicates.
+ $result[ $slug ] = $slug;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Transforms a slug into a CSS Custom Property.
+ *
+ * @since 5.9.0
+ *
+ * @param string $input String to replace.
+ * @param string $slug The slug value to use to generate the custom property.
+ * @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`.
+ */
+ protected static function replace_slug_in_string( $input, $slug ) {
+ return strtr( $input, array( '$slug' => $slug ) );
+ }
+
+ /**
+ * Given the block settings, extracts the CSS Custom Properties
+ * for the presets and adds them to the $declarations array
+ * following the format:
+ *
+ * ```php
+ * array(
+ * 'name' => 'property_name',
+ * 'value' => 'property_value,
+ * )
+ * ```
+ *
+ * @since 5.8.0
+ * @since 5.9.0 Added the `$origins` parameter.
+ *
+ * @param array $settings Settings to process.
+ * @param array $origins List of origins to process.
+ * @return array The modified $declarations.
+ */
+ protected static function compute_preset_vars( $settings, $origins ) {
+ $declarations = array();
+ foreach ( static::PRESETS_METADATA as $preset_metadata ) {
+ $values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins );
+ foreach ( $values_by_slug as $slug => $value ) {
+ $declarations[] = array(
+ 'name' => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
+ 'value' => $value,
+ );
+ }
+ }
+
+ return $declarations;
+ }
+
+ /**
+ * Given an array of settings, extracts the CSS Custom Properties
+ * for the custom values and adds them to the $declarations
+ * array following the format:
+ *
+ * ```php
+ * array(
+ * 'name' => 'property_name',
+ * 'value' => 'property_value,
+ * )
+ * ```
+ *
+ * @since 5.8.0
+ *
+ * @param array $settings Settings to process.
+ * @return array The modified $declarations.
+ */
+ protected static function compute_theme_vars( $settings ) {
+ $declarations = array();
+ $custom_values = _wp_array_get( $settings, array( 'custom' ), array() );
+ $css_vars = static::flatten_tree( $custom_values );
+ foreach ( $css_vars as $key => $value ) {
+ $declarations[] = array(
+ 'name' => '--wp--custom--' . $key,
+ 'value' => $value,
+ );
+ }
+
+ return $declarations;
+ }
+
+ /**
+ * Given a tree, it creates a flattened one
+ * by merging the keys and binding the leaf values
+ * to the new keys.
+ *
+ * It also transforms camelCase names into kebab-case
+ * and substitutes '/' by '-'.
+ *
+ * This is thought to be useful to generate
+ * CSS Custom Properties from a tree,
+ * although there's nothing in the implementation
+ * of this function that requires that format.
+ *
+ * For example, assuming the given prefix is '--wp'
+ * and the token is '--', for this input tree:
+ *
+ * {
+ * 'some/property': 'value',
+ * 'nestedProperty': {
+ * 'sub-property': 'value'
+ * }
+ * }
+ *
+ * it'll return this output:
+ *
+ * {
+ * '--wp--some-property': 'value',
+ * '--wp--nested-property--sub-property': 'value'
+ * }
+ *
+ * @since 5.8.0
+ *
+ * @param array $tree Input tree to process.
+ * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string.
+ * @param string $token Optional. Token to use between levels. Default '--'.
+ * @return array The flattened tree.
+ */
+ protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
+ $result = array();
+ foreach ( $tree as $property => $value ) {
+ $new_key = $prefix . str_replace(
+ '/',
+ '-',
+ strtolower( _wp_to_kebab_case( $property ) )
+ );
+
+ if ( is_array( $value ) ) {
+ $new_prefix = $new_key . $token;
+ $flattened_subtree = static::flatten_tree( $value, $new_prefix, $token );
+ foreach ( $flattened_subtree as $subtree_key => $subtree_value ) {
+ $result[ $subtree_key ] = $subtree_value;
+ }
+ } else {
+ $result[ $new_key ] = $value;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Given a styles array, it extracts the style properties
+ * and adds them to the $declarations array following the format:
+ *
+ * ```php
+ * array(
+ * 'name' => 'property_name',
+ * 'value' => 'property_value,
+ * )
+ * ```
+ *
+ * @since 5.8.0
+ * @since 5.9.0 Added the `$settings` and `$properties` parameters.
+ * @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters.
+ *
+ * @param array $styles Styles to process.
+ * @param array $settings Theme settings.
+ * @param array $properties Properties metadata.
+ * @param array $theme_json Theme JSON array.
+ * @param string $selector The style block selector.
+ * @param boolean $use_root_padding Whether to add custom properties at root level.
+ * @return array Returns the modified $declarations.
+ */
+ protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
+ if ( null === $properties ) {
+ $properties = static::PROPERTIES_METADATA;
+ }
+
+ $declarations = array();
+ if ( empty( $styles ) ) {
+ return $declarations;
+ }
+
+ $root_variable_duplicates = array();
+
+ foreach ( $properties as $css_property => $value_path ) {
+ $value = static::get_property_value( $styles, $value_path, $theme_json );
+
+ if ( str_starts_with( $css_property, '--wp--style--root--' ) && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) {
+ continue;
+ }
+ // Root-level padding styles don't currently support strings with CSS shorthand values.
+ // This may change: https://github.com/WordPress/gutenberg/issues/40132.
+ if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) {
+ continue;
+ }
+
+ if ( str_starts_with( $css_property, '--wp--style--root--' ) && $use_root_padding ) {
+ $root_variable_duplicates[] = substr( $css_property, strlen( '--wp--style--root--' ) );
+ }
+
+ // Look up protected properties, keyed by value path.
+ // Skip protected properties that are explicitly set to `null`.
+ if ( is_array( $value_path ) ) {
+ $path_string = implode( '.', $value_path );
+ if (
+ // TODO: Replace array_key_exists() with isset() check once WordPress drops
+ // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
+ array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) &&
+ _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null
+ ) {
+ continue;
+ }
+ }
+
+ // Skip if empty and not "0" or value represents array of longhand values.
+ $has_missing_value = empty( $value ) && ! is_numeric( $value );
+ if ( $has_missing_value || is_array( $value ) ) {
+ continue;
+ }
+
+ // Calculates fluid typography rules where available.
+ if ( 'font-size' === $css_property ) {
+ /*
+ * wp_get_typography_font_size_value() will check
+ * if fluid typography has been activated and also
+ * whether the incoming value can be converted to a fluid value.
+ * Values that already have a clamp() function will not pass the test,
+ * and therefore the original $value will be returned.
+ */
+ $value = gutenberg_get_typography_font_size_value( array( 'size' => $value ) );
+ }
+
+ $declarations[] = array(
+ 'name' => $css_property,
+ 'value' => $value,
+ );
+ }
+
+ // If a variable value is added to the root, the corresponding property should be removed.
+ foreach ( $root_variable_duplicates as $duplicate ) {
+ $discard = array_search( $duplicate, array_column( $declarations, 'name' ), true );
+ if ( is_numeric( $discard ) ) {
+ array_splice( $declarations, $discard, 1 );
+ }
+ }
+
+ return $declarations;
+ }
+
+ /**
+ * Returns the style property for the given path.
+ *
+ * It also converts CSS Custom Property stored as
+ * "var:preset|color|secondary" to the form
+ * "--wp--preset--color--secondary".
+ *
+ * It also converts references to a path to the value
+ * stored at that location, e.g.
+ * { "ref": "style.color.background" } => "#fff".
+ *
+ * @since 5.8.0
+ * @since 5.9.0 Added support for values of array type, which are returned as is.
+ * @since 6.1.0 Added the `$theme_json` parameter.
+ *
+ * @param array $styles Styles subtree.
+ * @param array $path Which property to process.
+ * @param array $theme_json Theme JSON array.
+ * @return string|array Style property value.
+ */
+ protected static function get_property_value( $styles, $path, $theme_json = null ) {
+ $value = _wp_array_get( $styles, $path, '' );
+
+ // Gutenberg didn't have this check.
+ if ( '' === $value || null === $value ) {
+ // No need to process the value further.
+ return '';
+ }
+
+ /*
+ * This converts references to a path to the value at that path
+ * where the values is an array with a "ref" key, pointing to a path.
+ * For example: { "ref": "style.color.background" } => "#fff".
+ */
+ if ( is_array( $value ) && isset( $value['ref'] ) ) {
+ $value_path = explode( '.', $value['ref'] );
+ $ref_value = _wp_array_get( $theme_json, $value_path );
+ // Only use the ref value if we find anything.
+ if ( ! empty( $ref_value ) && is_string( $ref_value ) ) {
+ $value = $ref_value;
+ }
+
+ if ( is_array( $ref_value ) && isset( $ref_value['ref'] ) ) {
+ $path_string = json_encode( $path );
+ $ref_value_string = json_encode( $ref_value );
+ _doing_it_wrong(
+ 'get_property_value',
+ sprintf(
+ /* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */
+ __( '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.', 'gutenberg' ),
+ 'theme.json',
+ $ref_value_string,
+ $path_string,
+ $ref_value['ref']
+ ),
+ '6.1.0'
+ );
+ }
+ }
+
+ if ( is_array( $value ) ) {
+ return $value;
+ }
+
+ // Convert custom CSS properties.
+ $prefix = 'var:';
+ $prefix_len = strlen( $prefix );
+ $token_in = '|';
+ $token_out = '--';
+ if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) {
+ $unwrapped_name = str_replace(
+ $token_in,
+ $token_out,
+ substr( $value, $prefix_len )
+ );
+ $value = "var(--wp--$unwrapped_name)";
+ }
+
+ return $value;
+ }
+
+ /**
+ * Builds metadata for the setting nodes, which returns in the form of:
+ *
+ * [
+ * [
+ * 'path' => ['path', 'to', 'some', 'node' ],
+ * 'selector' => 'CSS selector for some node'
+ * ],
+ * [
+ * 'path' => [ 'path', 'to', 'other', 'node' ],
+ * 'selector' => 'CSS selector for other node'
+ * ],
+ * ]
+ *
+ * @since 5.8.0
+ *
+ * @param array $theme_json The tree to extract setting nodes from.
+ * @param array $selectors List of selectors per block.
+ * @return array An array of setting nodes metadata.
+ */
+ protected static function get_setting_nodes( $theme_json, $selectors = array() ) {
+ $nodes = array();
+ if ( ! isset( $theme_json['settings'] ) ) {
+ return $nodes;
+ }
+
+ // Top-level.
+ $nodes[] = array(
+ 'path' => array( 'settings' ),
+ 'selector' => static::ROOT_BLOCK_SELECTOR,
+ );
+
+ // Calculate paths for blocks.
+ if ( ! isset( $theme_json['settings']['blocks'] ) ) {
+ return $nodes;
+ }
+
+ foreach ( $theme_json['settings']['blocks'] as $name => $node ) {
+ $selector = null;
+ if ( isset( $selectors[ $name ]['selector'] ) ) {
+ $selector = $selectors[ $name ]['selector'];
+ }
+
+ $nodes[] = array(
+ 'path' => array( 'settings', 'blocks', $name ),
+ 'selector' => $selector,
+ );
+ }
+
+ return $nodes;
+ }
+
+ /**
+ * Builds metadata for the style nodes, which returns in the form of:
+ *
+ * [
+ * [
+ * 'path' => [ 'path', 'to', 'some', 'node' ],
+ * 'selector' => 'CSS selector for some node',
+ * 'duotone' => 'CSS selector for duotone for some node'
+ * ],
+ * [
+ * 'path' => ['path', 'to', 'other', 'node' ],
+ * 'selector' => 'CSS selector for other node',
+ * 'duotone' => null
+ * ],
+ * ]
+ *
+ * @since 5.8.0
+ *
+ * @param array $theme_json The tree to extract style nodes from.
+ * @param array $selectors List of selectors per block.
+ * @return array An array of style nodes metadata.
+ */
+ protected static function get_style_nodes( $theme_json, $selectors = array() ) {
+ $nodes = array();
+ if ( ! isset( $theme_json['styles'] ) ) {
+ return $nodes;
+ }
+
+ // Top-level.
+ $nodes[] = array(
+ 'path' => array( 'styles' ),
+ 'selector' => static::ROOT_BLOCK_SELECTOR,
+ );
+
+ if ( isset( $theme_json['styles']['elements'] ) ) {
+ foreach ( self::ELEMENTS as $element => $selector ) {
+ if ( ! isset( $theme_json['styles']['elements'][ $element ] ) || ! array_key_exists( $element, static::ELEMENTS ) ) {
+ continue;
+ }
+
+ // Handle element defaults.
+ $nodes[] = array(
+ 'path' => array( 'styles', 'elements', $element ),
+ 'selector' => static::ELEMENTS[ $element ],
+ );
+
+ // Handle any pseudo selectors for the element.
+ // TODO: Replace array_key_exists() with isset() check once WordPress drops
+ // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
+ if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
+ foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
+
+ if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) {
+ $nodes[] = array(
+ 'path' => array( 'styles', 'elements', $element ),
+ 'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ),
+ );
+ }
+ }
+ }
+ }
+ }
+
+ // Blocks.
+ if ( ! isset( $theme_json['styles']['blocks'] ) ) {
+ return $nodes;
+ }
+
+ $block_nodes = static::get_block_nodes( $theme_json, $selectors );
+ foreach ( $block_nodes as $block_node ) {
+ $nodes[] = $block_node;
+ }
+
+ /**
+ * Filters the list of style nodes with metadata.
+ *
+ * This allows for things like loading block CSS independently.
+ *
+ * @since 6.1.0
+ *
+ * @param array $nodes Style nodes with metadata.
+ */
+ return apply_filters( 'wp_theme_json_get_style_nodes', $nodes );
+ }
+
+ /**
+ * A public helper to get the block nodes from a theme.json file.
+ *
+ * @since 6.1.0
+ *
+ * @return array The block nodes in theme.json.
+ */
+ public function get_styles_block_nodes() {
+ return static::get_block_nodes( $this->theme_json );
+ }
+
+ /**
+ * Returns a filtered declarations array if there is a separator block with only a background
+ * style defined in theme.json by adding a color attribute to reflect the changes in the front.
+ *
+ * @since 6.1.1
+ *
+ * @param array $declarations List of declarations.
+ * @return array $declarations List of declarations filtered.
+ */
+ private static function update_separator_declarations( $declarations ) {
+ // Gutenberg and core implementation differed.
+ // https://github.com/WordPress/gutenberg/pull/44943.
+ $background_color = '';
+ $border_color_matches = false;
+ $text_color_matches = false;
+
+ foreach ( $declarations as $declaration ) {
+ if ( 'background-color' === $declaration['name'] && ! $background_color && isset( $declaration['value'] ) ) {
+ $background_color = $declaration['value'];
+ } elseif ( 'border-color' === $declaration['name'] ) {
+ $border_color_matches = true;
+ } elseif ( 'color' === $declaration['name'] ) {
+ $text_color_matches = true;
+ }
+
+ if ( $background_color && $border_color_matches && $text_color_matches ) {
+ break;
+ }
+ }
+
+ if ( $background_color && ! $border_color_matches && ! $text_color_matches ) {
+ $declarations[] = array(
+ 'name' => 'color',
+ 'value' => $background_color,
+ );
+ }
+
+ return $declarations;
+ }
+
+ /**
+ * An internal method to get the block nodes from a theme.json file.
+ *
+ * @since 6.1.0
+ *
+ * @param array $theme_json The theme.json converted to an array.
+ * @param array $selectors Optional list of selectors per block.
+ * @return array The block nodes in theme.json.
+ */
+ private static function get_block_nodes( $theme_json, $selectors = array() ) {
+ $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
+ $nodes = array();
+ if ( ! isset( $theme_json['styles'] ) ) {
+ return $nodes;
+ }
+
+ // Blocks.
+ if ( ! isset( $theme_json['styles']['blocks'] ) ) {
+ return $nodes;
+ }
+
+ foreach ( $theme_json['styles']['blocks'] as $name => $node ) {
+ $selector = null;
+ if ( isset( $selectors[ $name ]['selector'] ) ) {
+ $selector = $selectors[ $name ]['selector'];
+ }
+
+ $duotone_selector = null;
+ if ( isset( $selectors[ $name ]['duotone'] ) ) {
+ $duotone_selector = $selectors[ $name ]['duotone'];
+ }
+
+ $feature_selectors = null;
+ if ( isset( $selectors[ $name ]['features'] ) ) {
+ $feature_selectors = $selectors[ $name ]['features'];
+ }
+
+ $nodes[] = array(
+ 'name' => $name,
+ 'path' => array( 'styles', 'blocks', $name ),
+ 'selector' => $selector,
+ 'duotone' => $duotone_selector,
+ 'features' => $feature_selectors,
+ );
+
+ if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
+ foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
+ $nodes[] = array(
+ 'path' => array( 'styles', 'blocks', $name, 'elements', $element ),
+ 'selector' => $selectors[ $name ]['elements'][ $element ],
+ );
+
+ // Handle any pseudo selectors for the element.
+ // TODO: Replace array_key_exists() with isset() check once WordPress drops
+ // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
+ if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
+ foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
+ if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
+ $nodes[] = array(
+ 'path' => array( 'styles', 'blocks', $name, 'elements', $element ),
+ 'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ),
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return $nodes;
+ }
+
+ /**
+ * Gets the CSS rules for a particular block from theme.json.
+ *
+ * @since 6.1.0
+ *
+ * @param array $block_metadata Metadata about the block to get styles for.
+ *
+ * @return string Styles for the block.
+ */
+ public function get_styles_for_block( $block_metadata ) {
+ $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
+ $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
+ $selector = $block_metadata['selector'];
+ $settings = _wp_array_get( $this->theme_json, array( 'settings' ) );
+
+ /*
+ * Process style declarations for block support features the current
+ * block contains selectors for. Values for a feature with a custom
+ * selector are filtered from the theme.json node before it is
+ * processed as normal.
+ */
+ $feature_declarations = array();
+
+ if ( ! empty( $block_metadata['features'] ) ) {
+ foreach ( $block_metadata['features'] as $feature_name => $feature_selector ) {
+ if ( ! empty( $node[ $feature_name ] ) ) {
+ // Create temporary node containing only the feature data
+ // to leverage existing `compute_style_properties` function.
+ $feature = array( $feature_name => $node[ $feature_name ] );
+ // Generate the feature's declarations only.
+ $new_feature_declarations = static::compute_style_properties( $feature, $settings, null, $this->theme_json );
+
+ // Merge new declarations with any that already exist for
+ // the feature selector. This may occur when multiple block
+ // support features use the same custom selector.
+ if ( isset( $feature_declarations[ $feature_selector ] ) ) {
+ foreach ( $new_feature_declarations as $new_feature_declaration ) {
+ $feature_declarations[ $feature_selector ][] = $new_feature_declaration;
+ }
+ } else {
+ $feature_declarations[ $feature_selector ] = $new_feature_declarations;
+ }
+
+ // Remove the feature from the block's node now the
+ // styles will be included under the feature level selector.
+ unset( $node[ $feature_name ] );
+ }
+ }
+ }
+
+ /*
+ * Get a reference to element name from path.
+ * $block_metadata['path'] = array( 'styles','elements','link' );
+ * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ].
+ * Skip non-element paths like just ['styles'].
+ */
+ $is_processing_element = in_array( 'elements', $block_metadata['path'], true );
+
+ $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null;
+
+ $element_pseudo_allowed = array();
+
+ // TODO: Replace array_key_exists() with isset() check once WordPress drops
+ // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
+ if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
+ $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ];
+ }
+
+ /*
+ * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover").
+ * This also resets the array keys.
+ */
+ $pseudo_matches = array_values(
+ array_filter(
+ $element_pseudo_allowed,
+ function( $pseudo_selector ) use ( $selector ) {
+ return str_contains( $selector, $pseudo_selector );
+ }
+ )
+ );
+
+ $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null;
+
+ /*
+ * If the current selector is a pseudo selector that's defined in the allow list for the current
+ * element then compute the style properties for it.
+ * Otherwise just compute the styles for the default selector as normal.
+ */
+ if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) &&
+ // TODO: Replace array_key_exists() with isset() check once WordPress drops
+ // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
+ array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS )
+ && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true )
+ ) {
+ $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding );
+ } else {
+ $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
+ }
+
+ $block_rules = '';
+
+ /*
+ * 1. Separate the declarations that use the general selector
+ * from the ones using the duotone selector.
+ */
+ $declarations_duotone = array();
+ foreach ( $declarations as $index => $declaration ) {
+ if ( 'filter' === $declaration['name'] ) {
+ unset( $declarations[ $index ] );
+ $declarations_duotone[] = $declaration;
+ }
+ }
+
+ // Update declarations if there are separators with only background color defined.
+ if ( '.wp-block-separator' === $selector ) {
+ $declarations = static::update_separator_declarations( $declarations );
+ }
+
+ // 2. Generate and append the rules that use the general selector.
+ $block_rules .= static::to_ruleset( $selector, $declarations );
+
+ // 3. Generate and append the rules that use the duotone selector.
+ if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
+ $selector_duotone = static::scope_selector( $block_metadata['selector'], $block_metadata['duotone'] );
+ $block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone );
+ }
+
+ // 4. Generate Layout block gap styles.
+ if (
+ static::ROOT_BLOCK_SELECTOR !== $selector &&
+ ! empty( $block_metadata['name'] )
+ ) {
+ $block_rules .= $this->get_layout_styles( $block_metadata );
+ }
+
+ // 5. Generate and append the feature level rulesets.
+ foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) {
+ $block_rules .= static::to_ruleset( $feature_selector, $individual_feature_declarations );
+ }
+
+ return $block_rules;
+ }
+
+ /**
+ * Outputs the CSS for layout rules on the root.
+ *
+ * @since 6.1.0
+ *
+ * @param string $selector The root node selector.
+ * @param array $block_metadata The metadata for the root block.
+ * @return string The additional root rules CSS.
+ */
+ public function get_root_layout_rules( $selector, $block_metadata ) {
+ $css = '';
+ $settings = _wp_array_get( $this->theme_json, array( 'settings' ) );
+ $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
+
+ /*
+ * Reset default browser margin on the root body element.
+ * This is set on the root selector **before** generating the ruleset
+ * from the `theme.json`. This is to ensure that if the `theme.json` declares
+ * `margin` in its `spacing` declaration for the `body` element then these
+ * user-generated values take precedence in the CSS cascade.
+ * @link https://github.com/WordPress/gutenberg/issues/36147.
+ */
+ $css .= 'body { margin: 0;';
+
+ /*
+ * If there are content and wide widths in theme.json, output them
+ * as custom properties on the body element so all blocks can use them.
+ */
+ if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) {
+ $content_size = isset( $settings['layout']['contentSize'] ) ? $settings['layout']['contentSize'] : $settings['layout']['wideSize'];
+ $content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial';
+ $wide_size = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize'];
+ $wide_size = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial';
+ $css .= '--wp--style--global--content-size: ' . $content_size . ';';
+ $css .= '--wp--style--global--wide-size: ' . $wide_size . ';';
+ }
+
+ $css .= '}';
+
+ if ( $use_root_padding ) {
+ // Top and bottom padding are applied to the outer block container.
+ $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }';
+ // Right and left padding are applied to the first container with `.has-global-padding` class.
+ $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
+ // Nested containers with `.has-global-padding` class do not get padding.
+ $css .= '.has-global-padding :where(.has-global-padding) { padding-right: 0; padding-left: 0; }';
+ // Alignfull children of the container with left and right padding have negative margins so they can still be full width.
+ $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); }';
+ // The above rule is negated for alignfull children of nested containers.
+ $css .= '.has-global-padding :where(.has-global-padding) > .alignfull { margin-right: 0; margin-left: 0; }';
+ // Some of the children of alignfull blocks without content width should also get padding: text blocks and non-alignfull container blocks.
+ $css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
+ // The above rule also has to be negated for blocks inside nested `.has-global-padding` blocks.
+ $css .= '.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }';
+ }
+
+ $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
+ $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
+ $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
+
+ $block_gap_value = _wp_array_get( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ), '0.5em' );
+ $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
+ if ( $has_block_gap_support ) {
+ $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) );
+ $css .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }';
+ $css .= ".wp-site-blocks > * + * { margin-block-start: $block_gap_value; }";
+
+ // For backwards compatibility, ensure the legacy block gap CSS variable is still available.
+ $css .= "$selector { --wp--style--block-gap: $block_gap_value; }";
+ }
+ $css .= $this->get_layout_styles( $block_metadata );
+
+ return $css;
+ }
+
+ /**
+ * For metadata values that can either be booleans or paths to booleans, gets the value.
+ *
+ * ```php
+ * $data = array(
+ * 'color' => array(
+ * 'defaultPalette' => true
+ * )
+ * );
+ *
+ * static::get_metadata_boolean( $data, false );
+ * // => false
+ *
+ * static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) );
+ * // => true
+ * ```
+ *
+ * @since 6.0.0
+ *
+ * @param array $data The data to inspect.
+ * @param bool|array $path Boolean or path to a boolean.
+ * @param bool $default Default value if the referenced path is missing.
+ * Default false.
+ * @return bool Value of boolean metadata.
+ */
+ protected static function get_metadata_boolean( $data, $path, $default = false ) {
+ if ( is_bool( $path ) ) {
+ return $path;
+ }
+
+ if ( is_array( $path ) ) {
+ $value = _wp_array_get( $data, $path );
+ if ( null !== $value ) {
+ return $value;
+ }
+ }
+
+ return $default;
+ }
+
+ /**
+ * Merges new incoming data.
+ *
+ * @since 5.8.0
+ * @since 5.9.0 Duotone preset also has origins.
+ *
+ * @param WP_Theme_JSON $incoming Data to merge.
+ */
+ public function merge( $incoming ) {
+ $incoming_data = $incoming->get_raw_data();
+ $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
+
+ /*
+ * The array_replace_recursive algorithm merges at the leaf level,
+ * but we don't want leaf arrays to be merged, so we overwrite it.
+ *
+ * For leaf values that are sequential arrays it will use the numeric indexes for replacement.
+ * We rather replace the existing with the incoming value, if it exists.
+ * This is the case of spacing.units.
+ *
+ * For leaf values that are associative arrays it will merge them as expected.
+ * This is also not the behavior we want for the current associative arrays (presets).
+ * We rather replace the existing with the incoming value, if it exists.
+ * This happens, for example, when we merge data from theme.json upon existing
+ * theme supports or when we merge anything coming from the same source twice.
+ * This is the case of color.palette, color.gradients, color.duotone,
+ * typography.fontSizes, or typography.fontFamilies.
+ *
+ * Additionally, for some preset types, we also want to make sure the
+ * values they introduce don't conflict with default values. We do so
+ * by checking the incoming slugs for theme presets and compare them
+ * with the equivalent default presets: if a slug is present as a default
+ * we remove it from the theme presets.
+ */
+ $nodes = static::get_setting_nodes( $incoming_data );
+ $slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) );
+ foreach ( $nodes as $node ) {
+ // Replace the spacing.units.
+ $path = $node['path'];
+ $path[] = 'spacing';
+ $path[] = 'units';
+
+ $content = _wp_array_get( $incoming_data, $path, null );
+ if ( isset( $content ) ) {
+ _wp_array_set( $this->theme_json, $path, $content );
+ }
+
+ // Replace the presets.
+ foreach ( static::PRESETS_METADATA as $preset ) {
+ $override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true );
+
+ foreach ( static::VALID_ORIGINS as $origin ) {
+ $base_path = $node['path'];
+ foreach ( $preset['path'] as $leaf ) {
+ $base_path[] = $leaf;
+ }
+
+ $path = $base_path;
+ $path[] = $origin;
- if ( $use_root_padding ) {
- // Top and bottom padding are applied to the outer block container.
- $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }';
- // Right and left padding are applied to the first container with `.has-global-padding` class.
- $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
- // Nested containers with `.has-global-padding` class do not get padding.
- $css .= '.has-global-padding :where(.has-global-padding) { padding-right: 0; padding-left: 0; }';
- // Alignfull children of the container with left and right padding have negative margins so they can still be full width.
- $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); }';
- // The above rule is negated for alignfull children of nested containers.
- $css .= '.has-global-padding :where(.has-global-padding) > .alignfull { margin-right: 0; margin-left: 0; }';
- // Some of the children of alignfull blocks without content width should also get padding: text blocks and non-alignfull container blocks.
- $css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
- // The above rule also has to be negated for blocks inside nested `.has-global-padding` blocks.
- $css .= '.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }';
+ $content = _wp_array_get( $incoming_data, $path, null );
+ if ( ! isset( $content ) ) {
+ continue;
+ }
+
+ if ( 'theme' === $origin && $preset['use_default_names'] ) {
+ foreach ( $content as $key => $item ) {
+ if ( ! isset( $item['name'] ) ) {
+ $name = static::get_name_from_defaults( $item['slug'], $base_path );
+ if ( null !== $name ) {
+ $content[ $key ]['name'] = $name;
+ }
+ }
+ }
+ }
+
+ if (
+ ( 'theme' !== $origin ) ||
+ ( 'theme' === $origin && $override_preset )
+ ) {
+ _wp_array_set( $this->theme_json, $path, $content );
+ } else {
+ $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] );
+ $slugs = array_merge_recursive( $slugs_global, $slugs_node );
+
+ $slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() );
+ $content = static::filter_slugs( $content, $slugs_for_preset );
+ _wp_array_set( $this->theme_json, $path, $content );
+ }
+ }
+ }
}
+ }
- $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
- $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
- $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
+ /**
+ * Converts all filter (duotone) presets into SVGs.
+ *
+ * @since 5.9.1
+ *
+ * @param array $origins List of origins to process.
+ * @return string SVG filters.
+ */
+ public function get_svg_filters( $origins ) {
+ $blocks_metadata = static::get_blocks_metadata();
+ $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
- $block_gap_value = _wp_array_get( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ), '0.5em' );
- $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
- if ( $has_block_gap_support ) {
- $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) );
- $css .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }';
- $css .= ".wp-site-blocks > * + * { margin-block-start: $block_gap_value; }";
+ $filters = '';
+ foreach ( $setting_nodes as $metadata ) {
+ $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
+ if ( empty( $node['color']['duotone'] ) ) {
+ continue;
+ }
- // For backwards compatibility, ensure the legacy block gap CSS variable is still available.
- $css .= "$selector { --wp--style--block-gap: $block_gap_value; }";
+ $duotone_presets = $node['color']['duotone'];
+
+ foreach ( $origins as $origin ) {
+ if ( ! isset( $duotone_presets[ $origin ] ) ) {
+ continue;
+ }
+ foreach ( $duotone_presets[ $origin ] as $duotone_preset ) {
+ $filters .= wp_get_duotone_filter_svg( $duotone_preset );
+ }
+ }
}
- $css .= $this->get_layout_styles( $block_metadata );
- return $css;
+ return $filters;
}
/**
- * Converts each style section into a list of rulesets
- * containing the block styles to be appended to the stylesheet.
+ * Determines whether a presets should be overridden or not.
*
- * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
+ * @since 5.9.0
+ * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead.
*
- * For each section this creates a new ruleset such as:
+ * @param array $theme_json The theme.json like structure to inspect.
+ * @param array $path Path to inspect.
+ * @param bool|array $override Data to compute whether to override the preset.
+ * @return boolean
+ */
+ protected static function should_override_preset( $theme_json, $path, $override ) {
+ _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' );
+
+ if ( is_bool( $override ) ) {
+ return $override;
+ }
+
+ /*
+ * The relationship between whether to override the defaults
+ * and whether the defaults are enabled is inverse:
+ *
+ * - If defaults are enabled => theme presets should not be overridden
+ * - If defaults are disabled => theme presets should be overridden
+ *
+ * For example, a theme sets defaultPalette to false,
+ * making the default palette hidden from the user.
+ * In that case, we want all the theme presets to be present,
+ * so they should override the defaults.
+ */
+ if ( is_array( $override ) ) {
+ $value = _wp_array_get( $theme_json, array_merge( $path, $override ) );
+ if ( isset( $value ) ) {
+ return ! $value;
+ }
+
+ // Search the top-level key if none was found for this node.
+ $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) );
+ if ( isset( $value ) ) {
+ return ! $value;
+ }
+
+ return true;
+ }
+ }
+
+ /**
+ * Returns the default slugs for all the presets in an associative array
+ * whose keys are the preset paths and the leafs is the list of slugs.
*
- * block-selector {
- * style-property-one: value;
- * }
+ * For example:
*
- * @param array $style_nodes Nodes with styles.
- * @return string The new stylesheet.
+ * array(
+ * 'color' => array(
+ * 'palette' => array( 'slug-1', 'slug-2' ),
+ * 'gradients' => array( 'slug-3', 'slug-4' ),
+ * ),
+ * )
+ *
+ * @since 5.9.0
+ *
+ * @param array $data A theme.json like structure.
+ * @param array $node_path The path to inspect. It's 'settings' by default.
+ * @return array
*/
- protected function get_block_classes( $style_nodes ) {
- $block_rules = '';
+ protected static function get_default_slugs( $data, $node_path ) {
+ $slugs = array();
- foreach ( $style_nodes as $metadata ) {
- if ( null === $metadata['selector'] ) {
+ foreach ( static::PRESETS_METADATA as $metadata ) {
+ $path = $node_path;
+ foreach ( $metadata['path'] as $leaf ) {
+ $path[] = $leaf;
+ }
+ $path[] = 'default';
+
+ $preset = _wp_array_get( $data, $path, null );
+ if ( ! isset( $preset ) ) {
continue;
}
- $block_rules .= static::get_styles_for_block( $metadata );
+
+ $slugs_for_preset = array();
+ foreach ( $preset as $item ) {
+ if ( isset( $item['slug'] ) ) {
+ $slugs_for_preset[] = $item['slug'];
+ }
+ }
+
+ _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset );
}
- return $block_rules;
+ return $slugs;
}
/**
- * Given a styles array, it extracts the style properties
- * and adds them to the $declarations array following the format:
+ * Gets a `default`'s preset name by a provided slug.
*
- * ```php
- * array(
- * 'name' => 'property_name',
- * 'value' => 'property_value,
- * )
- * ```
+ * @since 5.9.0
*
- * @param array $styles Styles to process.
- * @param array $settings Theme settings.
- * @param array $properties Properties metadata.
- * @param array $theme_json Theme JSON array.
- * @param string $selector The style block selector.
- * @param boolean $use_root_padding Whether to add custom properties at root level.
- * @return array Returns the modified $declarations.
+ * @param string $slug The slug we want to find a match from default presets.
+ * @param array $base_path The path to inspect. It's 'settings' by default.
+ * @return string|null
*/
- protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
- if ( null === $properties ) {
- $properties = static::PROPERTIES_METADATA;
+ protected function get_name_from_defaults( $slug, $base_path ) {
+ $path = $base_path;
+ $path[] = 'default';
+ $default_content = _wp_array_get( $this->theme_json, $path, null );
+ if ( ! $default_content ) {
+ return null;
+ }
+ foreach ( $default_content as $item ) {
+ if ( $slug === $item['slug'] ) {
+ return $item['name'];
+ }
}
+ return null;
+ }
- $declarations = array();
- if ( empty( $styles ) ) {
- return $declarations;
+ /**
+ * Removes the preset values whose slug is equal to any of given slugs.
+ *
+ * @since 5.9.0
+ *
+ * @param array $node The node with the presets to validate.
+ * @param array $slugs The slugs that should not be overridden.
+ * @return array The new node.
+ */
+ protected static function filter_slugs( $node, $slugs ) {
+ if ( empty( $slugs ) ) {
+ return $node;
}
- $root_variable_duplicates = array();
+ $new_node = array();
+ foreach ( $node as $value ) {
+ if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) {
+ $new_node[] = $value;
+ }
+ }
- foreach ( $properties as $css_property => $value_path ) {
- $value = static::get_property_value( $styles, $value_path, $theme_json );
+ return $new_node;
+ }
- if ( str_starts_with( $css_property, '--wp--style--root--' ) && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) {
+ /**
+ * Removes insecure data from theme.json.
+ *
+ * @since 5.9.0
+ *
+ * @param array $theme_json Structure to sanitize.
+ * @return array Sanitized structure.
+ */
+ public static function remove_insecure_properties( $theme_json ) {
+ $sanitized = array();
+
+ $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json );
+
+ $valid_block_names = array_keys( static::get_blocks_metadata() );
+ $valid_element_names = array_keys( static::ELEMENTS );
+
+ $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names );
+
+ $blocks_metadata = static::get_blocks_metadata();
+ $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata );
+
+ foreach ( $style_nodes as $metadata ) {
+ $input = _wp_array_get( $theme_json, $metadata['path'], array() );
+ if ( empty( $input ) ) {
continue;
}
- // Root-level padding styles don't currently support strings with CSS shorthand values.
- // This may change: https://github.com/WordPress/gutenberg/issues/40132.
- if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) {
+
+ $output = static::remove_insecure_styles( $input );
+
+ /*
+ * Get a reference to element name from path.
+ * $metadata['path'] = array( 'styles', 'elements', 'link' );
+ */
+ $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ];
+
+ /*
+ * $output is stripped of pseudo selectors. Re-add and process them
+ * or insecure styles here.
+ */
+ // TODO: Replace array_key_exists() with isset() check once WordPress drops
+ // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067.
+ if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) {
+ foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) {
+ if ( isset( $input[ $pseudo_selector ] ) ) {
+ $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] );
+ }
+ }
+ }
+
+ if ( ! empty( $output ) ) {
+ _wp_array_set( $sanitized, $metadata['path'], $output );
+ }
+ }
+
+ $setting_nodes = static::get_setting_nodes( $theme_json );
+ foreach ( $setting_nodes as $metadata ) {
+ $input = _wp_array_get( $theme_json, $metadata['path'], array() );
+ if ( empty( $input ) ) {
continue;
}
- if ( str_starts_with( $css_property, '--wp--style--root--' ) && $use_root_padding ) {
- $root_variable_duplicates[] = substr( $css_property, strlen( '--wp--style--root--' ) );
+ $output = static::remove_insecure_settings( $input );
+ if ( ! empty( $output ) ) {
+ _wp_array_set( $sanitized, $metadata['path'], $output );
}
+ }
- // Look up protected properties, keyed by value path.
- // Skip protected properties that are explicitly set to `null`.
- if ( is_array( $value_path ) ) {
- $path_string = implode( '.', $value_path );
- if (
- array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) &&
- _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null
- ) {
+ if ( empty( $sanitized['styles'] ) ) {
+ unset( $theme_json['styles'] );
+ } else {
+ $theme_json['styles'] = $sanitized['styles'];
+ }
+
+ if ( empty( $sanitized['settings'] ) ) {
+ unset( $theme_json['settings'] );
+ } else {
+ $theme_json['settings'] = $sanitized['settings'];
+ }
+
+ return $theme_json;
+ }
+
+ /**
+ * Processes a setting node and returns the same node
+ * without the insecure settings.
+ *
+ * @since 5.9.0
+ *
+ * @param array $input Node to process.
+ * @return array
+ */
+ protected static function remove_insecure_settings( $input ) {
+ $output = array();
+ foreach ( static::PRESETS_METADATA as $preset_metadata ) {
+ foreach ( static::VALID_ORIGINS as $origin ) {
+ $path_with_origin = $preset_metadata['path'];
+ $path_with_origin[] = $origin;
+ $presets = _wp_array_get( $input, $path_with_origin, null );
+ if ( null === $presets ) {
continue;
}
+
+ $escaped_preset = array();
+ foreach ( $presets as $preset ) {
+ if (
+ esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] &&
+ sanitize_html_class( $preset['slug'] ) === $preset['slug']
+ ) {
+ $value = null;
+ if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
+ $value = $preset[ $preset_metadata['value_key'] ];
+ } elseif (
+ isset( $preset_metadata['value_func'] ) &&
+ is_callable( $preset_metadata['value_func'] )
+ ) {
+ $value = call_user_func( $preset_metadata['value_func'], $preset );
+ }
+
+ $preset_is_valid = true;
+ foreach ( $preset_metadata['properties'] as $property ) {
+ if ( ! static::is_safe_css_declaration( $property, $value ) ) {
+ $preset_is_valid = false;
+ break;
+ }
+ }
+
+ if ( $preset_is_valid ) {
+ $escaped_preset[] = $preset;
+ }
+ }
+ }
+
+ if ( ! empty( $escaped_preset ) ) {
+ _wp_array_set( $output, $path_with_origin, $escaped_preset );
+ }
}
+ }
- // Skip if empty and not "0" or value represents array of longhand values.
- $has_missing_value = empty( $value ) && ! is_numeric( $value );
- if ( $has_missing_value || is_array( $value ) ) {
- continue;
+ foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) {
+ foreach ( $paths as $path ) {
+ $value = _wp_array_get( $input, $path, array() );
+ if (
+ isset( $value ) &&
+ ! is_array( $value ) &&
+ static::is_safe_css_declaration( $property, $value )
+ ) {
+ _wp_array_set( $output, $path, $value );
+ }
}
+ }
+ return $output;
+ }
- // Calculates fluid typography rules where available.
- if ( 'font-size' === $css_property ) {
- /*
- * gutenberg_get_typography_font_size_value() will check
- * if fluid typography has been activated and also
- * whether the incoming value can be converted to a fluid value.
- * Values that already have a "clamp()" function will not pass the test,
- * and therefore the original $value will be returned.
- */
- $value = gutenberg_get_typography_font_size_value( array( 'size' => $value ) );
+ /**
+ * Processes a style node and returns the same node
+ * without the insecure styles.
+ *
+ * @since 5.9.0
+ * @since 6.2.0 Allow indirect properties used outside of `compute_style_properties`.
+ *
+ * @param array $input Node to process.
+ * @return array
+ */
+ protected static function remove_insecure_styles( $input ) {
+ $output = array();
+ $declarations = static::compute_style_properties( $input );
+
+ foreach ( $declarations as $declaration ) {
+ if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) {
+ $path = static::PROPERTIES_METADATA[ $declaration['name'] ];
+
+ // Check the value isn't an array before adding so as to not
+ // double up shorthand and longhand styles.
+ $value = _wp_array_get( $input, $path, array() );
+ if ( ! is_array( $value ) ) {
+ _wp_array_set( $output, $path, $value );
+ }
}
-
- $declarations[] = array(
- 'name' => $css_property,
- 'value' => $value,
- );
}
- // If a variable value is added to the root, the corresponding property should be removed.
- foreach ( $root_variable_duplicates as $duplicate ) {
- $discard = array_search( $duplicate, array_column( $declarations, 'name' ), true );
- if ( is_numeric( $discard ) ) {
- array_splice( $declarations, $discard, 1 );
+ // Ensure indirect properties not handled by `compute_style_properties` are allowed.
+ foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) {
+ foreach ( $paths as $path ) {
+ $value = _wp_array_get( $input, $path, array() );
+ if (
+ isset( $value ) &&
+ ! is_array( $value ) &&
+ static::is_safe_css_declaration( $property, $value )
+ ) {
+ _wp_array_set( $output, $path, $value );
+ }
}
}
- return $declarations;
+ return $output;
}
/**
- * Returns the style property for the given path.
+ * Checks that a declaration provided by the user is safe.
*
- * It also converts CSS Custom Property stored as
- * "var:preset|color|secondary" to the form
- * "--wp--preset--color--secondary".
+ * @since 5.9.0
*
- * It also converts references to a path to the value
- * stored at that location, e.g.
- * { "ref": "style.color.background" } => "#fff".
+ * @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`.
+ * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`.
+ * @return bool
+ */
+ protected static function is_safe_css_declaration( $property_name, $property_value ) {
+ $style_to_validate = $property_name . ': ' . $property_value;
+ $filtered = esc_html( safecss_filter_attr( $style_to_validate ) );
+ return ! empty( trim( $filtered ) );
+ }
+
+ /**
+ * Returns the raw data.
*
- * @param array $styles Styles subtree.
- * @param array $path Which property to process.
- * @param array $theme_json Theme JSON array.
- * @return string|array|null Style property value.
+ * @since 5.8.0
+ *
+ * @return array Raw data.
*/
- protected static function get_property_value( $styles, $path, $theme_json = null ) {
- $value = _wp_array_get( $styles, $path );
+ public function get_raw_data() {
+ return $this->theme_json;
+ }
- // This converts references to a path to the value at that path
- // where the values is an array with a "ref" key, pointing to a path.
- // For example: { "ref": "style.color.background" } => "#fff".
- if ( is_array( $value ) && array_key_exists( 'ref', $value ) ) {
- $value_path = explode( '.', $value['ref'] );
- $ref_value = _wp_array_get( $theme_json, $value_path );
- // Only use the ref value if we find anything.
- if ( ! empty( $ref_value ) && is_string( $ref_value ) ) {
- $value = $ref_value;
+ /**
+ * Transforms the given editor settings according the
+ * add_theme_support format to the theme.json format.
+ *
+ * @since 5.8.0
+ *
+ * @param array $settings Existing editor settings.
+ * @return array Config that adheres to the theme.json schema.
+ */
+ public static function get_from_editor_settings( $settings ) {
+ $theme_settings = array(
+ 'version' => static::LATEST_SCHEMA,
+ 'settings' => array(),
+ );
+
+ // Deprecated theme supports.
+ if ( isset( $settings['disableCustomColors'] ) ) {
+ if ( ! isset( $theme_settings['settings']['color'] ) ) {
+ $theme_settings['settings']['color'] = array();
}
+ $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors'];
+ }
- if ( is_array( $ref_value ) && array_key_exists( 'ref', $ref_value ) ) {
- $path_string = json_encode( $path );
- $ref_value_string = json_encode( $ref_value );
- _doing_it_wrong( 'get_property_value', "Your theme.json file uses a dynamic value ({$ref_value_string}) for the path at {$path_string}. However, the value at {$path_string} is also a dynamic value (pointing to {$ref_value['ref']}) and pointing to another dynamic value is not supported. Please update {$path_string} to point directly to {$ref_value['ref']}.", '6.1.0' );
+ if ( isset( $settings['disableCustomGradients'] ) ) {
+ if ( ! isset( $theme_settings['settings']['color'] ) ) {
+ $theme_settings['settings']['color'] = array();
}
+ $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients'];
}
- if ( ! $value || is_array( $value ) ) {
- return $value;
+ if ( isset( $settings['disableCustomFontSizes'] ) ) {
+ if ( ! isset( $theme_settings['settings']['typography'] ) ) {
+ $theme_settings['settings']['typography'] = array();
+ }
+ $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes'];
}
- // Convert custom CSS properties.
- $prefix = 'var:';
- $prefix_len = strlen( $prefix );
- $token_in = '|';
- $token_out = '--';
- if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) {
- $unwrapped_name = str_replace(
- $token_in,
- $token_out,
- substr( $value, $prefix_len )
- );
- $value = "var(--wp--$unwrapped_name)";
+ if ( isset( $settings['enableCustomLineHeight'] ) ) {
+ if ( ! isset( $theme_settings['settings']['typography'] ) ) {
+ $theme_settings['settings']['typography'] = array();
+ }
+ $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight'];
}
- return $value;
+ if ( isset( $settings['enableCustomUnits'] ) ) {
+ if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
+ $theme_settings['settings']['spacing'] = array();
+ }
+ $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ?
+ array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) :
+ $settings['enableCustomUnits'];
+ }
+
+ if ( isset( $settings['colors'] ) ) {
+ if ( ! isset( $theme_settings['settings']['color'] ) ) {
+ $theme_settings['settings']['color'] = array();
+ }
+ $theme_settings['settings']['color']['palette'] = $settings['colors'];
+ }
+
+ if ( isset( $settings['gradients'] ) ) {
+ if ( ! isset( $theme_settings['settings']['color'] ) ) {
+ $theme_settings['settings']['color'] = array();
+ }
+ $theme_settings['settings']['color']['gradients'] = $settings['gradients'];
+ }
+
+ if ( isset( $settings['fontSizes'] ) ) {
+ $font_sizes = $settings['fontSizes'];
+ // Back-compatibility for presets without units.
+ foreach ( $font_sizes as $key => $font_size ) {
+ if ( is_numeric( $font_size['size'] ) ) {
+ $font_sizes[ $key ]['size'] = $font_size['size'] . 'px';
+ }
+ }
+ if ( ! isset( $theme_settings['settings']['typography'] ) ) {
+ $theme_settings['settings']['typography'] = array();
+ }
+ $theme_settings['settings']['typography']['fontSizes'] = $font_sizes;
+ }
+
+ if ( isset( $settings['enableCustomSpacing'] ) ) {
+ if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
+ $theme_settings['settings']['spacing'] = array();
+ }
+ $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing'];
+ }
+
+ return $theme_settings;
}
/**
- * Presets are a set of values that serve
- * to bootstrap some styles: colors, font sizes, etc.
- *
- * They are a unkeyed array of values such as:
- *
- * ```php
- * array(
- * array(
- * 'slug' => 'unique-name-within-the-set',
- * 'name' => 'Name for the UI',
- *
- * $scope = '.a, .b .c';
- * $selector = '> .x, .y';
- * $merged = scope_selector( $scope, $selector );
- * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
- *
- *
- * @since 5.9.0
- *
- * @param string $scope Selector to scope to.
- * @param string $selector Original selector.
- * @return string Scoped selector.
- */
- public static function scope_selector( $scope, $selector ) {
- $scopes = explode( ',', $scope );
- $selectors = explode( ',', $selector );
-
- $selectors_scoped = array();
- foreach ( $scopes as $outer ) {
- foreach ( $selectors as $inner ) {
- $outer = trim( $outer );
- $inner = trim( $inner );
- if ( ! empty( $outer ) && ! empty( $inner ) ) {
- $selectors_scoped[] = $outer . ' ' . $inner;
- } elseif ( empty( $outer ) ) {
- $selectors_scoped[] = $inner;
- } elseif ( empty( $inner ) ) {
- $selectors_scoped[] = $outer;
- }
- }
- }
-
- $result = implode( ', ', $selectors_scoped );
- return $result;
- }
-
}
diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php
new file mode 100644
index 00000000000000..8733fd13bf2c77
--- /dev/null
+++ b/lib/class-wp-theme-json-resolver-gutenberg.php
@@ -0,0 +1,701 @@
+ array(),
+ 'blocks' => array(),
+ 'theme' => array(),
+ 'user' => array(),
+ );
+
+ /**
+ * Container for data coming from core.
+ *
+ * @since 5.8.0
+ * @var WP_Theme_JSON
+ */
+ protected static $core = null;
+
+ /**
+ * Container for data coming from the blocks.
+ *
+ * @since 6.1.0
+ * @var WP_Theme_JSON
+ */
+ protected static $blocks = null;
+
+ /**
+ * Container for data coming from the theme.
+ *
+ * @since 5.8.0
+ * @var WP_Theme_JSON
+ */
+ protected static $theme = null;
+
+ /**
+ * Container for data coming from the user.
+ *
+ * @since 5.9.0
+ * @var WP_Theme_JSON
+ */
+ protected static $user = null;
+
+ /**
+ * Stores the ID of the custom post type
+ * that holds the user data.
+ *
+ * @since 5.9.0
+ * @var int
+ */
+ protected static $user_custom_post_type_id = null;
+
+ /**
+ * Container to keep loaded i18n schema for `theme.json`.
+ *
+ * @since 5.8.0 As `$theme_json_i18n`.
+ * @since 5.9.0 Renamed from `$theme_json_i18n` to `$i18n_schema`.
+ * @var array
+ */
+ protected static $i18n_schema = null;
+
+ /**
+ * `theme.json` file cache.
+ *
+ * @since 6.1.0
+ * @var array
+ */
+ protected static $theme_json_file_cache = array();
+
+ /**
+ * Processes a file that adheres to the theme.json schema
+ * and returns an array with its contents, or a void array if none found.
+ *
+ * @since 5.8.0
+ * @since 6.1.0 Added caching.
+ *
+ * @param string $file_path Path to file. Empty if no file.
+ * @return array Contents that adhere to the theme.json schema.
+ */
+ protected static function read_json_file( $file_path ) {
+ if ( $file_path ) {
+ if ( array_key_exists( $file_path, static::$theme_json_file_cache ) ) {
+ return static::$theme_json_file_cache[ $file_path ];
+ }
+
+ $decoded_file = wp_json_file_decode( $file_path, array( 'associative' => true ) );
+ if ( is_array( $decoded_file ) ) {
+ static::$theme_json_file_cache[ $file_path ] = $decoded_file;
+ return static::$theme_json_file_cache[ $file_path ];
+ }
+ }
+
+ return array();
+ }
+
+ /**
+ * Returns a data structure used in theme.json translation.
+ *
+ * @since 5.8.0
+ * @deprecated 5.9.0
+ *
+ * @return array An array of theme.json fields that are translatable and the keys that are translatable.
+ */
+ public static function get_fields_to_translate() {
+ _deprecated_function( __METHOD__, '5.9.0' );
+ return array();
+ }
+
+ /**
+ * Given a theme.json structure modifies it in place to update certain values
+ * by its translated strings according to the language set by the user.
+ *
+ * @since 5.8.0
+ *
+ * @param array $theme_json The theme.json to translate.
+ * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings.
+ * Default 'default'.
+ * @return array Returns the modified $theme_json_structure.
+ */
+ protected static function translate( $theme_json, $domain = 'default' ) {
+ if ( null === static::$i18n_schema ) {
+ $i18n_schema = wp_json_file_decode( __DIR__ . '/theme-i18n.json' );
+ static::$i18n_schema = null === $i18n_schema ? array() : $i18n_schema;
+ }
+
+ return translate_settings_using_i18n_schema( static::$i18n_schema, $theme_json, $domain );
+ }
+
+ /**
+ * Returns core's origin config.
+ *
+ * @since 5.8.0
+ *
+ * @return WP_Theme_JSON Entity that holds core data.
+ */
+ public static function get_core_data() {
+ if ( null !== static::$core && static::has_same_registered_blocks( 'core' ) ) {
+ return static::$core;
+ }
+
+ $config = static::read_json_file( __DIR__ . '/theme.json' );
+ $config = static::translate( $config );
+
+ /**
+ * Filters the default data provided by WordPress for global styles & settings.
+ *
+ * @since 6.1.0
+ *
+ * @param WP_Theme_JSON_Data Class to access and update the underlying data.
+ */
+ $theme_json = apply_filters( 'wp_theme_json_data_default', new WP_Theme_JSON_Data_Gutenberg( $config, 'default' ) );
+ $config = $theme_json->get_data();
+ static::$core = new WP_Theme_JSON_Gutenberg( $config, 'default' );
+
+ return static::$core;
+ }
+
+ /**
+ * Checks whether the registered blocks were already processed for this origin.
+ *
+ * @since 6.1.0
+ *
+ * @param string $origin Data source for which to cache the blocks.
+ * Valid values are 'core', 'blocks', 'theme', and 'user'.
+ * @return bool True on success, false otherwise.
+ */
+ protected static function has_same_registered_blocks( $origin ) {
+ // Bail out if the origin is invalid.
+ if ( ! isset( static::$blocks_cache[ $origin ] ) ) {
+ return false;
+ }
+
+ $registry = WP_Block_Type_Registry::get_instance();
+ $blocks = $registry->get_all_registered();
+
+ // Is there metadata for all currently registered blocks?
+ $block_diff = array_diff_key( $blocks, static::$blocks_cache[ $origin ] );
+ if ( empty( $block_diff ) ) {
+ return true;
+ }
+
+ foreach ( $blocks as $block_name => $block_type ) {
+ static::$blocks_cache[ $origin ][ $block_name ] = true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the theme's data.
+ *
+ * Data from theme.json will be backfilled from existing
+ * theme supports, if any. Note that if the same data
+ * is present in theme.json and in theme supports,
+ * the theme.json takes precedence.
+ *
+ * @since 5.8.0
+ * @since 5.9.0 Theme supports have been inlined and the `$theme_support_data` argument removed.
+ * @since 6.0.0 Added an `$options` parameter to allow the theme data to be returned without theme supports.
+ *
+ * @param array $deprecated Deprecated. Not used.
+ * @param array $options {
+ * Options arguments.
+ *
+ * @type bool $with_supports Whether to include theme supports in the data. Default true.
+ * }
+ * @return WP_Theme_JSON Entity that holds theme data.
+ */
+ public static function get_theme_data( $deprecated = array(), $options = array() ) {
+ if ( ! empty( $deprecated ) ) {
+ _deprecated_argument( __METHOD__, '5.9.0' );
+ }
+
+ $options = wp_parse_args( $options, array( 'with_supports' => true ) );
+
+ if ( null === static::$theme || ! static::has_same_registered_blocks( 'theme' ) ) {
+ $theme_json_file = static::get_file_path_from_theme( 'theme.json' );
+ $wp_theme = wp_get_theme();
+ if ( '' !== $theme_json_file ) {
+ $theme_json_data = static::read_json_file( $theme_json_file );
+ $theme_json_data = static::translate( $theme_json_data, $wp_theme->get( 'TextDomain' ) );
+ } else {
+ $theme_json_data = array();
+ }
+ // BEGIN OF EXPERIMENTAL CODE. Not to backport to core.
+ $theme_json_data = gutenberg_add_registered_webfonts_to_theme_json( $theme_json_data );
+ // END OF EXPERIMENTAL CODE.
+
+ /**
+ * Filters the data provided by the theme for global styles and settings.
+ *
+ * @since 6.1.0
+ *
+ * @param WP_Theme_JSON_Data Class to access and update the underlying data.
+ */
+ $theme_json = apply_filters( 'wp_theme_json_data_theme', new WP_Theme_JSON_Data_Gutenberg( $theme_json_data, 'theme' ) );
+ $theme_json_data = $theme_json->get_data();
+ static::$theme = new WP_Theme_JSON_Gutenberg( $theme_json_data );
+
+ if ( $wp_theme->parent() ) {
+ // Get parent theme.json.
+ $parent_theme_json_file = static::get_file_path_from_theme( 'theme.json', true );
+ if ( '' !== $parent_theme_json_file ) {
+ $parent_theme_json_data = static::read_json_file( $parent_theme_json_file );
+ $parent_theme_json_data = static::translate( $parent_theme_json_data, $wp_theme->parent()->get( 'TextDomain' ) );
+ // BEGIN OF EXPERIMENTAL CODE. Not to backport to core.
+ $parent_theme_json_data = gutenberg_add_registered_webfonts_to_theme_json( $parent_theme_json_data );
+ // END OF EXPERIMENTAL CODE.
+ $parent_theme = new WP_Theme_JSON_Gutenberg( $parent_theme_json_data );
+
+ /*
+ * Merge the child theme.json into the parent theme.json.
+ * The child theme takes precedence over the parent.
+ */
+ $parent_theme->merge( static::$theme );
+ static::$theme = $parent_theme;
+ }
+ }
+ }
+
+ if ( ! $options['with_supports'] ) {
+ return static::$theme;
+ }
+
+ /*
+ * We want the presets and settings declared in theme.json
+ * to override the ones declared via theme supports.
+ * So we take theme supports, transform it to theme.json shape
+ * and merge the static::$theme upon that.
+ */
+ $theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( gutenberg_get_legacy_theme_supports_for_theme_json() );
+ if ( ! wp_theme_has_theme_json() ) {
+ if ( ! isset( $theme_support_data['settings']['color'] ) ) {
+ $theme_support_data['settings']['color'] = array();
+ }
+
+ $default_palette = false;
+ if ( current_theme_supports( 'default-color-palette' ) ) {
+ $default_palette = true;
+ }
+ if ( ! isset( $theme_support_data['settings']['color']['palette'] ) ) {
+ // If the theme does not have any palette, we still want to show the core one.
+ $default_palette = true;
+ }
+ $theme_support_data['settings']['color']['defaultPalette'] = $default_palette;
+
+ $default_gradients = false;
+ if ( current_theme_supports( 'default-gradient-presets' ) ) {
+ $default_gradients = true;
+ }
+ if ( ! isset( $theme_support_data['settings']['color']['gradients'] ) ) {
+ // If the theme does not have any gradients, we still want to show the core ones.
+ $default_gradients = true;
+ }
+ $theme_support_data['settings']['color']['defaultGradients'] = $default_gradients;
+
+ // Classic themes without a theme.json don't support global duotone.
+ $theme_support_data['settings']['color']['defaultDuotone'] = false;
+
+ // Allow themes to enable appearance tools via theme_support.
+ if ( current_theme_supports( 'appearance-tools' ) ) {
+ $theme_support_data['settings']['appearanceTools'] = true;
+ }
+ }
+ $with_theme_supports = new WP_Theme_JSON_Gutenberg( $theme_support_data );
+ $with_theme_supports->merge( static::$theme );
+ return $with_theme_supports;
+ }
+
+ /**
+ * Gets the styles for blocks from the block.json file.
+ *
+ * @since 6.1.0
+ *
+ * @return WP_Theme_JSON
+ */
+ public static function get_block_data() {
+ $registry = WP_Block_Type_Registry::get_instance();
+ $blocks = $registry->get_all_registered();
+
+ if ( null !== static::$blocks && static::has_same_registered_blocks( 'blocks' ) ) {
+ return static::$blocks;
+ }
+
+ $config = array( 'version' => 2 );
+ foreach ( $blocks as $block_name => $block_type ) {
+ if ( isset( $block_type->supports['__experimentalStyle'] ) ) {
+ $config['styles']['blocks'][ $block_name ] = static::remove_json_comments( $block_type->supports['__experimentalStyle'] );
+ }
+
+ if (
+ isset( $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ) &&
+ null === _wp_array_get( $config, array( 'styles', 'blocks', $block_name, 'spacing', 'blockGap' ), null )
+ ) {
+ // Ensure an empty placeholder value exists for the block, if it provides a default blockGap value.
+ // The real blockGap value to be used will be determined when the styles are rendered for output.
+ $config['styles']['blocks'][ $block_name ]['spacing']['blockGap'] = null;
+ }
+ }
+
+ /**
+ * Filters the data provided by the blocks for global styles & settings.
+ *
+ * @since 6.1.0
+ *
+ * @param WP_Theme_JSON_Data Class to access and update the underlying data.
+ */
+ $theme_json = apply_filters( 'wp_theme_json_data_blocks', new WP_Theme_JSON_Data_Gutenberg( $config, 'blocks' ) );
+ $config = $theme_json->get_data();
+
+ static::$blocks = new WP_Theme_JSON_Gutenberg( $config, 'blocks' );
+ return static::$blocks;
+ }
+
+ /**
+ * When given an array, this will remove any keys with the name `//`.
+ *
+ * @param array $array The array to filter.
+ * @return array The filtered array.
+ */
+ private static function remove_json_comments( $array ) {
+ unset( $array['//'] );
+ foreach ( $array as $k => $v ) {
+ if ( is_array( $v ) ) {
+ $array[ $k ] = static::remove_json_comments( $v );
+ }
+ }
+
+ return $array;
+ }
+
+ /**
+ * Returns the custom post type that contains the user's origin config
+ * for the active theme or a void array if none are found.
+ *
+ * This can also create and return a new draft custom post type.
+ *
+ * @since 5.9.0
+ *
+ * @param WP_Theme $theme The theme object. If empty, it
+ * defaults to the active theme.
+ * @param bool $create_post Optional. Whether a new custom post
+ * type should be created if none are
+ * found. Default false.
+ * @param array $post_status_filter Optional. Filter custom post type by
+ * post status. Default `array( 'publish' )`,
+ * so it only fetches published posts.
+ * @return array Custom Post Type for the user's origin config.
+ */
+ public static function get_user_data_from_wp_global_styles( $theme, $create_post = false, $post_status_filter = array( 'publish' ) ) {
+ if ( ! $theme instanceof WP_Theme ) {
+ $theme = wp_get_theme();
+ }
+
+ /*
+ * Bail early if the theme does not support a theme.json.
+ *
+ * Since wp_theme_has_theme_json only supports the active
+ * theme, the extra condition for whether $theme is the active theme is
+ * present here.
+ */
+ if ( $theme->get_stylesheet() === get_stylesheet() && ! wp_theme_has_theme_json() ) {
+ return array();
+ }
+
+ $user_cpt = array();
+ $post_type_filter = 'wp_global_styles';
+ $stylesheet = $theme->get_stylesheet();
+ $args = array(
+ 'posts_per_page' => 1,
+ 'orderby' => 'date',
+ 'order' => 'desc',
+ 'post_type' => $post_type_filter,
+ 'post_status' => $post_status_filter,
+ 'ignore_sticky_posts' => true,
+ 'no_found_rows' => true,
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false,
+ 'tax_query' => array(
+ array(
+ 'taxonomy' => 'wp_theme',
+ 'field' => 'name',
+ 'terms' => $stylesheet,
+ ),
+ ),
+ );
+
+ $global_style_query = new WP_Query();
+ $recent_posts = $global_style_query->query( $args );
+ if ( count( $recent_posts ) === 1 ) {
+ $user_cpt = get_object_vars( $recent_posts[0] );
+ } elseif ( $create_post ) {
+ $cpt_post_id = wp_insert_post(
+ array(
+ 'post_content' => '{"version": ' . WP_Theme_JSON_Gutenberg::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }',
+ 'post_status' => 'publish',
+ 'post_title' => 'Custom Styles', // Do not make string translatable, see https://core.trac.wordpress.org/ticket/54518.
+ 'post_type' => $post_type_filter,
+ 'post_name' => sprintf( 'wp-global-styles-%s', urlencode( $stylesheet ) ),
+ 'tax_input' => array(
+ 'wp_theme' => array( $stylesheet ),
+ ),
+ ),
+ true
+ );
+ if ( ! is_wp_error( $cpt_post_id ) ) {
+ $user_cpt = get_object_vars( get_post( $cpt_post_id ) );
+ }
+ }
+
+ return $user_cpt;
+ }
+
+ /**
+ * Returns the user's origin config.
+ *
+ * @since 5.9.0
+ *
+ * @return WP_Theme_JSON Entity that holds styles for user data.
+ */
+ public static function get_user_data() {
+ if ( null !== static::$user && static::has_same_registered_blocks( 'user' ) ) {
+ return static::$user;
+ }
+
+ $config = array();
+ $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() );
+
+ if ( array_key_exists( 'post_content', $user_cpt ) ) {
+ $decoded_data = json_decode( $user_cpt['post_content'], true );
+
+ $json_decoding_error = json_last_error();
+ if ( JSON_ERROR_NONE !== $json_decoding_error ) {
+ trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() );
+ /**
+ * Filters the data provided by the user for global styles & settings.
+ *
+ * @since 6.1.0
+ *
+ * @param WP_Theme_JSON_Data Class to access and update the underlying data.
+ */
+ $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) );
+ $config = $theme_json->get_data();
+ return new WP_Theme_JSON_Gutenberg( $config, 'custom' );
+ }
+
+ // Very important to verify that the flag isGlobalStylesUserThemeJSON is true.
+ // If it's not true then the content was not escaped and is not safe.
+ if (
+ is_array( $decoded_data ) &&
+ isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
+ $decoded_data['isGlobalStylesUserThemeJSON']
+ ) {
+ unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
+ $config = $decoded_data;
+ }
+ }
+
+ /** This filter is documented in wp-includes/class-wp-theme-json-resolver.php */
+ $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) );
+ $config = $theme_json->get_data();
+ static::$user = new WP_Theme_JSON_Gutenberg( $config, 'custom' );
+
+ return static::$user;
+ }
+
+ /**
+ * Returns the data merged from multiple origins.
+ *
+ * There are four sources of data (origins) for a site:
+ *
+ * - default => WordPress
+ * - blocks => each one of the blocks provides data for itself
+ * - theme => the active theme
+ * - custom => data provided by the user
+ *
+ * The custom's has higher priority than the theme's, the theme's higher than blocks',
+ * and block's higher than default's.
+ *
+ * Unlike the getters
+ * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_core_data/ get_core_data},
+ * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_theme_data/ get_theme_data},
+ * and {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_user_data/ get_user_data},
+ * this method returns data after it has been merged with the previous origins.
+ * This means that if the same piece of data is declared in different origins
+ * (default, blocks, theme, custom), the last origin overrides the previous.
+ *
+ * For example, if the user has set a background color
+ * for the paragraph block, and the theme has done it as well,
+ * the user preference wins.
+ *
+ * @since 5.8.0
+ * @since 5.9.0 Added user data, removed the `$settings` parameter,
+ * added the `$origin` parameter.
+ * @since 6.1.0 Added block data and generation of spacingSizes array.
+ *
+ * @param string $origin Optional. To what level should we merge data:'default', 'blocks', 'theme' or 'custom'.
+ * 'custom' is used as default value as well as fallback value if the origin is unknown.
+ *
+ * @return WP_Theme_JSON
+ */
+ public static function get_merged_data( $origin = 'custom' ) {
+ if ( is_array( $origin ) ) {
+ _deprecated_argument( __FUNCTION__, '5.9.0' );
+ }
+
+ $result = static::get_core_data();
+ if ( 'default' === $origin ) {
+ $result->set_spacing_sizes();
+ return $result;
+ }
+
+ $result->merge( static::get_block_data() );
+ if ( 'blocks' === $origin ) {
+ return $result;
+ }
+
+ $result->merge( static::get_theme_data() );
+ if ( 'theme' === $origin ) {
+ $result->set_spacing_sizes();
+ return $result;
+ }
+
+ $result->merge( static::get_user_data() );
+ $result->set_spacing_sizes();
+ return $result;
+ }
+
+ /**
+ * Returns the ID of the custom post type
+ * that stores user data.
+ *
+ * @since 5.9.0
+ *
+ * @return integer|null
+ */
+ public static function get_user_global_styles_post_id() {
+ if ( null !== static::$user_custom_post_type_id ) {
+ return static::$user_custom_post_type_id;
+ }
+
+ $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme(), true );
+
+ if ( array_key_exists( 'ID', $user_cpt ) ) {
+ static::$user_custom_post_type_id = $user_cpt['ID'];
+ }
+
+ return static::$user_custom_post_type_id;
+ }
+
+ /**
+ * Determines whether the active theme has a theme.json file.
+ *
+ * @since 5.8.0
+ * @since 5.9.0 Added a check in the parent theme.
+ * @deprecated 6.2.0 Use wp_theme_has_theme_json() instead.
+ *
+ * @return bool
+ */
+ public static function theme_has_support() {
+ _deprecated_function( __METHOD__, '6.2.0', 'wp_theme_has_theme_json()' );
+
+ return wp_theme_has_theme_json();
+ }
+
+ /**
+ * Builds the path to the given file and checks that it is readable.
+ *
+ * If it isn't, returns an empty string, otherwise returns the whole file path.
+ *
+ * @since 5.8.0
+ * @since 5.9.0 Adapted to work with child themes, added the `$template` argument.
+ *
+ * @param string $file_name Name of the file.
+ * @param bool $template Optional. Use template theme directory. Default false.
+ * @return string The whole file path or empty if the file doesn't exist.
+ */
+ protected static function get_file_path_from_theme( $file_name, $template = false ) {
+ $path = $template ? get_template_directory() : get_stylesheet_directory();
+ $candidate = $path . '/' . $file_name;
+
+ return is_readable( $candidate ) ? $candidate : '';
+ }
+
+ /**
+ * Cleans the cached data so it can be recalculated.
+ *
+ * @since 5.8.0
+ * @since 5.9.0 Added the `$user`, `$user_custom_post_type_id`,
+ * and `$i18n_schema` variables to reset.
+ * @since 6.1.0 Added the `$blocks` and `$blocks_cache` variables
+ * to reset.
+ */
+ public static function clean_cached_data() {
+ static::$core = null;
+ static::$blocks = null;
+ static::$blocks_cache = array(
+ 'core' => array(),
+ 'blocks' => array(),
+ 'theme' => array(),
+ 'user' => array(),
+ );
+ static::$theme = null;
+ static::$user = null;
+ static::$user_custom_post_type_id = null;
+ static::$i18n_schema = null;
+ }
+
+ /**
+ * Returns the style variations defined by the theme.
+ *
+ * @since 6.0.0
+ *
+ * @return array
+ */
+ public static function get_style_variations() {
+ $variations = array();
+ $base_directory = get_stylesheet_directory() . '/styles';
+ if ( is_dir( $base_directory ) ) {
+ $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory ) );
+ $nested_html_files = iterator_to_array( new RegexIterator( $nested_files, '/^.+\.json$/i', RecursiveRegexIterator::GET_MATCH ) );
+ ksort( $nested_html_files );
+ foreach ( $nested_html_files as $path => $file ) {
+ $decoded_file = wp_json_file_decode( $path, array( 'associative' => true ) );
+ if ( is_array( $decoded_file ) ) {
+ $translated = static::translate( $decoded_file, wp_get_theme()->get( 'TextDomain' ) );
+ $variation = ( new WP_Theme_JSON_Gutenberg( $translated ) )->get_raw_data();
+ if ( empty( $variation['title'] ) ) {
+ $variation['title'] = basename( $path, '.json' );
+ }
+ $variations[] = $variation;
+ }
+ }
+ }
+ return $variations;
+ }
+
+}
diff --git a/lib/client-assets.php b/lib/client-assets.php
index b7a11724a5e096..e23e1c4b89bd76 100644
--- a/lib/client-assets.php
+++ b/lib/client-assets.php
@@ -274,7 +274,7 @@ function gutenberg_register_packages_styles( $styles ) {
$styles,
'wp-editor',
gutenberg_url( 'build/editor/style.css' ),
- array( 'wp-components', 'wp-block-editor', 'wp-nux', 'wp-reusable-blocks' ),
+ array( 'wp-components', 'wp-block-editor', 'wp-reusable-blocks' ),
$version
);
$styles->add_data( 'wp-editor', 'rtl', 'replace' );
@@ -283,7 +283,7 @@ function gutenberg_register_packages_styles( $styles ) {
$styles,
'wp-edit-post',
gutenberg_url( 'build/edit-post/style.css' ),
- array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library', 'wp-nux' ),
+ array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library' ),
$version
);
$styles->add_data( 'wp-edit-post', 'rtl', 'replace' );
@@ -367,15 +367,6 @@ function gutenberg_register_packages_styles( $styles ) {
);
$styles->add_data( 'wp-edit-blocks', 'rtl', 'replace' );
- gutenberg_override_style(
- $styles,
- 'wp-nux',
- gutenberg_url( 'build/nux/style.css' ),
- array( 'wp-components' ),
- $version
- );
- $styles->add_data( 'wp-nux', 'rtl', 'replace' );
-
gutenberg_override_style(
$styles,
'wp-block-library-theme',
@@ -558,13 +549,15 @@ function gutenberg_register_vendor_scripts( $scripts ) {
'react',
gutenberg_url( 'build/vendors/react' . $extension ),
// See https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/TROUBLESHOOTING.md#externalising-react.
- SCRIPT_DEBUG ? array( 'wp-react-refresh-entry', 'wp-polyfill' ) : array( 'wp-polyfill' )
+ SCRIPT_DEBUG ? array( 'wp-react-refresh-entry', 'wp-polyfill' ) : array( 'wp-polyfill' ),
+ '18'
);
gutenberg_override_script(
$scripts,
'react-dom',
gutenberg_url( 'build/vendors/react-dom' . $extension ),
- array( 'react' )
+ array( 'react' ),
+ '18'
);
}
add_action( 'wp_default_scripts', 'gutenberg_register_vendor_scripts' );
diff --git a/lib/compat/wordpress-6.1/class-gutenberg-rest-templates-controller.php b/lib/compat/wordpress-6.1/class-gutenberg-rest-templates-controller.php
index e911b0f1e4d203..f0416e0a50e96f 100644
--- a/lib/compat/wordpress-6.1/class-gutenberg-rest-templates-controller.php
+++ b/lib/compat/wordpress-6.1/class-gutenberg-rest-templates-controller.php
@@ -55,7 +55,7 @@ public function register_routes() {
* @return WP_REST_Response|WP_Error
*/
public function get_template_fallback( $request ) {
- $hierarchy = get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] );
+ $hierarchy = gutenberg_get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] );
$fallback_template = resolve_block_template( $request['slug'], $hierarchy, '' );
$response = $this->prepare_item_for_response( $fallback_template, $request );
return rest_ensure_response( $response );
diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php
deleted file mode 100644
index 1d4a83e3406fbe..00000000000000
--- a/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php
+++ /dev/null
@@ -1,191 +0,0 @@
-get_data();
- static::$core = new WP_Theme_JSON_Gutenberg( $config, 'default' );
-
- return static::$core;
- }
-
- /**
- * Returns the user's origin config.
- *
- * @return WP_Theme_JSON_Gutenberg Entity that holds styles for user data.
- */
- public static function get_user_data() {
- if ( null !== static::$user ) {
- return static::$user;
- }
-
- $config = array();
- $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() );
-
- if ( array_key_exists( 'post_content', $user_cpt ) ) {
- $decoded_data = json_decode( $user_cpt['post_content'], true );
-
- $json_decoding_error = json_last_error();
- if ( JSON_ERROR_NONE !== $json_decoding_error ) {
- trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() );
- /**
- * Filters the data provided by the user for global styles & settings.
- *
- * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data.
- */
- $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) );
- $config = $theme_json->get_data();
- return new WP_Theme_JSON_Gutenberg( $config, 'custom' );
- }
-
- // Very important to verify if the flag isGlobalStylesUserThemeJSON is true.
- // If is not true the content was not escaped and is not safe.
- if (
- is_array( $decoded_data ) &&
- isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
- $decoded_data['isGlobalStylesUserThemeJSON']
- ) {
- unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
- $config = $decoded_data;
- }
- }
-
- /**
- * Filters the data provided by the user for global styles & settings.
- *
- * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data.
- */
- $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) );
- $config = $theme_json->get_data();
- static::$user = new WP_Theme_JSON_Gutenberg( $config, 'custom' );
-
- return static::$user;
- }
-
- /**
- * Returns the custom post type that contains the user's origin config
- * for the active theme or a void array if none are found.
- *
- * This can also create and return a new draft custom post type.
- *
- * @since 5.9.0
- *
- * @param WP_Theme $theme The theme object. If empty, it
- * defaults to the active theme.
- * @param bool $create_post Optional. Whether a new custom post
- * type should be created if none are
- * found. Default false.
- * @param array $post_status_filter Optional. Filter custom post type by
- * post status. Default `array( 'publish' )`,
- * so it only fetches published posts.
- * @return array Custom Post Type for the user's origin config.
- */
- public static function get_user_data_from_wp_global_styles( $theme, $create_post = false, $post_status_filter = array( 'publish' ) ) {
- if ( ! $theme instanceof WP_Theme ) {
- $theme = wp_get_theme();
- }
- $user_cpt = array();
- $post_type_filter = 'wp_global_styles';
- $stylesheet = $theme->get_stylesheet();
- $args = array(
- 'posts_per_page' => 1,
- 'orderby' => 'date',
- 'order' => 'desc',
- 'post_type' => $post_type_filter,
- 'post_status' => $post_status_filter,
- 'ignore_sticky_posts' => true,
- 'no_found_rows' => true,
- 'tax_query' => array(
- array(
- 'taxonomy' => 'wp_theme',
- 'field' => 'name',
- 'terms' => $stylesheet,
- ),
- ),
- );
-
- $global_style_query = new WP_Query();
- $recent_posts = $global_style_query->query( $args );
- if ( count( $recent_posts ) === 1 ) {
- $user_cpt = get_post( $recent_posts[0], ARRAY_A );
- } elseif ( $create_post ) {
- $cpt_post_id = wp_insert_post(
- array(
- 'post_content' => '{"version": ' . WP_Theme_JSON_Gutenberg::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }',
- 'post_status' => 'publish',
- 'post_title' => 'Custom Styles', // Do not make string translatable, see https://core.trac.wordpress.org/ticket/54518.
- 'post_type' => $post_type_filter,
- 'post_name' => sprintf( 'wp-global-styles-%s', urlencode( $stylesheet ) ),
- 'tax_input' => array(
- 'wp_theme' => array( $stylesheet ),
- ),
- ),
- true
- );
- if ( ! is_wp_error( $cpt_post_id ) ) {
- $user_cpt = get_post( $cpt_post_id, ARRAY_A );
- }
- }
-
- return $user_cpt;
- }
-}
diff --git a/lib/compat/wordpress-6.2/block-patterns.php b/lib/compat/wordpress-6.2/block-patterns.php
index 1e529faf963210..13d9af96220494 100644
--- a/lib/compat/wordpress-6.2/block-patterns.php
+++ b/lib/compat/wordpress-6.2/block-patterns.php
@@ -225,6 +225,7 @@ function gutenberg_register_theme_block_patterns() {
'blockTypes' => 'Block Types',
'postTypes' => 'Post Types',
'inserter' => 'Inserter',
+ 'templateTypes' => 'Template Types',
);
/*
@@ -294,7 +295,7 @@ function gutenberg_register_theme_block_patterns() {
}
// For properties of type array, parse data as comma-separated.
- foreach ( array( 'categories', 'keywords', 'blockTypes', 'postTypes' ) as $property ) {
+ foreach ( array( 'categories', 'keywords', 'blockTypes', 'postTypes', 'templateTypes' ) as $property ) {
if ( ! empty( $pattern_data[ $property ] ) ) {
$pattern_data[ $property ] = array_filter(
preg_split(
diff --git a/lib/compat/wordpress-6.2/block-template-utils.php b/lib/compat/wordpress-6.2/block-template-utils.php
new file mode 100644
index 00000000000000..832b9714dd13d3
--- /dev/null
+++ b/lib/compat/wordpress-6.2/block-template-utils.php
@@ -0,0 +1,112 @@
+ The template hierarchy.
+ */
+function gutenberg_get_template_hierarchy( $slug, $is_custom = false, $template_prefix = '' ) {
+ if ( 'index' === $slug ) {
+ return array( 'index' );
+ }
+ if ( $is_custom ) {
+ return array( 'page', 'singular', 'index' );
+ }
+ if ( 'front-page' === $slug ) {
+ return array( 'front-page', 'home', 'index' );
+ }
+
+ $template_hierarchy = array( $slug );
+ // Most default templates don't have `$template_prefix` assigned.
+ if ( ! empty( $template_prefix ) ) {
+ list($type) = explode( '-', $template_prefix );
+ // We need these checks because we always add the `$slug` above.
+ if ( ! in_array( $template_prefix, array( $slug, $type ), true ) ) {
+ $template_hierarchy[] = $template_prefix;
+ }
+ if ( $slug !== $type ) {
+ $template_hierarchy[] = $type;
+ }
+ } else {
+ $matches = array();
+ if ( preg_match( '/^(author|category|archive|tag|page)-(.+)$/', $slug, $matches ) ) {
+ $template_hierarchy[] = $matches[1];
+ } elseif ( preg_match( '/^(single|taxonomy)-(.+)$/', $slug, $matches ) ) {
+ $type = $matches[1];
+ $slug_remaining = $matches[2];
+ if ( 'single' === $type ) {
+ $post_types = get_post_types();
+ foreach ( $post_types as $post_type ) {
+ if ( str_starts_with( $slug_remaining, $post_type ) ) {
+ // If $slug_remaining is equal to $post_type we have the single-$post_type template.
+ if ( $slug_remaining === $post_type ) {
+ $template_hierarchy[] = 'single';
+ break;
+ }
+ // If $slug_remaining is single-$post_type-$slug template.
+ if ( str_starts_with( $slug_remaining, $post_type . '-' ) && strlen( $slug_remaining ) > strlen( $post_type ) + 1 ) {
+ $template_hierarchy[] = "single-$post_type";
+ $template_hierarchy[] = 'single';
+ break;
+ }
+ }
+ }
+ } elseif ( 'taxonomy' === $type ) {
+ $taxonomies = get_taxonomies();
+ foreach ( $taxonomies as $taxonomy ) {
+ if ( str_starts_with( $slug_remaining, $taxonomy ) ) {
+ // If $slug_remaining is equal to $taxonomy we have the taxonomy-$taxonomy template.
+ if ( $slug_remaining === $taxonomy ) {
+ $template_hierarchy[] = 'taxonomy';
+ break;
+ }
+ // If $slug_remaining is taxonomy-$taxonomy-$term template.
+ if ( str_starts_with( $slug_remaining, $taxonomy . '-' ) && strlen( $slug_remaining ) > strlen( $taxonomy ) + 1 ) {
+ $template_hierarchy[] = "taxonomy-$taxonomy";
+ $template_hierarchy[] = 'taxonomy';
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ // Handle `archive` template.
+ if (
+ str_starts_with( $slug, 'author' ) ||
+ str_starts_with( $slug, 'taxonomy' ) ||
+ str_starts_with( $slug, 'category' ) ||
+ str_starts_with( $slug, 'tag' ) ||
+ 'date' === $slug
+ ) {
+ $template_hierarchy[] = 'archive';
+ }
+ // Handle `single` template.
+ if ( 'attachment' === $slug ) {
+ $template_hierarchy[] = 'single';
+ }
+ // Handle `singular` template.
+ if (
+ str_starts_with( $slug, 'single' ) ||
+ str_starts_with( $slug, 'page' ) ||
+ 'attachment' === $slug
+ ) {
+ $template_hierarchy[] = 'singular';
+ }
+ $template_hierarchy[] = 'index';
+ return $template_hierarchy;
+}
diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php b/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php
index d34160edb2af0b..604818fcd30a70 100644
--- a/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php
+++ b/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php
@@ -34,6 +34,128 @@ class Gutenberg_REST_Block_Patterns_Controller_6_2 extends Gutenberg_REST_Block_
'query' => 'posts',
);
+ /**
+ * Prepare a raw block pattern before it gets output in a REST API response.
+ *
+ * @since 6.0.0
+ *
+ * @param array $item Raw pattern as registered, before any changes.
+ * @param WP_REST_Request $request Request object.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function prepare_item_for_response( $item, $request ) {
+ $fields = $this->get_fields_for_response( $request );
+ $keys = array(
+ 'name' => 'name',
+ 'title' => 'title',
+ 'description' => 'description',
+ 'viewportWidth' => 'viewport_width',
+ 'blockTypes' => 'block_types',
+ 'postTypes' => 'post_types',
+ 'categories' => 'categories',
+ 'keywords' => 'keywords',
+ 'content' => 'content',
+ 'inserter' => 'inserter',
+ 'templateTypes' => 'template_types',
+ );
+ $data = array();
+ foreach ( $keys as $item_key => $rest_key ) {
+ if ( isset( $item[ $item_key ] ) && rest_is_field_included( $rest_key, $fields ) ) {
+ $data[ $rest_key ] = $item[ $item_key ];
+ }
+ }
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+ $data = $this->add_additional_fields_to_object( $data, $request );
+ $data = $this->filter_response_by_context( $data, $context );
+ return rest_ensure_response( $data );
+ }
+
+ /**
+ * Retrieves the block pattern schema, conforming to JSON Schema.
+ *
+ * @since 6.0.0
+ * @since 6.1.0 Added `post_types` property.
+ *
+ * @return array Item schema data.
+ */
+ public function get_item_schema() {
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'block-pattern',
+ 'type' => 'object',
+ 'properties' => array(
+ 'name' => array(
+ 'description' => __( 'The pattern name.', 'gutenberg' ),
+ 'type' => 'string',
+ 'readonly' => true,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'title' => array(
+ 'description' => __( 'The pattern title, in human readable format.', 'gutenberg' ),
+ 'type' => 'string',
+ 'readonly' => true,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'description' => array(
+ 'description' => __( 'The pattern detailed description.', 'gutenberg' ),
+ 'type' => 'string',
+ 'readonly' => true,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'viewport_width' => array(
+ 'description' => __( 'The pattern viewport width for inserter preview.', 'gutenberg' ),
+ 'type' => 'number',
+ 'readonly' => true,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'block_types' => array(
+ 'description' => __( 'Block types that the pattern is intended to be used with.', 'gutenberg' ),
+ 'type' => 'array',
+ 'readonly' => true,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'post_types' => array(
+ 'description' => __( 'An array of post types that the pattern is restricted to be used with.', 'gutenberg' ),
+ 'type' => 'array',
+ 'readonly' => true,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'categories' => array(
+ 'description' => __( 'The pattern category slugs.', 'gutenberg' ),
+ 'type' => 'array',
+ 'readonly' => true,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'keywords' => array(
+ 'description' => __( 'The pattern keywords.', 'gutenberg' ),
+ 'type' => 'array',
+ 'readonly' => true,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'template_types' => array(
+ 'description' => __( 'An array of template types where the pattern fits.', 'gutenberg' ),
+ 'type' => 'array',
+ 'readonly' => true,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'content' => array(
+ 'description' => __( 'The pattern content.', 'gutenberg' ),
+ 'type' => 'string',
+ 'readonly' => true,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'inserter' => array(
+ 'description' => __( 'Determines whether the pattern is visible in inserter.', 'gutenberg' ),
+ 'type' => 'boolean',
+ 'readonly' => true,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ ),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
/**
* Registers the routes for the objects of the controller.
*
diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php b/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php
index 9f762dd961d058..643374aba490cc 100644
--- a/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php
+++ b/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php
@@ -151,10 +151,12 @@ protected function prepare_item_for_database( $request ) {
if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) {
$config = array();
if ( isset( $request['styles'] ) ) {
- $config['styles'] = $request['styles'];
- $validate_custom_css = $this->validate_custom_css( $request['styles']['css'] );
- if ( is_wp_error( $validate_custom_css ) ) {
- return $validate_custom_css;
+ $config['styles'] = $request['styles'];
+ if ( isset( $request['styles']['css'] ) ) {
+ $validate_custom_css = $this->validate_custom_css( $request['styles']['css'] );
+ if ( is_wp_error( $validate_custom_css ) ) {
+ return $validate_custom_css;
+ }
}
} elseif ( isset( $existing_config['styles'] ) ) {
$config['styles'] = $existing_config['styles'];
@@ -199,4 +201,55 @@ private function validate_custom_css( $css ) {
}
return true;
}
+
+ /**
+ * Returns the given theme global styles config.
+ * Duplicated from core.
+ * The only change is that we call WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( 'theme' ) instead of WP_Theme_JSON_Resolver::get_merged_data( 'theme' ).
+ *
+ * @since 6.2.0
+ *
+ * @param WP_REST_Request $request The request instance.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function get_theme_item( $request ) {
+ if ( get_stylesheet() !== $request['stylesheet'] ) {
+ // This endpoint only supports the active theme for now.
+ return new WP_Error(
+ 'rest_theme_not_found',
+ __( 'Theme not found.', 'gutenberg' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ $theme = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( 'theme' );
+ $data = array();
+ $fields = $this->get_fields_for_response( $request );
+
+ if ( rest_is_field_included( 'settings', $fields ) ) {
+ $data['settings'] = $theme->get_settings();
+ }
+
+ if ( rest_is_field_included( 'styles', $fields ) ) {
+ $raw_data = $theme->get_raw_data();
+ $data['styles'] = isset( $raw_data['styles'] ) ? $raw_data['styles'] : array();
+ }
+
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+ $data = $this->add_additional_fields_to_object( $data, $request );
+ $data = $this->filter_response_by_context( $data, $context );
+
+ $response = rest_ensure_response( $data );
+
+ if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
+ $links = array(
+ 'self' => array(
+ 'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ),
+ ),
+ );
+ $response->add_links( $links );
+ }
+
+ return $response;
+ }
}
diff --git a/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php b/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php
deleted file mode 100644
index fe01eb273066e2..00000000000000
--- a/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php
+++ /dev/null
@@ -1,391 +0,0 @@
- array( 'color', 'gradient' ),
- 'background-color' => array( 'color', 'background' ),
- 'border-radius' => array( 'border', 'radius' ),
- 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ),
- 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ),
- 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ),
- 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ),
- 'border-color' => array( 'border', 'color' ),
- 'border-width' => array( 'border', 'width' ),
- 'border-style' => array( 'border', 'style' ),
- 'border-top-color' => array( 'border', 'top', 'color' ),
- 'border-top-width' => array( 'border', 'top', 'width' ),
- 'border-top-style' => array( 'border', 'top', 'style' ),
- 'border-right-color' => array( 'border', 'right', 'color' ),
- 'border-right-width' => array( 'border', 'right', 'width' ),
- 'border-right-style' => array( 'border', 'right', 'style' ),
- 'border-bottom-color' => array( 'border', 'bottom', 'color' ),
- 'border-bottom-width' => array( 'border', 'bottom', 'width' ),
- 'border-bottom-style' => array( 'border', 'bottom', 'style' ),
- 'border-left-color' => array( 'border', 'left', 'color' ),
- 'border-left-width' => array( 'border', 'left', 'width' ),
- 'border-left-style' => array( 'border', 'left', 'style' ),
- 'color' => array( 'color', 'text' ),
- 'font-family' => array( 'typography', 'fontFamily' ),
- 'font-size' => array( 'typography', 'fontSize' ),
- 'font-style' => array( 'typography', 'fontStyle' ),
- 'font-weight' => array( 'typography', 'fontWeight' ),
- 'letter-spacing' => array( 'typography', 'letterSpacing' ),
- 'line-height' => array( 'typography', 'lineHeight' ),
- 'margin' => array( 'spacing', 'margin' ),
- 'margin-top' => array( 'spacing', 'margin', 'top' ),
- 'margin-right' => array( 'spacing', 'margin', 'right' ),
- 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ),
- 'margin-left' => array( 'spacing', 'margin', 'left' ),
- 'min-height' => array( 'dimensions', 'minHeight' ),
- 'padding' => array( 'spacing', 'padding' ),
- 'padding-top' => array( 'spacing', 'padding', 'top' ),
- 'padding-right' => array( 'spacing', 'padding', 'right' ),
- 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
- 'padding-left' => array( 'spacing', 'padding', 'left' ),
- '--wp--style--root--padding' => array( 'spacing', 'padding' ),
- '--wp--style--root--padding-top' => array( 'spacing', 'padding', 'top' ),
- '--wp--style--root--padding-right' => array( 'spacing', 'padding', 'right' ),
- '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
- '--wp--style--root--padding-left' => array( 'spacing', 'padding', 'left' ),
- 'text-decoration' => array( 'typography', 'textDecoration' ),
- 'text-transform' => array( 'typography', 'textTransform' ),
- 'filter' => array( 'filter', 'duotone' ),
- 'box-shadow' => array( 'shadow' ),
- );
-
- /**
- * Indirect metadata for style properties that are not directly output.
- *
- * Each element is a direct mapping from a CSS property name to the
- * path to the value in theme.json & block attributes.
- *
- * Indirect properties are not output directly by `compute_style_properties`,
- * but are used elsewhere in the processing of global styles. The indirect
- * property is used to validate whether or not a style value is allowed.
- *
- * @since 6.2.0
- * @var array
- */
- const INDIRECT_PROPERTIES_METADATA = array(
- 'gap' => array( 'spacing', 'blockGap' ),
- 'column-gap' => array( 'spacing', 'blockGap', 'left' ),
- 'row-gap' => array( 'spacing', 'blockGap', 'top' ),
- );
-
- /**
- * The valid properties under the settings key.
- *
- * @since 5.8.0 As `ALLOWED_SETTINGS`.
- * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`,
- * added new properties for `border`, `color`, `spacing`,
- * and `typography`, and renamed others according to the new schema.
- * @since 6.0.0 Added `color.defaultDuotone`.
- * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`.
- * @since 6.2.0 Added `dimensions.minHeight`.
- * @var array
- */
- const VALID_SETTINGS = array(
- 'appearanceTools' => null,
- 'useRootPaddingAwareAlignments' => null,
- 'border' => array(
- 'color' => null,
- 'radius' => null,
- 'style' => null,
- 'width' => null,
- ),
- 'color' => array(
- 'background' => null,
- 'custom' => null,
- 'customDuotone' => null,
- 'customGradient' => null,
- 'defaultDuotone' => null,
- 'defaultGradients' => null,
- 'defaultPalette' => null,
- 'duotone' => null,
- 'gradients' => null,
- 'link' => null,
- 'palette' => null,
- 'text' => null,
- ),
- 'custom' => null,
- 'dimensions' => array(
- 'minHeight' => null,
- ),
- 'layout' => array(
- 'contentSize' => null,
- 'definitions' => null,
- 'wideSize' => null,
- ),
- 'spacing' => array(
- 'customSpacingSize' => null,
- 'spacingSizes' => null,
- 'spacingScale' => null,
- 'blockGap' => null,
- 'margin' => null,
- 'padding' => null,
- 'units' => null,
- ),
- 'typography' => array(
- 'fluid' => null,
- 'customFontSize' => null,
- 'dropCap' => null,
- 'fontFamilies' => null,
- 'fontSizes' => null,
- 'fontStyle' => null,
- 'fontWeight' => null,
- 'letterSpacing' => null,
- 'lineHeight' => null,
- 'textDecoration' => null,
- 'textTransform' => null,
- ),
- );
-
- /**
- * The valid properties under the styles key.
- *
- * @since 5.8.0 As `ALLOWED_STYLES`.
- * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`,
- * added new properties for `border`, `filter`, `spacing`,
- * and `typography`.
- * @since 6.1.0 Added new side properties for `border`,
- * added new property `shadow`,
- * updated `blockGap` to be allowed at any level.
- * @since 6.2.0 Added new property `css`.
- * @var array
- */
- const VALID_STYLES = array(
- 'border' => array(
- 'color' => null,
- 'radius' => null,
- 'style' => null,
- 'width' => null,
- 'top' => null,
- 'right' => null,
- 'bottom' => null,
- 'left' => null,
- ),
- 'color' => array(
- 'background' => null,
- 'gradient' => null,
- 'text' => null,
- ),
- 'dimensions' => array(
- 'minHeight' => null,
- ),
- 'filter' => array(
- 'duotone' => null,
- ),
- 'shadow' => null,
- 'spacing' => array(
- 'margin' => null,
- 'padding' => null,
- 'blockGap' => null,
- ),
- 'typography' => array(
- 'fontFamily' => null,
- 'fontSize' => null,
- 'fontStyle' => null,
- 'fontWeight' => null,
- 'letterSpacing' => null,
- 'lineHeight' => null,
- 'textDecoration' => null,
- 'textTransform' => null,
- ),
- 'css' => null,
- );
-
- /**
- * Processes a style node and returns the same node
- * without the insecure styles.
- *
- * @since 5.9.0
- * @since 6.2.0 Allow indirect properties used outside of `compute_style_properties`.
- *
- * @param array $input Node to process.
- * @return array
- */
- protected static function remove_insecure_styles( $input ) {
- $output = array();
- $declarations = static::compute_style_properties( $input );
-
- foreach ( $declarations as $declaration ) {
- if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) {
- $path = static::PROPERTIES_METADATA[ $declaration['name'] ];
-
- // Check the value isn't an array before adding so as to not
- // double up shorthand and longhand styles.
- $value = _wp_array_get( $input, $path, array() );
- if ( ! is_array( $value ) ) {
- _wp_array_set( $output, $path, $value );
- }
- }
- }
-
- // Ensure indirect properties not handled by `compute_style_properties` are allowed.
- foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $path ) {
- $value = _wp_array_get( $input, $path, array() );
- if (
- isset( $value ) &&
- ! is_array( $value ) &&
- static::is_safe_css_declaration( $property, $value )
- ) {
- _wp_array_set( $output, $path, $value );
- }
- }
-
- return $output;
- }
-
- /**
- * Returns the stylesheet that results of processing
- * the theme.json structure this object represents.
- *
- * @param array $types Types of styles to load. Will load all by default. It accepts:
- * 'variables': only the CSS Custom Properties for presets & custom ones.
- * 'styles': only the styles section in theme.json.
- * 'presets': only the classes for the presets.
- * 'custom-css': only the css from global styles.css.
- * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS.
- * @param array $options An array of options for now used for internal purposes only (may change without notice).
- * The options currently supported are 'scope' that makes sure all style are scoped to a given selector,
- * and root_selector which overwrites and forces a given selector to be used on the root node.
- * @return string Stylesheet.
- */
- public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) {
- if ( null === $origins ) {
- $origins = static::VALID_ORIGINS;
- }
-
- if ( is_string( $types ) ) {
- // Dispatch error and map old arguments to new ones.
- _deprecated_argument( __FUNCTION__, '5.9' );
- if ( 'block_styles' === $types ) {
- $types = array( 'styles', 'presets' );
- } elseif ( 'css_variables' === $types ) {
- $types = array( 'variables' );
- } else {
- $types = array( 'variables', 'styles', 'presets' );
- }
- }
-
- $blocks_metadata = static::get_blocks_metadata();
- $style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata );
- $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
-
- $root_style_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true );
- $root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true );
-
- if ( ! empty( $options['scope'] ) ) {
- foreach ( $setting_nodes as &$node ) {
- $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
- }
- foreach ( $style_nodes as &$node ) {
- $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
- }
- }
-
- if ( ! empty( $options['root_selector'] ) ) {
- if ( false !== $root_settings_key ) {
- $setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector'];
- }
- if ( false !== $root_style_key ) {
- $setting_nodes[ $root_style_key ]['selector'] = $options['root_selector'];
- }
- }
-
- $stylesheet = '';
-
- if ( in_array( 'variables', $types, true ) ) {
- $stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
- }
-
- if ( in_array( 'styles', $types, true ) ) {
- if ( false !== $root_style_key ) {
- $stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] );
- }
- $stylesheet .= $this->get_block_classes( $style_nodes );
- } elseif ( in_array( 'base-layout-styles', $types, true ) ) {
- $root_selector = static::ROOT_BLOCK_SELECTOR;
- $columns_selector = '.wp-block-columns';
- if ( ! empty( $options['scope'] ) ) {
- $root_selector = static::scope_selector( $options['scope'], $root_selector );
- $columns_selector = static::scope_selector( $options['scope'], $columns_selector );
- }
- if ( ! empty( $options['root_selector'] ) ) {
- $root_selector = $options['root_selector'];
- }
- // Base layout styles are provided as part of `styles`, so only output separately if explicitly requested.
- // For backwards compatibility, the Columns block is explicitly included, to support a different default gap value.
- $base_styles_nodes = array(
- array(
- 'path' => array( 'styles' ),
- 'selector' => $root_selector,
- ),
- array(
- 'path' => array( 'styles', 'blocks', 'core/columns' ),
- 'selector' => $columns_selector,
- 'name' => 'core/columns',
- ),
- );
-
- foreach ( $base_styles_nodes as $base_style_node ) {
- $stylesheet .= $this->get_layout_styles( $base_style_node );
- }
- }
-
- if ( in_array( 'presets', $types, true ) ) {
- $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
- }
-
- // Load the custom CSS last so it has the highest specificity.
- if ( in_array( 'custom-css', $types, true ) ) {
- $stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) );
- }
-
- return $stylesheet;
- }
-}
diff --git a/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php b/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php
deleted file mode 100644
index 110c8bac7b147c..00000000000000
--- a/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php
+++ /dev/null
@@ -1,230 +0,0 @@
-get_stylesheet() === get_stylesheet() && ! wp_theme_has_theme_json() ) {
- return array();
- }
-
- $user_cpt = array();
- $post_type_filter = 'wp_global_styles';
- $stylesheet = $theme->get_stylesheet();
- $args = array(
- 'posts_per_page' => 1,
- 'orderby' => 'date',
- 'order' => 'desc',
- 'post_type' => $post_type_filter,
- 'post_status' => $post_status_filter,
- 'ignore_sticky_posts' => true,
- 'no_found_rows' => true,
- 'update_post_meta_cache' => false,
- 'update_post_term_cache' => false,
- 'tax_query' => array(
- array(
- 'taxonomy' => 'wp_theme',
- 'field' => 'name',
- 'terms' => $stylesheet,
- ),
- ),
- );
-
- $global_style_query = new WP_Query();
- $recent_posts = $global_style_query->query( $args );
- if ( count( $recent_posts ) === 1 ) {
- $user_cpt = get_object_vars( $recent_posts[0] );
- } elseif ( $create_post ) {
- $cpt_post_id = wp_insert_post(
- array(
- 'post_content' => '{"version": ' . WP_Theme_JSON::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }',
- 'post_status' => 'publish',
- 'post_title' => 'Custom Styles', // Do not make string translatable, see https://core.trac.wordpress.org/ticket/54518.
- 'post_type' => $post_type_filter,
- 'post_name' => sprintf( 'wp-global-styles-%s', urlencode( $stylesheet ) ),
- 'tax_input' => array(
- 'wp_theme' => array( $stylesheet ),
- ),
- ),
- true
- );
- if ( ! is_wp_error( $cpt_post_id ) ) {
- $user_cpt = get_object_vars( get_post( $cpt_post_id ) );
- }
- }
-
- return $user_cpt;
- }
-
- /**
- * Determines whether the active theme has a theme.json file.
- *
- * @since 5.8.0
- * @since 5.9.0 Added a check in the parent theme.
- * @deprecated 6.2.0 Use wp_theme_has_theme_json() instead.
- *
- * @return bool
- */
- public static function theme_has_support() {
- _deprecated_function( __METHOD__, '6.2.0', 'wp_theme_has_theme_json()' );
-
- return wp_theme_has_theme_json();
- }
-
- /**
- * Returns the data merged from multiple origins.
- *
- * There are four sources of data (origins) for a site:
- *
- * - default => WordPress
- * - blocks => each one of the blocks provides data for itself
- * - theme => the active theme
- * - custom => data provided by the user
- *
- * The custom's has higher priority than the theme's, the theme's higher than blocks',
- * and block's higher than default's.
- *
- * Unlike the getters
- * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_core_data/ get_core_data},
- * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_theme_data/ get_theme_data},
- * and {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_user_data/ get_user_data},
- * this method returns data after it has been merged with the previous origins.
- * This means that if the same piece of data is declared in different origins
- * (default, blocks, theme, custom), the last origin overrides the previous.
- *
- * For example, if the user has set a background color
- * for the paragraph block, and the theme has done it as well,
- * the user preference wins.
- *
- * @param string $origin Optional. To what level should we merge data:'default', 'blocks', 'theme' or 'custom'.
- * 'custom' is used as default value as well as fallback value if the origin is unknown.
- *
- * @return WP_Theme_JSON
- */
- public static function get_merged_data( $origin = 'custom' ) {
- if ( is_array( $origin ) ) {
- _deprecated_argument( __FUNCTION__, '5.9.0' );
- }
-
- $result = static::get_core_data();
- if ( 'default' === $origin ) {
- $result->set_spacing_sizes();
- return $result;
- }
-
- $result->merge( static::get_block_data() );
- if ( 'blocks' === $origin ) {
- return $result;
- }
-
- $result->merge( static::get_theme_data() );
- if ( 'theme' === $origin ) {
- $result->set_spacing_sizes();
- return $result;
- }
-
- $result->merge( static::get_user_data() );
- $result->set_spacing_sizes();
- return $result;
- }
-
- /**
- * Returns the user's origin config.
- *
- * @since 6.2 Added check for the WP_Theme_JSON_Gutenberg class to prevent $user
- * values set in core fron overriding the new custom css values added to VALID_STYLES.
- * This does not need to be backported to core as the new VALID_STYLES[css] value will
- * be added to core with 6.2.
- *
- * @return WP_Theme_JSON_Gutenberg Entity that holds styles for user data.
- */
- public static function get_user_data() {
- if ( null !== static::$user && static::$user instanceof WP_Theme_JSON_Gutenberg ) {
- return static::$user;
- }
-
- $config = array();
- $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() );
-
- if ( array_key_exists( 'post_content', $user_cpt ) ) {
- $decoded_data = json_decode( $user_cpt['post_content'], true );
-
- $json_decoding_error = json_last_error();
- if ( JSON_ERROR_NONE !== $json_decoding_error ) {
- trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() );
- /**
- * Filters the data provided by the user for global styles & settings.
- *
- * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data.
- */
- $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) );
- $config = $theme_json->get_data();
- return new WP_Theme_JSON_Gutenberg( $config, 'custom' );
- }
-
- // Very important to verify if the flag isGlobalStylesUserThemeJSON is true.
- // If is not true the content was not escaped and is not safe.
- if (
- is_array( $decoded_data ) &&
- isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
- $decoded_data['isGlobalStylesUserThemeJSON']
- ) {
- unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
- $config = $decoded_data;
- }
- }
-
- /**
- * Filters the data provided by the user for global styles & settings.
- *
- * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data.
- */
- $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) );
- $config = $theme_json->get_data();
-
- static::$user = new WP_Theme_JSON_Gutenberg( $config, 'custom' );
-
- return static::$user;
- }
-}
diff --git a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php
index 8556a0be1663f4..4006b4c23cc0b1 100644
--- a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php
+++ b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php
@@ -63,9 +63,10 @@ function wp_theme_has_theme_json_clean_cache() {
* Returns the stylesheet resulting of merging core, theme, and user data.
*
* @param array $types Types of styles to load. Optional.
- * It accepts 'variables', 'styles', 'presets', 'custom-css' as values.
- * If empty, it'll load all for themes with theme.json support
- * and only [ 'variables', 'presets' ] for themes without theme.json support.
+ * It accepts as values: 'variables', 'presets', 'styles', 'base-layout-styles, and 'custom-css'.
+ * If empty, it'll load the following:
+ * - for themes without theme.json: 'variables', 'presets', 'base-layout-styles'.
+ * - for temes with theme.json: 'variables', 'presets', 'styles', 'custom-css'.
*
* @return string Stylesheet.
*/
@@ -85,7 +86,7 @@ function gutenberg_get_global_stylesheet( $types = array() ) {
if ( empty( $types ) && ! $supports_theme_json ) {
$types = array( 'variables', 'presets', 'base-layout-styles' );
} elseif ( empty( $types ) ) {
- $types = array( 'variables', 'styles', 'presets', 'custom-css' );
+ $types = array( 'variables', 'presets', 'styles', 'custom-css' );
}
/*
diff --git a/lib/compat/wordpress-6.2/rest-api.php b/lib/compat/wordpress-6.2/rest-api.php
index 4900a622b6c01b..12f7afda3b4d5d 100644
--- a/lib/compat/wordpress-6.2/rest-api.php
+++ b/lib/compat/wordpress-6.2/rest-api.php
@@ -101,3 +101,18 @@ function gutenberg_register_global_styles_endpoints() {
$editor_settings->register_routes();
}
add_action( 'rest_api_init', 'gutenberg_register_global_styles_endpoints' );
+
+/**
+ * Updates REST API response for the sidebars and marks them as 'inactive'.
+ *
+ * Note: This can be a part of the `prepare_item_for_response` in `class-wp-rest-sidebars-controller.php`.
+ *
+ * @param WP_REST_Response $response The sidebar response object.
+ * @return WP_REST_Response $response Updated response object.
+ */
+function gutenberg_modify_rest_sidebars_response( $response ) {
+ $response->data['status'] = wp_is_block_theme() ? 'inactive' : 'active';
+
+ return $response;
+}
+add_filter( 'rest_prepare_sidebar', 'gutenberg_modify_rest_sidebars_response' );
diff --git a/lib/compat/wordpress-6.2/script-loader.php b/lib/compat/wordpress-6.2/script-loader.php
index e08bfa6e46ca8e..e3e11d9cf1f563 100644
--- a/lib/compat/wordpress-6.2/script-loader.php
+++ b/lib/compat/wordpress-6.2/script-loader.php
@@ -127,6 +127,30 @@ function gutenberg_resolve_assets_override() {
$scripts = ob_get_clean();
+ /*
+ * Generate web font @font-face styles for the site editor iframe.
+ * Use the registered font families for printing.
+ */
+ if ( class_exists( 'WP_Web_Fonts' ) ) {
+ $wp_webfonts = wp_webfonts();
+ $registered = $wp_webfonts->get_registered_font_families();
+ if ( ! empty( $registered ) ) {
+ $queue = $wp_webfonts->queue;
+ $done = $wp_webfonts->done;
+
+ $wp_webfonts->done = array();
+ $wp_webfonts->queue = $registered;
+
+ ob_start();
+ $wp_webfonts->do_items();
+ $styles .= ob_get_clean();
+
+ // Reset the Web Fonts API.
+ $wp_webfonts->done = $done;
+ $wp_webfonts->queue = $queue;
+ }
+ }
+
return array(
'styles' => $styles,
'scripts' => $scripts,
@@ -137,7 +161,8 @@ function gutenberg_resolve_assets_override() {
'block_editor_settings_all',
function( $settings ) {
// We must override what core is passing now.
- $settings['__unstableResolvedAssets'] = gutenberg_resolve_assets_override();
+ $settings['__unstableResolvedAssets'] = gutenberg_resolve_assets_override();
+ $settings['__unstableIsBlockBasedTheme'] = wp_is_block_theme();
return $settings;
},
100
diff --git a/lib/compat/wordpress-6.2/theme.php b/lib/compat/wordpress-6.2/theme.php
new file mode 100644
index 00000000000000..79d55206449472
--- /dev/null
+++ b/lib/compat/wordpress-6.2/theme.php
@@ -0,0 +1,23 @@
+is_block_theme() ) {
+ set_theme_mod( 'wp_legacy_sidebars', $wp_registered_sidebars );
+ }
+}
+add_action( 'switch_theme', 'gutenberg_set_legacy_sidebars', 10, 2 );
diff --git a/lib/compat/wordpress-6.2/widgets.php b/lib/compat/wordpress-6.2/widgets.php
new file mode 100644
index 00000000000000..19591ae64607e3
--- /dev/null
+++ b/lib/compat/wordpress-6.2/widgets.php
@@ -0,0 +1,32 @@
+ array( 'post-editor', 'site-editor', 'widgets-editor' ),
),
+ '__experimentalBlockInspectorAnimation' => array(
+ 'description' => __( 'Whether to enable animation when showing and hiding the block inspector.', 'gutenberg' ),
+ 'type' => 'object',
+ 'context' => array( 'site-editor' ),
+ ),
+
'alignWide' => array(
'description' => __( 'Enable/Disable Wide/Full Alignments.', 'gutenberg' ),
'type' => 'boolean',
diff --git a/lib/experimental/class-wp-theme-json-gutenberg.php b/lib/experimental/class-wp-theme-json-gutenberg.php
deleted file mode 100644
index a16697dff07d07..00000000000000
--- a/lib/experimental/class-wp-theme-json-gutenberg.php
+++ /dev/null
@@ -1,31 +0,0 @@
- true ) ) {
- if ( ! empty( $deprecated ) ) {
- _deprecated_argument( __METHOD__, '5.9' );
- }
-
- // When backporting to core, remove the instanceof Gutenberg class check, as it is only required for the Gutenberg plugin.
- if ( null === static::$theme || ! static::has_same_registered_blocks( 'theme' ) ) {
- $theme_json_file = static::get_file_path_from_theme( 'theme.json' );
- $wp_theme = wp_get_theme();
- if ( '' !== $theme_json_file ) {
- $theme_json_data = static::read_json_file( $theme_json_file );
- $theme_json_data = static::translate( $theme_json_data, $wp_theme->get( 'TextDomain' ) );
- } else {
- $theme_json_data = array();
- }
- $theme_json_data = gutenberg_add_registered_webfonts_to_theme_json( $theme_json_data );
-
- /**
- * Filters the data provided by the theme for global styles & settings.
- *
- * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data.
- */
- $theme_json = apply_filters( 'wp_theme_json_data_theme', new WP_Theme_JSON_Data_Gutenberg( $theme_json_data, 'theme' ) );
- $theme_json_data = $theme_json->get_data();
- static::$theme = new WP_Theme_JSON_Gutenberg( $theme_json_data );
-
- if ( $wp_theme->parent() ) {
- // Get parent theme.json.
- $parent_theme_json_file = static::get_file_path_from_theme( 'theme.json', true );
- if ( '' !== $parent_theme_json_file ) {
- $parent_theme_json_data = static::read_json_file( $parent_theme_json_file );
- $parent_theme_json_data = gutenberg_add_registered_webfonts_to_theme_json( $parent_theme_json_data );
- $parent_theme = new WP_Theme_JSON_Gutenberg( $parent_theme_json_data );
-
- /*
- * Merge the child theme.json into the parent theme.json.
- * The child theme takes precedence over the parent.
- */
- $parent_theme->merge( static::$theme );
- static::$theme = $parent_theme;
- }
- }
- }
-
- if ( ! $settings['with_supports'] ) {
- return static::$theme;
- }
-
- /*
- * We want the presets and settings declared in theme.json
- * to override the ones declared via theme supports.
- * So we take theme supports, transform it to theme.json shape
- * and merge the static::$theme upon that.
- */
- $theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( gutenberg_get_legacy_theme_supports_for_theme_json() );
- if ( ! wp_theme_has_theme_json() ) {
- if ( ! isset( $theme_support_data['settings']['color'] ) ) {
- $theme_support_data['settings']['color'] = array();
- }
-
- $default_palette = false;
- if ( current_theme_supports( 'default-color-palette' ) ) {
- $default_palette = true;
- }
- if ( ! isset( $theme_support_data['settings']['color']['palette'] ) ) {
- // If the theme does not have any palette, we still want to show the core one.
- $default_palette = true;
- }
- $theme_support_data['settings']['color']['defaultPalette'] = $default_palette;
-
- $default_gradients = false;
- if ( current_theme_supports( 'default-gradient-presets' ) ) {
- $default_gradients = true;
- }
- if ( ! isset( $theme_support_data['settings']['color']['gradients'] ) ) {
- // If the theme does not have any gradients, we still want to show the core ones.
- $default_gradients = true;
- }
- $theme_support_data['settings']['color']['defaultGradients'] = $default_gradients;
-
- // Classic themes without a theme.json don't support global duotone.
- $theme_support_data['settings']['color']['defaultDuotone'] = false;
-
- // Allow themes to enable appearance tools via theme_support.
- if ( current_theme_supports( 'appearance-tools' ) ) {
- $theme_support_data['settings']['appearanceTools'] = true;
- }
- }
- $with_theme_supports = new WP_Theme_JSON_Gutenberg( $theme_support_data );
- $with_theme_supports->merge( static::$theme );
-
- return $with_theme_supports;
- }
-
- /**
- * Gets the styles for blocks from the block.json file.
- *
- * @return WP_Theme_JSON
- */
- public static function get_block_data() {
- $registry = WP_Block_Type_Registry::get_instance();
- $blocks = $registry->get_all_registered();
- $config = array( 'version' => 1 );
- foreach ( $blocks as $block_name => $block_type ) {
- if ( isset( $block_type->supports['__experimentalStyle'] ) ) {
- $config['styles']['blocks'][ $block_name ] = static::remove_JSON_comments( $block_type->supports['__experimentalStyle'] );
- }
-
- if (
- isset( $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ) &&
- null === _wp_array_get( $config, array( 'styles', 'blocks', $block_name, 'spacing', 'blockGap' ), null )
- ) {
- // Ensure an empty placeholder value exists for the block, if it provides a default blockGap value.
- // The real blockGap value to be used will be determined when the styles are rendered for output.
- $config['styles']['blocks'][ $block_name ]['spacing']['blockGap'] = null;
- }
- }
-
- /**
- * Filters the data provided by the blocks for global styles & settings.
- *
- * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data.
- */
- $theme_json = apply_filters( 'wp_theme_json_data_blocks', new WP_Theme_JSON_Data_Gutenberg( $config, 'blocks' ) );
- $config = $theme_json->get_data();
-
- return new WP_Theme_JSON_Gutenberg( $config, 'blocks' );
- }
-
- /**
- * When given an array, this will remove any keys with the name `//`.
- *
- * @param array $array The array to filter.
- * @return array The filtered array.
- */
- private static function remove_JSON_comments( $array ) {
- unset( $array['//'] );
- foreach ( $array as $k => $v ) {
- if ( is_array( $v ) ) {
- $array[ $k ] = static::remove_JSON_comments( $v );
- }
- }
-
- return $array;
- }
-
-}
diff --git a/lib/experimental/class-wp-web-fonts.php b/lib/experimental/class-wp-web-fonts.php
new file mode 100644
index 00000000000000..15df7ecf47e5c5
--- /dev/null
+++ b/lib/experimental/class-wp-web-fonts.php
@@ -0,0 +1,744 @@
+ 'local',
+ 'font-family' => '',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ );
+
+ /**
+ * Constructor.
+ *
+ * @since X.X.X
+ */
+ public function __construct() {
+ /**
+ * Filters the web font variation's property defaults.
+ *
+ * @since X.X.X
+ *
+ * @param array $defaults {
+ * An array of required web font properties and defaults.
+ *
+ * @type string $provider The provider ID. Default 'local'.
+ * @type string $font-family The font-family property. Default empty string.
+ * @type string $font-style The font-style property. Default 'normal'.
+ * @type string $font-weight The font-weight property. Default '400'.
+ * @type string $font-display The font-display property. Default 'fallback'.
+ * }
+ */
+ $this->variation_property_defaults = apply_filters( 'wp_webfont_variation_defaults', $this->variation_property_defaults );
+
+ /**
+ * Fires when the WP_Webfonts instance is initialized.
+ *
+ * @since X.X.X
+ *
+ * @param WP_Web_Fonts $wp_webfonts WP_Web_Fonts instance (passed by reference).
+ */
+ do_action_ref_array( 'wp_default_webfonts', array( &$this ) );
+ }
+
+ /**
+ * Get the list of registered providers.
+ *
+ * @since X.X.X
+ *
+ * @return array $providers {
+ * An associative array of registered providers, keyed by their unique ID.
+ *
+ * @type string $provider_id => array {
+ * An associate array of provider's class name and fonts.
+ *
+ * @type string $class Fully qualified name of the provider's class.
+ * @type string[] $fonts An array of enqueued font handles for this provider.
+ * }
+ * }
+ */
+ public function get_providers() {
+ return $this->providers;
+ }
+
+ /**
+ * Register a provider.
+ *
+ * @since X.X.X
+ *
+ * @param string $provider_id The provider's unique ID.
+ * @param string $class The provider class name.
+ * @return bool True if successfully registered, else false.
+ */
+ public function register_provider( $provider_id, $class ) {
+ if ( empty( $provider_id ) || empty( $class ) || ! class_exists( $class ) ) {
+ return false;
+ }
+
+ $this->providers[ $provider_id ] = array(
+ 'class' => $class,
+ 'fonts' => array(),
+ );
+ return true;
+ }
+
+ /**
+ * Get the list of all registered font family handles.
+ *
+ * @since X.X.X
+ *
+ * @return string[]
+ */
+ public function get_registered_font_families() {
+ $font_families = array();
+ foreach ( $this->registered as $handle => $obj ) {
+ if ( $obj->extra['is_font_family'] ) {
+ $font_families[] = $handle;
+ }
+ }
+ return $font_families;
+ }
+
+ /**
+ * Get the list of all registered font families and their variations.
+ *
+ * @since X.X.X
+ *
+ * @return string[]
+ */
+ public function get_registered() {
+ return array_keys( $this->registered );
+ }
+
+ /**
+ * Get the list of enqueued font families and their variations.
+ *
+ * @since X.X.X
+ *
+ * @return array[]
+ */
+ public function get_enqueued() {
+ return $this->queue;
+ }
+
+ /**
+ * Registers a font family.
+ *
+ * @since X.X.X
+ *
+ * @param string $font_family Font family name to register.
+ * @return string|null Font family handle when registration successes. Null on failure.
+ */
+ public function add_font_family( $font_family ) {
+ $font_family_handle = WP_Webfonts_Utils::convert_font_family_into_handle( $font_family );
+ if ( ! $font_family_handle ) {
+ return null;
+ }
+
+ if ( isset( $this->registered[ $font_family_handle ] ) ) {
+ return $font_family_handle;
+ }
+
+ $registered = $this->add( $font_family_handle, false );
+ if ( ! $registered ) {
+ return null;
+ }
+
+ $this->add_data( $font_family_handle, 'font-properties', array( 'font-family' => $font_family ) );
+ $this->add_data( $font_family_handle, 'is_font_family', true );
+
+ return $font_family_handle;
+ }
+
+ /**
+ * Removes a font family and all registered variations.
+ *
+ * @since X.X.X
+ *
+ * @param string $font_family_handle The font family to remove.
+ */
+ public function remove_font_family( $font_family_handle ) {
+ if ( ! isset( $this->registered[ $font_family_handle ] ) ) {
+ return;
+ }
+
+ $variations = $this->registered[ $font_family_handle ]->deps;
+
+ foreach ( $variations as $variation ) {
+ $this->remove( $variation );
+ }
+
+ $this->remove( $font_family_handle );
+ }
+
+ /**
+ * Add a variation to an existing family or register family if none exists.
+ *
+ * @since X.X.X
+ *
+ * @param string $font_family_handle The font family's handle for this variation.
+ * @param array $variation An array of variation properties to add.
+ * @param string $variation_handle Optional. The variation's handle. When none is provided, the
+ * handle will be dynamically generated.
+ * Default empty string.
+ * @return string|null Variation handle on success. Else null.
+ */
+ public function add_variation( $font_family_handle, array $variation, $variation_handle = '' ) {
+ if ( ! WP_Webfonts_Utils::is_defined( $font_family_handle ) ) {
+ trigger_error( 'Font family handle must be a non-empty string.' );
+ return null;
+ }
+
+ // When there is a variation handle, check it.
+ if ( '' !== $variation_handle && ! WP_Webfonts_Utils::is_defined( $variation_handle ) ) {
+ trigger_error( 'Variant handle must be a non-empty string.' );
+ return null;
+ }
+
+ // Register the font family when it does not yet exist.
+ if ( ! isset( $this->registered[ $font_family_handle ] ) ) {
+ if ( ! $this->add_font_family( $font_family_handle ) ) {
+ return null;
+ }
+ }
+
+ $variation = $this->validate_variation( $variation );
+
+ // Variation validation failed.
+ if ( ! $variation ) {
+ return null;
+ }
+
+ // When there's no variation handle, attempt to create one.
+ if ( '' === $variation_handle ) {
+ $variation_handle = WP_Webfonts_Utils::convert_variation_into_handle( $font_family_handle, $variation );
+ if ( is_null( $variation_handle ) ) {
+ return null;
+ }
+ }
+
+ // Bail out if the variant is already registered.
+ if ( $this->is_variation_registered( $font_family_handle, $variation_handle ) ) {
+ return $variation_handle;
+ }
+
+ $variation_src = array_key_exists( 'src', $variation ) ? $variation['src'] : false;
+ $result = $this->add( $variation_handle, $variation_src );
+
+ // Bail out if the registration failed.
+ if ( ! $result ) {
+ return null;
+ }
+
+ $this->add_data( $variation_handle, 'font-properties', $variation );
+ $this->add_data( $variation_handle, 'is_font_family', false );
+
+ // Add the font variation as a dependency to the registered font family.
+ $this->add_dependency( $font_family_handle, $variation_handle );
+
+ $this->providers[ $variation['provider'] ]['fonts'][] = $variation_handle;
+
+ return $variation_handle;
+ }
+
+ /**
+ * Removes a variation.
+ *
+ * @since X.X.X
+ *
+ * @param string $font_family_handle The font family for this variation.
+ * @param string $variation_handle The variation's handle to remove.
+ */
+ public function remove_variation( $font_family_handle, $variation_handle ) {
+ if ( isset( $this->registered[ $variation_handle ] ) ) {
+ $this->remove( $variation_handle );
+ }
+
+ if ( ! $this->is_variation_registered( $font_family_handle, $variation_handle ) ) {
+ return;
+ }
+
+ // Remove the variation as a dependency from its font family.
+ $this->registered[ $font_family_handle ]->deps = array_values(
+ array_diff(
+ $this->registered[ $font_family_handle ]->deps,
+ array( $variation_handle )
+ )
+ );
+ }
+
+ /**
+ * Checks if the variation is registered.
+ *
+ * @since X.X.X
+ *
+ * @param string $font_family_handle The font family's handle for this variation.
+ * @param string $variation_handle Variation's handle.
+ * @return bool True when registered to the given font family. Else false.
+ */
+ private function is_variation_registered( $font_family_handle, $variation_handle ) {
+ if ( ! isset( $this->registered[ $font_family_handle ] ) ) {
+ return false;
+ }
+
+ return in_array( $variation_handle, $this->registered[ $font_family_handle ]->deps, true );
+ }
+
+ /**
+ * Adds a variation as a dependency to the given font family.
+ *
+ * @since X.X.X
+ *
+ * @param string $font_family_handle The font family's handle for this variation.
+ * @param string $variation_handle The variation's handle.
+ */
+ private function add_dependency( $font_family_handle, $variation_handle ) {
+ $this->registered[ $font_family_handle ]->deps[] = $variation_handle;
+ }
+
+ /**
+ * Validates and sanitizes a variation.
+ *
+ * @since X.X.X
+ *
+ * @param array $variation Variation properties to add.
+ * @return false|array Validated variation on success. Else, false.
+ */
+ private function validate_variation( $variation ) {
+ $variation = wp_parse_args( $variation, $this->variation_property_defaults );
+
+ // Check the font-family.
+ if ( empty( $variation['font-family'] ) || ! is_string( $variation['font-family'] ) ) {
+ trigger_error( 'Webfont font-family must be a non-empty string.' );
+ return false;
+ }
+
+ // Local fonts need a "src".
+ if ( 'local' === $variation['provider'] ) {
+ // Make sure that local fonts have 'src' defined.
+ if ( empty( $variation['src'] ) || ( ! is_string( $variation['src'] ) && ! is_array( $variation['src'] ) ) ) {
+ trigger_error( 'Webfont src must be a non-empty string or an array of strings.' );
+ return false;
+ }
+ } elseif ( ! isset( $this->providers[ $variation['provider'] ] ) ) {
+ trigger_error( sprintf( 'The provider "%s" is not registered', $variation['provider'] ) );
+ return false;
+ } elseif ( ! class_exists( $this->providers[ $variation['provider'] ]['class'] ) ) {
+ trigger_error( sprintf( 'The provider class "%s" does not exist', $variation['provider'] ) );
+ return false;
+ }
+
+ // Validate the 'src' property.
+ if ( ! empty( $variation['src'] ) ) {
+ foreach ( (array) $variation['src'] as $src ) {
+ if ( empty( $src ) || ! is_string( $src ) ) {
+ trigger_error( 'Each webfont src must be a non-empty string.' );
+ return false;
+ }
+ }
+ }
+
+ // Check the font-weight.
+ if ( ! is_string( $variation['font-weight'] ) && ! is_int( $variation['font-weight'] ) ) {
+ trigger_error( 'Webfont font-weight must be a properly formatted string or integer.' );
+ return false;
+ }
+
+ // Check the font-display.
+ if ( ! in_array( $variation['font-display'], array( 'auto', 'block', 'fallback', 'swap', 'optional' ), true ) ) {
+ $variation['font-display'] = 'fallback';
+ }
+
+ $valid_props = array(
+ 'ascent-override',
+ 'descent-override',
+ 'font-display',
+ 'font-family',
+ 'font-stretch',
+ 'font-style',
+ 'font-weight',
+ 'font-variant',
+ 'font-feature-settings',
+ 'font-variation-settings',
+ 'line-gap-override',
+ 'size-adjust',
+ 'src',
+ 'unicode-range',
+
+ // Exceptions.
+ 'provider',
+ );
+
+ foreach ( $variation as $prop => $value ) {
+ if ( ! in_array( $prop, $valid_props, true ) ) {
+ unset( $variation[ $prop ] );
+ }
+ }
+
+ return $variation;
+ }
+
+ /**
+ * Processes the items and dependencies.
+ *
+ * Processes the items passed to it or the queue, and their dependencies.
+ *
+ * @since X.X.X
+ *
+ * @param string|string[]|bool $handles Optional. Items to be processed: queue (false),
+ * single item (string), or multiple items (array of strings).
+ * Default false.
+ * @param int|false $group Optional. Group level: level (int), no group (false).
+ *
+ * @return array|string[] Array of web font handles that have been processed.
+ * An empty array if none were processed.
+ */
+ public function do_items( $handles = false, $group = false ) {
+ $handles = $this->prepare_handles_for_printing( $handles );
+
+ if ( empty( $handles ) ) {
+ return $this->done;
+ }
+
+ $this->all_deps( $handles );
+ if ( empty( $this->to_do ) ) {
+ return $this->done;
+ }
+
+ $this->to_do_keyed_handles = array_flip( $this->to_do );
+
+ foreach ( $this->get_providers() as $provider_id => $provider ) {
+ // Alert and skip if the provider class does not exist.
+ if ( ! class_exists( $provider['class'] ) ) {
+ /* translators: %s is the provider name. */
+ trigger_error(
+ sprintf(
+ 'Class "%s" not found for "%s" web font provider',
+ $provider['class'],
+ $provider_id
+ )
+ );
+ continue;
+ }
+
+ $this->do_item( $provider_id, $group );
+ }
+
+ $this->process_font_families_after_printing( $handles );
+
+ return $this->done;
+ }
+
+ /**
+ * Prepares the given handles for printing.
+ *
+ * @since X.X.X
+ *
+ * @param string|string[]|bool $handles Optional. Handles to prepare.
+ * Default false.
+ * @return array Array of handles.
+ */
+ private function prepare_handles_for_printing( $handles = false ) {
+ if ( false !== $handles ) {
+ $handles = $this->validate_handles( $handles );
+ // Bail out when invalid.
+ if ( empty( $handles ) ) {
+ return array();
+ }
+ }
+
+ // Use the enqueued queue.
+ if ( empty( $handles ) ) {
+ if ( empty( $this->queue ) ) {
+ return array();
+ }
+ $handles = $this->queue;
+ }
+
+ return $handles;
+ }
+
+ /**
+ * Validates handle(s) to ensure each is a non-empty string.
+ *
+ * @since X.X.X
+ *
+ * @param string|string[] $handles Handles to prepare.
+ * @return string[]|null Array of handles on success. Else null.
+ */
+ private function validate_handles( $handles ) {
+ // Validate each element is a non-empty string handle.
+ $handles = array_filter( (array) $handles, array( WP_Webfonts_Utils::class, 'is_defined' ) );
+
+ if ( empty( $handles ) ) {
+ trigger_error( 'Handles must be a non-empty string or array of non-empty strings' );
+ return null;
+ }
+
+ return $handles;
+ }
+
+ /**
+ * Invokes each provider to process and print its styles.
+ *
+ * @since X.X.X
+ *
+ * @see WP_Dependencies::do_item()
+ *
+ * @param string $provider_id The provider to process.
+ * @param int|false $group Not used.
+ * @return bool
+ */
+ public function do_item( $provider_id, $group = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ // Bail out if the provider is not registered.
+ if ( ! isset( $this->providers[ $provider_id ] ) ) {
+ return false;
+ }
+
+ $font_handles = $this->get_enqueued_fonts_for_provider( $provider_id );
+ if ( empty( $font_handles ) ) {
+ return false;
+ }
+
+ $properties_by_font = $this->get_font_properties_for_provider( $font_handles );
+ if ( empty( $properties_by_font ) ) {
+ return false;
+ }
+
+ // Invoke provider to print its styles.
+ $provider = $this->get_provider_instance( $provider_id );
+ $provider->set_webfonts( $properties_by_font );
+ $provider->print_styles();
+
+ // Clean up.
+ $this->update_queues_for_printed_fonts( $font_handles );
+
+ return true;
+ }
+
+ /**
+ * Retrieves a list of enqueued web font variations for a provider.
+ *
+ * @since X.X.X
+ *
+ * @param string $provider_id The provider to process.
+ * @return array[] Webfonts organized by providers.
+ */
+ private function get_enqueued_fonts_for_provider( $provider_id ) {
+ $providers = $this->get_providers();
+
+ if ( empty( $providers[ $provider_id ] ) ) {
+ return array();
+ }
+
+ return array_intersect(
+ $providers[ $provider_id ]['fonts'],
+ $this->to_do
+ );
+ }
+
+ /**
+ * Gets a list of font properties for each of the given font handles.
+ *
+ * @since X.X.X
+ *
+ * @param array $font_handles Font handles to get properties.
+ * @return array A list of fonts with each font's properties.
+ */
+ private function get_font_properties_for_provider( array $font_handles ) {
+ $font_properties = array();
+
+ foreach ( $font_handles as $font_handle ) {
+ $properties = $this->get_data( $font_handle, 'font-properties' );
+ if ( ! $properties ) {
+ continue;
+ }
+ $font_properties[ $font_handle ] = $properties;
+ }
+
+ return $font_properties;
+ }
+
+ /**
+ * Gets the instance of the provider from the WP_Webfonts::$provider_instance store.
+ *
+ * @since X.X.X
+ *
+ * @param string $provider_id The provider to get.
+ * @return object Instance of the provider.
+ */
+ private function get_provider_instance( $provider_id ) {
+ if ( ! isset( $this->provider_instances[ $provider_id ] ) ) {
+ $this->provider_instances[ $provider_id ] = new $this->providers[ $provider_id ]['class']();
+ }
+ return $this->provider_instances[ $provider_id ];
+ }
+
+ /**
+ * Update queues for the given printed fonts.
+ *
+ * @since X.X.X
+ *
+ * @param array $font_handles Font handles to get properties.
+ */
+ private function update_queues_for_printed_fonts( array $font_handles ) {
+ foreach ( $font_handles as $font_handle ) {
+ $this->set_as_done( $font_handle );
+ $this->remove_from_to_do_queues( $font_handle );
+ }
+ }
+
+ /**
+ * Processes the font families after printing the variations.
+ *
+ * For each queued font family:
+ *
+ * a. if any of their variations were printed, the font family is added to the `done` list.
+ * b. removes each from the to_do queues.
+ *
+ * @since X.X.X
+ *
+ * @param array $handles Handles to process.
+ */
+ private function process_font_families_after_printing( array $handles ) {
+ foreach ( $handles as $handle ) {
+ if (
+ ! $this->get_data( $handle, 'is_font_family' ) ||
+ ! isset( $this->to_do_keyed_handles[ $handle ] )
+ ) {
+ continue;
+ }
+ $font_family = $this->registered[ $handle ];
+
+ // Add the font family to `done` list if any of its variations were printed.
+ if ( ! empty( $font_family->deps ) ) {
+ $processed = array_intersect( $font_family->deps, $this->done );
+ if ( ! empty( $processed ) ) {
+ $this->set_as_done( $handle );
+ }
+ }
+
+ $this->remove_from_to_do_queues( $handle );
+ }
+ }
+
+ /**
+ * Removes the handle from the `to_do` and `to_do_keyed_handles` lists.
+ *
+ * @since X.X.X
+ *
+ * @param string $handle Handle to remove.
+ */
+ private function remove_from_to_do_queues( $handle ) {
+ unset(
+ $this->to_do[ $this->to_do_keyed_handles[ $handle ] ],
+ $this->to_do_keyed_handles[ $handle ]
+ );
+ }
+
+ /**
+ * Sets the given handle to done by adding it to the `done` list.
+ *
+ * @since X.X.X
+ *
+ * @param string $handle Handle to set as done.
+ */
+ private function set_as_done( $handle ) {
+ if ( ! is_array( $this->done ) ) {
+ $this->done = array();
+ }
+ $this->done[] = $handle;
+ }
+
+ /**
+ * Converts the font family and its variations into theme.json structural format.
+ *
+ * @since X.X.X
+ *
+ * @param string $font_family_handle Font family to convert.
+ * @return array Webfonts in theme.json structural format.
+ */
+ public function to_theme_json( $font_family_handle ) {
+ if ( ! isset( $this->registered[ $font_family_handle ] ) ) {
+ return array();
+ }
+
+ $font_family_name = $this->registered[ $font_family_handle ]->extra['font-properties']['font-family'];
+ $theme_json_format = array(
+ 'fontFamily' => str_contains( $font_family_name, ' ' ) ? "'{$font_family_name}'" : $font_family_name,
+ 'name' => $font_family_name,
+ 'slug' => $font_family_handle,
+ 'fontFace' => array(),
+ );
+
+ foreach ( $this->registered[ $font_family_handle ]->deps as $variation_handle ) {
+ if ( ! isset( $this->registered[ $variation_handle ] ) ) {
+ continue;
+ }
+
+ $variation_obj = $this->registered[ $variation_handle ];
+ $variation_properties = array( 'origin' => 'gutenberg_wp_webfonts_api' );
+ foreach ( $variation_obj->extra['font-properties'] as $property_name => $property_value ) {
+ $property_in_camelcase = lcfirst( str_replace( '-', '', ucwords( $property_name, '-' ) ) );
+ $variation_properties[ $property_in_camelcase ] = $property_value;
+ }
+ $theme_json_format['fontFace'][ $variation_obj->handle ] = $variation_properties;
+ }
+
+ return $theme_json_format;
+ }
+}
diff --git a/lib/experimental/class-wp-webfonts-provider-local.php b/lib/experimental/class-wp-webfonts-provider-local.php
index 9af27fa7c436a7..3950ebcd624c39 100644
--- a/lib/experimental/class-wp-webfonts-provider-local.php
+++ b/lib/experimental/class-wp-webfonts-provider-local.php
@@ -35,6 +35,21 @@ class WP_Webfonts_Provider_Local extends WP_Webfonts_Provider {
*/
protected $id = 'local';
+ /**
+ * Constructor.
+ *
+ * @since 6.1.0
+ */
+ public function __construct() {
+ if (
+ function_exists( 'is_admin' ) && ! is_admin()
+ &&
+ function_exists( 'current_theme_supports' ) && ! current_theme_supports( 'html5', 'style' )
+ ) {
+ $this->style_tag_atts = array( 'type' => 'text/css' );
+ }
+ }
+
/**
* Gets the `@font-face` CSS styles for locally-hosted font files.
*
@@ -81,7 +96,7 @@ class WP_Webfonts_Provider_Local extends WP_Webfonts_Provider {
* }
*
*
- * @since 6.0.0
+ * @since X.X.X
*
* @return string The `@font-face` CSS.
*/
@@ -183,7 +198,8 @@ private function order_src( array $webfont ) {
private function build_font_face_css( array $webfont ) {
$css = '';
- // Wrap font-family in quotes if it contains spaces.
+ // Wrap font-family in quotes if it contains spaces
+ // and is not already wrapped in quotes.
if (
str_contains( $webfont['font-family'], ' ' ) &&
! str_contains( $webfont['font-family'], '"' ) &&
diff --git a/lib/experimental/class-wp-webfonts-provider.php b/lib/experimental/class-wp-webfonts-provider.php
index a49b8a324b7f5f..8fcbd76a76d3cf 100644
--- a/lib/experimental/class-wp-webfonts-provider.php
+++ b/lib/experimental/class-wp-webfonts-provider.php
@@ -6,7 +6,7 @@
*
* @package WordPress
* @subpackage WebFonts
- * @since 6.0.0
+ * @since X.X.X
*/
if ( class_exists( 'WP_Webfonts_Provider' ) ) {
@@ -30,26 +30,46 @@
* into styles (in a performant way for the provider service
* it manages).
*
- * @since 6.0.0
+ * @since X.X.X
*/
abstract class WP_Webfonts_Provider {
+ /**
+ * The provider's unique ID.
+ *
+ * @since X.X.X
+ *
+ * @var string
+ */
+ protected $id = '';
+
/**
* Webfonts to be processed.
*
- * @since 6.0.0
+ * @since X.X.X
*
* @var array[]
*/
protected $webfonts = array();
+ /**
+ * Array of Font-face style tag's attribute(s)
+ * where the key is the attribute name and the
+ * value is its value.
+ *
+ * @since X.X.X
+ *
+ * @var string[]
+ */
+ protected $style_tag_atts = array();
+
/**
* Sets this provider's webfonts property.
*
* The API's Controller passes this provider's webfonts
* for processing here in the provider.
*
- * @since 6.0.0
+ * @since X.X.X
*
* @param array[] $webfonts Registered webfonts.
*/
@@ -57,6 +77,18 @@ public function set_webfonts( array $webfonts ) {
$this->webfonts = $webfonts;
}
+ /**
+ * Prints the generated styles.
+ *
+ * @since X.X.X
+ */
+ public function print_styles() {
+ printf(
+ $this->get_style_element(),
+ $this->get_css()
+ );
+ }
+
/**
* Gets the `@font-face` CSS for the provider's webfonts.
*
@@ -64,9 +96,37 @@ public function set_webfonts( array $webfonts ) {
* needed `@font-face` CSS for all of its webfonts. Specifics of how
* this processing is done is contained in each provider.
*
- * @since 6.0.0
+ * @since X.X.X
*
* @return string The `@font-face` CSS.
*/
abstract public function get_css();
+
+ /**
+ * Gets the `\n";
+ }
+
+ /**
+ * Gets the defined
- { styles.map(
+ { [ ...styles, ...neededCompatStyles ].map(
( { tagName, href, id, rel, media, textContent } ) => {
const TagName = tagName.toLowerCase();
@@ -342,7 +256,7 @@ function Iframe(
ref={ bodyRef }
className={ classnames(
'block-editor-iframe__body',
- BODY_CLASS_NAME,
+ 'editor-styles-wrapper',
...bodyClasses
) }
style={ {
@@ -359,15 +273,6 @@ function Iframe(
inert={ readonly ? 'true' : undefined }
>
{ contentResizeListener }
- { /*
- * This is a wrapper for the extra styles and scripts
- * rendered imperatively by cloning the parent,
- * it's important that this div's content remains uncontrolled.
- */ }
-
{ device.label } is used here for testing purposes to ensure we have access to details about the device.
-