diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php
index 6e9d05cd7f238..1e825e3c6bbe4 100644
--- a/lib/class-wp-theme-json-resolver-gutenberg.php
+++ b/lib/class-wp-theme-json-resolver-gutenberg.php
@@ -275,7 +275,9 @@ public static function get_theme_data( $deprecated = array(), $options = array()
}
// BEGIN OF EXPERIMENTAL CODE. Not to backport to core.
- static::$theme = WP_Fonts_Resolver::add_missing_fonts_to_theme_json( static::$theme );
+ if ( ! class_exists( 'WP_Font_Face' ) && class_exists( 'WP_Fonts_Resolver' ) ) {
+ static::$theme = WP_Fonts_Resolver::add_missing_fonts_to_theme_json( static::$theme );
+ }
// END OF EXPERIMENTAL CODE.
}
diff --git a/lib/compat/wordpress-6.3/script-loader.php b/lib/compat/wordpress-6.3/script-loader.php
index c515eb10fdc6b..8f7bda2a64811 100644
--- a/lib/compat/wordpress-6.3/script-loader.php
+++ b/lib/compat/wordpress-6.3/script-loader.php
@@ -81,7 +81,6 @@ function _gutenberg_get_iframed_editor_assets() {
ob_start();
wp_print_styles();
- wp_print_fonts( true );
$styles = ob_get_clean();
ob_start();
diff --git a/lib/experimental/fonts-api/fonts-api.php b/lib/experimental/fonts-api/fonts-api.php
index e2ad6e1cb53af..8d07dc118f56e 100644
--- a/lib/experimental/fonts-api/fonts-api.php
+++ b/lib/experimental/fonts-api/fonts-api.php
@@ -243,3 +243,17 @@ static function( $mime_types ) {
* during the build. See: tools/webpack/blocks.js.
*/
add_action( 'init', 'WP_Fonts_Resolver::register_fonts_from_theme_json', 21 );
+
+add_filter(
+ 'block_editor_settings_all',
+ static function( $settings ) {
+ ob_start();
+ wp_print_fonts( true );
+ $styles = ob_get_clean();
+
+ // Add the font-face styles to iframed editor assets.
+ $settings['__unstableResolvedAssets']['styles'] .= $styles;
+ return $settings;
+ },
+ 11
+);
diff --git a/lib/experimental/fonts/class-wp-font-face-resolver.php b/lib/experimental/fonts/class-wp-font-face-resolver.php
new file mode 100644
index 0000000000000..16e74d6051aa7
--- /dev/null
+++ b/lib/experimental/fonts/class-wp-font-face-resolver.php
@@ -0,0 +1,158 @@
+get_settings();
+
+ // Bail out early if there are no font settings.
+ if ( empty( $settings['typography'] ) || empty( $settings['typography']['fontFamilies'] ) ) {
+ return array();
+ }
+
+ return static::parse_settings( $settings );
+ }
+
+ /**
+ * Parse theme.json settings to extract font definitions with variations grouped by font-family.
+ *
+ * @since X.X.X
+ *
+ * @param array $settings Font settings to parse.
+ * @return array Returns an array of fonts, grouped by font-family.
+ */
+ private static function parse_settings( array $settings ) {
+ $fonts = array();
+
+ foreach ( $settings['typography']['fontFamilies'] as $font_families ) {
+ foreach ( $font_families as $definition ) {
+
+ // Skip if font-family "name" is not defined.
+ if ( empty( $definition['name'] ) ) {
+ continue;
+ }
+
+ // Skip if "fontFace" is not defined, meaning there are no variations.
+ if ( empty( $definition['fontFace'] ) ) {
+ continue;
+ }
+
+ $font_family = $definition['name'];
+
+ // Prepare the fonts array structure for this font-family.
+ if ( ! array_key_exists( $font_family, $fonts ) ) {
+ $fonts[ $font_family ] = array();
+ }
+
+ $fonts[ $font_family ] = static::convert_font_face_properties( $definition['fontFace'], $font_family );
+ }
+ }
+
+ return $fonts;
+ }
+
+ /**
+ * Converts font-face properties from theme.json format.
+ *
+ * @since X.X.X
+ *
+ * @param array $font_face_definition The font-face definitions to convert.
+ * @param string $font_family_property The value to store in the font-face font-family property.
+ * @return array Converted font-face properties.
+ */
+ private static function convert_font_face_properties( array $font_face_definition, $font_family_property ) {
+ $converted_font_faces = array();
+
+ foreach ( $font_face_definition as $font_face ) {
+ // Add the font-family property to the font-face.
+ $font_face['font-family'] = $font_family_property;
+
+ // Converts the "file:./" src placeholder into a theme font file URI.
+ if ( ! empty( $font_face['src'] ) ) {
+ $font_face['src'] = static::to_theme_file_uri( (array) $font_face['src'] );
+ }
+
+ // Convert camelCase properties into kebab-case.
+ $font_face = static::to_kebab_case( $font_face );
+
+ $converted_font_faces[] = $font_face;
+ }
+
+ return $converted_font_faces;
+ }
+
+ /**
+ * Converts each 'file:./' placeholder into a URI to the font file in the theme.
+ *
+ * The 'file:./' is specified in the theme's `theme.json` as a placeholder to be
+ * replaced with the URI to the font file's location in the theme. When a "src"
+ * beings with this placeholder, it is replaced, converting the src into a URI.
+ *
+ * @since X.X.X
+ *
+ * @param array $src An array of font file sources to process.
+ * @return array An array of font file src URI(s).
+ */
+ private static function to_theme_file_uri( array $src ) {
+ $placeholder = 'file:./';
+
+ foreach ( $src as $src_key => $src_url ) {
+ // Skip if the src doesn't start with the placeholder, as there's nothing to replace.
+ if ( ! str_starts_with( $src_url, $placeholder ) ) {
+ continue;
+ }
+
+ $src_file = str_replace( $placeholder, '', $src_url );
+ $src[ $src_key ] = get_theme_file_uri( $src_file );
+ }
+
+ return $src;
+ }
+
+ /**
+ * Converts all first dimension keys into kebab-case.
+ *
+ * @since X.X.X
+ *
+ * @param array $data The array to process.
+ * @return array Data with first dimension keys converted into kebab-case.
+ */
+ private static function to_kebab_case( array $data ) {
+ foreach ( $data as $key => $value ) {
+ $kebab_case = _wp_to_kebab_case( $key );
+ $data[ $kebab_case ] = $value;
+ if ( $kebab_case !== $key ) {
+ unset( $data[ $key ] );
+ }
+ }
+
+ return $data;
+ }
+}
diff --git a/lib/experimental/fonts/class-wp-font-face.php b/lib/experimental/fonts/class-wp-font-face.php
new file mode 100644
index 0000000000000..482bf4d42396d
--- /dev/null
+++ b/lib/experimental/fonts/class-wp-font-face.php
@@ -0,0 +1,418 @@
+ '',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ );
+
+ /**
+ * Valid font-face property names.
+ *
+ * @since X.X.X
+ *
+ * @var string[]
+ */
+ private $valid_font_face_properties = array(
+ 'ascent-override',
+ 'descent-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',
+ );
+
+ /**
+ * Valid font-display values.
+ *
+ * @since X.X.X
+ *
+ * @var string[]
+ */
+ private $valid_font_display = array( 'auto', 'block', 'fallback', 'swap', 'optional' );
+
+ /**
+ * Array of font-face style tag's attribute(s)
+ * where the key is the attribute name and the
+ * value is its value.
+ *
+ * @since X.X.X
+ *
+ * @var string[]
+ */
+ private $style_tag_attrs = array();
+
+ /**
+ * Creates and initializes an instance of WP_Font_Face.
+ *
+ * @since X.X.X
+ */
+ public function __construct() {
+ /**
+ * Filters the font-face property defaults.
+ *
+ * @since X.X.X
+ *
+ * @param array $defaults {
+ * An array of required font-face properties and defaults.
+ *
+ * @type string $provider The provider ID. Default 'local'.
+ * @type string $font-family The font-family property. Default empty string.
+ * @type string $font-style The font-style property. Default 'normal'.
+ * @type string $font-weight The font-weight property. Default '400'.
+ * @type string $font-display The font-display property. Default 'fallback'.
+ * }
+ */
+ $this->font_face_property_defaults = apply_filters( 'wp_font_face_property_defaults', $this->font_face_property_defaults );
+
+ if (
+ function_exists( 'is_admin' ) && ! is_admin()
+ &&
+ function_exists( 'current_theme_supports' ) && ! current_theme_supports( 'html5', 'style' )
+ ) {
+ $this->style_tag_attrs = array( 'type' => 'text/css' );
+ }
+ }
+
+ /**
+ * Generates and prints the `@font-face` styles for the given fonts.
+ *
+ * @since X.X.X
+ *
+ * @param array $fonts The fonts to generate and print @font-face styles.
+ */
+ public function generate_and_print( array $fonts ) {
+ $fonts = $this->validate_fonts( $fonts );
+
+ // Bail out if there are no fonts are given to process.
+ if ( empty( $fonts ) ) {
+ return;
+ }
+
+ printf(
+ $this->get_style_element(),
+ $this->get_css( $fonts )
+ );
+ }
+
+ /**
+ * Validates each of the font-face properties.
+ *
+ * @since X.X.X
+ *
+ * @param array $fonts The fonts to valid.
+ * @return array Prepared font-faces organized by provider and font-family.
+ */
+ private function validate_fonts( array $fonts ) {
+ $validated_fonts = array();
+
+ foreach ( $fonts as $font_faces ) {
+ foreach ( $font_faces as $font_face ) {
+ $font_face = $this->validate_font_face_properties( $font_face );
+ // Skip if failed validation.
+ if ( false === $font_face ) {
+ continue;
+ }
+
+ $validated_fonts[] = $font_face;
+ }
+ }
+
+ return $validated_fonts;
+ }
+
+ /**
+ * Validates each font-face property.
+ *
+ * @since X.X.X
+ *
+ * @param array $font_face Font face properties to validate.
+ * @return false|array Validated font-face on success. Else, false.
+ */
+ private function validate_font_face_properties( array $font_face ) {
+ $font_face = wp_parse_args( $font_face, $this->font_face_property_defaults );
+
+ // Check the font-family.
+ if ( empty( $font_face['font-family'] ) || ! is_string( $font_face['font-family'] ) ) {
+ trigger_error( 'Font font-family must be a non-empty string.' );
+ return false;
+ }
+
+ // Make sure that local fonts have 'src' defined.
+ if ( empty( $font_face['src'] ) || ( ! is_string( $font_face['src'] ) && ! is_array( $font_face['src'] ) ) ) {
+ trigger_error( 'Font src must be a non-empty string or an array of strings.' );
+ return false;
+ }
+
+ // Validate the 'src' property.
+ if ( ! empty( $font_face['src'] ) ) {
+ foreach ( (array) $font_face['src'] as $src ) {
+ if ( empty( $src ) || ! is_string( $src ) ) {
+ trigger_error( 'Each font src must be a non-empty string.' );
+ return false;
+ }
+ }
+ }
+
+ // Check the font-weight.
+ if ( ! is_string( $font_face['font-weight'] ) && ! is_int( $font_face['font-weight'] ) ) {
+ trigger_error( 'Font font-weight must be a properly formatted string or integer.' );
+ return false;
+ }
+
+ // Check the font-display.
+ if ( ! in_array( $font_face['font-display'], $this->valid_font_display, true ) ) {
+ $font_face['font-display'] = $this->font_face_property_defaults['font-display'];
+ }
+
+ // Remove invalid properties.
+ foreach ( $font_face as $prop => $value ) {
+ if ( ! in_array( $prop, $this->valid_font_face_properties, true ) ) {
+ unset( $font_face[ $prop ] );
+ }
+ }
+
+ return $font_face;
+ }
+
+ /**
+ * Gets the `\n";
+ }
+
+ /**
+ * Gets the defined \n";
+ $expected_output = sprintf( $style_element, $expected );
+
+ $this->expectOutputString( $expected_output );
+ $font_face->generate_and_print( $fonts );
+ }
+}
diff --git a/phpunit/fonts/wpFontFaceResolver/getFontsFromThemeJson-test.php b/phpunit/fonts/wpFontFaceResolver/getFontsFromThemeJson-test.php
new file mode 100644
index 0000000000000..74c0f0a4f3c42
--- /dev/null
+++ b/phpunit/fonts/wpFontFaceResolver/getFontsFromThemeJson-test.php
@@ -0,0 +1,116 @@
+assertIsArray( $fonts, 'Should return an array data type' );
+ $this->assertEmpty( $fonts, 'Should return an empty array' );
+ }
+
+ public function test_should_return_all_fonts_from_theme() {
+ switch_theme( static::FONTS_THEME );
+
+ $actual = WP_Font_Face_Resolver::get_fonts_from_theme_json();
+ $expected = $this->get_expected_fonts_for_fonts_block_theme( 'fonts' );
+ $this->assertSame( $expected, $actual );
+ }
+
+ /**
+ * @dataProvider data_should_replace_src_file_placeholder
+ *
+ * @param string $font_name Font's name.
+ * @param string $font_index Font's index in the $fonts array.
+ * @param string $expected Expected src.
+ */
+ public function test_should_replace_src_file_placeholder( $font_name, $font_index, $expected ) {
+ switch_theme( static::FONTS_THEME );
+
+ $fonts = WP_Font_Face_Resolver::get_fonts_from_theme_json();
+
+ $actual = $fonts[ $font_name ][ $font_index ]['src'][0];
+ $expected = get_stylesheet_directory_uri() . $expected;
+
+ $this->assertStringNotContainsString( 'file:./', $actual, 'Font src should not contain the "file:./" placeholder' );
+ $this->assertSame( $expected, $actual, 'Font src should be an URL to its file' );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_should_replace_src_file_placeholder() {
+ return array(
+ // Theme's theme.json.
+ 'DM Sans: 400 normal' => array(
+ 'font_name' => 'DM Sans',
+ 'font_index' => 0,
+ 'expected' => '/assets/fonts/dm-sans/DMSans-Regular.woff2',
+ ),
+ 'DM Sans: 400 italic' => array(
+ 'font_name' => 'DM Sans',
+ 'font_index' => 1,
+ 'expected' => '/assets/fonts/dm-sans/DMSans-Regular-Italic.woff2',
+ ),
+ 'DM Sans: 700 normal' => array(
+ 'font_name' => 'DM Sans',
+ 'font_index' => 2,
+ 'expected' => '/assets/fonts/dm-sans/DMSans-Bold.woff2',
+ ),
+ 'DM Sans: 700 italic' => array(
+ 'font_name' => 'DM Sans',
+ 'font_index' => 3,
+ 'expected' => '/assets/fonts/dm-sans/DMSans-Bold-Italic.woff2',
+ ),
+ 'Source Serif Pro: 200-900 normal' => array(
+ 'font_name' => 'Source Serif Pro',
+ 'font_index' => 0,
+ 'expected' => '/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2',
+ ),
+ 'Source Serif Pro: 200-900 italic' => array(
+ 'font_name' => 'Source Serif Pro',
+ 'font_index' => 1,
+ 'expected' => '/assets/fonts/source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2',
+ ),
+ );
+ }
+}
diff --git a/phpunit/fonts/wpPrintFontFaces-test.php b/phpunit/fonts/wpPrintFontFaces-test.php
new file mode 100644
index 0000000000000..aa44dd4f71b0d
--- /dev/null
+++ b/phpunit/fonts/wpPrintFontFaces-test.php
@@ -0,0 +1,75 @@
+expectOutputString( '' );
+ wp_print_font_faces();
+ }
+
+ /**
+ * @dataProvider data_should_print_given_fonts
+ *
+ * @param array $fonts Fonts to process.
+ * @param string $expected Expected CSS.
+ */
+ public function test_should_print_given_fonts( array $fonts, $expected ) {
+ $expected_output = $this->get_expected_styles_output( $expected );
+
+ $this->expectOutputString( $expected_output );
+ wp_print_font_faces( $fonts );
+ }
+
+ public function test_should_print_fonts_in_merged_data() {
+ switch_theme( static::FONTS_THEME );
+
+ $expected = $this->get_expected_fonts_for_fonts_block_theme( 'font_face_styles' );
+ $expected_output = $this->get_expected_styles_output( $expected );
+
+ $this->expectOutputString( $expected_output );
+ wp_print_font_faces();
+ }
+
+ private function get_expected_styles_output( $styles ) {
+ $style_element = "\n";
+ return sprintf( $style_element, $styles );
+ }
+}