PluginProbe
Gutenberg / 12.1.0
Gutenberg v12.1.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 / duotone.php

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

474 lines 12.5 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 ) && false !== strpos( $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 ) && false !== strpos( $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 += 1;
87 }
88 if ( $t > 1 ) {
89 $t -= 1;
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 /**
295 * Registers the style and colors block attributes for block types that support it.
296 *
297 * @param WP_Block_Type $block_type Block Type.
298 */
299 function gutenberg_register_duotone_support( $block_type ) {
300 $has_duotone_support = false;
301 if ( property_exists( $block_type, 'supports' ) ) {
302 $has_duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false );
303 }
304
305 if ( $has_duotone_support ) {
306 if ( ! $block_type->attributes ) {
307 $block_type->attributes = array();
308 }
309
310 if ( ! array_key_exists( 'style', $block_type->attributes ) ) {
311 $block_type->attributes['style'] = array(
312 'type' => 'object',
313 );
314 }
315 }
316 }
317
318 /**
319 * Renders the duotone filter SVG and returns the CSS filter property to
320 * reference the rendered SVG.
321 *
322 * @param array $preset Duotone preset value as seen in theme.json.
323 *
324 * @return string Duotone CSS filter property.
325 */
326 function gutenberg_render_duotone_filter_preset( $preset ) {
327 $duotone_id = $preset['slug'];
328 $duotone_colors = $preset['colors'];
329 $filter_id = 'wp-duotone-' . $duotone_id;
330 $duotone_values = array(
331 'r' => array(),
332 'g' => array(),
333 'b' => array(),
334 'a' => array(),
335 );
336 foreach ( $duotone_colors as $color_str ) {
337 $color = gutenberg_tinycolor_string_to_rgb( $color_str );
338
339 $duotone_values['r'][] = $color['r'] / 255;
340 $duotone_values['g'][] = $color['g'] / 255;
341 $duotone_values['b'][] = $color['b'] / 255;
342 $duotone_values['a'][] = $color['a'];
343 }
344
345 ob_start();
346
347 ?>
348
349 <svg
350 xmlns="http://www.w3.org/2000/svg"
351 viewBox="0 0 0 0"
352 width="0"
353 height="0"
354 focusable="false"
355 role="none"
356 style="visibility: hidden; position: absolute; left: -9999px; overflow: hidden;"
357 >
358 <defs>
359 <filter id="<?php echo esc_attr( $filter_id ); ?>">
360 <feColorMatrix
361 color-interpolation-filters="sRGB"
362 type="matrix"
363 values="
364 .299 .587 .114 0 0
365 .299 .587 .114 0 0
366 .299 .587 .114 0 0
367 .299 .587 .114 0 0
368 "
369 />
370 <feComponentTransfer color-interpolation-filters="sRGB" >
371 <feFuncR type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['r'] ) ); ?>" />
372 <feFuncG type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['g'] ) ); ?>" />
373 <feFuncB type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['b'] ) ); ?>" />
374 <feFuncA type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['a'] ) ); ?>" />
375 </feComponentTransfer>
376 <feComposite in2="SourceGraphic" operator="in" />
377 </filter>
378 </defs>
379 </svg>
380
381 <?php
382
383 $svg = ob_get_clean();
384
385 if ( ! defined( 'SCRIPT_DEBUG' ) || ! SCRIPT_DEBUG ) {
386 // Clean up the whitespace.
387 $svg = preg_replace( "/[\r\n\t ]+/", ' ', $svg );
388 $svg = preg_replace( '/> </', '><', $svg );
389 $svg = trim( $svg );
390 }
391
392 add_action(
393 // SVG filters won't render at all in the head of a document and
394 // Safari incorrectly renders SVG filters in the footer, so the
395 // beginning of the body seems to be the safest place to render.
396 'wp_body_open',
397 function () use ( $svg ) {
398 echo $svg;
399 }
400 );
401
402 return "url('#" . $filter_id . "')";
403 }
404
405 /**
406 * Render out the duotone stylesheet and SVG.
407 *
408 * @param string $block_content Rendered block content.
409 * @param array $block Block object.
410 * @return string Filtered block content.
411 */
412 function gutenberg_render_duotone_support( $block_content, $block ) {
413 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
414
415 $duotone_support = false;
416 if ( $block_type && property_exists( $block_type, 'supports' ) ) {
417 $duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false );
418 }
419
420 $has_duotone_attribute = isset( $block['attrs']['style']['color']['duotone'] );
421
422 if (
423 ! $duotone_support ||
424 ! $has_duotone_attribute
425 ) {
426 return $block_content;
427 }
428
429 $filter_preset = array(
430 'slug' => uniqid(),
431 'colors' => $block['attrs']['style']['color']['duotone'],
432 );
433 $filter_property = gutenberg_render_duotone_filter_preset( $filter_preset );
434 $filter_id = 'wp-duotone-' . $filter_preset['slug'];
435
436 $scope = '.' . $filter_id;
437 $selectors = explode( ',', $duotone_support );
438 $scoped = array();
439 foreach ( $selectors as $sel ) {
440 $scoped[] = $scope . ' ' . trim( $sel );
441 }
442 $selector = implode( ', ', $scoped );
443
444 // !important is needed because these styles render before global styles,
445 // and they should be overriding the duotone filters set by global styles.
446 $filter_style = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG
447 ? $selector . " {\n\tfilter: " . $filter_property . " !important;\n}\n"
448 : $selector . '{filter:' . $filter_property . ' !important;}';
449
450 wp_register_style( $filter_id, false, array(), true, true );
451 wp_add_inline_style( $filter_id, $filter_style );
452 wp_enqueue_style( $filter_id );
453
454 // Like the layout hook, this assumes the hook only applies to blocks with a single wrapper.
455 return preg_replace(
456 '/' . preg_quote( 'class="', '/' ) . '/',
457 'class="' . $filter_id . ' ',
458 $block_content,
459 1
460 );
461 }
462
463 // Register the block support.
464 WP_Block_Supports::get_instance()->register(
465 'duotone',
466 array(
467 'register_attribute' => 'gutenberg_register_duotone_support',
468 )
469 );
470
471 // Remove WordPress core filter to avoid rendering duplicate support elements.
472 remove_filter( 'render_block', 'wp_render_duotone_support', 10, 2 );
473 add_filter( 'render_block', 'gutenberg_render_duotone_support', 10, 2 );
474