Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 83
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Google_Font_Face
0.00% covered (danger)
0.00%
0 / 83
0.00% covered (danger)
0.00%
0 / 12
1806
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 wp_loaded
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 current_screen
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 print_font_faces
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 collect_global_styles_fonts
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
 collect_block_fonts
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 add_font
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 format_font
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_font_slug_aliases
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
 get_font_family_name
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_font_slug_from_setting
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 is_block_editor
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * Jetpack_Google_Font_Face class
4 *
5 * @package automattic/jetpack
6 */
7
8/**
9 * Jetpack Google Font Face disables Font Face hooks in Core that prints **ALL** font faces.
10 * Instead, it collects fonts that are used in global styles or block-level settings and
11 * print those fonts in use.
12 *
13 * @phan-constructor-used-for-side-effects
14 */
15class Jetpack_Google_Font_Face {
16    /**
17     * The fonts that are used in global styles or block-level settings.
18     *
19     * @var array
20     */
21    private $fonts_in_use = array();
22
23    /**
24     * The constructor.
25     */
26    public function __construct() {
27        // Turns off hooks to print fonts
28        add_action( 'wp_loaded', array( $this, 'wp_loaded' ) );
29        add_action( 'current_screen', array( $this, 'current_screen' ), 10 );
30
31        // Collect and print fonts in use
32        if ( wp_is_block_theme() ) {
33            add_action( 'wp_head', array( $this, 'print_font_faces' ), 50 );
34        } else {
35            // In classic themes wp_head runs before the blocks are processed to collect the block fonts.
36            add_action( 'wp_footer', array( $this, 'print_font_faces' ), 50 );
37        }
38        add_filter( 'pre_render_block', array( $this, 'collect_block_fonts' ), 10, 2 );
39    }
40
41    /**
42     * Turn off hooks to print fonts on frontend
43     */
44    public function wp_loaded() {
45        remove_action( 'wp_head', 'wp_print_fonts', 50 );
46        remove_action( 'wp_head', 'wp_print_font_faces', 50 );
47        // Gutenberg 22.4+ overrides Core's font printing with its own function
48        // for classic theme support. Remove it so we can print only fonts in use.
49        remove_action( 'wp_head', 'gutenberg_print_font_faces', 50 );
50    }
51
52    /**
53     * Turn off hooks to print fonts on wp-admin, except for GB editor pages.
54     */
55    public function current_screen() {
56        remove_action( 'admin_print_styles', 'wp_print_fonts', 50 );
57
58        if ( ! $this->is_block_editor() ) {
59            remove_action( 'admin_print_styles', 'wp_print_font_faces', 50 );
60        }
61    }
62
63    /**
64     * Print fonts that are used in global styles or block-level settings.
65     */
66    public function print_font_faces() {
67        $fonts             = WP_Font_Face_Resolver::get_fonts_from_theme_json();
68        $font_slug_aliases = $this->get_font_slug_aliases();
69        $fonts_to_print    = array();
70
71        $this->collect_global_styles_fonts();
72        $fonts_in_use = array_values( array_unique( $this->fonts_in_use, SORT_STRING ) );
73        $fonts_in_use = array_map(
74            function ( $font_slug ) use ( $font_slug_aliases ) {
75                return $font_slug_aliases[ $font_slug ] ?? $font_slug;
76            },
77            $this->fonts_in_use
78        );
79
80        foreach ( $fonts as $font_faces ) {
81            $font_family = $font_faces[0]['font-family'] ?? '';
82            if ( in_array( $this->format_font( $font_family ), $fonts_in_use, true ) ) {
83                $fonts_to_print[] = $font_faces;
84            }
85        }
86
87        if ( ! empty( $fonts_to_print ) ) {
88            wp_print_font_faces( $fonts_to_print );
89        }
90    }
91
92    /**
93     * Collect fonts used for global styles settings.
94     */
95    public function collect_global_styles_fonts() {
96        $global_styles = wp_get_global_styles();
97
98        $global_styles_font_slug = $this->get_font_slug_from_setting( $global_styles );
99        if ( $global_styles_font_slug ) {
100            $this->add_font( $global_styles_font_slug );
101        }
102
103        if ( isset( $global_styles['blocks'] ) ) {
104            foreach ( $global_styles['blocks'] as $setting ) {
105                $font_slug = $this->get_font_slug_from_setting( $setting );
106
107                if ( $font_slug ) {
108                    $this->add_font( $font_slug );
109                }
110            }
111        }
112
113        if ( isset( $global_styles['elements'] ) ) {
114            foreach ( $global_styles['elements'] as $setting ) {
115                $font_slug = $this->get_font_slug_from_setting( $setting );
116
117                if ( $font_slug ) {
118                    $this->add_font( $font_slug );
119                }
120            }
121        }
122    }
123
124    /**
125     * Collect fonts used for block-level settings.
126     *
127     * @filter pre_render_block
128     *
129     * @param string|null $content The pre-rendered content. Default null.
130     * @param array       $parsed_block The block being rendered.
131     */
132    public function collect_block_fonts( $content, $parsed_block ) {
133        if ( ! is_admin() && isset( $parsed_block['attrs']['fontFamily'] ) ) {
134            $block_font_family = $parsed_block['attrs']['fontFamily'];
135            $this->add_font( $block_font_family );
136        }
137
138        return $content;
139    }
140
141    /**
142     * Add the specify font to the fonts_in_use list.
143     *
144     * @param string $font_slug The font slug.
145     */
146    public function add_font( $font_slug ) {
147        if ( is_string( $font_slug ) ) {
148            $this->fonts_in_use[] = $this->format_font( $font_slug );
149        }
150    }
151
152    /**
153     * Format the given font slug.
154     *
155     * @example "ABeeZee" formats to "abeezee"
156     * @example "ADLaM Display" formats to "adlam-display"
157     * @param string $font_slug The font slug.
158     * @return string The formatted font slug.
159     */
160    public function format_font( $font_slug ) {
161        return _wp_to_kebab_case( strtolower( $font_slug ) );
162    }
163
164    /**
165     * Get the font slug aliases that maps the font slug to the font family if they are different.
166     *
167     * The font definition may define an alias slug name, so we have to add the map from the slug name to the font family.
168     * See https://github.com/WordPress/twentytwentyfour/blob/df92472089ede6fae5924c124a93c843b84e8cbd/theme.json#L215.
169     */
170    public function get_font_slug_aliases() {
171        $font_slug_aliases = array();
172
173        $theme_json = WP_Theme_JSON_Resolver::get_theme_data();
174        $raw_data   = $theme_json->get_data();
175        if ( ! empty( $raw_data['settings']['typography']['fontFamilies'] ) ) {
176            foreach ( $raw_data['settings']['typography']['fontFamilies'] as $font ) {
177                if ( ! isset( $font['fontFamily'] ) ) {
178                    continue;
179                }
180                $font_family_name = $this->format_font( $this->get_font_family_name( $font ) );
181                $font_slug        = $font['slug'] ?? '';
182                if ( $font_slug && $font_slug !== $font_family_name && ! array_key_exists( $font_slug, $font_slug_aliases ) ) {
183                    $font_slug_aliases[ $font_slug ] = $font_family_name;
184                }
185            }
186        }
187
188        return $font_slug_aliases;
189    }
190
191    /**
192     * Get the font family name from a font.
193     *
194     * @param array $font The font definition object.
195     */
196    public static function get_font_family_name( $font ) {
197        $font_family = $font['fontFamily'];
198        if ( str_contains( $font_family, ',' ) ) {
199            $font_family = explode( ',', $font_family )[0];
200        }
201
202        return trim( $font_family, "\"'" );
203    }
204
205    /**
206     * Get the font family slug from a settings array.
207     *
208     * @param array $setting The settings object.
209     *
210     * @return string|null
211     */
212    public function get_font_slug_from_setting( $setting ) {
213        if ( ! isset( $setting['typography']['fontFamily'] ) ) {
214            return null;
215        }
216
217        $font_family = $setting['typography']['fontFamily'];
218
219        // The font family may be a reference to a path to the value stored at that location,
220        // e.g.: { "ref": "styles.elements.heading.typography.fontFamily" }.
221        // Ignore it as we also get the value stored at that location from the setting.
222        if ( ! is_string( $font_family ) ) {
223            return null;
224        }
225
226        // Full string: var(--wp--preset--font-family--slug).
227        // We do not care about the origin of the font, only its slug.
228        preg_match( '/font-family--(?P<slug>.+)\)$/', $font_family, $matches );
229
230        if ( isset( $matches['slug'] ) ) {
231            return $matches['slug'];
232        }
233
234        // Full string: var:preset|font-family|slug
235        // We do not care about the origin of the font, only its slug.
236        preg_match( '/font-family\|(?P<slug>.+)$/', $font_family, $matches );
237
238        if ( isset( $matches['slug'] ) ) {
239            return $matches['slug'];
240        }
241
242        return $font_family;
243    }
244
245    /**
246     * Check if the current screen is the block editor.
247     *
248     * @return bool
249     */
250    public function is_block_editor() {
251        if ( function_exists( 'get_current_screen' ) ) {
252            $current_screen = get_current_screen();
253            if ( ! empty( $current_screen ) && method_exists( $current_screen, 'is_block_editor' ) && $current_screen->is_block_editor() ) {
254                return true;
255            }
256        }
257
258        return false;
259    }
260}