1<?php 2/** 3 * Duotone block support flag. 4 * 5 * Parts of this source were derived and modified from TinyColor, 6 * released under the MIT license. 7 * 8 * https://github.com/bgrins/TinyColor 9 * 10 * Copyright (c), Brian Grinstead, http://briangrinstead.com 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 WordPress 32 * @since 5.8.0 33 */ 34 35/** 36 * Takes input from [0, n] and returns it as [0, 1]. 37 * 38 * Direct port of TinyColor's function, lightly simplified to maintain 39 * consistency with TinyColor. 40 * 41 * @see https://github.com/bgrins/TinyColor 42 * 43 * @since 5.8.0 44 * @access private 45 * 46 * @param mixed $n Number of unknown type. 47 * @param int $max Upper value of the range to bound to. 48 * 49 * @return float Value in the range [0, 1]. 50 */ 51function wp_tinycolor_bound01( $n, $max ) { 52 if ( 'string' === gettype( $n ) && false !== strpos( $n, '.' ) && 1 === (float) $n ) { 53 $n = '100%'; 54 } 55 56 $n = min( $max, max( 0, (float) $n ) ); 57 58 // Automatically convert percentage into number. 59 if ( 'string' === gettype( $n ) && false !== strpos( $n, '%' ) ) { 60 $n = (int) ( $n * $max ) / 100; 61 } 62 63 // Handle floating point rounding errors. 64 if ( ( abs( $n - $max ) < 0.000001 ) ) { 65 return 1.0; 66 } 67 68 // Convert into [0, 1] range if it isn't already. 69 return ( $n % $max ) / (float) $max; 70} 71 72/** 73 * Round and convert values of an RGB object. 74 * 75 * Direct port of TinyColor's function, lightly simplified to maintain 76 * consistency with TinyColor. 77 * 78 * @see https://github.com/bgrins/TinyColor 79 * 80 * @since 5.8.0 81 * @access private 82 * 83 * @param array $rgb_color RGB object. 84 * 85 * @return array Rounded and converted RGB object. 86 */ 87function wp_tinycolor_rgb_to_rgb( $rgb_color ) { 88 return array( 89 'r' => wp_tinycolor_bound01( $rgb_color['r'], 255 ) * 255, 90 'g' => wp_tinycolor_bound01( $rgb_color['g'], 255 ) * 255, 91 'b' => wp_tinycolor_bound01( $rgb_color['b'], 255 ) * 255, 92 ); 93} 94 95/** 96 * Helper function for hsl to rgb conversion. 97 * 98 * Direct port of TinyColor's function, lightly simplified to maintain 99 * consistency with TinyColor. 100 * 101 * @see https://github.com/bgrins/TinyColor 102 * 103 * @since 5.8.0 104 * @access private 105 * 106 * @param float $p first component. 107 * @param float $q second component. 108 * @param float $t third component. 109 * 110 * @return float R, G, or B component. 111 */ 112function wp_tinycolor_hue_to_rgb( $p, $q, $t ) { 113 if ( $t < 0 ) { 114 $t += 1; 115 } 116 if ( $t > 1 ) { 117 $t -= 1; 118 } 119 if ( $t < 1 / 6 ) { 120 return $p + ( $q - $p ) * 6 * $t; 121 } 122 if ( $t < 1 / 2 ) { 123 return $q; 124 } 125 if ( $t < 2 / 3 ) { 126 return $p + ( $q - $p ) * ( 2 / 3 - $t ) * 6; 127 } 128 return $p; 129} 130 131/** 132 * Convert an HSL object to an RGB object with converted and rounded values. 133 * 134 * Direct port of TinyColor's function, lightly simplified to maintain 135 * consistency with TinyColor. 136 * 137 * @see https://github.com/bgrins/TinyColor 138 * 139 * @since 5.8.0 140 * @access private 141 * 142 * @param array $hsl_color HSL object. 143 * 144 * @return array Rounded and converted RGB object. 145 */ 146function wp_tinycolor_hsl_to_rgb( $hsl_color ) { 147 $h = wp_tinycolor_bound01( $hsl_color['h'], 360 ); 148 $s = wp_tinycolor_bound01( $hsl_color['s'], 100 ); 149 $l = wp_tinycolor_bound01( $hsl_color['l'], 100 ); 150 151 if ( 0 === $s ) { 152 // Achromatic. 153 $r = $l; 154 $g = $l; 155 $b = $l; 156 } else { 157 $q = $l < 0.5 ? $l * ( 1 + $s ) : $l + $s - $l * $s; 158 $p = 2 * $l - $q; 159 $r = wp_tinycolor_hue_to_rgb( $p, $q, $h + 1 / 3 ); 160 $g = wp_tinycolor_hue_to_rgb( $p, $q, $h ); 161 $b = wp_tinycolor_hue_to_rgb( $p, $q, $h - 1 / 3 ); 162 } 163 164 return array( 165 'r' => $r * 255, 166 'g' => $g * 255, 167 'b' => $b * 255, 168 ); 169} 170 171/** 172 * Parses hex, hsl, and rgb CSS strings using the same regex as TinyColor v1.4.2 173 * used in the JavaScript. Only colors output from react-color are implemented 174 * and the alpha value is ignored as it is not used in duotone. 175 * 176 * Direct port of TinyColor's function, lightly simplified to maintain 177 * consistency with TinyColor. 178 * 179 * @see https://github.com/bgrins/TinyColor 180 * @see https://github.com/casesandberg/react-color/ 181 * 182 * @since 5.8.0 183 * @access private 184 * 185 * @param string $color_str CSS color string. 186 * 187 * @return array RGB object. 188 */ 189function wp_tinycolor_string_to_rgb( $color_str ) { 190 $color_str = strtolower( trim( $color_str ) ); 191 192 $css_integer = '[-\\+]?\\d+%?'; 193 $css_number = '[-\\+]?\\d*\\.\\d+%?'; 194 195 $css_unit = '(?:' . $css_number . ')|(?:' . $css_integer . ')'; 196 197 $permissive_match3 = '[\\s|\\(]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')\\s*\\)?'; 198 $permissive_match4 = '[\\s|\\(]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')\\s*\\)?'; 199 200 $rgb_regexp = '/^rgb' . $permissive_match3 . '$/'; 201 if ( preg_match( $rgb_regexp, $color_str, $match ) ) { 202 return wp_tinycolor_rgb_to_rgb( 203 array( 204 'r' => $match[1], 205 'g' => $match[2], 206 'b' => $match[3], 207 ) 208 ); 209 } 210 211 $rgba_regexp = '/^rgba' . $permissive_match4 . '$/'; 212 if ( preg_match( $rgba_regexp, $color_str, $match ) ) { 213 return wp_tinycolor_rgb_to_rgb( 214 array( 215 'r' => $match[1], 216 'g' => $match[2], 217 'b' => $match[3], 218 ) 219 ); 220 } 221 222 $hsl_regexp = '/^hsl' . $permissive_match3 . '$/'; 223 if ( preg_match( $hsl_regexp, $color_str, $match ) ) { 224 return wp_tinycolor_hsl_to_rgb( 225 array( 226 'h' => $match[1], 227 's' => $match[2], 228 'l' => $match[3], 229 ) 230 ); 231 } 232 233 $hsla_regexp = '/^hsla' . $permissive_match4 . '$/'; 234 if ( preg_match( $hsla_regexp, $color_str, $match ) ) { 235 return wp_tinycolor_hsl_to_rgb( 236 array( 237 'h' => $match[1], 238 's' => $match[2], 239 'l' => $match[3], 240 ) 241 ); 242 } 243 244 $hex8_regexp = '/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/'; 245 if ( preg_match( $hex8_regexp, $color_str, $match ) ) { 246 return wp_tinycolor_rgb_to_rgb( 247 array( 248 'r' => base_convert( $match[1], 16, 10 ), 249 'g' => base_convert( $match[2], 16, 10 ), 250 'b' => base_convert( $match[3], 16, 10 ), 251 ) 252 ); 253 } 254 255 $hex6_regexp = '/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/'; 256 if ( preg_match( $hex6_regexp, $color_str, $match ) ) { 257 return wp_tinycolor_rgb_to_rgb( 258 array( 259 'r' => base_convert( $match[1], 16, 10 ), 260 'g' => base_convert( $match[2], 16, 10 ), 261 'b' => base_convert( $match[3], 16, 10 ), 262 ) 263 ); 264 } 265 266 $hex4_regexp = '/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/'; 267 if ( preg_match( $hex4_regexp, $color_str, $match ) ) { 268 return wp_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 277 $hex3_regexp = '/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/'; 278 if ( preg_match( $hex3_regexp, $color_str, $match ) ) { 279 return wp_tinycolor_rgb_to_rgb( 280 array( 281 'r' => base_convert( $match[1] . $match[1], 16, 10 ), 282 'g' => base_convert( $match[2] . $match[2], 16, 10 ), 283 'b' => base_convert( $match[3] . $match[3], 16, 10 ), 284 ) 285 ); 286 } 287} 288 289 290/** 291 * Registers the style and colors block attributes for block types that support it. 292 * 293 * @since 5.8.0 294 * @access private 295 * 296 * @param WP_Block_Type $block_type Block Type. 297 */ 298function wp_register_duotone_support( $block_type ) { 299 $has_duotone_support = false; 300 if ( property_exists( $block_type, 'supports' ) ) { 301 $has_duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false ); 302 } 303 304 if ( $has_duotone_support ) { 305 if ( ! $block_type->attributes ) { 306 $block_type->attributes = array(); 307 } 308 309 if ( ! array_key_exists( 'style', $block_type->attributes ) ) { 310 $block_type->attributes['style'] = array( 311 'type' => 'object', 312 ); 313 } 314 } 315} 316 317/** 318 * Render out the duotone stylesheet and SVG. 319 * 320 * @since 5.8.0 321 * @access private 322 * 323 * @param string $block_content Rendered block content. 324 * @param array $block Block object. 325 * 326 * @return string Filtered block content. 327 */ 328function wp_render_duotone_support( $block_content, $block ) { 329 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); 330 331 $duotone_support = false; 332 if ( $block_type && property_exists( $block_type, 'supports' ) ) { 333 $duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false ); 334 } 335 336 $has_duotone_attribute = isset( $block['attrs']['style']['color']['duotone'] ); 337 338 if ( 339 ! $duotone_support || 340 ! $has_duotone_attribute 341 ) { 342 return $block_content; 343 } 344 345 $duotone_colors = $block['attrs']['style']['color']['duotone']; 346 347 $duotone_values = array( 348 'r' => array(), 349 'g' => array(), 350 'b' => array(), 351 ); 352 foreach ( $duotone_colors as $color_str ) { 353 $color = wp_tinycolor_string_to_rgb( $color_str ); 354 355 $duotone_values['r'][] = $color['r'] / 255; 356 $duotone_values['g'][] = $color['g'] / 255; 357 $duotone_values['b'][] = $color['b'] / 255; 358 } 359 360 $duotone_id = 'wp-duotone-filter-' . uniqid(); 361 362 $selectors = explode( ',', $duotone_support ); 363 $selectors_scoped = array_map( 364 function ( $selector ) use ( $duotone_id ) { 365 return '.' . $duotone_id . ' ' . trim( $selector ); 366 }, 367 $selectors 368 ); 369 $selectors_group = implode( ', ', $selectors_scoped ); 370 371 ob_start(); 372 373 ?> 374 375 <style> 376 <?php echo $selectors_group; ?> { 377 filter: url( <?php echo esc_url( '#' . $duotone_id ); ?> ); 378 } 379 </style> 380 381 <svg 382 xmlns:xlink="http://www.w3.org/1999/xlink" 383 viewBox="0 0 0 0" 384 width="0" 385 height="0" 386 focusable="false" 387 role="none" 388 style="visibility: hidden; position: absolute; left: -9999px; overflow: hidden;" 389 > 390 <defs> 391 <filter id="<?php echo esc_attr( $duotone_id ); ?>"> 392 <feColorMatrix 393 type="matrix" 394 <?php // phpcs:disable Generic.WhiteSpace.DisallowSpaceIndent ?> 395 values=".299 .587 .114 0 0 396 .299 .587 .114 0 0 397 .299 .587 .114 0 0 398 0 0 0 1 0" 399 <?php // phpcs:enable Generic.WhiteSpace.DisallowSpaceIndent ?> 400 /> 401 <feComponentTransfer color-interpolation-filters="sRGB" > 402 <feFuncR type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['r'] ) ); ?>" /> 403 <feFuncG type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['g'] ) ); ?>" /> 404 <feFuncB type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['b'] ) ); ?>" /> 405 </feComponentTransfer> 406 </filter> 407 </defs> 408 </svg> 409 410 <?php 411 412 $duotone = ob_get_clean(); 413 414 // Like the layout hook, this assumes the hook only applies to blocks with a single wrapper. 415 $content = preg_replace( 416 '/' . preg_quote( 'class="', '/' ) . '/', 417 'class="' . $duotone_id . ' ', 418 $block_content, 419 1 420 ); 421 422 return $content . $duotone; 423} 424 425// Register the block support. 426WP_Block_Supports::get_instance()->register( 427 'duotone', 428 array( 429 'register_attribute' => 'wp_register_duotone_support', 430 ) 431); 432add_filter( 'render_block', 'wp_render_duotone_support', 10, 2 ); 433