diff --git a/packages/global-styles-ui/src/font-library/upload-fonts.tsx b/packages/global-styles-ui/src/font-library/upload-fonts.tsx index e9fb375f0c70c6..8b11439c510605 100644 --- a/packages/global-styles-ui/src/font-library/upload-fonts.tsx +++ b/packages/global-styles-ui/src/font-library/upload-fonts.tsx @@ -13,7 +13,6 @@ import { ProgressBar, } from '@wordpress/components'; import { useContext, useState } from '@wordpress/element'; -import type { FontFace } from '@wordpress/core-data'; /** * Internal dependencies @@ -22,7 +21,21 @@ import { ALLOWED_FILE_EXTENSIONS } from './utils/constants'; import { FontLibraryContext } from './context'; import { Font } from './lib/lib-font.browser'; import makeFamiliesFromFaces from './utils/make-families-from-faces'; -import { loadFontFaceInBrowser } from './utils'; +import { loadFontFaceInBrowser, normalizeCSSFontFaceFontFamily } from './utils'; + +export interface FontFaceMetadata { + /* + * Font name for display + */ + name: string; + + /** + * CSS @font-face font-family value. + */ + fontFamily: string; + fontStyle?: string | undefined; + fontWeight?: string | number | undefined; +} function UploadFonts() { const { installFonts } = useContext( FontLibraryContext ); @@ -108,11 +121,7 @@ function UploadFonts() { const fontFacesLoaded = await Promise.all( files.map( async ( fontFile: File ) => { const fontFaceData = await getFontFaceMetadata( fontFile ); - await loadFontFaceInBrowser( - fontFaceData, - fontFaceData.file, - 'all' - ); + await loadFontFaceInBrowser( fontFaceData, fontFile, 'all' ); return fontFaceData; } ) ); @@ -146,7 +155,9 @@ function UploadFonts() { } ); } - const getFontFaceMetadata = async ( fontFile: File ) => { + const getFontFaceMetadata = async ( + fontFile: File + ): Promise< FontFaceMetadata > => { const buffer = await readFileAsArrayBuffer( fontFile ); const fontObj: Font & { onload?: ( val: { detail: { font: any } } ) => void; @@ -171,9 +182,12 @@ function UploadFonts() { const weightRange = weightAxis ? `${ weightAxis.minValue } ${ weightAxis.maxValue }` : null; + + const cssFontFamily = normalizeCSSFontFaceFontFamily( fontName ); + return { - file: fontFile, - fontFamily: fontName, + name: fontName, + fontFamily: cssFontFamily, fontStyle: isItalic ? 'italic' : 'normal', fontWeight: weightRange || fontWeight, }; @@ -185,7 +199,7 @@ function UploadFonts() { * @param {Array} fontFaces The font faces to be installed * @return {void} */ - const handleInstall = async ( fontFaces: FontFace[] ) => { + const handleInstall = async ( fontFaces: FontFaceMetadata[] ) => { const fontFamilies = makeFamiliesFromFaces( fontFaces ); try { diff --git a/packages/global-styles-ui/src/font-library/utils/index.ts b/packages/global-styles-ui/src/font-library/utils/index.ts index 8feddd77c44f4d..bdadbef0f947aa 100644 --- a/packages/global-styles-ui/src/font-library/utils/index.ts +++ b/packages/global-styles-ui/src/font-library/utils/index.ts @@ -10,7 +10,6 @@ import type { DataRegistry } from '@wordpress/data'; */ import { FONT_WEIGHTS, FONT_STYLES } from './constants'; import { fetchInstallFontFace } from '../api'; -import { formatFontFaceName } from './preview-styles'; import type { FontFamilyToUpload, FontUploadResult } from '../types'; import { unlock } from '../../lock-unlock'; @@ -114,14 +113,10 @@ export async function loadFontFaceInBrowser( return; } - const newFont = new window.FontFace( - formatFontFaceName( fontFace.fontFamily ), - dataSource, - { - style: fontFace.fontStyle, - weight: String( fontFace.fontWeight ), - } - ); + const newFont = new window.FontFace( fontFace.fontFamily, dataSource, { + style: fontFace.fontStyle, + weight: String( fontFace.fontWeight ), + } ); const loadedFace = await newFont.load(); @@ -155,7 +150,7 @@ export function unloadFontFaceInBrowser( const unloadFontFace = ( fonts: FontFaceSet ) => { fonts.forEach( ( f ) => { if ( - f.family === formatFontFaceName( fontFace?.fontFamily ) && + f.family === fontFace?.fontFamily && f.weight === fontFace?.fontWeight && f.style === fontFace?.fontStyle ) { @@ -368,3 +363,37 @@ export function checkFontFaceInstalled( } ) ); } + +export function normalizeCSSFontFaceFontFamily( fontName: string ): string { + return `"${ fontName + .trim() + + /* + * CSS Unicode escaping for problematic characters. + * https://www.w3.org/TR/css-syntax-3/#escaping + * + * These characters are not required by CSS but may be problematic in WordPress: + * + * - Normalize and replace newlines. https://www.w3.org/TR/css-syntax-3/#input-preprocessing + * - "<", ">", and "&" are replaced to prevent issues with KSES and other sanitization that + * is confused by HTML-like text. + * is confused by HTML-like text. + * - `,`, `"` and `'` are replaced to prevent issues where font families may be processed later. + * + * Note that the Unicode escape sequences are used rather than backslash-escaping so the + * problematic characters are removed completely. + */ + // Escape existing backslashes before any other processing + .replaceAll( '\\', '\\5C ' ) + // Carriage return + line feed must be the first newline replacement. + .replaceAll( '\r\n', '\\A ' ) + .replaceAll( '\r', '\\A ' ) + .replaceAll( '\f', '\\A ' ) + .replaceAll( '\n', '\\A ' ) + .replaceAll( ',', '\\2C ' ) + .replaceAll( '"', '\\22 ' ) + .replaceAll( "'", '\\27 ' ) + .replaceAll( '<', '\\3C ' ) + .replaceAll( '>', '\\3E ' ) + .replaceAll( '&', '\\26 ' ) }"`; +} diff --git a/packages/global-styles-ui/src/font-library/utils/make-families-from-faces.ts b/packages/global-styles-ui/src/font-library/utils/make-families-from-faces.ts index 5fd587c81692e7..6dfe0de3864683 100644 --- a/packages/global-styles-ui/src/font-library/utils/make-families-from-faces.ts +++ b/packages/global-styles-ui/src/font-library/utils/make-families-from-faces.ts @@ -2,33 +2,31 @@ * WordPress dependencies */ import { privateApis as componentsPrivateApis } from '@wordpress/components'; -import type { FontFamily, FontFace } from '@wordpress/core-data'; +import type { FontFamily } from '@wordpress/core-data'; /** * Internal dependencies */ import { unlock } from '../../lock-unlock'; +import type { FontFaceMetadata } from '../upload-fonts'; const { kebabCase } = unlock( componentsPrivateApis ); export default function makeFamiliesFromFaces( - fontFaces: FontFace[] + fontFaces: FontFaceMetadata[] ): FontFamily[] { - const fontFamiliesObject = fontFaces.reduce( - ( acc: Record< string, FontFamily >, item: FontFace ) => { - if ( ! acc[ item.fontFamily ] ) { - acc[ item.fontFamily ] = { - name: item.fontFamily, - fontFamily: item.fontFamily, - slug: kebabCase( item.fontFamily.toLowerCase() ), - fontFace: [], - }; - } - // @ts-expect-error - acc[ item.fontFamily ].fontFace.push( item ); - return acc; - }, - {} - ); + const fontFamiliesObject = new Map< string, FontFamily >(); + for ( const item of fontFaces ) { + if ( fontFamiliesObject.has( item.fontFamily ) ) { + fontFamiliesObject.get( item.fontFamily )!.fontFace!.push( item ); + } + + fontFamiliesObject.set( item.fontFamily, { + name: item.name, + fontFamily: item.fontFamily, + slug: kebabCase( item.fontFamily.toLowerCase() ), + fontFace: [], + } ); + } return Object.values( fontFamiliesObject ) as FontFamily[]; } diff --git a/packages/global-styles-ui/src/font-library/utils/preview-styles.ts b/packages/global-styles-ui/src/font-library/utils/preview-styles.ts index 7661bc78899764..da5b69a1956158 100644 --- a/packages/global-styles-ui/src/font-library/utils/preview-styles.ts +++ b/packages/global-styles-ui/src/font-library/utils/preview-styles.ts @@ -82,44 +82,6 @@ export function formatFontFamily( input: string ) { 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: string ) { - 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(); - } - // 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 }"`; - } - return output; -} - export function getFamilyPreviewStyle( family: FontFamily | FontFace ): CSSProperties {