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

Add a client-side font face resolver in the editor #65019

Open
wants to merge 20 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6219a6d
add editor font face resolver component
matiasbenedetto Sep 3, 2024
1e8ef55
remove code not needed
matiasbenedetto Sep 4, 2024
65f3754
default value when font families aren't defined
matiasbenedetto Sep 5, 2024
77f7638
comments formatting
matiasbenedetto Sep 9, 2024
0bed01d
use callback for loadFontFaceAsset
matiasbenedetto Sep 9, 2024
48bfe65
improve syntax
matiasbenedetto Sep 9, 2024
65d147a
Merge branch 'trunk' into add/editor-font-face-resolver
matiasbenedetto Sep 11, 2024
c6a32f9
Move EditorFontsResolver inside EditorStyles, use fontFamilies data f…
matiasbenedetto Sep 13, 2024
5a9e2d0
use a ref to reference the current document
matiasbenedetto Sep 13, 2024
27b5e64
currentTheme default to empty object
matiasbenedetto Sep 16, 2024
463fa95
revert changes on useDarkThemeBodyClassName, refactor useEditorFontsR…
matiasbenedetto Sep 18, 2024
9bd3378
revert not needed change
matiasbenedetto Sep 18, 2024
5cecd18
Merge branch 'trunk' into add/editor-font-face-resolver
matiasbenedetto Sep 20, 2024
c262896
Merge branch 'trunk' into add/editor-font-face-resolver
matiasbenedetto Sep 20, 2024
8ced871
try adding currentTheme to the editor settings
matiasbenedetto Sep 20, 2024
ee98b6e
add theme fonts uris
matiasbenedetto Sep 26, 2024
1461989
use _links in the font resolver
matiasbenedetto Sep 27, 2024
8172039
Merge branch 'trunk' into add/editor-font-face-resolver
matiasbenedetto Sep 30, 2024
7cb4c01
Fix PHP linting
getdave Oct 7, 2024
20a00e3
Use correct alias
getdave Oct 7, 2024
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: 4 additions & 0 deletions packages/block-editor/src/components/editor-styles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ] );

Expand Down Expand Up @@ -105,6 +106,9 @@ function EditorStyles( { styles, scope, transformOptions } ) {
<style
ref={ useDarkThemeBodyClassName( transformedStyles, scope ) }
/>

<style ref={ useEditorFontsResolver() } />

{ transformedStyles.map( ( css, index ) => (
<style key={ index }>{ css }</style>
) ) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* WordPress dependencies
*/
import { useState, useMemo, useCallback } from '@wordpress/element';
import { useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import { getDisplaySrcFromFontFace, loadFontFaceInBrowser } from './utils';
import { store as editorStore } from '../../store';
Copy link
Member

Choose a reason for hiding this comment

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

Let's use blockEditorStore to be specific and differentiate from the editor package's store.

Suggested change
import { store as editorStore } from '../../store';
import { store as blockEditorStore } from '../../store';


function useEditorFontsResolver() {
const [ loadedFontUrls, setLoadedFontUrls ] = useState( new Set() );

const { currentTheme = {}, fontFamilies = [] } = useSelect( ( select ) => {
return {
currentTheme:
// Disable Reason: Using 'core' as string to avoid circular dependency importing from @wordpress/core-data.
// eslint-disable-next-line @wordpress/data-no-store-string-literals
select( 'core' )?.getCurrentTheme(),
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not allowed in the block-editor package. We shouldn't add an eslint exception. Why do we need the theme object here?

Copy link
Contributor Author

@matiasbenedetto matiasbenedetto Sep 18, 2024

Choose a reason for hiding this comment

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

To get the base url of the theme's fonts.

Example: the theme.json data has something like file:/assets/fonts/one-font.woff2.
We need to know how to resolve that theme relative font URL in the browser.

Copy link
Contributor

Choose a reason for hiding this comment

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

We need to find an alternative solution. The block editor is a generic package independent of WordPress and can't make REST API calls.

The alternative I suggested initially is to "resolve" the files in the server and add a new _unstableType for the "styles" object received by the editor.

Copy link
Contributor Author

@matiasbenedetto matiasbenedetto Sep 18, 2024

Choose a reason for hiding this comment

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

to "resolve" the files in the server

Do you mean when the theme.json data is read from the file / database?

add a new _unstableType for the "styles" object received by the editor.

Sorry but I don't understand this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you mean when the theme.json data is read from the file / database?

Yes, basically in this place, in the code base. We read global styles and enqueue "styles" setting in the block editor. We could add a new "style" in that array with a specific "_unstableType" => "font" that we treat in a custom way in the EditorStyles component.

https://github.com/WordPress/wordpress-develop/blob/bf09fe506620678fb82c3b872309edda0ed8ce61/src/wp-includes/block-editor.php#L497-L553

I know that function is in WordPress Core but in Gutenberg we have a hook to override the output of this function block_editor_settings_all filter I think.


I know that we used this approach in the past for "svg filters" which are also "special styles" that are not just CSS. @ajlende would know more but I think we moved a little bit from it (not entirely sure why though)

Copy link
Contributor Author

@matiasbenedetto matiasbenedetto Sep 26, 2024

Choose a reason for hiding this comment

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

I'm trying to follow the pattern of this PR using the _links array. It seems to work with the regular theme.json, but I'm unable to access the links for the style variations. How I can pass the variation _settings to the editor settings?

Copy link
Member

Choose a reason for hiding this comment

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

I'm trying to understand the use case. What do we need to do with the resolved font paths? Preload the fonts? Could you provide some testing steps and expectations for style variations?

Just to confirm, if I run await wp.data.resolveSelect( 'core' ).__experimentalGetCurrentThemeGlobalStylesVariations() in the console I see that the variations' fonts are resolved in the _links object.

How I can pass the variation _settings to the editor settings?

Aren't the links already available in the settings?

It's private, so, in the useEditorFontsResolver useSelect you'd have to import the private symbol key (import { globalStylesLinksDataKey } from '../../store/private-keys';) and access it like settings[ globalStylesLinksDataKey ].

I just logged the settings out in that hook and I can see the font _links object, and when I change variations the links update.

I'm not sure that helps, maybe it leads you in the right direction?


Just to clarify and contrast what I was working on with background images, the resolved background image paths are used to build the correct CSS. It's done at this point in the global styles output generation:

export function getStylesDeclarations(
blockStyles = {},
selector = '',
useRootPaddingAlign,
tree = {},
disableRootPadding = false
) {

tree contains the embedded _links object and path resolution is done "on the fly" when outputting CSS.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do we need to do with the resolved font paths? Preload the fonts? Could you provide some testing steps and expectations for style variations?

This is the issue we are trying to fix: #59965
(load the fonts in the main frame and in the editor iframes when the fonts are defined in variations)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this commit I added the JS consumption of the _links. It seems to work for the main editor but not for the editors loaded to display the style variations.

The commit includes a console.log to display the font families and the _links.

const { _links = [], fontFamilies = [] } = useSelect( ( select ) => {
const { getSettings } = select( editorStore );
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 );

In the screencast you can see how the editors instances created to display the style variations doesn´t get the right list of font families (the ones from the style variation) and the _links array is empty.

Screencast.from.27-09-24.14.04.29.webm

Is there any way to get the right list of font families and _links in the editors used to feature a style variation?

Copy link
Member

Choose a reason for hiding this comment

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

So it looks like here you need all the variations' fonts at once for the sidebar. Is that right?

If you look at where the key value is set in the settings, you'll notice that the _links passed to settings contains only the resolved links for the merged global styles that are currently loaded in the editor.

That's why the resolved paths for style variations only work once you click on them, because it's it that point they're loaded into the editor.

So you'll probably need to iterate over the output of await wp.data.resolveSelect( 'core' ).__experimentalGetCurrentThemeGlobalStylesVariations() as hinted at above since they contain all the variations their resolved paths, and somehow preload the fonts for each variation.

However you can't use these core selectors in the block editor package, that is, in useEditorFontsResolver().

Rather, you might like to tackle from the edit-site package, where the style variations are rendered.

For example, export useEditorFontsResolver() as a private method and have it accept _links and fontFamilies as dependencies. I don't know if that will work, but it's the first thing I'd try without having extra context.

fontFamilies:
select( editorStore ).getSettings()?.__experimentalFeatures
?.typography?.fontFamilies,
};
}, [] );

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 = getDisplaySrcFromFontFace(
fontFace.src,
currentTheme?.stylesheet_uri
);

if ( ! src || loadedFontUrls.has( src ) ) {
return;
}

loadFontFaceInBrowser( fontFace, src, ownerDocument );
setLoadedFontUrls( ( prevUrls ) => new Set( prevUrls ).add( src ) );
},
[ currentTheme, loadedFontUrls ]
);

return useCallback(
( node ) => {
if ( ! node ) {
return;
}

const { ownerDocument } = node;
fontFaces.forEach( ( fontFace ) =>
loadFontFaceAsset( fontFace, ownerDocument )
);
},
[ fontFaces, loadFontFaceAsset ]
);
}

export default useEditorFontsResolver;
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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();
}
Comment on lines +21 to +27
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we describe why we need to do this? I can see what but at a casual glance I'm not sure as to why.

// 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 }"`;
}
Comment on lines +31 to +34
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if we just let all browsers have the wrapped version?

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

function isUrlEncoded( url ) {
if ( typeof url !== 'string' ) {
return false;
}
return url !== decodeURIComponent( url );
}

/*
* Retrieves the display source from a font face src.
*
* @param {string|string[]} fontSrc - The font face src.
* @param {string} baseUrl - The base URL to resolve the src.
* @return {string|undefined} The display source or undefined if the input is invalid.
*/
export function getDisplaySrcFromFontFace( fontSrc, baseUrl ) {
if ( ! fontSrc ) {
return;
}

let src;
if ( Array.isArray( fontSrc ) ) {
src = fontSrc[ 0 ];
} else {
src = fontSrc;
}

if ( ! isUrlEncoded( src ) ) {
src = encodeURI( src );
}

// If baseUrl is provided, use it to resolve the src.
if ( src.startsWith( 'file:.' ) ) {
src = baseUrl + '/' + src.replace( 'file:./', '' );
}

return src;
}
Loading