PluginProbe
Gutenberg / 23.4.0
Gutenberg v23.4.0
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / lib / block-supports / states.php

states.php in Gutenberg 23.4.0, at lib/block-supports/states.php

597 lines 17.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Block state support for frontend CSS generation.
4 *
5 * Generates scoped CSS for per-instance state styles declared in block attributes,
6 * including pseudo-states (e.g., `style[':hover']`) and responsive states
7 * (e.g., `style['mobile']` and `style['mobile'][':hover']`).
8 *
9 * @package WordPress
10 */
11
12 /**
13 * Converts internal preset references to CSS custom property references.
14 *
15 * State styles are emitted as CSS rules and cannot rely on preset classnames.
16 * Converting `var:preset|color|contrast` to
17 * `var(--wp--preset--color--contrast)` ensures preset values are emitted as
18 * declarations by the style engine.
19 *
20 * @param mixed $value Style value to normalize.
21 * @return mixed Normalized style value.
22 */
23 function gutenberg_normalize_state_preset_vars( $value ) {
24 if ( is_array( $value ) ) {
25 foreach ( $value as $key => $nested_value ) {
26 $value[ $key ] = gutenberg_normalize_state_preset_vars( $nested_value );
27 }
28 return $value;
29 }
30
31 if ( ! is_string( $value ) || ! str_starts_with( $value, 'var:preset|' ) ) {
32 return $value;
33 }
34
35 $unwrapped_name = str_replace( '|', '--', substr( $value, strlen( 'var:' ) ) );
36 return "var(--wp--$unwrapped_name)";
37 }
38
39 /**
40 * Normalizes a state style object before generating CSS declarations.
41 *
42 * @param array $style State style object.
43 * @return array Normalized state style object.
44 */
45 function gutenberg_normalize_state_style_for_css_output( $style ) {
46 // Layout is processed separately by gutenberg_render_layout_support_flag(), so we remove it before declaration generation.
47 unset( $style['layout'] );
48 $style = gutenberg_normalize_state_preset_vars( $style );
49 return $style;
50 }
51
52 /**
53 * Adds fallback border-style declarations for visible border declarations.
54 *
55 * CSS does not render border color or width unless a border style is also set.
56 * State styles are emitted as stylesheet rules rather than inline styles, so
57 * they cannot rely on the block-library inline-style attribute fallback rules.
58 *
59 * @param array $declarations CSS declarations generated by the style engine.
60 * @return array CSS declarations with fallback border styles applied where needed.
61 */
62 function gutenberg_get_state_declarations_with_fallback_border_styles( $declarations ) {
63 if ( ! is_array( $declarations ) ) {
64 return $declarations;
65 }
66
67 $has_border_style = isset( $declarations['border-style'] ) && '' !== $declarations['border-style'];
68 $has_border_color = isset( $declarations['border-color'] ) && '' !== $declarations['border-color'];
69 $has_border_width = isset( $declarations['border-width'] ) && '' !== $declarations['border-width'];
70
71 if ( ! $has_border_style && ( $has_border_color || $has_border_width ) ) {
72 $declarations['border-style'] = 'solid';
73 }
74
75 $sides = array( 'top', 'right', 'bottom', 'left' );
76 foreach ( $sides as $side ) {
77 $side_style_property = "border-$side-style";
78 $side_color_property = "border-$side-color";
79 $side_width_property = "border-$side-width";
80
81 $has_side_style = isset( $declarations[ $side_style_property ] ) && '' !== $declarations[ $side_style_property ];
82 $has_side_color = isset( $declarations[ $side_color_property ] ) && '' !== $declarations[ $side_color_property ];
83 $has_side_width = isset( $declarations[ $side_width_property ] ) && '' !== $declarations[ $side_width_property ];
84
85 if ( ! $has_border_style && ! $has_side_style && ( $has_side_color || $has_side_width ) ) {
86 $declarations[ $side_style_property ] = 'solid';
87 }
88 }
89
90 return $declarations;
91 }
92
93 /**
94 * Adds a style fragment to a selector-keyed state style group.
95 *
96 * @param array $groups Selector-keyed style groups.
97 * @param string|null $selector Block or feature selector.
98 * @param array $style Style fragment.
99 */
100 function gutenberg_add_state_style_group( &$groups, $selector, $style ) {
101 $key = is_string( $selector ) ? $selector : '';
102
103 if ( ! isset( $groups[ $key ] ) ) {
104 $groups[ $key ] = array(
105 'selector' => $selector,
106 'style' => array(),
107 );
108 }
109
110 $groups[ $key ]['style'] = array_replace_recursive( $groups[ $key ]['style'], $style );
111 }
112
113 /**
114 * Splits a state style object into groups based on block feature selectors.
115 *
116 * @param array $state_style State style object.
117 * @param array $block_selectors Block selectors metadata.
118 * @return array[] Selector/style groups.
119 */
120 function gutenberg_get_state_style_groups( $state_style, $block_selectors ) {
121 $groups = array();
122
123 foreach ( $state_style as $feature => $feature_styles ) {
124 $feature_selectors = $block_selectors[ $feature ] ?? null;
125
126 if ( is_string( $feature_selectors ) ) {
127 gutenberg_add_state_style_group(
128 $groups,
129 $feature_selectors,
130 array( $feature => $feature_styles )
131 );
132 continue;
133 }
134
135 if ( is_array( $feature_selectors ) && is_array( $feature_styles ) ) {
136 $remaining_styles = $feature_styles;
137
138 foreach ( $feature_selectors as $subfeature => $subfeature_selector ) {
139 if (
140 'root' === $subfeature ||
141 ! is_string( $subfeature_selector ) ||
142 ! array_key_exists( $subfeature, $feature_styles )
143 ) {
144 continue;
145 }
146
147 gutenberg_add_state_style_group(
148 $groups,
149 $subfeature_selector,
150 array(
151 $feature => array(
152 $subfeature => $feature_styles[ $subfeature ],
153 ),
154 )
155 );
156 unset( $remaining_styles[ $subfeature ] );
157 }
158
159 if ( array() !== $remaining_styles ) {
160 gutenberg_add_state_style_group(
161 $groups,
162 $feature_selectors['root'] ?? ( $block_selectors['root'] ?? null ),
163 array( $feature => $remaining_styles )
164 );
165 }
166 continue;
167 }
168
169 gutenberg_add_state_style_group(
170 $groups,
171 $block_selectors['root'] ?? null,
172 array( $feature => $feature_styles )
173 );
174 }
175
176 return array_values( $groups );
177 }
178
179 /**
180 * Returns a style object with nested state keys removed.
181 *
182 * @param array $state_style State style object.
183 * @param array $nested_keys Keys to remove from the root style object.
184 * @return array Root-only style object.
185 */
186 function gutenberg_get_root_state_style( $state_style, $nested_keys ) {
187 if ( ! is_array( $state_style ) ) {
188 return $state_style;
189 }
190
191 $root_style = $state_style;
192 foreach ( $nested_keys as $key ) {
193 unset( $root_style[ $key ] );
194 }
195
196 return $root_style;
197 }
198
199 /**
200 * Generates all element selectors for a block root selector.
201 *
202 * @param string $root_selector The block root CSS selector.
203 * @return string[] Element selectors keyed by element name.
204 */
205 function gutenberg_get_block_state_element_selectors( $root_selector ) {
206 if ( ! is_string( $root_selector ) || '' === trim( $root_selector ) ) {
207 return array();
208 }
209
210 $block_selectors = gutenberg_split_selector_list( $root_selector );
211 $element_selectors = array();
212
213 foreach ( WP_Theme_JSON_Gutenberg::ELEMENTS as $element_name => $element_selector ) {
214 $selectors = array();
215
216 foreach ( $block_selectors as $block_selector ) {
217 $block_selector = trim( $block_selector );
218 if ( '' === $block_selector ) {
219 continue;
220 }
221
222 if ( $block_selector === $element_selector ) {
223 $selectors = array( $element_selector );
224 break;
225 }
226
227 $selector_prefix = "$block_selector ";
228 if ( ! str_contains( $element_selector, ',' ) ) {
229 $selectors[] = $selector_prefix . $element_selector;
230 continue;
231 }
232
233 $prepended_selectors = array();
234 foreach ( gutenberg_split_selector_list( $element_selector ) as $selector ) {
235 $prepended_selectors[] = $selector_prefix . $selector;
236 }
237 $selectors[] = implode( ',', $prepended_selectors );
238 }
239
240 if ( ! empty( $selectors ) ) {
241 $element_selectors[ $element_name ] = implode( ',', $selectors );
242 }
243 }
244
245 return $element_selectors;
246 }
247
248 /**
249 * Adds a compiled state style rule to a rule list.
250 *
251 * @param array $css_rules Style rules.
252 * @param string $state Pseudo-state selector.
253 * @param string|null $selector Block, feature, or element selector.
254 * @param array $style Style object.
255 * @param string|null $rules_group Optional CSS grouping rule, e.g. a media query.
256 */
257 function gutenberg_add_block_state_style_rule( &$css_rules, $state, $selector, $style, $rules_group = null ) {
258 if ( empty( $style ) || ! is_array( $style ) ) {
259 return;
260 }
261
262 $compiled = gutenberg_style_engine_get_styles(
263 gutenberg_normalize_state_style_for_css_output( $style )
264 );
265
266 if ( empty( $compiled['declarations'] ) ) {
267 return;
268 }
269
270 $css_rules[] = array(
271 'state' => $state,
272 'selector' => $selector,
273 'declarations' => $compiled['declarations'],
274 );
275 if ( ! empty( $rules_group ) ) {
276 $css_rules[ count( $css_rules ) - 1 ]['rules_group'] = $rules_group;
277 }
278 }
279
280 /**
281 * Builds compiled state style rules, preserving the selector each rule targets.
282 *
283 * @param array $state_styles Map of state to style array.
284 * @param WP_Block_Type $block_type Block type.
285 * @param string|null $rules_group Optional CSS grouping rule, e.g. a media query.
286 * @return array[] State style rules.
287 */
288 function gutenberg_get_block_state_style_rules( $state_styles, $block_type, $rules_group = null ) {
289 $css_rules = array();
290 $block_selectors = isset( $block_type->selectors ) && is_array( $block_type->selectors )
291 ? $block_type->selectors
292 : array();
293
294 foreach ( $state_styles as $state => $state_style ) {
295 if ( empty( $state_style ) || ! is_array( $state_style ) ) {
296 continue;
297 }
298
299 foreach ( gutenberg_get_state_style_groups( $state_style, $block_selectors ) as $group ) {
300 gutenberg_add_block_state_style_rule(
301 $css_rules,
302 $state,
303 $group['selector'],
304 $group['style'],
305 $rules_group
306 );
307 }
308 }
309
310 return $css_rules;
311 }
312
313 /**
314 * Returns a unique class for a set of state style rules.
315 *
316 * @param string $block_name Block name.
317 * @param array $css_rules State style rules.
318 * @return string Unique class name.
319 */
320 function gutenberg_get_block_state_unique_class( $block_name, $css_rules ) {
321 return 'wp-states-' . substr(
322 md5(
323 wp_json_encode(
324 array(
325 'blockName' => $block_name,
326 'rules' => $css_rules,
327 )
328 )
329 ),
330 0,
331 8
332 );
333 }
334
335 /**
336 * Splits a selector list by top-level commas.
337 *
338 * @param string $selector CSS selector list.
339 * @return string[] Selectors.
340 */
341 function gutenberg_split_selector_list( $selector ) {
342 if ( ! str_contains( $selector, ',' ) ) {
343 return array( $selector );
344 }
345
346 $selectors = array();
347 $current_selector = '';
348 $parentheses_depth = 0;
349 $selector_length = strlen( $selector );
350
351 for ( $i = 0; $i < $selector_length; $i++ ) {
352 $char = $selector[ $i ];
353
354 if ( '(' === $char ) {
355 ++$parentheses_depth;
356 } elseif ( ')' === $char && $parentheses_depth > 0 ) {
357 --$parentheses_depth;
358 } elseif ( ',' === $char && 0 === $parentheses_depth ) {
359 $selectors[] = $current_selector;
360 $current_selector = '';
361 continue;
362 }
363
364 $current_selector .= $char;
365 }
366
367 $selectors[] = $current_selector;
368
369 return $selectors;
370 }
371
372 /**
373 * Builds a scoped selector from a block selector and optional pseudo-state.
374 *
375 * @param string $base_selector Block-instance scoping selector.
376 * @param string|null $block_selector Block or feature selector from metadata.
377 * @param string $state Pseudo-state selector.
378 * @return string Scoped selector.
379 */
380 function gutenberg_build_state_selector( $base_selector, $block_selector, $state ) {
381 if ( ! is_string( $block_selector ) || '' === trim( $block_selector ) ) {
382 return $base_selector . $state;
383 }
384
385 $selectors = gutenberg_split_selector_list( $block_selector );
386 $scoped_selectors = array();
387
388 foreach ( $selectors as $selector ) {
389 $selector = trim( $selector );
390 if ( '' === $selector ) {
391 continue;
392 }
393
394 /*
395 * Replace only the leading block selector part (e.g. class name,
396 * attribute selector, ID, or tag name) with the block instance selector.
397 * Preserve anything after that prefix, including modifier classes on the
398 * same element and combinators without spaces.
399 */
400 if ( preg_match( '/^([.#]?[-_a-zA-Z0-9]+|\[[^\]]+\])/', $selector, $matches ) ) {
401 $scoped_selectors[] = $base_selector . substr( $selector, strlen( $matches[0] ) ) . $state;
402 continue;
403 }
404
405 $scoped_selectors[] = $base_selector . $state;
406 }
407
408 return empty( $scoped_selectors )
409 ? $base_selector . $state
410 : implode( ', ', $scoped_selectors );
411 }
412
413 /**
414 * Renders per-instance state styles on the frontend.
415 *
416 * @param string $block_content The block's rendered HTML.
417 * @param array $block The block data including blockName and attrs.
418 * @return string Modified block content with injected state styles.
419 */
420 function gutenberg_render_block_states_support( $block_content, $block ) {
421 if ( empty( $block['blockName'] ) || empty( $block_content ) ) {
422 return $block_content;
423 }
424
425 $block_name = $block['blockName'];
426 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name );
427 if ( ! $block_type ) {
428 return $block_content;
429 }
430
431 $supported_pseudo_states = WP_Theme_JSON_Gutenberg::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ?? array();
432 $style = $block['attrs']['style'] ?? array();
433 $css_rules = array();
434
435 foreach ( $supported_pseudo_states as $pseudo_state ) {
436 if ( empty( $style[ $pseudo_state ] ) || ! is_array( $style[ $pseudo_state ] ) ) {
437 continue;
438 }
439
440 $css_rules = array_merge(
441 $css_rules,
442 gutenberg_get_block_state_style_rules(
443 array( $pseudo_state => $style[ $pseudo_state ] ),
444 $block_type
445 )
446 );
447 }
448
449 foreach ( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) {
450 if ( empty( $style[ $breakpoint ] ) || ! is_array( $style[ $breakpoint ] ) ) {
451 continue;
452 }
453
454 $root_state_style = gutenberg_get_root_state_style(
455 $style[ $breakpoint ],
456 array_merge( array( 'elements' ), $supported_pseudo_states )
457 );
458
459 if ( ! empty( $root_state_style ) ) {
460 $css_rules = array_merge(
461 $css_rules,
462 gutenberg_get_block_state_style_rules(
463 array( '' => $root_state_style ),
464 $block_type,
465 $media_query
466 )
467 );
468 }
469
470 if (
471 ! empty( $style[ $breakpoint ]['elements'] ) &&
472 is_array( $style[ $breakpoint ]['elements'] )
473 ) {
474 $element_selectors = gutenberg_get_block_state_element_selectors(
475 wp_get_block_css_selector( $block_type )
476 );
477
478 foreach (
479 $style[ $breakpoint ]['elements'] as $element_name => $element_style
480 ) {
481 if (
482 empty( $element_style ) ||
483 ! is_array( $element_style ) ||
484 empty( $element_selectors[ $element_name ] )
485 ) {
486 continue;
487 }
488
489 $element_pseudo_states = WP_Theme_JSON_Gutenberg::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ]
490 ?? array();
491 $root_element_style = gutenberg_get_root_state_style(
492 $element_style,
493 $element_pseudo_states
494 );
495
496 gutenberg_add_block_state_style_rule(
497 $css_rules,
498 '',
499 $element_selectors[ $element_name ],
500 $root_element_style,
501 $media_query
502 );
503
504 foreach ( $element_pseudo_states as $pseudo_state ) {
505 if (
506 empty( $element_style[ $pseudo_state ] ) ||
507 ! is_array( $element_style[ $pseudo_state ] )
508 ) {
509 continue;
510 }
511
512 gutenberg_add_block_state_style_rule(
513 $css_rules,
514 $pseudo_state,
515 $element_selectors[ $element_name ],
516 $element_style[ $pseudo_state ],
517 $media_query
518 );
519 }
520 }
521 }
522
523 foreach ( $supported_pseudo_states as $pseudo_state ) {
524 if ( empty( $style[ $breakpoint ][ $pseudo_state ] ) || ! is_array( $style[ $breakpoint ][ $pseudo_state ] ) ) {
525 continue;
526 }
527
528 $css_rules = array_merge(
529 $css_rules,
530 gutenberg_get_block_state_style_rules(
531 array( $pseudo_state => $style[ $breakpoint ][ $pseudo_state ] ),
532 $block_type,
533 $media_query
534 )
535 );
536 }
537 }
538
539 if ( empty( $css_rules ) ) {
540 return $block_content;
541 }
542
543 $unique_class = gutenberg_get_block_state_unique_class( $block_name, $css_rules );
544
545 /*
546 * Register each state's CSS rules with the block-supports style engine store.
547 * The store deduplicates rules by selector — two block instances with identical
548 * state styles share the same hash class and therefore the same selector,
549 * so only one CSS rule is emitted. The store is flushed to the page by
550 * gutenberg_enqueue_stored_styles() rather than injected inline here.
551 *
552 * State declarations need !important to apply reliably over inline styles and
553 * preset utility classes such as .has-accent-3-background-color.
554 *
555 * Layout-driven state styles (responsive layout, blockGap, child layout) are
556 * handled by gutenberg_render_layout_support_flag() so they share a selector
557 * with the base layout and target the correct (inner) wrapper element.
558 */
559 $style_rules = array();
560 foreach ( $css_rules as $rule ) {
561 $declarations = array();
562 foreach ( $rule['declarations'] as $property => $value ) {
563 $declarations[ $property ] = is_string( $value ) && str_contains( $value, '!important' )
564 ? $value
565 : $value . ' !important';
566 }
567 $declarations = gutenberg_get_state_declarations_with_fallback_border_styles( $declarations );
568 $style_rule = array(
569 'selector' => gutenberg_build_state_selector(
570 ".$unique_class",
571 $rule['selector'],
572 $rule['state']
573 ),
574 'declarations' => $declarations,
575 );
576 if ( ! empty( $rule['rules_group'] ) ) {
577 $style_rule['rules_group'] = $rule['rules_group'];
578 }
579 $style_rules[] = $style_rule;
580 }
581
582 gutenberg_style_engine_get_stylesheet_from_css_rules(
583 $style_rules,
584 array(
585 'context' => 'block-supports',
586 'prettify' => false,
587 )
588 );
589
590 $processor = new WP_HTML_Tag_Processor( $block_content );
591 if ( $processor->next_tag() ) {
592 $processor->add_class( $unique_class );
593 }
594 return $processor->get_updated_html();
595 }
596 add_filter( 'render_block', 'gutenberg_render_block_states_support', 10, 2 );
597