= 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. * * @see https://github.com/bgrins/TinyColor * * @since 5.8.0 * @access private * * @param array $rgb_color RGB object. * @return array Rounded and converted RGB object. */ function wp_tinycolor_rgb_to_rgb( $rgb_color ) { return array( 'r' => wp_tinycolor_bound01( $rgb_color['r'], 255 ) * 255, 'g' => wp_tinycolor_bound01( $rgb_color['g'], 255 ) * 255, 'b' => wp_tinycolor_bound01( $rgb_color['b'], 255 ) * 255, ); } /** * Helper function for hsl to rgb conversion. * * Direct port of TinyColor's function, lightly simplified to maintain * consistency with TinyColor. * * @see https://github.com/bgrins/TinyColor * * @since 5.8.0 * @access private * * @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 ) { if ( $t < 0 ) { $t += 1; } if ( $t > 1 ) { $t -= 1; } if ( $t < 1 / 6 ) { return $p + ( $q - $p ) * 6 * $t; } if ( $t < 1 / 2 ) { return $q; } if ( $t < 2 / 3 ) { return $p + ( $q - $p ) * ( 2 / 3 - $t ) * 6; } return $p; } /** * 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. * * @see https://github.com/bgrins/TinyColor * * @since 5.8.0 * @access private * * @param array $hsl_color HSL object. * @return array Rounded and converted RGB object. */ function wp_tinycolor_hsl_to_rgb( $hsl_color ) { $h = wp_tinycolor_bound01( $hsl_color['h'], 360 ); $s = wp_tinycolor_bound01( $hsl_color['s'], 100 ); $l = wp_tinycolor_bound01( $hsl_color['l'], 100 ); if ( 0 === $s ) { // Achromatic. $r = $l; $g = $l; $b = $l; } else { $q = $l < 0.5 ? $l * ( 1 + $s ) : $l + $s - $l * $s; $p = 2 * $l - $q; $r = wp_tinycolor_hue_to_rgb( $p, $q, $h + 1 / 3 ); $g = wp_tinycolor_hue_to_rgb( $p, $q, $h ); $b = wp_tinycolor_hue_to_rgb( $p, $q, $h - 1 / 3 ); } return array( 'r' => $r * 255, 'g' => $g * 255, 'b' => $b * 255, ); } /** * 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. * * Direct port of TinyColor's function, lightly simplified to maintain * consistency with TinyColor. * * @see https://github.com/bgrins/TinyColor * @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 ) { $color_str = strtolower( trim( $color_str ) ); $css_integer = '[-\\+]?\\d+%?'; $css_number = '[-\\+]?\\d*\\.\\d+%?'; $css_unit = '(?:' . $css_number . ')|(?:' . $css_integer . ')'; $permissive_match3 = '[\\s|\\(]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')\\s*\\)?'; $permissive_match4 = '[\\s|\\(]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')\\s*\\)?'; $rgb_regexp = '/^rgb' . $permissive_match3 . '$/'; if ( preg_match( $rgb_regexp, $color_str, $match ) ) { $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], ) ); $rgb['a'] = _wp_tinycolor_bound_alpha( $match[4] ); return $rgb; } $hsl_regexp = '/^hsl' . $permissive_match3 . '$/'; if ( preg_match( $hsl_regexp, $color_str, $match ) ) { $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 ) ) { $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 ) ) { $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 ) ) { $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 ) ) { $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; } /* * 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 ); $svg = trim( $svg ); } return $svg; } /** * Registers the style and colors block attributes for block types that support it. * * @since 5.8.0 * @access private * * @param WP_Block_Type $block_type Block Type. */ function wp_register_duotone_support( $block_type ) { $has_duotone_support = false; if ( property_exists( $block_type, 'supports' ) ) { $has_duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false ); } if ( $has_duotone_support ) { if ( ! $block_type->attributes ) { $block_type->attributes = array(); } if ( ! array_key_exists( 'style', $block_type->attributes ) ) { $block_type->attributes['style'] = array( 'type' => 'object', ); } } } /** * Render out the duotone stylesheet and SVG. * * @since 5.8.0 * @access private * * @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 ) { $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); $duotone_support = false; if ( $block_type && property_exists( $block_type, 'supports' ) ) { $duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false ); } $has_duotone_attribute = isset( $block['attrs']['style']['color']['duotone'] ); if ( ! $duotone_support || ! $has_duotone_attribute ) { return $block_content; } $filter_preset = array( 'slug' => wp_unique_id( sanitize_key( implode( '-', $block['attrs']['style']['color']['duotone'] ) . '-' ) ), 'colors' => $block['attrs']['style']['color']['duotone'], ); $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 ); $scope = '.' . $filter_id; $selectors = explode( ',', $duotone_support ); $scoped = array(); foreach ( $selectors as $sel ) { $scoped[] = $scope . ' ' . trim( $sel ); } $selector = implode( ', ', $scoped ); // !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;}'; 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; /* * 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. '', wp_json_encode( $selector ) ); } } ); // Like the layout hook, this assumes the hook only applies to blocks with a single wrapper. return preg_replace( '/' . preg_quote( 'class="', '/' ) . '/', 'class="' . $filter_id . ' ', $block_content, 1 ); } // Register the block support. WP_Block_Supports::get_instance()->register( 'duotone', array( 'register_attribute' => 'wp_register_duotone_support', ) ); add_filter( 'render_block', 'wp_render_duotone_support', 10, 2 );