Make WordPress Core


Ignore:
Timestamp:
08/31/2023 09:47:40 PM (18 months ago)
Author:
hellofromTonya
Message:

Introduce font-face styles generator and printer.

Introducing Font Face, a server-side @font-face styles generator and printer.

tl;dr:

  • Introduces Font Face.
  • Deprecates _wp_theme_json_webfonts_handler().

Introduce Font Face

From an array of fonts (i.e. each font-family and its font variations to be processed), it:

  1. Validates each font-face declaration, i.e. the CSS property and value pairing. If validation fails, processing stops with no font-face styles printed.
  2. Generates the @font-face CSS for each font-family.
  3. Prints the CSS within a <style id="wp-fonts-local"> element.

The entry point into Font Face is through a new global function called wp_print_font_faces(), which is automatically called:

  • when the 'wp_head' hook runs (for the front-end).
  • when the 'admin_print_styles' hook runs (for the back-end).
  • when _wp_get_iframed_editor_assets() runs to inject the @font-face styles into the iframed editor.

Once called, it gets the fonts from Theme_JSON merged data layer, which includes theme defined fonts and user activated fonts (once the Font Library #59166 is introduced into Core).

For classic sites, themes and plugins can directly call wp_print_font_faces() and pass their fonts array to it for processing.

Deprecates _wp_theme_json_webfonts_handler().

As Font Face is a direct replacement, the stopgap code in _wp_theme_json_webfonts_handler() (introduced in 6.0.0 via [53282]) is deprecated and unused in Core.

Props note:
There's a long multiple year history baked into Font Face, which dates back to the early versions of a web font API (see #46370 and roadmap. The props list includes those who contributed from those early versions up to this commit.

References:

Follow-up to [53282].

Props aristath, jonoaldersonwp, hellofromTonya, andraganescu, annezazu, antonvlasenko, arena, askdesign, azaozz, bph, bradley2083, colorful-tones, costdev, davidbaumwald, desrosj, dingo_d, djcowan, domainsupport, dryanpress, elmastudio, flixos90, francina, garrett-eclipse, gigitux, grantmkin, grapplerulrich, gziolo, ironprogrammer, jb510, jeffpaul, jeremyyip, jffng, joostdevalk, jorgefilipecosta, juanmaguitar, mamaduka, matveb, mburridge, mitogh, ndiego, ntsekouras, oandregal, ocean90, oglekler, paaljoachim, pagelab, peterwilsoncc, poena, priethor, scruffian, SergeyBiryukov, shiloey, simison, skorasaurus, soean, westonruter, wildworks, zaguiini.
Fixes #59165.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/script-loader.php

    r56496 r56500  
    32253225
    32263226/**
    3227  * Runs the theme.json webfonts handler.
    3228  *
    3229  * Using `WP_Theme_JSON_Resolver`, it gets the fonts defined
    3230  * in the `theme.json` for the current selection and style
    3231  * variations, validates the font-face properties, generates
    3232  * the '@font-face' style declarations, and then enqueues the
    3233  * styles for both the editor and front-end.
    3234  *
    3235  * Design Notes:
    3236  * This is not a public API, but rather an internal handler.
    3237  * A future public Webfonts API will replace this stopgap code.
    3238  *
    3239  * This code design is intentional.
    3240  *    a. It hides the inner-workings.
    3241  *    b. It does not expose API ins or outs for consumption.
    3242  *    c. It only works with a theme's `theme.json`.
    3243  *
    3244  * Why?
    3245  *    a. To avoid backwards-compatibility issues when
    3246  *       the Webfonts API is introduced in Core.
    3247  *    b. To make `fontFace` declarations in `theme.json` work.
    3248  *
    3249  * @link  https://github.com/WordPress/gutenberg/issues/40472
    3250  *
    3251  * @since 6.0.0
    3252  * @access private
    3253  */
    3254 function _wp_theme_json_webfonts_handler() {
    3255     // Block themes are unavailable during installation.
    3256     if ( wp_installing() ) {
    3257         return;
    3258     }
    3259 
    3260     if ( ! wp_theme_has_theme_json() ) {
    3261         return;
    3262     }
    3263 
    3264     // Webfonts to be processed.
    3265     $registered_webfonts = array();
    3266 
    3267     /**
    3268      * Gets the webfonts from theme.json.
    3269      *
    3270      * @since 6.0.0
    3271      *
    3272      * @return array Array of defined webfonts.
    3273      */
    3274     $fn_get_webfonts_from_theme_json = static function() {
    3275         // Get settings from theme.json.
    3276         $settings = WP_Theme_JSON_Resolver::get_merged_data()->get_settings();
    3277 
    3278         // If in the editor, add webfonts defined in variations.
    3279         if ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
    3280             $variations = WP_Theme_JSON_Resolver::get_style_variations();
    3281             foreach ( $variations as $variation ) {
    3282                 // Skip if fontFamilies are not defined in the variation.
    3283                 if ( empty( $variation['settings']['typography']['fontFamilies'] ) ) {
    3284                     continue;
    3285                 }
    3286 
    3287                 // Initialize the array structure.
    3288                 if ( empty( $settings['typography'] ) ) {
    3289                     $settings['typography'] = array();
    3290                 }
    3291                 if ( empty( $settings['typography']['fontFamilies'] ) ) {
    3292                     $settings['typography']['fontFamilies'] = array();
    3293                 }
    3294                 if ( empty( $settings['typography']['fontFamilies']['theme'] ) ) {
    3295                     $settings['typography']['fontFamilies']['theme'] = array();
    3296                 }
    3297 
    3298                 // Combine variations with settings. Remove duplicates.
    3299                 $settings['typography']['fontFamilies']['theme'] = array_merge( $settings['typography']['fontFamilies']['theme'], $variation['settings']['typography']['fontFamilies']['theme'] );
    3300                 $settings['typography']['fontFamilies']          = array_unique( $settings['typography']['fontFamilies'] );
    3301             }
    3302         }
    3303 
    3304         // Bail out early if there are no settings for webfonts.
    3305         if ( empty( $settings['typography']['fontFamilies'] ) ) {
    3306             return array();
    3307         }
    3308 
    3309         $webfonts = array();
    3310 
    3311         // Look for fontFamilies.
    3312         foreach ( $settings['typography']['fontFamilies'] as $font_families ) {
    3313             foreach ( $font_families as $font_family ) {
    3314 
    3315                 // Skip if fontFace is not defined.
    3316                 if ( empty( $font_family['fontFace'] ) ) {
    3317                     continue;
    3318                 }
    3319 
    3320                 // Skip if fontFace is not an array of webfonts.
    3321                 if ( ! is_array( $font_family['fontFace'] ) ) {
    3322                     continue;
    3323                 }
    3324 
    3325                 $webfonts = array_merge( $webfonts, $font_family['fontFace'] );
    3326             }
    3327         }
    3328 
    3329         return $webfonts;
    3330     };
    3331 
    3332     /**
    3333      * Transforms each 'src' into an URI by replacing 'file:./'
    3334      * placeholder from theme.json.
    3335      *
    3336      * The absolute path to the webfont file(s) cannot be defined in
    3337      * theme.json. `file:./` is the placeholder which is replaced by
    3338      * the theme's URL path to the theme's root.
    3339      *
    3340      * @since 6.0.0
    3341      *
    3342      * @param array $src Webfont file(s) `src`.
    3343      * @return array Webfont's `src` in URI.
    3344      */
    3345     $fn_transform_src_into_uri = static function( array $src ) {
    3346         foreach ( $src as $key => $url ) {
    3347             // Tweak the URL to be relative to the theme root.
    3348             if ( ! str_starts_with( $url, 'file:./' ) ) {
    3349                 continue;
    3350             }
    3351 
    3352             $src[ $key ] = get_theme_file_uri( str_replace( 'file:./', '', $url ) );
    3353         }
    3354 
    3355         return $src;
    3356     };
    3357 
    3358     /**
    3359      * Converts the font-face properties (i.e. keys) into kebab-case.
    3360      *
    3361      * @since 6.0.0
    3362      *
    3363      * @param array $font_face Font face to convert.
    3364      * @return array Font faces with each property in kebab-case format.
    3365      */
    3366     $fn_convert_keys_to_kebab_case = static function( array $font_face ) {
    3367         foreach ( $font_face as $property => $value ) {
    3368             $kebab_case               = _wp_to_kebab_case( $property );
    3369             $font_face[ $kebab_case ] = $value;
    3370             if ( $kebab_case !== $property ) {
    3371                 unset( $font_face[ $property ] );
    3372             }
    3373         }
    3374 
    3375         return $font_face;
    3376     };
    3377 
    3378     /**
    3379      * Validates a webfont.
    3380      *
    3381      * @since 6.0.0
    3382      *
    3383      * @param array $webfont The webfont arguments.
    3384      * @return array|false The validated webfont arguments, or false if the webfont is invalid.
    3385      */
    3386     $fn_validate_webfont = static function( $webfont ) {
    3387         $webfont = wp_parse_args(
    3388             $webfont,
    3389             array(
    3390                 'font-family'  => '',
    3391                 'font-style'   => 'normal',
    3392                 'font-weight'  => '400',
    3393                 'font-display' => 'fallback',
    3394                 'src'          => array(),
    3395             )
    3396         );
    3397 
    3398         // Check the font-family.
    3399         if ( empty( $webfont['font-family'] ) || ! is_string( $webfont['font-family'] ) ) {
    3400             trigger_error( __( 'Webfont font family must be a non-empty string.' ) );
    3401 
    3402             return false;
    3403         }
    3404 
    3405         // Check that the `src` property is defined and a valid type.
    3406         if ( empty( $webfont['src'] ) || ( ! is_string( $webfont['src'] ) && ! is_array( $webfont['src'] ) ) ) {
    3407             trigger_error( __( 'Webfont src must be a non-empty string or an array of strings.' ) );
    3408 
    3409             return false;
    3410         }
    3411 
    3412         // Validate the `src` property.
    3413         foreach ( (array) $webfont['src'] as $src ) {
    3414             if ( ! is_string( $src ) || '' === trim( $src ) ) {
    3415                 trigger_error( __( 'Each webfont src must be a non-empty string.' ) );
    3416 
    3417                 return false;
    3418             }
    3419         }
    3420 
    3421         // Check the font-weight.
    3422         if ( ! is_string( $webfont['font-weight'] ) && ! is_int( $webfont['font-weight'] ) ) {
    3423             trigger_error( __( 'Webfont font weight must be a properly formatted string or integer.' ) );
    3424 
    3425             return false;
    3426         }
    3427 
    3428         // Check the font-display.
    3429         if ( ! in_array( $webfont['font-display'], array( 'auto', 'block', 'fallback', 'optional', 'swap' ), true ) ) {
    3430             $webfont['font-display'] = 'fallback';
    3431         }
    3432 
    3433         $valid_props = array(
    3434             'ascend-override',
    3435             'descend-override',
    3436             'font-display',
    3437             'font-family',
    3438             'font-stretch',
    3439             'font-style',
    3440             'font-weight',
    3441             'font-variant',
    3442             'font-feature-settings',
    3443             'font-variation-settings',
    3444             'line-gap-override',
    3445             'size-adjust',
    3446             'src',
    3447             'unicode-range',
    3448         );
    3449 
    3450         foreach ( $webfont as $prop => $value ) {
    3451             if ( ! in_array( $prop, $valid_props, true ) ) {
    3452                 unset( $webfont[ $prop ] );
    3453             }
    3454         }
    3455 
    3456         return $webfont;
    3457     };
    3458 
    3459     /**
    3460      * Registers webfonts declared in theme.json.
    3461      *
    3462      * @since 6.0.0
    3463      *
    3464      * @uses $registered_webfonts To access and update the registered webfonts registry (passed by reference).
    3465      * @uses $fn_get_webfonts_from_theme_json To run the function that gets the webfonts from theme.json.
    3466      * @uses $fn_convert_keys_to_kebab_case To run the function that converts keys into kebab-case.
    3467      * @uses $fn_validate_webfont To run the function that validates each font-face (webfont) from theme.json.
    3468      */
    3469     $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 ) {
    3470         $registered_webfonts = array();
    3471 
    3472         foreach ( $fn_get_webfonts_from_theme_json() as $webfont ) {
    3473             if ( ! is_array( $webfont ) ) {
    3474                 continue;
    3475             }
    3476 
    3477             $webfont = $fn_convert_keys_to_kebab_case( $webfont );
    3478 
    3479             $webfont = $fn_validate_webfont( $webfont );
    3480 
    3481             $webfont['src'] = $fn_transform_src_into_uri( (array) $webfont['src'] );
    3482 
    3483             // Skip if not valid.
    3484             if ( empty( $webfont ) ) {
    3485                 continue;
    3486             }
    3487 
    3488             $registered_webfonts[] = $webfont;
    3489         }
    3490     };
    3491 
    3492     /**
    3493      * Orders 'src' items to optimize for browser support.
    3494      *
    3495      * @since 6.0.0
    3496      *
    3497      * @param array $webfont Webfont to process.
    3498      * @return array Ordered `src` items.
    3499      */
    3500     $fn_order_src = static function( array $webfont ) {
    3501         $src         = array();
    3502         $src_ordered = array();
    3503 
    3504         foreach ( $webfont['src'] as $url ) {
    3505             // Add data URIs first.
    3506             if ( str_starts_with( trim( $url ), 'data:' ) ) {
    3507                 $src_ordered[] = array(
    3508                     'url'    => $url,
    3509                     'format' => 'data',
    3510                 );
    3511                 continue;
    3512             }
    3513             $format         = pathinfo( $url, PATHINFO_EXTENSION );
    3514             $src[ $format ] = $url;
    3515         }
    3516 
    3517         // Add woff2.
    3518         if ( ! empty( $src['woff2'] ) ) {
    3519             $src_ordered[] = array(
    3520                 'url'    => sanitize_url( $src['woff2'] ),
    3521                 'format' => 'woff2',
    3522             );
    3523         }
    3524 
    3525         // Add woff.
    3526         if ( ! empty( $src['woff'] ) ) {
    3527             $src_ordered[] = array(
    3528                 'url'    => sanitize_url( $src['woff'] ),
    3529                 'format' => 'woff',
    3530             );
    3531         }
    3532 
    3533         // Add ttf.
    3534         if ( ! empty( $src['ttf'] ) ) {
    3535             $src_ordered[] = array(
    3536                 'url'    => sanitize_url( $src['ttf'] ),
    3537                 'format' => 'truetype',
    3538             );
    3539         }
    3540 
    3541         // Add eot.
    3542         if ( ! empty( $src['eot'] ) ) {
    3543             $src_ordered[] = array(
    3544                 'url'    => sanitize_url( $src['eot'] ),
    3545                 'format' => 'embedded-opentype',
    3546             );
    3547         }
    3548 
    3549         // Add otf.
    3550         if ( ! empty( $src['otf'] ) ) {
    3551             $src_ordered[] = array(
    3552                 'url'    => sanitize_url( $src['otf'] ),
    3553                 'format' => 'opentype',
    3554             );
    3555         }
    3556         $webfont['src'] = $src_ordered;
    3557 
    3558         return $webfont;
    3559     };
    3560 
    3561     /**
    3562      * Compiles the 'src' into valid CSS.
    3563      *
    3564      * @since 6.0.0
    3565      * @since 6.2.0 Removed local() CSS.
    3566      *
    3567      * @param string $font_family Font family.
    3568      * @param array  $value       Value to process.
    3569      * @return string The CSS.
    3570      */
    3571     $fn_compile_src = static function( $font_family, array $value ) {
    3572         $src = '';
    3573 
    3574         foreach ( $value as $item ) {
    3575             $src .= ( 'data' === $item['format'] )
    3576                 ? ", url({$item['url']})"
    3577                 : ", url('{$item['url']}') format('{$item['format']}')";
    3578         }
    3579 
    3580         $src = ltrim( $src, ', ' );
    3581 
    3582         return $src;
    3583     };
    3584 
    3585     /**
    3586      * Compiles the font variation settings.
    3587      *
    3588      * @since 6.0.0
    3589      *
    3590      * @param array $font_variation_settings Array of font variation settings.
    3591      * @return string The CSS.
    3592      */
    3593     $fn_compile_variations = static function( array $font_variation_settings ) {
    3594         $variations = '';
    3595 
    3596         foreach ( $font_variation_settings as $key => $value ) {
    3597             $variations .= "$key $value";
    3598         }
    3599 
    3600         return $variations;
    3601     };
    3602 
    3603     /**
    3604      * Builds the font-family's CSS.
    3605      *
    3606      * @since 6.0.0
    3607      *
    3608      * @uses $fn_compile_src To run the function that compiles the src.
    3609      * @uses $fn_compile_variations To run the function that compiles the variations.
    3610      *
    3611      * @param array $webfont Webfont to process.
    3612      * @return string This font-family's CSS.
    3613      */
    3614     $fn_build_font_face_css = static function( array $webfont ) use ( $fn_compile_src, $fn_compile_variations ) {
    3615         $css = '';
    3616 
    3617         // Wrap font-family in quotes if it contains spaces.
    3618         if (
    3619             str_contains( $webfont['font-family'], ' ' ) &&
    3620             ! str_contains( $webfont['font-family'], '"' ) &&
    3621             ! str_contains( $webfont['font-family'], "'" )
    3622         ) {
    3623             $webfont['font-family'] = '"' . $webfont['font-family'] . '"';
    3624         }
    3625 
    3626         foreach ( $webfont as $key => $value ) {
    3627             /*
    3628              * Skip "provider", since it's for internal API use,
    3629              * and not a valid CSS property.
    3630              */
    3631             if ( 'provider' === $key ) {
    3632                 continue;
    3633             }
    3634 
    3635             // Compile the "src" parameter.
    3636             if ( 'src' === $key ) {
    3637                 $value = $fn_compile_src( $webfont['font-family'], $value );
    3638             }
    3639 
    3640             // If font-variation-settings is an array, convert it to a string.
    3641             if ( 'font-variation-settings' === $key && is_array( $value ) ) {
    3642                 $value = $fn_compile_variations( $value );
    3643             }
    3644 
    3645             if ( ! empty( $value ) ) {
    3646                 $css .= "$key:$value;";
    3647             }
    3648         }
    3649 
    3650         return $css;
    3651     };
    3652 
    3653     /**
    3654      * Gets the '@font-face' CSS styles for locally-hosted font files.
    3655      *
    3656      * @since 6.0.0
    3657      *
    3658      * @uses $registered_webfonts To access and update the registered webfonts registry (passed by reference).
    3659      * @uses $fn_order_src To run the function that orders the src.
    3660      * @uses $fn_build_font_face_css To run the function that builds the font-face CSS.
    3661      *
    3662      * @return string The `@font-face` CSS.
    3663      */
    3664     $fn_get_css = static function() use ( &$registered_webfonts, $fn_order_src, $fn_build_font_face_css ) {
    3665         $css = '';
    3666 
    3667         foreach ( $registered_webfonts as $webfont ) {
    3668             // Order the webfont's `src` items to optimize for browser support.
    3669             $webfont = $fn_order_src( $webfont );
    3670 
    3671             // Build the @font-face CSS for this webfont.
    3672             $css .= '@font-face{' . $fn_build_font_face_css( $webfont ) . '}';
    3673         }
    3674 
    3675         return $css;
    3676     };
    3677 
    3678     /**
    3679      * Generates and enqueues webfonts styles.
    3680      *
    3681      * @since 6.0.0
    3682      *
    3683      * @uses $fn_get_css To run the function that gets the CSS.
    3684      */
    3685     $fn_generate_and_enqueue_styles = static function() use ( $fn_get_css ) {
    3686         // Generate the styles.
    3687         $styles = $fn_get_css();
    3688 
    3689         // Bail out if there are no styles to enqueue.
    3690         if ( '' === $styles ) {
    3691             return;
    3692         }
    3693 
    3694         // Enqueue the stylesheet.
    3695         wp_register_style( 'wp-webfonts', '' );
    3696         wp_enqueue_style( 'wp-webfonts' );
    3697 
    3698         // Add the styles to the stylesheet.
    3699         wp_add_inline_style( 'wp-webfonts', $styles );
    3700     };
    3701 
    3702     /**
    3703      * Generates and enqueues editor styles.
    3704      *
    3705      * @since 6.0.0
    3706      *
    3707      * @uses $fn_get_css To run the function that gets the CSS.
    3708      */
    3709     $fn_generate_and_enqueue_editor_styles = static function() use ( $fn_get_css ) {
    3710         // Generate the styles.
    3711         $styles = $fn_get_css();
    3712 
    3713         // Bail out if there are no styles to enqueue.
    3714         if ( '' === $styles ) {
    3715             return;
    3716         }
    3717 
    3718         wp_add_inline_style( 'wp-block-library', $styles );
    3719     };
    3720 
    3721     add_action( 'wp_loaded', $fn_register_webfonts );
    3722     add_action( 'wp_enqueue_scripts', $fn_generate_and_enqueue_styles );
    3723     add_action( 'admin_init', $fn_generate_and_enqueue_editor_styles );
    3724 }
    3725 
    3726 /**
    37273227 * Loads classic theme styles on classic themes in the frontend.
    37283228 *
Note: See TracChangeset for help on using the changeset viewer.