wp/wp-includes/block-supports/duotone.php
changeset 19 3d72ae0968f4
parent 18 be944660c56a
child 21 48c4eec2b7e6
--- a/wp/wp-includes/block-supports/duotone.php	Wed Sep 21 18:19:35 2022 +0200
+++ b/wp/wp-includes/block-supports/duotone.php	Tue Sep 27 16:37:53 2022 +0200
@@ -45,7 +45,6 @@
  *
  * @param mixed $n   Number of unknown type.
  * @param int   $max Upper value of the range to bound to.
- *
  * @return float Value in the range [0, 1].
  */
 function wp_tinycolor_bound01( $n, $max ) {
@@ -70,7 +69,29 @@
 }
 
 /**
- * Round and convert values of an RGB object.
+ * Direct port of tinycolor's boundAlpha function to maintain consistency with
+ * how tinycolor works.
+ *
+ * @see https://github.com/bgrins/TinyColor
+ *
+ * @since 5.9.0
+ * @access private
+ *
+ * @param mixed $n Number of unknown type.
+ * @return float Value in the range [0,1].
+ */
+function _wp_tinycolor_bound_alpha( $n ) {
+	if ( is_numeric( $n ) ) {
+		$n = (float) $n;
+		if ( $n >= 0 && $n <= 1 ) {
+			return $n;
+		}
+	}
+	return 1;
+}
+
+/**
+ * Rounds and converts values of an RGB object.
  *
  * Direct port of TinyColor's function, lightly simplified to maintain
  * consistency with TinyColor.
@@ -81,7 +102,6 @@
  * @access private
  *
  * @param array $rgb_color RGB object.
- *
  * @return array Rounded and converted RGB object.
  */
 function wp_tinycolor_rgb_to_rgb( $rgb_color ) {
@@ -106,7 +126,6 @@
  * @param float $p first component.
  * @param float $q second component.
  * @param float $t third component.
- *
  * @return float R, G, or B component.
  */
 function wp_tinycolor_hue_to_rgb( $p, $q, $t ) {
@@ -129,7 +148,7 @@
 }
 
 /**
- * Convert an HSL object to an RGB object with converted and rounded values.
+ * Converts an HSL object to an RGB object with converted and rounded values.
  *
  * Direct port of TinyColor's function, lightly simplified to maintain
  * consistency with TinyColor.
@@ -140,7 +159,6 @@
  * @access private
  *
  * @param array $hsl_color HSL object.
- *
  * @return array Rounded and converted RGB object.
  */
 function wp_tinycolor_hsl_to_rgb( $hsl_color ) {
@@ -170,8 +188,7 @@
 
 /**
  * Parses hex, hsl, and rgb CSS strings using the same regex as TinyColor v1.4.2
- * used in the JavaScript. Only colors output from react-color are implemented
- * and the alpha value is ignored as it is not used in duotone.
+ * used in the JavaScript. Only colors output from react-color are implemented.
  *
  * Direct port of TinyColor's function, lightly simplified to maintain
  * consistency with TinyColor.
@@ -180,10 +197,10 @@
  * @see https://github.com/casesandberg/react-color/
  *
  * @since 5.8.0
+ * @since 5.9.0 Added alpha processing.
  * @access private
  *
  * @param string $color_str CSS color string.
- *
  * @return array RGB object.
  */
 function wp_tinycolor_string_to_rgb( $color_str ) {
@@ -199,93 +216,254 @@
 
 	$rgb_regexp = '/^rgb' . $permissive_match3 . '$/';
 	if ( preg_match( $rgb_regexp, $color_str, $match ) ) {
-		return wp_tinycolor_rgb_to_rgb(
+		$rgb = wp_tinycolor_rgb_to_rgb(
+			array(
+				'r' => $match[1],
+				'g' => $match[2],
+				'b' => $match[3],
+			)
+		);
+
+		$rgb['a'] = 1;
+
+		return $rgb;
+	}
+
+	$rgba_regexp = '/^rgba' . $permissive_match4 . '$/';
+	if ( preg_match( $rgba_regexp, $color_str, $match ) ) {
+		$rgb = wp_tinycolor_rgb_to_rgb(
 			array(
 				'r' => $match[1],
 				'g' => $match[2],
 				'b' => $match[3],
 			)
 		);
-	}
 
-	$rgba_regexp = '/^rgba' . $permissive_match4 . '$/';
-	if ( preg_match( $rgba_regexp, $color_str, $match ) ) {
-		return wp_tinycolor_rgb_to_rgb(
-			array(
-				'r' => $match[1],
-				'g' => $match[2],
-				'b' => $match[3],
-			)
-		);
+		$rgb['a'] = _wp_tinycolor_bound_alpha( $match[4] );
+
+		return $rgb;
 	}
 
 	$hsl_regexp = '/^hsl' . $permissive_match3 . '$/';
 	if ( preg_match( $hsl_regexp, $color_str, $match ) ) {
-		return wp_tinycolor_hsl_to_rgb(
+		$rgb = wp_tinycolor_hsl_to_rgb(
 			array(
 				'h' => $match[1],
 				's' => $match[2],
 				'l' => $match[3],
 			)
 		);
+
+		$rgb['a'] = 1;
+
+		return $rgb;
 	}
 
 	$hsla_regexp = '/^hsla' . $permissive_match4 . '$/';
 	if ( preg_match( $hsla_regexp, $color_str, $match ) ) {
-		return wp_tinycolor_hsl_to_rgb(
+		$rgb = wp_tinycolor_hsl_to_rgb(
 			array(
 				'h' => $match[1],
 				's' => $match[2],
 				'l' => $match[3],
 			)
 		);
+
+		$rgb['a'] = _wp_tinycolor_bound_alpha( $match[4] );
+
+		return $rgb;
 	}
 
 	$hex8_regexp = '/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/';
 	if ( preg_match( $hex8_regexp, $color_str, $match ) ) {
-		return wp_tinycolor_rgb_to_rgb(
+		$rgb = wp_tinycolor_rgb_to_rgb(
 			array(
 				'r' => base_convert( $match[1], 16, 10 ),
 				'g' => base_convert( $match[2], 16, 10 ),
 				'b' => base_convert( $match[3], 16, 10 ),
 			)
 		);
+
+		$rgb['a'] = _wp_tinycolor_bound_alpha(
+			base_convert( $match[4], 16, 10 ) / 255
+		);
+
+		return $rgb;
 	}
 
 	$hex6_regexp = '/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/';
 	if ( preg_match( $hex6_regexp, $color_str, $match ) ) {
-		return wp_tinycolor_rgb_to_rgb(
+		$rgb = wp_tinycolor_rgb_to_rgb(
 			array(
 				'r' => base_convert( $match[1], 16, 10 ),
 				'g' => base_convert( $match[2], 16, 10 ),
 				'b' => base_convert( $match[3], 16, 10 ),
 			)
 		);
+
+		$rgb['a'] = 1;
+
+		return $rgb;
 	}
 
 	$hex4_regexp = '/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/';
 	if ( preg_match( $hex4_regexp, $color_str, $match ) ) {
-		return wp_tinycolor_rgb_to_rgb(
+		$rgb = wp_tinycolor_rgb_to_rgb(
+			array(
+				'r' => base_convert( $match[1] . $match[1], 16, 10 ),
+				'g' => base_convert( $match[2] . $match[2], 16, 10 ),
+				'b' => base_convert( $match[3] . $match[3], 16, 10 ),
+			)
+		);
+
+		$rgb['a'] = _wp_tinycolor_bound_alpha(
+			base_convert( $match[4] . $match[4], 16, 10 ) / 255
+		);
+
+		return $rgb;
+	}
+
+	$hex3_regexp = '/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/';
+	if ( preg_match( $hex3_regexp, $color_str, $match ) ) {
+		$rgb = wp_tinycolor_rgb_to_rgb(
 			array(
 				'r' => base_convert( $match[1] . $match[1], 16, 10 ),
 				'g' => base_convert( $match[2] . $match[2], 16, 10 ),
 				'b' => base_convert( $match[3] . $match[3], 16, 10 ),
 			)
 		);
+
+		$rgb['a'] = 1;
+
+		return $rgb;
 	}
 
-	$hex3_regexp = '/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/';
-	if ( preg_match( $hex3_regexp, $color_str, $match ) ) {
-		return wp_tinycolor_rgb_to_rgb(
-			array(
-				'r' => base_convert( $match[1] . $match[1], 16, 10 ),
-				'g' => base_convert( $match[2] . $match[2], 16, 10 ),
-				'b' => base_convert( $match[3] . $match[3], 16, 10 ),
-			)
+	/*
+	 * The JS color picker considers the string "transparent" to be a hex value,
+	 * so we need to handle it here as a special case.
+	 */
+	if ( 'transparent' === $color_str ) {
+		return array(
+			'r' => 0,
+			'g' => 0,
+			'b' => 0,
+			'a' => 0,
 		);
 	}
 }
 
+/**
+ * Returns the prefixed id for the duotone filter for use as a CSS id.
+ *
+ * @since 5.9.1
+ * @access private
+ *
+ * @param array $preset Duotone preset value as seen in theme.json.
+ * @return string Duotone filter CSS id.
+ */
+function wp_get_duotone_filter_id( $preset ) {
+	if ( ! isset( $preset['slug'] ) ) {
+		return '';
+	}
+
+	return 'wp-duotone-' . $preset['slug'];
+}
+
+/**
+ * Returns the CSS filter property url to reference the rendered SVG.
+ *
+ * @since 5.9.0
+ * @access private
+ *
+ * @param array $preset Duotone preset value as seen in theme.json.
+ * @return string Duotone CSS filter property url value.
+ */
+function wp_get_duotone_filter_property( $preset ) {
+	$filter_id = wp_get_duotone_filter_id( $preset );
+	return "url('#" . $filter_id . "')";
+}
+
+/**
+ * Returns the duotone filter SVG string for the preset.
+ *
+ * @since 5.9.1
+ * @access private
+ *
+ * @param array $preset Duotone preset value as seen in theme.json.
+ * @return string Duotone SVG filter.
+ */
+function wp_get_duotone_filter_svg( $preset ) {
+	$filter_id = wp_get_duotone_filter_id( $preset );
+
+	$duotone_values = array(
+		'r' => array(),
+		'g' => array(),
+		'b' => array(),
+		'a' => array(),
+	);
+
+	if ( ! isset( $preset['colors'] ) || ! is_array( $preset['colors'] ) ) {
+		$preset['colors'] = array();
+	}
+
+	foreach ( $preset['colors'] as $color_str ) {
+		$color = wp_tinycolor_string_to_rgb( $color_str );
+
+		$duotone_values['r'][] = $color['r'] / 255;
+		$duotone_values['g'][] = $color['g'] / 255;
+		$duotone_values['b'][] = $color['b'] / 255;
+		$duotone_values['a'][] = $color['a'];
+	}
+
+	ob_start();
+
+	?>
+
+	<svg
+		xmlns="http://www.w3.org/2000/svg"
+		viewBox="0 0 0 0"
+		width="0"
+		height="0"
+		focusable="false"
+		role="none"
+		style="visibility: hidden; position: absolute; left: -9999px; overflow: hidden;"
+	>
+		<defs>
+			<filter id="<?php echo esc_attr( $filter_id ); ?>">
+				<feColorMatrix
+					color-interpolation-filters="sRGB"
+					type="matrix"
+					values="
+						.299 .587 .114 0 0
+						.299 .587 .114 0 0
+						.299 .587 .114 0 0
+						.299 .587 .114 0 0
+					"
+				/>
+				<feComponentTransfer color-interpolation-filters="sRGB" >
+					<feFuncR type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['r'] ) ); ?>" />
+					<feFuncG type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['g'] ) ); ?>" />
+					<feFuncB type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['b'] ) ); ?>" />
+					<feFuncA type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['a'] ) ); ?>" />
+				</feComponentTransfer>
+				<feComposite in2="SourceGraphic" operator="in" />
+			</filter>
+		</defs>
+	</svg>
+
+	<?php
+
+	$svg = ob_get_clean();
+
+	if ( ! defined( 'SCRIPT_DEBUG' ) || ! SCRIPT_DEBUG ) {
+		// Clean up the whitespace.
+		$svg = preg_replace( "/[\r\n\t ]+/", ' ', $svg );
+		$svg = preg_replace( '/> </', '><', $svg );
+		$svg = trim( $svg );
+	}
+
+	return $svg;
+}
 
 /**
  * Registers the style and colors block attributes for block types that support it.
@@ -322,7 +500,6 @@
  *
  * @param string $block_content Rendered block content.
  * @param array  $block         Block object.
- *
  * @return string Filtered block content.
  */
 function wp_render_duotone_support( $block_content, $block ) {
@@ -342,84 +519,61 @@
 		return $block_content;
 	}
 
-	$duotone_colors = $block['attrs']['style']['color']['duotone'];
-
-	$duotone_values = array(
-		'r' => array(),
-		'g' => array(),
-		'b' => array(),
+	$filter_preset   = array(
+		'slug'   => wp_unique_id( sanitize_key( implode( '-', $block['attrs']['style']['color']['duotone'] ) . '-' ) ),
+		'colors' => $block['attrs']['style']['color']['duotone'],
 	);
-	foreach ( $duotone_colors as $color_str ) {
-		$color = wp_tinycolor_string_to_rgb( $color_str );
+	$filter_property = wp_get_duotone_filter_property( $filter_preset );
+	$filter_id       = wp_get_duotone_filter_id( $filter_preset );
+	$filter_svg      = wp_get_duotone_filter_svg( $filter_preset );
 
-		$duotone_values['r'][] = $color['r'] / 255;
-		$duotone_values['g'][] = $color['g'] / 255;
-		$duotone_values['b'][] = $color['b'] / 255;
+	$scope     = '.' . $filter_id;
+	$selectors = explode( ',', $duotone_support );
+	$scoped    = array();
+	foreach ( $selectors as $sel ) {
+		$scoped[] = $scope . ' ' . trim( $sel );
 	}
+	$selector = implode( ', ', $scoped );
 
-	$duotone_id = 'wp-duotone-filter-' . uniqid();
+	// !important is needed because these styles render before global styles,
+	// and they should be overriding the duotone filters set by global styles.
+	$filter_style = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG
+		? $selector . " {\n\tfilter: " . $filter_property . " !important;\n}\n"
+		: $selector . '{filter:' . $filter_property . ' !important;}';
 
-	$selectors        = explode( ',', $duotone_support );
-	$selectors_scoped = array_map(
-		function ( $selector ) use ( $duotone_id ) {
-			return '.' . $duotone_id . ' ' . trim( $selector );
-		},
-		$selectors
-	);
-	$selectors_group  = implode( ', ', $selectors_scoped );
+	wp_register_style( $filter_id, false, array(), true, true );
+	wp_add_inline_style( $filter_id, $filter_style );
+	wp_enqueue_style( $filter_id );
+
+	add_action(
+		'wp_footer',
+		static function () use ( $filter_svg, $selector ) {
+			echo $filter_svg;
 
-	ob_start();
-
-	?>
-
-	<style>
-		<?php echo $selectors_group; ?> {
-			filter: url( <?php echo esc_url( '#' . $duotone_id ); ?> );
+			/*
+			 * Safari renders elements incorrectly on first paint when the SVG
+			 * filter comes after the content that it is filtering, so we force
+			 * a repaint with a WebKit hack which solves the issue.
+			 */
+			global $is_safari;
+			if ( $is_safari ) {
+				printf(
+					// Simply accessing el.offsetHeight flushes layout and style
+					// changes in WebKit without having to wait for setTimeout.
+					'<script>( function() { var el = document.querySelector( %s ); var display = el.style.display; el.style.display = "none"; el.offsetHeight; el.style.display = display; } )();</script>',
+					wp_json_encode( $selector )
+				);
+			}
 		}
-	</style>
-
-	<svg
-		xmlns:xlink="http://www.w3.org/1999/xlink"
-		viewBox="0 0 0 0"
-		width="0"
-		height="0"
-		focusable="false"
-		role="none"
-		style="visibility: hidden; position: absolute; left: -9999px; overflow: hidden;"
-	>
-		<defs>
-			<filter id="<?php echo esc_attr( $duotone_id ); ?>">
-				<feColorMatrix
-					type="matrix"
-					<?php // phpcs:disable Generic.WhiteSpace.DisallowSpaceIndent ?>
-					values=".299 .587 .114 0 0
-							.299 .587 .114 0 0
-							.299 .587 .114 0 0
-							0 0 0 1 0"
-					<?php // phpcs:enable Generic.WhiteSpace.DisallowSpaceIndent ?>
-				/>
-				<feComponentTransfer color-interpolation-filters="sRGB" >
-					<feFuncR type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['r'] ) ); ?>" />
-					<feFuncG type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['g'] ) ); ?>" />
-					<feFuncB type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['b'] ) ); ?>" />
-				</feComponentTransfer>
-			</filter>
-		</defs>
-	</svg>
-
-	<?php
-
-	$duotone = ob_get_clean();
+	);
 
 	// Like the layout hook, this assumes the hook only applies to blocks with a single wrapper.
-	$content = preg_replace(
+	return preg_replace(
 		'/' . preg_quote( 'class="', '/' ) . '/',
-		'class="' . $duotone_id . ' ',
+		'class="' . $filter_id . ' ',
 		$block_content,
 		1
 	);
-
-	return $content . $duotone;
 }
 
 // Register the block support.