WP_Theme_JSON::get_block_nodesprivate staticWP 6.1.0

An internal method to get the block nodes from a theme.json file.

Method of the class: WP_Theme_JSON{}

No Hooks.

Returns

Array. The block nodes in theme.json.

Usage

$result = WP_Theme_JSON::get_block_nodes( $theme_json, $selectors, $options );
$theme_json(array) (required)
The theme.json converted to an array.
$selectors(array)
Optional list of selectors per block.
Default: array()
$options(array)

An array of options for now used for internal purposes only (may change without notice).

Default: array()

  • include_block_style_variations(true|false)
    Include nodes for block style variations.
    Default: false

  • include_node_paths_only(true|false)
    Return only block nodes node paths.
    Default: false

Changelog

Since 6.1.0 Introduced.
Since 6.3.0 Refactored and stabilized selectors API.
Since 6.6.0 Added optional selectors and options for generating block nodes.
Since 6.7.0 Added $include_node_paths_only option.
Since 7.1.0 Added responsive block nodes for breakpoint-based styles.

WP_Theme_JSON::get_block_nodes() code WP 7.1

private static function get_block_nodes( $theme_json, $selectors = array(), $options = array() ) {
	$nodes = array();

	if ( ! isset( $theme_json['styles']['blocks'] ) ) {
		return $nodes;
	}

	$include_variations       = $options['include_block_style_variations'] ?? false;
	$include_node_paths_only  = $options['include_node_paths_only'] ?? false;
	$responsive_media_queries = static::get_viewport_media_queries( $theme_json['settings']['viewport'] ?? null );

	// If only node paths are to be returned, skip selector assignment.
	if ( ! $include_node_paths_only ) {
		$selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
	}

	foreach ( $theme_json['styles']['blocks'] as $name => $node ) {
		$node_path = array( 'styles', 'blocks', $name );
		if ( $include_node_paths_only ) {
			$variation_paths = array();
			if ( $include_variations && isset( $node['variations'] ) ) {
				foreach ( $node['variations'] as $variation => $variation_node ) {
					$variation_paths[] = array(
						'path' => array( 'styles', 'blocks', $name, 'variations', $variation ),
					);
				}
			}
			$node = array(
				'path' => $node_path,
			);
			if ( ! empty( $variation_paths ) ) {
				$node['variations'] = $variation_paths;
			}
			$nodes[] = $node;
		} else {
			$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 ]['selectors'] ) ) {
				$feature_selectors = $selectors[ $name ]['selectors'];
			}

			$variation_selectors = array();

			if ( $include_variations && isset( $node['variations'] ) ) {
				foreach ( $node['variations'] as $variation => $node ) {
					$variation_selectors[] = array(
						'name'     => $variation,
						'path'     => array( 'styles', 'blocks', $name, 'variations', $variation ),
						'selector' => $selectors[ $name ]['styleVariations'][ $variation ],
					);
				}
			}

			$nodes[] = array(
				'name'       => $name,
				'path'       => $node_path,
				'selector'   => $selector,
				'selectors'  => $feature_selectors,
				'elements'   => $selectors[ $name ]['elements'] ?? array(),
				'duotone'    => $duotone_selector,
				'variations' => $variation_selectors,
				'css'        => $selector,
			);

			// Responsive block nodes: emit one node per breakpoint that has styles.
			// These are rendered immediately after the base block node so that
			// the cascade order is: .block{} → @media{.block{}}
			foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
				if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ] ) ) {
					$nodes[] = array(
						'name'        => $name,
						'path'        => array( 'styles', 'blocks', $name, $breakpoint ),
						'media_query' => $responsive_media_queries[ $breakpoint ],
						'selector'    => $selector,
						'selectors'   => $feature_selectors,
						'elements'    => $selectors[ $name ]['elements'] ?? array(),
						'variations'  => $variation_selectors,
						'css'         => $selector,
					);
				}
			}

			// Handle any pseudo selectors for the block.
			if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
				foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) {
					$has_pseudo            = isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] );
					$has_responsive_pseudo = false;
					foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
						if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
							$has_responsive_pseudo = true;
							break;
						}
					}

					if ( ! $has_pseudo && ! $has_responsive_pseudo ) {
						continue;
					}

					/*
					 * Append the pseudo-selector to each feature selector so that
					 * get_feature_declarations_for_node generates CSS scoped to the
					 * pseudo-state (e.g. '.wp-block-button:hover') rather than the
					 * default state (e.g. '.wp-block-button').
					 */
					$pseudo_feature_selectors = array();
					foreach ( $feature_selectors ?? array() as $feature => $feature_selector ) {
						if ( is_array( $feature_selector ) ) {
							$pseudo_feature_selectors[ $feature ] = array();
							foreach ( $feature_selector as $subfeature => $subfeature_selector ) {
								$pseudo_feature_selectors[ $feature ][ $subfeature ] = static::append_to_selector( $subfeature_selector, $pseudo_selector );
							}
						} else {
							$pseudo_feature_selectors[ $feature ] = static::append_to_selector( $feature_selector, $pseudo_selector );
						}
					}

					if ( $has_pseudo ) {
						$nodes[] = array(
							'name'       => $name,
							'path'       => array( 'styles', 'blocks', $name, $pseudo_selector ),
							'selector'   => static::append_to_selector( $selector, $pseudo_selector ),
							'selectors'  => $pseudo_feature_selectors,
							'elements'   => $selectors[ $name ]['elements'] ?? array(),
							'duotone'    => $duotone_selector,
							'variations' => $variation_selectors,
							'css'        => static::append_to_selector( $selector, $pseudo_selector ),
						);
					}

					// Responsive pseudo nodes: emit one node per breakpoint that has
					// this pseudo state, immediately after the default pseudo node.
					// Cascade order: .block:hover{} → @media{.block:hover{}}
					foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
						if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) {
							$nodes[] = array(
								'name'        => $name,
								'path'        => array( 'styles', 'blocks', $name, $breakpoint, $pseudo_selector ),
								'media_query' => $responsive_media_queries[ $breakpoint ],
								'selector'    => static::append_to_selector( $selector, $pseudo_selector ),
								'selectors'   => $pseudo_feature_selectors,
								'elements'    => $selectors[ $name ]['elements'] ?? array(),
								'variations'  => $variation_selectors,
								'css'         => static::append_to_selector( $selector, $pseudo_selector ),
							);
						}
					}
				}
			}

			// Handle custom states (e.g. '-current' for navigation).
			if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $name ] ) ) {
				foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $name ] as $custom_state ) {
					if (
						isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ] ) &&
						isset( $selectors[ $name ]['states'][ $custom_state ] )
					) {
						$custom_css_selector = $selectors[ $name ]['states'][ $custom_state ];
						$nodes[]             = array(
							'name'       => $name,
							'path'       => array( 'styles', 'blocks', $name, $custom_state ),
							'selector'   => $custom_css_selector,
							'selectors'  => $feature_selectors,
							'elements'   => $selectors[ $name ]['elements'] ?? array(),
							'duotone'    => $duotone_selector,
							'variations' => $variation_selectors,
							'css'        => $custom_css_selector,
						);

						// Sub-pseudo-selectors within the custom state.
						if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
							foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo ) {
								if ( isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ][ $pseudo ] ) ) {
									$compound_css_selector = static::append_to_selector( $custom_css_selector, $pseudo );
									$nodes[]               = array(
										'name'       => $name,
										'path'       => array( 'styles', 'blocks', $name, $custom_state, $pseudo ),
										'selector'   => $compound_css_selector,
										'selectors'  => $feature_selectors,
										'elements'   => $selectors[ $name ]['elements'] ?? array(),
										'duotone'    => $duotone_selector,
										'variations' => $variation_selectors,
										'css'        => $compound_css_selector,
									);
								}
							}
						}
					}
				}
			}
		}
		/*
		 * Elements can be styled outside any breakpoint, inside one, or both,
		 * so collect the names from all of those places before looping. An
		 * element styled only inside a breakpoint still needs a node.
		 */
		$block_node    = $theme_json['styles']['blocks'][ $name ] ?? array();
		$element_names = array_keys( $block_node['elements'] ?? array() );
		foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
			$element_names = array_merge(
				$element_names,
				array_keys( $block_node[ $breakpoint ]['elements'] ?? array() )
			);
		}
		$element_names = array_unique( $element_names );

		if ( ! empty( $element_names ) ) {
			foreach ( $element_names as $element ) {
				$element_path = array( 'styles', 'blocks', $name, 'elements', $element );
				if ( $include_node_paths_only ) {
					if ( isset( $block_node['elements'][ $element ] ) ) {
						$nodes[] = array(
							'path' => $element_path,
						);
					}
					continue;
				}

				if ( ! isset( $selectors[ $name ]['elements'][ $element ] ) ) {
					continue;
				}

				$element_selector = $selectors[ $name ]['elements'][ $element ];

				if ( isset( $block_node['elements'][ $element ] ) ) {
					$nodes[] = array(
						'path'     => $element_path,
						'selector' => $element_selector,
					);
				}

				// Responsive element nodes: one node per breakpoint that has
				// styles for this element. Cascade: a{} → @media{a{}}
				foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
					if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ] ) ) {
						$nodes[] = array(
							'path'        => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
							'selector'    => $element_selector,
							'media_query' => $responsive_media_queries[ $breakpoint ],
						);
					}
				}

				// Handle any pseudo selectors for the element.
				if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
					foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
						// Emit the default pseudo node only when the default state styles
						// the pseudo. Otherwise get_styles_for_block() falls back to the
						// element's base styles, outputting a rule the theme never defined.
						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( $element_selector, $pseudo_selector ),
							);
						}

						// Responsive element pseudo nodes: one node per breakpoint
						// that has this pseudo state for this element.
						// Cascade: a:hover{} → @media{a:hover{}}
						foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
							if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ][ $pseudo_selector ] ) ) {
								$nodes[] = array(
									'path'        => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ),
									'selector'    => static::append_to_selector( $element_selector, $pseudo_selector ),
									'media_query' => $responsive_media_queries[ $breakpoint ],
								);
							}
						}
					}
				}
			}
		}
	}

	return $nodes;
}