PluginProbe
Gutenberg / 14.7.0
Gutenberg v14.7.0
24.0.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 All 403 releases
gutenberg / lib / block-supports / duotone.php

duotone.php in Gutenberg 14.7.0, at lib/block-supports/duotone.php

522 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Duotone block support flag.
4 *
5 * @package gutenberg
6 */
7
8 /**
9 * Direct port of tinycolor's bound01 function, lightly simplified to maintain
10 * consistency with tinycolor.
11 *
12 * @see https://github.com/bgrins/TinyColor
13 *
14 * @param mixed $n Number of unknown type.
15 * @param int $max Upper value of the range to bound to.
16 * @return float Value in the range [0,1].
17 */
18 function gutenberg_tinycolor_bound01( $n, $max ) {
19 if ( 'string' === gettype( $n ) && str_contains( $n, '.' ) && 1 === (float) $n ) {
20 $n = '100%';
21 }
22
23 $n = min( $max, max( 0, (float) $n ) );
24
25 // Automatically convert percentage into number.
26 if ( 'string' === gettype( $n ) && str_contains( $n, '%' ) ) {
27 $n = (int) ( $n * $max ) / 100;
28 }
29
30 // Handle floating point rounding errors.
31 if ( ( abs( $n - $max ) < 0.000001 ) ) {
32 return 1.0;
33 }
34
35 // Convert into [0, 1] range if it isn't already.
36 return ( $n % $max ) / (float) $max;
37 }
38
39 /**
40 * Direct port of tinycolor's boundAlpha function to maintain consistency with
41 * how tinycolor works.
42 *
43 * @see https://github.com/bgrins/TinyColor
44 *
45 * @param mixed $n Number of unknown type.
46 * @return float Value in the range [0,1].
47 */
48 function gutenberg_tinycolor_bound_alpha( $n ) {
49 if ( is_numeric( $n ) ) {
50 $n = (float) $n;
51 if ( $n >= 0 && $n <= 1 ) {
52 return $n;
53 }
54 }
55 return 1;
56 }
57
58 /**
59 * Round and convert values of an RGB object.
60 *
61 * @see https://github.com/bgrins/TinyColor
62 *
63 * @param array $rgb_color RGB object.
64 * @return array Rounded and converted RGB object.
65 */
66 function gutenberg_tinycolor_rgb_to_rgb( $rgb_color ) {
67 return array(
68 'r' => gutenberg_tinycolor_bound01( $rgb_color['r'], 255 ) * 255,
69 'g' => gutenberg_tinycolor_bound01( $rgb_color['g'], 255 ) * 255,
70 'b' => gutenberg_tinycolor_bound01( $rgb_color['b'], 255 ) * 255,
71 );
72 }
73
74 /**
75 * Helper function for hsl to rgb conversion.
76 *
77 * @see https://github.com/bgrins/TinyColor
78 *
79 * @param float $p first component.
80 * @param float $q second component.
81 * @param float $t third component.
82 * @return float R, G, or B component.
83 */
84 function gutenberg_tinycolor_hue_to_rgb( $p, $q, $t ) {
85 if ( $t < 0 ) {
86 ++$t;
87 }
88 if ( $t > 1 ) {
89 --$t;
90 }
91 if ( $t < 1 / 6 ) {
92 return $p + ( $q - $p ) * 6 * $t;
93 }
94 if ( $t < 1 / 2 ) {
95 return $q;
96 }
97 if ( $t < 2 / 3 ) {
98 return $p + ( $q - $p ) * ( 2 / 3 - $t ) * 6;
99 }
100 return $p;
101 }
102
103 /**
104 * Convert an HSL object to an RGB object with converted and rounded values.
105 *
106 * @see https://github.com/bgrins/TinyColor
107 *
108 * @param array $hsl_color HSL object.
109 * @return array Rounded and converted RGB object.
110 */
111 function gutenberg_tinycolor_hsl_to_rgb( $hsl_color ) {
112 $h = gutenberg_tinycolor_bound01( $hsl_color['h'], 360 );
113 $s = gutenberg_tinycolor_bound01( $hsl_color['s'], 100 );
114 $l = gutenberg_tinycolor_bound01( $hsl_color['l'], 100 );
115
116 if ( 0 === $s ) {
117 // Achromatic.
118 $r = $l;
119 $g = $l;
120 $b = $l;
121 } else {
122 $q = $l < 0.5 ? $l * ( 1 + $s ) : $l + $s - $l * $s;
123 $p = 2 * $l - $q;
124 $r = gutenberg_tinycolor_hue_to_rgb( $p, $q, $h + 1 / 3 );
125 $g = gutenberg_tinycolor_hue_to_rgb( $p, $q, $h );
126 $b = gutenberg_tinycolor_hue_to_rgb( $p, $q, $h - 1 / 3 );
127 }
128
129 return array(
130 'r' => $r * 255,
131 'g' => $g * 255,
132 'b' => $b * 255,
133 );
134 }
135
136 /**
137 * Parses hex, hsl, and rgb CSS strings using the same regex as tinycolor v1.4.2
138 * used in the JavaScript. Only colors output from react-color are implemented.
139 *
140 * @see https://github.com/bgrins/TinyColor
141 * @see https://github.com/casesandberg/react-color/
142 *
143 * @param string $color_str CSS color string.
144 * @return array RGB object.
145 */
146 function gutenberg_tinycolor_string_to_rgb( $color_str ) {
147 $color_str = strtolower( trim( $color_str ) );
148
149 $css_integer = '[-\\+]?\\d+%?';
150 $css_number = '[-\\+]?\\d*\\.\\d+%?';
151
152 $css_unit = '(?:' . $css_number . ')|(?:' . $css_integer . ')';
153
154 $permissive_match3 = '[\\s|\\(]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')\\s*\\)?';
155 $permissive_match4 = '[\\s|\\(]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')\\s*\\)?';
156
157 $rgb_regexp = '/^rgb' . $permissive_match3 . '$/';
158 if ( preg_match( $rgb_regexp, $color_str, $match ) ) {
159 $rgb = gutenberg_tinycolor_rgb_to_rgb(
160 array(
161 'r' => $match[1],
162 'g' => $match[2],
163 'b' => $match[3],
164 )
165 );
166
167 $rgb['a'] = 1;
168
169 return $rgb;
170 }
171
172 $rgba_regexp = '/^rgba' . $permissive_match4 . '$/';
173 if ( preg_match( $rgba_regexp, $color_str, $match ) ) {
174 $rgb = gutenberg_tinycolor_rgb_to_rgb(
175 array(
176 'r' => $match[1],
177 'g' => $match[2],
178 'b' => $match[3],
179 )
180 );
181
182 $rgb['a'] = gutenberg_tinycolor_bound_alpha( $match[4] );
183
184 return $rgb;
185 }
186
187 $hsl_regexp = '/^hsl' . $permissive_match3 . '$/';
188 if ( preg_match( $hsl_regexp, $color_str, $match ) ) {
189 $rgb = gutenberg_tinycolor_hsl_to_rgb(
190 array(
191 'h' => $match[1],
192 's' => $match[2],
193 'l' => $match[3],
194 )
195 );
196
197 $rgb['a'] = 1;
198
199 return $rgb;
200 }
201
202 $hsla_regexp = '/^hsla' . $permissive_match4 . '$/';
203 if ( preg_match( $hsla_regexp, $color_str, $match ) ) {
204 $rgb = gutenberg_tinycolor_hsl_to_rgb(
205 array(
206 'h' => $match[1],
207 's' => $match[2],
208 'l' => $match[3],
209 )
210 );
211
212 $rgb['a'] = gutenberg_tinycolor_bound_alpha( $match[4] );
213
214 return $rgb;
215 }
216
217 $hex8_regexp = '/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/';
218 if ( preg_match( $hex8_regexp, $color_str, $match ) ) {
219 $rgb = gutenberg_tinycolor_rgb_to_rgb(
220 array(
221 'r' => base_convert( $match[1], 16, 10 ),
222 'g' => base_convert( $match[2], 16, 10 ),
223 'b' => base_convert( $match[3], 16, 10 ),
224 )
225 );
226
227 $rgb['a'] = gutenberg_tinycolor_bound_alpha(
228 base_convert( $match[4], 16, 10 ) / 255
229 );
230
231 return $rgb;
232 }
233
234 $hex6_regexp = '/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/';
235 if ( preg_match( $hex6_regexp, $color_str, $match ) ) {
236 $rgb = gutenberg_tinycolor_rgb_to_rgb(
237 array(
238 'r' => base_convert( $match[1], 16, 10 ),
239 'g' => base_convert( $match[2], 16, 10 ),
240 'b' => base_convert( $match[3], 16, 10 ),
241 )
242 );
243
244 $rgb['a'] = 1;
245
246 return $rgb;
247 }
248
249 $hex4_regexp = '/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/';
250 if ( preg_match( $hex4_regexp, $color_str, $match ) ) {
251 $rgb = gutenberg_tinycolor_rgb_to_rgb(
252 array(
253 'r' => base_convert( $match[1] . $match[1], 16, 10 ),
254 'g' => base_convert( $match[2] . $match[2], 16, 10 ),
255 'b' => base_convert( $match[3] . $match[3], 16, 10 ),
256 )
257 );
258
259 $rgb['a'] = gutenberg_tinycolor_bound_alpha(
260 base_convert( $match[4] . $match[4], 16, 10 ) / 255
261 );
262
263 return $rgb;
264 }
265
266 $hex3_regexp = '/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/';
267 if ( preg_match( $hex3_regexp, $color_str, $match ) ) {
268 $rgb = gutenberg_tinycolor_rgb_to_rgb(
269 array(
270 'r' => base_convert( $match[1] . $match[1], 16, 10 ),
271 'g' => base_convert( $match[2] . $match[2], 16, 10 ),
272 'b' => base_convert( $match[3] . $match[3], 16, 10 ),
273 )
274 );
275
276 $rgb['a'] = 1;
277
278 return $rgb;
279 }
280
281 // The JS color picker considers the string "transparent" to be a hex value,
282 // so we need to handle it here as a special case.
283 if ( 'transparent' === $color_str ) {
284 return array(
285 'r' => 0,
286 'g' => 0,
287 'b' => 0,
288 'a' => 0,
289 );
290 }
291 }
292
293 /**
294 * Returns the prefixed id for the duotone filter for use as a CSS id.
295 *
296 * @param array $preset Duotone preset value as seen in theme.json.
297 * @return string Duotone filter CSS id.
298 */
299 function gutenberg_get_duotone_filter_id( $preset ) {
300 if ( ! isset( $preset['slug'] ) ) {
301 return '';
302 }
303
304 return 'wp-duotone-' . $preset['slug'];
305 }
306
307 /**
308 * Returns the CSS filter property url to reference the rendered SVG.
309 *
310 * @param array $preset Duotone preset value as seen in theme.json.
311 * @return string Duotone CSS filter property url value.
312 */
313 function gutenberg_get_duotone_filter_property( $preset ) {
314 if ( isset( $preset['colors'] ) && 'unset' === $preset['colors'] ) {
315 return 'none';
316 }
317 $filter_id = gutenberg_get_duotone_filter_id( $preset );
318 return "url('#" . $filter_id . "')";
319 }
320
321 /**
322 * Returns the duotone filter SVG string for the preset.
323 *
324 * @param array $preset Duotone preset value as seen in theme.json.
325 * @return string Duotone SVG filter.
326 */
327 function gutenberg_get_duotone_filter_svg( $preset ) {
328 $filter_id = gutenberg_get_duotone_filter_id( $preset );
329
330 $duotone_values = array(
331 'r' => array(),
332 'g' => array(),
333 'b' => array(),
334 'a' => array(),
335 );
336
337 if ( ! isset( $preset['colors'] ) || ! is_array( $preset['colors'] ) ) {
338 $preset['colors'] = array();
339 }
340
341 foreach ( $preset['colors'] as $color_str ) {
342 $color = gutenberg_tinycolor_string_to_rgb( $color_str );
343
344 $duotone_values['r'][] = $color['r'] / 255;
345 $duotone_values['g'][] = $color['g'] / 255;
346 $duotone_values['b'][] = $color['b'] / 255;
347 $duotone_values['a'][] = $color['a'];
348 }
349
350 ob_start();
351
352 ?>
353
354 <svg
355 xmlns="http://www.w3.org/2000/svg"
356 viewBox="0 0 0 0"
357 width="0"
358 height="0"
359 focusable="false"
360 role="none"
361 style="visibility: hidden; position: absolute; left: -9999px; overflow: hidden;"
362 >
363 <defs>
364 <filter id="<?php echo esc_attr( $filter_id ); ?>">
365 <feColorMatrix
366 color-interpolation-filters="sRGB"
367 type="matrix"
368 values="
369 .299 .587 .114 0 0
370 .299 .587 .114 0 0
371 .299 .587 .114 0 0
372 .299 .587 .114 0 0
373 "
374 />
375 <feComponentTransfer color-interpolation-filters="sRGB" >
376 <feFuncR type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['r'] ) ); ?>" />
377 <feFuncG type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['g'] ) ); ?>" />
378 <feFuncB type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['b'] ) ); ?>" />
379 <feFuncA type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['a'] ) ); ?>" />
380 </feComponentTransfer>
381 <feComposite in2="SourceGraphic" operator="in" />
382 </filter>
383 </defs>
384 </svg>
385
386 <?php
387
388 $svg = ob_get_clean();
389
390 if ( ! SCRIPT_DEBUG ) {
391 // Clean up the whitespace.
392 $svg = preg_replace( "/[\r\n\t ]+/", ' ', $svg );
393 $svg = str_replace( '> <', '><', $svg );
394 $svg = trim( $svg );
395 }
396
397 return $svg;
398 }
399
400 /**
401 * Registers the style and colors block attributes for block types that support it.
402 *
403 * @param WP_Block_Type $block_type Block Type.
404 */
405 function gutenberg_register_duotone_support( $block_type ) {
406 $has_duotone_support = false;
407 if ( property_exists( $block_type, 'supports' ) ) {
408 $has_duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false );
409 }
410
411 if ( $has_duotone_support ) {
412 if ( ! $block_type->attributes ) {
413 $block_type->attributes = array();
414 }
415
416 if ( ! array_key_exists( 'style', $block_type->attributes ) ) {
417 $block_type->attributes['style'] = array(
418 'type' => 'object',
419 );
420 }
421 }
422 }
423
424 /**
425 * Render out the duotone stylesheet and SVG.
426 *
427 * @param string $block_content Rendered block content.
428 * @param array $block Block object.
429 * @return string Filtered block content.
430 */
431 function gutenberg_render_duotone_support( $block_content, $block ) {
432 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
433
434 $duotone_support = false;
435 if ( $block_type && property_exists( $block_type, 'supports' ) ) {
436 $duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false );
437 }
438
439 $has_duotone_attribute = isset( $block['attrs']['style']['color']['duotone'] );
440
441 if (
442 ! $duotone_support ||
443 ! $has_duotone_attribute
444 ) {
445 return $block_content;
446 }
447
448 $colors = $block['attrs']['style']['color']['duotone'];
449 $filter_key = is_array( $colors ) ? implode( '-', $colors ) : $colors;
450 $filter_preset = array(
451 'slug' => wp_unique_id( sanitize_key( $filter_key . '-' ) ),
452 'colors' => $colors,
453 );
454 $filter_property = gutenberg_get_duotone_filter_property( $filter_preset );
455 $filter_id = gutenberg_get_duotone_filter_id( $filter_preset );
456
457 $scope = '.' . $filter_id;
458 $selectors = explode( ',', $duotone_support );
459 $scoped = array();
460 foreach ( $selectors as $sel ) {
461 $scoped[] = $scope . ' ' . trim( $sel );
462 }
463 $selector = implode( ', ', $scoped );
464
465 // !important is needed because these styles render before global styles,
466 // and they should be overriding the duotone filters set by global styles.
467 $filter_style = SCRIPT_DEBUG
468 ? $selector . " {\n\tfilter: " . $filter_property . " !important;\n}\n"
469 : $selector . '{filter:' . $filter_property . ' !important;}';
470
471 wp_register_style( $filter_id, false, array(), true, true );
472 wp_add_inline_style( $filter_id, $filter_style );
473 wp_enqueue_style( $filter_id );
474
475 if ( 'unset' !== $colors ) {
476 $filter_svg = gutenberg_get_duotone_filter_svg( $filter_preset );
477 add_action(
478 'wp_footer',
479 static function () use ( $filter_svg, $selector ) {
480 echo $filter_svg;
481
482 /*
483 * Safari renders elements incorrectly on first paint when the
484 * SVG filter comes after the content that it is filtering, so
485 * we force a repaint with a WebKit hack which solves the issue.
486 */
487 global $is_safari;
488 if ( $is_safari ) {
489 /*
490 * Simply accessing el.offsetHeight flushes layout and style
491 * changes in WebKit without having to wait for setTimeout.
492 */
493 printf(
494 '<script>( function() { var el = document.querySelector( %s ); var display = el.style.display; el.style.display = "none"; el.offsetHeight; el.style.display = display; } )();</script>',
495 wp_json_encode( $selector )
496 );
497 }
498 }
499 );
500 }
501
502 // Like the layout hook, this assumes the hook only applies to blocks with a single wrapper.
503 return preg_replace(
504 '/' . preg_quote( 'class="', '/' ) . '/',
505 'class="' . $filter_id . ' ',
506 $block_content,
507 1
508 );
509 }
510
511 // Register the block support.
512 WP_Block_Supports::get_instance()->register(
513 'duotone',
514 array(
515 'register_attribute' => 'gutenberg_register_duotone_support',
516 )
517 );
518
519 // Remove WordPress core filter to avoid rendering duplicate support elements.
520 remove_filter( 'render_block', 'wp_render_duotone_support', 10, 2 );
521 add_filter( 'render_block', 'gutenberg_render_duotone_support', 10, 2 );
522