From b6a787b8b16b0b3502a3f58b4f680ee37c21226b Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Mon, 28 Feb 2022 14:55:36 +0200 Subject: [PATCH] Webfonts API (#37140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * rebase - combining 46 commits to 1 and resolving lots of conflicts * Revert changes to resolver class * Override the parts we need * Rename file for a better description * Add webfonts to the parent theme * indentation fix * Update Global Styles endpoint to use Gutenberg callback and theme json resolver * rebase - combining 46 commits to 1 and resolving lots of conflicts * Add missing textdomain * add missing inline docs * inline doc (copy from 5.9 class) * Remove extra blank line * Remove non-applicable docs * This already exists in the parent * add full item * add missing global styles * remove multiples * update test * remove extra comma * Missed this in previous commit * Remove old docs (no longer applicable) * doc * revert 5.9 changes * get_merged_data no longer needs to be overriden * use static instead of self * simplify * add an explanation for when porting to wp-core * Revert adding fonts to the webfonts stylesheet * Update test * explain why we skip the provider * Move webfonts-API files to the compat/wordpress-6.0 folder * Remove out-of-date comment * Trigger a notice when an unregistered provider is used. * use error_log instead of trigger_error * typo Co-authored-by: André <583546+oandregal@users.noreply.github.com> Co-authored-by: Grant Kinney --- ...p => class-wp-theme-json-resolver-5-9.php} | 2 +- ...class-wp-theme-json-resolver-gutenberg.php | 92 ++++++ .../class-wp-webfonts-provider-local.php | 259 +++++++++++++++ .../class-wp-webfonts-provider.php | 68 ++++ .../wordpress-6.0/class-wp-webfonts.php | 294 ++++++++++++++++++ .../register-webfonts-from-theme-json.php | 149 +++++++++ lib/compat/wordpress-6.0/webfonts.php | 165 ++++++++++ lib/load.php | 8 +- .../class-wp-webfonts-local-provider-test.php | 149 +++++++++ phpunit/class-wp-webfonts-test.php | 114 +++++++ 10 files changed, 1298 insertions(+), 2 deletions(-) rename lib/compat/wordpress-5.9/{class-wp-theme-json-resolver-gutenberg.php => class-wp-theme-json-resolver-5-9.php} (99%) create mode 100644 lib/compat/wordpress-6.0/class-wp-theme-json-resolver-gutenberg.php create mode 100644 lib/compat/wordpress-6.0/class-wp-webfonts-provider-local.php create mode 100644 lib/compat/wordpress-6.0/class-wp-webfonts-provider.php create mode 100644 lib/compat/wordpress-6.0/class-wp-webfonts.php create mode 100644 lib/compat/wordpress-6.0/register-webfonts-from-theme-json.php create mode 100644 lib/compat/wordpress-6.0/webfonts.php create mode 100644 phpunit/class-wp-webfonts-local-provider-test.php create mode 100644 phpunit/class-wp-webfonts-test.php diff --git a/lib/compat/wordpress-5.9/class-wp-theme-json-resolver-gutenberg.php b/lib/compat/wordpress-5.9/class-wp-theme-json-resolver-5-9.php similarity index 99% rename from lib/compat/wordpress-5.9/class-wp-theme-json-resolver-gutenberg.php rename to lib/compat/wordpress-5.9/class-wp-theme-json-resolver-5-9.php index 21d546fb66891f..c16f64f46cee6d 100644 --- a/lib/compat/wordpress-5.9/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/compat/wordpress-5.9/class-wp-theme-json-resolver-5-9.php @@ -15,7 +15,7 @@ * * @access private */ -class WP_Theme_JSON_Resolver_Gutenberg { +class WP_Theme_JSON_Resolver_5_9 { /** * Container for data coming from core. diff --git a/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-gutenberg.php b/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-gutenberg.php new file mode 100644 index 00000000000000..a79c510141be32 --- /dev/null +++ b/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-gutenberg.php @@ -0,0 +1,92 @@ +get( 'TextDomain' ) ); + $theme_json_data = gutenberg_add_registered_webfonts_to_theme_json( $theme_json_data ); + static::$theme = new WP_Theme_JSON_Gutenberg( $theme_json_data ); + + if ( wp_get_theme()->parent() ) { + // Get parent theme.json. + $parent_theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json', true ) ); + $parent_theme_json_data = static::translate( $parent_theme_json_data, wp_get_theme()->parent()->get( 'TextDomain' ) ); + $parent_theme_json_data = gutenberg_add_registered_webfonts_to_theme_json( $parent_theme_json_data ); + $parent_theme = new WP_Theme_JSON_Gutenberg( $parent_theme_json_data ); + + // Merge the child theme.json into the parent theme.json. + // The child theme takes precedence over the parent. + $parent_theme->merge( static::$theme ); + static::$theme = $parent_theme; + } + } + + /* + * We want the presets and settings declared in theme.json + * to override the ones declared via theme supports. + * So we take theme supports, transform it to theme.json shape + * and merge the static::$theme upon that. + */ + $theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( get_default_block_editor_settings() ); + if ( ! static::theme_has_support() ) { + if ( ! isset( $theme_support_data['settings']['color'] ) ) { + $theme_support_data['settings']['color'] = array(); + } + + $default_palette = false; + if ( current_theme_supports( 'default-color-palette' ) ) { + $default_palette = true; + } + if ( ! isset( $theme_support_data['settings']['color']['palette'] ) ) { + // If the theme does not have any palette, we still want to show the core one. + $default_palette = true; + } + $theme_support_data['settings']['color']['defaultPalette'] = $default_palette; + + $default_gradients = false; + if ( current_theme_supports( 'default-gradient-presets' ) ) { + $default_gradients = true; + } + if ( ! isset( $theme_support_data['settings']['color']['gradients'] ) ) { + // If the theme does not have any gradients, we still want to show the core ones. + $default_gradients = true; + } + $theme_support_data['settings']['color']['defaultGradients'] = $default_gradients; + } + $with_theme_supports = new WP_Theme_JSON_Gutenberg( $theme_support_data ); + $with_theme_supports->merge( static::$theme ); + + return $with_theme_supports; + } +} diff --git a/lib/compat/wordpress-6.0/class-wp-webfonts-provider-local.php b/lib/compat/wordpress-6.0/class-wp-webfonts-provider-local.php new file mode 100644 index 00000000000000..6d1da877ae6d48 --- /dev/null +++ b/lib/compat/wordpress-6.0/class-wp-webfonts-provider-local.php @@ -0,0 +1,259 @@ + + * array( + * 'source-serif-pro.normal.200 900' => array( + * 'provider' => 'local', + * 'font_family' => 'Source Serif Pro', + * 'font_weight' => '200 900', + * 'font_style' => 'normal', + * 'src' => 'https://example.com/wp-content/themes/twentytwentytwo/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2' ), + * ), + * 'source-serif-pro.italic.400 900' => array( + * 'provider' => 'local', + * 'font_family' => 'Source Serif Pro', + * 'font_weight' => '200 900', + * 'font_style' => 'italic', + * 'src' => 'https://example.com/wp-content/themes/twentytwentytwo/assets/fonts/source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2' ), + * ), + * ) + * + * + * the following `@font-face` styles are generated and returned: + * + * + * @font-face{ + * font-family:"Source Serif Pro"; + * font-style:normal; + * font-weight:200 900; + * font-stretch:normal; + * src:local("Source Serif Pro"), url('/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2') format('woff2'); + * } + * @font-face{ + * font-family:"Source Serif Pro"; + * font-style:italic; + * font-weight:200 900; + * font-stretch:normal; + * src:local("Source Serif Pro"), url('/assets/fonts/source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2') format('woff2'); + * } + * + * + * @since 6.0.0 + * + * @return string The `@font-face` CSS. + */ + public function get_css() { + $css = ''; + + foreach ( $this->webfonts as $webfont ) { + // Order the webfont's `src` items to optimize for browser support. + $webfont = $this->order_src( $webfont ); + + // Build the @font-face CSS for this webfont. + $css .= '@font-face{' . $this->build_font_face_css( $webfont ) . '}'; + } + + return $css; + } + + /** + * Order `src` items to optimize for browser support. + * + * @since 6.0.0 + * + * @param array $webfont Webfont to process. + * @return array + */ + private function order_src( array $webfont ) { + if ( ! is_array( $webfont['src'] ) ) { + $webfont['src'] = (array) $webfont['src']; + } + + $src = array(); + $src_ordered = array(); + + foreach ( $webfont['src'] as $url ) { + // Add data URIs first. + if ( 0 === strpos( trim( $url ), 'data:' ) ) { + $src_ordered[] = array( + 'url' => $url, + 'format' => 'data', + ); + continue; + } + $format = pathinfo( $url, PATHINFO_EXTENSION ); + $src[ $format ] = $url; + } + + // Add woff2. + if ( ! empty( $src['woff2'] ) ) { + $src_ordered[] = array( + 'url' => $src['woff2'], + 'format' => 'woff2', + ); + } + + // Add woff. + if ( ! empty( $src['woff'] ) ) { + $src_ordered[] = array( + 'url' => $src['woff'], + 'format' => 'woff', + ); + } + + // Add ttf. + if ( ! empty( $src['ttf'] ) ) { + $src_ordered[] = array( + 'url' => $src['ttf'], + 'format' => 'truetype', + ); + } + + // Add eot. + if ( ! empty( $src['eot'] ) ) { + $src_ordered[] = array( + 'url' => $src['eot'], + 'format' => 'embedded-opentype', + ); + } + + // Add otf. + if ( ! empty( $src['otf'] ) ) { + $src_ordered[] = array( + 'url' => $src['otf'], + 'format' => 'opentype', + ); + } + $webfont['src'] = $src_ordered; + + return $webfont; + } + + /** + * Builds the font-family's CSS. + * + * @since 6.0.0 + * + * @param array $webfont Webfont to process. + * @return string This font-family's CSS. + */ + private function build_font_face_css( array $webfont ) { + $css = ''; + + // Wrap font-family in quotes if it contains spaces. + if ( + false !== strpos( $webfont['font-family'], ' ' ) && + false === strpos( $webfont['font-family'], '"' ) && + false === strpos( $webfont['font-family'], "'" ) + ) { + $webfont['font-family'] = '"' . $webfont['font-family'] . '"'; + } + + foreach ( $webfont as $key => $value ) { + + // Skip "provider", since it's for internal API use, + // and not a valid CSS property. + if ( 'provider' === $key ) { + continue; + } + + // Compile the "src" parameter. + if ( 'src' === $key ) { + $value = $this->compile_src( $webfont['font-family'], $value ); + } + + // If font-variation-settings is an array, convert it to a string. + if ( 'font-variation-settings' === $key && is_array( $value ) ) { + $value = $this->compile_variations( $value ); + } + + if ( ! empty( $value ) ) { + $css .= "$key:$value;"; + } + } + + return $css; + } + + /** + * Compiles the `src` into valid CSS. + * + * @since 6.0.0 + * + * @param string $font_family Font family. + * @param array $value Value to process. + * @return string The CSS. + */ + private function compile_src( $font_family, array $value ) { + $src = "local($font_family)"; + + foreach ( $value as $item ) { + + if ( 0 === strpos( $item['url'], get_site_url() ) ) { + $item['url'] = wp_make_link_relative( $item['url'] ); + } + + $src .= ( 'data' === $item['format'] ) + ? ", url({$item['url']})" + : ", url('{$item['url']}') format('{$item['format']}')"; + } + return $src; + } + + /** + * Compiles the font variation settings. + * + * @since 6.0.0 + * + * @param array $font_variation_settings Array of font variation settings. + * @return string The CSS. + */ + private function compile_variations( array $font_variation_settings ) { + $variations = ''; + + foreach ( $font_variation_settings as $key => $value ) { + $variations .= "$key $value"; + } + + return $variations; + } +} diff --git a/lib/compat/wordpress-6.0/class-wp-webfonts-provider.php b/lib/compat/wordpress-6.0/class-wp-webfonts-provider.php new file mode 100644 index 00000000000000..96b12547986425 --- /dev/null +++ b/lib/compat/wordpress-6.0/class-wp-webfonts-provider.php @@ -0,0 +1,68 @@ +webfonts = $webfonts; + } + + /** + * Gets the `@font-face` CSS for the provider's webfonts. + * + * This method is where the provider does it processing to build the + * needed `@font-face` CSS for all of its webfonts. Specifics of how + * this processing is done is contained in each provider. + * + * @since 6.0.0 + * + * @return string The `@font-face` CSS. + */ + abstract public function get_css(); +} diff --git a/lib/compat/wordpress-6.0/class-wp-webfonts.php b/lib/compat/wordpress-6.0/class-wp-webfonts.php new file mode 100644 index 00000000000000..9b5114b7452bb2 --- /dev/null +++ b/lib/compat/wordpress-6.0/class-wp-webfonts.php @@ -0,0 +1,294 @@ +register_provider( 'local', 'WP_Webfonts_Provider_Local' ); + + // Register callback to generate and enqueue styles. + if ( did_action( 'wp_enqueue_scripts' ) ) { + $this->stylesheet_handle = 'webfonts-footer'; + $hook = 'wp_print_footer_scripts'; + } else { + $this->stylesheet_handle = 'webfonts'; + $hook = 'wp_enqueue_scripts'; + } + add_action( $hook, array( $this, 'generate_and_enqueue_styles' ) ); + + // Enqueue webfonts in the block editor. + add_action( 'admin_init', array( $this, 'generate_and_enqueue_editor_styles' ) ); + } + + /** + * Get the list of fonts. + * + * @return array + */ + public function get_fonts() { + return self::$webfonts; + } + + /** + * Get the list of providers. + * + * @return array + */ + public function get_providers() { + return self::$providers; + } + + /** + * Register a webfont. + * + * @param array $font The font arguments. + */ + public function register_font( $font ) { + $font = $this->validate_font( $font ); + if ( $font ) { + $id = $this->get_font_id( $font ); + self::$webfonts[ $id ] = $font; + } + } + + /** + * Get the font ID. + * + * @param array $font The font arguments. + * @return string + */ + public function get_font_id( $font ) { + return sanitize_title( "{$font['font-family']}-{$font['font-weight']}-{$font['font-style']}-{$font['provider']}" ); + } + + /** + * Validate a font. + * + * @param array $font The font arguments. + * + * @return array|false The validated font arguments, or false if the font is invalid. + */ + public function validate_font( $font ) { + $font = wp_parse_args( + $font, + array( + 'provider' => 'local', + 'font-family' => '', + 'font-style' => 'normal', + 'font-weight' => '400', + 'font-display' => 'fallback', + ) + ); + + // Check the font-family. + if ( empty( $font['font-family'] ) || ! is_string( $font['font-family'] ) ) { + trigger_error( __( 'Webfont font family must be a non-empty string.', 'gutenberg' ) ); + return false; + } + + // Local fonts need a "src". + if ( 'local' === $font['provider'] ) { + // Make sure that local fonts have 'src' defined. + if ( empty( $font['src'] ) || ( ! is_string( $font['src'] ) && ! is_array( $font['src'] ) ) ) { + trigger_error( __( 'Webfont src must be a non-empty string or an array of strings.', 'gutenberg' ) ); + return false; + } + } + + // Validate the 'src' property. + if ( ! empty( $font['src'] ) ) { + foreach ( (array) $font['src'] as $src ) { + if ( empty( $src ) || ! is_string( $src ) ) { + trigger_error( __( 'Each webfont src must be a non-empty string.', 'gutenberg' ) ); + return false; + } + } + } + + // Check the font-weight. + if ( ! is_string( $font['font-weight'] ) && ! is_int( $font['font-weight'] ) ) { + trigger_error( __( 'Webfont font weight must be a properly formatted string or integer.', 'gutenberg' ) ); + return false; + } + + // Check the font-display. + if ( ! in_array( $font['font-display'], array( 'auto', 'block', 'fallback', 'swap' ), true ) ) { + $font['font-display'] = 'fallback'; + } + + $valid_props = array( + 'ascend-override', + 'descend-override', + 'font-display', + 'font-family', + 'font-stretch', + 'font-style', + 'font-weight', + 'font-variant', + 'font-feature-settings', + 'font-variation-settings', + 'line-gap-override', + 'size-adjust', + 'src', + 'unicode-range', + + // Exceptions. + 'provider', + ); + + foreach ( $font as $prop => $value ) { + if ( ! in_array( $prop, $valid_props, true ) ) { + unset( $font[ $prop ] ); + } + } + + return $font; + } + + /** + * Register a provider. + * + * @param string $provider The provider name. + * @param string $class The provider class name. + * + * @return bool Whether the provider was registered successfully. + */ + public function register_provider( $provider, $class ) { + if ( empty( $provider ) || empty( $class ) ) { + return false; + } + self::$providers[ $provider ] = $class; + return true; + } + + /** + * Generate and enqueue webfonts styles. + */ + public function generate_and_enqueue_styles() { + // Generate the styles. + $styles = $this->generate_styles(); + + // Bail out if there are no styles to enqueue. + if ( '' === $styles ) { + return; + } + + // Enqueue the stylesheet. + wp_register_style( $this->stylesheet_handle, '' ); + wp_enqueue_style( $this->stylesheet_handle ); + + // Add the styles to the stylesheet. + wp_add_inline_style( $this->stylesheet_handle, $styles ); + } + + /** + * Generate and enqueue editor styles. + */ + public function generate_and_enqueue_editor_styles() { + // Generate the styles. + $styles = $this->generate_styles(); + + // Bail out if there are no styles to enqueue. + if ( '' === $styles ) { + return; + } + + wp_add_inline_style( 'wp-block-library', $styles ); + } + + /** + * Generate styles for webfonts. + * + * @since 6.0.0 + * + * @return string $styles Generated styles. + */ + public function generate_styles() { + $styles = ''; + $providers = $this->get_providers(); + + // Group webfonts by provider. + $webfonts_by_provider = array(); + $registered_webfonts = $this->get_fonts(); + foreach ( $registered_webfonts as $id => $webfont ) { + $provider = $webfont['provider']; + if ( ! isset( $providers[ $provider ] ) ) { + /* translators: %s is the provider name. */ + error_log( sprintf( __( 'Webfont provider "%s" is not registered.', 'gutenberg' ), $provider ) ); + continue; + } + $webfonts_by_provider[ $provider ] = isset( $webfonts_by_provider[ $provider ] ) ? $webfonts_by_provider[ $provider ] : array(); + $webfonts_by_provider[ $provider ][ $id ] = $webfont; + } + + /* + * Loop through each of the providers to get the CSS for their respective webfonts + * to incrementally generate the collective styles for all of them. + */ + foreach ( $providers as $provider_id => $provider_class ) { + + // Bail out if the provider class does not exist. + if ( ! class_exists( $provider_class ) ) { + /* translators: %s is the provider name. */ + error_log( sprintf( __( 'Webfont provider "%s" is not registered.', 'gutenberg' ), $provider_id ) ); + continue; + } + + $provider_webfonts = isset( $webfonts_by_provider[ $provider_id ] ) + ? $webfonts_by_provider[ $provider_id ] + : array(); + + // If there are no registered webfonts for this provider, skip it. + if ( empty( $provider_webfonts ) ) { + continue; + } + + /* + * Process the webfonts by first passing them to the provider via `set_webfonts()` + * and then getting the CSS from the provider. + */ + $provider = new $provider_class(); + $provider->set_webfonts( $provider_webfonts ); + $styles .= $provider->get_css(); + } + + return $styles; + } +} diff --git a/lib/compat/wordpress-6.0/register-webfonts-from-theme-json.php b/lib/compat/wordpress-6.0/register-webfonts-from-theme-json.php new file mode 100644 index 00000000000000..f18c301764c710 --- /dev/null +++ b/lib/compat/wordpress-6.0/register-webfonts-from-theme-json.php @@ -0,0 +1,149 @@ +get_settings(); + + // Bail out early if there are no settings for webfonts. + if ( empty( $theme_settings['typography'] ) || empty( $theme_settings['typography']['fontFamilies'] ) ) { + return; + } + + $webfonts = array(); + + // Look for fontFamilies. + foreach ( $theme_settings['typography']['fontFamilies'] as $font_families ) { + foreach ( $font_families as $font_family ) { + + // Skip if fontFace is not defined. + if ( empty( $font_family['fontFace'] ) ) { + continue; + } + + $font_family['fontFace'] = (array) $font_family['fontFace']; + + foreach ( $font_family['fontFace'] as $font_face ) { + // Check if webfonts have a "src" param, and if they do account for the use of "file:./". + if ( ! empty( $font_face['src'] ) ) { + $font_face['src'] = (array) $font_face['src']; + + foreach ( $font_face['src'] as $src_key => $url ) { + // Tweak the URL to be relative to the theme root. + if ( 0 !== strpos( $url, 'file:./' ) ) { + continue; + } + $font_face['src'][ $src_key ] = get_theme_file_uri( str_replace( 'file:./', '', $url ) ); + } + } + + // Convert keys to kebab-case. + foreach ( $font_face as $property => $value ) { + $kebab_case = _wp_to_kebab_case( $property ); + $font_face[ $kebab_case ] = $value; + if ( $kebab_case !== $property ) { + unset( $font_face[ $property ] ); + } + } + + $webfonts[] = $font_face; + } + } + } + foreach ( $webfonts as $webfont ) { + wp_webfonts()->register_font( $webfont ); + } +} + +/** + * Add missing fonts data to the global styles. + * + * @param array $data The global styles. + * + * @return array The global styles with missing fonts data. + */ +function gutenberg_add_registered_webfonts_to_theme_json( $data ) { + $font_families_registered = wp_webfonts()->get_fonts(); + $font_families_from_theme = array(); + if ( ! empty( $data['settings'] ) && ! empty( $data['settings']['typography'] ) && ! empty( $data['settings']['typography']['fontFamilies'] ) ) { + $font_families_from_theme = $data['settings']['typography']['fontFamilies']; + } + + /** + * Helper to get an array of the font-families. + * + * @param array $families_data The font-families data. + * + * @return array The font-families array. + */ + $get_families = function( $families_data ) { + $families = array(); + foreach ( $families_data as $family ) { + if ( isset( $family['font-family'] ) ) { + $families[] = $family['font-family']; + } elseif ( isset( $family['fontFamily'] ) ) { + $families[] = $family['fontFamily']; + } + } + + // Micro-optimization: Use array_flip( array_flip( $array ) ) + // instead of array_unique( $array ) because it's faster. + // The result is the same. + return array_flip( array_flip( $families ) ); + }; + + // Diff the arrays to find the missing fonts. + $to_add = array_diff( + $get_families( $font_families_registered ), + $get_families( $font_families_from_theme ) + ); + + // Bail out early if there are no missing fonts. + if ( empty( $to_add ) ) { + return $data; + } + + // Make sure the path to settings.typography.fontFamilies.theme exists + // before adding missing fonts. + if ( empty( $data['settings'] ) ) { + $data['settings'] = array(); + } + if ( empty( $data['settings']['typography'] ) ) { + $data['settings']['typography'] = array(); + } + if ( empty( $data['settings']['typography']['fontFamilies'] ) ) { + $data['settings']['typography']['fontFamilies'] = array(); + } + + // Add missing fonts. + foreach ( $to_add as $family ) { + $font_face = array(); + foreach ( $font_families_registered as $font_family ) { + if ( $family !== $font_family['font-family'] ) { + continue; + } + $camel_cased = array(); + foreach ( $font_family as $key => $value ) { + $camel_cased[ lcfirst( str_replace( '-', '', ucwords( $key, '-' ) ) ) ] = $value; + } + $font_face[] = $camel_cased; + } + $data['settings']['typography']['fontFamilies'][] = array( + 'fontFamily' => false !== strpos( $family, ' ' ) ? "'{$family}'" : $family, + 'name' => $family, + 'slug' => sanitize_title( $family ), + 'fontFace' => $font_face, + ); + } + + return $data; +} + +add_action( 'wp_loaded', 'gutenberg_register_webfonts_from_theme_json' ); diff --git a/lib/compat/wordpress-6.0/webfonts.php b/lib/compat/wordpress-6.0/webfonts.php new file mode 100644 index 00000000000000..2bc7a714c16727 --- /dev/null +++ b/lib/compat/wordpress-6.0/webfonts.php @@ -0,0 +1,165 @@ +init(); + } + + return $instance; +} + +/** + * Registers a collection of webfonts. + * + * Example of how to register Source Serif Pro font with font-weight range of 200-900 + * and font-style of normal and italic: + * + * If the font files are contained within the theme: + * + * wp_register_webfonts( + * array( + * array( + * 'provider' => 'local', + * 'font_family' => 'Source Serif Pro', + * 'font_weight' => '200 900', + * 'font_style' => 'normal', + * 'src' => get_theme_file_uri( 'assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2' ), + * ), + * array( + * 'provider' => 'local', + * 'font_family' => 'Source Serif Pro', + * 'font_weight' => '200 900', + * 'font_style' => 'italic', + * 'src' => get_theme_file_uri( 'assets/fonts/source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2' ), + * ), + * ) + * ); + * + * + * Webfonts should be registered in the `after_setup_theme` hook. + * + * @since 6.0.0 + * + * @param array $webfonts Webfonts to be registered. + * This contains an array of webfonts to be registered. + * Each webfont is an array. + * See {@see WP_Webfonts_Registry::register()} for a list of + * supported arguments for each webfont. + */ +function wp_register_webfonts( array $webfonts = array() ) { + foreach ( $webfonts as $webfont ) { + wp_register_webfont( $webfont ); + } +} + +/** + * Registers a single webfont. + * + * Example of how to register Source Serif Pro font with font-weight range of 200-900: + * + * If the font file is contained within the theme: + * ``` + * wp_register_webfont( + * array( + * 'provider' => 'local', + * 'font_family' => 'Source Serif Pro', + * 'font_weight' => '200 900', + * 'font_style' => 'normal', + * 'src' => get_theme_file_uri( 'assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2' ), + * ) + * ); + * ``` + * + * @since 6.0.0 + * + * @param array $webfont Webfont to be registered. + * See {@see WP_Webfonts_Registry::register()} for a list of supported arguments. + */ +function wp_register_webfont( array $webfont ) { + wp_webfonts()->register_font( $webfont ); +} + +/** + * Registers a custom font service provider. + * + * A webfont provider contains the business logic for how to + * interact with a remote font service and how to generate + * the `@font-face` styles for that remote service. + * + * How to register a custom font service provider: + * 1. Load its class file into memory before registration. + * 2. Pass the class' name to this function. + * + * For example, for a class named `My_Custom_Font_Service_Provider`: + * ``` + * wp_register_webfont_provider( My_Custom_Font_Service_Provider::class ); + * ``` + * + * @since 6.0.0 + * + * @param string $name The provider's name. + * @param string $classname The provider's class name. + * The class should be a child of `WP_Webfonts_Provider`. + * See {@see WP_Webfonts_Provider}. + * + * @return bool True when registered. False when provider does not exist. + */ +function wp_register_webfont_provider( $name, $classname ) { + return wp_webfonts()->register_provider( $name, $classname ); +} + +/** + * Gets all registered providers. + * + * Return an array of providers, each keyed by their unique + * ID (i.e. the `$id` property in the provider's object) with + * an instance of the provider (object): + * ID => provider instance + * + * Each provider contains the business logic for how to + * process its specific font service (i.e. local or remote) + * and how to generate the `@font-face` styles for its service. + * + * @since 6.0.0 + * + * @return WP_Webfonts_Provider[] All registered providers, + * each keyed by their unique ID. + */ +function wp_get_webfont_providers() { + return wp_webfonts()->get_providers(); +} + +/** + * Add webfonts mime types. + */ +add_filter( + 'mime_types', + function( $mime_types ) { + // Webfonts formats. + $mime_types['woff2'] = 'font/woff2'; + $mime_types['woff'] = 'font/woff'; + $mime_types['ttf'] = 'font/ttf'; + $mime_types['eot'] = 'application/vnd.ms-fontobject'; + $mime_types['otf'] = 'application/x-font-opentype'; + + return $mime_types; + } +); diff --git a/lib/load.php b/lib/load.php index 4ab6ccbf8a46c2..b008f233700714 100644 --- a/lib/load.php +++ b/lib/load.php @@ -84,7 +84,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/editor-settings.php'; require __DIR__ . '/compat/wordpress-5.9/class-wp-theme-json-schema-gutenberg.php'; require __DIR__ . '/compat/wordpress-5.9/class-wp-theme-json-5-9.php'; -require __DIR__ . '/compat/wordpress-5.9/class-wp-theme-json-resolver-gutenberg.php'; +require __DIR__ . '/compat/wordpress-5.9/class-wp-theme-json-resolver-5-9.php'; require __DIR__ . '/compat/wordpress-5.9/theme.php'; require __DIR__ . '/compat/wordpress-5.9/admin-menu.php'; require __DIR__ . '/full-site-editing/edit-site-page.php'; @@ -101,6 +101,12 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php'; require __DIR__ . '/compat/wordpress-6.0/rest-api.php'; require __DIR__ . '/compat/wordpress-6.0/block-patterns.php'; +require __DIR__ . '/compat/wordpress-6.0/register-webfonts-from-theme-json.php'; +require __DIR__ . '/compat/wordpress-6.0/class-wp-theme-json-resolver-gutenberg.php'; +require __DIR__ . '/compat/wordpress-6.0/class-wp-webfonts.php'; +require __DIR__ . '/compat/wordpress-6.0/class-wp-webfonts-provider.php'; +require __DIR__ . '/compat/wordpress-6.0/class-wp-webfonts-provider-local.php'; +require __DIR__ . '/compat/wordpress-6.0/webfonts.php'; require __DIR__ . '/compat/experimental/blocks.php'; require __DIR__ . '/blocks.php'; diff --git a/phpunit/class-wp-webfonts-local-provider-test.php b/phpunit/class-wp-webfonts-local-provider-test.php new file mode 100644 index 00000000000000..69892d277bc227 --- /dev/null +++ b/phpunit/class-wp-webfonts-local-provider-test.php @@ -0,0 +1,149 @@ +provider = new WP_Webfonts_Provider_Local(); + + $this->set_up_theme(); + } + + /** + * Local `src` paths to need to be relative to the theme. This method sets up the + * `wp-content/themes/` directory to ensure consistency when running tests. + */ + private function set_up_theme() { + $this->theme_root = realpath( DIR_TESTDATA . '/themedir1' ); + $this->orig_theme_dir = $GLOBALS['wp_theme_directories']; + $GLOBALS['wp_theme_directories'] = array( $this->theme_root ); + + $theme_root_callback = function () { + return $this->theme_root; + }; + add_filter( 'theme_root', $theme_root_callback ); + add_filter( 'stylesheet_root', $theme_root_callback ); + add_filter( 'template_root', $theme_root_callback ); + + // Clear caches. + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + } + + function tear_down() { + // Restore the original theme directory setup. + $GLOBALS['wp_theme_directories'] = $this->orig_theme_dir; + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + + parent::tear_down(); + } + + /** + * @covers WP_Webfonts_Provider_Local::set_webfonts + */ + public function test_set_webfonts() { + $webfonts = array( + 'source-serif-pro-200-900-normal-local' => array( + 'provider' => 'local', + 'font-family' => 'Source Serif Pro', + 'font-style' => 'normal', + 'font-weight' => '200 900', + 'font-stretch' => 'normal', + 'src' => 'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2', + ), + 'source-serif-pro-200-900-italic-local' => array( + 'provider' => 'local', + 'font-family' => 'Source Serif Pro', + 'font-style' => 'italic', + 'font-weight' => '200 900', + 'font-stretch' => 'normal', + 'src' => 'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2', + ), + ); + + $this->provider->set_webfonts( $webfonts ); + + $property = $this->get_webfonts_property(); + $this->assertSame( $webfonts, $property->getValue( $this->provider ) ); + } + + /** + * @covers WP_Webfonts_Provider_Local::get_css + * + * @dataProvider data_get_css + * + * @param array $webfonts Prepared webfonts (to store in WP_Webfonts_Provider_Local::$webfonts property). + * @param string $expected Expected CSS. + */ + public function test_get_css( array $webfonts, $expected ) { + $property = $this->get_webfonts_property(); + $property->setValue( $this->provider, $webfonts ); + + $this->assertSame( $expected, $this->provider->get_css() ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_css() { + return array( + 'truetype format' => array( + 'webfonts' => array( + 'open-sans-bold-italic-local' => array( + 'provider' => 'local', + 'font-family' => 'Open Sans', + 'font-style' => 'italic', + 'font-weight' => 'bold', + 'src' => 'http://example.org/assets/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf', + ), + ), + 'expected' => << array( + 'webfonts' => array( + 'source-serif-pro-200-900-normal-local' => array( + 'provider' => 'local', + 'font-family' => 'Source Serif Pro', + 'font-style' => 'normal', + 'font-weight' => '200 900', + 'font-stretch' => 'normal', + 'src' => 'http://example.org/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2', + ), + 'source-serif-pro-400-900-italic-local' => array( + 'provider' => 'local', + 'font-family' => 'Source Serif Pro', + 'font-style' => 'italic', + 'font-weight' => '200 900', + 'font-stretch' => 'normal', + 'src' => 'http://example.org/assets/fonts/source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2', + ), + ), + 'expected' => <<provider, 'webfonts' ); + $property->setAccessible( true ); + + return $property; + } +} diff --git a/phpunit/class-wp-webfonts-test.php b/phpunit/class-wp-webfonts-test.php new file mode 100644 index 00000000000000..239f2fae59fb12 --- /dev/null +++ b/phpunit/class-wp-webfonts-test.php @@ -0,0 +1,114 @@ + 'local', + 'font-family' => 'Source Serif Pro', + 'font-style' => 'normal', + 'font-weight' => '200 900', + 'font-stretch' => 'normal', + 'src' => 'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2', + 'font-display' => 'fallback', + ), + array( + 'provider' => 'local', + 'font-family' => 'Source Serif Pro', + 'font-style' => 'italic', + 'font-weight' => '200 900', + 'font-stretch' => 'normal', + 'src' => 'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2', + 'font-display' => 'fallback', + ), + ); + + $expected = array( + 'source-serif-pro-200-900-normal-local' => $fonts[0], + 'source-serif-pro-200-900-italic-local' => $fonts[1], + ); + + wp_register_webfonts( $fonts ); + $this->assertEquals( $expected, wp_webfonts()->get_fonts() ); + } + + /** + * @covers wp_register_webfont + * @covers WP_Webfonts::register_provider + * @covers WP_Webfonts::get_providers + */ + public function test_get_providers() { + wp_register_webfont_provider( 'test-provider', 'Test_Provider' ); + $this->assertEquals( + array( + 'local' => 'WP_Webfonts_Provider_Local', + 'test-provider' => 'Test_Provider', + ), + wp_get_webfont_providers() + ); + } + + /** + * @covers WP_Webfonts::validate_font + */ + public function test_validate_font() { + // Test empty array. + $this->assertFalse( wp_webfonts()->validate_font( array() ) ); + + $font = array( + 'font-family' => 'Test Font 1', + 'src' => 'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2', + ); + + // Test missing provider fallback to local. + $this->assertEquals( 'local', wp_webfonts()->validate_font( $font )['provider'] ); + + // Test missing font-weight fallback to 400. + $this->assertEquals( '400', wp_webfonts()->validate_font( $font )['font-weight'] ); + + // Test missing font-style fallback to normal. + $this->assertEquals( 'normal', wp_webfonts()->validate_font( $font )['font-style'] ); + + // Test missing font-display fallback to fallback. + $this->assertEquals( 'fallback', wp_webfonts()->validate_font( $font )['font-display'] ); + + // Test local font with missing "src". + $this->assertFalse( wp_webfonts()->validate_font( array( 'font-family' => 'Test Font 2' ) ) ); + + // Test valid src URL, without a protocol. + $font['src'] = '//example.com/SourceSerif4Variable-Roman.ttf.woff2'; + $this->assertEquals( wp_webfonts()->validate_font( $font )['src'], $font['src'] ); + + // Test font-weight. + $font_weights = array( 100, '100', '100 900', 'normal' ); + foreach ( $font_weights as $value ) { + $font['font-weight'] = $value; + $this->assertEquals( wp_webfonts()->validate_font( $font )['font-weight'], $value ); + } + + // Test that invalid keys get removed from the font. + $font['invalid-key'] = 'invalid'; + $this->assertArrayNotHasKey( 'invalid-key', wp_webfonts()->validate_font( $font ) ); + } + + /** + * @covers WP_Webfonts::generate_styles + */ + public function test_generate_styles() { + $this->assertEquals( + '@font-face{font-family:"Source Serif Pro";font-style:normal;font-weight:200 900;font-display:fallback;font-stretch:normal;src:local("Source Serif Pro"), url(\'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2\') format(\'woff2\');}@font-face{font-family:"Source Serif Pro";font-style:italic;font-weight:200 900;font-display:fallback;font-stretch:normal;src:local("Source Serif Pro"), url(\'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2\') format(\'woff2\');}', + wp_webfonts()->generate_styles() + ); + } +}