Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 167 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
Webfonts | |
0.00% |
0 / 167 |
|
0.00% |
0 / 11 |
4556 | |
0.00% |
0 / 1 |
get_webfonts_from_theme_json | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
210 | |||
transform_src_into_uri | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
convert_keys_to_kebab_case | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
validate_webfont | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
210 | |||
get_registered_webfonts_from_theme_json | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
order_src | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
72 | |||
compile_src | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
compile_variations | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
build_font_face_css | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
110 | |||
get_css_from_webfonts | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
get_wp_theme_json_webfonts_css | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | namespace NewfoldLabs\WP\Module\Onboarding\Services; |
3 | |
4 | class Webfonts { |
5 | public static function get_webfonts_from_theme_json() { |
6 | // Get settings from theme.json. |
7 | $settings = \WP_Theme_JSON_Resolver::get_merged_data()->get_settings(); |
8 | |
9 | // If in the editor, add webfonts defined in variations. |
10 | if ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { |
11 | $variations = \WP_Theme_JSON_Resolver::get_style_variations(); |
12 | foreach ( $variations as $variation ) { |
13 | // Skip if fontFamilies are not defined in the variation. |
14 | if ( empty( $variation['settings']['typography']['fontFamilies'] ) ) { |
15 | continue; |
16 | } |
17 | |
18 | // Initialize the array structure. |
19 | if ( empty( $settings['typography'] ) ) { |
20 | $settings['typography'] = array(); |
21 | } |
22 | if ( empty( $settings['typography']['fontFamilies'] ) ) { |
23 | $settings['typography']['fontFamilies'] = array(); |
24 | } |
25 | if ( empty( $settings['typography']['fontFamilies']['theme'] ) ) { |
26 | $settings['typography']['fontFamilies']['theme'] = array(); |
27 | } |
28 | |
29 | // Combine variations with settings. Remove duplicates. |
30 | $settings['typography']['fontFamilies']['theme'] = array_merge( $settings['typography']['fontFamilies']['theme'], $variation['settings']['typography']['fontFamilies']['theme'] ); |
31 | $settings['typography']['fontFamilies'] = array_unique( $settings['typography']['fontFamilies'] ); |
32 | } |
33 | } |
34 | |
35 | // Bail out early if there are no settings for webfonts. |
36 | if ( empty( $settings['typography']['fontFamilies'] ) ) { |
37 | return array(); |
38 | } |
39 | |
40 | $webfonts = array(); |
41 | |
42 | // Look for fontFamilies. |
43 | foreach ( $settings['typography']['fontFamilies'] as $font_families ) { |
44 | foreach ( $font_families as $font_family ) { |
45 | |
46 | // Skip if fontFace is not defined. |
47 | if ( empty( $font_family['fontFace'] ) ) { |
48 | continue; |
49 | } |
50 | |
51 | // Skip if fontFace is not an array of webfonts. |
52 | if ( ! is_array( $font_family['fontFace'] ) ) { |
53 | continue; |
54 | } |
55 | |
56 | $webfonts = array_merge( $webfonts, $font_family['fontFace'] ); |
57 | } |
58 | } |
59 | |
60 | return $webfonts; |
61 | } |
62 | |
63 | private static function transform_src_into_uri( array $src ) { |
64 | foreach ( $src as $key => $url ) { |
65 | // Tweak the URL to be relative to the theme root. |
66 | if ( ! str_starts_with( $url, 'file:./' ) ) { |
67 | continue; |
68 | } |
69 | |
70 | $src[ $key ] = get_theme_file_uri( str_replace( 'file:./', '', $url ) ); |
71 | } |
72 | |
73 | return $src; |
74 | } |
75 | |
76 | /** |
77 | * Converts the font-face properties (i.e. keys) into kebab-case. |
78 | * |
79 | * @since 6.0.0 |
80 | * |
81 | * @param array $font_face Font face to convert. |
82 | * @return array Font faces with each property in kebab-case format. |
83 | */ |
84 | private static function convert_keys_to_kebab_case( array $font_face ) { |
85 | foreach ( $font_face as $property => $value ) { |
86 | $kebab_case = _wp_to_kebab_case( $property ); |
87 | $font_face[ $kebab_case ] = $value; |
88 | if ( $kebab_case !== $property ) { |
89 | unset( $font_face[ $property ] ); |
90 | } |
91 | } |
92 | |
93 | return $font_face; |
94 | } |
95 | |
96 | /** |
97 | * Validates a webfont. |
98 | * |
99 | * @since 6.0.0 |
100 | * |
101 | * @param array $webfont The webfont arguments. |
102 | * @return array|false The validated webfont arguments, or false if the webfont is invalid. |
103 | */ |
104 | private static function validate_webfont( $webfont ) { |
105 | $webfont = wp_parse_args( |
106 | $webfont, |
107 | array( |
108 | 'font-family' => '', |
109 | 'font-style' => 'normal', |
110 | 'font-weight' => '400', |
111 | 'font-display' => 'fallback', |
112 | 'src' => array(), |
113 | ) |
114 | ); |
115 | |
116 | // Check the font-family. |
117 | if ( empty( $webfont['font-family'] ) || ! is_string( $webfont['font-family'] ) ) { |
118 | trigger_error( __( 'Webfont font family must be a non-empty string.', 'wp-module-onboarding' ) ); |
119 | |
120 | return false; |
121 | } |
122 | |
123 | // Check that the `src` property is defined and a valid type. |
124 | if ( empty( $webfont['src'] ) || ( ! is_string( $webfont['src'] ) && ! is_array( $webfont['src'] ) ) ) { |
125 | trigger_error( __( 'Webfont src must be a non-empty string or an array of strings.', 'wp-module-onboarding' ) ); |
126 | |
127 | return false; |
128 | } |
129 | |
130 | // Validate the `src` property. |
131 | foreach ( (array) $webfont['src'] as $src ) { |
132 | if ( ! is_string( $src ) || '' === trim( $src ) ) { |
133 | trigger_error( __( 'Each webfont src must be a non-empty string.', 'wp-module-onboarding' ) ); |
134 | |
135 | return false; |
136 | } |
137 | } |
138 | |
139 | // Check the font-weight. |
140 | if ( ! is_string( $webfont['font-weight'] ) && ! is_int( $webfont['font-weight'] ) ) { |
141 | trigger_error( __( 'Webfont font weight must be a properly formatted string or integer.', 'wp-module-onboarding' ) ); |
142 | |
143 | return false; |
144 | } |
145 | |
146 | // Check the font-display. |
147 | if ( ! in_array( $webfont['font-display'], array( 'auto', 'block', 'fallback', 'swap' ), true ) ) { |
148 | $webfont['font-display'] = 'fallback'; |
149 | } |
150 | |
151 | $valid_props = array( |
152 | 'ascend-override', |
153 | 'descend-override', |
154 | 'font-display', |
155 | 'font-family', |
156 | 'font-stretch', |
157 | 'font-style', |
158 | 'font-weight', |
159 | 'font-variant', |
160 | 'font-feature-settings', |
161 | 'font-variation-settings', |
162 | 'line-gap-override', |
163 | 'size-adjust', |
164 | 'src', |
165 | 'unicode-range', |
166 | ); |
167 | |
168 | foreach ( $webfont as $prop => $value ) { |
169 | if ( ! in_array( $prop, $valid_props, true ) ) { |
170 | unset( $webfont[ $prop ] ); |
171 | } |
172 | } |
173 | |
174 | return $webfont; |
175 | } |
176 | |
177 | public static function get_registered_webfonts_from_theme_json() { |
178 | $registered_webfonts = array(); |
179 | |
180 | foreach ( self::get_webfonts_from_theme_json() as $webfont ) { |
181 | if ( ! is_array( $webfont ) ) { |
182 | continue; |
183 | } |
184 | |
185 | $webfont = self::convert_keys_to_kebab_case( $webfont ); |
186 | |
187 | $webfont = self::validate_webfont( $webfont ); |
188 | |
189 | $webfont['src'] = self::transform_src_into_uri( (array) $webfont['src'] ); |
190 | |
191 | // Skip if not valid. |
192 | if ( empty( $webfont ) ) { |
193 | continue; |
194 | } |
195 | |
196 | $registered_webfonts[] = $webfont; |
197 | } |
198 | |
199 | return $registered_webfonts; |
200 | } |
201 | |
202 | private static function order_src( array $webfont ) { |
203 | $src = array(); |
204 | $src_ordered = array(); |
205 | |
206 | foreach ( $webfont['src'] as $url ) { |
207 | // Add data URIs first. |
208 | if ( str_starts_with( trim( $url ), 'data:' ) ) { |
209 | $src_ordered[] = array( |
210 | 'url' => $url, |
211 | 'format' => 'data', |
212 | ); |
213 | continue; |
214 | } |
215 | $format = pathinfo( $url, PATHINFO_EXTENSION ); |
216 | $src[ $format ] = $url; |
217 | } |
218 | |
219 | // Add woff2. |
220 | if ( ! empty( $src['woff2'] ) ) { |
221 | $src_ordered[] = array( |
222 | 'url' => sanitize_url( $src['woff2'] ), |
223 | 'format' => 'woff2', |
224 | ); |
225 | } |
226 | |
227 | // Add woff. |
228 | if ( ! empty( $src['woff'] ) ) { |
229 | $src_ordered[] = array( |
230 | 'url' => sanitize_url( $src['woff'] ), |
231 | 'format' => 'woff', |
232 | ); |
233 | } |
234 | |
235 | // Add ttf. |
236 | if ( ! empty( $src['ttf'] ) ) { |
237 | $src_ordered[] = array( |
238 | 'url' => sanitize_url( $src['ttf'] ), |
239 | 'format' => 'truetype', |
240 | ); |
241 | } |
242 | |
243 | // Add eot. |
244 | if ( ! empty( $src['eot'] ) ) { |
245 | $src_ordered[] = array( |
246 | 'url' => sanitize_url( $src['eot'] ), |
247 | 'format' => 'embedded-opentype', |
248 | ); |
249 | } |
250 | |
251 | // Add otf. |
252 | if ( ! empty( $src['otf'] ) ) { |
253 | $src_ordered[] = array( |
254 | 'url' => sanitize_url( $src['otf'] ), |
255 | 'format' => 'opentype', |
256 | ); |
257 | } |
258 | $webfont['src'] = $src_ordered; |
259 | |
260 | return $webfont; |
261 | } |
262 | |
263 | private static function compile_src( $font_family, array $value ) { |
264 | $src = "local($font_family)"; |
265 | |
266 | foreach ( $value as $item ) { |
267 | |
268 | if ( |
269 | str_starts_with( $item['url'], site_url() ) || |
270 | str_starts_with( $item['url'], home_url() ) |
271 | ) { |
272 | $item['url'] = wp_make_link_relative( $item['url'] ); |
273 | } |
274 | |
275 | $src .= ( 'data' === $item['format'] ) |
276 | ? ", url({$item['url']})" |
277 | : ", url('{$item['url']}') format('{$item['format']}')"; |
278 | } |
279 | |
280 | return $src; |
281 | } |
282 | |
283 | /** |
284 | * Compiles the font variation settings. |
285 | * |
286 | * @since 6.0.0 |
287 | * |
288 | * @param array $font_variation_settings Array of font variation settings. |
289 | * @return string The CSS. |
290 | */ |
291 | private static function compile_variations( array $font_variation_settings ) { |
292 | $variations = ''; |
293 | |
294 | foreach ( $font_variation_settings as $key => $value ) { |
295 | $variations .= "$key $value"; |
296 | } |
297 | |
298 | return $variations; |
299 | } |
300 | |
301 | private static function build_font_face_css( array $webfont ) { |
302 | $css = ''; |
303 | |
304 | // Wrap font-family in quotes if it contains spaces. |
305 | if ( |
306 | str_contains( $webfont['font-family'], ' ' ) && |
307 | ! str_contains( $webfont['font-family'], '"' ) && |
308 | ! str_contains( $webfont['font-family'], "'" ) |
309 | ) { |
310 | $webfont['font-family'] = '"' . $webfont['font-family'] . '"'; |
311 | } |
312 | |
313 | foreach ( $webfont as $key => $value ) { |
314 | /* |
315 | * Skip "provider", since it's for internal API use, |
316 | * and not a valid CSS property. |
317 | */ |
318 | if ( 'provider' === $key ) { |
319 | continue; |
320 | } |
321 | |
322 | // Compile the "src" parameter. |
323 | if ( 'src' === $key ) { |
324 | $value = self::compile_src( $webfont['font-family'], $value ); |
325 | } |
326 | |
327 | // If font-variation-settings is an array, convert it to a string. |
328 | if ( 'font-variation-settings' === $key && is_array( $value ) ) { |
329 | $value = self::compile_variations( $value ); |
330 | } |
331 | |
332 | if ( ! empty( $value ) ) { |
333 | $css .= "$key:$value;"; |
334 | } |
335 | } |
336 | |
337 | return $css; |
338 | } |
339 | |
340 | private static function get_css_from_webfonts( $registered_webfonts ) { |
341 | $css = ''; |
342 | |
343 | foreach ( $registered_webfonts as $webfont ) { |
344 | // Order the webfont's `src` items to optimize for browser support. |
345 | $webfont = self::order_src( $webfont ); |
346 | |
347 | // Build the @font-face CSS for this webfont. |
348 | $css .= '@font-face{' . self::build_font_face_css( $webfont ) . '}'; |
349 | } |
350 | |
351 | return $css; |
352 | } |
353 | |
354 | public static function get_wp_theme_json_webfonts_css() { |
355 | |
356 | $styles = self::get_css_from_webfonts( self::get_registered_webfonts_from_theme_json() ); |
357 | |
358 | if ( '' === $styles ) { |
359 | return false; |
360 | } |
361 | |
362 | return $styles; |
363 | } |
364 | } |