PluginProbe
Gutenberg / 17.2.1
Gutenberg v17.2.1
24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 All 403 releases
gutenberg / lib / experimental / fonts-api / class-wp-fonts.php

class-wp-fonts.php in Gutenberg 17.2.1, at lib/experimental/fonts-api/class-wp-fonts.php

754 lines 20.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WP Fonts API class.
4 *
5 * @package WordPress
6 * @subpackage Fonts API
7 * @since X.X.X
8 */
9
10 if ( class_exists( 'WP_Fonts' ) ) {
11 return;
12 }
13
14 /**
15 * Class WP_Web_Fonts
16 *
17 * @since X.X.X
18 */
19 class WP_Fonts extends WP_Dependencies {
20
21 /**
22 * Registered "origin", indicating the font is registered in the API.
23 *
24 * @since X.X.X
25 *
26 * @var string
27 */
28 const REGISTERED_ORIGIN = 'gutenberg_wp_fonts_api';
29
30 /**
31 * An array of registered providers.
32 *
33 * @since X.X.X
34 *
35 * @var array
36 */
37 private $providers = array();
38
39 /**
40 * The flipped $to_do array of font handles.
41 *
42 * Used for a faster lookup of the font handles.
43 *
44 * @since X.X.X
45 *
46 * @var string[]
47 */
48 private $to_do_keyed_handles;
49
50 /**
51 * Provider instance store, keyed by provider ID.
52 *
53 * @since X.X.X
54 *
55 * @var array
56 */
57 private $provider_instances = array();
58
59 /**
60 * Variation property defaults.
61 *
62 * @since X.X.X
63 *
64 * @var array
65 */
66 private $variation_property_defaults = array(
67 'provider' => 'local',
68 'font-family' => '',
69 'font-style' => 'normal',
70 'font-weight' => '400',
71 'font-display' => 'fallback',
72 );
73
74 /**
75 * Constructor.
76 *
77 * @since X.X.X
78 */
79 public function __construct() {
80 /**
81 * Filters the font variation's property defaults.
82 *
83 * @since X.X.X
84 *
85 * @param array $defaults {
86 * An array of required font properties and defaults.
87 *
88 * @type string $provider The provider ID. Default 'local'.
89 * @type string $font-family The font-family property. Default empty string.
90 * @type string $font-style The font-style property. Default 'normal'.
91 * @type string $font-weight The font-weight property. Default '400'.
92 * @type string $font-display The font-display property. Default 'fallback'.
93 * }
94 */
95 $this->variation_property_defaults = apply_filters( 'wp_font_variation_defaults', $this->variation_property_defaults );
96
97 /**
98 * Fires when the WP_Fonts instance is initialized.
99 *
100 * @since X.X.X
101 *
102 * @param WP_Fonts $wp_fonts WP_Fonts instance (passed by reference).
103 */
104 do_action_ref_array( 'wp_default_fonts', array( &$this ) );
105 }
106
107 /**
108 * Get the list of registered providers.
109 *
110 * @since X.X.X
111 *
112 * @return array $providers {
113 * An associative array of registered providers, keyed by their unique ID.
114 *
115 * @type string $provider_id => array {
116 * An associate array of provider's class name and fonts.
117 *
118 * @type string $class_name Fully qualified name of the provider's class.
119 * @type string[] $fonts An array of enqueued font handles for this provider.
120 * }
121 * }
122 */
123 public function get_providers() {
124 return $this->providers;
125 }
126
127 /**
128 * Register a provider.
129 *
130 * @since X.X.X
131 *
132 * @param string $provider_id The provider's unique ID.
133 * @param string $class_name The provider class name.
134 * @return bool True if successfully registered, else false.
135 */
136 public function register_provider( $provider_id, $class_name ) {
137 if ( empty( $provider_id ) || empty( $class_name ) || ! class_exists( $class_name ) ) {
138 return false;
139 }
140
141 $this->providers[ $provider_id ] = array(
142 'class' => $class_name,
143 'fonts' => array(),
144 );
145 return true;
146 }
147
148 /**
149 * Get the list of all registered font family handles.
150 *
151 * @since X.X.X
152 *
153 * @return string[]
154 */
155 public function get_registered_font_families() {
156 $font_families = array();
157 foreach ( $this->registered as $handle => $obj ) {
158 if ( $obj->extra['is_font_family'] ) {
159 $font_families[] = $handle;
160 }
161 }
162 return $font_families;
163 }
164
165 /**
166 * Get the list of all registered font families and their variations.
167 *
168 * @since X.X.X
169 *
170 * @return string[]
171 */
172 public function get_registered() {
173 return array_keys( $this->registered );
174 }
175
176 /**
177 * Get the list of enqueued font families and their variations.
178 *
179 * @since X.X.X
180 *
181 * @return array[]
182 */
183 public function get_enqueued() {
184 return $this->queue;
185 }
186
187 /**
188 * Registers a font family.
189 *
190 * @since X.X.X
191 *
192 * @param string $font_family Font family name to register.
193 * @return string|null Font family handle when registration successes. Null on failure.
194 */
195 public function add_font_family( $font_family ) {
196 $font_family_handle = WP_Fonts_Utils::convert_font_family_into_handle( $font_family );
197 if ( ! $font_family_handle ) {
198 return null;
199 }
200
201 if ( isset( $this->registered[ $font_family_handle ] ) ) {
202 return $font_family_handle;
203 }
204
205 $registered = $this->add( $font_family_handle, false );
206 if ( ! $registered ) {
207 return null;
208 }
209
210 $this->add_data( $font_family_handle, 'font-properties', array( 'font-family' => $font_family ) );
211 $this->add_data( $font_family_handle, 'is_font_family', true );
212
213 return $font_family_handle;
214 }
215
216 /**
217 * Removes a font family and all registered variations.
218 *
219 * @since X.X.X
220 *
221 * @param string $font_family_handle The font family to remove.
222 */
223 public function remove_font_family( $font_family_handle ) {
224 if ( ! isset( $this->registered[ $font_family_handle ] ) ) {
225 return;
226 }
227
228 $variations = $this->registered[ $font_family_handle ]->deps;
229
230 foreach ( $variations as $variation ) {
231 $this->remove( $variation );
232 }
233
234 $this->remove( $font_family_handle );
235 }
236
237 /**
238 * Add a variation to an existing family or register family if none exists.
239 *
240 * @since X.X.X
241 *
242 * @param string $font_family_handle The font family's handle for this variation.
243 * @param array $variation An array of variation properties to add.
244 * @param string $variation_handle Optional. The variation's handle. When none is provided, the
245 * handle will be dynamically generated.
246 * Default empty string.
247 * @return string|null Variation handle on success. Else null.
248 */
249 public function add_variation( $font_family_handle, array $variation, $variation_handle = '' ) {
250 if ( ! WP_Fonts_Utils::is_defined( $font_family_handle ) ) {
251 trigger_error( 'Font family handle must be a non-empty string.' );
252 return null;
253 }
254
255 // When there is a variation handle, check it.
256 if ( '' !== $variation_handle && ! WP_Fonts_Utils::is_defined( $variation_handle ) ) {
257 trigger_error( 'Variant handle must be a non-empty string.' );
258 return null;
259 }
260
261 // Register the font family when it does not yet exist.
262 if ( ! isset( $this->registered[ $font_family_handle ] ) ) {
263 if ( ! $this->add_font_family( $font_family_handle ) ) {
264 return null;
265 }
266 }
267
268 $variation = $this->validate_variation( $variation );
269
270 // Variation validation failed.
271 if ( ! $variation ) {
272 return null;
273 }
274
275 // When there's no variation handle, attempt to create one.
276 if ( '' === $variation_handle ) {
277 $variation_handle = WP_Fonts_Utils::convert_variation_into_handle( $font_family_handle, $variation );
278 if ( is_null( $variation_handle ) ) {
279 return null;
280 }
281 }
282
283 // Bail out if the variant is already registered.
284 if ( $this->is_variation_registered( $font_family_handle, $variation_handle ) ) {
285 return $variation_handle;
286 }
287
288 $variation_src = array_key_exists( 'src', $variation ) ? $variation['src'] : false;
289 $result = $this->add( $variation_handle, $variation_src );
290
291 // Bail out if the registration failed.
292 if ( ! $result ) {
293 return null;
294 }
295
296 $this->add_data( $variation_handle, 'font-properties', $variation );
297 $this->add_data( $variation_handle, 'is_font_family', false );
298
299 // Add the font variation as a dependency to the registered font family.
300 $this->add_dependency( $font_family_handle, $variation_handle );
301
302 $this->providers[ $variation['provider'] ]['fonts'][] = $variation_handle;
303
304 return $variation_handle;
305 }
306
307 /**
308 * Removes a variation.
309 *
310 * @since X.X.X
311 *
312 * @param string $font_family_handle The font family for this variation.
313 * @param string $variation_handle The variation's handle to remove.
314 */
315 public function remove_variation( $font_family_handle, $variation_handle ) {
316 if ( isset( $this->registered[ $variation_handle ] ) ) {
317 $this->remove( $variation_handle );
318 }
319
320 if ( ! $this->is_variation_registered( $font_family_handle, $variation_handle ) ) {
321 return;
322 }
323
324 // Remove the variation as a dependency from its font family.
325 $this->registered[ $font_family_handle ]->deps = array_values(
326 array_diff(
327 $this->registered[ $font_family_handle ]->deps,
328 array( $variation_handle )
329 )
330 );
331 }
332
333 /**
334 * Checks if the variation is registered.
335 *
336 * @since X.X.X
337 *
338 * @param string $font_family_handle The font family's handle for this variation.
339 * @param string $variation_handle Variation's handle.
340 * @return bool True when registered to the given font family. Else false.
341 */
342 private function is_variation_registered( $font_family_handle, $variation_handle ) {
343 if ( ! isset( $this->registered[ $font_family_handle ] ) ) {
344 return false;
345 }
346
347 return in_array( $variation_handle, $this->registered[ $font_family_handle ]->deps, true );
348 }
349
350 /**
351 * Adds a variation as a dependency to the given font family.
352 *
353 * @since X.X.X
354 *
355 * @param string $font_family_handle The font family's handle for this variation.
356 * @param string $variation_handle The variation's handle.
357 */
358 private function add_dependency( $font_family_handle, $variation_handle ) {
359 $this->registered[ $font_family_handle ]->deps[] = $variation_handle;
360 }
361
362 /**
363 * Validates and sanitizes a variation.
364 *
365 * @since X.X.X
366 *
367 * @param array $variation Variation properties to add.
368 * @return false|array Validated variation on success. Else, false.
369 */
370 private function validate_variation( $variation ) {
371 $variation = wp_parse_args( $variation, $this->variation_property_defaults );
372
373 // Check the font-family.
374 if ( empty( $variation['font-family'] ) || ! is_string( $variation['font-family'] ) ) {
375 trigger_error( 'Webfont font-family must be a non-empty string.' );
376 return false;
377 }
378
379 // Local fonts need a "src".
380 if ( 'local' === $variation['provider'] ) {
381 // Make sure that local fonts have 'src' defined.
382 if ( empty( $variation['src'] ) || ( ! is_string( $variation['src'] ) && ! is_array( $variation['src'] ) ) ) {
383 trigger_error( 'Webfont src must be a non-empty string or an array of strings.' );
384 return false;
385 }
386 } elseif ( ! isset( $this->providers[ $variation['provider'] ] ) ) {
387 trigger_error( sprintf( 'The provider "%s" is not registered', $variation['provider'] ) );
388 return false;
389 } elseif ( ! class_exists( $this->providers[ $variation['provider'] ]['class'] ) ) {
390 trigger_error( sprintf( 'The provider class "%s" does not exist', $variation['provider'] ) );
391 return false;
392 }
393
394 // Validate the 'src' property.
395 if ( ! empty( $variation['src'] ) ) {
396 foreach ( (array) $variation['src'] as $src ) {
397 if ( empty( $src ) || ! is_string( $src ) ) {
398 trigger_error( 'Each font src must be a non-empty string.' );
399 return false;
400 }
401 }
402 }
403
404 // Check the font-weight.
405 if ( ! is_string( $variation['font-weight'] ) && ! is_int( $variation['font-weight'] ) ) {
406 trigger_error( 'Webfont font-weight must be a properly formatted string or integer.' );
407 return false;
408 }
409
410 // Check the font-display.
411 if ( ! in_array( $variation['font-display'], array( 'auto', 'block', 'fallback', 'swap', 'optional' ), true ) ) {
412 $variation['font-display'] = 'fallback';
413 }
414
415 $valid_props = array(
416 'ascent-override',
417 'descent-override',
418 'font-display',
419 'font-family',
420 'font-stretch',
421 'font-style',
422 'font-weight',
423 'font-variant',
424 'font-feature-settings',
425 'font-variation-settings',
426 'line-gap-override',
427 'size-adjust',
428 'src',
429 'unicode-range',
430
431 // Exceptions.
432 'provider',
433 );
434
435 foreach ( $variation as $prop => $value ) {
436 if ( ! in_array( $prop, $valid_props, true ) ) {
437 unset( $variation[ $prop ] );
438 }
439 }
440
441 return $variation;
442 }
443
444 /**
445 * Processes the items and dependencies.
446 *
447 * Processes the items passed to it or the queue, and their dependencies.
448 *
449 * @since X.X.X
450 *
451 * @param string|string[]|bool $handles Optional. Items to be processed: queue (false),
452 * single item (string), or multiple items (array of strings).
453 * Default false.
454 * @param int|false $group Optional. Group level: level (int), no group (false).
455 *
456 * @return array|string[] Array of font handles that have been processed.
457 * An empty array if none were processed.
458 */
459 public function do_items( $handles = false, $group = false ) {
460 $handles = $this->prepare_handles_for_printing( $handles );
461
462 if ( empty( $handles ) ) {
463 return $this->done;
464 }
465
466 $this->all_deps( $handles );
467 if ( empty( $this->to_do ) ) {
468 return $this->done;
469 }
470
471 $this->to_do_keyed_handles = array_flip( $this->to_do );
472
473 foreach ( $this->get_providers() as $provider_id => $provider ) {
474 // Alert and skip if the provider class does not exist.
475 if ( ! class_exists( $provider['class'] ) ) {
476 /* translators: %s is the provider name. */
477 trigger_error(
478 sprintf(
479 'Class "%s" not found for "%s" font provider',
480 $provider['class'],
481 $provider_id
482 )
483 );
484 continue;
485 }
486
487 $this->do_item( $provider_id, $group );
488 }
489
490 $this->process_font_families_after_printing( $handles );
491
492 return $this->done;
493 }
494
495 /**
496 * Prepares the given handles for printing.
497 *
498 * @since X.X.X
499 *
500 * @param string|string[]|bool $handles Optional. Handles to prepare.
501 * Default false.
502 * @return array Array of handles.
503 */
504 private function prepare_handles_for_printing( $handles = false ) {
505 if ( false !== $handles ) {
506 $handles = $this->validate_handles( $handles );
507 // Bail out when invalid.
508 if ( empty( $handles ) ) {
509 return array();
510 }
511 }
512
513 // Use the enqueued queue.
514 if ( empty( $handles ) ) {
515 if ( empty( $this->queue ) ) {
516 return array();
517 }
518 $handles = $this->queue;
519 }
520
521 return $handles;
522 }
523
524 /**
525 * Validates handle(s) to ensure each is a non-empty string.
526 *
527 * @since X.X.X
528 *
529 * @param string|string[] $handles Handles to prepare.
530 * @return string[]|null Array of handles on success. Else null.
531 */
532 private function validate_handles( $handles ) {
533 // Validate each element is a non-empty string handle.
534 $handles = array_filter( (array) $handles, array( WP_Fonts_Utils::class, 'is_defined' ) );
535
536 if ( empty( $handles ) ) {
537 trigger_error( 'Handles must be a non-empty string or array of non-empty strings' );
538 return null;
539 }
540
541 return $handles;
542 }
543
544 /**
545 * Invokes each provider to process and print its styles.
546 *
547 * @since X.X.X
548 *
549 * @see WP_Dependencies::do_item()
550 *
551 * @param string $provider_id The provider to process.
552 * @param int|false $group Not used.
553 * @return bool
554 */
555 public function do_item( $provider_id, $group = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
556 // Bail out if the provider is not registered.
557 if ( ! isset( $this->providers[ $provider_id ] ) ) {
558 return false;
559 }
560
561 $font_handles = $this->get_enqueued_fonts_for_provider( $provider_id );
562 if ( empty( $font_handles ) ) {
563 return false;
564 }
565
566 $properties_by_font = $this->get_font_properties_for_provider( $font_handles );
567 if ( empty( $properties_by_font ) ) {
568 return false;
569 }
570
571 // Invoke provider to print its styles.
572 $provider = $this->get_provider_instance( $provider_id );
573 $provider->set_fonts( $properties_by_font );
574 $provider->print_styles();
575
576 // Clean up.
577 $this->update_queues_for_printed_fonts( $font_handles );
578
579 return true;
580 }
581
582 /**
583 * Retrieves a list of enqueued font variations for a provider.
584 *
585 * @since X.X.X
586 *
587 * @param string $provider_id The provider to process.
588 * @return array[] Webfonts organized by providers.
589 */
590 private function get_enqueued_fonts_for_provider( $provider_id ) {
591 $providers = $this->get_providers();
592
593 if ( empty( $providers[ $provider_id ] ) ) {
594 return array();
595 }
596
597 return array_intersect(
598 $providers[ $provider_id ]['fonts'],
599 $this->to_do
600 );
601 }
602
603 /**
604 * Gets a list of font properties for each of the given font handles.
605 *
606 * @since X.X.X
607 *
608 * @param array $font_handles Font handles to get properties.
609 * @return array A list of fonts with each font's properties.
610 */
611 private function get_font_properties_for_provider( array $font_handles ) {
612 $font_properties = array();
613
614 foreach ( $font_handles as $font_handle ) {
615 $properties = $this->get_data( $font_handle, 'font-properties' );
616 if ( ! $properties ) {
617 continue;
618 }
619 $font_properties[ $font_handle ] = $properties;
620 }
621
622 return $font_properties;
623 }
624
625 /**
626 * Gets the instance of the provider from the WP_Webfonts::$provider_instance store.
627 *
628 * @since X.X.X
629 *
630 * @param string $provider_id The provider to get.
631 * @return object Instance of the provider.
632 */
633 private function get_provider_instance( $provider_id ) {
634 if ( ! isset( $this->provider_instances[ $provider_id ] ) ) {
635 $this->provider_instances[ $provider_id ] = new $this->providers[ $provider_id ]['class']();
636 }
637 return $this->provider_instances[ $provider_id ];
638 }
639
640 /**
641 * Update queues for the given printed fonts.
642 *
643 * @since X.X.X
644 *
645 * @param array $font_handles Font handles to get properties.
646 */
647 private function update_queues_for_printed_fonts( array $font_handles ) {
648 foreach ( $font_handles as $font_handle ) {
649 $this->set_as_done( $font_handle );
650 $this->remove_from_to_do_queues( $font_handle );
651 }
652 }
653
654 /**
655 * Processes the font families after printing the variations.
656 *
657 * For each queued font family:
658 *
659 * a. if any of their variations were printed, the font family is added to the `done` list.
660 * b. removes each from the to_do queues.
661 *
662 * @since X.X.X
663 *
664 * @param array $handles Handles to process.
665 */
666 private function process_font_families_after_printing( array $handles ) {
667 foreach ( $handles as $handle ) {
668 if (
669 ! $this->get_data( $handle, 'is_font_family' ) ||
670 ! isset( $this->to_do_keyed_handles[ $handle ] )
671 ) {
672 continue;
673 }
674 $font_family = $this->registered[ $handle ];
675
676 // Add the font family to `done` list if any of its variations were printed.
677 if ( ! empty( $font_family->deps ) ) {
678 $processed = array_intersect( $font_family->deps, $this->done );
679 if ( ! empty( $processed ) ) {
680 $this->set_as_done( $handle );
681 }
682 }
683
684 $this->remove_from_to_do_queues( $handle );
685 }
686 }
687
688 /**
689 * Removes the handle from the `to_do` and `to_do_keyed_handles` lists.
690 *
691 * @since X.X.X
692 *
693 * @param string $handle Handle to remove.
694 */
695 private function remove_from_to_do_queues( $handle ) {
696 unset(
697 $this->to_do[ $this->to_do_keyed_handles[ $handle ] ],
698 $this->to_do_keyed_handles[ $handle ]
699 );
700 }
701
702 /**
703 * Sets the given handle to done by adding it to the `done` list.
704 *
705 * @since X.X.X
706 *
707 * @param string $handle Handle to set as done.
708 */
709 private function set_as_done( $handle ) {
710 if ( ! is_array( $this->done ) ) {
711 $this->done = array();
712 }
713 $this->done[] = $handle;
714 }
715
716 /**
717 * Converts the font family and its variations into theme.json structural format.
718 *
719 * @since X.X.X
720 *
721 * @param string $font_family_handle Font family to convert.
722 * @return array Webfonts in theme.json structural format.
723 */
724 public function to_theme_json( $font_family_handle ) {
725 if ( ! isset( $this->registered[ $font_family_handle ] ) ) {
726 return array();
727 }
728
729 $font_family_name = $this->registered[ $font_family_handle ]->extra['font-properties']['font-family'];
730 $theme_json_format = array(
731 'fontFamily' => str_contains( $font_family_name, ' ' ) ? "'{$font_family_name}'" : $font_family_name,
732 'name' => $font_family_name,
733 'slug' => $font_family_handle,
734 'fontFace' => array(),
735 );
736
737 foreach ( $this->registered[ $font_family_handle ]->deps as $variation_handle ) {
738 if ( ! isset( $this->registered[ $variation_handle ] ) ) {
739 continue;
740 }
741
742 $variation_obj = $this->registered[ $variation_handle ];
743 $variation_properties = array( 'origin' => static::REGISTERED_ORIGIN );
744 foreach ( $variation_obj->extra['font-properties'] as $property_name => $property_value ) {
745 $property_in_camelcase = lcfirst( str_replace( '-', '', ucwords( $property_name, '-' ) ) );
746 $variation_properties[ $property_in_camelcase ] = $property_value;
747 }
748 $theme_json_format['fontFace'][ $variation_obj->handle ] = $variation_properties;
749 }
750
751 return $theme_json_format;
752 }
753 }
754