diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php
index 2231cb0f11538..8d520256a2da2 100644
--- a/lib/class-wp-theme-json-resolver-gutenberg.php
+++ b/lib/class-wp-theme-json-resolver-gutenberg.php
@@ -831,6 +831,50 @@ public static function get_style_variations_from_directory( $directory, $scope =
return $variations;
}
+ /**
+ * Resolves relative paths in theme.json typography to theme absolute paths
+ * and returns them in an array that can be embedded
+ * as the value of `_link` object in REST API responses.
+ *
+ * @since 6.6.0
+ *
+ * @param
+ * @return array An array of resolved paths.
+ */
+ private static function get_resolved_fonts_theme_uris( $theme_json_data ) {
+ $resolved_theme_uris = array();
+
+ if ( ! empty( $theme_json_data['settings']['typography']['fontFamilies'] ) ) {
+
+ $font_families = ( $theme_json_data['settings']['typography']['fontFamilies']['theme'] ?? array() )
+ + ( $theme_json_data['settings']['typography']['fontFamilies']['custom'] ?? array() )
+ + ( $theme_json_data['settings']['typography']['fontFamilies']['default'] ?? array() );
+
+ foreach ( $font_families as $font_family ) {
+ if ( ! empty( $font_family['fontFace'] ) ) {
+ foreach ( $font_family['fontFace'] as $font_face ) {
+ if ( ! empty( $font_face['src'] ) ) {
+ $sources = is_string( $font_face['src'] )
+ ? array( $font_face['src'] )
+ : $font_face['src'];
+ foreach ( $sources as $source ) {
+ if ( str_starts_with( $source, 'file:' ) ) {
+ $resolved_theme_uris[] = array(
+ 'name' => $source,
+ 'href' => sanitize_url( get_theme_file_uri( str_replace( 'file:./', '', $source ) ) ),
+ 'target' => "typography.fontFamilies.{$font_family['slug']}.fontFace.src",
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return $resolved_theme_uris;
+ }
+
/**
* Resolves relative paths in theme.json styles to theme absolute paths
@@ -852,6 +896,12 @@ public static function get_resolved_theme_uris( $theme_json ) {
$theme_json_data = $theme_json->get_raw_data();
+ // Add font URIs.
+ $resolved_theme_uris = array_merge(
+ $resolved_theme_uris,
+ static::get_resolved_fonts_theme_uris( $theme_json_data )
+ );
+
// Using the same file convention when registering web fonts. See: WP_Font_Face_Resolver:: to_theme_file_uri.
$placeholder = 'file:./';
diff --git a/packages/block-editor/src/components/editor-styles/index.js b/packages/block-editor/src/components/editor-styles/index.js
index a59ac310bcd30..8af3b493e4496 100644
--- a/packages/block-editor/src/components/editor-styles/index.js
+++ b/packages/block-editor/src/components/editor-styles/index.js
@@ -18,6 +18,7 @@ import { useSelect } from '@wordpress/data';
import transformStyles from '../../utils/transform-styles';
import { store as blockEditorStore } from '../../store';
import { unlock } from '../../lock-unlock';
+import useEditorFontsResolver from '../use-editor-fonts-resolver';
extend( [ namesPlugin, a11yPlugin ] );
@@ -105,6 +106,9 @@ function EditorStyles( { styles, scope, transformOptions } ) {
+
+
+
{ transformedStyles.map( ( css, index ) => (
) ) }
diff --git a/packages/block-editor/src/components/use-editor-fonts-resolver/index.js b/packages/block-editor/src/components/use-editor-fonts-resolver/index.js
new file mode 100644
index 0000000000000..c240e76b29309
--- /dev/null
+++ b/packages/block-editor/src/components/use-editor-fonts-resolver/index.js
@@ -0,0 +1,76 @@
+/**
+ * WordPress dependencies
+ */
+import { useState, useMemo, useCallback } from '@wordpress/element';
+import { useSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { loadFontFaceInBrowser } from './utils';
+import { store as blockEditorStore } from '../../store';
+import { globalStylesLinksDataKey } from '../../store/private-keys';
+
+function resolveThemeFontFaceSrc( src, _links ) {
+ const firstSrc = Array.isArray( src ) ? src[ 0 ] : src;
+ return _links.find( ( link ) => link.name === firstSrc )?.href || firstSrc;
+}
+
+function useEditorFontsResolver() {
+ const [ loadedFontUrls, setLoadedFontUrls ] = useState( new Set() );
+
+ const { _links = [], fontFamilies = [] } = useSelect( ( select ) => {
+ const { getSettings } = select( blockEditorStore );
+ const _settings = getSettings();
+ return {
+ _links: _settings[ globalStylesLinksDataKey ]?.[ 'wp:theme-file' ],
+ fontFamilies:
+ _settings?.__experimentalFeatures?.typography?.fontFamilies,
+ };
+ }, [] );
+
+ // eslint-disable-next-line no-console
+ console.log( 'fontFamilies', fontFamilies, 'links', _links );
+
+ const fontFaces = useMemo( () => {
+ return Object.values( fontFamilies )
+ .flat()
+ .map( ( family ) => family.fontFace )
+ .filter( Boolean )
+ .flat();
+ }, [ fontFamilies ] );
+
+ const loadFontFaceAsset = useCallback(
+ async ( fontFace, ownerDocument ) => {
+ if ( ! fontFace.src ) {
+ return;
+ }
+
+ const src = resolveThemeFontFaceSrc( fontFace.src, _links );
+
+ if ( ! src || loadedFontUrls.has( src ) ) {
+ return;
+ }
+
+ loadFontFaceInBrowser( fontFace, src, ownerDocument );
+ setLoadedFontUrls( ( prevUrls ) => new Set( prevUrls ).add( src ) );
+ },
+ [ loadedFontUrls, _links ]
+ );
+
+ return useCallback(
+ ( node ) => {
+ if ( ! node ) {
+ return;
+ }
+
+ const { ownerDocument } = node;
+ fontFaces.forEach( ( fontFace ) =>
+ loadFontFaceAsset( fontFace, ownerDocument )
+ );
+ },
+ [ fontFaces, loadFontFaceAsset ]
+ );
+}
+
+export default useEditorFontsResolver;
diff --git a/packages/block-editor/src/components/use-editor-fonts-resolver/utils.js b/packages/block-editor/src/components/use-editor-fonts-resolver/utils.js
new file mode 100644
index 0000000000000..4bd2f5db26aa0
--- /dev/null
+++ b/packages/block-editor/src/components/use-editor-fonts-resolver/utils.js
@@ -0,0 +1,65 @@
+/*
+ * Format the font face name to use in the font-family property of a font face.
+ *
+ * The input can be a string with the font face name or a string with multiple font face names separated by commas.
+ * It removes the leading and trailing quotes from the font face name.
+ *
+ * @param {string} input - The font face name.
+ * @return {string} The formatted font face name.
+ *
+ * Example:
+ * formatFontFaceName("Open Sans") => "Open Sans"
+ * formatFontFaceName("'Open Sans', sans-serif") => "Open Sans"
+ * formatFontFaceName(", 'Open Sans', 'Helvetica Neue', sans-serif") => "Open Sans"
+ */
+function formatFontFaceName( input ) {
+ if ( ! input ) {
+ return '';
+ }
+
+ let output = input.trim();
+ if ( output.includes( ',' ) ) {
+ output = output
+ .split( ',' )
+ // Finds the first item that is not an empty string.
+ .find( ( item ) => item.trim() !== '' )
+ .trim();
+ }
+ // Removes leading and trailing quotes.
+ output = output.replace( /^["']|["']$/g, '' );
+
+ // Firefox needs the font name to be wrapped in double quotes meanwhile other browsers don't.
+ if ( window.navigator.userAgent.toLowerCase().includes( 'firefox' ) ) {
+ output = `"${ output }"`;
+ }
+ return output;
+}
+
+/*
+ * Loads the font face from a URL and adds it to the browser.
+ * It also adds it to the iframe document.
+ */
+export async function loadFontFaceInBrowser( fontFace, source, documentRef ) {
+ if ( typeof source !== 'string' ) {
+ return;
+ }
+ const dataSource = `url(${ source })`;
+ const newFont = new window.FontFace(
+ formatFontFaceName( fontFace.fontFamily ),
+ dataSource,
+ {
+ style: fontFace.fontStyle,
+ weight: fontFace.fontWeight,
+ }
+ );
+
+ const loadedFace = await newFont.load();
+
+ // Add the font to the ref document.
+ documentRef.fonts.add( loadedFace );
+
+ // Add the font to the window document.
+ if ( documentRef !== window.document ) {
+ window.document.fonts.add( loadedFace );
+ }
+}
diff --git a/packages/edit-site/src/components/global-styles-renderer/index.js b/packages/edit-site/src/components/global-styles-renderer/index.js
index 2e840a7acdc37..0b65627a7bb44 100644
--- a/packages/edit-site/src/components/global-styles-renderer/index.js
+++ b/packages/edit-site/src/components/global-styles-renderer/index.js
@@ -4,6 +4,7 @@
import { useEffect } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
+import { store as coreDataStore } from '@wordpress/core-data';
/**
* Internal dependencies
@@ -15,12 +16,18 @@ import { TEMPLATE_POST_TYPE } from '../../utils/constants';
const { useGlobalStylesOutput } = unlock( blockEditorPrivateApis );
function useGlobalStylesRenderer() {
- const postType = useSelect( ( select ) => {
- return select( editSiteStore ).getEditedPostType();
+ const { postType, currentTheme } = useSelect( ( select ) => {
+ return {
+ postType: select( editSiteStore ).getEditedPostType(),
+ currentTheme: select( coreDataStore ).getCurrentTheme(),
+ };
} );
const [ styles, settings ] = useGlobalStylesOutput(
postType !== TEMPLATE_POST_TYPE
);
+
+ settings.currentTheme = currentTheme;
+
const { getSettings } = useSelect( editSiteStore );
const { updateSettings } = useDispatch( editSiteStore );