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 / class-wp-duotone-gutenberg.php

class-wp-duotone-gutenberg.php in Gutenberg 23.4.0, at lib/class-wp-duotone-gutenberg.php

1,176 lines 37.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WP_Duotone_Gutenberg class
4 *
5 * Parts of this source were derived and modified from colord,
6 * released under the MIT license.
7 *
8 * https://github.com/omgovich/colord
9 *
10 * Copyright (c) 2020 Vlad Shilov omgovich@ya.ru
11 *
12 * Permission is hereby granted, free of charge, to any person obtaining
13 * a copy of this software and associated documentation files (the
14 * "Software"), to deal in the Software without restriction, including
15 * without limitation the rights to use, copy, modify, merge, publish,
16 * distribute, sublicense, and/or sell copies of the Software, and to
17 * permit persons to whom the Software is furnished to do so, subject to
18 * the following conditions:
19 *
20 * The above copyright notice and this permission notice shall be
21 * included in all copies or substantial portions of the Software.
22 *
23 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 *
31 * @package gutenberg
32 * @since 6.3.0
33 */
34
35 /**
36 * Manages duotone block supports and global styles.
37 *
38 * @access public
39 */
40 class WP_Duotone_Gutenberg {
41 /**
42 * Block names from global, theme, and custom styles that use duotone presets and the slug of
43 * the preset they are using.
44 *
45 * Example:
46 * [
47 * 'core/featured-image' => 'blue-orange',
48 * …
49 * ]
50 *
51 * @var ?array<string, string>
52 */
53 private static $global_styles_block_names;
54
55 /**
56 * An array of duotone filter data from global, theme, and custom presets.
57 *
58 * Example:
59 * [
60 * 'wp-duotone-blue-orange' => [
61 * 'slug' => 'blue-orange',
62 * 'colors' => [ '#0000ff', '#ffcc00' ],
63 * ],
64 * 'wp-duotone-red-yellow' => [
65 * 'slug' => 'red-yellow',
66 * 'colors' => [ '#cc0000', '#ffff33' ],
67 * ],
68 * …
69 * ]
70 *
71 * @var ?array<string, array<string, string|string[]>>
72 */
73 private static $global_styles_presets;
74
75 /**
76 * All of the duotone filter data from presets for CSS custom properties on
77 * the page.
78 *
79 * Example:
80 * [
81 * 'wp-duotone-blue-orange' => [
82 * 'slug' => 'blue-orange',
83 * 'colors' => [ '#0000ff', '#ffcc00' ],
84 * ],
85 * …
86 * ]
87 *
88 * @var array
89 */
90 private static $used_global_styles_presets = array();
91
92 /**
93 * All of the duotone filter data for SVGs on the page. Includes both
94 * presets and custom filters.
95 *
96 * Example:
97 * [
98 * 'wp-duotone-blue-orange' => [
99 * 'slug' => 'blue-orange',
100 * 'colors' => [ '#0000ff', '#ffcc00' ],
101 * ],
102 * 'wp-duotone-000000-ffffff-2' => [
103 * 'slug' => '000000-ffffff-2',
104 * 'colors' => [ '#000000', '#ffffff' ],
105 * ],
106 * …
107 * ]
108 *
109 * @var array
110 */
111 private static $used_svg_filter_data = array();
112
113 /**
114 * All of the block CSS declarations for styles on the page.
115 *
116 * Example:
117 * [
118 * [
119 * 'selector' => '.wp-duotone-000000-ffffff-2.wp-block-image img',
120 * 'declarations' => [
121 * 'filter' => 'url(#wp-duotone-000000-ffffff-2)',
122 * ],
123 * ],
124 * …
125 * ]
126 *
127 * @var array
128 */
129 private static $block_css_declarations = array();
130
131 /**
132 * Direct port of colord's clamp function. Using min/max instead of
133 * nested ternaries.
134 *
135 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/helpers.ts#L23
136 *
137 * @param float $number The number to clamp.
138 * @param float $min The minimum value.
139 * @param float $max The maximum value.
140 * @return float The clamped value.
141 */
142 private static function colord_clamp( $number, $min = 0, $max = 1 ) {
143 return $number > $max ? $max : ( $number > $min ? $number : $min );
144 }
145
146 /**
147 * Direct port of colord's clampHue function.
148 *
149 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/helpers.ts#L32
150 *
151 * @param float $degrees The hue to clamp.
152 * @return float The clamped hue.
153 */
154 private static function colord_clamp_hue( $degrees ) {
155 $degrees = is_finite( $degrees ) ? $degrees % 360 : 0;
156 return $degrees > 0 ? $degrees : $degrees + 360;
157 }
158
159 /**
160 * Direct port of colord's parseHue function.
161 *
162 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/helpers.ts#L40
163 *
164 * @param float $value The hue value to parse.
165 * @param string $unit The unit of the hue value.
166 * @return float The parsed hue value.
167 */
168 private static function colord_parse_hue( $value, $unit = 'deg' ) {
169 $angle_units = array(
170 'grad' => 360 / 400,
171 'turn' => 360,
172 'rad' => 360 / ( M_PI * 2 ),
173 );
174
175 $factor = $angle_units[ $unit ];
176 if ( ! $factor ) {
177 $factor = 1;
178 }
179
180 return (float) $value * $factor;
181 }
182
183 /**
184 * Direct port of colord's parseHex function.
185 *
186 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hex.ts#L8
187 *
188 * @param string $hex The hex string to parse.
189 * @return array|null An array of RGBA values or null if the hex string is invalid.
190 */
191 private static function colord_parse_hex( $hex ) {
192 $is_match = preg_match(
193 '/^#([0-9a-f]{3,8})$/i',
194 $hex,
195 $hex_match
196 );
197
198 if ( ! $is_match ) {
199 return null;
200 }
201
202 $hex = $hex_match[1];
203
204 if ( 4 >= strlen( $hex ) ) {
205 return array(
206 'r' => (int) base_convert( $hex[0] . $hex[0], 16, 10 ),
207 'g' => (int) base_convert( $hex[1] . $hex[1], 16, 10 ),
208 'b' => (int) base_convert( $hex[2] . $hex[2], 16, 10 ),
209 'a' => 4 === strlen( $hex ) ? round( base_convert( $hex[3] . $hex[3], 16, 10 ) / 255, 2 ) : 1,
210 );
211 }
212
213 if ( 6 === strlen( $hex ) || 8 === strlen( $hex ) ) {
214 return array(
215 'r' => (int) base_convert( substr( $hex, 0, 2 ), 16, 10 ),
216 'g' => (int) base_convert( substr( $hex, 2, 2 ), 16, 10 ),
217 'b' => (int) base_convert( substr( $hex, 4, 2 ), 16, 10 ),
218 'a' => 8 === strlen( $hex ) ? round( (int) base_convert( substr( $hex, 6, 2 ), 16, 10 ) / 255, 2 ) : 1,
219 );
220 }
221
222 return null;
223 }
224
225 /**
226 * Direct port of colord's clampRgba function.
227 *
228 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/rgb.ts#L5
229 *
230 * @param array $rgba The RGBA array to clamp.
231 * @return array The clamped RGBA array.
232 */
233 private static function colord_clamp_rgba( $rgba ) {
234 $rgba['r'] = self::colord_clamp( $rgba['r'], 0, 255 );
235 $rgba['g'] = self::colord_clamp( $rgba['g'], 0, 255 );
236 $rgba['b'] = self::colord_clamp( $rgba['b'], 0, 255 );
237 $rgba['a'] = self::colord_clamp( $rgba['a'] );
238
239 return $rgba;
240 }
241
242 /**
243 * Direct port of colord's parseRgbaString function.
244 *
245 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/rgbString.ts#L18
246 *
247 * @param string $input The RGBA string to parse.
248 * @return array|null An array of RGBA values or null if the RGB string is invalid.
249 */
250 private static function colord_parse_rgba_string( $input ) {
251 // Functional syntax.
252 $is_match = preg_match(
253 '/^rgba?\(\s*([+-]?\d*\.?\d+)(%)?\s*,\s*([+-]?\d*\.?\d+)(%)?\s*,\s*([+-]?\d*\.?\d+)(%)?\s*(?:,\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i',
254 $input,
255 $match
256 );
257
258 if ( ! $is_match ) {
259 // Whitespace syntax.
260 $is_match = preg_match(
261 '/^rgba?\(\s*([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i',
262 $input,
263 $match
264 );
265 }
266
267 if ( ! $is_match ) {
268 return null;
269 }
270
271 // For some reason, preg_match doesn't include empty matches at the end
272 // of the array, so we add them manually to make things easier later.
273 for ( $i = 1; $i <= 8; $i++ ) {
274 if ( ! isset( $match[ $i ] ) ) {
275 $match[ $i ] = '';
276 }
277 }
278
279 if ( $match[2] !== $match[4] || $match[4] !== $match[6] ) {
280 return null;
281 }
282
283 return self::colord_clamp_rgba(
284 array(
285 'r' => (float) $match[1] / ( $match[2] ? 100 / 255 : 1 ),
286 'g' => (float) $match[3] / ( $match[4] ? 100 / 255 : 1 ),
287 'b' => (float) $match[5] / ( $match[6] ? 100 / 255 : 1 ),
288 'a' => '' === $match[7] ? 1 : (float) $match[7] / ( $match[8] ? 100 : 1 ),
289 )
290 );
291 }
292
293 /**
294 * Direct port of colord's clampHsla function.
295 *
296 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hsl.ts#L6
297 *
298 * @param array $hsla The HSLA array to clamp.
299 * @return array The clamped HSLA array.
300 */
301 private static function colord_clamp_hsla( $hsla ) {
302 $hsla['h'] = self::colord_clamp_hue( $hsla['h'] );
303 $hsla['s'] = self::colord_clamp( $hsla['s'], 0, 100 );
304 $hsla['l'] = self::colord_clamp( $hsla['l'], 0, 100 );
305 $hsla['a'] = self::colord_clamp( $hsla['a'] );
306
307 return $hsla;
308 }
309
310 /**
311 * Direct port of colord's hsvaToRgba function.
312 *
313 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hsv.ts#L52
314 *
315 * @param array $hsva The HSVA array to convert.
316 * @return array The RGBA array.
317 */
318 private static function colord_hsva_to_rgba( $hsva ) {
319 $h = ( $hsva['h'] / 360 ) * 6;
320 $s = $hsva['s'] / 100;
321 $v = $hsva['v'] / 100;
322 $a = $hsva['a'];
323
324 $hh = floor( $h );
325 $b = $v * ( 1 - $s );
326 $c = $v * ( 1 - ( $h - $hh ) * $s );
327 $d = $v * ( 1 - ( 1 - $h + $hh ) * $s );
328 $module = $hh % 6;
329
330 return array(
331 'r' => array( $v, $c, $b, $b, $d, $v )[ $module ] * 255,
332 'g' => array( $d, $v, $v, $c, $b, $b )[ $module ] * 255,
333 'b' => array( $b, $b, $d, $v, $v, $c )[ $module ] * 255,
334 'a' => $a,
335 );
336 }
337
338 /**
339 * Direct port of colord's hslaToHsva function.
340 *
341 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hsl.ts#L33
342 *
343 * @param array $hsla The HSLA array to convert.
344 * @return array The HSVA array.
345 */
346 private static function colord_hsla_to_hsva( $hsla ) {
347 $h = $hsla['h'];
348 $s = $hsla['s'];
349 $l = $hsla['l'];
350 $a = $hsla['a'];
351
352 $s *= ( $l < 50 ? $l : 100 - $l ) / 100;
353
354 return array(
355 'h' => $h,
356 's' => $s > 0 ? ( ( 2 * $s ) / ( $l + $s ) ) * 100 : 0,
357 'v' => $l + $s,
358 'a' => $a,
359 );
360 }
361
362 /**
363 * Direct port of colord's hslaToRgba function.
364 *
365 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hsl.ts#L55
366 *
367 * @param array $hsla The HSLA array to convert.
368 * @return array The RGBA array.
369 */
370 private static function colord_hsla_to_rgba( $hsla ) {
371 return self::colord_hsva_to_rgba( self::colord_hsla_to_hsva( $hsla ) );
372 }
373
374 /**
375 * Direct port of colord's parseHslaString function.
376 *
377 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hslString.ts#L17
378 *
379 * @param string $input The HSLA string to parse.
380 * @return array|null An array of RGBA values or null if the RGB string is invalid.
381 */
382 private static function colord_parse_hsla_string( $input ) {
383 // Functional syntax.
384 $is_match = preg_match(
385 '/^hsla?\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s*,\s*([+-]?\d*\.?\d+)%\s*,\s*([+-]?\d*\.?\d+)%\s*(?:,\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i',
386 $input,
387 $match
388 );
389
390 if ( ! $is_match ) {
391 // Whitespace syntax.
392 $is_match = preg_match(
393 '/^hsla?\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s+([+-]?\d*\.?\d+)%\s+([+-]?\d*\.?\d+)%\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i',
394 $input,
395 $match
396 );
397 }
398
399 if ( ! $is_match ) {
400 return null;
401 }
402
403 // For some reason, preg_match doesn't include empty matches at the end
404 // of the array, so we add them manually to make things easier later.
405 for ( $i = 1; $i <= 6; $i++ ) {
406 if ( ! isset( $match[ $i ] ) ) {
407 $match[ $i ] = '';
408 }
409 }
410
411 $hsla = self::colord_clamp_hsla(
412 array(
413 'h' => self::colord_parse_hue( $match[1], $match[2] ),
414 's' => (float) $match[3],
415 'l' => (float) $match[4],
416 'a' => '' === $match[5] ? 1 : (float) $match[5] / ( $match[6] ? 100 : 1 ),
417 )
418 );
419
420 return self::colord_hsla_to_rgba( $hsla );
421 }
422
423 /**
424 * Direct port of colord's parse function simplified for our use case. This
425 * version only supports string parsing and only returns RGBA values.
426 *
427 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/parse.ts#L37
428 *
429 * @param string $input The string to parse.
430 * @return array|null An array of RGBA values or null if the string is invalid.
431 */
432 private static function colord_parse( $input ) {
433 $result = self::colord_parse_hex( $input );
434
435 if ( ! $result ) {
436 $result = self::colord_parse_rgba_string( $input );
437 }
438
439 if ( ! $result ) {
440 $result = self::colord_parse_hsla_string( $input );
441 }
442
443 return $result;
444 }
445
446 /**
447 * Take the inline CSS duotone variable from a block and return the slug. Handles styles slugs like:
448 * var:preset|duotone|blue-orange
449 * var(--wp--preset--duotone--blue-orange)
450 *
451 * @param string|string[] $duotone_attr The duotone attribute from a block.
452 * @return string The slug of the duotone preset or an empty string if no slug is found (including when an array was passed).
453 */
454 private static function get_slug_from_attribute( $duotone_attr ) {
455 if ( ! is_string( $duotone_attr ) ) {
456 return '';
457 }
458
459 // Uses Branch Reset Groups `(?|…)` to return one capture group.
460 preg_match( '/(?|var:preset\|duotone\|(\S+)|var\(--wp--preset--duotone--(\S+)\))/', $duotone_attr, $matches );
461
462 return ! empty( $matches[1] ) ? $matches[1] : '';
463 }
464
465 /**
466 * Check if we have a valid duotone preset.
467 *
468 * @param string|string[] $duotone_attr The duotone attribute from a block.
469 * @return bool True if the duotone preset present and valid.
470 */
471 private static function is_preset( $duotone_attr ) {
472 if ( ! is_string( $duotone_attr ) ) {
473 return false;
474 }
475
476 $slug = self::get_slug_from_attribute( $duotone_attr );
477 $filter_id = self::get_filter_id( $slug );
478
479 return array_key_exists( $filter_id, self::get_all_global_styles_presets() );
480 }
481
482 /**
483 * Get the CSS variable name for a duotone preset.
484 *
485 * @param string $slug The slug of the duotone preset.
486 * @return string The CSS variable name.
487 */
488 private static function get_css_custom_property_name( $slug ) {
489 return "--wp--preset--duotone--$slug";
490 }
491
492 /**
493 * Get the ID of the duotone filter.
494 *
495 * @param string $slug The slug of the duotone preset.
496 * @return string The ID of the duotone filter.
497 */
498 private static function get_filter_id( $slug ) {
499 return "wp-duotone-$slug";
500 }
501
502 /**
503 * Get the CSS variable for a duotone preset.
504 *
505 * @param string $slug The slug of the duotone preset.
506 * @return string The CSS variable.
507 */
508 private static function get_css_var( $slug ) {
509 $name = self::get_css_custom_property_name( $slug );
510 return "var($name)";
511 }
512
513 /**
514 * Get the URL for a duotone filter.
515 *
516 * @param string $filter_id The ID of the filter.
517 * @return string The URL for the duotone filter.
518 */
519 private static function get_filter_url( $filter_id ) {
520 return "url(#$filter_id)";
521 }
522
523 /**
524 * Gets the SVG for the duotone filter definition.
525 *
526 * @param string $filter_id The ID of the filter.
527 * @param array $colors An array of color strings.
528 * @return string An SVG with a duotone filter definition.
529 */
530 private static function get_filter_svg( $filter_id, $colors ) {
531 $duotone_values = array(
532 'r' => array(),
533 'g' => array(),
534 'b' => array(),
535 'a' => array(),
536 );
537
538 foreach ( $colors as $color_str ) {
539 $color = self::colord_parse( $color_str );
540
541 if ( null === $color ) {
542 $error_message = sprintf(
543 /* translators: %s: duotone colors */
544 __( '"%s" in theme.json settings.color.duotone is not a hex or rgb string.', 'gutenberg' ),
545 $color_str
546 );
547 _doing_it_wrong( __METHOD__, $error_message, '6.3.0' );
548 } else {
549 $duotone_values['r'][] = $color['r'] / 255;
550 $duotone_values['g'][] = $color['g'] / 255;
551 $duotone_values['b'][] = $color['b'] / 255;
552 $duotone_values['a'][] = $color['a'];
553 }
554 }
555
556 ob_start();
557
558 ?>
559
560 <svg
561 xmlns="http://www.w3.org/2000/svg"
562 viewBox="0 0 0 0"
563 width="0"
564 height="0"
565 focusable="false"
566 role="none"
567 style="visibility: hidden; position: absolute; left: -9999px; overflow: hidden;"
568 >
569 <defs>
570 <filter id="<?php echo esc_attr( $filter_id ); ?>">
571 <feColorMatrix
572 color-interpolation-filters="sRGB"
573 type="matrix"
574 values="
575 .299 .587 .114 0 0
576 .299 .587 .114 0 0
577 .299 .587 .114 0 0
578 .299 .587 .114 0 0
579 "
580 />
581 <feComponentTransfer color-interpolation-filters="sRGB" >
582 <feFuncR type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['r'] ) ); ?>" />
583 <feFuncG type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['g'] ) ); ?>" />
584 <feFuncB type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['b'] ) ); ?>" />
585 <feFuncA type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['a'] ) ); ?>" />
586 </feComponentTransfer>
587 <feComposite in2="SourceGraphic" operator="in" />
588 </filter>
589 </defs>
590 </svg>
591
592 <?php
593
594 $svg = ob_get_clean();
595
596 if ( ! SCRIPT_DEBUG ) {
597 // Clean up the whitespace.
598 $svg = preg_replace( "/[\r\n\t ]+/", ' ', $svg );
599 $svg = str_replace( '> <', '><', $svg );
600 $svg = trim( $svg );
601 }
602
603 return $svg;
604 }
605
606 /**
607 * Get the SVGs for the duotone filters.
608 *
609 * Example output:
610 * <svg><defs><filter id="wp-duotone-blue-orange">…</filter></defs></svg><svg>…</svg>
611 *
612 * @param array $sources The duotone presets.
613 * @return string The SVGs for the duotone filters.
614 */
615 private static function get_svg_definitions( $sources ) {
616 $svgs = '';
617 foreach ( $sources as $filter_id => $filter_data ) {
618 $colors = $filter_data['colors'];
619 $svgs .= self::get_filter_svg( $filter_id, $colors );
620 }
621 return $svgs;
622 }
623
624 /**
625 * Get the CSS for global styles.
626 *
627 * Example output:
628 * body{--wp--preset--duotone--blue-orange:url('#wp-duotone-blue-orange');}
629 *
630 * @param array $sources The duotone presets.
631 * @return string The CSS for global styles.
632 */
633 private static function get_global_styles_presets( $sources ) {
634 $css = WP_Theme_JSON_Gutenberg::ROOT_CSS_PROPERTIES_SELECTOR . '{';
635 foreach ( $sources as $filter_id => $filter_data ) {
636 $slug = $filter_data['slug'];
637 $colors = $filter_data['colors'];
638 $css_property_name = self::get_css_custom_property_name( $slug );
639 $declaration_value = is_string( $colors ) ? $colors : self::get_filter_url( $filter_id );
640 $css .= "$css_property_name:$declaration_value;";
641 }
642 $css .= '}';
643 return $css;
644 }
645
646 /**
647 * Get the CSS selector for a block type.
648 *
649 * @param WP_Block_Type $block_type Block type to check for support.
650 *
651 * @return string|null The CSS selector. Null if there is no support or the $block_type is not a WP_Block_Type.
652 */
653 private static function get_selector( $block_type ) {
654 if ( ! ( $block_type instanceof WP_Block_Type ) ) {
655 return null;
656 }
657
658 /*
659 * Backward compatibility with `supports.color.__experimentalDuotone`
660 * is provided via the `block_type_metadata_settings` filter. If
661 * `supports.filter.duotone` has not been set and the experimental
662 * property has been, the experimental property value is copied into
663 * `supports.filter.duotone`.
664 */
665 $duotone_support = block_has_support( $block_type, array( 'filter', 'duotone' ) );
666 if ( ! $duotone_support ) {
667 return null;
668 }
669
670 /*
671 * If the experimental duotone support was set, that value is to be
672 * treated as a selector and requires scoping.
673 */
674 $experimental_duotone = $block_type->supports['color']['__experimentalDuotone'] ?? false;
675 if ( $experimental_duotone ) {
676 $root_selector = wp_get_block_css_selector( $block_type );
677 return is_string( $experimental_duotone )
678 ? WP_Theme_JSON::scope_selector( $root_selector, $experimental_duotone )
679 : $root_selector;
680 }
681
682 // Regular filter.duotone support uses filter.duotone selectors with fallbacks.
683 return wp_get_block_css_selector( $block_type, array( 'filter', 'duotone' ), true );
684 }
685
686 /**
687 * Enqueue a block CSS declaration for the page.
688 *
689 * @param string $filter_id The filter ID. e.g. 'wp-duotone-000000-ffffff-2'.
690 * @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'.
691 * @param string $filter_value The filter CSS value. e.g. 'url(#wp-duotone-000000-ffffff-2)' or 'unset'.
692 */
693 private static function enqueue_block_css( $filter_id, $duotone_selector, $filter_value ) {
694 // Build the CSS selectors to which the filter will be applied.
695 $selectors = explode( ',', $duotone_selector );
696
697 $selectors_scoped = array();
698 foreach ( $selectors as $selector_part ) {
699 // Assuming the selector part is a subclass selector (not a tag name)
700 // so we can prepend the filter id class. If we want to support elements
701 // such as `img` or namespaces, we'll need to add a case for that here.
702 $selectors_scoped[] = '.' . $filter_id . trim( $selector_part );
703 }
704
705 $selector = implode( ', ', $selectors_scoped );
706
707 self::$block_css_declarations[] = array(
708 'selector' => $selector,
709 'declarations' => array(
710 'filter' => $filter_value,
711 ),
712 );
713 }
714
715 /**
716 * Enqueue custom filter assets for the page. Includes an SVG filter and block CSS declaration.
717 *
718 * @param string $filter_id The filter ID. e.g. 'wp-duotone-000000-ffffff-2'.
719 * @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'.
720 * @param string $filter_value The filter CSS value. e.g. 'url(#wp-duotone-000000-ffffff-2)' or 'unset'.
721 * @param array $filter_data Duotone filter data with 'slug' and 'colors' keys.
722 */
723 private static function enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $filter_data ) {
724 self::$used_svg_filter_data[ $filter_id ] = $filter_data;
725 self::enqueue_block_css( $filter_id, $duotone_selector, $filter_value );
726 }
727
728 /**
729 * Enqueue preset assets for the page. Includes a CSS custom property, SVG filter, and block CSS declaration.
730 *
731 * @param string $filter_id The filter ID. e.g. 'wp-duotone-blue-orange'.
732 * @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'.
733 * @param string $filter_value The filter CSS value. e.g. 'url(#wp-duotone-blue-orange)' or 'unset'.
734 */
735 private static function enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value ) {
736 $global_styles_presets = self::get_all_global_styles_presets();
737 if ( ! array_key_exists( $filter_id, $global_styles_presets ) ) {
738 $error_message = sprintf(
739 /* translators: %s: duotone filter ID */
740 __( 'The duotone id "%s" is not registered in theme.json settings', 'gutenberg' ),
741 $filter_id
742 );
743 _doing_it_wrong( __METHOD__, $error_message, '6.3.0' );
744 return;
745 }
746 self::$used_global_styles_presets[ $filter_id ] = $global_styles_presets[ $filter_id ];
747 self::enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $global_styles_presets[ $filter_id ] );
748 }
749
750 /**
751 * Registers the style and colors block attributes for block types that support it.
752 *
753 * @since 6.3.0
754 *
755 * @param WP_Block_Type $block_type Block Type.
756 */
757 public static function register_duotone_support( $block_type ) {
758 // Previous `color.__experimentalDuotone` support flag is migrated
759 // to `filter.duotone` via `block_type_metadata_settings` filter.
760 if ( block_has_support( $block_type, array( 'filter', 'duotone' ), null ) ) {
761 if ( ! $block_type->attributes ) {
762 $block_type->attributes = array();
763 }
764
765 if ( ! array_key_exists( 'style', $block_type->attributes ) ) {
766 $block_type->attributes['style'] = array(
767 'type' => 'object',
768 );
769 }
770 }
771 }
772
773 /**
774 * Get all possible duotone presets from global and theme styles and store as slug => [ colors array ]
775 * We only want to process this one time. On block render we'll access and output only the needed presets for that page.
776 *
777 * @since 6.3.0
778 *
779 * @return array<string, array<string, string|string[]>> An array of global styles presets, keyed on the filter ID.
780 */
781 private static function get_all_global_styles_presets() {
782 if ( isset( self::$global_styles_presets ) ) {
783 return self::$global_styles_presets;
784 }
785
786 // Get the per block settings from the theme.json.
787 $tree = gutenberg_get_global_settings();
788 $presets_by_origin = $tree['color']['duotone'] ?? array();
789
790 self::$global_styles_presets = array();
791 foreach ( $presets_by_origin as $presets ) {
792 foreach ( $presets as $preset ) {
793 $filter_id = self::get_filter_id( _wp_to_kebab_case( $preset['slug'] ) );
794
795 self::$global_styles_presets[ $filter_id ] = $preset;
796 }
797 }
798
799 return self::$global_styles_presets;
800 }
801
802 /**
803 * Ensure all possible duotone presets are ready.
804 * This function is deprecated, as it's no longer necessary to handle this manually.
805 * It is kept to ensure external users of this method don't break.
806 *
807 * @since 6.3.0
808 * @deprecated
809 */
810 public static function set_global_styles_presets() {
811 _deprecated_function( __METHOD__, 'Gutenberg 22.5.0' );
812 self::get_all_global_styles_presets();
813 }
814
815 /**
816 * Scrape all block names from global styles and store in self::$global_styles_block_names
817 *
818 * @since 6.3.0
819 *
820 * @return null|array<string, string> An array of global style block slugs, keyed on the block name.
821 */
822 private static function get_all_global_style_block_names() {
823 if ( isset( self::$global_styles_block_names ) ) {
824 return self::$global_styles_block_names;
825 }
826
827 // Get the per block settings from the theme.json.
828 $tree = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data();
829 $block_nodes = $tree->get_styles_block_nodes();
830 $theme_json = $tree->get_raw_data();
831
832 self::$global_styles_block_names = array();
833
834 foreach ( $block_nodes as $block_node ) {
835 // This block definition doesn't include any duotone settings. Skip it.
836 if ( empty( $block_node['duotone'] ) ) {
837 continue;
838 }
839
840 // Value looks like this: 'var(--wp--preset--duotone--blue-orange)' or 'var:preset|duotone|blue-orange'.
841 $duotone_attr_path = array_merge( $block_node['path'], array( 'filter', 'duotone' ) );
842 $duotone_attr = _wp_array_get( $theme_json, $duotone_attr_path, array() );
843
844 if ( empty( $duotone_attr ) ) {
845 continue;
846 }
847 // If it has a duotone filter preset, save the block name and the preset slug.
848 // Only process if it's a string (preset reference), not an array (custom colors).
849 if ( ! is_string( $duotone_attr ) ) {
850 continue;
851 }
852
853 $slug = self::get_slug_from_attribute( $duotone_attr );
854
855 if ( $slug && $slug !== $duotone_attr ) {
856 self::$global_styles_block_names[ $block_node['name'] ] = $slug;
857 }
858 }
859
860 return self::$global_styles_block_names;
861 }
862
863 /**
864 * Ensure all possible global style block names are ready.
865 * This function is deprecated, as it's no longer necessary to handle this manually.
866 * It is kept to ensure external users of this method don't break.
867 *
868 * @since 6.3.0
869 * @deprecated
870 */
871 public static function set_global_style_block_names() {
872 _deprecated_function( __METHOD__, 'Gutenberg 22.5.0' );
873 self::get_all_global_style_block_names();
874 }
875
876 /**
877 * Render out the duotone CSS styles and SVG.
878 *
879 * @since 6.3.0
880 *
881 * @param string $block_content Rendered block content.
882 * @param array $block Block object.
883 * @param WP_Block $wp_block The block instance.
884 * @return string Filtered block content.
885 */
886 public static function render_duotone_support( $block_content, $block, $wp_block ) {
887 if ( ! $block['blockName'] ) {
888 return $block_content;
889 }
890 $duotone_selector = self::get_selector( $wp_block->block_type );
891
892 if ( ! $duotone_selector ) {
893 return $block_content;
894 }
895
896 $global_styles_block_names = self::get_all_global_style_block_names();
897
898 // The block should have a duotone attribute or have duotone defined in its theme.json to be processed.
899 $has_duotone_attribute = isset( $block['attrs']['style']['color']['duotone'] );
900 $has_global_styles_duotone = array_key_exists( $block['blockName'], $global_styles_block_names );
901
902 if ( ! $has_duotone_attribute && ! $has_global_styles_duotone ) {
903 return $block_content;
904 }
905
906 // Generate the pieces needed for rendering a duotone to the page.
907 if ( $has_duotone_attribute ) {
908
909 // Possible values for duotone attribute:
910 // 1. Array of colors - e.g. array('#000000', '#ffffff').
911 // 2. Variable for an existing Duotone preset - e.g. 'var:preset|duotone|blue-orange' or 'var(--wp--preset--duotone--blue-orange)''
912 // 3. A CSS string - e.g. 'unset' to remove globally applied duotone.
913
914 $duotone_attr = $block['attrs']['style']['color']['duotone'];
915 $is_preset = is_string( $duotone_attr ) && self::is_preset( $duotone_attr );
916 $is_css = is_string( $duotone_attr ) && ! $is_preset;
917 $is_custom = is_array( $duotone_attr );
918
919 if ( $is_preset ) {
920 $slug = self::get_slug_from_attribute( $duotone_attr ); // e.g. 'blue-orange'.
921 $filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-blue-orange'.
922 $filter_value = self::get_css_var( $slug ); // e.g. 'var(--wp--preset--duotone--blue-orange)'.
923
924 // CSS custom property, SVG filter, and block CSS.
925 self::enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value );
926
927 } elseif ( $is_css ) {
928 $slug = wp_unique_id( sanitize_key( $duotone_attr . '-' ) ); // e.g. 'unset-1'.
929 $filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-unset-1'.
930 $filter_value = $duotone_attr; // e.g. 'unset'.
931
932 // Just block CSS.
933 self::enqueue_block_css( $filter_id, $duotone_selector, $filter_value );
934 } elseif ( $is_custom ) {
935 $slug = wp_unique_id( sanitize_key( implode( '-', $duotone_attr ) . '-' ) ); // e.g. '000000-ffffff-2'.
936 $filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-000000-ffffff-2'.
937 $filter_value = self::get_filter_url( $filter_id ); // e.g. 'url(#wp-duotone-filter-000000-ffffff-2)'.
938 $filter_data = array(
939 'slug' => $slug,
940 'colors' => $duotone_attr,
941 );
942
943 // SVG filter and block CSS.
944 self::enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $filter_data );
945 }
946 } elseif ( $has_global_styles_duotone ) {
947 $slug = $global_styles_block_names[ $block['blockName'] ]; // e.g. 'blue-orange'.
948 $filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-blue-orange'.
949 $filter_value = self::get_css_var( $slug ); // e.g. 'var(--wp--preset--duotone--blue-orange)'.
950
951 // CSS custom property, SVG filter, and block CSS.
952 self::enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value );
953 }
954
955 // Like the layout hook, this assumes the hook only applies to blocks with a single wrapper.
956 $tags = new WP_HTML_Tag_Processor( $block_content );
957 if ( $tags->next_tag() && isset( $filter_id ) ) {
958 $tags->add_class( $filter_id );
959 }
960
961 return $tags->get_updated_html();
962 }
963
964 /**
965 * Fixes the issue with our generated class name not being added to the block's outer container
966 * in classic themes due to gutenberg_restore_image_outer_container from layout block supports.
967 *
968 * @since 6.5.0
969 *
970 * @param string $block_content Rendered block content.
971 * @return string Filtered block content.
972 */
973 public static function restore_image_outer_container( $block_content ) {
974 if ( wp_theme_has_theme_json() ) {
975 return $block_content;
976 }
977
978 $tags = new WP_HTML_Tag_Processor( $block_content );
979 $wrapper_query = array(
980 'tag_name' => 'div',
981 'class_name' => 'wp-block-image',
982 );
983 if ( ! $tags->next_tag( $wrapper_query ) ) {
984 return $block_content;
985 }
986
987 $tags->set_bookmark( 'wrapper-div' );
988 $tags->next_tag();
989
990 $inner_classnames = explode( ' ', $tags->get_attribute( 'class' ) );
991 foreach ( $inner_classnames as $classname ) {
992 if ( 0 === strpos( $classname, 'wp-duotone' ) ) {
993 $tags->remove_class( $classname );
994 $tags->seek( 'wrapper-div' );
995 $tags->add_class( $classname );
996 break;
997 }
998 }
999
1000 return $tags->get_updated_html();
1001 }
1002
1003 /**
1004 * Appends the used block duotone filter declarations to the inline block supports CSS.
1005 *
1006 * @since 6.3.0
1007 */
1008 public static function output_block_styles() {
1009 if ( ! empty( self::$block_css_declarations ) ) {
1010 gutenberg_style_engine_get_stylesheet_from_css_rules(
1011 self::$block_css_declarations,
1012 array(
1013 'context' => 'block-supports',
1014 )
1015 );
1016 }
1017 }
1018
1019 /**
1020 * Appends the used global style duotone filter presets (CSS custom
1021 * properties) to the inline global styles CSS.
1022 *
1023 * @since 6.3.0
1024 */
1025 public static function output_global_styles() {
1026 if ( ! empty( self::$used_global_styles_presets ) ) {
1027 wp_add_inline_style( 'global-styles', self::get_global_styles_presets( self::$used_global_styles_presets ) );
1028 }
1029 }
1030
1031 /**
1032 * Outputs all necessary SVG for duotone filters, CSS for classic themes.
1033 *
1034 * @since 6.3.0
1035 */
1036 public static function output_footer_assets() {
1037 if ( ! empty( self::$used_svg_filter_data ) ) {
1038 echo self::get_svg_definitions( self::$used_svg_filter_data );
1039 }
1040
1041 // In block themes, the CSS is added in the head via wp_add_inline_style in the wp_enqueue_scripts action.
1042 if ( ! wp_is_block_theme() ) {
1043 $style_tag_id = 'core-block-supports-duotone';
1044 wp_register_style( $style_tag_id, false );
1045 if ( ! empty( self::$used_global_styles_presets ) ) {
1046 wp_add_inline_style( $style_tag_id, self::get_global_styles_presets( self::$used_global_styles_presets ) );
1047 }
1048 if ( ! empty( self::$block_css_declarations ) ) {
1049 wp_add_inline_style( $style_tag_id, gutenberg_style_engine_get_stylesheet_from_css_rules( self::$block_css_declarations ) );
1050 }
1051 wp_enqueue_style( $style_tag_id );
1052 }
1053 }
1054
1055 /**
1056 * Adds the duotone SVGs and CSS custom properties to the editor settings so
1057 * they can be pulled in by the EditorStyles component in JS and rendered in
1058 * the post editor.
1059 *
1060 * @since 6.3.0
1061 *
1062 * @param array $settings The block editor settings from the `block_editor_settings_all` filter.
1063 * @return array The editor settings with duotone SVGs and CSS custom properties.
1064 */
1065 public static function add_editor_settings( $settings ) {
1066 $global_styles_presets = self::get_all_global_styles_presets();
1067 if ( ! empty( $global_styles_presets ) ) {
1068 if ( ! isset( $settings['styles'] ) ) {
1069 $settings['styles'] = array();
1070 }
1071
1072 $settings['styles'][] = array(
1073 // For the editor we can add all of the presets by default.
1074 'assets' => self::get_svg_definitions( $global_styles_presets ),
1075 // The 'svgs' type is new in 6.3 and requires the corresponding JS changes in the EditorStyles component to work.
1076 '__unstableType' => 'svgs',
1077 // These styles not generated by global styles, so this must be false or they will be stripped out in gutenberg_get_block_editor_settings.
1078 'isGlobalStyles' => false,
1079 );
1080
1081 $settings['styles'][] = array(
1082 // For the editor we can add all of the presets by default.
1083 'css' => self::get_global_styles_presets( $global_styles_presets ),
1084 // This must be set and must be something other than 'theme' or they will be stripped out in the post editor <Editor> component.
1085 '__unstableType' => 'presets',
1086 // These styles are no longer generated by global styles, so this must be false or they will be stripped out in gutenberg_get_block_editor_settings.
1087 'isGlobalStyles' => false,
1088 );
1089 }
1090
1091 return $settings;
1092 }
1093
1094 /**
1095 * Migrate the old experimental duotone support flag to its stabilized location
1096 * under `supports.filter.duotone` and sets.
1097 *
1098 * @since 6.3.0
1099 *
1100 * @param array $settings Current block type settings.
1101 * @param array $metadata Block metadata as read in via block.json.
1102 *
1103 * @return array Filtered block type settings.
1104 */
1105 public static function migrate_experimental_duotone_support_flag( $settings, $metadata ) {
1106 $duotone_support = $metadata['supports']['color']['__experimentalDuotone'] ?? null;
1107
1108 if ( ! isset( $settings['supports']['filter']['duotone'] ) && null !== $duotone_support ) {
1109 _wp_array_set( $settings, array( 'supports', 'filter', 'duotone' ), (bool) $duotone_support );
1110 }
1111
1112 return $settings;
1113 }
1114
1115 /**
1116 * Returns the prefixed id for the duotone filter for use as a CSS id.
1117 *
1118 * Exported for the deprecated function gutenberg_get_duotone_filter_id().
1119 *
1120 * @since 6.3.0
1121 * @deprecated 6.3.0
1122 *
1123 * @param array $preset Duotone preset value as seen in theme.json.
1124 * @return string Duotone filter CSS id.
1125 */
1126 public static function get_filter_id_from_preset( $preset ) {
1127 _deprecated_function( __FUNCTION__, '6.3.0' );
1128
1129 $filter_id = '';
1130 if ( isset( $preset['slug'] ) ) {
1131 $filter_id = self::get_filter_id( $preset['slug'] );
1132 }
1133 return $filter_id;
1134 }
1135
1136 /**
1137 * Gets the SVG for the duotone filter definition from a preset.
1138 *
1139 * Exported for the deprecated function gutenberg_get_duotone_filter_property().
1140 *
1141 * @since 6.3.0
1142 * @deprecated 6.3.0
1143 *
1144 * @param array $preset The duotone preset.
1145 * @return string The SVG for the filter definition.
1146 */
1147 public static function get_filter_svg_from_preset( $preset ) {
1148 _deprecated_function( __FUNCTION__, '6.3.0' );
1149
1150 $filter_id = self::get_filter_id_from_preset( $preset );
1151 return self::get_filter_svg( $filter_id, $preset['colors'] );
1152 }
1153
1154 /**
1155 * Gets the CSS filter property value from a preset.
1156 *
1157 * Exported for the deprecated function gutenberg_get_duotone_filter_id().
1158 *
1159 * @since 6.3.0
1160 * @deprecated 6.3.0
1161 *
1162 * @param array $preset The duotone preset.
1163 * @return string The CSS filter property value.
1164 */
1165 public static function get_filter_css_property_value_from_preset( $preset ) {
1166 _deprecated_function( __FUNCTION__, '6.3.0' );
1167
1168 if ( isset( $preset['colors'] ) && is_string( $preset['colors'] ) ) {
1169 return $preset['colors'];
1170 }
1171
1172 $filter_id = self::get_filter_id_from_preset( $preset );
1173 return 'url(#' . $filter_id . ')';
1174 }
1175 }
1176