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

Font Library Refactor #57688

Merged
merged 28 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5dbb7fc
Font Library: add wp_font_face post type and scaffold font face REST …
creativecoder Jan 9, 2024
e89854a
Font Library: create font faces through the REST API (#57702)
creativecoder Jan 11, 2024
998f084
Refactor Font Family Controller (#57785)
creativecoder Jan 12, 2024
90b5717
Font Family and Font Face REST API endpoints: better data handling an…
creativecoder Jan 15, 2024
efffcc8
Font Families REST API endpoint: ensure unique font family slugs (#57…
creativecoder Jan 16, 2024
c263a04
Font Library: delete child font faces and font assets when deleting p…
creativecoder Jan 16, 2024
e8ca12c
Font Library: refactor client side install functions to work with rev…
jffng Jan 17, 2024
13b5640
Cleanup/font library view error handling (#57926)
pbking Jan 17, 2024
3e37968
Font Faces endpoint: prevent creating font faces with duplicate setti…
creativecoder Jan 17, 2024
d1f8dcf
Font Library: Update uninstall/delete on client side (#57932)
mikachan Jan 18, 2024
3e5e987
Update packages/edit-site/src/components/global-styles/font-library-m…
mikachan Jan 18, 2024
dd885b5
Font Library: address JS feedback in #57688 (#57961)
mikachan Jan 18, 2024
2ed7a3b
Font Library REST API endpoints: address initial feedback from featur…
creativecoder Jan 19, 2024
c0e9949
Font Library: font collection refactor to use the new schema (#57884)
matiasbenedetto Jan 19, 2024
51345f0
Fix font asset download when font faces are installed (#58021)
creativecoder Jan 19, 2024
d45d540
Font Families and Faces: disable autosaves using empty class (#58018)
creativecoder Jan 19, 2024
1320d20
Adds migration for legacy font family content (#58032)
creativecoder Jan 22, 2024
4dce262
Font Library: Fix font collection filtering (#58091)
matiasbenedetto Jan 23, 2024
3da3f45
Merge branch 'trunk' into try/font-library-refactor
mikachan Jan 23, 2024
2743793
Fix load.php
mikachan Jan 23, 2024
b98c028
Font Library: fix to activate and display the right activation state …
matiasbenedetto Jan 23, 2024
921ec13
Fix font face files not being deleted with family (#58128)
creativecoder Jan 23, 2024
14b9e53
Font Library Preview: fix quoting of fontFamily property (#58127)
creativecoder Jan 23, 2024
f460811
Fix Font Library Tests_Font_Library_Hooks test missed in #58128
creativecoder Jan 23, 2024
51e4bed
Font library: Fix React key prop warnings (#57939)
mikachan Jan 23, 2024
23ce4e9
removing repeated comment
matiasbenedetto Jan 23, 2024
eb30f83
Update default Google fonts collection URL
mikachan Jan 23, 2024
dde2059
Font Library: Prevent error when installing a system font twice (#58141)
creativecoder Jan 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
183 changes: 140 additions & 43 deletions lib/experimental/fonts/font-library/class-wp-font-collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,41 +21,128 @@
class WP_Font_Collection {

/**
* Font collection configuration.
* The unique slug for the font collection.
*
* @since 6.5.0
*
* @var string
*/
private $slug;

/**
* The name of the font collection.
*
* @since 6.5.0
*
* @var string
*/
private $name;

/**
* Description of the font collection.
*
* @since 6.5.0
*
* @var string
*/
private $description;

/**
* Source of the font collection.
*
* @since 6.5.0
*
* @var string
*/
private $src;

/**
* Array of font families in the collection.
*
* @since 6.5.0
*
* @var array
*/
private $config;
private $font_families;

/**
* Categories associated with the font collection.
*
* @since 6.5.0
*
* @var array
*/
private $categories;


/**
* WP_Font_Collection constructor.
*
* @since 6.5.0
*
* @param array $config Font collection config options.
* See {@see wp_register_font_collection()} for the supported fields.
* @throws Exception If the required parameters are missing.
* @param array $config Font collection config options. {
* @type string $slug The font collection's unique slug.
* @type string $name The font collection's name.
* @type string $description The font collection's description.
* @type string $src The font collection's source.
* @type array $font_families An array of font families in the font collection.
* @type array $categories The font collection's categories.
* }
*/
public function __construct( $config ) {
if ( empty( $config ) || ! is_array( $config ) ) {
throw new Exception( 'Font Collection config options is required as a non-empty array.' );
}
$this->is_config_valid( $config );

$this->slug = isset( $config['slug'] ) ? $config['slug'] : '';
$this->name = isset( $config['name'] ) ? $config['name'] : '';
$this->description = isset( $config['description'] ) ? $config['description'] : '';
$this->src = isset( $config['src'] ) ? $config['src'] : '';
$this->font_families = isset( $config['font_families'] ) ? $config['font_families'] : array();
$this->categories = isset( $config['categories'] ) ? $config['categories'] : array();
}

if ( empty( $config['slug'] ) || ! is_string( $config['slug'] ) ) {
throw new Exception( 'Font Collection config slug is required as a non-empty string.' );
/**
* Checks if the font collection config is valid.
*
* @since 6.5.0
*
* @param array $config Font collection config options. {
* @type string $slug The font collection's unique slug.
* @type string $name The font collection's name.
* @type string $description The font collection's description.
* @type string $src The font collection's source.
* @type array $font_families An array of font families in the font collection.
* @type array $categories The font collection's categories.
* }
* @return bool True if the font collection config is valid and false otherwise.
*/
public static function is_config_valid( $config ) {
if ( empty( $config ) || ! is_array( $config ) ) {
_doing_it_wrong( __METHOD__, __( 'Font Collection config options are required as a non-empty array.', 'gutenberg' ), '6.5.0' );
return false;
}

if ( empty( $config['name'] ) || ! is_string( $config['name'] ) ) {
throw new Exception( 'Font Collection config name is required as a non-empty string.' );
$required_keys = array( 'slug', 'name' );
foreach ( $required_keys as $key ) {
if ( empty( $config[ $key ] ) ) {
_doing_it_wrong(
__METHOD__,
// translators: %s: Font collection config key.
sprintf( __( 'Font Collection config %s is required as a non-empty string.', 'gutenberg' ), $key ),
'6.5.0'
);
return false;
}
}

if ( ( empty( $config['src'] ) || ! is_string( $config['src'] ) ) && ( empty( $config['data'] ) ) ) {
throw new Exception( 'Font Collection config "src" option OR "data" option is required.' );
if (
( empty( $config['src'] ) && empty( $config['font_families'] ) ) ||
( ! empty( $config['src'] ) && ! empty( $config['font_families'] ) )
) {
_doing_it_wrong( __METHOD__, __( 'Font Collection config "src" option OR "font_families" option are required.', 'gutenberg' ), '6.5.0' );
Copy link
Member

Choose a reason for hiding this comment

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

Best to use sprintf placeholders for src and font_families here to prevent accidental translation

Copy link
Contributor

Choose a reason for hiding this comment

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

added here: 18f9963

return false;
}

$this->config = $config;
return true;
}

/**
Expand All @@ -73,56 +160,59 @@ public function __construct( $config ) {
*/
public function get_config() {
return array(
'slug' => $this->config['slug'],
'name' => $this->config['name'],
'description' => $this->config['description'] ?? '',
'slug' => $this->slug,
'name' => $this->name,
'description' => $this->description,
);
}

/**
* Gets the font collection config and data.
* Gets the font collection content.
*
* This function returns an array containing the font collection's unique ID,
* name, and its data as a PHP array.
* Load the font collection data from the src if it is not already loaded.
*
* @since 6.5.0
*
* @return array {
* An array of font collection config and data.
* @return array|WP_Error {
* An array of font collection contents.
*
* @type string $slug The font collection's unique ID.
* @type string $name The font collection's name.
* @type string $description The font collection's description.
* @type array $data The font collection's data as a PHP array.
* @type array $font_families The font collection's font families.
* @type string $categories The font collection's categories.
* }
*
* A WP_Error object if there was an error loading the font collection data.
*/
public function get_config_and_data() {
$config_and_data = $this->get_config();
$config_and_data['data'] = $this->load_data();
return $config_and_data;
public function get_content() {
// If the font families are not loaded, and the src is not empty, load the data from the src.
if ( empty( $this->font_families ) && ! empty( $this->src ) ) {
$data = $this->load_contents_from_src();
if ( is_wp_error( $data ) ) {
return $data;
}
}

return array(
'font_families' => $this->font_families,
'categories' => $this->categories,
);
}

/**
* Loads the font collection data.
* Loads the font collection data from the src.
*
* @since 6.5.0
*
* @return array|WP_Error An array containing the list of font families in font-collection.json format on success,
* else an instance of WP_Error on failure.
*/
public function load_data() {

if ( ! empty( $this->config['data'] ) ) {
return $this->config['data'];
}

private function load_contents_from_src() {
// If the src is a URL, fetch the data from the URL.
if ( str_contains( $this->config['src'], 'http' ) && str_contains( $this->config['src'], '://' ) ) {
if ( ! wp_http_validate_url( $this->config['src'] ) ) {
if ( preg_match( '#^https?://#', $this->src ) ) {
if ( ! wp_http_validate_url( $this->src ) ) {
return new WP_Error( 'font_collection_read_error', __( 'Invalid URL for Font Collection data.', 'gutenberg' ) );
}

$response = wp_remote_get( $this->config['src'] );
$response = wp_remote_get( $this->src );
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'font_collection_read_error', __( 'Error fetching the Font Collection data from a URL.', 'gutenberg' ) );
}
Expand All @@ -133,15 +223,22 @@ public function load_data() {
}
// If the src is a file path, read the data from the file.
} else {
if ( ! file_exists( $this->config['src'] ) ) {
if ( ! file_exists( $this->src ) ) {
return new WP_Error( 'font_collection_read_error', __( 'Font Collection data JSON file does not exist.', 'gutenberg' ) );
}
$data = wp_json_file_decode( $this->config['src'], array( 'associative' => true ) );
$data = wp_json_file_decode( $this->src, array( 'associative' => true ) );
if ( empty( $data ) ) {
return new WP_Error( 'font_collection_read_error', __( 'Error reading the Font Collection data JSON file contents.', 'gutenberg' ) );
}
}

if ( empty( $data['font_families'] ) ) {
return new WP_Error( 'font_collection_contents_error', __( 'Font Collection data JSON file does not contain font families.', 'gutenberg' ) );
}

$this->font_families = $data['font_families'];
$this->categories = $data['categories'] ?? array();

return $data;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public static function format_font_family( $font_family ) {
function ( $family ) {
$trimmed = trim( $family );
if ( ! empty( $trimmed ) && strpos( $trimmed, ' ' ) !== false && strpos( $trimmed, "'" ) === false && strpos( $trimmed, '"' ) === false ) {
return "'" . $trimmed . "'";
return '"' . $trimmed . '"';
}
return $trimmed;
},
Expand All @@ -107,4 +107,84 @@ function ( $family ) {

return $font_family;
}

/**
* Generates a slug from font face properties, e.g. `open sans;normal;400;100%;U+0-10FFFF`
*
* Used for comparison with other font faces in the same family, to prevent duplicates
* that would both match according the CSS font matching spec. Uses only simple case-insensitive
* matching for fontFamily and unicodeRange, so does not handle overlapping font-family lists or
* unicode ranges.
*
* @since 6.5.0
*
* @link https://drafts.csswg.org/css-fonts/#font-style-matching
*
* @param array $settings {
* Font face settings.
*
* @type string $fontFamily Font family name.
* @type string $fontStyle Optional font style, defaults to 'normal'.
* @type string $fontWeight Optional font weight, defaults to 400.
* @type string $fontStretch Optional font stretch, defaults to '100%'.
* @type string $unicodeRange Optional unicode range, defaults to 'U+0-10FFFF'.
* }
* @return string Font face slug.
*/
public static function get_font_face_slug( $settings ) {
$settings = wp_parse_args(
$settings,
array(
'fontFamily' => '',
'fontStyle' => 'normal',
'fontWeight' => '400',
'fontStretch' => '100%',
'unicodeRange' => 'U+0-10FFFF',
)
);

// Convert all values to lowercase for comparison.
// Font family names may use multibyte characters.
$font_family = mb_strtolower( $settings['fontFamily'] );
$font_style = strtolower( $settings['fontStyle'] );
$font_weight = strtolower( $settings['fontWeight'] );
$font_stretch = strtolower( $settings['fontStretch'] );
$unicode_range = strtoupper( $settings['unicodeRange'] );

// Convert weight keywords to numeric strings.
$font_weight = str_replace( 'normal', '400', $font_weight );
$font_weight = str_replace( 'bold', '700', $font_weight );

// Convert stretch keywords to numeric strings.
$font_stretch_map = array(
'ultra-condensed' => '50%',
'extra-condensed' => '62.5%',
'condensed' => '75%',
'semi-condensed' => '87.5%',
'normal' => '100%',
'semi-expanded' => '112.5%',
'expanded' => '125%',
'extra-expanded' => '150%',
'untra-expanded' => '200%',
);
$font_stretch = str_replace( array_keys( $font_stretch_map ), array_values( $font_stretch_map ), $font_stretch );

$slug_elements = array( $font_family, $font_style, $font_weight, $font_stretch, $unicode_range );

$slug_elements = array_map(
function ( $elem ) {
// Remove quotes to normalize font-family names, and ';' to use as a separator.
$elem = trim( str_replace( array( '"', "'", ';' ), '', $elem ) );

// Normalize comma separated lists by removing whitespace in between items,
// but keep whitespace within items (e.g. "Open Sans" and "OpenSans" are different fonts).
// CSS spec for whitespace includes: U+000A LINE FEED, U+0009 CHARACTER TABULATION, or U+0020 SPACE,
// which by default are all matched by \s in PHP.
return preg_replace( '/,\s+/', ',', $elem );
},
$slug_elements
);

return join( ';', $slug_elements );
}
}
12 changes: 9 additions & 3 deletions lib/experimental/fonts/font-library/class-wp-font-library.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,17 @@ public static function get_expected_font_mime_types_per_php_version( $php_versio
* @return WP_Font_Collection|WP_Error A font collection is it was registered successfully and a WP_Error otherwise.
*/
public static function register_font_collection( $config ) {
if ( ! WP_Font_Collection::is_config_valid( $config ) ) {
$error_message = __( 'Font collection config is invalid.', 'gutenberg' );
return new WP_Error( 'font_collection_registration_error', $error_message );
}

$new_collection = new WP_Font_Collection( $config );
if ( self::is_collection_registered( $config['slug'] ) ) {

if ( self::is_collection_registered( $new_collection->get_config()['slug'] ) ) {
$error_message = sprintf(
/* translators: %s: Font collection slug. */
__( 'Font collection with slug: "%s" is already registered.', 'default' ),
__( 'Font collection with slug: "%s" is already registered.', 'gutenberg' ),
$config['slug']
);
_doing_it_wrong(
Expand All @@ -76,7 +82,7 @@ public static function register_font_collection( $config ) {
);
return new WP_Error( 'font_collection_registration_error', $error_message );
}
self::$collections[ $config['slug'] ] = $new_collection;
self::$collections[ $new_collection->get_config()['slug'] ] = $new_collection;
return $new_collection;
}

Expand Down