diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php index 2231cb0f11538f..211108532c69b9 100644 --- a/lib/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/class-wp-theme-json-resolver-gutenberg.php @@ -838,7 +838,7 @@ public static function get_style_variations_from_directory( $directory, $scope = * as the value of `_link` object in REST API responses. * * @since 6.6.0 - * @since 6.7.0 Added support for resolving block styles. + * @since 6.7.0 Added support for resolving block style and font face URIs. * * @param WP_Theme_JSON_Gutenberg $theme_json A theme json instance. * @return array An array of resolved paths. @@ -855,6 +855,35 @@ public static function get_resolved_theme_uris( $theme_json ) { // Using the same file convention when registering web fonts. See: WP_Font_Face_Resolver:: to_theme_file_uri. $placeholder = 'file:./'; + // Add font URIs. + if ( ! empty( $theme_json_data['settings']['typography']['fontFamilies'] ) ) { + $font_families = array_merge( + $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, $placeholder ) ) { + $resolved_theme_uris[] = array( + 'name' => $source, + 'href' => sanitize_url( get_theme_file_uri( str_replace( $placeholder, '', $source ) ) ), + 'target' => "typography.fontFamilies.{$font_family['slug']}.fontFace.src", + ); + } + } + } + } + } + } + } + // Top level styles. $background_image_url = $theme_json_data['styles']['background']['backgroundImage']['url'] ?? null; if ( diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index cd4ad0cea50e0d..b37debe38cde4f 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -25,6 +25,7 @@ import { appendToSelector, getBlockStyleVariationSelector, getResolvedValue, + getResolvedThemeFilePath, } from './utils'; import { getBlockCSSSelector } from './get-block-css-selector'; import { getTypographyFontSizeValue } from './typography-utils'; @@ -884,6 +885,91 @@ export const toCustomProperties = ( tree, blockSelectors ) => { return ruleset; }; +/** + * Generates CSS @font-face declarations based on font settings in the theme.json tree. + * + * This function processes font families defined in the theme's typography settings, + * creating @font-face rules for each font face. It handles both file-based and URL-based + * font sources, resolving file paths to URLs when necessary. + * + * @param {Object} tree - The theme.json tree containing typography settings. + * @return {string} A string of CSS @font-face rules for the defined fonts. + * + * @example + * // Example output: + * // @font-face { + * // font-family: "Open Sans"; + * // font-style: normal; + * // font-weight: 400; + * // src: url('https://example.com/wp-content/themes/example/assets/fonts/open-sans-regular.woff2') format('woff2'); + * // } + */ +const getFontFaceDeclarations = ( tree ) => { + const fonts = tree?.settings?.typography?.fontFamilies; + let ruleset = ''; + + if ( ! fonts ) { + return ruleset; + } + + const themeFileURIs = tree?._links?.[ 'wp:theme-file' ] ?? []; + + // Iterate over main origins (theme, custom, default) + for ( const origin in fonts ) { + if ( Array.isArray( fonts[ origin ] ) ) { + fonts[ origin ].forEach( ( font ) => { + if ( font.fontFace ) { + font.fontFace.forEach( ( face ) => { + ruleset += '@font-face {\n'; + + // Optional properties + for ( const [ key, value ] of Object.entries( face ) ) { + if ( key !== 'src' ) { + ruleset += ` ${ kebabCase( + key + ) }: ${ value };\n`; + } + } + + if ( face.src ) { + const srcs = ( + face.src && Array.isArray( face.src ) + ? face.src + : [ face.src ] + ).map( ( src ) => { + if ( src.startsWith( 'file:' ) ) { + // Convert file path to URL and assume format based on extension + const resolvedSrc = + getResolvedThemeFilePath( + src, + themeFileURIs + ); + const format = resolvedSrc + .split( '.' ) + .pop(); + return `url('${ resolvedSrc.replace( + 'file:', + '' + ) }') format('${ format }')`; + } + // Assume it's already a URL and format is provided + return `url('${ src }')`; + } ); + ruleset += ` src: ${ srcs.join( + ',\n ' + ) };\n`; + } + + ruleset += '}\n\n'; + } ); + } + } ); + } + } + + return ruleset; +}; + export const toStyles = ( tree, blockSelectors, @@ -913,6 +999,8 @@ export const toStyles = ( let ruleset = ''; + ruleset += getFontFaceDeclarations( tree ); + if ( options.presets && ( contentSize || wideSize ) ) { ruleset += `${ ROOT_CSS_PROPERTIES_SELECTOR } {`; ruleset = contentSize @@ -1399,7 +1487,6 @@ export function useGlobalStylesOutputWithConfig( } ); const { getBlockStyles } = useSelect( blocksStore ); - return useMemo( () => { if ( ! mergedConfig?.styles || ! mergedConfig?.settings ) { return []; @@ -1415,7 +1502,6 @@ export function useGlobalStylesOutputWithConfig( updatedConfig, blockSelectors ); - const globalStyles = toStyles( updatedConfig, blockSelectors, diff --git a/packages/edit-site/src/components/global-styles/variations/variations-typography.js b/packages/edit-site/src/components/global-styles/variations/variations-typography.js index 65e84a9d965ffe..957d3bac97e9f9 100644 --- a/packages/edit-site/src/components/global-styles/variations/variations-typography.js +++ b/packages/edit-site/src/components/global-styles/variations/variations-typography.js @@ -15,7 +15,7 @@ import Subtitle from '../subtitle'; import Variation from './variation'; export default function TypographyVariations( { title, gap = 2 } ) { - const propertiesToFilter = [ 'typography' ]; + const propertiesToFilter = [ 'typography', '_links' ]; const typographyVariations = useCurrentMergeThemeStyleVariationsWithUserConfig( propertiesToFilter ); diff --git a/phpunit/class-wp-theme-json-resolver-test.php b/phpunit/class-wp-theme-json-resolver-test.php index d2339f2496290c..164d80edc8369d 100644 --- a/phpunit/class-wp-theme-json-resolver-test.php +++ b/phpunit/class-wp-theme-json-resolver-test.php @@ -1286,8 +1286,44 @@ public function test_resolve_theme_file_uris() { public function test_get_resolved_theme_uris() { $theme_json = new WP_Theme_JSON_Gutenberg( array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'settings' => array( + 'typography' => array( + 'fontFamilies' => array( + array( + 'fontFace' => array( + array( + 'fontFamily' => 'Tocco', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => array( + 'file:./example/fonts/tocco/tocco-400-normal.woff2', + ), + ), + ), + 'fontFamily' => 'Tocco, system-ui', + 'name' => 'Tocco', + 'slug' => 'secondary', + ), + array( + 'fontFace' => array( + array( + 'fontFamily' => '"Strozzapreti"', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => array( + 'file:./example/fonts/strozzapreti/strozzapreti-400-normal.woff2', + ), + ), + ), + 'fontFamily' => '"Strozzapreti", cursive', + 'name' => 'Strozzapreti', + 'slug' => 'primary', + ), + ), + ), + ), + 'styles' => array( 'background' => array( 'backgroundImage' => array( 'url' => 'file:./example/img/image.png', @@ -1314,6 +1350,16 @@ public function test_get_resolved_theme_uris() { ); $expected_data = array( + array( + 'name' => 'file:./example/fonts/tocco/tocco-400-normal.woff2', + 'href' => 'https://example.org/wp-content/themes/example-theme/example/fonts/tocco/tocco-400-normal.woff2', + 'target' => 'typography.fontFamilies.secondary.fontFace.src', + ), + array( + 'name' => 'file:./example/fonts/strozzapreti/strozzapreti-400-normal.woff2', + 'href' => 'https://example.org/wp-content/themes/example-theme/example/fonts/strozzapreti/strozzapreti-400-normal.woff2', + 'target' => 'typography.fontFamilies.primary.fontFace.src', + ), array( 'name' => 'file:./example/img/image.png', 'href' => 'https://example.org/wp-content/themes/example-theme/example/img/image.png',