|
1 <?php |
|
2 /** |
|
3 * WP_Font_Face class. |
|
4 * |
|
5 * @package WordPress |
|
6 * @subpackage Fonts |
|
7 * @since 6.4.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Font Face generates and prints `@font-face` styles for given fonts. |
|
12 * |
|
13 * @since 6.4.0 |
|
14 */ |
|
15 class WP_Font_Face { |
|
16 |
|
17 /** |
|
18 * The font-face property defaults. |
|
19 * |
|
20 * @since 6.4.0 |
|
21 * |
|
22 * @var string[] |
|
23 */ |
|
24 private $font_face_property_defaults = array( |
|
25 'font-family' => '', |
|
26 'font-style' => 'normal', |
|
27 'font-weight' => '400', |
|
28 'font-display' => 'fallback', |
|
29 ); |
|
30 |
|
31 /** |
|
32 * Valid font-face property names. |
|
33 * |
|
34 * @since 6.4.0 |
|
35 * |
|
36 * @var string[] |
|
37 */ |
|
38 private $valid_font_face_properties = array( |
|
39 'ascent-override', |
|
40 'descent-override', |
|
41 'font-display', |
|
42 'font-family', |
|
43 'font-stretch', |
|
44 'font-style', |
|
45 'font-weight', |
|
46 'font-variant', |
|
47 'font-feature-settings', |
|
48 'font-variation-settings', |
|
49 'line-gap-override', |
|
50 'size-adjust', |
|
51 'src', |
|
52 'unicode-range', |
|
53 ); |
|
54 |
|
55 /** |
|
56 * Valid font-display values. |
|
57 * |
|
58 * @since 6.4.0 |
|
59 * |
|
60 * @var string[] |
|
61 */ |
|
62 private $valid_font_display = array( 'auto', 'block', 'fallback', 'swap', 'optional' ); |
|
63 |
|
64 /** |
|
65 * Array of font-face style tag's attribute(s) |
|
66 * where the key is the attribute name and the |
|
67 * value is its value. |
|
68 * |
|
69 * @since 6.4.0 |
|
70 * |
|
71 * @var string[] |
|
72 */ |
|
73 private $style_tag_attrs = array(); |
|
74 |
|
75 /** |
|
76 * Creates and initializes an instance of WP_Font_Face. |
|
77 * |
|
78 * @since 6.4.0 |
|
79 */ |
|
80 public function __construct() { |
|
81 if ( |
|
82 function_exists( 'is_admin' ) && ! is_admin() |
|
83 && |
|
84 function_exists( 'current_theme_supports' ) && ! current_theme_supports( 'html5', 'style' ) |
|
85 ) { |
|
86 $this->style_tag_attrs = array( 'type' => 'text/css' ); |
|
87 } |
|
88 } |
|
89 |
|
90 /** |
|
91 * Generates and prints the `@font-face` styles for the given fonts. |
|
92 * |
|
93 * @since 6.4.0 |
|
94 * |
|
95 * @param array[][] $fonts Optional. The font-families and their font variations. |
|
96 * See {@see wp_print_font_faces()} for the supported fields. |
|
97 * Default empty array. |
|
98 */ |
|
99 public function generate_and_print( array $fonts ) { |
|
100 $fonts = $this->validate_fonts( $fonts ); |
|
101 |
|
102 // Bail out if there are no fonts are given to process. |
|
103 if ( empty( $fonts ) ) { |
|
104 return; |
|
105 } |
|
106 |
|
107 $css = $this->get_css( $fonts ); |
|
108 |
|
109 /* |
|
110 * The font-face CSS is contained within <style> tags and can only be interpreted |
|
111 * as CSS in the browser. Using wp_strip_all_tags() is sufficient escaping |
|
112 * to avoid malicious attempts to close </style> and open a <script>. |
|
113 */ |
|
114 $css = wp_strip_all_tags( $css ); |
|
115 |
|
116 // Bail out if there is no CSS to print. |
|
117 if ( empty( $css ) ) { |
|
118 return; |
|
119 } |
|
120 |
|
121 printf( $this->get_style_element(), $css ); |
|
122 } |
|
123 |
|
124 /** |
|
125 * Validates each of the font-face properties. |
|
126 * |
|
127 * @since 6.4.0 |
|
128 * |
|
129 * @param array $fonts The fonts to valid. |
|
130 * @return array Prepared font-faces organized by provider and font-family. |
|
131 */ |
|
132 private function validate_fonts( array $fonts ) { |
|
133 $validated_fonts = array(); |
|
134 |
|
135 foreach ( $fonts as $font_faces ) { |
|
136 foreach ( $font_faces as $font_face ) { |
|
137 $font_face = $this->validate_font_face_declarations( $font_face ); |
|
138 // Skip if failed validation. |
|
139 if ( false === $font_face ) { |
|
140 continue; |
|
141 } |
|
142 |
|
143 $validated_fonts[] = $font_face; |
|
144 } |
|
145 } |
|
146 |
|
147 return $validated_fonts; |
|
148 } |
|
149 |
|
150 /** |
|
151 * Validates each font-face declaration (property and value pairing). |
|
152 * |
|
153 * @since 6.4.0 |
|
154 * |
|
155 * @param array $font_face Font face property and value pairings to validate. |
|
156 * @return array|false Validated font-face on success, or false on failure. |
|
157 */ |
|
158 private function validate_font_face_declarations( array $font_face ) { |
|
159 $font_face = wp_parse_args( $font_face, $this->font_face_property_defaults ); |
|
160 |
|
161 // Check the font-family. |
|
162 if ( empty( $font_face['font-family'] ) || ! is_string( $font_face['font-family'] ) ) { |
|
163 // @todo replace with `wp_trigger_error()`. |
|
164 _doing_it_wrong( |
|
165 __METHOD__, |
|
166 __( 'Font font-family must be a non-empty string.' ), |
|
167 '6.4.0' |
|
168 ); |
|
169 return false; |
|
170 } |
|
171 |
|
172 // Make sure that local fonts have 'src' defined. |
|
173 if ( empty( $font_face['src'] ) || ( ! is_string( $font_face['src'] ) && ! is_array( $font_face['src'] ) ) ) { |
|
174 // @todo replace with `wp_trigger_error()`. |
|
175 _doing_it_wrong( |
|
176 __METHOD__, |
|
177 __( 'Font src must be a non-empty string or an array of strings.' ), |
|
178 '6.4.0' |
|
179 ); |
|
180 return false; |
|
181 } |
|
182 |
|
183 // Validate the 'src' property. |
|
184 foreach ( (array) $font_face['src'] as $src ) { |
|
185 if ( empty( $src ) || ! is_string( $src ) ) { |
|
186 // @todo replace with `wp_trigger_error()`. |
|
187 _doing_it_wrong( |
|
188 __METHOD__, |
|
189 __( 'Each font src must be a non-empty string.' ), |
|
190 '6.4.0' |
|
191 ); |
|
192 return false; |
|
193 } |
|
194 } |
|
195 |
|
196 // Check the font-weight. |
|
197 if ( ! is_string( $font_face['font-weight'] ) && ! is_int( $font_face['font-weight'] ) ) { |
|
198 // @todo replace with `wp_trigger_error()`. |
|
199 _doing_it_wrong( |
|
200 __METHOD__, |
|
201 __( 'Font font-weight must be a properly formatted string or integer.' ), |
|
202 '6.4.0' |
|
203 ); |
|
204 return false; |
|
205 } |
|
206 |
|
207 // Check the font-display. |
|
208 if ( ! in_array( $font_face['font-display'], $this->valid_font_display, true ) ) { |
|
209 $font_face['font-display'] = $this->font_face_property_defaults['font-display']; |
|
210 } |
|
211 |
|
212 // Remove invalid properties. |
|
213 foreach ( $font_face as $property => $value ) { |
|
214 if ( ! in_array( $property, $this->valid_font_face_properties, true ) ) { |
|
215 unset( $font_face[ $property ] ); |
|
216 } |
|
217 } |
|
218 |
|
219 return $font_face; |
|
220 } |
|
221 |
|
222 /** |
|
223 * Gets the style element for wrapping the `@font-face` CSS. |
|
224 * |
|
225 * @since 6.4.0 |
|
226 * |
|
227 * @return string The style element. |
|
228 */ |
|
229 private function get_style_element() { |
|
230 $attributes = $this->generate_style_element_attributes(); |
|
231 |
|
232 return "<style id='wp-fonts-local'{$attributes}>\n%s\n</style>\n"; |
|
233 } |
|
234 |
|
235 /** |
|
236 * Gets the defined <style> element's attributes. |
|
237 * |
|
238 * @since 6.4.0 |
|
239 * |
|
240 * @return string A string of attribute=value when defined, else, empty string. |
|
241 */ |
|
242 private function generate_style_element_attributes() { |
|
243 $attributes = ''; |
|
244 foreach ( $this->style_tag_attrs as $name => $value ) { |
|
245 $attributes .= " {$name}='{$value}'"; |
|
246 } |
|
247 return $attributes; |
|
248 } |
|
249 |
|
250 /** |
|
251 * Gets the `@font-face` CSS styles for locally-hosted font files. |
|
252 * |
|
253 * This method does the following processing tasks: |
|
254 * 1. Orchestrates an optimized `src` (with format) for browser support. |
|
255 * 2. Generates the `@font-face` for all its fonts. |
|
256 * |
|
257 * @since 6.4.0 |
|
258 * |
|
259 * @param array[] $font_faces The font-faces to generate @font-face CSS styles. |
|
260 * @return string The `@font-face` CSS styles. |
|
261 */ |
|
262 private function get_css( $font_faces ) { |
|
263 $css = ''; |
|
264 |
|
265 foreach ( $font_faces as $font_face ) { |
|
266 // Order the font's `src` items to optimize for browser support. |
|
267 $font_face = $this->order_src( $font_face ); |
|
268 |
|
269 // Build the @font-face CSS for this font. |
|
270 $css .= '@font-face{' . $this->build_font_face_css( $font_face ) . '}' . "\n"; |
|
271 } |
|
272 |
|
273 // Don't print the last newline character. |
|
274 return rtrim( $css, "\n" ); |
|
275 } |
|
276 |
|
277 /** |
|
278 * Orders `src` items to optimize for browser support. |
|
279 * |
|
280 * @since 6.4.0 |
|
281 * |
|
282 * @param array $font_face Font face to process. |
|
283 * @return array Font-face with ordered src items. |
|
284 */ |
|
285 private function order_src( array $font_face ) { |
|
286 if ( ! is_array( $font_face['src'] ) ) { |
|
287 $font_face['src'] = (array) $font_face['src']; |
|
288 } |
|
289 |
|
290 $src = array(); |
|
291 $src_ordered = array(); |
|
292 |
|
293 foreach ( $font_face['src'] as $url ) { |
|
294 // Add data URIs first. |
|
295 if ( str_starts_with( trim( $url ), 'data:' ) ) { |
|
296 $src_ordered[] = array( |
|
297 'url' => $url, |
|
298 'format' => 'data', |
|
299 ); |
|
300 continue; |
|
301 } |
|
302 $format = pathinfo( $url, PATHINFO_EXTENSION ); |
|
303 $src[ $format ] = $url; |
|
304 } |
|
305 |
|
306 // Add woff2. |
|
307 if ( ! empty( $src['woff2'] ) ) { |
|
308 $src_ordered[] = array( |
|
309 'url' => $src['woff2'], |
|
310 'format' => 'woff2', |
|
311 ); |
|
312 } |
|
313 |
|
314 // Add woff. |
|
315 if ( ! empty( $src['woff'] ) ) { |
|
316 $src_ordered[] = array( |
|
317 'url' => $src['woff'], |
|
318 'format' => 'woff', |
|
319 ); |
|
320 } |
|
321 |
|
322 // Add ttf. |
|
323 if ( ! empty( $src['ttf'] ) ) { |
|
324 $src_ordered[] = array( |
|
325 'url' => $src['ttf'], |
|
326 'format' => 'truetype', |
|
327 ); |
|
328 } |
|
329 |
|
330 // Add eot. |
|
331 if ( ! empty( $src['eot'] ) ) { |
|
332 $src_ordered[] = array( |
|
333 'url' => $src['eot'], |
|
334 'format' => 'embedded-opentype', |
|
335 ); |
|
336 } |
|
337 |
|
338 // Add otf. |
|
339 if ( ! empty( $src['otf'] ) ) { |
|
340 $src_ordered[] = array( |
|
341 'url' => $src['otf'], |
|
342 'format' => 'opentype', |
|
343 ); |
|
344 } |
|
345 $font_face['src'] = $src_ordered; |
|
346 |
|
347 return $font_face; |
|
348 } |
|
349 |
|
350 /** |
|
351 * Builds the font-family's CSS. |
|
352 * |
|
353 * @since 6.4.0 |
|
354 * |
|
355 * @param array $font_face Font face to process. |
|
356 * @return string This font-family's CSS. |
|
357 */ |
|
358 private function build_font_face_css( array $font_face ) { |
|
359 $css = ''; |
|
360 |
|
361 /* |
|
362 * Wrap font-family in quotes if it contains spaces |
|
363 * and is not already wrapped in quotes. |
|
364 */ |
|
365 if ( |
|
366 str_contains( $font_face['font-family'], ' ' ) && |
|
367 ! str_contains( $font_face['font-family'], '"' ) && |
|
368 ! str_contains( $font_face['font-family'], "'" ) |
|
369 ) { |
|
370 $font_face['font-family'] = '"' . $font_face['font-family'] . '"'; |
|
371 } |
|
372 |
|
373 foreach ( $font_face as $key => $value ) { |
|
374 // Compile the "src" parameter. |
|
375 if ( 'src' === $key ) { |
|
376 $value = $this->compile_src( $value ); |
|
377 } |
|
378 |
|
379 // If font-variation-settings is an array, convert it to a string. |
|
380 if ( 'font-variation-settings' === $key && is_array( $value ) ) { |
|
381 $value = $this->compile_variations( $value ); |
|
382 } |
|
383 |
|
384 if ( ! empty( $value ) ) { |
|
385 $css .= "$key:$value;"; |
|
386 } |
|
387 } |
|
388 |
|
389 return $css; |
|
390 } |
|
391 |
|
392 /** |
|
393 * Compiles the `src` into valid CSS. |
|
394 * |
|
395 * @since 6.4.0 |
|
396 * |
|
397 * @param array $value Value to process. |
|
398 * @return string The CSS. |
|
399 */ |
|
400 private function compile_src( array $value ) { |
|
401 $src = ''; |
|
402 |
|
403 foreach ( $value as $item ) { |
|
404 $src .= ( 'data' === $item['format'] ) |
|
405 ? ", url({$item['url']})" |
|
406 : ", url('{$item['url']}') format('{$item['format']}')"; |
|
407 } |
|
408 |
|
409 $src = ltrim( $src, ', ' ); |
|
410 return $src; |
|
411 } |
|
412 |
|
413 /** |
|
414 * Compiles the font variation settings. |
|
415 * |
|
416 * @since 6.4.0 |
|
417 * |
|
418 * @param array $font_variation_settings Array of font variation settings. |
|
419 * @return string The CSS. |
|
420 */ |
|
421 private function compile_variations( array $font_variation_settings ) { |
|
422 $variations = ''; |
|
423 |
|
424 foreach ( $font_variation_settings as $key => $value ) { |
|
425 $variations .= "$key $value"; |
|
426 } |
|
427 |
|
428 return $variations; |
|
429 } |
|
430 } |