Make WordPress Core

Changeset 53282


Ignore:
Timestamp:
04/26/2022 02:46:37 PM (4 years ago)
Author:
hellofromTonya
Message:

Themes: Add internal-only theme.json's webfonts handler (stopgap).

Adds _wp_theme_json_webfonts_handler() for handling fontFace declarations in a theme's theme.json file to generate the @font-face styles for both the editor and front-end.

Design notes:

  • It is not a public API, but rather an internal, Core-only handler.
  • It is a stopgap implementation that will be replaced when the public Webfonts API is introduced in Core.
  • The code design is intentional, albeit funky, with the purpose of avoiding backwards-compatibility issues when the public Webfonts API is introduced in Core.
    • It hides the inter-workings.
    • Does not exposing API ins and outs for external consumption.
    • Only works for theme.json.
    • Does not provide registration or enqueuing access for plugins.

For more context on the decision to include this stopgap and the Webfonts API, see:

Props aristath, hellofromTonya, peterwilsoncc, costdev, jffng, zieladam, gziolo, bph, jonoaldersonwp, desrosj.

See #55567, #46370.

Location:
trunk
Files:
16 added
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/default-filters.php

    r53266 r53282  
    352352add_action( 'after_switch_theme', '_wp_sidebars_changed' );
    353353add_action( 'wp_print_styles', 'print_emoji_styles' );
     354add_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' );
    354355
    355356if ( isset( $_GET['replytocom'] ) ) {
  • trunk/src/wp-includes/script-loader.php

    r53235 r53282  
    30283028        add_action( 'enqueue_block_assets', $callback );
    30293029}
     3030
     3031/**
     3032 * Runs the theme.json webfonts handler.
     3033 *
     3034 * Using `WP_Theme_JSON_Resolver`, it gets the fonts defined
     3035 * in the `theme.json` for the current selection and style
     3036 * variations, validates the font-face properties, generates
     3037 * the '@font-face' style declarations, and then enqueues the
     3038 * styles for both the editor and front-end.
     3039 *
     3040 * Design Notes:
     3041 * This is not a public API, but rather an internal handler.
     3042 * A future public Webfonts API will replace this stopgap code.
     3043 *
     3044 * This code design is intentional.
     3045 *    a. It hides the inner-workings.
     3046 *    b. It does not expose API ins or outs for consumption.
     3047 *    c. It only works with a theme's `theme.json`.
     3048 *
     3049 * Why?
     3050 *    a. To avoid backwards-compatibility issues when
     3051 *       the Webfonts API is introduced in Core.
     3052 *    b. To make `fontFace` declarations in `theme.json` work.
     3053 *
     3054 * @link  https://github.com/WordPress/gutenberg/issues/40472
     3055 *
     3056 * @since 6.0.0
     3057 * @access private
     3058 */
     3059function _wp_theme_json_webfonts_handler() {
     3060        // Webfonts to be processed.
     3061        $registered_webfonts = array();
     3062
     3063        /**
     3064         * Gets the webfonts from theme.json.
     3065         *
     3066         * @since 6.0.0
     3067         *
     3068         * @return array Array of defined webfonts.
     3069         */
     3070        $fn_get_webfonts_from_theme_json = static function() {
     3071                // Get settings from theme.json.
     3072                $settings = WP_Theme_JSON_Resolver::get_merged_data()->get_settings();
     3073
     3074                // If in the editor, add webfonts defined in variations.
     3075                if ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
     3076                        $variations = WP_Theme_JSON_Resolver::get_style_variations();
     3077                        foreach ( $variations as $variation ) {
     3078                                // Skip if fontFamilies are not defined in the variation.
     3079                                if ( empty( $variation['settings']['typography']['fontFamilies'] ) ) {
     3080                                        continue;
     3081                                }
     3082
     3083                                // Initialize the array structure.
     3084                                if ( empty( $settings['typography'] ) ) {
     3085                                        $settings['typography'] = array();
     3086                                }
     3087                                if ( empty( $settings['typography']['fontFamilies'] ) ) {
     3088                                        $settings['typography']['fontFamilies'] = array();
     3089                                }
     3090                                if ( empty( $settings['typography']['fontFamilies']['theme'] ) ) {
     3091                                        $settings['typography']['fontFamilies']['theme'] = array();
     3092                                }
     3093
     3094                                // Combine variations with settings. Remove duplicates.
     3095                                $settings['typography']['fontFamilies']['theme'] = array_merge( $settings['typography']['fontFamilies']['theme'], $variation['settings']['typography']['fontFamilies']['theme'] );
     3096                                $settings['typography']['fontFamilies']          = array_unique( $settings['typography']['fontFamilies'] );
     3097                        }
     3098                }
     3099
     3100                // Bail out early if there are no settings for webfonts.
     3101                if ( empty( $settings['typography']['fontFamilies'] ) ) {
     3102                        return array();
     3103                }
     3104
     3105                $webfonts = array();
     3106
     3107                // Look for fontFamilies.
     3108                foreach ( $settings['typography']['fontFamilies'] as $font_families ) {
     3109                        foreach ( $font_families as $font_family ) {
     3110
     3111                                // Skip if fontFace is not defined.
     3112                                if ( empty( $font_family['fontFace'] ) ) {
     3113                                        continue;
     3114                                }
     3115
     3116                                // Skip if fontFace is not an array of webfonts.
     3117                                if ( ! is_array( $font_family['fontFace'] ) ) {
     3118                                        continue;
     3119                                }
     3120
     3121                                $webfonts = array_merge( $webfonts, $font_family['fontFace'] );
     3122                        }
     3123                }
     3124
     3125                return $webfonts;
     3126        };
     3127
     3128        /**
     3129         * Transforms each 'src' into an URI by replacing 'file:./'
     3130         * placeholder from theme.json.
     3131         *
     3132         * The absolute path to the webfont file(s) cannot be defined in
     3133         * theme.json. `file:./` is the placeholder which is replaced by
     3134         * the theme's URL path to the theme's root.
     3135         *
     3136         * @since 6.0.0
     3137         *
     3138         * @param array $src Webfont file(s) `src`.
     3139         * @return array Webfont's `src` in URI.
     3140         */
     3141        $fn_transform_src_into_uri = static function( array $src ) {
     3142                foreach ( $src as $key => $url ) {
     3143                        // Tweak the URL to be relative to the theme root.
     3144                        if ( ! str_starts_with( $url, 'file:./' ) ) {
     3145                                continue;
     3146                        }
     3147
     3148                        $src[ $key ] = get_theme_file_uri( str_replace( 'file:./', '', $url ) );
     3149                }
     3150
     3151                return $src;
     3152        };
     3153
     3154        /**
     3155         * Converts the font-face properties (i.e. keys) into kebab-case.
     3156         *
     3157         * @since 6.0.0
     3158         *
     3159         * @param array $font_face Font face to convert.
     3160         * @return array Font faces with each property in kebab-case format.
     3161         */
     3162        $fn_convert_keys_to_kebab_case = static function( array $font_face ) {
     3163                foreach ( $font_face as $property => $value ) {
     3164                        $kebab_case               = _wp_to_kebab_case( $property );
     3165                        $font_face[ $kebab_case ] = $value;
     3166                        if ( $kebab_case !== $property ) {
     3167                                unset( $font_face[ $property ] );
     3168                        }
     3169                }
     3170
     3171                return $font_face;
     3172        };
     3173
     3174        /**
     3175         * Validates a webfont.
     3176         *
     3177         * @since 6.0.0
     3178         *
     3179         * @param array $webfont The webfont arguments.
     3180         * @return array|false The validated webfont arguments, or false if the webfont is invalid.
     3181         */
     3182        $fn_validate_webfont = static function( $webfont ) {
     3183                $webfont = wp_parse_args(
     3184                        $webfont,
     3185                        array(
     3186                                'font-family'  => '',
     3187                                'font-style'   => 'normal',
     3188                                'font-weight'  => '400',
     3189                                'font-display' => 'fallback',
     3190                                'src'          => array(),
     3191                        )
     3192                );
     3193
     3194                // Check the font-family.
     3195                if ( empty( $webfont['font-family'] ) || ! is_string( $webfont['font-family'] ) ) {
     3196                        trigger_error( __( 'Webfont font family must be a non-empty string.', 'gutenberg' ) );
     3197
     3198                        return false;
     3199                }
     3200
     3201                // Check that the `src` property is defined and a valid type.
     3202                if ( empty( $webfont['src'] ) || ( ! is_string( $webfont['src'] ) && ! is_array( $webfont['src'] ) ) ) {
     3203                        trigger_error( __( 'Webfont src must be a non-empty string or an array of strings.', 'gutenberg' ) );
     3204
     3205                        return false;
     3206                }
     3207
     3208                // Validate the `src` property.
     3209                foreach ( (array) $webfont['src'] as $src ) {
     3210                        if ( ! is_string( $src ) || '' === trim( $src ) ) {
     3211                                trigger_error( __( 'Each webfont src must be a non-empty string.', 'gutenberg' ) );
     3212
     3213                                return false;
     3214                        }
     3215                }
     3216
     3217                // Check the font-weight.
     3218                if ( ! is_string( $webfont['font-weight'] ) && ! is_int( $webfont['font-weight'] ) ) {
     3219                        trigger_error( __( 'Webfont font weight must be a properly formatted string or integer.', 'gutenberg' ) );
     3220
     3221                        return false;
     3222                }
     3223
     3224                // Check the font-display.
     3225                if ( ! in_array( $webfont['font-display'], array( 'auto', 'block', 'fallback', 'swap' ), true ) ) {
     3226                        $webfont['font-display'] = 'fallback';
     3227                }
     3228
     3229                $valid_props = array(
     3230                        'ascend-override',
     3231                        'descend-override',
     3232                        'font-display',
     3233                        'font-family',
     3234                        'font-stretch',
     3235                        'font-style',
     3236                        'font-weight',
     3237                        'font-variant',
     3238                        'font-feature-settings',
     3239                        'font-variation-settings',
     3240                        'line-gap-override',
     3241                        'size-adjust',
     3242                        'src',
     3243                        'unicode-range',
     3244                );
     3245
     3246                foreach ( $webfont as $prop => $value ) {
     3247                        if ( ! in_array( $prop, $valid_props, true ) ) {
     3248                                unset( $webfont[ $prop ] );
     3249                        }
     3250                }
     3251
     3252                return $webfont;
     3253        };
     3254
     3255        /**
     3256         * Registers webfonts declared in theme.json.
     3257         *
     3258         * @since 6.0.0
     3259         *
     3260         * @uses $registered_webfonts To access and update the registered webfonts registry (passed by reference).
     3261         * @uses $fn_get_webfonts_from_theme_json To run the function that gets the webfonts from theme.json.
     3262         * @uses $fn_convert_keys_to_kebab_case To run the function that converts keys into kebab-case.
     3263         * @uses $fn_validate_webfont To run the function that validates each font-face (webfont) from theme.json.
     3264         */
     3265        $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 ) {
     3266                $registered_webfonts = array();
     3267
     3268                foreach ( $fn_get_webfonts_from_theme_json() as $webfont ) {
     3269                        if ( ! is_array( $webfont ) ) {
     3270                                continue;
     3271                        }
     3272
     3273                        $webfont = $fn_convert_keys_to_kebab_case( $webfont );
     3274
     3275                        $webfont = $fn_validate_webfont( $webfont );
     3276
     3277                        $webfont['src'] = $fn_transform_src_into_uri( (array) $webfont['src'] );
     3278
     3279                        // Skip if not valid.
     3280                        if ( empty( $webfont ) ) {
     3281                                continue;
     3282                        }
     3283
     3284                        $registered_webfonts[] = $webfont;
     3285                }
     3286        };
     3287
     3288        /**
     3289         * Orders 'src' items to optimize for browser support.
     3290         *
     3291         * @since 6.0.0
     3292         *
     3293         * @param array $webfont Webfont to process.
     3294         * @return array Ordered `src` items.
     3295         */
     3296        $fn_order_src = static function( array $webfont ) {
     3297                $src         = array();
     3298                $src_ordered = array();
     3299
     3300                foreach ( $webfont['src'] as $url ) {
     3301                        // Add data URIs first.
     3302                        if ( str_starts_with( trim( $url ), 'data:' ) ) {
     3303                                $src_ordered[] = array(
     3304                                        'url'    => $url,
     3305                                        'format' => 'data',
     3306                                );
     3307                                continue;
     3308                        }
     3309                        $format         = pathinfo( $url, PATHINFO_EXTENSION );
     3310                        $src[ $format ] = $url;
     3311                }
     3312
     3313                // Add woff2.
     3314                if ( ! empty( $src['woff2'] ) ) {
     3315                        $src_ordered[] = array(
     3316                                'url'    => sanitize_url( $src['woff2'] ),
     3317                                'format' => 'woff2',
     3318                        );
     3319                }
     3320
     3321                // Add woff.
     3322                if ( ! empty( $src['woff'] ) ) {
     3323                        $src_ordered[] = array(
     3324                                'url'    => sanitize_url( $src['woff'] ),
     3325                                'format' => 'woff',
     3326                        );
     3327                }
     3328
     3329                // Add ttf.
     3330                if ( ! empty( $src['ttf'] ) ) {
     3331                        $src_ordered[] = array(
     3332                                'url'    => sanitize_url( $src['ttf'] ),
     3333                                'format' => 'truetype',
     3334                        );
     3335                }
     3336
     3337                // Add eot.
     3338                if ( ! empty( $src['eot'] ) ) {
     3339                        $src_ordered[] = array(
     3340                                'url'    => sanitize_url( $src['eot'] ),
     3341                                'format' => 'embedded-opentype',
     3342                        );
     3343                }
     3344
     3345                // Add otf.
     3346                if ( ! empty( $src['otf'] ) ) {
     3347                        $src_ordered[] = array(
     3348                                'url'    => sanitize_url( $src['otf'] ),
     3349                                'format' => 'opentype',
     3350                        );
     3351                }
     3352                $webfont['src'] = $src_ordered;
     3353
     3354                return $webfont;
     3355        };
     3356
     3357        /**
     3358         * Compiles the 'src' into valid CSS.
     3359         *
     3360         * @since 6.0.0
     3361         *
     3362         * @param string $font_family Font family.
     3363         * @param array  $value       Value to process.
     3364         * @return string The CSS.
     3365         */
     3366        $fn_compile_src = static function( $font_family, array $value ) {
     3367                $src = "local($font_family)";
     3368
     3369                foreach ( $value as $item ) {
     3370
     3371                        if (
     3372                                str_starts_with( $item['url'], site_url() ) ||
     3373                                str_starts_with( $item['url'], home_url() )
     3374                        ) {
     3375                                $item['url'] = wp_make_link_relative( $item['url'] );
     3376                        }
     3377
     3378                        $src .= ( 'data' === $item['format'] )
     3379                                ? ", url({$item['url']})"
     3380                                : ", url('{$item['url']}') format('{$item['format']}')";
     3381                }
     3382
     3383                return $src;
     3384        };
     3385
     3386        /**
     3387         * Compiles the font variation settings.
     3388         *
     3389         * @since 6.0.0
     3390         *
     3391         * @param array $font_variation_settings Array of font variation settings.
     3392         * @return string The CSS.
     3393         */
     3394        $fn_compile_variations = static function( array $font_variation_settings ) {
     3395                $variations = '';
     3396
     3397                foreach ( $font_variation_settings as $key => $value ) {
     3398                        $variations .= "$key $value";
     3399                }
     3400
     3401                return $variations;
     3402        };
     3403
     3404        /**
     3405         * Builds the font-family's CSS.
     3406         *
     3407         * @since 6.0.0
     3408         *
     3409         * @uses $fn_compile_src To run the function that compiles the src.
     3410         * @uses $fn_compile_variations To run the function that compiles the variations.
     3411         *
     3412         * @param array $webfont Webfont to process.
     3413         * @return string This font-family's CSS.
     3414         */
     3415        $fn_build_font_face_css = static function( array $webfont ) use ( $fn_compile_src, $fn_compile_variations ) {
     3416                $css = '';
     3417
     3418                // Wrap font-family in quotes if it contains spaces.
     3419                if (
     3420                        str_contains( $webfont['font-family'], ' ' ) &&
     3421                        ! str_contains( $webfont['font-family'], '"' ) &&
     3422                        ! str_contains( $webfont['font-family'], "'" )
     3423                ) {
     3424                        $webfont['font-family'] = '"' . $webfont['font-family'] . '"';
     3425                }
     3426
     3427                foreach ( $webfont as $key => $value ) {
     3428                        /*
     3429                         * Skip "provider", since it's for internal API use,
     3430                         * and not a valid CSS property.
     3431                         */
     3432                        if ( 'provider' === $key ) {
     3433                                continue;
     3434                        }
     3435
     3436                        // Compile the "src" parameter.
     3437                        if ( 'src' === $key ) {
     3438                                $value = $fn_compile_src( $webfont['font-family'], $value );
     3439                        }
     3440
     3441                        // If font-variation-settings is an array, convert it to a string.
     3442                        if ( 'font-variation-settings' === $key && is_array( $value ) ) {
     3443                                $value = $fn_compile_variations( $value );
     3444                        }
     3445
     3446                        if ( ! empty( $value ) ) {
     3447                                $css .= "$key:$value;";
     3448                        }
     3449                }
     3450
     3451                return $css;
     3452        };
     3453
     3454        /**
     3455         * Gets the '@font-face' CSS styles for locally-hosted font files.
     3456         *
     3457         * @since 6.0.0
     3458         *
     3459         * @uses $registered_webfonts To access and update the registered webfonts registry (passed by reference).
     3460         * @uses $fn_order_src To run the function that orders the src.
     3461         * @uses $fn_build_font_face_css To run the function that builds the font-face CSS.
     3462         *
     3463         * @return string The `@font-face` CSS.
     3464         */
     3465        $fn_get_css = static function() use ( &$registered_webfonts, $fn_order_src, $fn_build_font_face_css ) {
     3466                $css = '';
     3467
     3468                foreach ( $registered_webfonts as $webfont ) {
     3469                        // Order the webfont's `src` items to optimize for browser support.
     3470                        $webfont = $fn_order_src( $webfont );
     3471
     3472                        // Build the @font-face CSS for this webfont.
     3473                        $css .= '@font-face{' . $fn_build_font_face_css( $webfont ) . '}';
     3474                }
     3475
     3476                return $css;
     3477        };
     3478
     3479        /**
     3480         * Generates and enqueues webfonts styles.
     3481         *
     3482         * @since 6.0.0
     3483         *
     3484         * @uses $fn_get_css To run the function that gets the CSS.
     3485         */
     3486        $fn_generate_and_enqueue_styles = static function() use ( $fn_get_css ) {
     3487                // Generate the styles.
     3488                $styles = $fn_get_css();
     3489
     3490                // Bail out if there are no styles to enqueue.
     3491                if ( '' === $styles ) {
     3492                        return;
     3493                }
     3494
     3495                // Enqueue the stylesheet.
     3496                wp_register_style( 'wp-webfonts', '' );
     3497                wp_enqueue_style( 'wp-webfonts' );
     3498
     3499                // Add the styles to the stylesheet.
     3500                wp_add_inline_style( 'wp-webfonts', $styles );
     3501        };
     3502
     3503        /**
     3504         * Generates and enqueues editor styles.
     3505         *
     3506         * @since 6.0.0
     3507         *
     3508         * @uses $fn_get_css To run the function that gets the CSS.
     3509         */
     3510        $fn_generate_and_enqueue_editor_styles = static function() use ( $fn_get_css ) {
     3511                // Generate the styles.
     3512                $styles = $fn_get_css();
     3513
     3514                // Bail out if there are no styles to enqueue.
     3515                if ( '' === $styles ) {
     3516                        return;
     3517                }
     3518
     3519                wp_add_inline_style( 'wp-block-library', $styles );
     3520        };
     3521
     3522        add_action( 'wp_loaded', $fn_register_webfonts );
     3523        add_action( 'wp_enqueue_scripts', $fn_generate_and_enqueue_styles );
     3524        add_action( 'admin_init', $fn_generate_and_enqueue_editor_styles );
     3525}
  • trunk/tests/phpunit/tests/theme/themeDir.php

    r52399 r53282  
    166166                        'Block Theme [0.4.0]',
    167167                        'Block Theme [1.0.0] in subdirectory',
     168                        'Webfonts theme',
     169                        'Empty `fontFace` in theme.json - no webfonts defined',
    168170                );
    169171
Note: See TracChangeset for help on using the changeset viewer.