diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php b/lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php index 4a01f748d16e42..cd402a8e545003 100644 --- a/lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php @@ -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( diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php b/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php index 23c95d633fb47b..3ba5225f491011 100644 --- a/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php @@ -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 @@ -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 ); } /** diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index 04eabb149f5f4e..35276b0ad8b2b4 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -392,7 +392,7 @@ function FontLibraryProvider( { children } ) { loadFontFaceInBrowser( face, getDisplaySrcFromFontFace( face.src ), - 'iframe' + 'all' ); } ); } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js index d0a53a4acea4f8..011f09b12a841f 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js @@ -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 @@ -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, diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/make-families-from-faces.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/make-families-from-faces.js index cbc995bedefd45..7cdbf405c0b5bc 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/make-families-from-faces.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/make-families-from-faces.js @@ -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: [], }; } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/preview-styles.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/preview-styles.js index 389cebde9249af..9cadc90448dfc4 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/preview-styles.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/preview-styles.js @@ -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 ) { diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/preview-styles.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/preview-styles.spec.js index 0273709502a43d..4fa18e00f2a8dd 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/preview-styles.spec.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/preview-styles.spec.js @@ -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', () => { @@ -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' ); } ); @@ -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"' ); + } ); } );