PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
jetpack / modules / google-fonts / current / class-jetpack-google-font-face.php

class-jetpack-google-font-face.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at modules/google-fonts/current/class-jetpack-google-font-face.php

338 lines 9.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Jetpack_Google_Font_Face class
4 *
5 * @package automattic/jetpack
6 */
7
8 /**
9 * Prints used legacy catalogue faces while native font printers handle theme and user fonts.
10 *
11 * @phan-constructor-used-for-side-effects
12 */
13 class Jetpack_Google_Font_Face {
14 /**
15 * The fonts that are used in global styles or block-level settings.
16 *
17 * @var array
18 */
19 private $fonts_in_use = array();
20
21 /**
22 * The constructor.
23 */
24 public function __construct() {
25 // Retire the legacy printer while keeping native font-face printing.
26 add_action( 'wp_loaded', array( $this, 'wp_loaded' ) );
27 add_action( 'current_screen', array( $this, 'current_screen' ), 10 );
28
29 // Collect and print fonts in use
30 if ( wp_is_block_theme() ) {
31 add_action( 'wp_head', array( $this, 'print_font_faces' ), 50 );
32 } else {
33 // In classic themes wp_head runs before the blocks are processed to collect the block fonts.
34 add_action( 'wp_footer', array( $this, 'print_font_faces' ), 50 );
35 }
36 add_filter( 'pre_render_block', array( $this, 'collect_block_fonts' ), 10, 2 );
37 }
38
39 /**
40 * Turn off the legacy webfonts printer on the frontend.
41 */
42 public function wp_loaded() {
43 remove_action( 'wp_head', 'wp_print_fonts', 50 );
44 }
45
46 /**
47 * Turn off hooks to print fonts on wp-admin, except for GB editor pages.
48 */
49 public function current_screen() {
50 remove_action( 'admin_print_styles', 'wp_print_fonts', 50 );
51
52 if ( ! $this->is_block_editor() ) {
53 remove_action( 'admin_print_styles', 'wp_print_font_faces', 50 );
54 }
55 }
56
57 /**
58 * Print fonts that are used in global styles or block-level settings.
59 */
60 public function print_font_faces() {
61 if ( jetpack_google_fonts_load_font_faces() ) {
62 return;
63 }
64
65 $this->collect_global_styles_fonts();
66 $fonts_in_use = array_fill_keys( $this->fonts_in_use, true );
67 if ( empty( $fonts_in_use ) ) {
68 return;
69 }
70
71 $settings = function_exists( 'gutenberg_get_global_settings' )
72 ? gutenberg_get_global_settings()
73 : wp_get_global_settings();
74 $native_slugs = array();
75 $native_faces = array();
76 foreach ( $settings['typography']['fontFamilies'] ?? array() as $origin => $families ) {
77 foreach ( $families as $family ) {
78 if ( 'default' === $origin && empty( $family['fontFace'] ) ) {
79 continue;
80 }
81 if ( empty( $family['fontFamily'] ) ) {
82 continue;
83 }
84 $key = $this->format_font( self::get_font_family_name( $family ) );
85 if ( ! empty( $family['slug'] ) ) {
86 $native_slugs[ $this->format_font( $family['slug'] ) ] = $key;
87 }
88 if ( ! empty( $family['fontFace'] ) ) {
89 foreach ( $family['fontFace'] as $face ) {
90 $native_faces[ $key ][] = $face;
91 }
92 }
93 }
94 }
95
96 // Presets without their own faces can still refer to a legacy catalogue family.
97 $fonts_in_use = array_fill_keys(
98 array_map(
99 static function ( $font ) use ( $native_slugs ) {
100 return $native_slugs[ $font ] ?? $font;
101 },
102 array_keys( $fonts_in_use )
103 ),
104 true
105 );
106
107 $data = jetpack_get_google_fonts_data();
108 if ( empty( $data['fontFamilies'] ) ) {
109 return;
110 }
111 $available_fonts = jetpack_get_available_google_fonts_map( $data );
112 $fonts_to_print = array();
113 foreach ( $data['fontFamilies'] as $family ) {
114 if ( empty( $available_fonts[ $family['name'] ] ) || empty( $family['fontFace'] ) ) {
115 continue;
116 }
117
118 $name = self::get_font_family_name( $family );
119 $key = $this->format_font( $name );
120 $slug = $this->format_font( $family['slug'] );
121 // A preset that reuses this slug for another family replaces it.
122 if ( isset( $native_slugs[ $slug ] ) && $native_slugs[ $slug ] !== $key ) {
123 continue;
124 }
125 if ( ! isset( $fonts_in_use[ $slug ] ) && ! isset( $fonts_in_use[ $key ] ) ) {
126 continue;
127 }
128
129 $catalogue_faces = $family['fontFace'];
130 if ( isset( $native_faces[ $key ] ) ) {
131 // Native files win; the catalogue only fills styles and weights they lack.
132 $catalogue_faces = array_filter(
133 $catalogue_faces,
134 static function ( $face ) use ( $native_faces, $key ) {
135 return ! self::is_face_provided( $face, $native_faces[ $key ] );
136 }
137 );
138 if ( empty( $catalogue_faces ) ) {
139 continue;
140 }
141 }
142
143 $faces = array();
144 foreach ( $catalogue_faces as $face ) {
145 $face['font-family'] = $name;
146 $converted = array();
147 foreach ( $face as $property => $value ) {
148 $converted[ _wp_to_kebab_case( $property ) ] = $value;
149 }
150 $faces[] = $converted;
151 }
152 $fonts_to_print[] = $faces;
153 }
154
155 if ( ! empty( $fonts_to_print ) ) {
156 wp_print_font_faces( $fonts_to_print );
157 }
158 }
159
160 /**
161 * Whether a native face already covers a catalogue face's style and weight.
162 *
163 * @param array $face Catalogue face.
164 * @param array[] $native_faces Native faces for the same family.
165 * @return bool
166 */
167 private static function is_face_provided( $face, $native_faces ) {
168 $style = $face['fontStyle'] ?? 'normal';
169 list( $min, $max ) = self::get_weight_range( $face['fontWeight'] ?? '400' );
170 foreach ( $native_faces as $native ) {
171 if ( ( $native['fontStyle'] ?? 'normal' ) !== $style ) {
172 continue;
173 }
174 list( $native_min, $native_max ) = self::get_weight_range( $native['fontWeight'] ?? '400' );
175 // Overlap, not containment: a later catalogue rule would win at a shared weight.
176 if ( $native_min <= $max && $min <= $native_max ) {
177 return true;
178 }
179 }
180 return false;
181 }
182
183 /**
184 * Parses a font-weight descriptor such as "400", "bold" or "100 900".
185 *
186 * @param string|int $weight Font weight.
187 * @return int[] Minimum and maximum weight.
188 */
189 private static function get_weight_range( $weight ) {
190 $weight = str_replace( array( 'normal', 'bold' ), array( '400', '700' ), (string) $weight );
191 $parts = preg_split( '/\s+/', trim( $weight ) );
192 return array( (int) $parts[0], (int) ( $parts[1] ?? $parts[0] ) );
193 }
194
195 /**
196 * Collect fonts used for global styles settings.
197 */
198 public function collect_global_styles_fonts() {
199 $global_styles = function_exists( 'gutenberg_get_global_styles' )
200 ? gutenberg_get_global_styles()
201 : wp_get_global_styles();
202
203 $global_styles_font_slug = $this->get_font_slug_from_setting( $global_styles );
204 if ( $global_styles_font_slug ) {
205 $this->add_font( $global_styles_font_slug );
206 }
207
208 if ( isset( $global_styles['blocks'] ) ) {
209 foreach ( $global_styles['blocks'] as $setting ) {
210 $font_slug = $this->get_font_slug_from_setting( $setting );
211
212 if ( $font_slug ) {
213 $this->add_font( $font_slug );
214 }
215 }
216 }
217
218 if ( isset( $global_styles['elements'] ) ) {
219 foreach ( $global_styles['elements'] as $setting ) {
220 $font_slug = $this->get_font_slug_from_setting( $setting );
221
222 if ( $font_slug ) {
223 $this->add_font( $font_slug );
224 }
225 }
226 }
227 }
228
229 /**
230 * Collect fonts used for block-level settings.
231 *
232 * @filter pre_render_block
233 *
234 * @param string|null $content The pre-rendered content. Default null.
235 * @param array $parsed_block The block being rendered.
236 */
237 public function collect_block_fonts( $content, $parsed_block ) {
238 if ( ! is_admin() && isset( $parsed_block['attrs']['fontFamily'] ) ) {
239 $block_font_family = $parsed_block['attrs']['fontFamily'];
240 $this->add_font( $block_font_family );
241 }
242
243 return $content;
244 }
245
246 /**
247 * Add the specify font to the fonts_in_use list.
248 *
249 * @param string $font_slug The font slug.
250 */
251 public function add_font( $font_slug ) {
252 if ( is_string( $font_slug ) ) {
253 $this->fonts_in_use[] = $this->format_font( $font_slug );
254 }
255 }
256
257 /**
258 * Format the given font slug.
259 *
260 * @example "ABeeZee" formats to "abeezee"
261 * @example "ADLaM Display" formats to "adlam-display"
262 * @param string $font_slug The font slug.
263 * @return string The formatted font slug.
264 */
265 public function format_font( $font_slug ) {
266 return _wp_to_kebab_case( strtolower( $font_slug ) );
267 }
268
269 /**
270 * Get the font family name from a font.
271 *
272 * @param array $font The font definition object.
273 */
274 public static function get_font_family_name( $font ) {
275 $font_family = $font['fontFamily'];
276 if ( str_contains( $font_family, ',' ) ) {
277 $font_family = explode( ',', $font_family )[0];
278 }
279
280 return trim( $font_family, "\"'" );
281 }
282
283 /**
284 * Get the font family slug from a settings array.
285 *
286 * @param array $setting The settings object.
287 *
288 * @return string|null
289 */
290 public function get_font_slug_from_setting( $setting ) {
291 if ( ! isset( $setting['typography']['fontFamily'] ) ) {
292 return null;
293 }
294
295 $font_family = $setting['typography']['fontFamily'];
296
297 // The font family may be a reference to a path to the value stored at that location,
298 // e.g.: { "ref": "styles.elements.heading.typography.fontFamily" }.
299 // Ignore it as we also get the value stored at that location from the setting.
300 if ( ! is_string( $font_family ) ) {
301 return null;
302 }
303
304 // A preset reference can include a var() fallback and a trailing font stack.
305 preg_match( '/^\s*var\(\s*--wp--preset--font-family--(?P<slug>[^\s,()]+)\s*[,)]/', $font_family, $matches );
306
307 if ( isset( $matches['slug'] ) ) {
308 return $matches['slug'];
309 }
310
311 // Full string: var:preset|font-family|slug
312 // We do not care about the origin of the font, only its slug.
313 preg_match( '/^\s*var:preset\|font-family\|(?P<slug>[^\s,]+)/', $font_family, $matches );
314
315 if ( isset( $matches['slug'] ) ) {
316 return $matches['slug'];
317 }
318
319 return self::get_font_family_name( $setting['typography'] );
320 }
321
322 /**
323 * Check if the current screen is the block editor.
324 *
325 * @return bool
326 */
327 public function is_block_editor() {
328 if ( function_exists( 'get_current_screen' ) ) {
329 $current_screen = get_current_screen();
330 if ( ! empty( $current_screen ) && method_exists( $current_screen, 'is_block_editor' ) && $current_screen->is_block_editor() ) {
331 return true;
332 }
333 }
334
335 return false;
336 }
337 }
338