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 } ) { ) ) } 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 );