wp/wp-includes/class-wp-duotone.php
changeset 21 48c4eec2b7e6
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
       
     1 <?php
       
     2 /**
       
     3  * WP_Duotone class
       
     4  *
       
     5  * Parts of this source were derived and modified from colord,
       
     6  * released under the MIT license.
       
     7  *
       
     8  * https://github.com/omgovich/colord
       
     9  *
       
    10  * Copyright (c) 2020 Vlad Shilov omgovich@ya.ru
       
    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 6.3.0
       
    33  */
       
    34 
       
    35 /**
       
    36  * Manages duotone block supports and global styles.
       
    37  *
       
    38  * @access private
       
    39  */
       
    40 class WP_Duotone {
       
    41 	/**
       
    42 	 * Block names from global, theme, and custom styles that use duotone presets and the slug of
       
    43 	 * the preset they are using.
       
    44 	 *
       
    45 	 * Example:
       
    46 	 *  [
       
    47 	 *      'core/featured-image' => 'blue-orange',
       
    48 	 *       …
       
    49 	 *  ]
       
    50 	 *
       
    51 	 * @internal
       
    52 	 *
       
    53 	 * @since 6.3.0
       
    54 	 *
       
    55 	 * @var array
       
    56 	 */
       
    57 	private static $global_styles_block_names;
       
    58 
       
    59 	/**
       
    60 	 * An array of duotone filter data from global, theme, and custom presets.
       
    61 	 *
       
    62 	 * Example:
       
    63 	 *  [
       
    64 	 *      'wp-duotone-blue-orange' => [
       
    65 	 *          'slug'  => 'blue-orange',
       
    66 	 *          'colors' => [ '#0000ff', '#ffcc00' ],
       
    67 	 *      ],
       
    68 	 *      'wp-duotone-red-yellow' => [
       
    69 	 *          'slug'   => 'red-yellow',
       
    70 	 *          'colors' => [ '#cc0000', '#ffff33' ],
       
    71 	 *      ],
       
    72 	 *      …
       
    73 	 *  ]
       
    74 	 *
       
    75 	 * @internal
       
    76 	 *
       
    77 	 * @since 6.3.0
       
    78 	 *
       
    79 	 * @var array
       
    80 	 */
       
    81 	private static $global_styles_presets;
       
    82 
       
    83 	/**
       
    84 	 * All of the duotone filter data from presets for CSS custom properties on
       
    85 	 * the page.
       
    86 	 *
       
    87 	 * Example:
       
    88 	 *  [
       
    89 	 *      'wp-duotone-blue-orange' => [
       
    90 	 *          'slug'   => 'blue-orange',
       
    91 	 *          'colors' => [ '#0000ff', '#ffcc00' ],
       
    92 	 *      ],
       
    93 	 *      …
       
    94 	 *  ]
       
    95 	 *
       
    96 	 * @internal
       
    97 	 *
       
    98 	 * @since 6.3.0
       
    99 	 *
       
   100 	 * @var array
       
   101 	 */
       
   102 	private static $used_global_styles_presets = array();
       
   103 
       
   104 	/**
       
   105 	 * All of the duotone filter data for SVGs on the page. Includes both
       
   106 	 * presets and custom filters.
       
   107 	 *
       
   108 	 * Example:
       
   109 	 *  [
       
   110 	 *      'wp-duotone-blue-orange' => [
       
   111 	 *          'slug'   => 'blue-orange',
       
   112 	 *          'colors' => [ '#0000ff', '#ffcc00' ],
       
   113 	 *      ],
       
   114 	 *      'wp-duotone-000000-ffffff-2' => [
       
   115 	 *          'slug'   => '000000-ffffff-2',
       
   116 	 *          'colors' => [ '#000000', '#ffffff' ],
       
   117 	 *      ],
       
   118 	 *      …
       
   119 	 *  ]
       
   120 	 *
       
   121 	 * @internal
       
   122 	 *
       
   123 	 * @since 6.3.0
       
   124 	 *
       
   125 	 * @var array
       
   126 	 */
       
   127 	private static $used_svg_filter_data = array();
       
   128 
       
   129 	/**
       
   130 	 * All of the block CSS declarations for styles on the page.
       
   131 	 *
       
   132 	 * Example:
       
   133 	 *  [
       
   134 	 *      [
       
   135 	 *          'selector'     => '.wp-duotone-000000-ffffff-2.wp-block-image img',
       
   136 	 *          'declarations' => [
       
   137 	 *              'filter' => 'url(#wp-duotone-000000-ffffff-2)',
       
   138 	 *          ],
       
   139 	 *      ],
       
   140 	 *      …
       
   141 	 *  ]
       
   142 	 *
       
   143 	 * @internal
       
   144 	 *
       
   145 	 * @since 6.3.0
       
   146 	 *
       
   147 	 * @var array
       
   148 	 */
       
   149 	private static $block_css_declarations = array();
       
   150 
       
   151 	/**
       
   152 	 * Clamps a value between an upper and lower bound.
       
   153 	 *
       
   154 	 * Direct port of colord's clamp function.
       
   155 	 *
       
   156 	 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/helpers.ts#L23 Sourced from colord.
       
   157 	 *
       
   158 	 * @internal
       
   159 	 *
       
   160 	 * @since 6.3.0
       
   161 	 *
       
   162 	 * @param float $number The number to clamp.
       
   163 	 * @param float $min    The minimum value.
       
   164 	 * @param float $max    The maximum value.
       
   165 	 * @return float The clamped value.
       
   166 	 */
       
   167 	private static function colord_clamp( $number, $min = 0, $max = 1 ) {
       
   168 		return $number > $max ? $max : ( $number > $min ? $number : $min );
       
   169 	}
       
   170 
       
   171 	/**
       
   172 	 * Processes and clamps a degree (angle) value properly.
       
   173 	 *
       
   174 	 * Direct port of colord's clampHue function.
       
   175 	 *
       
   176 	 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/helpers.ts#L32 Sourced from colord.
       
   177 	 *
       
   178 	 * @internal
       
   179 	 *
       
   180 	 * @since 6.3.0
       
   181 	 *
       
   182 	 * @param float $degrees The hue to clamp.
       
   183 	 * @return float The clamped hue.
       
   184 	 */
       
   185 	private static function colord_clamp_hue( $degrees ) {
       
   186 		$degrees = is_finite( $degrees ) ? $degrees % 360 : 0;
       
   187 		return $degrees > 0 ? $degrees : $degrees + 360;
       
   188 	}
       
   189 
       
   190 	/**
       
   191 	 * Converts a hue value to degrees from 0 to 360 inclusive.
       
   192 	 *
       
   193 	 * Direct port of colord's parseHue function.
       
   194 	 *
       
   195 	 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/helpers.ts#L40 Sourced from colord.
       
   196 	 *
       
   197 	 * @internal
       
   198 	 *
       
   199 	 * @since 6.3.0
       
   200 	 *
       
   201 	 * @param float  $value The hue value to parse.
       
   202 	 * @param string $unit  The unit of the hue value.
       
   203 	 * @return float The parsed hue value.
       
   204 	 */
       
   205 	private static function colord_parse_hue( $value, $unit = 'deg' ) {
       
   206 		$angle_units = array(
       
   207 			'grad' => 360 / 400,
       
   208 			'turn' => 360,
       
   209 			'rad'  => 360 / ( M_PI * 2 ),
       
   210 		);
       
   211 
       
   212 		$factor = isset( $angle_units[ $unit ] ) ? $angle_units[ $unit ] : 1;
       
   213 
       
   214 		return (float) $value * $factor;
       
   215 	}
       
   216 
       
   217 	/**
       
   218 	 * Parses any valid Hex3, Hex4, Hex6 or Hex8 string and converts it to an RGBA object.
       
   219 	 *
       
   220 	 * Direct port of colord's parseHex function.
       
   221 	 *
       
   222 	 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hex.ts#L8 Sourced from colord.
       
   223 	 *
       
   224 	 * @internal
       
   225 	 *
       
   226 	 * @since 6.3.0
       
   227 	 *
       
   228 	 * @param string $hex The hex string to parse.
       
   229 	 * @return array|null An array of RGBA values or null if the hex string is invalid.
       
   230 	 */
       
   231 	private static function colord_parse_hex( $hex ) {
       
   232 		$is_match = preg_match(
       
   233 			'/^#([0-9a-f]{3,8})$/i',
       
   234 			$hex,
       
   235 			$hex_match
       
   236 		);
       
   237 
       
   238 		if ( ! $is_match ) {
       
   239 			return null;
       
   240 		}
       
   241 
       
   242 		$hex = $hex_match[1];
       
   243 
       
   244 		if ( 4 >= strlen( $hex ) ) {
       
   245 			return array(
       
   246 				'r' => (int) base_convert( $hex[0] . $hex[0], 16, 10 ),
       
   247 				'g' => (int) base_convert( $hex[1] . $hex[1], 16, 10 ),
       
   248 				'b' => (int) base_convert( $hex[2] . $hex[2], 16, 10 ),
       
   249 				'a' => 4 === strlen( $hex ) ? round( base_convert( $hex[3] . $hex[3], 16, 10 ) / 255, 2 ) : 1,
       
   250 			);
       
   251 		}
       
   252 
       
   253 		if ( 6 === strlen( $hex ) || 8 === strlen( $hex ) ) {
       
   254 			return array(
       
   255 				'r' => (int) base_convert( substr( $hex, 0, 2 ), 16, 10 ),
       
   256 				'g' => (int) base_convert( substr( $hex, 2, 2 ), 16, 10 ),
       
   257 				'b' => (int) base_convert( substr( $hex, 4, 2 ), 16, 10 ),
       
   258 				'a' => 8 === strlen( $hex ) ? round( (int) base_convert( substr( $hex, 6, 2 ), 16, 10 ) / 255, 2 ) : 1,
       
   259 			);
       
   260 		}
       
   261 
       
   262 		return null;
       
   263 	}
       
   264 
       
   265 	/**
       
   266 	 * Clamps an array of RGBA values.
       
   267 	 *
       
   268 	 * Direct port of colord's clampRgba function.
       
   269 	 *
       
   270 	 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/rgb.ts#L5 Sourced from colord.
       
   271 	 *
       
   272 	 * @internal
       
   273 	 *
       
   274 	 * @since 6.3.0
       
   275 	 *
       
   276 	 * @param array $rgba The RGBA array to clamp.
       
   277 	 * @return array The clamped RGBA array.
       
   278 	 */
       
   279 	private static function colord_clamp_rgba( $rgba ) {
       
   280 		$rgba['r'] = self::colord_clamp( $rgba['r'], 0, 255 );
       
   281 		$rgba['g'] = self::colord_clamp( $rgba['g'], 0, 255 );
       
   282 		$rgba['b'] = self::colord_clamp( $rgba['b'], 0, 255 );
       
   283 		$rgba['a'] = self::colord_clamp( $rgba['a'] );
       
   284 
       
   285 		return $rgba;
       
   286 	}
       
   287 
       
   288 	/**
       
   289 	 * Parses a valid RGB[A] CSS color function/string.
       
   290 	 *
       
   291 	 * Direct port of colord's parseRgbaString function.
       
   292 	 *
       
   293 	 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/rgbString.ts#L18 Sourced from colord.
       
   294 	 *
       
   295 	 * @internal
       
   296 	 *
       
   297 	 * @since 6.3.0
       
   298 	 *
       
   299 	 * @param string $input The RGBA string to parse.
       
   300 	 * @return array|null An array of RGBA values or null if the RGB string is invalid.
       
   301 	 */
       
   302 	private static function colord_parse_rgba_string( $input ) {
       
   303 		// Functional syntax.
       
   304 		$is_match = preg_match(
       
   305 			'/^rgba?\(\s*([+-]?\d*\.?\d+)(%)?\s*,\s*([+-]?\d*\.?\d+)(%)?\s*,\s*([+-]?\d*\.?\d+)(%)?\s*(?:,\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i',
       
   306 			$input,
       
   307 			$match
       
   308 		);
       
   309 
       
   310 		if ( ! $is_match ) {
       
   311 			// Whitespace syntax.
       
   312 			$is_match = preg_match(
       
   313 				'/^rgba?\(\s*([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i',
       
   314 				$input,
       
   315 				$match
       
   316 			);
       
   317 		}
       
   318 
       
   319 		if ( ! $is_match ) {
       
   320 			return null;
       
   321 		}
       
   322 
       
   323 		/*
       
   324 		 * For some reason, preg_match doesn't include empty matches at the end
       
   325 		 * of the array, so we add them manually to make things easier later.
       
   326 		 */
       
   327 		for ( $i = 1; $i <= 8; $i++ ) {
       
   328 			if ( ! isset( $match[ $i ] ) ) {
       
   329 				$match[ $i ] = '';
       
   330 			}
       
   331 		}
       
   332 
       
   333 		if ( $match[2] !== $match[4] || $match[4] !== $match[6] ) {
       
   334 			return null;
       
   335 		}
       
   336 
       
   337 		return self::colord_clamp_rgba(
       
   338 			array(
       
   339 				'r' => (float) $match[1] / ( $match[2] ? 100 / 255 : 1 ),
       
   340 				'g' => (float) $match[3] / ( $match[4] ? 100 / 255 : 1 ),
       
   341 				'b' => (float) $match[5] / ( $match[6] ? 100 / 255 : 1 ),
       
   342 				'a' => '' === $match[7] ? 1 : (float) $match[7] / ( $match[8] ? 100 : 1 ),
       
   343 			)
       
   344 		);
       
   345 	}
       
   346 
       
   347 	/**
       
   348 	 * Clamps an array of HSLA values.
       
   349 	 *
       
   350 	 * Direct port of colord's clampHsla function.
       
   351 	 *
       
   352 	 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hsl.ts#L6 Sourced from colord.
       
   353 	 *
       
   354 	 * @internal
       
   355 	 *
       
   356 	 * @since 6.3.0
       
   357 	 *
       
   358 	 * @param array $hsla The HSLA array to clamp.
       
   359 	 * @return array The clamped HSLA array.
       
   360 	 */
       
   361 	private static function colord_clamp_hsla( $hsla ) {
       
   362 		$hsla['h'] = self::colord_clamp_hue( $hsla['h'] );
       
   363 		$hsla['s'] = self::colord_clamp( $hsla['s'], 0, 100 );
       
   364 		$hsla['l'] = self::colord_clamp( $hsla['l'], 0, 100 );
       
   365 		$hsla['a'] = self::colord_clamp( $hsla['a'] );
       
   366 
       
   367 		return $hsla;
       
   368 	}
       
   369 
       
   370 	/**
       
   371 	 * Converts an HSVA array to RGBA.
       
   372 	 *
       
   373 	 * Direct port of colord's hsvaToRgba function.
       
   374 	 *
       
   375 	 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hsv.ts#L52 Sourced from colord.
       
   376 	 *
       
   377 	 * @internal
       
   378 	 *
       
   379 	 * @since 6.3.0
       
   380 	 *
       
   381 	 * @param array $hsva The HSVA array to convert.
       
   382 	 * @return array The RGBA array.
       
   383 	 */
       
   384 	private static function colord_hsva_to_rgba( $hsva ) {
       
   385 		$h = ( $hsva['h'] / 360 ) * 6;
       
   386 		$s = $hsva['s'] / 100;
       
   387 		$v = $hsva['v'] / 100;
       
   388 		$a = $hsva['a'];
       
   389 
       
   390 		$hh     = floor( $h );
       
   391 		$b      = $v * ( 1 - $s );
       
   392 		$c      = $v * ( 1 - ( $h - $hh ) * $s );
       
   393 		$d      = $v * ( 1 - ( 1 - $h + $hh ) * $s );
       
   394 		$module = $hh % 6;
       
   395 
       
   396 		return array(
       
   397 			'r' => array( $v, $c, $b, $b, $d, $v )[ $module ] * 255,
       
   398 			'g' => array( $d, $v, $v, $c, $b, $b )[ $module ] * 255,
       
   399 			'b' => array( $b, $b, $d, $v, $v, $c )[ $module ] * 255,
       
   400 			'a' => $a,
       
   401 		);
       
   402 	}
       
   403 
       
   404 	/**
       
   405 	 * Converts an HSLA array to HSVA.
       
   406 	 *
       
   407 	 * Direct port of colord's hslaToHsva function.
       
   408 	 *
       
   409 	 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hsl.ts#L33 Sourced from colord.
       
   410 	 *
       
   411 	 * @internal
       
   412 	 *
       
   413 	 * @since 6.3.0
       
   414 	 *
       
   415 	 * @param array $hsla The HSLA array to convert.
       
   416 	 * @return array The HSVA array.
       
   417 	 */
       
   418 	private static function colord_hsla_to_hsva( $hsla ) {
       
   419 		$h = $hsla['h'];
       
   420 		$s = $hsla['s'];
       
   421 		$l = $hsla['l'];
       
   422 		$a = $hsla['a'];
       
   423 
       
   424 		$s *= ( $l < 50 ? $l : 100 - $l ) / 100;
       
   425 
       
   426 		return array(
       
   427 			'h' => $h,
       
   428 			's' => $s > 0 ? ( ( 2 * $s ) / ( $l + $s ) ) * 100 : 0,
       
   429 			'v' => $l + $s,
       
   430 			'a' => $a,
       
   431 		);
       
   432 	}
       
   433 
       
   434 	/**
       
   435 	 * Converts an HSLA array to RGBA.
       
   436 	 *
       
   437 	 * Direct port of colord's hslaToRgba function.
       
   438 	 *
       
   439 	 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hsl.ts#L55 Sourced from colord.
       
   440 	 *
       
   441 	 * @internal
       
   442 	 *
       
   443 	 * @since 6.3.0
       
   444 	 *
       
   445 	 * @param array $hsla The HSLA array to convert.
       
   446 	 * @return array The RGBA array.
       
   447 	 */
       
   448 	private static function colord_hsla_to_rgba( $hsla ) {
       
   449 		return self::colord_hsva_to_rgba( self::colord_hsla_to_hsva( $hsla ) );
       
   450 	}
       
   451 
       
   452 	/**
       
   453 	 * Parses a valid HSL[A] CSS color function/string.
       
   454 	 *
       
   455 	 * Direct port of colord's parseHslaString function.
       
   456 	 *
       
   457 	 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hslString.ts#L17 Sourced from colord.
       
   458 	 *
       
   459 	 * @internal
       
   460 	 *
       
   461 	 * @since 6.3.0
       
   462 	 *
       
   463 	 * @param string $input The HSLA string to parse.
       
   464 	 * @return array|null An array of RGBA values or null if the RGB string is invalid.
       
   465 	 */
       
   466 	private static function colord_parse_hsla_string( $input ) {
       
   467 		// Functional syntax.
       
   468 		$is_match = preg_match(
       
   469 			'/^hsla?\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s*,\s*([+-]?\d*\.?\d+)%\s*,\s*([+-]?\d*\.?\d+)%\s*(?:,\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i',
       
   470 			$input,
       
   471 			$match
       
   472 		);
       
   473 
       
   474 		if ( ! $is_match ) {
       
   475 			// Whitespace syntax.
       
   476 			$is_match = preg_match(
       
   477 				'/^hsla?\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s+([+-]?\d*\.?\d+)%\s+([+-]?\d*\.?\d+)%\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i',
       
   478 				$input,
       
   479 				$match
       
   480 			);
       
   481 		}
       
   482 
       
   483 		if ( ! $is_match ) {
       
   484 			return null;
       
   485 		}
       
   486 
       
   487 		/*
       
   488 		 * For some reason, preg_match doesn't include empty matches at the end
       
   489 		 * of the array, so we add them manually to make things easier later.
       
   490 		 */
       
   491 		for ( $i = 1; $i <= 6; $i++ ) {
       
   492 			if ( ! isset( $match[ $i ] ) ) {
       
   493 				$match[ $i ] = '';
       
   494 			}
       
   495 		}
       
   496 
       
   497 		$hsla = self::colord_clamp_hsla(
       
   498 			array(
       
   499 				'h' => self::colord_parse_hue( $match[1], $match[2] ),
       
   500 				's' => (float) $match[3],
       
   501 				'l' => (float) $match[4],
       
   502 				'a' => '' === $match[5] ? 1 : (float) $match[5] / ( $match[6] ? 100 : 1 ),
       
   503 			)
       
   504 		);
       
   505 
       
   506 		return self::colord_hsla_to_rgba( $hsla );
       
   507 	}
       
   508 
       
   509 	/**
       
   510 	 * Tries to convert an incoming string into RGBA values.
       
   511 	 *
       
   512 	 * Direct port of colord's parse function simplified for our use case. This
       
   513 	 * version only supports string parsing and only returns RGBA values.
       
   514 	 *
       
   515 	 * @link https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/parse.ts#L37 Sourced from colord.
       
   516 	 *
       
   517 	 * @internal
       
   518 	 *
       
   519 	 * @since 6.3.0
       
   520 	 *
       
   521 	 * @param string $input The string to parse.
       
   522 	 * @return array|null An array of RGBA values or null if the string is invalid.
       
   523 	 */
       
   524 	private static function colord_parse( $input ) {
       
   525 		$result = self::colord_parse_hex( $input );
       
   526 
       
   527 		if ( ! $result ) {
       
   528 			$result = self::colord_parse_rgba_string( $input );
       
   529 		}
       
   530 
       
   531 		if ( ! $result ) {
       
   532 			$result = self::colord_parse_hsla_string( $input );
       
   533 		}
       
   534 
       
   535 		return $result;
       
   536 	}
       
   537 
       
   538 	/**
       
   539 	 * Takes the inline CSS duotone variable from a block and return the slug.
       
   540 	 *
       
   541 	 * Handles styles slugs like:
       
   542 	 * var:preset|duotone|blue-orange
       
   543 	 * var(--wp--preset--duotone--blue-orange)
       
   544 	 *
       
   545 	 * @internal
       
   546 	 *
       
   547 	 * @since 6.3.0
       
   548 	 *
       
   549 	 * @param string $duotone_attr The duotone attribute from a block.
       
   550 	 * @return string The slug of the duotone preset or an empty string if no slug is found.
       
   551 	 */
       
   552 	private static function get_slug_from_attribute( $duotone_attr ) {
       
   553 		// Uses Branch Reset Groups `(?|…)` to return one capture group.
       
   554 		preg_match( '/(?|var:preset\|duotone\|(\S+)|var\(--wp--preset--duotone--(\S+)\))/', $duotone_attr, $matches );
       
   555 
       
   556 		return ! empty( $matches[1] ) ? $matches[1] : '';
       
   557 	}
       
   558 
       
   559 	/**
       
   560 	 * Checks if we have a valid duotone preset.
       
   561 	 *
       
   562 	 * Valid presets are defined in the $global_styles_presets array.
       
   563 	 *
       
   564 	 * @internal
       
   565 	 *
       
   566 	 * @since 6.3.0
       
   567 	 *
       
   568 	 * @param string $duotone_attr The duotone attribute from a block.
       
   569 	 * @return bool True if the duotone preset present and valid.
       
   570 	 */
       
   571 	private static function is_preset( $duotone_attr ) {
       
   572 		$slug      = self::get_slug_from_attribute( $duotone_attr );
       
   573 		$filter_id = self::get_filter_id( $slug );
       
   574 
       
   575 		return array_key_exists( $filter_id, self::get_all_global_styles_presets() );
       
   576 	}
       
   577 
       
   578 	/**
       
   579 	 * Gets the CSS variable name for a duotone preset.
       
   580 	 *
       
   581 	 * Example output:
       
   582 	 *  --wp--preset--duotone--blue-orange
       
   583 	 *
       
   584 	 * @internal
       
   585 	 *
       
   586 	 * @since 6.3.0
       
   587 	 *
       
   588 	 * @param string $slug The slug of the duotone preset.
       
   589 	 * @return string The CSS variable name.
       
   590 	 */
       
   591 	private static function get_css_custom_property_name( $slug ) {
       
   592 		return "--wp--preset--duotone--$slug";
       
   593 	}
       
   594 
       
   595 	/**
       
   596 	 * Get the ID of the duotone filter.
       
   597 	 *
       
   598 	 * Example output:
       
   599 	 *  wp-duotone-blue-orange
       
   600 	 *
       
   601 	 * @internal
       
   602 	 *
       
   603 	 * @since 6.3.0
       
   604 	 *
       
   605 	 * @param string $slug The slug of the duotone preset.
       
   606 	 * @return string The ID of the duotone filter.
       
   607 	 */
       
   608 	private static function get_filter_id( $slug ) {
       
   609 		return "wp-duotone-$slug";
       
   610 	}
       
   611 
       
   612 	/**
       
   613 	 * Get the CSS variable for a duotone preset.
       
   614 	 *
       
   615 	 * Example output:
       
   616 	 *  var(--wp--preset--duotone--blue-orange)
       
   617 	 *
       
   618 	 * @internal
       
   619 	 *
       
   620 	 * @since 6.3.0
       
   621 	 *
       
   622 	 * @param string $slug The slug of the duotone preset.
       
   623 	 * @return string The CSS variable.
       
   624 	 */
       
   625 	private static function get_css_var( $slug ) {
       
   626 		$name = self::get_css_custom_property_name( $slug );
       
   627 		return "var($name)";
       
   628 	}
       
   629 
       
   630 	/**
       
   631 	 * Get the URL for a duotone filter.
       
   632 	 *
       
   633 	 * Example output:
       
   634 	 *  url(#wp-duotone-blue-orange)
       
   635 	 *
       
   636 	 * @internal
       
   637 	 *
       
   638 	 * @since 6.3.0
       
   639 	 *
       
   640 	 * @param string $filter_id The ID of the filter.
       
   641 	 * @return string The URL for the duotone filter.
       
   642 	 */
       
   643 	private static function get_filter_url( $filter_id ) {
       
   644 		return "url(#$filter_id)";
       
   645 	}
       
   646 
       
   647 	/**
       
   648 	 * Gets the SVG for the duotone filter definition.
       
   649 	 *
       
   650 	 * Whitespace is removed when SCRIPT_DEBUG is not enabled.
       
   651 	 *
       
   652 	 * @internal
       
   653 	 *
       
   654 	 * @since 6.3.0
       
   655 	 *
       
   656 	 * @param string $filter_id The ID of the filter.
       
   657 	 * @param array  $colors    An array of color strings.
       
   658 	 * @return string An SVG with a duotone filter definition.
       
   659 	 */
       
   660 	private static function get_filter_svg( $filter_id, $colors ) {
       
   661 		$duotone_values = array(
       
   662 			'r' => array(),
       
   663 			'g' => array(),
       
   664 			'b' => array(),
       
   665 			'a' => array(),
       
   666 		);
       
   667 
       
   668 		foreach ( $colors as $color_str ) {
       
   669 			$color = self::colord_parse( $color_str );
       
   670 
       
   671 			if ( null === $color ) {
       
   672 				$error_message = sprintf(
       
   673 					/* translators: 1: Duotone colors, 2: theme.json, 3: settings.color.duotone */
       
   674 					__( '"%1$s" in %2$s %3$s is not a hex or rgb string.' ),
       
   675 					$color_str,
       
   676 					'theme.json',
       
   677 					'settings.color.duotone'
       
   678 				);
       
   679 				_doing_it_wrong( __METHOD__, $error_message, '6.3.0' );
       
   680 			} else {
       
   681 				$duotone_values['r'][] = $color['r'] / 255;
       
   682 				$duotone_values['g'][] = $color['g'] / 255;
       
   683 				$duotone_values['b'][] = $color['b'] / 255;
       
   684 				$duotone_values['a'][] = $color['a'];
       
   685 			}
       
   686 		}
       
   687 
       
   688 		ob_start();
       
   689 
       
   690 		?>
       
   691 
       
   692 		<svg
       
   693 			xmlns="http://www.w3.org/2000/svg"
       
   694 			viewBox="0 0 0 0"
       
   695 			width="0"
       
   696 			height="0"
       
   697 			focusable="false"
       
   698 			role="none"
       
   699 			style="visibility: hidden; position: absolute; left: -9999px; overflow: hidden;"
       
   700 		>
       
   701 			<defs>
       
   702 				<filter id="<?php echo esc_attr( $filter_id ); ?>">
       
   703 					<feColorMatrix
       
   704 						color-interpolation-filters="sRGB"
       
   705 						type="matrix"
       
   706 						values="
       
   707 							.299 .587 .114 0 0
       
   708 							.299 .587 .114 0 0
       
   709 							.299 .587 .114 0 0
       
   710 							.299 .587 .114 0 0
       
   711 						"
       
   712 					/>
       
   713 					<feComponentTransfer color-interpolation-filters="sRGB" >
       
   714 						<feFuncR type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['r'] ) ); ?>" />
       
   715 						<feFuncG type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['g'] ) ); ?>" />
       
   716 						<feFuncB type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['b'] ) ); ?>" />
       
   717 						<feFuncA type="table" tableValues="<?php echo esc_attr( implode( ' ', $duotone_values['a'] ) ); ?>" />
       
   718 					</feComponentTransfer>
       
   719 					<feComposite in2="SourceGraphic" operator="in" />
       
   720 				</filter>
       
   721 			</defs>
       
   722 		</svg>
       
   723 
       
   724 		<?php
       
   725 
       
   726 		$svg = ob_get_clean();
       
   727 
       
   728 		if ( ! SCRIPT_DEBUG ) {
       
   729 			// Clean up the whitespace.
       
   730 			$svg = preg_replace( "/[\r\n\t ]+/", ' ', $svg );
       
   731 			$svg = str_replace( '> <', '><', $svg );
       
   732 			$svg = trim( $svg );
       
   733 		}
       
   734 
       
   735 		return $svg;
       
   736 	}
       
   737 
       
   738 	/**
       
   739 	 * Returns the prefixed id for the duotone filter for use as a CSS id.
       
   740 	 *
       
   741 	 * Exported for the deprecated function wp_get_duotone_filter_id().
       
   742 	 *
       
   743 	 * @internal
       
   744 	 *
       
   745 	 * @since 6.3.0
       
   746 	 * @deprecated 6.3.0
       
   747 	 *
       
   748 	 * @param  array $preset Duotone preset value as seen in theme.json.
       
   749 	 * @return string        Duotone filter CSS id.
       
   750 	 */
       
   751 	public static function get_filter_id_from_preset( $preset ) {
       
   752 		_deprecated_function( __FUNCTION__, '6.3.0' );
       
   753 
       
   754 		$filter_id = '';
       
   755 		if ( isset( $preset['slug'] ) ) {
       
   756 			$filter_id = self::get_filter_id( $preset['slug'] );
       
   757 		}
       
   758 		return $filter_id;
       
   759 	}
       
   760 
       
   761 	/**
       
   762 	 * Gets the SVG for the duotone filter definition from a preset.
       
   763 	 *
       
   764 	 * Exported for the deprecated function wp_get_duotone_filter_property().
       
   765 	 *
       
   766 	 * @internal
       
   767 	 *
       
   768 	 * @since 6.3.0
       
   769 	 * @deprecated 6.3.0
       
   770 	 *
       
   771 	 * @param array $preset The duotone preset.
       
   772 	 * @return string The SVG for the filter definition.
       
   773 	 */
       
   774 	public static function get_filter_svg_from_preset( $preset ) {
       
   775 		_deprecated_function( __FUNCTION__, '6.3.0' );
       
   776 
       
   777 		$filter_id = self::get_filter_id_from_preset( $preset );
       
   778 		return self::get_filter_svg( $filter_id, $preset['colors'] );
       
   779 	}
       
   780 
       
   781 	/**
       
   782 	 * Get the SVGs for the duotone filters.
       
   783 	 *
       
   784 	 * Example output:
       
   785 	 *  <svg><defs><filter id="wp-duotone-blue-orange">…</filter></defs></svg><svg>…</svg>
       
   786 	 *
       
   787 	 * @internal
       
   788 	 *
       
   789 	 * @since 6.3.0
       
   790 	 *
       
   791 	 * @param array $sources The duotone presets.
       
   792 	 * @return string The SVGs for the duotone filters.
       
   793 	 */
       
   794 	private static function get_svg_definitions( $sources ) {
       
   795 		$svgs = '';
       
   796 		foreach ( $sources as $filter_id => $filter_data ) {
       
   797 			$colors = $filter_data['colors'];
       
   798 			$svgs  .= self::get_filter_svg( $filter_id, $colors );
       
   799 		}
       
   800 		return $svgs;
       
   801 	}
       
   802 
       
   803 	/**
       
   804 	 * Get the CSS for global styles.
       
   805 	 *
       
   806 	 * Example output:
       
   807 	 *  body{--wp--preset--duotone--blue-orange:url('#wp-duotone-blue-orange');}
       
   808 	 *
       
   809 	 * @internal
       
   810 	 *
       
   811 	 * @since 6.3.0
       
   812 	 * @since 6.6.0 Replaced body selector with `WP_Theme_JSON::ROOT_CSS_PROPERTIES_SELECTOR`.
       
   813 	 *
       
   814 	 * @param array $sources The duotone presets.
       
   815 	 * @return string The CSS for global styles.
       
   816 	 */
       
   817 	private static function get_global_styles_presets( $sources ) {
       
   818 		$css = WP_Theme_JSON::ROOT_CSS_PROPERTIES_SELECTOR . '{';
       
   819 		foreach ( $sources as $filter_id => $filter_data ) {
       
   820 			$slug              = $filter_data['slug'];
       
   821 			$colors            = $filter_data['colors'];
       
   822 			$css_property_name = self::get_css_custom_property_name( $slug );
       
   823 			$declaration_value = is_string( $colors ) ? $colors : self::get_filter_url( $filter_id );
       
   824 			$css              .= "$css_property_name:$declaration_value;";
       
   825 		}
       
   826 		$css .= '}';
       
   827 		return $css;
       
   828 	}
       
   829 
       
   830 	/**
       
   831 	 * Enqueue a block CSS declaration for the page.
       
   832 	 *
       
   833 	 * This does not include any SVGs.
       
   834 	 *
       
   835 	 * @internal
       
   836 	 *
       
   837 	 * @since 6.3.0
       
   838 	 *
       
   839 	 * @param string $filter_id        The filter ID. e.g. 'wp-duotone-000000-ffffff-2'.
       
   840 	 * @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'.
       
   841 	 * @param string $filter_value     The filter CSS value. e.g. 'url(#wp-duotone-000000-ffffff-2)' or 'unset'.
       
   842 	 */
       
   843 	private static function enqueue_block_css( $filter_id, $duotone_selector, $filter_value ) {
       
   844 		// Build the CSS selectors to which the filter will be applied.
       
   845 		$selectors = explode( ',', $duotone_selector );
       
   846 
       
   847 		$selectors_scoped = array();
       
   848 		foreach ( $selectors as $selector_part ) {
       
   849 			/*
       
   850 			 * Assuming the selector part is a subclass selector (not a tag name)
       
   851 			 * so we can prepend the filter id class. If we want to support elements
       
   852 			 * such as `img` or namespaces, we'll need to add a case for that here.
       
   853 			 */
       
   854 			$selectors_scoped[] = '.' . $filter_id . trim( $selector_part );
       
   855 		}
       
   856 
       
   857 		$selector = implode( ', ', $selectors_scoped );
       
   858 
       
   859 		self::$block_css_declarations[] = array(
       
   860 			'selector'     => $selector,
       
   861 			'declarations' => array(
       
   862 				'filter' => $filter_value,
       
   863 			),
       
   864 		);
       
   865 	}
       
   866 
       
   867 	/**
       
   868 	 * Enqueue custom filter assets for the page.
       
   869 	 *
       
   870 	 * Includes an SVG filter and block CSS declaration.
       
   871 	 *
       
   872 	 * @internal
       
   873 	 *
       
   874 	 * @since 6.3.0
       
   875 	 *
       
   876 	 * @param string $filter_id        The filter ID. e.g. 'wp-duotone-000000-ffffff-2'.
       
   877 	 * @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'.
       
   878 	 * @param string $filter_value     The filter CSS value. e.g. 'url(#wp-duotone-000000-ffffff-2)' or 'unset'.
       
   879 	 * @param array  $filter_data      Duotone filter data with 'slug' and 'colors' keys.
       
   880 	 */
       
   881 	private static function enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $filter_data ) {
       
   882 		self::$used_svg_filter_data[ $filter_id ] = $filter_data;
       
   883 		self::enqueue_block_css( $filter_id, $duotone_selector, $filter_value );
       
   884 	}
       
   885 
       
   886 	/**
       
   887 	 * Enqueue preset assets for the page.
       
   888 	 *
       
   889 	 * Includes a CSS custom property, SVG filter, and block CSS declaration.
       
   890 	 *
       
   891 	 * @internal
       
   892 	 *
       
   893 	 * @since 6.3.0
       
   894 	 *
       
   895 	 * @param string $filter_id        The filter ID. e.g. 'wp-duotone-blue-orange'.
       
   896 	 * @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'.
       
   897 	 * @param string $filter_value     The filter CSS value. e.g. 'url(#wp-duotone-blue-orange)' or 'unset'.
       
   898 	 */
       
   899 	private static function enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value ) {
       
   900 		$global_styles_presets = self::get_all_global_styles_presets();
       
   901 		if ( ! array_key_exists( $filter_id, $global_styles_presets ) ) {
       
   902 			$error_message = sprintf(
       
   903 				/* translators: 1: Duotone filter ID, 2: theme.json */
       
   904 				__( 'The duotone id "%1$s" is not registered in %2$s settings' ),
       
   905 				$filter_id,
       
   906 				'theme.json'
       
   907 			);
       
   908 			_doing_it_wrong( __METHOD__, $error_message, '6.3.0' );
       
   909 			return;
       
   910 		}
       
   911 		self::$used_global_styles_presets[ $filter_id ] = $global_styles_presets[ $filter_id ];
       
   912 		self::enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $global_styles_presets[ $filter_id ] );
       
   913 	}
       
   914 
       
   915 	/**
       
   916 	 * Registers the style and colors block attributes for block types that support it.
       
   917 	 *
       
   918 	 * Block support is added with `supports.filter.duotone` in block.json.
       
   919 	 *
       
   920 	 * @since 6.3.0
       
   921 	 *
       
   922 	 * @param WP_Block_Type $block_type Block Type.
       
   923 	 */
       
   924 	public static function register_duotone_support( $block_type ) {
       
   925 		/*
       
   926 		 * Previous `color.__experimentalDuotone` support flag is migrated
       
   927 		 * to `filter.duotone` via `block_type_metadata_settings` filter.
       
   928 		 */
       
   929 		if ( block_has_support( $block_type, array( 'filter', 'duotone' ), null ) ) {
       
   930 			if ( ! $block_type->attributes ) {
       
   931 				$block_type->attributes = array();
       
   932 			}
       
   933 
       
   934 			if ( ! array_key_exists( 'style', $block_type->attributes ) ) {
       
   935 				$block_type->attributes['style'] = array(
       
   936 					'type' => 'object',
       
   937 				);
       
   938 			}
       
   939 		}
       
   940 	}
       
   941 
       
   942 	/**
       
   943 	 * Get the CSS selector for a block type.
       
   944 	 *
       
   945 	 * This handles selectors defined in `color.__experimentalDuotone` support
       
   946 	 * if `filter.duotone` support is not defined.
       
   947 	 *
       
   948 	 * @internal
       
   949 	 * @since 6.3.0
       
   950 	 *
       
   951 	 * @param WP_Block_Type $block_type Block type to check for support.
       
   952 	 * @return string|null The CSS selector or null if there is no support.
       
   953 	 */
       
   954 	private static function get_selector( $block_type ) {
       
   955 		if ( ! ( $block_type instanceof WP_Block_Type ) ) {
       
   956 			return null;
       
   957 		}
       
   958 
       
   959 		/*
       
   960 		 * Backward compatibility with `supports.color.__experimentalDuotone`
       
   961 		 * is provided via the `block_type_metadata_settings` filter. If
       
   962 		 * `supports.filter.duotone` has not been set and the experimental
       
   963 		 * property has been, the experimental property value is copied into
       
   964 		 * `supports.filter.duotone`.
       
   965 		 */
       
   966 		$duotone_support = block_has_support( $block_type, array( 'filter', 'duotone' ) );
       
   967 		if ( ! $duotone_support ) {
       
   968 			return null;
       
   969 		}
       
   970 
       
   971 		/*
       
   972 		 * If the experimental duotone support was set, that value is to be
       
   973 		 * treated as a selector and requires scoping.
       
   974 		 */
       
   975 		$experimental_duotone = isset( $block_type->supports['color']['__experimentalDuotone'] )
       
   976 			? $block_type->supports['color']['__experimentalDuotone']
       
   977 			: false;
       
   978 		if ( $experimental_duotone ) {
       
   979 			$root_selector = wp_get_block_css_selector( $block_type );
       
   980 			return is_string( $experimental_duotone )
       
   981 				? WP_Theme_JSON::scope_selector( $root_selector, $experimental_duotone )
       
   982 				: $root_selector;
       
   983 		}
       
   984 
       
   985 		// Regular filter.duotone support uses filter.duotone selectors with fallbacks.
       
   986 		return wp_get_block_css_selector( $block_type, array( 'filter', 'duotone' ), true );
       
   987 	}
       
   988 
       
   989 	/**
       
   990 	 * Scrape all possible duotone presets from global and theme styles and
       
   991 	 * store them in self::$global_styles_presets.
       
   992 	 *
       
   993 	 * Used in conjunction with self::render_duotone_support for blocks that
       
   994 	 * use duotone preset filters.
       
   995 	 *
       
   996 	 * @since 6.3.0
       
   997 	 *
       
   998 	 * @return array An array of global styles presets, keyed on the filter ID.
       
   999 	 */
       
  1000 	private static function get_all_global_styles_presets() {
       
  1001 		if ( isset( self::$global_styles_presets ) ) {
       
  1002 			return self::$global_styles_presets;
       
  1003 		}
       
  1004 		// Get the per block settings from the theme.json.
       
  1005 		$tree              = wp_get_global_settings();
       
  1006 		$presets_by_origin = isset( $tree['color']['duotone'] ) ? $tree['color']['duotone'] : array();
       
  1007 
       
  1008 		self::$global_styles_presets = array();
       
  1009 		foreach ( $presets_by_origin as $presets ) {
       
  1010 			foreach ( $presets as $preset ) {
       
  1011 				$filter_id = self::get_filter_id( _wp_to_kebab_case( $preset['slug'] ) );
       
  1012 
       
  1013 				self::$global_styles_presets[ $filter_id ] = $preset;
       
  1014 			}
       
  1015 		}
       
  1016 
       
  1017 		return self::$global_styles_presets;
       
  1018 	}
       
  1019 
       
  1020 	/**
       
  1021 	 * Scrape all block names from global styles and store in self::$global_styles_block_names.
       
  1022 	 *
       
  1023 	 * Used in conjunction with self::render_duotone_support to output the
       
  1024 	 * duotone filters defined in the theme.json global styles.
       
  1025 	 *
       
  1026 	 * @since 6.3.0
       
  1027 	 *
       
  1028 	 * @return string[] An array of global style block slugs, keyed on the block name.
       
  1029 	 */
       
  1030 	private static function get_all_global_style_block_names() {
       
  1031 		if ( isset( self::$global_styles_block_names ) ) {
       
  1032 			return self::$global_styles_block_names;
       
  1033 		}
       
  1034 		// Get the per block settings from the theme.json.
       
  1035 		$tree        = WP_Theme_JSON_Resolver::get_merged_data();
       
  1036 		$block_nodes = $tree->get_styles_block_nodes();
       
  1037 		$theme_json  = $tree->get_raw_data();
       
  1038 
       
  1039 		self::$global_styles_block_names = array();
       
  1040 
       
  1041 		foreach ( $block_nodes as $block_node ) {
       
  1042 			// This block definition doesn't include any duotone settings. Skip it.
       
  1043 			if ( empty( $block_node['duotone'] ) ) {
       
  1044 				continue;
       
  1045 			}
       
  1046 
       
  1047 			// Value looks like this: 'var(--wp--preset--duotone--blue-orange)' or 'var:preset|duotone|blue-orange'.
       
  1048 			$duotone_attr_path = array_merge( $block_node['path'], array( 'filter', 'duotone' ) );
       
  1049 			$duotone_attr      = _wp_array_get( $theme_json, $duotone_attr_path, array() );
       
  1050 
       
  1051 			if ( empty( $duotone_attr ) ) {
       
  1052 				continue;
       
  1053 			}
       
  1054 			// If it has a duotone filter preset, save the block name and the preset slug.
       
  1055 			$slug = self::get_slug_from_attribute( $duotone_attr );
       
  1056 
       
  1057 			if ( $slug && $slug !== $duotone_attr ) {
       
  1058 				self::$global_styles_block_names[ $block_node['name'] ] = $slug;
       
  1059 			}
       
  1060 		}
       
  1061 		return self::$global_styles_block_names;
       
  1062 	}
       
  1063 
       
  1064 	/**
       
  1065 	 * Render out the duotone CSS styles and SVG.
       
  1066 	 *
       
  1067 	 * The hooks self::set_global_style_block_names and self::set_global_styles_presets
       
  1068 	 * must be called before this function.
       
  1069 	 *
       
  1070 	 * @since 6.3.0
       
  1071 	 *
       
  1072 	 * @param  string   $block_content Rendered block content.
       
  1073 	 * @param  array    $block         Block object.
       
  1074 	 * @param  WP_Block $wp_block      The block instance.
       
  1075 	 * @return string Filtered block content.
       
  1076 	 */
       
  1077 	public static function render_duotone_support( $block_content, $block, $wp_block ) {
       
  1078 		if ( ! $block['blockName'] ) {
       
  1079 			return $block_content;
       
  1080 		}
       
  1081 		$duotone_selector = self::get_selector( $wp_block->block_type );
       
  1082 
       
  1083 		if ( ! $duotone_selector ) {
       
  1084 			return $block_content;
       
  1085 		}
       
  1086 
       
  1087 		$global_styles_block_names = self::get_all_global_style_block_names();
       
  1088 
       
  1089 		// The block should have a duotone attribute or have duotone defined in its theme.json to be processed.
       
  1090 		$has_duotone_attribute     = isset( $block['attrs']['style']['color']['duotone'] );
       
  1091 		$has_global_styles_duotone = array_key_exists( $block['blockName'], $global_styles_block_names );
       
  1092 
       
  1093 		if ( ! $has_duotone_attribute && ! $has_global_styles_duotone ) {
       
  1094 			return $block_content;
       
  1095 		}
       
  1096 
       
  1097 		// Generate the pieces needed for rendering a duotone to the page.
       
  1098 		if ( $has_duotone_attribute ) {
       
  1099 
       
  1100 			/*
       
  1101 			 * Possible values for duotone attribute:
       
  1102 			 * 1. Array of colors - e.g. array('#000000', '#ffffff').
       
  1103 			 * 2. Variable for an existing Duotone preset - e.g. 'var:preset|duotone|blue-orange' or 'var(--wp--preset--duotone--blue-orange)''
       
  1104 			 * 3. A CSS string - e.g. 'unset' to remove globally applied duotone.
       
  1105 			 */
       
  1106 
       
  1107 			$duotone_attr = $block['attrs']['style']['color']['duotone'];
       
  1108 			$is_preset    = is_string( $duotone_attr ) && self::is_preset( $duotone_attr );
       
  1109 			$is_css       = is_string( $duotone_attr ) && ! $is_preset;
       
  1110 			$is_custom    = is_array( $duotone_attr );
       
  1111 
       
  1112 			if ( $is_preset ) {
       
  1113 
       
  1114 				$slug         = self::get_slug_from_attribute( $duotone_attr ); // e.g. 'blue-orange'.
       
  1115 				$filter_id    = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-blue-orange'.
       
  1116 				$filter_value = self::get_css_var( $slug ); // e.g. 'var(--wp--preset--duotone--blue-orange)'.
       
  1117 
       
  1118 				// CSS custom property, SVG filter, and block CSS.
       
  1119 				self::enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value );
       
  1120 
       
  1121 			} elseif ( $is_css ) {
       
  1122 				$slug         = wp_unique_id( sanitize_key( $duotone_attr . '-' ) ); // e.g. 'unset-1'.
       
  1123 				$filter_id    = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-unset-1'.
       
  1124 				$filter_value = $duotone_attr; // e.g. 'unset'.
       
  1125 
       
  1126 				// Just block CSS.
       
  1127 				self::enqueue_block_css( $filter_id, $duotone_selector, $filter_value );
       
  1128 			} elseif ( $is_custom ) {
       
  1129 				$slug         = wp_unique_id( sanitize_key( implode( '-', $duotone_attr ) . '-' ) ); // e.g. '000000-ffffff-2'.
       
  1130 				$filter_id    = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-000000-ffffff-2'.
       
  1131 				$filter_value = self::get_filter_url( $filter_id ); // e.g. 'url(#wp-duotone-filter-000000-ffffff-2)'.
       
  1132 				$filter_data  = array(
       
  1133 					'slug'   => $slug,
       
  1134 					'colors' => $duotone_attr,
       
  1135 				);
       
  1136 
       
  1137 				// SVG filter and block CSS.
       
  1138 				self::enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $filter_data );
       
  1139 			}
       
  1140 		} elseif ( $has_global_styles_duotone ) {
       
  1141 			$slug         = $global_styles_block_names[ $block['blockName'] ]; // e.g. 'blue-orange'.
       
  1142 			$filter_id    = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-blue-orange'.
       
  1143 			$filter_value = self::get_css_var( $slug ); // e.g. 'var(--wp--preset--duotone--blue-orange)'.
       
  1144 
       
  1145 			// CSS custom property, SVG filter, and block CSS.
       
  1146 			self::enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value );
       
  1147 		}
       
  1148 
       
  1149 		// Like the layout hook, this assumes the hook only applies to blocks with a single wrapper.
       
  1150 		$tags = new WP_HTML_Tag_Processor( $block_content );
       
  1151 		if ( $tags->next_tag() ) {
       
  1152 			$tags->add_class( $filter_id );
       
  1153 		}
       
  1154 		return $tags->get_updated_html();
       
  1155 	}
       
  1156 
       
  1157 	/**
       
  1158 	 * Fixes the issue with our generated class name not being added to the block's outer container
       
  1159 	 * in classic themes due to gutenberg_restore_image_outer_container from layout block supports.
       
  1160 	 *
       
  1161 	 * @since 6.6.0
       
  1162 	 *
       
  1163 	 * @param string $block_content Rendered block content.
       
  1164 	 * @return string Filtered block content.
       
  1165 	 */
       
  1166 	public static function restore_image_outer_container( $block_content ) {
       
  1167 		if ( wp_theme_has_theme_json() ) {
       
  1168 			return $block_content;
       
  1169 		}
       
  1170 
       
  1171 		$tags          = new WP_HTML_Tag_Processor( $block_content );
       
  1172 		$wrapper_query = array(
       
  1173 			'tag_name'   => 'div',
       
  1174 			'class_name' => 'wp-block-image',
       
  1175 		);
       
  1176 		if ( ! $tags->next_tag( $wrapper_query ) ) {
       
  1177 			return $block_content;
       
  1178 		}
       
  1179 
       
  1180 		$tags->set_bookmark( 'wrapper-div' );
       
  1181 		$tags->next_tag();
       
  1182 
       
  1183 		$inner_classnames = explode( ' ', $tags->get_attribute( 'class' ) );
       
  1184 		foreach ( $inner_classnames as $classname ) {
       
  1185 			if ( 0 === strpos( $classname, 'wp-duotone' ) ) {
       
  1186 				$tags->remove_class( $classname );
       
  1187 				$tags->seek( 'wrapper-div' );
       
  1188 				$tags->add_class( $classname );
       
  1189 				break;
       
  1190 			}
       
  1191 		}
       
  1192 
       
  1193 		return $tags->get_updated_html();
       
  1194 	}
       
  1195 
       
  1196 	/**
       
  1197 	 * Appends the used block duotone filter declarations to the inline block supports CSS.
       
  1198 	 *
       
  1199 	 * Uses the declarations saved in earlier calls to self::enqueue_block_css.
       
  1200 	 *
       
  1201 	 * @since 6.3.0
       
  1202 	 */
       
  1203 	public static function output_block_styles() {
       
  1204 		if ( ! empty( self::$block_css_declarations ) ) {
       
  1205 			wp_style_engine_get_stylesheet_from_css_rules(
       
  1206 				self::$block_css_declarations,
       
  1207 				array(
       
  1208 					'context' => 'block-supports',
       
  1209 				)
       
  1210 			);
       
  1211 		}
       
  1212 	}
       
  1213 
       
  1214 	/**
       
  1215 	 * Appends the used global style duotone filter presets (CSS custom
       
  1216 	 * properties) to the inline global styles CSS.
       
  1217 	 *
       
  1218 	 * Uses the declarations saved in earlier calls to self::enqueue_global_styles_preset.
       
  1219 	 *
       
  1220 	 * @since 6.3.0
       
  1221 	 */
       
  1222 	public static function output_global_styles() {
       
  1223 		if ( ! empty( self::$used_global_styles_presets ) ) {
       
  1224 			wp_add_inline_style( 'global-styles', self::get_global_styles_presets( self::$used_global_styles_presets ) );
       
  1225 		}
       
  1226 	}
       
  1227 
       
  1228 	/**
       
  1229 	 * Outputs all necessary SVG for duotone filters, CSS for classic themes.
       
  1230 	 *
       
  1231 	 * Uses the declarations saved in earlier calls to self::enqueue_global_styles_preset
       
  1232 	 * and self::enqueue_custom_filter.
       
  1233 	 *
       
  1234 	 * @since 6.3.0
       
  1235 	 */
       
  1236 	public static function output_footer_assets() {
       
  1237 		if ( ! empty( self::$used_svg_filter_data ) ) {
       
  1238 			echo self::get_svg_definitions( self::$used_svg_filter_data );
       
  1239 		}
       
  1240 
       
  1241 		// In block themes, the CSS is added in the head via wp_add_inline_style in the wp_enqueue_scripts action.
       
  1242 		if ( ! wp_is_block_theme() ) {
       
  1243 			$style_tag_id = 'core-block-supports-duotone';
       
  1244 			wp_register_style( $style_tag_id, false );
       
  1245 			if ( ! empty( self::$used_global_styles_presets ) ) {
       
  1246 				wp_add_inline_style( $style_tag_id, self::get_global_styles_presets( self::$used_global_styles_presets ) );
       
  1247 			}
       
  1248 			if ( ! empty( self::$block_css_declarations ) ) {
       
  1249 				wp_add_inline_style( $style_tag_id, wp_style_engine_get_stylesheet_from_css_rules( self::$block_css_declarations ) );
       
  1250 			}
       
  1251 			wp_enqueue_style( $style_tag_id );
       
  1252 		}
       
  1253 	}
       
  1254 
       
  1255 	/**
       
  1256 	 * Adds the duotone SVGs and CSS custom properties to the editor settings.
       
  1257 	 *
       
  1258 	 * This allows the properties to be pulled in by the EditorStyles component
       
  1259 	 * in JS and rendered in the post editor.
       
  1260 	 *
       
  1261 	 * @since 6.3.0
       
  1262 	 *
       
  1263 	 * @param array $settings The block editor settings from the `block_editor_settings_all` filter.
       
  1264 	 * @return array The editor settings with duotone SVGs and CSS custom properties.
       
  1265 	 */
       
  1266 	public static function add_editor_settings( $settings ) {
       
  1267 		$global_styles_presets = self::get_all_global_styles_presets();
       
  1268 		if ( ! empty( $global_styles_presets ) ) {
       
  1269 			if ( ! isset( $settings['styles'] ) ) {
       
  1270 				$settings['styles'] = array();
       
  1271 			}
       
  1272 
       
  1273 			$settings['styles'][] = array(
       
  1274 				// For the editor we can add all of the presets by default.
       
  1275 				'assets'         => self::get_svg_definitions( $global_styles_presets ),
       
  1276 				// The 'svgs' type is new in 6.3 and requires the corresponding JS changes in the EditorStyles component to work.
       
  1277 				'__unstableType' => 'svgs',
       
  1278 				// These styles not generated by global styles, so this must be false or they will be stripped out in wp_get_block_editor_settings.
       
  1279 				'isGlobalStyles' => false,
       
  1280 			);
       
  1281 
       
  1282 			$settings['styles'][] = array(
       
  1283 				// For the editor we can add all of the presets by default.
       
  1284 				'css'            => self::get_global_styles_presets( $global_styles_presets ),
       
  1285 				// This must be set and must be something other than 'theme' or they will be stripped out in the post editor <Editor> component.
       
  1286 				'__unstableType' => 'presets',
       
  1287 				// These styles are no longer generated by global styles, so this must be false or they will be stripped out in wp_get_block_editor_settings.
       
  1288 				'isGlobalStyles' => false,
       
  1289 			);
       
  1290 		}
       
  1291 
       
  1292 		return $settings;
       
  1293 	}
       
  1294 
       
  1295 	/**
       
  1296 	 * Migrates the experimental duotone support flag to the stabilized location.
       
  1297 	 *
       
  1298 	 * This moves `supports.color.__experimentalDuotone` to `supports.filter.duotone`.
       
  1299 	 *
       
  1300 	 * @since 6.3.0
       
  1301 	 *
       
  1302 	 * @param array $settings Current block type settings.
       
  1303 	 * @param array $metadata Block metadata as read in via block.json.
       
  1304 	 * @return array Filtered block type settings.
       
  1305 	 */
       
  1306 	public static function migrate_experimental_duotone_support_flag( $settings, $metadata ) {
       
  1307 		$duotone_support = isset( $metadata['supports']['color']['__experimentalDuotone'] )
       
  1308 			? $metadata['supports']['color']['__experimentalDuotone']
       
  1309 			: null;
       
  1310 
       
  1311 		if ( ! isset( $settings['supports']['filter']['duotone'] ) && null !== $duotone_support ) {
       
  1312 			_wp_array_set( $settings, array( 'supports', 'filter', 'duotone' ), (bool) $duotone_support );
       
  1313 		}
       
  1314 
       
  1315 		return $settings;
       
  1316 	}
       
  1317 
       
  1318 	/**
       
  1319 	 * Gets the CSS filter property value from a preset.
       
  1320 	 *
       
  1321 	 * Exported for the deprecated function wp_get_duotone_filter_id().
       
  1322 	 *
       
  1323 	 * @internal
       
  1324 	 *
       
  1325 	 * @since 6.3.0
       
  1326 	 * @deprecated 6.3.0
       
  1327 	 *
       
  1328 	 * @param array $preset The duotone preset.
       
  1329 	 * @return string The CSS filter property value.
       
  1330 	 */
       
  1331 	public static function get_filter_css_property_value_from_preset( $preset ) {
       
  1332 		_deprecated_function( __FUNCTION__, '6.3.0' );
       
  1333 
       
  1334 		if ( isset( $preset['colors'] ) && is_string( $preset['colors'] ) ) {
       
  1335 			return $preset['colors'];
       
  1336 		}
       
  1337 
       
  1338 		$filter_id = self::get_filter_id_from_preset( $preset );
       
  1339 
       
  1340 		return 'url(#' . $filter_id . ')';
       
  1341 	}
       
  1342 }