<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[57539] trunk: Editor: Introduce the Font Library post types and low level APIs.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/57539">57539</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/57539","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>youknowriad</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-02-06 08:40:38 +0000 (Tue, 06 Feb 2024)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Editor: Introduce the Font Library post types and low level APIs.

This is the first step towards adding the font library to WordPress.
This commit includes the font library and font face CPTs.
It also adds the necessary APIs and classes to register and manipulate font collections.

This PR backports the font library post types and low level APIs to Core. This is the first step to include the font library entirely into Core. Once this merged, we'll open a PR with the necessary REST API controllers.

Props youknowriad, get_dave, grantmkin, swissspidy, hellofromtonya, mukesh27, mcsf.
See <a href="https://core.trac.wordpress.org/ticket/59166">#59166</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesdefaultfiltersphp">trunk/src/wp-includes/default-filters.php</a></li>
<li><a href="#trunksrcwpincludesfontsphp">trunk/src/wp-includes/fonts.php</a></li>
<li><a href="#trunksrcwpincludespostphp">trunk/src/wp-includes/post.php</a></li>
<li><a href="#trunksrcwpsettingsphp">trunk/src/wp-settings.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesfontsclasswpfontcollectionphp">trunk/src/wp-includes/fonts/class-wp-font-collection.php</a></li>
<li><a href="#trunksrcwpincludesfontsclasswpfontlibraryphp">trunk/src/wp-includes/fonts/class-wp-font-library.php</a></li>
<li><a href="#trunksrcwpincludesfontsclasswpfontutilsphp">trunk/src/wp-includes/fonts/class-wp-font-utils.php</a></li>
<li>trunk/tests/phpunit/tests/fonts/font-library/</li>
<li><a href="#trunktestsphpunittestsfontsfontlibraryfontLibraryHooksphp">trunk/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php</a></li>
<li>trunk/tests/phpunit/tests/fonts/font-library/wpFontCollection/</li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpFontCollection__constructphp">trunk/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php</a></li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpFontCollectiongetDataphp">trunk/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php</a></li>
<li>trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/</li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpFontLibrarybasephp">trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php</a></li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpFontLibrarygetFontCollectionphp">trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php</a></li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpFontLibrarygetFontCollectionsphp">trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php</a></li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpFontLibraryregisterFontCollectionphp">trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php</a></li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpFontLibraryunregisterFontCollectionphp">trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php</a></li>
<li>trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/</li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpFontUtilsgetFontFaceSlugphp">trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php</a></li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpFontUtilssanitizeFontFamilyphp">trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php</a></li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpFontUtilssanitizeFromSchemaphp">trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php</a></li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpFontsDirphp">trunk/tests/phpunit/tests/fonts/font-library/wpFontsDir.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesdefaultfiltersphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/default-filters.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/default-filters.php 2024-02-06 01:23:54 UTC (rev 57538)
+++ trunk/src/wp-includes/default-filters.php   2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -748,5 +748,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> // Font management.
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'wp_head', 'wp_print_font_faces', 50 );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+add_action( 'deleted_post', '_wp_after_delete_font_family', 10, 2 );
+add_action( 'before_delete_post', '_wp_before_delete_font_face', 10, 2 );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> unset( $filter, $action );
</span></span></pre></div>
<a id="trunksrcwpincludesfontsclasswpfontcollectionphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/fonts/class-wp-font-collection.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/fonts/class-wp-font-collection.php                          (rev 0)
+++ trunk/src/wp-includes/fonts/class-wp-font-collection.php    2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,260 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Font Collection class.
+ *
+ * This file contains the Font Collection class definition.
+ *
+ * @package    WordPress
+ * @subpackage Fonts
+ * @since      6.5.0
+ */
+
+/**
+ * Font Collection class.
+ *
+ * @since 6.5.0
+ *
+ * @see wp_register_font_collection()
+ */
+final class WP_Font_Collection {
+       /**
+        * The unique slug for the font collection.
+        *
+        * @since 6.5.0
+        * @var string
+        */
+       public $slug;
+
+       /**
+        * Font collection data.
+        *
+        * @since 6.5.0
+        * @var array|WP_Error|null
+        */
+       private $data;
+
+       /**
+        * Font collection JSON file path or URL.
+        *
+        * @since 6.5.0
+        * @var string|null
+        */
+       private $src;
+
+       /**
+        * WP_Font_Collection constructor.
+        *
+        * @since 6.5.0
+        *
+        * @param string       $slug         Font collection slug.
+        * @param array|string $data_or_file Font collection data array or a path/URL to a JSON file
+        *                                   containing the font collection.
+        *                                   See {@see wp_register_font_collection()} for the supported fields.
+        */
+       public function __construct( $slug, $data_or_file ) {
+               $this->slug = sanitize_title( $slug );
+               if ( $this->slug !== $slug ) {
+                       _doing_it_wrong(
+                               __METHOD__,
+                               /* translators: %s: Font collection slug. */
+                               sprintf( __( 'Font collection slug "%s" is not valid. Slugs must use only alphanumeric characters, dashes, and underscores.' ), $slug ),
+                               '6.5.0'
+                       );
+               }
+
+               if ( is_array( $data_or_file ) ) {
+                       $this->data = $this->sanitize_and_validate_data( $data_or_file );
+               } else {
+                       // JSON data is lazy loaded by ::get_data().
+                       $this->src = $data_or_file;
+               }
+       }
+
+       /**
+        * Retrieves the font collection data.
+        *
+        * @since 6.5.0
+        *
+        * @return array|WP_Error An array containing the font collection data, or a WP_Error on failure.
+        */
+       public function get_data() {
+               // If the collection uses JSON data, load it and cache the data/error.
+               if ( $this->src && empty( $this->data ) ) {
+                       $this->data = $this->load_from_json( $this->src );
+               }
+
+               if ( is_wp_error( $this->data ) ) {
+                       return $this->data;
+               }
+
+               // Set defaults for optional properties.
+               $defaults = array(
+                       'description' => '',
+                       'categories'  => array(),
+               );
+
+               return wp_parse_args( $this->data, $defaults );
+       }
+
+       /**
+        * Loads font collection data from a JSON file or URL.
+        *
+        * @since 6.5.0
+        *
+        * @param string $file_or_url File path or URL to a JSON file containing the font collection data.
+        * @return array|WP_Error An array containing the font collection data on success,
+        *                        else an instance of WP_Error on failure.
+        */
+       private function load_from_json( $file_or_url ) {
+               $url  = wp_http_validate_url( $file_or_url );
+               $file = file_exists( $file_or_url ) ? wp_normalize_path( realpath( $file_or_url ) ) : false;
+
+               if ( ! $url && ! $file ) {
+                       // translators: %s: File path or URL to font collection JSON file.
+                       $message = __( 'Font collection JSON file is invalid or does not exist.' );
+                       _doing_it_wrong( __METHOD__, $message, '6.5.0' );
+                       return new WP_Error( 'font_collection_json_missing', $message );
+               }
+
+               return $url ? $this->load_from_url( $url ) : $this->load_from_file( $file );
+       }
+
+       /**
+        * Loads the font collection data from a JSON file path.
+        *
+        * @since 6.5.0
+        *
+        * @param string $file File path to a JSON file containing the font collection data.
+        * @return array|WP_Error An array containing the font collection data on success,
+        *                        else an instance of WP_Error on failure.
+        */
+       private function load_from_file( $file ) {
+               $data = wp_json_file_decode( $file, array( 'associative' => true ) );
+               if ( empty( $data ) ) {
+                       return new WP_Error( 'font_collection_decode_error', __( 'Error decoding the font collection JSON file contents.' ) );
+               }
+
+               return $this->sanitize_and_validate_data( $data );
+       }
+
+       /**
+        * Loads the font collection data from a JSON file URL.
+        *
+        * @since 6.5.0
+        *
+        * @param string $url URL to a JSON file containing the font collection data.
+        * @return array|WP_Error An array containing the font collection data on success,
+        *                        else an instance of WP_Error on failure.
+        */
+       private function load_from_url( $url ) {
+               // Limit key to 167 characters to avoid failure in the case of a long URL.
+               $transient_key = substr( 'wp_font_collection_url_' . $url, 0, 167 );
+               $data          = get_site_transient( $transient_key );
+
+               if ( false === $data ) {
+                       $response = wp_safe_remote_get( $url );
+                       if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
+                               // translators: %s: Font collection URL.
+                               return new WP_Error( 'font_collection_request_error', sprintf( __( 'Error fetching the font collection data from "%s".' ), $url ) );
+                       }
+
+                       $data = json_decode( wp_remote_retrieve_body( $response ), true );
+                       if ( empty( $data ) ) {
+                               return new WP_Error( 'font_collection_decode_error', __( 'Error decoding the font collection data from the HTTP response JSON.' ) );
+                       }
+
+                       // Make sure the data is valid before storing it in a transient.
+                       $data = $this->sanitize_and_validate_data( $data );
+                       if ( is_wp_error( $data ) ) {
+                               return $data;
+                       }
+
+                       set_site_transient( $transient_key, $data, DAY_IN_SECONDS );
+               }
+
+               return $data;
+       }
+
+       /**
+        * Sanitizes and validates the font collection data.
+        *
+        * @since 6.5.0
+        *
+        * @param array $data Font collection data to sanitize and validate.
+        * @return array|WP_Error Sanitized data if valid, otherwise a WP_Error instance.
+        */
+       private function sanitize_and_validate_data( $data ) {
+               $schema = self::get_sanitization_schema();
+               $data   = WP_Font_Utils::sanitize_from_schema( $data, $schema );
+
+               $required_properties = array( 'name', 'font_families' );
+               foreach ( $required_properties as $property ) {
+                       if ( empty( $data[ $property ] ) ) {
+                               $message = sprintf(
+                                       // translators: 1: Font collection slug, 2: Missing property name, e.g. "font_families".
+                                       __( 'Font collection "%1$s" has missing or empty property: "%2$s".' ),
+                                       $this->slug,
+                                       $property
+                               );
+                               _doing_it_wrong( __METHOD__, $message, '6.5.0' );
+                               return new WP_Error( 'font_collection_missing_property', $message );
+                       }
+               }
+
+               return $data;
+       }
+
+       /**
+        * Retrieves the font collection sanitization schema.
+        *
+        * @since 6.5.0
+        *
+        * @return array Font collection sanitization schema.
+        */
+       private static function get_sanitization_schema() {
+               return array(
+                       'name'          => 'sanitize_text_field',
+                       'description'   => 'sanitize_text_field',
+                       'font_families' => array(
+                               array(
+                                       'font_family_settings' => array(
+                                               'name'       => 'sanitize_text_field',
+                                               'slug'       => 'sanitize_title',
+                                               'fontFamily' => 'sanitize_text_field',
+                                               'preview'    => 'sanitize_url',
+                                               'fontFace'   => array(
+                                                       array(
+                                                               'fontFamily'            => 'sanitize_text_field',
+                                                               'fontStyle'             => 'sanitize_text_field',
+                                                               'fontWeight'            => 'sanitize_text_field',
+                                                               'src'                   => static function ( $value ) {
+                                                                       return is_array( $value )
+                                                                               ? array_map( 'sanitize_text_field', $value )
+                                                                               : sanitize_text_field( $value );
+                                                               },
+                                                               'preview'               => 'sanitize_url',
+                                                               'fontDisplay'           => 'sanitize_text_field',
+                                                               'fontStretch'           => 'sanitize_text_field',
+                                                               'ascentOverride'        => 'sanitize_text_field',
+                                                               'descentOverride'       => 'sanitize_text_field',
+                                                               'fontVariant'           => 'sanitize_text_field',
+                                                               'fontFeatureSettings'   => 'sanitize_text_field',
+                                                               'fontVariationSettings' => 'sanitize_text_field',
+                                                               'lineGapOverride'       => 'sanitize_text_field',
+                                                               'sizeAdjust'            => 'sanitize_text_field',
+                                                               'unicodeRange'          => 'sanitize_text_field',
+                                                       ),
+                                               ),
+                                       ),
+                                       'categories'           => array( 'sanitize_title' ),
+                               ),
+                       ),
+                       'categories'    => array(
+                               array(
+                                       'name' => 'sanitize_text_field',
+                                       'slug' => 'sanitize_title',
+                               ),
+                       ),
+               );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/fonts/class-wp-font-collection.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesfontsclasswpfontlibraryphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/fonts/class-wp-font-library.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/fonts/class-wp-font-library.php                             (rev 0)
+++ trunk/src/wp-includes/fonts/class-wp-font-library.php       2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,144 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Font Library class.
+ *
+ * This file contains the Font Library class definition.
+ *
+ * @package    WordPress
+ * @subpackage Fonts
+ * @since      6.5.0
+ */
+
+/**
+ * Font Library class.
+ *
+ * @since 6.5.0
+ */
+class WP_Font_Library {
+
+       /**
+        * Font collections.
+        *
+        * @since 6.5.0
+        * @var array
+        */
+       private $collections = array();
+
+       /**
+        * Container for the main instance of the class.
+        *
+        * @since 6.5.0
+        * @var WP_Font_Library|null
+        */
+       private static $instance = null;
+
+       /**
+        * Register a new font collection.
+        *
+        * @since 6.5.0
+        *
+        * @param string $slug         Font collection slug.
+        * @param array  $data_or_file Font collection data array or a path/URL to a JSON file
+        *                             containing the font collection.
+        *                             See {@see wp_register_font_collection()} for the supported fields.
+        * @return WP_Font_Collection|WP_Error A font collection if it was registered successfully,
+        *                                     or WP_Error object on failure.
+        */
+       public function register_font_collection( $slug, $data_or_file ) {
+               $new_collection = new WP_Font_Collection( $slug, $data_or_file );
+
+               if ( $this->is_collection_registered( $new_collection->slug ) ) {
+                       $error_message = sprintf(
+                               /* translators: %s: Font collection slug. */
+                               __( 'Font collection with slug: "%s" is already registered.' ),
+                               $new_collection->slug
+                       );
+                       _doing_it_wrong(
+                               __METHOD__,
+                               $error_message,
+                               '6.5.0'
+                       );
+                       return new WP_Error( 'font_collection_registration_error', $error_message );
+               }
+               $this->collections[ $new_collection->slug ] = $new_collection;
+               return $new_collection;
+       }
+
+       /**
+        * Unregisters a previously registered font collection.
+        *
+        * @since 6.5.0
+        *
+        * @param string $slug Font collection slug.
+        * @return bool True if the font collection was unregistered successfully and false otherwise.
+        */
+       public function unregister_font_collection( $slug ) {
+               if ( ! $this->is_collection_registered( $slug ) ) {
+                       _doing_it_wrong(
+                               __METHOD__,
+                               /* translators: %s: Font collection slug. */
+                               sprintf( __( 'Font collection "%s" not found.' ), $slug ),
+                               '6.5.0'
+                       );
+                       return false;
+               }
+               unset( $this->collections[ $slug ] );
+               return true;
+       }
+
+       /**
+        * Checks if a font collection is registered.
+        *
+        * @since 6.5.0
+        *
+        * @param string $slug Font collection slug.
+        * @return bool True if the font collection is registered and false otherwise.
+        */
+       private function is_collection_registered( $slug ) {
+               return array_key_exists( $slug, $this->collections );
+       }
+
+       /**
+        * Gets all the font collections available.
+        *
+        * @since 6.5.0
+        *
+        * @return array List of font collections.
+        */
+       public function get_font_collections() {
+               return $this->collections;
+       }
+
+       /**
+        * Gets a font collection.
+        *
+        * @since 6.5.0
+        *
+        * @param string $slug Font collection slug.
+        * @return WP_Font_Collection|WP_Error Font collection object,
+        *                                     or WP_Error object if the font collection doesn't exist.
+        */
+       public function get_font_collection( $slug ) {
+               if ( $this->is_collection_registered( $slug ) ) {
+                       return $this->collections[ $slug ];
+               }
+               return new WP_Error( 'font_collection_not_found', __( 'Font collection not found.' ) );
+       }
+
+       /**
+        * Utility method to retrieve the main instance of the class.
+        *
+        * The instance will be created if it does not exist yet.
+        *
+        * @since 6.5.0
+        *
+        * @return WP_Font_Library The main instance.
+        */
+       public static function get_instance() {
+               if ( null === self::$instance ) {
+                       self::$instance = new self();
+               }
+
+               return self::$instance;
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/fonts/class-wp-font-library.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesfontsclasswpfontutilsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/fonts/class-wp-font-utils.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/fonts/class-wp-font-utils.php                               (rev 0)
+++ trunk/src/wp-includes/fonts/class-wp-font-utils.php 2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,238 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Font Utils class.
+ *
+ * Provides utility functions for working with font families.
+ *
+ * @package    WordPress
+ * @subpackage Fonts
+ * @since      6.5.0
+ */
+
+/**
+ * A class of utilities for working with the Font Library.
+ *
+ * These utilities may change or be removed in the future and are intended for internal use only.
+ *
+ * @since 6.5.0
+ * @access private
+ */
+class WP_Font_Utils {
+       /**
+        * Sanitizes and formats font family names.
+        *
+        * - Applies `sanitize_text_field`
+        * - Adds surrounding quotes to names that contain spaces and are not already quoted
+        *
+        * @since 6.5.0
+        * @access private
+        *
+        * @see sanitize_text_field()
+        *
+        * @param string $font_family Font family name(s), comma-separated.
+        * @return string Sanitized and formatted font family name(s).
+        */
+       public static function sanitize_font_family( $font_family ) {
+               if ( ! $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 . '"';
+                               }
+                               return $trimmed;
+                       },
+                       $font_families
+               );
+
+               if ( count( $wrapped_font_families ) === 1 ) {
+                       $font_family = $wrapped_font_families[0];
+               } else {
+                       $font_family = implode( ', ', $wrapped_font_families );
+               }
+
+               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
+        * @access private
+        *
+        * @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 ) {
+               $defaults = array(
+                       'fontFamily'   => '',
+                       'fontStyle'    => 'normal',
+                       'fontWeight'   => '400',
+                       'fontStretch'  => '100%',
+                       'unicodeRange' => 'U+0-10FFFF',
+               );
+               $settings = wp_parse_args( $settings, $defaults );
+
+               $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( array( 'normal', 'bold' ), array( '400', '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%',
+                       'ultra-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 sanitize_text_field( join( ';', $slug_elements ) );
+       }
+
+       /**
+        * Sanitizes a tree of data using a schema.
+        *
+        * The schema structure should mirror the data tree. Each value provided in the
+        * schema should be a callable that will be applied to sanitize the corresponding
+        * value in the data tree. Keys that are in the data tree, but not present in the
+        * schema, will be removed in the santized data. Nested arrays are traversed recursively.
+        *
+        * @since 6.5.0
+        *
+        * @access private
+        *
+        * @param array $tree   The data to sanitize.
+        * @param array $schema The schema used for sanitization.
+        * @return array The sanitized data.
+        */
+       public static function sanitize_from_schema( $tree, $schema ) {
+               if ( ! is_array( $tree ) || ! is_array( $schema ) ) {
+                       return array();
+               }
+
+               foreach ( $tree as $key => $value ) {
+                       // Remove keys not in the schema or with null/empty values.
+                       if ( ! array_key_exists( $key, $schema ) ) {
+                               unset( $tree[ $key ] );
+                               continue;
+                       }
+
+                       $is_value_array  = is_array( $value );
+                       $is_schema_array = is_array( $schema[ $key ] ) && ! is_callable( $schema[ $key ] );
+
+                       if ( $is_value_array && $is_schema_array ) {
+                               if ( wp_is_numeric_array( $value ) ) {
+                                       // If indexed, process each item in the array.
+                                       foreach ( $value as $item_key => $item_value ) {
+                                               $tree[ $key ][ $item_key ] = isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] )
+                                                       ? self::sanitize_from_schema( $item_value, $schema[ $key ][0] )
+                                                       : self::apply_sanitizer( $item_value, $schema[ $key ][0] );
+                                       }
+                               } else {
+                                       // If it is an associative or indexed array, process as a single object.
+                                       $tree[ $key ] = self::sanitize_from_schema( $value, $schema[ $key ] );
+                               }
+                       } elseif ( ! $is_value_array && $is_schema_array ) {
+                               // If the value is not an array but the schema is, remove the key.
+                               unset( $tree[ $key ] );
+                       } elseif ( ! $is_schema_array ) {
+                               // If the schema is not an array, apply the sanitizer to the value.
+                               $tree[ $key ] = self::apply_sanitizer( $value, $schema[ $key ] );
+                       }
+
+                       // Remove keys with null/empty values.
+                       if ( empty( $tree[ $key ] ) ) {
+                               unset( $tree[ $key ] );
+                       }
+               }
+
+               return $tree;
+       }
+
+       /**
+        * Applies a sanitizer function to a value.
+        *
+        * @since 6.5.0
+        *
+        * @param mixed $value     The value to sanitize.
+        * @param mixed $sanitizer The sanitizer function to apply.
+        * @return mixed The sanitized value.
+        */
+       private static function apply_sanitizer( $value, $sanitizer ) {
+               if ( null === $sanitizer ) {
+                       return $value;
+
+               }
+               return call_user_func( $sanitizer, $value );
+       }
+
+       /**
+        * Returns the expected mime-type values for font files, depending on PHP version.
+        *
+        * This is needed because font mime types vary by PHP version, so checking the PHP version
+        * is necessary until a list of valid mime-types for each file extension can be provided to
+        * the 'upload_mimes' filter.
+        *
+        * @since 6.5.0
+        *
+        * @access private
+        *
+        * @return array A collection of mime types keyed by file extension.
+        */
+       public static function get_allowed_font_mime_types() {
+               $php_7_ttf_mime_type = PHP_VERSION_ID >= 70300 ? 'application/font-sfnt' : 'application/x-font-ttf';
+
+               return array(
+                       'otf'   => 'application/vnd.ms-opentype',
+                       'ttf'   => PHP_VERSION_ID >= 70400 ? 'font/sfnt' : $php_7_ttf_mime_type,
+                       'woff'  => PHP_VERSION_ID >= 80100 ? 'font/woff' : 'application/font-woff',
+                       'woff2' => PHP_VERSION_ID >= 80100 ? 'font/woff2' : 'application/font-woff2',
+               );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/fonts/class-wp-font-utils.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesfontsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/fonts.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/fonts.php   2024-02-06 01:23:54 UTC (rev 57538)
+++ trunk/src/wp-includes/fonts.php     2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -51,3 +51,140 @@
</span><span class="cx" style="display: block; padding: 0 10px">        $wp_font_face = new WP_Font_Face();
</span><span class="cx" style="display: block; padding: 0 10px">        $wp_font_face->generate_and_print( $fonts );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+/**
+ * Registers a new Font Collection in the Font Library.
+ *
+ * @since 6.5.0
+ *
+ * @param string       $slug Font collection slug. May only contain alphanumeric characters, dashes,
+ *                     and underscores. See sanitize_title().
+ * @param array|string $data_or_file {
+ *     Font collection data array or a path/URL to a JSON file containing the font collection.
+ *
+ *     @link https://schemas.wp.org/trunk/font-collection.json
+ *
+ *     @type string $name           Required. Name of the font collection shown in the Font Library.
+ *     @type string $description    Optional. A short descriptive summary of the font collection. Default empty.
+ *     @type array  $font_families  Required. Array of font family definitions that are in the collection.
+ *     @type array  $categories     Optional. Array of categories, each with a name and slug, that are used by the
+ *                                  fonts in the collection. Default empty.
+ * }
+ * @return WP_Font_Collection|WP_Error A font collection if it was registered
+ *                                     successfully, or WP_Error object on failure.
+ */
+function wp_register_font_collection( $slug, $data_or_file ) {
+       return WP_Font_Library::get_instance()->register_font_collection( $slug, $data_or_file );
+}
+
+/**
+ * Unregisters a font collection from the Font Library.
+ *
+ * @since 6.5.0
+ *
+ * @param string $slug Font collection slug.
+ * @return bool True if the font collection was unregistered successfully, else false.
+ */
+function wp_unregister_font_collection( $slug ) {
+       return WP_Font_Library::get_instance()->unregister_font_collection( $slug );
+}
+
+/**
+ * Returns an array containing the current fonts upload directory's path and URL.
+ *
+ * @since 6.5.0
+ *
+ * @param array $defaults {
+ *     Array of information about the upload directory.
+ *
+ *     @type string       $path    Base directory and subdirectory or full path to the fonts upload directory.
+ *     @type string       $url     Base URL and subdirectory or absolute URL to the fonts upload directory.
+ *     @type string       $subdir  Subdirectory
+ *     @type string       $basedir Path without subdir.
+ *     @type string       $baseurl URL path without subdir.
+ *     @type string|false $error   False or error message.
+ * }
+ * @return array $defaults {
+ *     Array of information about the upload directory.
+ *
+ *     @type string       $path    Base directory and subdirectory or full path to the fonts upload directory.
+ *     @type string       $url     Base URL and subdirectory or absolute URL to the fonts upload directory.
+ *     @type string       $subdir  Subdirectory
+ *     @type string       $basedir Path without subdir.
+ *     @type string       $baseurl URL path without subdir.
+ *     @type string|false $error   False or error message.
+ * }
+ */
+function wp_get_font_dir( $defaults = array() ) {
+       $site_path = '';
+       if ( is_multisite() && ! ( is_main_network() && is_main_site() ) ) {
+               $site_path = '/sites/' . get_current_blog_id();
+       }
+
+       // Sets the defaults.
+       $defaults['path']    = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path;
+       $defaults['url']     = untrailingslashit( content_url( 'fonts' ) ) . $site_path;
+       $defaults['subdir']  = '';
+       $defaults['basedir'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path;
+       $defaults['baseurl'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path;
+       $defaults['error']   = false;
+
+       /**
+        * Filters the fonts directory data.
+        *
+        * This filter allows developers to modify the fonts directory data.
+        *
+        * @since 6.5.0
+        *
+        * @param array $defaults The original fonts directory data.
+        */
+       return apply_filters( 'font_dir', $defaults );
+}
+
+/**
+ * Deletes child font faces when a font family is deleted.
+ *
+ * @access private
+ * @since 6.5.0
+ *
+ * @param int     $post_id Post ID.
+ * @param WP_Post $post    Post object.
+ */
+function _wp_after_delete_font_family( $post_id, $post ) {
+       if ( 'wp_font_family' !== $post->post_type ) {
+               return;
+       }
+
+       $font_faces = get_children(
+               array(
+                       'post_parent' => $post_id,
+                       'post_type'   => 'wp_font_face',
+               )
+       );
+
+       foreach ( $font_faces as $font_face ) {
+               wp_delete_post( $font_face->ID, true );
+       }
+}
+
+/**
+ * Deletes associated font files when a font face is deleted.
+ *
+ * @access private
+ * @since 6.5.0
+ *
+ * @param int     $post_id Post ID.
+ * @param WP_Post $post    Post object.
+ */
+function _wp_before_delete_font_face( $post_id, $post ) {
+       if ( 'wp_font_face' !== $post->post_type ) {
+               return;
+       }
+
+       $font_files = get_post_meta( $post_id, '_wp_font_face_file', false );
+       $font_dir   = wp_get_font_dir()['path'];
+
+       foreach ( $font_files as $font_file ) {
+               wp_delete_file( $font_dir . '/' . $font_file );
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludespostphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/post.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/post.php    2024-02-06 01:23:54 UTC (rev 57538)
+++ trunk/src/wp-includes/post.php      2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -564,6 +564,64 @@
</span><span class="cx" style="display: block; padding: 0 10px">                )
</span><span class="cx" style="display: block; padding: 0 10px">        );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        register_post_type(
+               'wp_font_family',
+               array(
+                       'labels'                         => array(
+                               'name'          => __( 'Font Families' ),
+                               'singular_name' => __( 'Font Family' ),
+                       ),
+                       'public'                         => false,
+                       '_builtin'                       => true, /* internal use only. don't use this when registering your own post type. */
+                       'hierarchical'                   => false,
+                       'capabilities'                   => array(
+                               'read'                   => 'edit_theme_options',
+                               'read_private_posts'     => 'edit_theme_options',
+                               'create_posts'           => 'edit_theme_options',
+                               'publish_posts'          => 'edit_theme_options',
+                               'edit_posts'             => 'edit_theme_options',
+                               'edit_others_posts'      => 'edit_theme_options',
+                               'edit_published_posts'   => 'edit_theme_options',
+                               'delete_posts'           => 'edit_theme_options',
+                               'delete_others_posts'    => 'edit_theme_options',
+                               'delete_published_posts' => 'edit_theme_options',
+                       ),
+                       'map_meta_cap'                   => true,
+                       'query_var'                      => false,
+                       'show_in_rest'                   => false,
+                       'rewrite'                        => false,
+               )
+       );
+
+       register_post_type(
+               'wp_font_face',
+               array(
+                       'labels'                         => array(
+                               'name'          => __( 'Font Faces' ),
+                               'singular_name' => __( 'Font Face' ),
+                       ),
+                       'public'                         => false,
+                       '_builtin'                       => true, /* internal use only. don't use this when registering your own post type. */
+                       'hierarchical'                   => false,
+                       'capabilities'                   => array(
+                               'read'                   => 'edit_theme_options',
+                               'read_private_posts'     => 'edit_theme_options',
+                               'create_posts'           => 'edit_theme_options',
+                               'publish_posts'          => 'edit_theme_options',
+                               'edit_posts'             => 'edit_theme_options',
+                               'edit_others_posts'      => 'edit_theme_options',
+                               'edit_published_posts'   => 'edit_theme_options',
+                               'delete_posts'           => 'edit_theme_options',
+                               'delete_others_posts'    => 'edit_theme_options',
+                               'delete_published_posts' => 'edit_theme_options',
+                       ),
+                       'map_meta_cap'                   => true,
+                       'query_var'                      => false,
+                       'show_in_rest'                   => false,
+                       'rewrite'                        => false,
+               )
+       );
+
</ins><span class="cx" style="display: block; padding: 0 10px">         register_post_status(
</span><span class="cx" style="display: block; padding: 0 10px">                'publish',
</span><span class="cx" style="display: block; padding: 0 10px">                array(
</span></span></pre></div>
<a id="trunksrcwpsettingsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-settings.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-settings.php 2024-02-06 01:23:54 UTC (rev 57538)
+++ trunk/src/wp-settings.php   2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -374,7 +374,10 @@
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/style-engine/class-wp-style-engine-css-rules-store.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/style-engine/class-wp-style-engine-processor.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/fonts/class-wp-font-face-resolver.php';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require ABSPATH . WPINC . '/fonts/class-wp-font-collection.php';
</ins><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/fonts/class-wp-font-face.php';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require ABSPATH . WPINC . '/fonts/class-wp-font-library.php';
+require ABSPATH . WPINC . '/fonts/class-wp-font-utils.php';
</ins><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/fonts.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/class-wp-script-modules.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/script-modules.php';
</span></span></pre></div>
<a id="trunktestsphpunittestsfontsfontlibraryfontLibraryHooksphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php                         (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php   2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,87 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test deleting wp_font_family and wp_font_face post types.
+ *
+ * @package WordPress
+ * @subpackage Font Library
+ *
+ * @group fonts
+ * @group font-library
+ */
+class Tests_Fonts_FontLibraryHooks extends WP_UnitTestCase {
+
+       public function test_deleting_font_family_deletes_child_font_faces() {
+               $font_family_id       = self::factory()->post->create(
+                       array(
+                               'post_type' => 'wp_font_family',
+                       )
+               );
+               $font_face_id         = self::factory()->post->create(
+                       array(
+                               'post_type'   => 'wp_font_face',
+                               'post_parent' => $font_family_id,
+                       )
+               );
+               $other_font_family_id = self::factory()->post->create(
+                       array(
+                               'post_type' => 'wp_font_family',
+                       )
+               );
+               $other_font_face_id   = self::factory()->post->create(
+                       array(
+                               'post_type'   => 'wp_font_face',
+                               'post_parent' => $other_font_family_id,
+                       )
+               );
+
+               wp_delete_post( $font_family_id, true );
+
+               $this->assertNull( get_post( $font_face_id ), 'Font face post should also have been deleted.' );
+               $this->assertNotNull( get_post( $other_font_face_id ), 'The other post should exist.' );
+       }
+
+       public function test_deleting_font_faces_deletes_associated_font_files() {
+               list( $font_face_id, $font_path ) = $this->create_font_face_with_file( 'OpenSans-Regular.woff2' );
+               list( , $other_font_path )        = $this->create_font_face_with_file( 'OpenSans-Regular.ttf' );
+
+               wp_delete_post( $font_face_id, true );
+
+               $this->assertFalse( file_exists( $font_path ), 'The font file should have been deleted when the post was deleted.' );
+               $this->assertTrue( file_exists( $other_font_path ), 'The other font file should exist.' );
+       }
+
+       protected function create_font_face_with_file( $filename ) {
+               $font_face_id = self::factory()->post->create(
+                       array(
+                               'post_type' => 'wp_font_face',
+                       )
+               );
+
+               $font_file = $this->upload_font_file( $filename );
+
+               // Make sure the font file uploaded successfully.
+               $this->assertFalse( $font_file['error'] );
+
+               $font_path     = $font_file['file'];
+               $font_filename = basename( $font_path );
+               add_post_meta( $font_face_id, '_wp_font_face_file', $font_filename );
+
+               return array( $font_face_id, $font_path );
+       }
+
+       protected function upload_font_file( $font_filename ) {
+               $font_file_path = DIR_TESTDATA . '/fonts/' . $font_filename;
+
+               add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
+               add_filter( 'upload_dir', 'wp_get_font_dir' );
+               $font_file = wp_upload_bits(
+                       $font_filename,
+                       null,
+                       file_get_contents( $font_file_path )
+               );
+               remove_filter( 'upload_dir', 'wp_get_font_dir' );
+               remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
+
+               return $font_file;
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsfontsfontlibrarywpFontCollection__constructphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php                             (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php       2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,26 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test WP_Font_Collection constructor.
+ *
+ * @package WordPress
+ * @subpackage Font Library
+ *
+ * @group fonts
+ * @group font-library
+ *
+ * @covers WP_Font_Collection::__construct
+ */
+class Tests_Fonts_WpFontCollection_Construct extends WP_UnitTestCase {
+
+       public function test_should_do_it_wrong_with_invalid_slug() {
+               $this->setExpectedIncorrectUsage( 'WP_Font_Collection::__construct' );
+               $mock_collection_data = array(
+                       'name'          => 'Test Collection',
+                       'font_families' => array( 'mock ' ),
+               );
+
+               $collection = new WP_Font_Collection( 'slug with spaces', $mock_collection_data );
+
+               $this->assertSame( 'slug-with-spaces', $collection->slug, 'Slug is not sanitized.' );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsfontsfontlibrarywpFontCollectiongetDataphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php                         (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php   2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,358 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test WP_Font_Collection::get_data.
+ *
+ * @package WordPress
+ * @subpackage Font Library
+ *
+ * @group fonts
+ * @group font-library
+ *
+ * @covers WP_Font_Collection::get_data
+ */
+class Tests_Fonts_WpFontCollection_GetData extends WP_UnitTestCase {
+
+       private static $mock_collection_data;
+
+       /**
+        * @dataProvider data_create_font_collection
+        *
+        * @param string $slug          Font collection slug.
+        * @param array  $config        Font collection config.
+        * @param array  $expected_data Expected collection data.
+        */
+       public function test_should_get_data_from_config_array( $slug, $config, $expected_data ) {
+               $collection = new WP_Font_Collection( $slug, $config );
+               $data       = $collection->get_data();
+
+               $this->assertSame( $slug, $collection->slug, 'The slug should match.' );
+               $this->assertSame( $expected_data, $data, 'The collection data should match.' );
+       }
+
+       /**
+        * @dataProvider data_create_font_collection
+        *
+        * @param string $slug          Font collection slug.
+        * @param array  $config        Font collection config.
+        * @param array  $expected_data Expected collection data.
+        */
+       public function test_should_get_data_from_json_file( $slug, $config, $expected_data ) {
+               $mock_file = wp_tempnam( 'my-collection-data-' );
+               file_put_contents( $mock_file, wp_json_encode( $config ) );
+
+               $collection = new WP_Font_Collection( $slug, $mock_file );
+               $data       = $collection->get_data();
+
+               $this->assertSame( $slug, $collection->slug, 'The slug should match.' );
+               $this->assertSame( $expected_data, $data, 'The collection data should match.' );
+       }
+
+       /**
+        * @dataProvider data_create_font_collection
+        *
+        * @param string $slug          Font collection slug.
+        * @param array  $config        Font collection config.
+        * @param array  $expected_data Expected collection data.
+        */
+       public function test_should_get_data_from_json_url( $slug, $config, $expected_data ) {
+               add_filter( 'pre_http_request', array( $this, 'mock_request' ), 10, 3 );
+
+               self::$mock_collection_data = $config;
+               $collection                 = new WP_Font_Collection( $slug, 'https://example.com/fonts/mock-font-collection.json' );
+               $data                       = $collection->get_data();
+
+               remove_filter( 'pre_http_request', array( $this, 'mock_request' ) );
+
+               $this->assertSame( $slug, $collection->slug, 'The slug should match.' );
+               $this->assertSame( $expected_data, $data, 'The collection data should match.' );
+       }
+
+       /**
+        * Data provider.
+        *
+        * @return array
+        */
+       public function data_create_font_collection() {
+               return array(
+
+                       'font collection with required data' => array(
+                               'slug'          => 'my-collection',
+                               'config'        => array(
+                                       'name'          => 'My Collection',
+                                       'font_families' => array( array() ),
+                               ),
+                               'expected_data' => array(
+                                       'description'   => '',
+                                       'categories'    => array(),
+                                       'name'          => 'My Collection',
+                                       'font_families' => array( array() ),
+                               ),
+                       ),
+
+                       'font collection with all data'      => array(
+                               'slug'          => 'my-collection',
+                               'config'        => array(
+                                       'name'          => 'My Collection',
+                                       'description'   => 'My collection description',
+                                       'font_families' => array( array() ),
+                                       'categories'    => array(),
+                               ),
+                               'expected_data' => array(
+                                       'description'   => 'My collection description',
+                                       'categories'    => array(),
+                                       'name'          => 'My Collection',
+                                       'font_families' => array( array() ),
+                               ),
+                       ),
+
+                       'font collection with risky data'    => array(
+                               'slug'          => 'my-collection',
+                               'config'        => array(
+                                       'name'              => 'My Collection<script>alert("xss")</script>',
+                                       'description'       => 'My collection description<script>alert("xss")</script>',
+                                       'font_families'     => array(
+                                               array(
+                                                       'font_family_settings' => array(
+                                                               'fontFamily'        => 'Open Sans, sans-serif<script>alert("xss")</script>',
+                                                               'slug'              => 'open-sans',
+                                                               'name'              => 'Open Sans<script>alert("xss")</script>',
+                                                               'fontFace'          => array(
+                                                                       array(
+                                                                               'fontFamily' => 'Open Sans',
+                                                                               'fontStyle'  => 'normal',
+                                                                               'fontWeight' => '400',
+                                                                               'src'        => 'https://example.com/src-as-string.ttf?a=<script>alert("xss")</script>',
+                                                                       ),
+                                                                       array(
+                                                                               'fontFamily' => 'Open Sans',
+                                                                               'fontStyle'  => 'normal',
+                                                                               'fontWeight' => '400',
+                                                                               'src'        => array(
+                                                                                       'https://example.com/src-as-array.woff2?a=<script>alert("xss")</script>',
+                                                                                       'https://example.com/src-as-array.ttf',
+                                                                               ),
+                                                                       ),
+                                                               ),
+                                                               'unwanted_property' => 'potentially evil value',
+                                                       ),
+                                                       'categories'           => array( 'sans-serif<script>alert("xss")</script>' ),
+                                               ),
+                                       ),
+                                       'categories'        => array(
+                                               array(
+                                                       'name'              => 'Mock col<script>alert("xss")</script>',
+                                                       'slug'              => 'mock-col<script>alert("xss")</script>',
+                                                       'unwanted_property' => 'potentially evil value',
+                                               ),
+                                       ),
+                                       'unwanted_property' => 'potentially evil value',
+                               ),
+                               'expected_data' => array(
+                                       'description'   => 'My collection description',
+                                       'categories'    => array(
+                                               array(
+                                                       'name' => 'Mock col',
+                                                       'slug' => 'mock-colalertxss',
+                                               ),
+                                       ),
+                                       'name'          => 'My Collection',
+                                       'font_families' => array(
+                                               array(
+                                                       'font_family_settings' => array(
+                                                               'fontFamily' => 'Open Sans, sans-serif',
+                                                               'slug'       => 'open-sans',
+                                                               'name'       => 'Open Sans',
+                                                               'fontFace'   => array(
+                                                                       array(
+                                                                               'fontFamily' => 'Open Sans',
+                                                                               'fontStyle'  => 'normal',
+                                                                               'fontWeight' => '400',
+                                                                               'src'        => 'https://example.com/src-as-string.ttf?a=',
+                                                                       ),
+                                                                       array(
+                                                                               'fontFamily' => 'Open Sans',
+                                                                               'fontStyle'  => 'normal',
+                                                                               'fontWeight' => '400',
+                                                                               'src'        => array(
+                                                                                       'https://example.com/src-as-array.woff2?a=',
+                                                                                       'https://example.com/src-as-array.ttf',
+                                                                               ),
+                                                                       ),
+                                                               ),
+                                                       ),
+                                                       'categories'           => array( 'sans-serifalertxss' ),
+                                               ),
+                                       ),
+                               ),
+                       ),
+
+               );
+       }
+
+       /**
+        * @dataProvider data_should_error_when_missing_properties
+        *
+        * @param array $config Font collection config.
+        */
+       public function test_should_error_when_missing_properties( $config ) {
+               $this->setExpectedIncorrectUsage( 'WP_Font_Collection::sanitize_and_validate_data' );
+
+               $collection = new WP_Font_Collection( 'my-collection', $config );
+               $data       = $collection->get_data();
+
+               $this->assertWPError( $data, 'Error is not returned when property is missing or invalid.' );
+               $this->assertSame(
+                       $data->get_error_code(),
+                       'font_collection_missing_property',
+                       'Incorrect error code when property is missing or invalid.'
+               );
+       }
+
+       /**
+        * Data provider.
+        *
+        * @return array
+        */
+       public function data_should_error_when_missing_properties() {
+               return array(
+                       'missing name'          => array(
+                               'config' => array(
+                                       'font_families' => array( 'mock' ),
+                               ),
+                       ),
+                       'empty name'            => array(
+                               'config' => array(
+                                       'name'          => '',
+                                       'font_families' => array( 'mock' ),
+                               ),
+                       ),
+                       'missing font_families' => array(
+                               'config' => array(
+                                       'name' => 'My Collection',
+                               ),
+                       ),
+                       'empty font_families'   => array(
+                               'config' => array(
+                                       'name'          => 'My Collection',
+                                       'font_families' => array(),
+                               ),
+                       ),
+               );
+       }
+
+       public function test_should_error_with_invalid_json_file_path() {
+               $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' );
+
+               $collection = new WP_Font_Collection( 'my-collection', 'non-existing.json' );
+               $data       = $collection->get_data();
+
+               $this->assertWPError( $data, 'Error is not returned when invalid file path is provided.' );
+               $this->assertSame(
+                       $data->get_error_code(),
+                       'font_collection_json_missing',
+                       'Incorrect error code when invalid file path is provided.'
+               );
+       }
+
+       public function test_should_error_with_invalid_json_from_file() {
+               $mock_file = wp_tempnam( 'my-collection-data-' );
+               file_put_contents( $mock_file, 'invalid-json' );
+
+               $collection = new WP_Font_Collection( 'my-collection', $mock_file );
+
+               // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Testing error response returned by `load_from_json`, not the underlying error from `wp_json_file_decode`.
+               $data = @$collection->get_data();
+
+               $this->assertWPError( $data, 'Error is not returned with invalid json file contents.' );
+               $this->assertSame(
+                       $data->get_error_code(),
+                       'font_collection_decode_error',
+                       'Incorrect error code with invalid json file contents.'
+               );
+       }
+
+       public function test_should_error_with_invalid_url() {
+               $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' );
+
+               $collection = new WP_Font_Collection( 'my-collection', 'not-a-url' );
+               $data       = $collection->get_data();
+
+               $this->assertWPError( $data, 'Error is not returned when invalid url is provided.' );
+               $this->assertSame(
+                       $data->get_error_code(),
+                       'font_collection_json_missing',
+                       'Incorrect error code when invalid url is provided.'
+               );
+       }
+
+       public function test_should_error_with_unsuccessful_response_status() {
+               add_filter( 'pre_http_request', array( $this, 'mock_request_unsuccessful_response' ), 10, 3 );
+
+               $collection = new WP_Font_Collection( 'my-collection', 'https://example.com/fonts/missing-collection.json' );
+               $data       = $collection->get_data();
+
+               remove_filter( 'pre_http_request', array( $this, 'mock_request_unsuccessful_response' ) );
+
+               $this->assertWPError( $data, 'Error is not returned when response is unsuccessful.' );
+               $this->assertSame(
+                       $data->get_error_code(),
+                       'font_collection_request_error',
+                       'Incorrect error code when response is unsuccussful.'
+               );
+       }
+
+       public function test_should_error_with_invalid_json_from_url() {
+               add_filter( 'pre_http_request', array( $this, 'mock_request_invalid_json' ), 10, 3 );
+
+               $collection = new WP_Font_Collection( 'my-collection', 'https://example.com/fonts/invalid-collection.json' );
+               $data       = $collection->get_data();
+
+               remove_filter( 'pre_http_request', array( $this, 'mock_request_invalid_json' ) );
+
+               $this->assertWPError( $data, 'Error is not returned when response is invalid json.' );
+               $this->assertSame(
+                       $data->get_error_code(),
+                       'font_collection_decode_error',
+                       'Incorrect error code when response is invalid json.'
+               );
+       }
+
+       public function mock_request( $preempt, $args, $url ) {
+               if ( 'https://example.com/fonts/mock-font-collection.json' !== $url ) {
+                       return false;
+               }
+
+               return array(
+                       'body'     => wp_json_encode( self::$mock_collection_data ),
+                       'response' => array(
+                               'code' => 200,
+                       ),
+               );
+       }
+
+       public function mock_request_unsuccessful_response( $preempt, $args, $url ) {
+               if ( 'https://example.com/fonts/missing-collection.json' !== $url ) {
+                       return false;
+               }
+
+               return array(
+                       'body'     => '',
+                       'response' => array(
+                               'code' => 404,
+                       ),
+               );
+       }
+
+       public function mock_request_invalid_json( $preempt, $args, $url ) {
+               if ( 'https://example.com/fonts/invalid-collection.json' !== $url ) {
+                       return false;
+               }
+
+               return array(
+                       'body'     => 'invalid',
+                       'response' => array(
+                               'code' => 200,
+                       ),
+               );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsfontsfontlibrarywpFontLibrarybasephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php                               (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php 2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,25 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test Case for WP_Font_Library tests.
+ *
+ * @package WordPress
+ * @subpackage Font Library
+ */
+abstract class WP_Font_Library_UnitTestCase extends WP_UnitTestCase {
+       public function reset_font_collections() {
+               $collections = WP_Font_Library::get_instance()->get_font_collections();
+               foreach ( $collections as $slug => $collection ) {
+                       WP_Font_Library::get_instance()->unregister_font_collection( $slug );
+               }
+       }
+
+       public function set_up() {
+               parent::set_up();
+               $this->reset_font_collections();
+       }
+
+       public function tear_down() {
+               parent::tear_down();
+               $this->reset_font_collections();
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsfontsfontlibrarywpFontLibrarygetFontCollectionphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php                          (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php    2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,30 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test WP_Font_Library::get_font_collections().
+ *
+ * @package WordPress
+ * @subpackage Font Library
+ *
+ * @group fonts
+ * @group font-library
+ *
+ * @covers WP_Font_Library::get_font_collection
+ */
+class Tests_Fonts_WpFontLibrary_GetFontCollection extends WP_Font_Library_UnitTestCase {
+
+       public function test_should_get_font_collection() {
+               $mock_collection_data = array(
+                       'name'          => 'Test Collection',
+                       'font_families' => array( 'mock' ),
+               );
+
+               wp_register_font_collection( 'my-font-collection', $mock_collection_data );
+               $font_collection = WP_Font_Library::get_instance()->get_font_collection( 'my-font-collection' );
+               $this->assertInstanceOf( 'WP_Font_Collection', $font_collection );
+       }
+
+       public function test_should_get_no_font_collection_if_the_slug_is_not_registered() {
+               $font_collection = WP_Font_Library::get_instance()->get_font_collection( 'not-registered-font-collection' );
+               $this->assertWPError( $font_collection );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsfontsfontlibrarywpFontLibrarygetFontCollectionsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php                         (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php   2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,34 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test WP_Font_Library::get_font_collections().
+ *
+ * @package WordPress
+ * @subpackage Font Library
+ *
+ * @group fonts
+ * @group font-library
+ *
+ * @covers WP_Font_Library::get_font_collections
+ */
+class Tests_Fonts_WpFontLibrary_GetFontCollections extends WP_Font_Library_UnitTestCase {
+       public function test_should_get_an_empty_list() {
+               $font_collections = WP_Font_Library::get_instance()->get_font_collections();
+               $this->assertEmpty( $font_collections, 'Should return an empty array.' );
+       }
+
+       public function test_should_get_mock_font_collection() {
+               $my_font_collection_config = array(
+                       'name'          => 'My Font Collection',
+                       'description'   => 'Demo about how to a font collection to your WordPress Font Library.',
+                       'font_families' => array( 'mock' ),
+               );
+
+               WP_Font_Library::get_instance()->register_font_collection( 'my-font-collection', $my_font_collection_config );
+
+               $font_collections = WP_Font_Library::get_instance()->get_font_collections();
+               $this->assertNotEmpty( $font_collections, 'Should return an array of font collections.' );
+               $this->assertCount( 1, $font_collections, 'Should return an array with one font collection.' );
+               $this->assertArrayHasKey( 'my-font-collection', $font_collections, 'The array should have the key of the registered font collection id.' );
+               $this->assertInstanceOf( 'WP_Font_Collection', $font_collections['my-font-collection'], 'The value of the array $font_collections[id] should be an instance of WP_Font_Collection class.' );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsfontsfontlibrarywpFontLibraryregisterFontCollectionphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php                             (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php       2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,40 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test WP_Font_Library::register_font_collection().
+ *
+ * @package WordPress
+ * @subpackage Font Library
+ *
+ * @group fonts
+ * @group font-library
+ *
+ * @covers WP_Font_Library::register_font_collection
+ */
+class Tests_Fonts_WpFontLibrary_RegisterFontCollection extends WP_Font_Library_UnitTestCase {
+       public function test_should_register_font_collection() {
+               $config = array(
+                       'name'          => 'My Collection',
+                       'font_families' => array( 'mock' ),
+               );
+
+               $collection = WP_Font_Library::get_instance()->register_font_collection( 'my-collection', $config );
+               $this->assertInstanceOf( 'WP_Font_Collection', $collection );
+       }
+
+       public function test_should_return_error_if_slug_is_repeated() {
+               $mock_collection_data = array(
+                       'name'          => 'Test Collection',
+                       'font_families' => array( 'mock' ),
+               );
+
+               // Register first collection.
+               $collection1 = WP_Font_Library::get_instance()->register_font_collection( 'my-collection-1', $mock_collection_data );
+               $this->assertInstanceOf( 'WP_Font_Collection', $collection1, 'A collection should be registered.' );
+
+               // Expects a _doing_it_wrong notice.
+               $this->setExpectedIncorrectUsage( 'WP_Font_Library::register_font_collection' );
+
+               // Try to register a second collection with same slug.
+               WP_Font_Library::get_instance()->register_font_collection( 'my-collection-1', $mock_collection_data );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsfontsfontlibrarywpFontLibraryunregisterFontCollectionphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php                           (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php     2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,46 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test WP_Font_Library::unregister_font_collection().
+ *
+ * @package WordPress
+ * @subpackage Font Library
+ *
+ * @group fonts
+ * @group font-library
+ *
+ * @covers WP_Font_Library::unregister_font_collection
+ */
+class Tests_Fonts_WpFontLibrary_UnregisterFontCollection extends WP_Font_Library_UnitTestCase {
+
+       public function test_should_unregister_font_collection() {
+               $mock_collection_data = array(
+                       'name'          => 'Test Collection',
+                       'font_families' => array( 'mock' ),
+               );
+
+               // Registers two mock font collections.
+               WP_Font_Library::get_instance()->register_font_collection( 'mock-font-collection-1', $mock_collection_data );
+               WP_Font_Library::get_instance()->register_font_collection( 'mock-font-collection-2', $mock_collection_data );
+
+               // Unregister mock font collection.
+               WP_Font_Library::get_instance()->unregister_font_collection( 'mock-font-collection-1' );
+               $collections = WP_Font_Library::get_instance()->get_font_collections();
+               $this->assertArrayNotHasKey( 'mock-font-collection-1', $collections, 'Font collection was not unregistered.' );
+               $this->assertArrayHasKey( 'mock-font-collection-2', $collections, 'Font collection was unregistered by mistake.' );
+
+               // Unregisters remaining mock font collection.
+               WP_Font_Library::get_instance()->unregister_font_collection( 'mock-font-collection-2' );
+               $collections = WP_Font_Library::get_instance()->get_font_collections();
+               $this->assertArrayNotHasKey( 'mock-font-collection-2', $collections, 'Mock font collection was not unregistered.' );
+
+               // Checks that all font collections were unregistered.
+               $this->assertEmpty( $collections, 'Font collections were not unregistered.' );
+       }
+
+       public function unregister_non_existing_collection() {
+               // Unregisters non-existing font collection.
+               WP_Font_Library::get_instance()->unregister_font_collection( 'non-existing-collection' );
+               $collections = WP_Font_Library::get_instance()->get_font_collections();
+               $this->assertEmpty( $collections, 'No collections should be registered.' );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsfontsfontlibrarywpFontUtilsgetFontFaceSlugphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php                              (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php        2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,92 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test WP_Font_Utils::get_font_face_slug().
+ *
+ * @package WordPress
+ * @subpackage Font Library
+ *
+ * @group fonts
+ * @group font-library
+ *
+ * @covers WP_Font_Utils::get_font_face_slug
+ */
+class Tests_Fonts_WpFontUtils_GetFontFaceSlug extends WP_UnitTestCase {
+       /**
+        * @dataProvider data_get_font_face_slug_normalizes_values
+        *
+        * @param string[] $settings      Settings to test.
+        * @param string   $expected_slug Expected slug results.
+        */
+       public function test_get_font_face_slug_normalizes_values( $settings, $expected_slug ) {
+               $slug = WP_Font_Utils::get_font_face_slug( $settings );
+
+               $this->assertSame( $expected_slug, $slug );
+       }
+
+       /**
+        * Data provider.
+        *
+        * @return array
+        */
+       public function data_get_font_face_slug_normalizes_values() {
+               return array(
+                       'Sets defaults'                           => array(
+                               'settings'      => array(
+                                       'fontFamily' => 'Open Sans',
+                               ),
+                               'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
+                       ),
+                       'Converts normal weight to 400'           => array(
+                               'settings'      => array(
+                                       'fontFamily' => 'Open Sans',
+                                       'fontWeight' => 'normal',
+                               ),
+                               'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
+                       ),
+                       'Converts bold weight to 700'             => array(
+                               'settings'      => array(
+                                       'fontFamily' => 'Open Sans',
+                                       'fontWeight' => 'bold',
+                               ),
+                               'expected_slug' => 'open sans;normal;700;100%;U+0-10FFFF',
+                       ),
+                       'Converts normal font-stretch to 100%'    => array(
+                               'settings'      => array(
+                                       'fontFamily'  => 'Open Sans',
+                                       'fontStretch' => 'normal',
+                               ),
+                               'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
+                       ),
+                       'Removes double quotes from fontFamilies' => array(
+                               'settings'      => array(
+                                       'fontFamily' => '"Open Sans"',
+                               ),
+                               'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
+                       ),
+                       'Removes single quotes from fontFamilies' => array(
+                               'settings'      => array(
+                                       'fontFamily' => "'Open Sans'",
+                               ),
+                               'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
+                       ),
+                       'Removes spaces between comma separated font families' => array(
+                               'settings'      => array(
+                                       'fontFamily' => 'Open Sans, serif',
+                               ),
+                               'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF',
+                       ),
+                       'Removes tabs between comma separated font families' => array(
+                               'settings'      => array(
+                                       'fontFamily' => "Open Sans,\tserif",
+                               ),
+                               'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF',
+                       ),
+                       'Removes new lines between comma separated font families' => array(
+                               'settings'      => array(
+                                       'fontFamily' => "Open Sans,\nserif",
+                               ),
+                               'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF',
+                       ),
+               );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsfontsfontlibrarywpFontUtilssanitizeFontFamilyphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php                           (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php     2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,63 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test WP_Font_Utils::sanitize_font_family().
+ *
+ * @package WordPress
+ * @subpackage Font Library
+ *
+ * @group fonts
+ * @group font-library
+ *
+ * @covers WP_Font_Utils::sanitize_font_family
+ */
+class Tests_Fonts_WpFontUtils_SanitizeFontFamily extends WP_UnitTestCase {
+
+       /**
+        * @dataProvider data_should_sanitize_font_family
+        *
+        * @param string $font_family Font family to test.
+        * @param string $expected    Expected family.
+        */
+       public function test_should_sanitize_font_family( $font_family, $expected ) {
+               $this->assertSame(
+                       $expected,
+                       WP_Font_Utils::sanitize_font_family(
+                               $font_family
+                       )
+               );
+       }
+
+       /**
+        * Data provider.
+        *
+        * @return array
+        */
+       public function data_should_sanitize_font_family() {
+               return array(
+                       'data_families_with_spaces_and_numbers' => array(
+                               'font_family' => 'Rock 3D , Open Sans,serif',
+                               'expected'    => '"Rock 3D", "Open Sans", serif',
+                       ),
+                       'data_single_font_family'               => array(
+                               'font_family' => 'Rock 3D',
+                               'expected'    => '"Rock 3D"',
+                       ),
+                       'data_no_spaces'                        => array(
+                               'font_family' => 'Rock3D',
+                               'expected'    => 'Rock3D',
+                       ),
+                       'data_many_spaces_and_existing_quotes'  => array(
+                               'font_family' => 'Rock 3D serif, serif,sans-serif, "Open Sans"',
+                               'expected'    => '"Rock 3D serif", serif, sans-serif, "Open Sans"',
+                       ),
+                       'data_empty_family'                     => array(
+                               'font_family' => ' ',
+                               'expected'    => '',
+                       ),
+                       'data_font_family_with_whitespace_tags_new_lines' => array(
+                               'font_family' => "   Rock      3D</style><script>alert('XSS');</script>\n    ",
+                               'expected'    => '"Rock 3D"',
+                       ),
+               );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsfontsfontlibrarywpFontUtilssanitizeFromSchemaphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php                           (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php     2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,310 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test WP_Font_Utils::sanitize_from_schema().
+ *
+ * @package WordPress
+ * @subpackage Font Library
+ *
+ * @group fonts
+ * @group font-library
+ *
+ * @covers WP_Font_Utils::sanitize_from_schema
+ */
+class Tests_Fonts_WpFontUtils_SanitizeFromSchema extends WP_UnitTestCase {
+       /**
+        * @dataProvider data_sanitize_from_schema
+        *
+        * @param array $data     Data to sanitize.
+        * @param array $schema   Schema to use for sanitization.
+        * @param array $expected Expected result.
+        */
+       public function test_sanitize_from_schema( $data, $schema, $expected ) {
+               $result = WP_Font_Utils::sanitize_from_schema( $data, $schema );
+
+               $this->assertSame( $result, $expected );
+       }
+
+       public function data_sanitize_from_schema() {
+               return array(
+                       'One level associative array'  => array(
+                               'data'     => array(
+                                       'slug'       => 'open      -       sans</style><script>alert("xss")</script>',
+                                       'fontFamily' => 'Open Sans, sans-serif</style><script>alert("xss")</script>',
+                                       'src'        => 'https://wordpress.org/example.json</style><script>alert("xss")</script>',
+                               ),
+                               'schema'   => array(
+                                       'slug'       => 'sanitize_title',
+                                       'fontFamily' => 'sanitize_text_field',
+                                       'src'        => 'sanitize_url',
+                               ),
+                               'expected' => array(
+                                       'slug'       => 'open-sansalertxss',
+                                       'fontFamily' => 'Open Sans, sans-serif',
+                                       'src'        => 'https://wordpress.org/example.json/stylescriptalert(xss)/script',
+                               ),
+                       ),
+
+                       'Nested associative arrays'    => array(
+                               'data'     => array(
+                                       'slug'       => 'open      -       sans</style><script>alert("xss")</script>',
+                                       'fontFamily' => 'Open Sans, sans-serif</style><script>alert("xss")</script>',
+                                       'src'        => 'https://wordpress.org/example.json</style><script>alert("xss")</script>',
+                                       'nested'     => array(
+                                               'key1'    => 'value1</style><script>alert("xss")</script>',
+                                               'key2'    => 'value2</style><script>alert("xss")</script>',
+                                               'nested2' => array(
+                                                       'key3' => 'value3</style><script>alert("xss")</script>',
+                                                       'key4' => 'value4</style><script>alert("xss")</script>',
+                                               ),
+                                       ),
+                               ),
+                               'schema'   => array(
+                                       'slug'       => 'sanitize_title',
+                                       'fontFamily' => 'sanitize_text_field',
+                                       'src'        => 'sanitize_url',
+                                       'nested'     => array(
+                                               'key1'    => 'sanitize_text_field',
+                                               'key2'    => 'sanitize_text_field',
+                                               'nested2' => array(
+                                                       'key3' => 'sanitize_text_field',
+                                                       'key4' => 'sanitize_text_field',
+                                               ),
+                                       ),
+                               ),
+                               'expected' => array(
+                                       'slug'       => 'open-sansalertxss',
+                                       'fontFamily' => 'Open Sans, sans-serif',
+                                       'src'        => 'https://wordpress.org/example.json/stylescriptalert(xss)/script',
+                                       'nested'     => array(
+                                               'key1'    => 'value1',
+                                               'key2'    => 'value2',
+                                               'nested2' => array(
+                                                       'key3' => 'value3',
+                                                       'key4' => 'value4',
+                                               ),
+                                       ),
+                               ),
+                       ),
+
+                       'Indexed arrays'               => array(
+                               'data'     => array(
+                                       'slug' => 'oPeN SaNs',
+                                       'enum' => array(
+                                               'value1<script>alert("xss")</script>',
+                                               'value2<script>alert("xss")</script>',
+                                               'value3<script>alert("xss")</script>',
+                                       ),
+                               ),
+                               'schema'   => array(
+                                       'slug' => 'sanitize_title',
+                                       'enum' => array( 'sanitize_text_field' ),
+                               ),
+                               'expected' => array(
+                                       'slug' => 'open-sans',
+                                       'enum' => array( 'value1', 'value2', 'value3' ),
+                               ),
+                       ),
+
+                       'Nested indexed arrays'        => array(
+                               'data'     => array(
+                                       'slug'     => 'OPEN-SANS',
+                                       'name'     => 'Open Sans</style><script>alert("xss")</script>',
+                                       'fontFace' => array(
+                                               array(
+                                                       'fontFamily' => 'Open Sans, sans-serif</style><script>alert("xss")</script>',
+                                                       'src'        => 'https://wordpress.org/example.json/stylescriptalert(xss)/script',
+                                               ),
+                                               array(
+                                                       'fontFamily' => 'Open Sans, sans-serif</style><script>alert("xss")</script>',
+                                                       'src'        => 'https://wordpress.org/example.json/stylescriptalert(xss)/script',
+                                               ),
+                                       ),
+                               ),
+                               'schema'   => array(
+                                       'slug'     => 'sanitize_title',
+                                       'name'     => 'sanitize_text_field',
+                                       'fontFace' => array(
+                                               array(
+                                                       'fontFamily' => 'sanitize_text_field',
+                                                       'src'        => 'sanitize_url',
+                                               ),
+                                       ),
+                               ),
+                               'expected' => array(
+                                       'slug'     => 'open-sans',
+                                       'name'     => 'Open Sans',
+                                       'fontFace' => array(
+                                               array(
+                                                       'fontFamily' => 'Open Sans, sans-serif',
+                                                       'src'        => 'https://wordpress.org/example.json/stylescriptalert(xss)/script',
+                                               ),
+                                               array(
+                                                       'fontFamily' => 'Open Sans, sans-serif',
+                                                       'src'        => 'https://wordpress.org/example.json/stylescriptalert(xss)/script',
+                                               ),
+                                       ),
+                               ),
+                       ),
+
+                       'Custom sanitization function' => array(
+                               'data'     => array(
+                                       'key1' => 'abc123edf456ghi789',
+                                       'key2' => 'value2',
+                               ),
+                               'schema'   => array(
+                                       'key1' => function ( $value ) {
+                                               // Remove the six first character.
+                                               return substr( $value, 6 );
+                                       },
+                                       'key2' => function ( $value ) {
+                                               // Capitalize the value.
+                                               return strtoupper( $value );
+                                       },
+                               ),
+                               'expected' => array(
+                                       'key1' => 'edf456ghi789',
+                                       'key2' => 'VALUE2',
+                               ),
+                       ),
+
+                       'Null as schema value'         => array(
+                               'data'     => array(
+                                       'key1'   => 'value1<script>alert("xss")</script>',
+                                       'key2'   => 'value2',
+                                       'nested' => array(
+                                               'key3' => 'value3',
+                                               'key4' => 'value4',
+                                       ),
+                               ),
+                               'schema'   => array(
+                                       'key1'   => null,
+                                       'key2'   => 'sanitize_text_field',
+                                       'nested' => null,
+                               ),
+                               'expected' => array(
+                                       'key1'   => 'value1<script>alert("xss")</script>',
+                                       'key2'   => 'value2',
+                                       'nested' => array(
+                                               'key3' => 'value3',
+                                               'key4' => 'value4',
+                                       ),
+                               ),
+                       ),
+
+                       'Keys to remove'               => array(
+                               'data'     => array(
+                                       'key1'              => 'value1',
+                                       'key2'              => 'value2',
+                                       'unwanted1'         => 'value',
+                                       'unwanted2'         => 'value',
+                                       'nestedAssociative' => array(
+                                               'key5'      => 'value5',
+                                               'unwanted3' => 'value',
+                                       ),
+                                       'nestedIndexed'     => array(
+                                               array(
+                                                       'key6'      => 'value7',
+                                                       'unwanted4' => 'value',
+                                               ),
+                                               array(
+                                                       'key6'      => 'value7',
+                                                       'unwanted5' => 'value',
+                                               ),
+                                       ),
+
+                               ),
+                               'schema'   => array(
+                                       'key1'              => 'sanitize_text_field',
+                                       'key2'              => 'sanitize_text_field',
+                                       'nestedAssociative' => array(
+                                               'key5' => 'sanitize_text_field',
+                                       ),
+                                       'nestedIndexed'     => array(
+                                               array(
+                                                       'key6' => 'sanitize_text_field',
+                                               ),
+                                       ),
+                               ),
+                               'expected' => array(
+                                       'key1'              => 'value1',
+                                       'key2'              => 'value2',
+                                       'nestedAssociative' => array(
+                                               'key5' => 'value5',
+                                       ),
+                                       'nestedIndexed'     => array(
+                                               array(
+                                                       'key6' => 'value7',
+                                               ),
+                                               array(
+                                                       'key6' => 'value7',
+                                               ),
+                                       ),
+                               ),
+                       ),
+
+                       'With empty structure'         => array(
+                               'data'     => array(
+                                       'slug'   => 'open-sans',
+                                       'nested' => array(
+                                               'key1'    => 'value</style><script>alert("xss")</script>',
+                                               'nested2' => array(
+                                                       'key2'    => 'value</style><script>alert("xss")</script>',
+                                                       'nested3' => array(
+                                                               'nested4' => array(),
+                                                       ),
+                                               ),
+                                       ),
+                               ),
+                               'schema'   => array(
+                                       'slug'   => 'sanitize_title',
+                                       'nested' => array(
+                                               'key1'    => 'sanitize_text_field',
+                                               'nested2' => array(
+                                                       'key2'    => 'sanitize_text_field',
+                                                       'nested3' => array(
+                                                               'key3'    => 'sanitize_text_field',
+                                                               'nested4' => array(
+                                                                       'key4' => 'sanitize_text_field',
+                                                               ),
+                                                       ),
+                                               ),
+                                       ),
+                               ),
+                               'expected' => array(
+                                       'slug'   => 'open-sans',
+                                       'nested' => array(
+                                               'key1'    => 'value',
+                                               'nested2' => array(
+                                                       'key2' => 'value',
+                                               ),
+                                       ),
+                               ),
+                       ),
+               );
+       }
+
+       public function test_sanitize_from_schema_with_invalid_data() {
+               $data   = 'invalid data';
+               $schema = array(
+                       'key1' => 'sanitize_text_field',
+                       'key2' => 'sanitize_text_field',
+               );
+
+               $result = WP_Font_Utils::sanitize_from_schema( $data, $schema );
+
+               $this->assertSame( $result, array() );
+       }
+
+
+       public function test_sanitize_from_schema_with_invalid_schema() {
+               $data   = array(
+                       'key1' => 'value1',
+                       'key2' => 'value2',
+               );
+               $schema = 'invalid schema';
+
+               $result = WP_Font_Utils::sanitize_from_schema( $data, $schema );
+
+               $this->assertSame( $result, array() );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsfontsfontlibrarywpFontsDirphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/fonts/font-library/wpFontsDir.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpFontsDir.php                               (rev 0)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpFontsDir.php 2024-02-06 08:40:38 UTC (rev 57539)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,72 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test wp_get_font_dir().
+ *
+ * @package WordPress
+ * @subpackage Font Library
+ *
+ * @group fonts
+ * @group font-library
+ *
+ * @covers ::wp_get_font_dir
+ */
+class Tests_Fonts_WpFontDir extends WP_UnitTestCase {
+       private static $dir_defaults;
+
+       public static function set_up_before_class() {
+               parent::set_up_before_class();
+
+               static::$dir_defaults = array(
+                       'path'    => path_join( WP_CONTENT_DIR, 'fonts' ),
+                       'url'     => content_url( 'fonts' ),
+                       'subdir'  => '',
+                       'basedir' => path_join( WP_CONTENT_DIR, 'fonts' ),
+                       'baseurl' => content_url( 'fonts' ),
+                       'error'   => false,
+               );
+       }
+
+       public function test_fonts_dir() {
+               $font_dir = wp_get_font_dir();
+
+               $this->assertSame( $font_dir, static::$dir_defaults );
+       }
+
+       public function test_fonts_dir_with_filter() {
+               // Define a callback function to pass to the filter.
+               function set_new_values( $defaults ) {
+                       $defaults['path']    = '/custom-path/fonts/my-custom-subdir';
+                       $defaults['url']     = 'http://example.com/custom-path/fonts/my-custom-subdir';
+                       $defaults['subdir']  = 'my-custom-subdir';
+                       $defaults['basedir'] = '/custom-path/fonts';
+                       $defaults['baseurl'] = 'http://example.com/custom-path/fonts';
+                       $defaults['error']   = false;
+                       return $defaults;
+               }
+
+               // Add the filter.
+               add_filter( 'font_dir', 'set_new_values' );
+
+               // Gets the fonts dir.
+               $font_dir = wp_get_font_dir();
+
+               $expected = array(
+                       'path'    => '/custom-path/fonts/my-custom-subdir',
+                       'url'     => 'http://example.com/custom-path/fonts/my-custom-subdir',
+                       'subdir'  => 'my-custom-subdir',
+                       'basedir' => '/custom-path/fonts',
+                       'baseurl' => 'http://example.com/custom-path/fonts',
+                       'error'   => false,
+               );
+
+               // Remove the filter.
+               remove_filter( 'font_dir', 'set_new_values' );
+
+               $this->assertSame( $expected, $font_dir, 'The wp_get_font_dir() method should return the expected values.' );
+
+               // Gets the fonts dir.
+               $font_dir = wp_get_font_dir();
+
+               $this->assertSame( static::$dir_defaults, $font_dir, 'The wp_get_font_dir() method should return the default values.' );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/fonts/font-library/wpFontsDir.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span></div>

</body>
</html>