Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 167
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Webfonts
0.00% covered (danger)
0.00%
0 / 167
0.00% covered (danger)
0.00%
0 / 11
4556
0.00% covered (danger)
0.00%
0 / 1
 get_webfonts_from_theme_json
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
210
 transform_src_into_uri
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 convert_keys_to_kebab_case
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 validate_webfont
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
210
 get_registered_webfonts_from_theme_json
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 order_src
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
72
 compile_src
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 compile_variations
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 build_font_face_css
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
110
 get_css_from_webfonts
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 get_wp_theme_json_webfonts_css
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2namespace NewfoldLabs\WP\Module\Onboarding\Services;
3
4class 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}