wp/wp-includes/block-supports/duotone.php
changeset 18 be944660c56a
child 19 3d72ae0968f4
equal deleted inserted replaced
17:34716fd837a4 18:be944660c56a
       
     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  */
       
    51 function 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  */
       
    87 function 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  */
       
   112 function 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  */
       
   146 function 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  */
       
   189 function 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  */
       
   298 function 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  */
       
   328 function 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.
       
   426 WP_Block_Supports::get_instance()->register(
       
   427 	'duotone',
       
   428 	array(
       
   429 		'register_attribute' => 'wp_register_duotone_support',
       
   430 	)
       
   431 );
       
   432 add_filter( 'render_block', 'wp_render_duotone_support', 10, 2 );