WordPress Version: 6.5
/**
* Runs the theme.json webfonts handler.
*
* Using `WP_Theme_JSON_Resolver`, it gets the fonts defined
* in the `theme.json` for the current selection and style
* variations, validates the font-face properties, generates
* the '@font-face' style declarations, and then enqueues the
* styles for both the editor and front-end.
*
* Design Notes:
* This is not a public API, but rather an internal handler.
* A future public Webfonts API will replace this stopgap code.
*
* This code design is intentional.
* a. It hides the inner-workings.
* b. It does not expose API ins or outs for consumption.
* c. It only works with a theme's `theme.json`.
*
* Why?
* a. To avoid backwards-compatibility issues when
* the Webfonts API is introduced in Core.
* b. To make `fontFace` declarations in `theme.json` work.
*
* @link https://github.com/WordPress/gutenberg/issues/40472
*
* @since 6.0.0
* @deprecated 6.4.0 Use wp_print_font_faces() instead.
* @access private
*/
function _wp_theme_json_webfonts_handler()
{
_deprecated_function(__FUNCTION__, '6.4.0', 'wp_print_font_faces');
// Block themes are unavailable during installation.
if (wp_installing()) {
return;
}
if (!wp_theme_has_theme_json()) {
return;
}
// Webfonts to be processed.
$registered_webfonts = array();
/**
* Gets the webfonts from theme.json.
*
* @since 6.0.0
*
* @return array Array of defined webfonts.
*/
$fn_get_webfonts_from_theme_json = static function () {
// Get settings from theme.json.
$settings = WP_Theme_JSON_Resolver::get_merged_data()->get_settings();
// If in the editor, add webfonts defined in variations.
if (is_admin() || wp_is_rest_endpoint()) {
$variations = WP_Theme_JSON_Resolver::get_style_variations();
foreach ($variations as $variation) {
// Skip if fontFamilies are not defined in the variation.
if (empty($variation['settings']['typography']['fontFamilies'])) {
continue;
}
// Initialize the array structure.
if (empty($settings['typography'])) {
$settings['typography'] = array();
}
if (empty($settings['typography']['fontFamilies'])) {
$settings['typography']['fontFamilies'] = array();
}
if (empty($settings['typography']['fontFamilies']['theme'])) {
$settings['typography']['fontFamilies']['theme'] = array();
}
// Combine variations with settings. Remove duplicates.
$settings['typography']['fontFamilies']['theme'] = array_merge($settings['typography']['fontFamilies']['theme'], $variation['settings']['typography']['fontFamilies']['theme']);
$settings['typography']['fontFamilies'] = array_unique($settings['typography']['fontFamilies']);
}
}
// Bail out early if there are no settings for webfonts.
if (empty($settings['typography']['fontFamilies'])) {
return array();
}
$webfonts = array();
// Look for fontFamilies.
foreach ($settings['typography']['fontFamilies'] as $font_families) {
foreach ($font_families as $font_family) {
// Skip if fontFace is not defined.
if (empty($font_family['fontFace'])) {
continue;
}
// Skip if fontFace is not an array of webfonts.
if (!is_array($font_family['fontFace'])) {
continue;
}
$webfonts = array_merge($webfonts, $font_family['fontFace']);
}
}
return $webfonts;
};
/**
* Transforms each 'src' into an URI by replacing 'file:./'
* placeholder from theme.json.
*
* The absolute path to the webfont file(s) cannot be defined in
* theme.json. `file:./` is the placeholder which is replaced by
* the theme's URL path to the theme's root.
*
* @since 6.0.0
*
* @param array $src Webfont file(s) `src`.
* @return array Webfont's `src` in URI.
*/
$fn_transform_src_into_uri = static function (array $src) {
foreach ($src as $key => $url) {
// Tweak the URL to be relative to the theme root.
if (!str_starts_with($url, 'file:./')) {
continue;
}
$src[$key] = get_theme_file_uri(str_replace('file:./', '', $url));
}
return $src;
};
/**
* Converts the font-face properties (i.e. keys) into kebab-case.
*
* @since 6.0.0
*
* @param array $font_face Font face to convert.
* @return array Font faces with each property in kebab-case format.
*/
$fn_convert_keys_to_kebab_case = static function (array $font_face) {
foreach ($font_face as $property => $value) {
$kebab_case = _wp_to_kebab_case($property);
$font_face[$kebab_case] = $value;
if ($kebab_case !== $property) {
unset($font_face[$property]);
}
}
return $font_face;
};
/**
* Validates a webfont.
*
* @since 6.0.0
*
* @param array $webfont The webfont arguments.
* @return array|false The validated webfont arguments, or false if the webfont is invalid.
*/
$fn_validate_webfont = static function ($webfont) {
$webfont = wp_parse_args($webfont, array('font-family' => '', 'font-style' => 'normal', 'font-weight' => '400', 'font-display' => 'fallback', 'src' => array()));
// Check the font-family.
if (empty($webfont['font-family']) || !is_string($webfont['font-family'])) {
trigger_error(__('Webfont font family must be a non-empty string.'));
return false;
}
// Check that the `src` property is defined and a valid type.
if (empty($webfont['src']) || !is_string($webfont['src']) && !is_array($webfont['src'])) {
trigger_error(__('Webfont src must be a non-empty string or an array of strings.'));
return false;
}
// Validate the `src` property.
foreach ((array) $webfont['src'] as $src) {
if (!is_string($src) || '' === trim($src)) {
trigger_error(__('Each webfont src must be a non-empty string.'));
return false;
}
}
// Check the font-weight.
if (!is_string($webfont['font-weight']) && !is_int($webfont['font-weight'])) {
trigger_error(__('Webfont font weight must be a properly formatted string or integer.'));
return false;
}
// Check the font-display.
if (!in_array($webfont['font-display'], array('auto', 'block', 'fallback', 'optional', 'swap'), true)) {
$webfont['font-display'] = 'fallback';
}
$valid_props = array('ascend-override', 'descend-override', 'font-display', 'font-family', 'font-stretch', 'font-style', 'font-weight', 'font-variant', 'font-feature-settings', 'font-variation-settings', 'line-gap-override', 'size-adjust', 'src', 'unicode-range');
foreach ($webfont as $prop => $value) {
if (!in_array($prop, $valid_props, true)) {
unset($webfont[$prop]);
}
}
return $webfont;
};
/**
* Registers webfonts declared in theme.json.
*
* @since 6.0.0
*
* @uses $registered_webfonts To access and update the registered webfonts registry (passed by reference).
* @uses $fn_get_webfonts_from_theme_json To run the function that gets the webfonts from theme.json.
* @uses $fn_convert_keys_to_kebab_case To run the function that converts keys into kebab-case.
* @uses $fn_validate_webfont To run the function that validates each font-face (webfont) from theme.json.
*/
$fn_register_webfonts = static function () use (&$registered_webfonts, $fn_get_webfonts_from_theme_json, $fn_convert_keys_to_kebab_case, $fn_validate_webfont, $fn_transform_src_into_uri) {
$registered_webfonts = array();
foreach ($fn_get_webfonts_from_theme_json() as $webfont) {
if (!is_array($webfont)) {
continue;
}
$webfont = $fn_convert_keys_to_kebab_case($webfont);
$webfont = $fn_validate_webfont($webfont);
$webfont['src'] = $fn_transform_src_into_uri((array) $webfont['src']);
// Skip if not valid.
if (empty($webfont)) {
continue;
}
$registered_webfonts[] = $webfont;
}
};
/**
* Orders 'src' items to optimize for browser support.
*
* @since 6.0.0
*
* @param array $webfont Webfont to process.
* @return array Ordered `src` items.
*/
$fn_order_src = static function (array $webfont) {
$src = array();
$src_ordered = array();
foreach ($webfont['src'] as $url) {
// Add data URIs first.
if (str_starts_with(trim($url), 'data:')) {
$src_ordered[] = array('url' => $url, 'format' => 'data');
continue;
}
$format = pathinfo($url, PATHINFO_EXTENSION);
$src[$format] = $url;
}
// Add woff2.
if (!empty($src['woff2'])) {
$src_ordered[] = array('url' => sanitize_url($src['woff2']), 'format' => 'woff2');
}
// Add woff.
if (!empty($src['woff'])) {
$src_ordered[] = array('url' => sanitize_url($src['woff']), 'format' => 'woff');
}
// Add ttf.
if (!empty($src['ttf'])) {
$src_ordered[] = array('url' => sanitize_url($src['ttf']), 'format' => 'truetype');
}
// Add eot.
if (!empty($src['eot'])) {
$src_ordered[] = array('url' => sanitize_url($src['eot']), 'format' => 'embedded-opentype');
}
// Add otf.
if (!empty($src['otf'])) {
$src_ordered[] = array('url' => sanitize_url($src['otf']), 'format' => 'opentype');
}
$webfont['src'] = $src_ordered;
return $webfont;
};
/**
* Compiles the 'src' into valid CSS.
*
* @since 6.0.0
* @since 6.2.0 Removed local() CSS.
*
* @param string $font_family Font family.
* @param array $value Value to process.
* @return string The CSS.
*/
$fn_compile_src = static function ($font_family, array $value) {
$src = '';
foreach ($value as $item) {
$src .= ('data' === $item['format']) ? ", url({$item['url']})" : ", url('{$item['url']}') format('{$item['format']}')";
}
$src = ltrim($src, ', ');
return $src;
};
/**
* Compiles the font variation settings.
*
* @since 6.0.0
*
* @param array $font_variation_settings Array of font variation settings.
* @return string The CSS.
*/
$fn_compile_variations = static function (array $font_variation_settings) {
$variations = '';
foreach ($font_variation_settings as $key => $value) {
$variations .= "{$key} {$value}";
}
return $variations;
};
/**
* Builds the font-family's CSS.
*
* @since 6.0.0
*
* @uses $fn_compile_src To run the function that compiles the src.
* @uses $fn_compile_variations To run the function that compiles the variations.
*
* @param array $webfont Webfont to process.
* @return string This font-family's CSS.
*/
$fn_build_font_face_css = static function (array $webfont) use ($fn_compile_src, $fn_compile_variations) {
$css = '';
// Wrap font-family in quotes if it contains spaces.
if (str_contains($webfont['font-family'], ' ') && !str_contains($webfont['font-family'], '"') && !str_contains($webfont['font-family'], "'")) {
$webfont['font-family'] = '"' . $webfont['font-family'] . '"';
}
foreach ($webfont as $key => $value) {
/*
* Skip "provider", since it's for internal API use,
* and not a valid CSS property.
*/
if ('provider' === $key) {
continue;
}
// Compile the "src" parameter.
if ('src' === $key) {
$value = $fn_compile_src($webfont['font-family'], $value);
}
// If font-variation-settings is an array, convert it to a string.
if ('font-variation-settings' === $key && is_array($value)) {
$value = $fn_compile_variations($value);
}
if (!empty($value)) {
$css .= "{$key}:{$value};";
}
}
return $css;
};
/**
* Gets the '@font-face' CSS styles for locally-hosted font files.
*
* @since 6.0.0
*
* @uses $registered_webfonts To access and update the registered webfonts registry (passed by reference).
* @uses $fn_order_src To run the function that orders the src.
* @uses $fn_build_font_face_css To run the function that builds the font-face CSS.
*
* @return string The `@font-face` CSS.
*/
$fn_get_css = static function () use (&$registered_webfonts, $fn_order_src, $fn_build_font_face_css) {
$css = '';
foreach ($registered_webfonts as $webfont) {
// Order the webfont's `src` items to optimize for browser support.
$webfont = $fn_order_src($webfont);
// Build the @font-face CSS for this webfont.
$css .= '@font-face{' . $fn_build_font_face_css($webfont) . '}';
}
return $css;
};
/**
* Generates and enqueues webfonts styles.
*
* @since 6.0.0
*
* @uses $fn_get_css To run the function that gets the CSS.
*/
$fn_generate_and_enqueue_styles = static function () use ($fn_get_css) {
// Generate the styles.
$styles = $fn_get_css();
// Bail out if there are no styles to enqueue.
if ('' === $styles) {
return;
}
// Enqueue the stylesheet.
wp_register_style('wp-webfonts', '');
wp_enqueue_style('wp-webfonts');
// Add the styles to the stylesheet.
wp_add_inline_style('wp-webfonts', $styles);
};
/**
* Generates and enqueues editor styles.
*
* @since 6.0.0
*
* @uses $fn_get_css To run the function that gets the CSS.
*/
$fn_generate_and_enqueue_editor_styles = static function () use ($fn_get_css) {
// Generate the styles.
$styles = $fn_get_css();
// Bail out if there are no styles to enqueue.
if ('' === $styles) {
return;
}
wp_add_inline_style('wp-block-library', $styles);
};
add_action('wp_loaded', $fn_register_webfonts);
add_action('wp_enqueue_scripts', $fn_generate_and_enqueue_styles);
add_action('admin_init', $fn_generate_and_enqueue_editor_styles);
}