Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Font Face: to generate and print font-face styles for theme.json fonts #51770

Merged
merged 38 commits into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3ffdfa1
Add WP_Font_Face_Resolver.
hellofromtonya Jun 21, 2023
fe8106c
Add WP_Font_Face.
hellofromtonya Jun 22, 2023
017862e
Add wp_print_font_faces().
hellofromtonya Jun 22, 2023
f91f244
Relocate iframed editor assets font printing.
hellofromtonya Jun 22, 2023
aaeb453
Load files & guard usage of Fonts API.
hellofromtonya Jun 22, 2023
0cf9f63
Fix wp_print_font_faces().
hellofromtonya Jun 22, 2023
5e189d4
Fixed property typo
hellofromtonya Jun 22, 2023
5aed83c
Test for Test WP_Font_Face::generate_and_print.
anton-vlasenko Jun 26, 2023
2f5f107
Fix class name.
anton-vlasenko Jun 26, 2023
ab03732
Fix the test.
anton-vlasenko Jun 26, 2023
9048aaf
Don't load WP_Fonts_TestCase
anton-vlasenko Jun 26, 2023
771934c
Don't print the last \n character
anton-vlasenko Jun 27, 2023
458b1dd
Add a comment.
anton-vlasenko Jun 27, 2023
818259d
Add more data to the dataprovider.
anton-vlasenko Jun 27, 2023
5f172e9
Rename files.
anton-vlasenko Jun 27, 2023
ac45ee2
Fix fatal error.
anton-vlasenko Jun 27, 2023
bd8b39c
Fix package name.
anton-vlasenko Jun 27, 2023
610f403
Add a draft test for WP_Font_Face_Resolver.
anton-vlasenko Jun 28, 2023
2986f7e
Commit draft test.
anton-vlasenko Jun 28, 2023
3a1af83
Return an empty array if no fonts are defined. This method should alw…
anton-vlasenko Jun 28, 2023
2fc983c
Improve the test.
anton-vlasenko Jun 28, 2023
d9a4e36
CS fix
aristath Jun 29, 2023
265b5e0
Fix font names.
anton-vlasenko Jun 29, 2023
b2e0a09
Fix package name.
anton-vlasenko Jun 29, 2023
3f90a70
Remove unrelated tests.
anton-vlasenko Jun 29, 2023
5c23eab
Add a test for placeholders.
anton-vlasenko Jun 29, 2023
674187e
Fix property names.
anton-vlasenko Jun 29, 2023
cf95e34
Fix DocBlocks.
anton-vlasenko Jun 29, 2023
a59bb60
Fix CS.
anton-vlasenko Jun 29, 2023
b878840
Fix test class names.
anton-vlasenko Jun 29, 2023
e392dca
Tests for wp_print_font_faces().
anton-vlasenko Jun 29, 2023
41860aa
Improve the test and kick off CI checks.
anton-vlasenko Jun 29, 2023
7fedb60
Fix the failing test.
anton-vlasenko Jun 29, 2023
33f65a2
Quick and dirty fix for non-iframed editor. See https://wordpress.sla…
anton-vlasenko Jul 5, 2023
c6746db
Revert "Quick and dirty fix for non-iframed editor. See https://wordp…
anton-vlasenko Jul 5, 2023
8ef75f1
Test reorganization.
hellofromtonya Jul 6, 2023
6359091
Add wp_print_fonts() tests.
hellofromtonya Jul 6, 2023
6f8e601
Bail out if no fonts to process.
hellofromtonya Jul 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/class-wp-theme-json-resolver-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.

}
Expand Down
1 change: 0 additions & 1 deletion lib/compat/wordpress-6.3/script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
14 changes: 14 additions & 0 deletions lib/experimental/fonts-api/fonts-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
158 changes: 158 additions & 0 deletions lib/experimental/fonts/class-wp-font-face-resolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php
/**
* WP_Font_Face_Resolver class.
*
* @package WordPress
* @subpackage Fonts
* @since X.X.X
*/

if ( class_exists( 'WP_Font_Face_Resolver' ) ) {
return;
}

/**
* The Font Face Resolver abstracts the processing of different data sources
* (such as theme.json) for processing within the Font Face.
*
* This class is for internal core usage and is not supposed to be used by
* extenders (plugins and/or themes).
*
* @access private
*/
class WP_Font_Face_Resolver {

/**
* Gets fonts defined in theme.json.
*
* @since X.X.X
*
* @return array Returns the font-families, each with their font-face variations.
*/
public static function get_fonts_from_theme_json() {
$settings = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data()->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'] );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is in Core already, but it relates to a feature being built for 6.6: relative paths to background images in theme.json styles

For consistency, and after a nudge from @creativecoder, I decided to follow the file:./ dotslash path convention to represent paths to these images.

@noisysocks then pointed out that dotslash indicates the current directory, and might imply other dot-style paths, e.g., ../

So, if web fonts can be used in theme variations, e.g., my-theme/styles/variation-with-fonts.json, the ./ would indicate a path relative to the styles directory.

It might also imply that paths with ../../some/path.file could also be resolved. But I'm not sure if get_theme_file_uri() supports such things as it looks for files relative to the active or parent theme root. Maybe I'm missing something.

https://github.com/WordPress/gutenberg/blob/trunk/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php#L147C27-L147C44

I was wondering if we should enforce a convention. Either:

  1. Enforce that all paths must be relative to the theme root, or
  2. Allow relative to current directory, and then in resolve them in the background with PHP trickery with realpath or something.

What do folks think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean... the syntax and behavior seem mismatched: file:./ seems to imply a reference to the current directory, but really it's a placeholder for the theme root looking at how it's coded.

Updating file:./ to apply to the current directory (e.g. theme-root/styles) for theme variations and/or adding resolution of ../ paths would make the syntax clearer. But I worry about backward compatibility (I imagine that some theme variations are already using file:./?). And some care is needed to prevent directory traversal outside the theme directory.

So I favor keeping things simple, and continuing to use file:./ simply as a placeholder to the theme directory root. But it's not a strong opinion.

(Note that either way, we should add to and update the theme handbook docs to make it clearer).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick reply @creativecoder

So I favor keeping things simple, and continuing to use file:./ simply as a placeholder to the theme directory root.

👍🏻

I went with this approach for now given that folks might be using to it already.

Updating file:./ to apply to the current directory (e.g. theme-root/styles) for theme variations and/or adding resolution of ../ paths would make the syntax clearer. But I worry about backward compatibility (I imagine that some theme variations are already using file:./?).

True. Backwards compatibility is now a requirement regardless. And I considered this in #61271 and contemplated supporting file:./ (with the ./ for folks who are now used to it) and also a file: prefix.

But I agree that it's simpler to go with what's there until folks come up with a viable alternative. Even if it's "Enforce that all paths must be relative to the theme root" - that way, we could update the docs now and make the ./ optional later without adding any complex ../ support etc. 🤷🏻

}

// 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;
}
}
Loading
Loading