Skip to content

Commit

Permalink
Font Library: fixes installed font families not rendering in the edit…
Browse files Browse the repository at this point in the history
…or or frontend. (#59019)

* Improves the sanitize_font_family function to output CSS valid font-family values according to CSS spec

* add  font-family css specific sanitization for fontFamily properties of the fonts from font collections

* use _wp_to_kebab_case to format the font slug property of fonts from font collections

* format comment

* improving fontFontFamily and add formatFontFaceName functions to ensure that CSS properties are valid

* load font face in both iframe and document

* use the wordpress function to generate slugs for font files uploads

* lint variable names

* add exception for firefox in the font face name formatting

* format php

* improve php check

* format php

* replace firefox by gecko to cover all gecko engine browsers

Co-authored-by: Juan Aldasoro <252415+juanfra@users.noreply.github.com>

* remove not needed repeated call

* using firefox and fxios to detect firefox browser user agent

---------

Co-authored-by: Juan Aldasoro <252415+juanfra@users.noreply.github.com>

Co-authored-by: matiasbenedetto <mmaattiiaass@git.wordpress.org>
Co-authored-by: juanfra <juanfra@git.wordpress.org>
Co-authored-by: arthur791004 <arthur791004@git.wordpress.org>
Co-authored-by: richtabor <richtabor@git.wordpress.org>
Co-authored-by: pbking <pbking@git.wordpress.org>
Co-authored-by: getdave <get_dave@git.wordpress.org>
Co-authored-by: okmttdhr <okat@git.wordpress.org>
Co-authored-by: nith53 <nithins53@git.wordpress.org>
  • Loading branch information
9 people authored Feb 14, 2024
1 parent 620b7d1 commit 0a82d9f
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 45 deletions.
6 changes: 4 additions & 2 deletions lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,10 @@ private static function get_sanitization_schema() {
array(
'font_family_settings' => array(
'name' => 'sanitize_text_field',
'slug' => 'sanitize_title',
'fontFamily' => 'sanitize_text_field',
'slug' => static function ( $value ) {
return _wp_to_kebab_case( sanitize_title( $value ) );
},
'fontFamily' => 'WP_Font_Utils::sanitize_font_family',
'preview' => 'sanitize_url',
'fontFace' => array(
array(
Expand Down
61 changes: 42 additions & 19 deletions lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,41 @@
* @access private
*/
class WP_Font_Utils {

/**
* Adds surrounding quotes to font family names that contain special characters.
*
* It follows the recommendations from the CSS Fonts Module Level 4.
* @link https://www.w3.org/TR/css-fonts-4/#font-family-prop
*
* @since 6.5.0
* @access private
*
* @see sanitize_font_family()
*
* @param string $item A font family name.
* @return string The font family name with surrounding quotes if necessary.
*/
private static function maybe_add_quotes( $item ) {
// Match any non alphabetic characters (a-zA-Z), dashes -, or parenthesis ().
$regex = '/[^a-zA-Z\-()]+/';
$item = trim( $item );
if ( preg_match( $regex, $item ) ) {
// Removes leading and trailing quotes.
$item = preg_replace( '/^["\']|["\']$/', '', $item );
return "\"$item\"";
}
return $item;
}

/**
* Sanitizes and formats font family names.
*
* - Applies `sanitize_text_field`
* - Adds surrounding quotes to names that contain spaces and are not already quoted
* - Adds surrounding quotes to names that special
*
* It follows the recommendations from the CSS Fonts Module Level 4.
* @link https://www.w3.org/TR/css-fonts-4/#font-family-prop
*
* @since 6.5.0
* @access private
Expand All @@ -39,26 +69,19 @@ public static function sanitize_font_family( $font_family ) {
return '';
}

$font_family = sanitize_text_field( $font_family );
$font_families = explode( ',', $font_family );
$wrapped_font_families = array_map(
function ( $family ) {
$trimmed = trim( $family );
if ( ! empty( $trimmed ) && str_contains( $trimmed, ' ' ) && ! str_contains( $trimmed, "'" ) && ! str_contains( $trimmed, '"' ) ) {
return '"' . $trimmed . '"';
$output = trim( sanitize_text_field( $font_family ) );
$formatted_items = array();
if ( str_contains( $output, ',' ) ) {
$items = explode( ',', $output );
foreach ( $items as $item ) {
$formatted_item = self::maybe_add_quotes( $item );
if ( ! empty( $formatted_item ) ) {
$formatted_items[] = $formatted_item;
}
return $trimmed;
},
$font_families
);

if ( count( $wrapped_font_families ) === 1 ) {
$font_family = $wrapped_font_families[0];
} else {
$font_family = implode( ', ', $wrapped_font_families );
}
return implode( ', ', $formatted_items );
}

return $font_family;
return self::maybe_add_quotes( $output );
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ function FontLibraryProvider( { children } ) {
loadFontFaceInBrowser(
face,
getDisplaySrcFromFontFace( face.src ),
'iframe'
'all'
);
} );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { privateApis as componentsPrivateApis } from '@wordpress/components';
import { FONT_WEIGHTS, FONT_STYLES } from './constants';
import { unlock } from '../../../../lock-unlock';
import { fetchInstallFontFace } from '../resolvers';
import { formatFontFamily } from './preview-styles';
import { formatFontFaceName } from './preview-styles';

/**
* Browser dependencies
Expand Down Expand Up @@ -99,7 +99,7 @@ export async function loadFontFaceInBrowser( fontFace, source, addTo = 'all' ) {
}

const newFont = new window.FontFace(
formatFontFamily( fontFace.fontFamily ),
formatFontFaceName( fontFace.fontFamily ),
dataSource,
{
style: fontFace.fontStyle,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
/**
* WordPress dependencies
*/
import { privateApis as componentsPrivateApis } from '@wordpress/components';

/**
* Internal dependencies
*/
import { unlock } from '../../../../lock-unlock';

const { kebabCase } = unlock( componentsPrivateApis );

export default function makeFamiliesFromFaces( fontFaces ) {
const fontFamiliesObject = fontFaces.reduce( ( acc, item ) => {
if ( ! acc[ item.fontFamily ] ) {
acc[ item.fontFamily ] = {
name: item.fontFamily,
fontFamily: item.fontFamily,
slug: item.fontFamily.replace( /\s+/g, '-' ).toLowerCase(),
slug: kebabCase( item.fontFamily.toLowerCase() ),
fontFace: [],
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,79 @@ function extractFontWeights( fontFaces ) {
return result;
}

/*
* Format the font family to use in the CSS font-family property of a CSS rule.
*
* The input can be a string with the font family name or a string with multiple font family names separated by commas.
* It follows the recommendations from the CSS Fonts Module Level 4.
* https://www.w3.org/TR/css-fonts-4/#font-family-prop
*
* @param {string} input - The font family.
* @return {string} The formatted font family.
*
* Example:
* formatFontFamily( "Open Sans, Font+Name, sans-serif" ) => '"Open Sans", "Font+Name", sans-serif'
* formatFontFamily( "'Open Sans', sans-serif" ) => '"Open Sans", sans-serif'
* formatFontFamily( "DotGothic16, Slabo 27px, serif" ) => '"DotGothic16","Slabo 27px",serif'
* formatFontFamily( "Mine's, Moe's Typography" ) => `"mine's","Moe's Typography"`
*/
export function formatFontFamily( input ) {
return input
.split( ',' )
.map( ( font ) => {
font = font.trim(); // Remove any leading or trailing white spaces
// If the font doesn't start with quotes and contains a space, then wrap in quotes.
// Check that string starts with a single or double quote and not a space
if (
! ( font.startsWith( '"' ) || font.startsWith( "'" ) ) &&
font.indexOf( ' ' ) !== -1
) {
return `"${ font }"`;
}
return font; // Return font as is if no transformation is needed
} )
.join( ', ' );
// Matchs any non alphabetic characters (a-zA-Z), dashes - , or parenthesis ()
const regex = /[^a-zA-Z\-()]+/;
const output = input.trim();

const formatItem = ( item ) => {
item = item.trim();
if ( item.match( regex ) ) {
// removes leading and trailing quotes.
item = item.replace( /^["']|["']$/g, '' );
return `"${ item }"`;
}
return item;
};

if ( output.includes( ',' ) ) {
return output
.split( ',' )
.map( formatItem )
.filter( ( item ) => item !== '' )
.join( ', ' );
}

return formatItem( output );
}

/*
* 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"
*/
export function formatFontFaceName( input ) {
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().match( /firefox|fxios/i ) ) {
output = `"${ output }"`;
}
return output;
}

export function getFamilyPreviewStyle( family ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
/**
* Internal dependencies
*/
import { getFamilyPreviewStyle, formatFontFamily } from '../preview-styles';
import {
getFamilyPreviewStyle,
formatFontFamily,
formatFontFaceName,
} from '../preview-styles';

describe( 'getFamilyPreviewStyle', () => {
it( 'should return default fontStyle and fontWeight if fontFace is not provided', () => {
Expand Down Expand Up @@ -139,7 +143,7 @@ describe( 'formatFontFamily', () => {
"Seravek, 'Gill Sans Nova', Ubuntu, Calibri, 'DejaVu Sans', source-sans-pro, sans-serif"
)
).toBe(
"Seravek, 'Gill Sans Nova', Ubuntu, Calibri, 'DejaVu Sans', source-sans-pro, sans-serif"
'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, sans-serif'
);
} );

Expand All @@ -153,9 +157,50 @@ describe( 'formatFontFamily', () => {
);
} );

it( 'should wrap only those font names with spaces which are not already quoted', () => {
expect( formatFontFamily( 'Baloo Bhai 2, Arial' ) ).toBe(
'"Baloo Bhai 2", Arial'
it( 'should wrap names with special characters in quotes', () => {
expect(
formatFontFamily(
'Font+Name, Font*Name, _Font_Name_, generic(kai), sans-serif'
)
).toBe(
'"Font+Name", "Font*Name", "_Font_Name_", generic(kai), sans-serif'
);
} );

it( 'should fix empty wrong formatted font family', () => {
expect( formatFontFamily( ', Abril Fatface,Times,serif' ) ).toBe(
'"Abril Fatface", Times, serif'
);
} );
} );

describe( 'formatFontFaceName', () => {
it( 'should remove leading and trailing quotes', () => {
expect( formatFontFaceName( '"Open Sans"' ) ).toBe( 'Open Sans' );
} );

it( 'should remove leading and trailing quotes from multiple font face names', () => {
expect(
formatFontFaceName( "'Open Sans', 'Helvetica Neue', sans-serif" )
).toBe( 'Open Sans' );
} );

it( 'should remove leading and trailing quotes even from names with spaces and special characters', () => {
expect( formatFontFaceName( "'Font+Name 24', sans-serif" ) ).toBe(
'Font+Name 24'
);
} );

it( 'should ouput the font face name with quotes on Firefox', () => {
const mockUserAgent =
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0';

// Mock the userAgent for this test
Object.defineProperty( window.navigator, 'userAgent', {
value: mockUserAgent,
configurable: true,
} );

expect( formatFontFaceName( 'Open Sans' ) ).toBe( '"Open Sans"' );
} );
} );

1 comment on commit 0a82d9f

@github-actions
Copy link

Choose a reason for hiding this comment

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

Flaky tests detected in 0a82d9f.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/7906889634
📝 Reported issues:

Please sign in to comment.