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