Skip to content

Commit

Permalink
Font Library Refactor (#57688)
Browse files Browse the repository at this point in the history
* Font Library: add wp_font_face post type and scaffold font face REST API controller (#57656)

* Font Library: create font faces through the REST API (#57702)

* Refactor Font Family Controller (#57785)

* Font Family and Font Face REST API endpoints: better data handling and errors  (#57843)

* Font Families REST API endpoint: ensure unique font family slugs (#57861)

* Font Library: delete child font faces and font assets when deleting parent (#57867)

* Font Library: refactor client side install functions to work with revised API (#57844)

* Cleanup/font library view error handling (#57926)

* Font Faces endpoint: prevent creating font faces with duplicate settings (#57903)

* Font Library: Update uninstall/delete on client side (#57932)

* Font Library: address JS feedback in #57688 (#57961)

* Font Library REST API endpoints: address initial feedback from feature branch (#57946)

* Font Library: font collection refactor to use the new schema (#57884)

* Fix font asset download when font faces are installed (#58021)

* Font Families and Faces: disable autosaves using empty class (#58018)

* Adds migration for legacy font family content (#58032)

---------

Co-authored-by: Jeff Ong <jonger4@gmail.com>
Co-authored-by: Matias Benedetto <matias.benedetto@gmail.com>
Co-authored-by: Jason Crist <jcrist@pbking.com>
Co-authored-by: Sarah Norris <sarah@sekai.co.uk>
Co-authored-by: Jonny Harris <spacedmonkey@users.noreply.github.com>
  • Loading branch information
6 people committed Jan 23, 2024
1 parent 1a0e79c commit 986ad30
Show file tree
Hide file tree
Showing 52 changed files with 4,605 additions and 1,788 deletions.
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' );
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
Loading

0 comments on commit 986ad30

Please sign in to comment.