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/deprecated.php

    r56249 r56500  
    53685368    return $colors;
    53695369}
     5370
     5371/**
     5372 * Runs the theme.json webfonts handler.
     5373 *
     5374 * Using `WP_Theme_JSON_Resolver`, it gets the fonts defined
     5375 * in the `theme.json` for the current selection and style
     5376 * variations, validates the font-face properties, generates
     5377 * the '@font-face' style declarations, and then enqueues the
     5378 * styles for both the editor and front-end.
     5379 *
     5380 * Design Notes:
     5381 * This is not a public API, but rather an internal handler.
     5382 * A future public Webfonts API will replace this stopgap code.
     5383 *
     5384 * This code design is intentional.
     5385 *    a. It hides the inner-workings.
     5386 *    b. It does not expose API ins or outs for consumption.
     5387 *    c. It only works with a theme's `theme.json`.
     5388 *
     5389 * Why?
     5390 *    a. To avoid backwards-compatibility issues when
     5391 *       the Webfonts API is introduced in Core.
     5392 *    b. To make `fontFace` declarations in `theme.json` work.
     5393 *
     5394 * @link  https://github.com/WordPress/gutenberg/issues/40472
     5395 *
     5396 * @since 6.0.0
     5397 * @deprecated 6.4.0 Use wp_print_font_faces() instead.
     5398 * @access private
     5399 */
     5400function _wp_theme_json_webfonts_handler() {
     5401    _deprecated_function( __FUNCTION__, '6.4.0', 'wp_print_font_faces' );
     5402
     5403    // Block themes are unavailable during installation.
     5404    if ( wp_installing() ) {
     5405        return;
     5406    }
     5407
     5408    if ( ! wp_theme_has_theme_json() ) {
     5409        return;
     5410    }
     5411
     5412    // Webfonts to be processed.
     5413    $registered_webfonts = array();
     5414
     5415    /**
     5416     * Gets the webfonts from theme.json.
     5417     *
     5418     * @since 6.0.0
     5419     *
     5420     * @return array Array of defined webfonts.
     5421     */
     5422    $fn_get_webfonts_from_theme_json = static function() {
     5423        // Get settings from theme.json.
     5424        $settings = WP_Theme_JSON_Resolver::get_merged_data()->get_settings();
     5425
     5426        // If in the editor, add webfonts defined in variations.
     5427        if ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
     5428            $variations = WP_Theme_JSON_Resolver::get_style_variations();
     5429            foreach ( $variations as $variation ) {
     5430                // Skip if fontFamilies are not defined in the variation.
     5431                if ( empty( $variation['settings']['typography']['fontFamilies'] ) ) {
     5432                    continue;
     5433                }
     5434
     5435                // Initialize the array structure.
     5436                if ( empty( $settings['typography'] ) ) {
     5437                    $settings['typography'] = array();
     5438                }
     5439                if ( empty( $settings['typography']['fontFamilies'] ) ) {
     5440                    $settings['typography']['fontFamilies'] = array();
     5441                }
     5442                if ( empty( $settings['typography']['fontFamilies']['theme'] ) ) {
     5443                    $settings['typography']['fontFamilies']['theme'] = array();
     5444                }
     5445
     5446                // Combine variations with settings. Remove duplicates.
     5447                $settings['typography']['fontFamilies']['theme'] = array_merge( $settings['typography']['fontFamilies']['theme'], $variation['settings']['typography']['fontFamilies']['theme'] );
     5448                $settings['typography']['fontFamilies']          = array_unique( $settings['typography']['fontFamilies'] );
     5449            }
     5450        }
     5451
     5452        // Bail out early if there are no settings for webfonts.
     5453        if ( empty( $settings['typography']['fontFamilies'] ) ) {
     5454            return array();
     5455        }
     5456
     5457        $webfonts = array();
     5458
     5459        // Look for fontFamilies.
     5460        foreach ( $settings['typography']['fontFamilies'] as $font_families ) {
     5461            foreach ( $font_families as $font_family ) {
     5462
     5463                // Skip if fontFace is not defined.
     5464                if ( empty( $font_family['fontFace'] ) ) {
     5465                    continue;
     5466                }
     5467
     5468                // Skip if fontFace is not an array of webfonts.
     5469                if ( ! is_array( $font_family['fontFace'] ) ) {
     5470                    continue;
     5471                }
     5472
     5473                $webfonts = array_merge( $webfonts, $font_family['fontFace'] );
     5474            }
     5475        }
     5476
     5477        return $webfonts;
     5478    };
     5479
     5480    /**
     5481     * Transforms each 'src' into an URI by replacing 'file:./'
     5482     * placeholder from theme.json.
     5483     *
     5484     * The absolute path to the webfont file(s) cannot be defined in
     5485     * theme.json. `file:./` is the placeholder which is replaced by
     5486     * the theme's URL path to the theme's root.
     5487     *
     5488     * @since 6.0.0
     5489     *
     5490     * @param array $src Webfont file(s) `src`.
     5491     * @return array Webfont's `src` in URI.
     5492     */
     5493    $fn_transform_src_into_uri = static function( array $src ) {
     5494        foreach ( $src as $key => $url ) {
     5495            // Tweak the URL to be relative to the theme root.
     5496            if ( ! str_starts_with( $url, 'file:./' ) ) {
     5497                continue;
     5498            }
     5499
     5500            $src[ $key ] = get_theme_file_uri( str_replace( 'file:./', '', $url ) );
     5501        }
     5502
     5503        return $src;
     5504    };
     5505
     5506    /**
     5507     * Converts the font-face properties (i.e. keys) into kebab-case.
     5508     *
     5509     * @since 6.0.0
     5510     *
     5511     * @param array $font_face Font face to convert.
     5512     * @return array Font faces with each property in kebab-case format.
     5513     */
     5514    $fn_convert_keys_to_kebab_case = static function( array $font_face ) {
     5515        foreach ( $font_face as $property => $value ) {
     5516            $kebab_case               = _wp_to_kebab_case( $property );
     5517            $font_face[ $kebab_case ] = $value;
     5518            if ( $kebab_case !== $property ) {
     5519                unset( $font_face[ $property ] );
     5520            }
     5521        }
     5522
     5523        return $font_face;
     5524    };
     5525
     5526    /**
     5527     * Validates a webfont.
     5528     *
     5529     * @since 6.0.0
     5530     *
     5531     * @param array $webfont The webfont arguments.
     5532     * @return array|false The validated webfont arguments, or false if the webfont is invalid.
     5533     */
     5534    $fn_validate_webfont = static function( $webfont ) {
     5535        $webfont = wp_parse_args(
     5536                $webfont,
     5537                array(
     5538                        'font-family'  => '',
     5539                        'font-style'   => 'normal',
     5540                        'font-weight'  => '400',
     5541                        'font-display' => 'fallback',
     5542                        'src'          => array(),
     5543                )
     5544        );
     5545
     5546        // Check the font-family.
     5547        if ( empty( $webfont['font-family'] ) || ! is_string( $webfont['font-family'] ) ) {
     5548            trigger_error( __( 'Webfont font family must be a non-empty string.' ) );
     5549
     5550            return false;
     5551        }
     5552
     5553        // Check that the `src` property is defined and a valid type.
     5554        if ( empty( $webfont['src'] ) || ( ! is_string( $webfont['src'] ) && ! is_array( $webfont['src'] ) ) ) {
     5555            trigger_error( __( 'Webfont src must be a non-empty string or an array of strings.' ) );
     5556
     5557            return false;
     5558        }
     5559
     5560        // Validate the `src` property.
     5561        foreach ( (array) $webfont['src'] as $src ) {
     5562            if ( ! is_string( $src ) || '' === trim( $src ) ) {
     5563                trigger_error( __( 'Each webfont src must be a non-empty string.' ) );
     5564
     5565                return false;
     5566            }
     5567        }
     5568
     5569        // Check the font-weight.
     5570        if ( ! is_string( $webfont['font-weight'] ) && ! is_int( $webfont['font-weight'] ) ) {
     5571            trigger_error( __( 'Webfont font weight must be a properly formatted string or integer.' ) );
     5572
     5573            return false;
     5574        }
     5575
     5576        // Check the font-display.
     5577        if ( ! in_array( $webfont['font-display'], array( 'auto', 'block', 'fallback', 'optional', 'swap' ), true ) ) {
     5578            $webfont['font-display'] = 'fallback';
     5579        }
     5580
     5581        $valid_props = array(
     5582                'ascend-override',
     5583                'descend-override',
     5584                'font-display',
     5585                'font-family',
     5586                'font-stretch',
     5587                'font-style',
     5588                'font-weight',
     5589                'font-variant',
     5590                'font-feature-settings',
     5591                'font-variation-settings',
     5592                'line-gap-override',
     5593                'size-adjust',
     5594                'src',
     5595                'unicode-range',
     5596        );
     5597
     5598        foreach ( $webfont as $prop => $value ) {
     5599            if ( ! in_array( $prop, $valid_props, true ) ) {
     5600                unset( $webfont[ $prop ] );
     5601            }
     5602        }
     5603
     5604        return $webfont;
     5605    };
     5606
     5607    /**
     5608     * Registers webfonts declared in theme.json.
     5609     *
     5610     * @since 6.0.0
     5611     *
     5612     * @uses $registered_webfonts To access and update the registered webfonts registry (passed by reference).
     5613     * @uses $fn_get_webfonts_from_theme_json To run the function that gets the webfonts from theme.json.
     5614     * @uses $fn_convert_keys_to_kebab_case To run the function that converts keys into kebab-case.
     5615     * @uses $fn_validate_webfont To run the function that validates each font-face (webfont) from theme.json.
     5616     */
     5617    $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 ) {
     5618        $registered_webfonts = array();
     5619
     5620        foreach ( $fn_get_webfonts_from_theme_json() as $webfont ) {
     5621            if ( ! is_array( $webfont ) ) {
     5622                continue;
     5623            }
     5624
     5625            $webfont = $fn_convert_keys_to_kebab_case( $webfont );
     5626
     5627            $webfont = $fn_validate_webfont( $webfont );
     5628
     5629            $webfont['src'] = $fn_transform_src_into_uri( (array) $webfont['src'] );
     5630
     5631            // Skip if not valid.
     5632            if ( empty( $webfont ) ) {
     5633                continue;
     5634            }
     5635
     5636            $registered_webfonts[] = $webfont;
     5637        }
     5638    };
     5639
     5640    /**
     5641     * Orders 'src' items to optimize for browser support.
     5642     *
     5643     * @since 6.0.0
     5644     *
     5645     * @param array $webfont Webfont to process.
     5646     * @return array Ordered `src` items.
     5647     */
     5648    $fn_order_src = static function( array $webfont ) {
     5649        $src         = array();
     5650        $src_ordered = array();
     5651
     5652        foreach ( $webfont['src'] as $url ) {
     5653            // Add data URIs first.
     5654            if ( str_starts_with( trim( $url ), 'data:' ) ) {
     5655                $src_ordered[] = array(
     5656                        'url'    => $url,
     5657                        'format' => 'data',
     5658                );
     5659                continue;
     5660            }
     5661            $format         = pathinfo( $url, PATHINFO_EXTENSION );
     5662            $src[ $format ] = $url;
     5663        }
     5664
     5665        // Add woff2.
     5666        if ( ! empty( $src['woff2'] ) ) {
     5667            $src_ordered[] = array(
     5668                    'url'    => sanitize_url( $src['woff2'] ),
     5669                    'format' => 'woff2',
     5670            );
     5671        }
     5672
     5673        // Add woff.
     5674        if ( ! empty( $src['woff'] ) ) {
     5675            $src_ordered[] = array(
     5676                    'url'    => sanitize_url( $src['woff'] ),
     5677                    'format' => 'woff',
     5678            );
     5679        }
     5680
     5681        // Add ttf.
     5682        if ( ! empty( $src['ttf'] ) ) {
     5683            $src_ordered[] = array(
     5684                    'url'    => sanitize_url( $src['ttf'] ),
     5685                    'format' => 'truetype',
     5686            );
     5687        }
     5688
     5689        // Add eot.
     5690        if ( ! empty( $src['eot'] ) ) {
     5691            $src_ordered[] = array(
     5692                    'url'    => sanitize_url( $src['eot'] ),
     5693                    'format' => 'embedded-opentype',
     5694            );
     5695        }
     5696
     5697        // Add otf.
     5698        if ( ! empty( $src['otf'] ) ) {
     5699            $src_ordered[] = array(
     5700                    'url'    => sanitize_url( $src['otf'] ),
     5701                    'format' => 'opentype',
     5702            );
     5703        }
     5704        $webfont['src'] = $src_ordered;
     5705
     5706        return $webfont;
     5707    };
     5708
     5709    /**
     5710     * Compiles the 'src' into valid CSS.
     5711     *
     5712     * @since 6.0.0
     5713     * @since 6.2.0 Removed local() CSS.
     5714     *
     5715     * @param string $font_family Font family.
     5716     * @param array  $value       Value to process.
     5717     * @return string The CSS.
     5718     */
     5719    $fn_compile_src = static function( $font_family, array $value ) {
     5720        $src = '';
     5721
     5722        foreach ( $value as $item ) {
     5723            $src .= ( 'data' === $item['format'] )
     5724                    ? ", url({$item['url']})"
     5725                    : ", url('{$item['url']}') format('{$item['format']}')";
     5726        }
     5727
     5728        $src = ltrim( $src, ', ' );
     5729
     5730        return $src;
     5731    };
     5732
     5733    /**
     5734     * Compiles the font variation settings.
     5735     *
     5736     * @since 6.0.0
     5737     *
     5738     * @param array $font_variation_settings Array of font variation settings.
     5739     * @return string The CSS.
     5740     */
     5741    $fn_compile_variations = static function( array $font_variation_settings ) {
     5742        $variations = '';
     5743
     5744        foreach ( $font_variation_settings as $key => $value ) {
     5745            $variations .= "$key $value";
     5746        }
     5747
     5748        return $variations;
     5749    };
     5750
     5751    /**
     5752     * Builds the font-family's CSS.
     5753     *
     5754     * @since 6.0.0
     5755     *
     5756     * @uses $fn_compile_src To run the function that compiles the src.
     5757     * @uses $fn_compile_variations To run the function that compiles the variations.
     5758     *
     5759     * @param array $webfont Webfont to process.
     5760     * @return string This font-family's CSS.
     5761     */
     5762    $fn_build_font_face_css = static function( array $webfont ) use ( $fn_compile_src, $fn_compile_variations ) {
     5763        $css = '';
     5764
     5765        // Wrap font-family in quotes if it contains spaces.
     5766        if (
     5767                str_contains( $webfont['font-family'], ' ' ) &&
     5768                ! str_contains( $webfont['font-family'], '"' ) &&
     5769                ! str_contains( $webfont['font-family'], "'" )
     5770        ) {
     5771            $webfont['font-family'] = '"' . $webfont['font-family'] . '"';
     5772        }
     5773
     5774        foreach ( $webfont as $key => $value ) {
     5775            /*
     5776             * Skip "provider", since it's for internal API use,
     5777             * and not a valid CSS property.
     5778             */
     5779            if ( 'provider' === $key ) {
     5780                continue;
     5781            }
     5782
     5783            // Compile the "src" parameter.
     5784            if ( 'src' === $key ) {
     5785                $value = $fn_compile_src( $webfont['font-family'], $value );
     5786            }
     5787
     5788            // If font-variation-settings is an array, convert it to a string.
     5789            if ( 'font-variation-settings' === $key && is_array( $value ) ) {
     5790                $value = $fn_compile_variations( $value );
     5791            }
     5792
     5793            if ( ! empty( $value ) ) {
     5794                $css .= "$key:$value;";
     5795            }
     5796        }
     5797
     5798        return $css;
     5799    };
     5800
     5801    /**
     5802     * Gets the '@font-face' CSS styles for locally-hosted font files.
     5803     *
     5804     * @since 6.0.0
     5805     *
     5806     * @uses $registered_webfonts To access and update the registered webfonts registry (passed by reference).
     5807     * @uses $fn_order_src To run the function that orders the src.
     5808     * @uses $fn_build_font_face_css To run the function that builds the font-face CSS.
     5809     *
     5810     * @return string The `@font-face` CSS.
     5811     */
     5812    $fn_get_css = static function() use ( &$registered_webfonts, $fn_order_src, $fn_build_font_face_css ) {
     5813        $css = '';
     5814
     5815        foreach ( $registered_webfonts as $webfont ) {
     5816            // Order the webfont's `src` items to optimize for browser support.
     5817            $webfont = $fn_order_src( $webfont );
     5818
     5819            // Build the @font-face CSS for this webfont.
     5820            $css .= '@font-face{' . $fn_build_font_face_css( $webfont ) . '}';
     5821        }
     5822
     5823        return $css;
     5824    };
     5825
     5826    /**
     5827     * Generates and enqueues webfonts styles.
     5828     *
     5829     * @since 6.0.0
     5830     *
     5831     * @uses $fn_get_css To run the function that gets the CSS.
     5832     */
     5833    $fn_generate_and_enqueue_styles = static function() use ( $fn_get_css ) {
     5834        // Generate the styles.
     5835        $styles = $fn_get_css();
     5836
     5837        // Bail out if there are no styles to enqueue.
     5838        if ( '' === $styles ) {
     5839            return;
     5840        }
     5841
     5842        // Enqueue the stylesheet.
     5843        wp_register_style( 'wp-webfonts', '' );
     5844        wp_enqueue_style( 'wp-webfonts' );
     5845
     5846        // Add the styles to the stylesheet.
     5847        wp_add_inline_style( 'wp-webfonts', $styles );
     5848    };
     5849
     5850    /**
     5851     * Generates and enqueues editor styles.
     5852     *
     5853     * @since 6.0.0
     5854     *
     5855     * @uses $fn_get_css To run the function that gets the CSS.
     5856     */
     5857    $fn_generate_and_enqueue_editor_styles = static function() use ( $fn_get_css ) {
     5858        // Generate the styles.
     5859        $styles = $fn_get_css();
     5860
     5861        // Bail out if there are no styles to enqueue.
     5862        if ( '' === $styles ) {
     5863            return;
     5864        }
     5865
     5866        wp_add_inline_style( 'wp-block-library', $styles );
     5867    };
     5868
     5869    add_action( 'wp_loaded', $fn_register_webfonts );
     5870    add_action( 'wp_enqueue_scripts', $fn_generate_and_enqueue_styles );
     5871    add_action( 'admin_init', $fn_generate_and_enqueue_editor_styles );
     5872}
Note: See TracChangeset for help on using the changeset viewer.