diff --git a/lib/experimental/add-registered-webfonts-to-theme-json.php b/lib/experimental/add-registered-webfonts-to-theme-json.php new file mode 100644 index 00000000000000..e7d0297fdaaf83 --- /dev/null +++ b/lib/experimental/add-registered-webfonts-to-theme-json.php @@ -0,0 +1,114 @@ +get_all_webfonts(); + + if ( empty( $registered_font_families ) ) { + 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(); + } + + /* + * Map the font families by slug to their corresponding index + * in theme.json, so we can avoid looping theme.json looking for + * font families every time we want to register a face. + */ + $font_family_indexes_in_theme_json = array(); + + foreach ( $data['settings']['typography']['fontFamilies'] as $index => $family ) { + $font_family_indexes_in_theme_json[ wp_webfonts()->get_font_slug( $family ) ] = $index; + } + + /** + * Transforms the keys in the given array to camelCase. + * + * @param array $to_transform The array to transform. + * @return array Given array with camelCase keys. + */ + $array_keys_to_camel_case = function( array $to_transform ) { + $camel_cased_array = array(); + + foreach ( $to_transform as $key => $value ) { + $camel_cased_array[ lcfirst( str_replace( '-', '', ucwords( $key, '-' ) ) ) ] = $value; + } + + return $camel_cased_array; + }; + + foreach ( $registered_font_families as $slug => $registered_font_faces ) { + // Font family not in theme.json, so let's add it. + if ( ! isset( $font_family_indexes_in_theme_json[ $slug ] ) ) { + $family_name = $registered_font_faces[0]['font-family']; + + $data['settings']['typography']['fontFamilies'][] = array( + 'origin' => 'gutenberg_wp_webfonts_api', + 'fontFamily' => str_contains( $family_name, ' ' ) ? "'{$family_name}'" : $family_name, + 'name' => $family_name, + 'slug' => $slug, + 'fontFaces' => array_map( + function( $font_face ) use ( $array_keys_to_camel_case ) { + $font_face['origin'] = 'gutenberg_wp_webfonts_api'; + + return $array_keys_to_camel_case( $font_face ); + }, + $registered_font_faces + ), + ); + + continue; + } + + $font_family_index_in_theme_json = $font_family_indexes_in_theme_json[ $slug ]; + $font_family_in_theme_json = $data['settings']['typography']['fontFamilies'][ $font_family_index_in_theme_json ]; + + if ( ! isset( $font_family_in_theme_json['fontFaces'] ) ) { + // Font family exists, but it's not declaring any font face + // Let's not get in their way. + continue; + } + + $font_faces_in_theme_json = $font_family_in_theme_json['fontFaces']; + + foreach ( $registered_font_faces as $registered_font_face ) { + $registered_font_face = $array_keys_to_camel_case( $registered_font_face ); + + if ( false !== wp_webfonts()->find_webfont( $font_faces_in_theme_json, $registered_font_face ) ) { + // Webfont is already there, so let's not add it. + continue; + } + + $registered_font_face['origin'] = 'gutenberg_wp_webfonts_api'; + + $data['settings']['typography']['fontFamilies'][ $font_family_index_in_theme_json ]['fontFaces'][] = $registered_font_face; + } + } + + return $data; + } +} diff --git a/lib/experimental/class-wp-theme-json-resolver-gutenberg.php b/lib/experimental/class-wp-theme-json-resolver-gutenberg.php index 960ea659e8d2ee..d2a9935d83f442 100644 --- a/lib/experimental/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/experimental/class-wp-theme-json-resolver-gutenberg.php @@ -36,15 +36,23 @@ public static function get_theme_data( $deprecated = array(), $settings = array( if ( null === static::$theme ) { $theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) ); $theme_json_data = static::translate( $theme_json_data, wp_get_theme()->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 ); + + $original_theme_json = new WP_Theme_JSON_Gutenberg( $theme_json_data ); + _wp_register_webfonts_from_theme_json( $original_theme_json->get_settings() ); + + if ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { + $theme_json_data = _wp_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 ); + + $parent_theme = new WP_Theme_JSON_Gutenberg( $parent_theme_json_data ); + _wp_register_webfonts_from_theme_json( $parent_theme->get_settings() ); // Merge the child theme.json into the parent theme.json. // The child theme takes precedence over the parent. @@ -98,4 +106,31 @@ public static function get_theme_data( $deprecated = array(), $settings = array( return $with_theme_supports; } + /** + * There are three sources of data (origins) for a site: + * default, theme, and custom. The custom's has higher priority + * than the theme's, and the theme's higher than defaults's. + * + * Unlike the getters {@link get_core_data}, + * {@link get_theme_data}, and {@link get_user_data}, + * this method returns data after it has been merged + * with the previous origins. This means that if the same piece of data + * is declared in different origins (user, theme, and core), + * the last origin overrides the previous. + * + * For example, if the user has set a background color + * for the paragraph block, and the theme has done it as well, + * the user preference wins. + * + * @param string $origin Optional. To what level should we merge data. + * Valid values are 'theme' or 'custom'. + * Default is 'custom'. + * @return WP_Theme_JSON_Gutenberg + */ + public static function get_merged_data( $origin = 'custom' ) { + $result = parent::get_merged_data( $origin ); + _wp_register_webfonts_from_theme_json( $result->get_settings() ); + + return $result; + } } diff --git a/lib/experimental/class-wp-webfonts.php b/lib/experimental/class-wp-webfonts.php index e422f51658befb..ea2bd1456845d8 100644 --- a/lib/experimental/class-wp-webfonts.php +++ b/lib/experimental/class-wp-webfonts.php @@ -63,10 +63,10 @@ public function init() { // Register callback to generate and enqueue styles. if ( did_action( 'wp_enqueue_scripts' ) ) { - $this->stylesheet_handle = 'webfonts-footer'; + $this->stylesheet_handle = 'wp-webfonts-footer'; $hook = 'wp_print_footer_scripts'; } else { - $this->stylesheet_handle = 'webfonts'; + $this->stylesheet_handle = 'wp-webfonts'; $hook = 'wp_enqueue_scripts'; } add_action( $hook, array( $this, 'generate_and_enqueue_styles' ) ); @@ -145,28 +145,81 @@ public function register_webfont( array $webfont ) { /** * Enqueue a font-family that has been already registered. * - * @param string $font_family_name The font family name to be enqueued. + * @param string $font_family_name The font family name to be enqueued. + * @param array|null $font_face The font face to selectively enqueue. * @return bool True if successfully enqueued, else false. */ - public function enqueue_webfont( $font_family_name ) { + public function enqueue_webfont( $font_family_name, $font_face = null ) { $slug = $this->get_font_slug( $font_family_name ); - if ( isset( $this->enqueued_webfonts[ $slug ] ) ) { + // The font family is already enqueued and there are none left to enqueue. + if ( isset( $this->enqueued_webfonts[ $slug ] ) && ! isset( $this->registered_webfonts[ $slug ] ) ) { return true; } if ( ! isset( $this->registered_webfonts[ $slug ] ) ) { - /* translators: %s unique slug to identify the font family of the webfont */ - _doing_it_wrong( __METHOD__, sprintf( __( 'The "%s" font family is not registered.', 'gutenberg' ), $slug ), '6.0.0' ); + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: %s unique slug to identify the font family of the webfont */ + esc_html__( 'The "%s" font family is not registered.', 'gutenberg' ), + esc_html( $slug ) + ), + '6.0.0' + ); return false; } - $this->enqueued_webfonts[ $slug ] = $this->registered_webfonts[ $slug ]; - unset( $this->registered_webfonts[ $slug ] ); + // Enqueueing the font family completely. + if ( ! $font_face ) { + $font_family_to_enqueue = $this->remove_font_family( $font_family_name ); + $this->enqueued_webfonts[ $slug ] = $font_family_to_enqueue; + + return true; + } + + // Enqueueing a single font face. + $font_face = _wp_array_keys_to_kebab_case( $font_face ); + $font_face_to_enqueue = $this->unregister_font_face( $font_face ); + + if ( ! $font_face_to_enqueue ) { + trigger_error( + sprintf( + /* translators: %1$s: font family, %2$s: font weight, %3$s: font style */ + esc_html__( 'The "%1$s:%2$s:%3$s" font face is not registered.', 'gutenberg' ), + esc_html( $font_face['font-family'] ), + esc_html( $font_face['font-weight'] ), + esc_html( $font_face['font-style'] ) + ) + ); + + return false; + } + + if ( ! isset( $this->enqueued_webfonts[ $slug ] ) ) { + $this->enqueued_webfonts[ $slug ] = array(); + } + + $this->enqueued_webfonts[ $slug ][] = $font_face_to_enqueue; + return true; } + /** + * Checks if a font family is registered. + * + * @since 6.0.0 + * + * @param string $font_family_name The font family name to check in the registry. + * @return bool True if found, else false. + */ + public function is_font_family_registered( $font_family_name ) { + $slug = $this->get_font_slug( $font_family_name ); + + return isset( $this->registered_webfonts[ $slug ] ); + } + /** * Get the font slug. * @@ -191,16 +244,68 @@ public static function get_font_slug( $to_convert ) { return sanitize_title( $to_convert ); } + /** + * Unregisters a font family. + * + * @param string $font_family_name The font family, by name, to unregister. + * @return array[]|false The font face objects of the family if unregistered, false otherwise. + */ + private function remove_font_family( $font_family_name ) { + $slug = $this->get_font_slug( $font_family_name ); + + if ( ! isset( $this->registered_webfonts[ $slug ] ) ) { + return false; + } + + $font_family = $this->registered_webfonts[ $slug ]; + unset( $this->registered_webfonts[ $slug ] ); + + return $font_family; + } + + /** + * Unregisters a font face. + * + * @since 6.0.0 + * + * @param array $font_face_to_unregister The font face to unregister. + * @return array|false The font face if unregistered, false otherwise. + */ + private function unregister_font_face( array $font_face_to_unregister ) { + $font_family_slug = $this->get_font_slug( $font_face_to_unregister ); + + if ( false === $font_family_slug || ! isset( $this->registered_webfonts[ $font_family_slug ] ) ) { + return false; + } + + $font_family = $this->registered_webfonts[ $font_family_slug ]; + $index = $this->find_webfont( $font_family, $font_face_to_unregister ); + + // Font face not found. + if ( false === $index ) { + return false; + } + + $font_face = $this->registered_webfonts[ $font_family_slug ][ $index ]; + unset( $this->registered_webfonts[ $font_family_slug ][ $index ] ); + + // No font faces left, let's remove the font family entry. + if ( empty( $this->registered_webfonts[ $font_family_slug ] ) ) { + unset( $this->registered_webfonts[ $font_family_slug ] ); + } + + return $font_face; + } + /** * Validate a webfont. * * @since 6.0.0 * * @param array $webfont The webfont arguments. - * * @return array|false The validated webfont arguments, or false if the webfont is invalid. */ - public function validate_webfont( $webfont ) { + public function validate_webfont( array $webfont ) { $webfont = wp_parse_args( $webfont, array( @@ -356,8 +461,14 @@ private function generate_styles( array $webfonts_by_provider ) { // Bail out if the provider class does not exist. if ( ! class_exists( $provider_class ) ) { - /* translators: %s is the provider name. */ - trigger_error( sprintf( __( 'Webfont provider "%s" is not registered.', 'gutenberg' ), $provider_id ) ); + trigger_error( + sprintf( + /* translators: %s is the provider name. */ + esc_html__( 'Webfont provider "%s" is not registered.', 'gutenberg' ), + esc_html( $provider_id ) + ) + ); + continue; } @@ -399,8 +510,13 @@ private function get_webfonts_by_provider( array $font_families ) { // Skip if the provider is not registered. if ( ! isset( $providers[ $provider ] ) ) { - /* translators: %s is the provider name. */ - trigger_error( sprintf( __( 'Webfont provider "%s" is not registered.', 'gutenberg' ), $provider ) ); + trigger_error( + sprintf( + /* translators: %s is the provider name. */ + esc_html__( 'Webfont provider "%s" is not registered.', 'gutenberg' ), + esc_html( $provider ) + ) + ); continue; } @@ -414,4 +530,45 @@ private function get_webfonts_by_provider( array $font_families ) { return $webfonts_by_provider; } + + /** + * Finds $webfont_to_find in $webfonts. + * + * @since 6.0.0 + * + * @param array[] $webfonts The webfonts array. + * @param array $webfont_to_find The webfont to find. + * @return integer|false The index of $webfont in $webfonts if found. False otherwise. + */ + public function find_webfont( array $webfonts, $webfont_to_find ) { + if ( empty( $webfonts ) ) { + return false; + } + + $is_camel_case = isset( $webfonts[0]['fontFamily'] ); + + foreach ( $webfonts as $index => $webfont ) { + $equality_attrs = $is_camel_case + ? array( 'fontFamily', 'fontStyle', 'fontWeight' ) + : array( 'font-family', 'font-style', 'font-weight' ); + + $found = $index; + foreach ( $equality_attrs as $attr ) { + // Bail out if the attribute does not exist, or if the values are not equal. + if ( + empty( $webfont[ $attr ] ) || + empty( $webfont_to_find[ $attr ] ) || + $webfont[ $attr ] !== $webfont_to_find[ $attr ] + ) { + $found = false; + break; + } + } + if ( false !== $found ) { + return $found; + } + } + + return false; + } } diff --git a/lib/experimental/enqueue-webfonts-listed-in-theme-json.php b/lib/experimental/enqueue-webfonts-listed-in-theme-json.php new file mode 100644 index 00000000000000..b214d856b0b7d6 --- /dev/null +++ b/lib/experimental/enqueue-webfonts-listed-in-theme-json.php @@ -0,0 +1,77 @@ +get_settings(); + + // Bail out early if there are no settings for webfonts. + if ( empty( $settings['typography'] ) || empty( $settings['typography']['fontFamilies'] ) ) { + return; + } + + // Look for fontFamilies. + foreach ( $settings['typography']['fontFamilies'] as $font_families ) { + foreach ( $font_families as $font_family ) { + // Skip dynamically included font families. We only want to enqueue explicitly added fonts. + if ( isset( $font_family['origin'] ) && 'gutenberg_wp_webfonts_api' === $font_family['origin'] ) { + continue; + } + + // If no font faces defined. + if ( ! isset( $font_family['fontFaces'] ) ) { + // And the font family is registered. + if ( ! wp_webfonts()->is_font_family_registered( $font_family['fontFamily'] ) ) { + continue; + } + + // Enqueue the entire family. + wp_webfonts()->enqueue_webfont( $font_family ); + continue; + } + + // Loop through all the font faces, enqueueing each one of them. + foreach ( $font_family['fontFaces'] as $font_face ) { + // Skip dynamically included font faces. We only want to enqueue the font faces listed in theme.json. + if ( isset( $font_face['origin'] ) && 'gutenberg_wp_webfonts_api' === $font_face['origin'] ) { + continue; + } + + /* + * Skip if this font-face's font-family is not defined. Why? + * The font-face's font-family is the key used during the registration + * process. Its font-family may be different from its parent `$font_family`. + * For example, the parent may define a fallback such as "serif", + * whereas this font-face may define only the font-family. + */ + if ( ! isset( $font_face['fontFamily'] ) ) { + continue; + } + + wp_webfonts()->enqueue_webfont( $font_face['fontFamily'], $font_face ); + } + } + } + } +} + +add_filter( 'wp_loaded', '_wp_enqueue_webfonts_listed_in_theme_json' ); + +// No need to run this -- opening the admin interface enqueues all the webfonts. +add_action( + 'admin_init', + function() { + remove_filter( 'wp_loaded', '_wp_enqueue_webfonts_listed_in_theme_json' ); + } +); diff --git a/lib/experimental/register-webfonts-from-theme-json.php b/lib/experimental/register-webfonts-from-theme-json.php index 071d88e325bc81..c678960b348916 100644 --- a/lib/experimental/register-webfonts-from-theme-json.php +++ b/lib/experimental/register-webfonts-from-theme-json.php @@ -5,171 +5,76 @@ * @package gutenberg */ -/** - * Register webfonts defined in theme.json. - */ -function gutenberg_register_webfonts_from_theme_json() { - // Get settings. - $settings = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data()->get_settings(); - - // If in the editor, add webfonts defined in variations. - if ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { - $variations = WP_Theme_JSON_Resolver_Gutenberg::get_style_variations(); +if ( ! function_exists( '_wp_register_webfonts_from_theme_json' ) ) { + /** + * Register webfonts defined in theme.json + * + * @private + * + * @param array $settings The theme.json file. + */ + function _wp_register_webfonts_from_theme_json( $settings ) { + // If in the editor, add webfonts defined in variations. + if ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { + $variations = WP_Theme_JSON_Resolver_Gutenberg::get_style_variations(); - foreach ( $variations as $variation ) { + foreach ( $variations as $variation ) { - // Sanity check: Skip if fontFamilies are not defined in the variation. - if ( - empty( $variation['settings'] ) || - empty( $variation['settings']['typography'] ) || - empty( $variation['settings']['typography']['fontFamilies'] ) - ) { - continue; - } + // Sanity check: Skip if fontFamilies are not defined in the variation. + if ( empty( $variation['settings']['typography']['fontFamilies'] ) ) { + continue; + } - // Merge the variation settings with the global settings. - $settings['typography'] = empty( $settings['typography'] ) ? array() : $settings['typography']; - $settings['typography']['fontFamilies'] = empty( $settings['typography']['fontFamilies'] ) ? array() : $settings['typography']['fontFamilies']; - $settings['typography']['fontFamilies']['theme'] = empty( $settings['typography']['fontFamilies'] ) ? array() : $settings['typography']['fontFamilies']['theme']; - $settings['typography']['fontFamilies']['theme'] = array_merge( $settings['typography']['fontFamilies']['theme'], $variation['settings']['typography']['fontFamilies']['theme'] ); + // Merge the variation settings with the global settings. + $settings['typography'] = empty( $settings['typography'] ) ? array() : $settings['typography']; + $settings['typography']['fontFamilies'] = empty( $settings['typography']['fontFamilies'] ) ? array() : $settings['typography']['fontFamilies']; + $settings['typography']['fontFamilies']['theme'] = empty( $settings['typography']['fontFamilies'] ) ? array() : $settings['typography']['fontFamilies']['theme']; + $settings['typography']['fontFamilies']['theme'] = array_merge( $settings['typography']['fontFamilies']['theme'], $variation['settings']['typography']['fontFamilies']['theme'] ); - // Make sure there are no duplicates. - $settings['typography']['fontFamilies'] = array_unique( $settings['typography']['fontFamilies'] ); + // Make sure there are no duplicates. + $settings['typography']['fontFamilies'] = array_unique( $settings['typography']['fontFamilies'] ); + } } - } - // Bail out early if there are no settings for webfonts. - if ( empty( $settings['typography'] ) || empty( $settings['typography']['fontFamilies'] ) ) { - return; - } - - $webfonts = array(); - - // Look for fontFamilies. - foreach ( $settings['typography']['fontFamilies'] as $font_families ) { - foreach ( $font_families as $font_family ) { - - // Skip if fontFace is not defined. - if ( empty( $font_family['fontFace'] ) ) { - continue; - } + // Bail out early if there are no settings for webfonts. + if ( empty( $settings['typography'] ) || empty( $settings['typography']['fontFamilies'] ) ) { + return; + } - $font_family['fontFace'] = (array) $font_family['fontFace']; + $font_faces_to_register = array(); - foreach ( $font_family['fontFace'] as $font_face ) { - // Skip if the webfont was registered through the Webfonts API. - if ( isset( $font_face['origin'] ) && 'gutenberg_wp_webfonts_api' === $font_face['origin'] ) { + foreach ( $settings['typography']['fontFamilies'] as $font_families_by_origin ) { + foreach ( $font_families_by_origin as $font_family ) { + // Skip if no `fontFaces` are defined. + if ( empty( $font_family['fontFaces'] ) ) { continue; } - // 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']; + // Fallback to the `local` provider if no `provider` is defined. + if ( ! isset( $font_family['provider'] ) ) { + $font_family['provider'] = 'local'; + } - foreach ( $font_face['src'] as $src_key => $url ) { - // Tweak the URL to be relative to the theme root. - if ( ! str_starts_with( $url, 'file:./' ) ) { - continue; + foreach ( $font_family['fontFaces'] as $font_face ) { + + // Transforms the source of the font face from `file.:/` into an actual URI. + 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 ( ! str_starts_with( $url, 'file:./' ) ) { + continue; + } + $font_face['src'][ $src_key ] = get_theme_file_uri( str_replace( 'file:./', '', $url ) ); } - $font_face['src'][ $src_key ] = get_theme_file_uri( str_replace( 'file:./', '', $url ) ); } - } + $font_face['provider'] = $font_family['provider']; + $font_face = _wp_array_keys_to_kebab_case( $font_face ); - // 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 ] ); - } + $font_faces_to_register[] = $font_face; } - - $webfonts[] = $font_face; - } - } - } - foreach ( $webfonts as $webfont ) { - wp_webfonts()->register_webfont( $webfont ); - wp_webfonts()->enqueue_webfont( $webfont['font-family'] ); - } -} - -/** - * 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_all_webfonts(); - $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 = static function( $families_data ) { - $families = array(); - foreach ( $families_data as $family ) { - $families[] = WP_Webfonts::get_font_slug( $family ); - } - - // 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( - array_keys( $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(); - } - - foreach ( $to_add as $slug ) { - $font_faces_for_family = $font_families_registered[ $slug ]; - $family_name = $font_faces_for_family[0]['font-family']; - $font_faces = array(); - - foreach ( $font_faces_for_family as $font_face ) { - $camel_cased = array( 'origin' => 'gutenberg_wp_webfonts_api' ); - foreach ( $font_face as $key => $value ) { - $camel_cased[ lcfirst( str_replace( '-', '', ucwords( $key, '-' ) ) ) ] = $value; } - $font_faces[] = $camel_cased; } - - $data['settings']['typography']['fontFamilies'][] = array( - 'fontFamily' => str_contains( $family_name, ' ' ) ? "'{$family_name}'" : $family_name, - 'name' => $family_name, - 'slug' => $slug, - 'fontFace' => $font_faces, - ); + wp_register_webfonts( $font_faces_to_register ); } - - return $data; } - -add_action( 'init', 'gutenberg_register_webfonts_from_theme_json' ); diff --git a/lib/experimental/webfonts-utils.php b/lib/experimental/webfonts-utils.php new file mode 100644 index 00000000000000..e8250b0cfddc07 --- /dev/null +++ b/lib/experimental/webfonts-utils.php @@ -0,0 +1,27 @@ + $value ) { + $kebab_cased_array[ _wp_to_kebab_case( $key ) ] = $value; + } + + return $kebab_cased_array; + } +} diff --git a/lib/load.php b/lib/load.php index 5d9969c409ffa1..1b1354c7d929e8 100644 --- a/lib/load.php +++ b/lib/load.php @@ -115,11 +115,14 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.0/block-template.php'; require __DIR__ . '/compat/wordpress-6.0/edit-form-blocks.php'; require __DIR__ . '/experimental/register-webfonts-from-theme-json.php'; +require __DIR__ . '/experimental/add-registered-webfonts-to-theme-json.php'; +require __DIR__ . '/experimental/enqueue-webfonts-listed-in-theme-json.php'; require __DIR__ . '/experimental/class-wp-theme-json-resolver-gutenberg.php'; require __DIR__ . '/experimental/class-wp-webfonts.php'; require __DIR__ . '/experimental/class-wp-webfonts-provider.php'; require __DIR__ . '/experimental/class-wp-webfonts-provider-local.php'; require __DIR__ . '/experimental/webfonts.php'; +require __DIR__ . '/experimental/webfonts-utils.php'; require __DIR__ . '/experimental/blocks.php'; require __DIR__ . '/blocks.php'; diff --git a/phpunit/class-wp-webfonts-mock-provider.php b/phpunit/class-wp-webfonts-mock-provider.php new file mode 100644 index 00000000000000..820de8d2f9b241 --- /dev/null +++ b/phpunit/class-wp-webfonts-mock-provider.php @@ -0,0 +1,21 @@ + 'Roboto', + 'font-style' => 'normal', + 'font-stretch' => 'normal', + 'font-weight' => '400', + 'src' => get_theme_file_uri( '/assets/fonts/Roboto-regular.ttf' ), + ) + ); + + wp_register_webfont( + array( + 'font-family' => 'Source Serif Pro', + 'font-style' => 'normal', + 'font-stretch' => 'normal', + 'font-weight' => '400', + 'src' => get_theme_file_uri( '/assets/fonts/Source-Serif-Pro-Regular.ttf' ), + ) + ); + } +); diff --git a/phpunit/data/themedir1/enqueue-font-family/style.css b/phpunit/data/themedir1/enqueue-font-family/style.css new file mode 100644 index 00000000000000..eb132a0ce97bcb --- /dev/null +++ b/phpunit/data/themedir1/enqueue-font-family/style.css @@ -0,0 +1,7 @@ +/* +Theme Name: Test theme +Theme URI: https://wordpress.org/ +Description: For testing purposes only. +Version: 1.0.0 +Text Domain: block-theme +*/ diff --git a/phpunit/data/themedir1/enqueue-font-family/theme.json b/phpunit/data/themedir1/enqueue-font-family/theme.json new file mode 100644 index 00000000000000..76c09ad7959620 --- /dev/null +++ b/phpunit/data/themedir1/enqueue-font-family/theme.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "settings": { + "typography": { + "fontFamilies": [ + { + "fontFamily": "Roboto", + "slug": "roboto", + "name": "Roboto" + } + ] + } + } +} diff --git a/phpunit/data/themedir1/enqueue-only-one-font-face/functions.php b/phpunit/data/themedir1/enqueue-only-one-font-face/functions.php new file mode 100644 index 00000000000000..2c5c2b825967e3 --- /dev/null +++ b/phpunit/data/themedir1/enqueue-only-one-font-face/functions.php @@ -0,0 +1,31 @@ + 'Roboto', + 'font-style' => 'normal', + 'font-stretch' => 'normal', + 'font-weight' => '400', + 'src' => get_theme_file_uri( '/assets/fonts/Roboto-Regular.ttf' ), + ) + ); + + wp_register_webfont( + array( + 'font-family' => 'Roboto', + 'font-style' => 'bold', + 'font-stretch' => 'normal', + 'font-weight' => '900', + 'src' => get_theme_file_uri( '/assets/fonts/Roboto-Bold.ttf' ), + ) + ); + } +); diff --git a/phpunit/data/themedir1/enqueue-only-one-font-face/style.css b/phpunit/data/themedir1/enqueue-only-one-font-face/style.css new file mode 100644 index 00000000000000..eb132a0ce97bcb --- /dev/null +++ b/phpunit/data/themedir1/enqueue-only-one-font-face/style.css @@ -0,0 +1,7 @@ +/* +Theme Name: Test theme +Theme URI: https://wordpress.org/ +Description: For testing purposes only. +Version: 1.0.0 +Text Domain: block-theme +*/ diff --git a/phpunit/data/themedir1/enqueue-only-one-font-face/theme.json b/phpunit/data/themedir1/enqueue-only-one-font-face/theme.json new file mode 100644 index 00000000000000..c009e413c64a35 --- /dev/null +++ b/phpunit/data/themedir1/enqueue-only-one-font-face/theme.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "settings": { + "typography": { + "fontFamilies": [ + { + "fontFamily": "Roboto", + "slug": "roboto", + "name": "Roboto", + "fontFaces": [ + { + "fontFamily": "Roboto", + "fontWeight": "900", + "fontStyle": "bold" + } + ] + } + ] + } + } +} diff --git a/phpunit/data/themedir1/register-and-enqueue-through-different-providers/style.css b/phpunit/data/themedir1/register-and-enqueue-through-different-providers/style.css new file mode 100644 index 00000000000000..eb132a0ce97bcb --- /dev/null +++ b/phpunit/data/themedir1/register-and-enqueue-through-different-providers/style.css @@ -0,0 +1,7 @@ +/* +Theme Name: Test theme +Theme URI: https://wordpress.org/ +Description: For testing purposes only. +Version: 1.0.0 +Text Domain: block-theme +*/ diff --git a/phpunit/data/themedir1/register-and-enqueue-through-different-providers/theme.json b/phpunit/data/themedir1/register-and-enqueue-through-different-providers/theme.json new file mode 100644 index 00000000000000..307013a664e7e8 --- /dev/null +++ b/phpunit/data/themedir1/register-and-enqueue-through-different-providers/theme.json @@ -0,0 +1,30 @@ +{ + "version": 1, + "settings": { + "typography": { + "fontFamilies": [ + { + "fontFamily": "Roboto", + "slug": "roboto", + "name": "Roboto", + "fontFaces": [ + { + "provider": "mock", + "fontFamily": "Roboto", + "fontWeight": "400", + "fontStyle": "regular", + "src": "file:./assets/fonts/Roboto-Regular.ttf" + }, + { + "provider": "local", + "fontFamily": "Roboto", + "fontWeight": "900", + "fontStyle": "bold", + "src": "file:./assets/fonts/Roboto-Bold.ttf" + } + ] + } + ] + } + } +} diff --git a/phpunit/data/themedir1/register-and-enqueue-to-the-same-provider/style.css b/phpunit/data/themedir1/register-and-enqueue-to-the-same-provider/style.css new file mode 100644 index 00000000000000..eb132a0ce97bcb --- /dev/null +++ b/phpunit/data/themedir1/register-and-enqueue-to-the-same-provider/style.css @@ -0,0 +1,7 @@ +/* +Theme Name: Test theme +Theme URI: https://wordpress.org/ +Description: For testing purposes only. +Version: 1.0.0 +Text Domain: block-theme +*/ diff --git a/phpunit/data/themedir1/register-and-enqueue-to-the-same-provider/theme.json b/phpunit/data/themedir1/register-and-enqueue-to-the-same-provider/theme.json new file mode 100644 index 00000000000000..167796c585fbd7 --- /dev/null +++ b/phpunit/data/themedir1/register-and-enqueue-to-the-same-provider/theme.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "settings": { + "typography": { + "fontFamilies": [ + { + "fontFamily": "Roboto", + "slug": "roboto", + "name": "Roboto", + "provider": "local", + "fontFaces": [ + { + "fontFamily": "Roboto", + "fontWeight": "400", + "fontStyle": "regular", + "src": "file:./assets/fonts/Roboto-Regular.ttf" + }, + { + "fontFamily": "Roboto", + "fontWeight": "900", + "fontStyle": "bold", + "src": "file:./assets/fonts/Roboto-Bold.ttf" + } + ] + } + ] + } + } +} diff --git a/phpunit/enqueue-only-webfonts-listed-in-theme-json-test.php b/phpunit/enqueue-only-webfonts-listed-in-theme-json-test.php new file mode 100644 index 00000000000000..6f2afe34563f75 --- /dev/null +++ b/phpunit/enqueue-only-webfonts-listed-in-theme-json-test.php @@ -0,0 +1,212 @@ +old_wp_styles = $wp_styles; + + $wp_styles = null; + + global $wp_webfonts; + $this->old_wp_webfonts = $wp_webfonts; + + $wp_webfonts = null; + + $this->theme_root = realpath( __DIR__ . '/data/themedir1' ); + + $this->old_theme_dir = $GLOBALS['wp_theme_directories']; + + // /themes is necessary as theme.php functions assume /themes is the root if there is only one root. + $GLOBALS['wp_theme_directories'] = array( WP_CONTENT_DIR . '/themes', $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'] ); + } + + /** + * Restores the original theme root and WP_Webfonts instance. + */ + public function tear_down() { + global $wp_webfonts; + global $wp_styles; + + $wp_webfonts = $this->old_wp_webfonts; + $wp_styles = $this->old_wp_styles; + + // Restore the original theme directory setup. + $GLOBALS['wp_theme_directories'] = $this->old_theme_dir; + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + + parent::tear_down(); + } + + /** + * Enqueue an externally registered font family. + * + * The `enqueue-font-family` theme registers two font families + * but only one is listed in theme.json. + */ + public function test_enqueue_an_externally_registered_font_family() { + $this->generate_styles_for_theme( 'enqueue-font-family' ); + + $expected = << +@font-face{font-family:Roboto;font-style:normal;font-weight:400;font-display:fallback;font-stretch:normal;src:local(Roboto), url('/wp-content/plugins/gutenberg/phpunit/data/themedir1/enqueue-font-family/assets/fonts/Roboto-regular.ttf') format('truetype');} + +EOF; + + $this->assertStringContainsString( + $expected, + get_echo( 'wp_print_styles' ) + ); + } + + /** + * Enqueue only one of all externally registered font faces. + * + * The `enqueue-only-one-font-face` theme registers two font faces + * for Roboto, but only one is listed in theme.json. + */ + public function test_enqueue_only_one_of_all_externally_registered_font_faces() { + $this->generate_styles_for_theme( 'enqueue-only-one-font-face' ); + + $expected = << +@font-face{font-family:Roboto;font-style:bold;font-weight:900;font-display:fallback;font-stretch:normal;src:local(Roboto), url('/wp-content/plugins/gutenberg/phpunit/data/themedir1/enqueue-only-one-font-face/assets/fonts/Roboto-Bold.ttf') format('truetype');} + +EOF; + + $this->assertStringContainsString( + $expected, + get_echo( 'wp_print_styles' ) + ); + } + + /** + * Register (and enqueue) a collection of font faces to the same provider. + * + * The `register-and-enqueue-to-the-same-provider` theme registers and + * enqueue a font family listed in theme.json through the same provider, declared + * at top level. + */ + public function test_register_and_enqueue_font_faces_to_same_provider() { + $this->generate_styles_for_theme( 'register-and-enqueue-to-the-same-provider' ); + + $expected = << +@font-face{font-family:Roboto;font-style:regular;font-weight:400;font-display:fallback;src:local(Roboto), url('/wp-content/plugins/gutenberg/phpunit/data/themedir1/register-and-enqueue-to-the-same-provider/assets/fonts/Roboto-Regular.ttf') format('truetype');}@font-face{font-family:Roboto;font-style:bold;font-weight:900;font-display:fallback;src:local(Roboto), url('/wp-content/plugins/gutenberg/phpunit/data/themedir1/register-and-enqueue-to-the-same-provider/assets/fonts/Roboto-Bold.ttf') format('truetype');} + +EOF; + + $this->assertStringContainsString( + $expected, + get_echo( 'wp_print_styles' ) + ); + } + + /** + * Register (and enqueue) a collection of font faces through different providers. + * + * The `register-and-enqueue-through-different-providers` theme registers and + * enqueue a font family listed in theme.json through different providers, + * declared at font face level. + */ + public function test_register_and_enqueue_font_faces_through_different_providers() { + wp_register_webfont_provider( 'mock', 'WP_Webfonts_Mock_Provider' ); + $this->generate_styles_for_theme( 'register-and-enqueue-through-different-providers' ); + + $expected = << +@font-face{font-family:Roboto;font-style:regular;font-weight:400;font-display:fallback;src:local(Roboto), url('/wp-content/plugins/gutenberg/phpunit/data/themedir1/register-and-enqueue-through-different-providers/assets/fonts/Roboto-Regular.ttf') format('truetype');}@font-face{font-family:Roboto;font-style:bold;font-weight:900;font-display:fallback;src:local(Roboto), url('/wp-content/plugins/gutenberg/phpunit/data/themedir1/register-and-enqueue-through-different-providers/assets/fonts/Roboto-Bold.ttf') format('truetype');} + +EOF; + + $this->assertStringContainsString( + $expected, + get_echo( 'wp_print_styles' ) + ); + } + + /** + * Switches to $theme_name, runs the necessary hooks + * and generates the styles for it. + * + * The styles will hold the webfonts that will get loaded + * in the front-end, and that's what this test suite is asserting. + * + * @param string $theme_name The theme name. Themes are located in phpunited/data/themedir1. + */ + private function generate_styles_for_theme( $theme_name ) { + switch_theme( $theme_name ); + + if ( file_exists( get_stylesheet_directory() . '/functions.php' ) ) { + require_once get_stylesheet_directory() . '/functions.php'; + } + + do_action( 'after_setup_theme' ); + WP_Theme_JSON_Resolver_Gutenberg::clean_cached_data(); + do_action( 'wp_loaded' ); + + wp_webfonts()->generate_and_enqueue_styles(); + } +}