|
1 <?php |
|
2 /** |
|
3 * Block support to enable per-section styling of block types via |
|
4 * block style variations. |
|
5 * |
|
6 * @package WordPress |
|
7 * @since 6.6.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Generate block style variation instance name. |
|
12 * |
|
13 * @since 6.6.0 |
|
14 * @access private |
|
15 * |
|
16 * @param array $block Block object. |
|
17 * @param string $variation Slug for the block style variation. |
|
18 * |
|
19 * @return string The unique variation name. |
|
20 */ |
|
21 function wp_create_block_style_variation_instance_name( $block, $variation ) { |
|
22 return $variation . '--' . md5( serialize( $block ) ); |
|
23 } |
|
24 |
|
25 /** |
|
26 * Determines the block style variation names within a CSS class string. |
|
27 * |
|
28 * @since 6.6.0 |
|
29 * |
|
30 * @param string $class_string CSS class string to look for a variation in. |
|
31 * |
|
32 * @return array|null The block style variation name if found. |
|
33 */ |
|
34 function wp_get_block_style_variation_name_from_class( $class_string ) { |
|
35 if ( ! is_string( $class_string ) ) { |
|
36 return null; |
|
37 } |
|
38 |
|
39 preg_match_all( '/\bis-style-(?!default)(\S+)\b/', $class_string, $matches ); |
|
40 return $matches[1] ?? null; |
|
41 } |
|
42 |
|
43 /** |
|
44 * Recursively resolves any `ref` values within a block style variation's data. |
|
45 * |
|
46 * @since 6.6.0 |
|
47 * @access private |
|
48 * |
|
49 * @param array $variation_data Reference to the variation data being processed. |
|
50 * @param array $theme_json Theme.json data to retrieve referenced values from. |
|
51 */ |
|
52 function wp_resolve_block_style_variation_ref_values( &$variation_data, $theme_json ) { |
|
53 foreach ( $variation_data as $key => &$value ) { |
|
54 // Only need to potentially process arrays. |
|
55 if ( is_array( $value ) ) { |
|
56 // If ref value is set, attempt to find its matching value and update it. |
|
57 if ( array_key_exists( 'ref', $value ) ) { |
|
58 // Clean up any invalid ref value. |
|
59 if ( empty( $value['ref'] ) || ! is_string( $value['ref'] ) ) { |
|
60 unset( $variation_data[ $key ] ); |
|
61 } |
|
62 |
|
63 $value_path = explode( '.', $value['ref'] ?? '' ); |
|
64 $ref_value = _wp_array_get( $theme_json, $value_path ); |
|
65 |
|
66 // Only update the current value if the referenced path matched a value. |
|
67 if ( null === $ref_value ) { |
|
68 unset( $variation_data[ $key ] ); |
|
69 } else { |
|
70 $value = $ref_value; |
|
71 } |
|
72 } else { |
|
73 // Recursively look for ref instances. |
|
74 wp_resolve_block_style_variation_ref_values( $value, $theme_json ); |
|
75 } |
|
76 } |
|
77 } |
|
78 } |
|
79 /** |
|
80 * Render the block style variation's styles. |
|
81 * |
|
82 * In the case of nested blocks with variations applied, we want the parent |
|
83 * variation's styles to be rendered before their descendants. This solves the |
|
84 * issue of a block type being styled in both the parent and descendant: we want |
|
85 * the descendant style to take priority, and this is done by loading it after, |
|
86 * in the DOM order. This is why the variation stylesheet generation is in a |
|
87 * different filter. |
|
88 * |
|
89 * @since 6.6.0 |
|
90 * @access private |
|
91 * |
|
92 * @param array $parsed_block The parsed block. |
|
93 * |
|
94 * @return array The parsed block with block style variation classname added. |
|
95 */ |
|
96 function wp_render_block_style_variation_support_styles( $parsed_block ) { |
|
97 $classes = $parsed_block['attrs']['className'] ?? null; |
|
98 $variations = wp_get_block_style_variation_name_from_class( $classes ); |
|
99 |
|
100 if ( ! $variations ) { |
|
101 return $parsed_block; |
|
102 } |
|
103 |
|
104 $tree = WP_Theme_JSON_Resolver::get_merged_data(); |
|
105 $theme_json = $tree->get_raw_data(); |
|
106 |
|
107 // Only the first block style variation with data is supported. |
|
108 $variation_data = array(); |
|
109 foreach ( $variations as $variation ) { |
|
110 $variation_data = $theme_json['styles']['blocks'][ $parsed_block['blockName'] ]['variations'][ $variation ] ?? array(); |
|
111 |
|
112 if ( ! empty( $variation_data ) ) { |
|
113 break; |
|
114 } |
|
115 } |
|
116 |
|
117 if ( empty( $variation_data ) ) { |
|
118 return $parsed_block; |
|
119 } |
|
120 |
|
121 /* |
|
122 * Recursively resolve any ref values with the appropriate value within the |
|
123 * theme_json data. |
|
124 */ |
|
125 wp_resolve_block_style_variation_ref_values( $variation_data, $theme_json ); |
|
126 |
|
127 $variation_instance = wp_create_block_style_variation_instance_name( $parsed_block, $variation ); |
|
128 $class_name = "is-style-$variation_instance"; |
|
129 $updated_class_name = $parsed_block['attrs']['className'] . " $class_name"; |
|
130 |
|
131 /* |
|
132 * Even though block style variations are effectively theme.json partials, |
|
133 * they can't be processed completely as though they are. |
|
134 * |
|
135 * Block styles support custom selectors to direct specific types of styles |
|
136 * to inner elements. For example, borders on Image block's get applied to |
|
137 * the inner `img` element rather than the wrapping `figure`. |
|
138 * |
|
139 * The following relocates the "root" block style variation styles to |
|
140 * under an appropriate blocks property to leverage the preexisting style |
|
141 * generation for simple block style variations. This way they get the |
|
142 * custom selectors they need. |
|
143 * |
|
144 * The inner elements and block styles for the variation itself are |
|
145 * still included at the top level but scoped by the variation's selector |
|
146 * when the stylesheet is generated. |
|
147 */ |
|
148 $elements_data = $variation_data['elements'] ?? array(); |
|
149 $blocks_data = $variation_data['blocks'] ?? array(); |
|
150 unset( $variation_data['elements'] ); |
|
151 unset( $variation_data['blocks'] ); |
|
152 |
|
153 _wp_array_set( |
|
154 $blocks_data, |
|
155 array( $parsed_block['blockName'], 'variations', $variation_instance ), |
|
156 $variation_data |
|
157 ); |
|
158 |
|
159 $config = array( |
|
160 'version' => WP_Theme_JSON::LATEST_SCHEMA, |
|
161 'styles' => array( |
|
162 'elements' => $elements_data, |
|
163 'blocks' => $blocks_data, |
|
164 ), |
|
165 ); |
|
166 |
|
167 // Turn off filter that excludes block nodes. They are needed here for the variation's inner block types. |
|
168 if ( ! is_admin() ) { |
|
169 remove_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' ); |
|
170 } |
|
171 |
|
172 // Temporarily prevent variation instance from being sanitized while processing theme.json. |
|
173 $styles_registry = WP_Block_Styles_Registry::get_instance(); |
|
174 $styles_registry->register( $parsed_block['blockName'], array( 'name' => $variation_instance ) ); |
|
175 |
|
176 $variation_theme_json = new WP_Theme_JSON( $config, 'blocks' ); |
|
177 $variation_styles = $variation_theme_json->get_stylesheet( |
|
178 array( 'styles' ), |
|
179 array( 'custom' ), |
|
180 array( |
|
181 'include_block_style_variations' => true, |
|
182 'skip_root_layout_styles' => true, |
|
183 'scope' => ".$class_name", |
|
184 ) |
|
185 ); |
|
186 |
|
187 // Clean up temporary block style now instance styles have been processed. |
|
188 $styles_registry->unregister( $parsed_block['blockName'], $variation_instance ); |
|
189 |
|
190 // Restore filter that excludes block nodes. |
|
191 if ( ! is_admin() ) { |
|
192 add_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' ); |
|
193 } |
|
194 |
|
195 if ( empty( $variation_styles ) ) { |
|
196 return $parsed_block; |
|
197 } |
|
198 |
|
199 wp_register_style( 'block-style-variation-styles', false, array( 'wp-block-library', 'global-styles' ) ); |
|
200 wp_add_inline_style( 'block-style-variation-styles', $variation_styles ); |
|
201 |
|
202 /* |
|
203 * Add variation instance class name to block's className string so it can |
|
204 * be enforced in the block markup via render_block filter. |
|
205 */ |
|
206 _wp_array_set( $parsed_block, array( 'attrs', 'className' ), $updated_class_name ); |
|
207 |
|
208 return $parsed_block; |
|
209 } |
|
210 |
|
211 /** |
|
212 * Ensure the variation block support class name generated and added to |
|
213 * block attributes in the `render_block_data` filter gets applied to the |
|
214 * block's markup. |
|
215 * |
|
216 * @see wp_render_block_style_variation_support_styles |
|
217 * |
|
218 * @since 6.6.0 |
|
219 * @access private |
|
220 * |
|
221 * @param string $block_content Rendered block content. |
|
222 * @param array $block Block object. |
|
223 * |
|
224 * @return string Filtered block content. |
|
225 */ |
|
226 function wp_render_block_style_variation_class_name( $block_content, $block ) { |
|
227 if ( ! $block_content || empty( $block['attrs']['className'] ) ) { |
|
228 return $block_content; |
|
229 } |
|
230 |
|
231 /* |
|
232 * Matches a class prefixed by `is-style`, followed by the |
|
233 * variation slug, then `--`, and finally a hash. |
|
234 * |
|
235 * See `wp_create_block_style_variation_instance_name` for class generation. |
|
236 */ |
|
237 preg_match( '/\bis-style-(\S+?--\w+)\b/', $block['attrs']['className'], $matches ); |
|
238 |
|
239 if ( empty( $matches ) ) { |
|
240 return $block_content; |
|
241 } |
|
242 |
|
243 $tags = new WP_HTML_Tag_Processor( $block_content ); |
|
244 |
|
245 if ( $tags->next_tag() ) { |
|
246 /* |
|
247 * Ensure the variation instance class name set in the |
|
248 * `render_block_data` filter is applied in markup. |
|
249 * See `wp_render_block_style_variation_support_styles`. |
|
250 */ |
|
251 $tags->add_class( $matches[0] ); |
|
252 } |
|
253 |
|
254 return $tags->get_updated_html(); |
|
255 } |
|
256 |
|
257 /** |
|
258 * Enqueues styles for block style variations. |
|
259 * |
|
260 * @since 6.6.0 |
|
261 * @access private |
|
262 */ |
|
263 function wp_enqueue_block_style_variation_styles() { |
|
264 wp_enqueue_style( 'block-style-variation-styles' ); |
|
265 } |
|
266 |
|
267 // Register the block support. |
|
268 WP_Block_Supports::get_instance()->register( 'block-style-variation', array() ); |
|
269 |
|
270 add_filter( 'render_block_data', 'wp_render_block_style_variation_support_styles', 10, 2 ); |
|
271 add_filter( 'render_block', 'wp_render_block_style_variation_class_name', 10, 2 ); |
|
272 add_action( 'wp_enqueue_scripts', 'wp_enqueue_block_style_variation_styles', 1 ); |
|
273 |
|
274 /** |
|
275 * Registers block style variations read in from theme.json partials. |
|
276 * |
|
277 * @since 6.6.0 |
|
278 * @access private |
|
279 * |
|
280 * @param array $variations Shared block style variations. |
|
281 */ |
|
282 function wp_register_block_style_variations_from_theme_json_partials( $variations ) { |
|
283 if ( empty( $variations ) ) { |
|
284 return; |
|
285 } |
|
286 |
|
287 $registry = WP_Block_Styles_Registry::get_instance(); |
|
288 |
|
289 foreach ( $variations as $variation ) { |
|
290 if ( empty( $variation['blockTypes'] ) || empty( $variation['styles'] ) ) { |
|
291 continue; |
|
292 } |
|
293 |
|
294 $variation_name = $variation['slug'] ?? _wp_to_kebab_case( $variation['title'] ); |
|
295 $variation_label = $variation['title'] ?? $variation_name; |
|
296 |
|
297 foreach ( $variation['blockTypes'] as $block_type ) { |
|
298 $registered_styles = $registry->get_registered_styles_for_block( $block_type ); |
|
299 |
|
300 // Register block style variation if it hasn't already been registered. |
|
301 if ( ! array_key_exists( $variation_name, $registered_styles ) ) { |
|
302 register_block_style( |
|
303 $block_type, |
|
304 array( |
|
305 'name' => $variation_name, |
|
306 'label' => $variation_label, |
|
307 ) |
|
308 ); |
|
309 } |
|
310 } |
|
311 } |
|
312 } |