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 and youknowriad committed Feb 20, 2024
1 parent 76971f7 commit bd03fa6
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"' );
} );
} );

0 comments on commit bd03fa6

Please sign in to comment.