is_block_editor() ) { remove_action( 'admin_print_styles', 'wp_print_font_faces', 50 ); } } /** * Print fonts that are used in global styles or block-level settings. */ public function print_font_faces() { if ( jetpack_google_fonts_load_font_faces() ) { return; } $this->collect_global_styles_fonts(); $fonts_in_use = array_fill_keys( $this->fonts_in_use, true ); if ( empty( $fonts_in_use ) ) { return; } $settings = function_exists( 'gutenberg_get_global_settings' ) ? gutenberg_get_global_settings() : wp_get_global_settings(); $native_slugs = array(); $native_faces = array(); foreach ( $settings['typography']['fontFamilies'] ?? array() as $origin => $families ) { foreach ( $families as $family ) { if ( 'default' === $origin && empty( $family['fontFace'] ) ) { continue; } if ( empty( $family['fontFamily'] ) ) { continue; } $key = $this->format_font( self::get_font_family_name( $family ) ); if ( ! empty( $family['slug'] ) ) { $native_slugs[ $this->format_font( $family['slug'] ) ] = $key; } if ( ! empty( $family['fontFace'] ) ) { foreach ( $family['fontFace'] as $face ) { $native_faces[ $key ][] = $face; } } } } // Presets without their own faces can still refer to a legacy catalogue family. $fonts_in_use = array_fill_keys( array_map( static function ( $font ) use ( $native_slugs ) { return $native_slugs[ $font ] ?? $font; }, array_keys( $fonts_in_use ) ), true ); $data = jetpack_get_google_fonts_data(); if ( empty( $data['fontFamilies'] ) ) { return; } $available_fonts = jetpack_get_available_google_fonts_map( $data ); $fonts_to_print = array(); foreach ( $data['fontFamilies'] as $family ) { if ( empty( $available_fonts[ $family['name'] ] ) || empty( $family['fontFace'] ) ) { continue; } $name = self::get_font_family_name( $family ); $key = $this->format_font( $name ); $slug = $this->format_font( $family['slug'] ); // A preset that reuses this slug for another family replaces it. if ( isset( $native_slugs[ $slug ] ) && $native_slugs[ $slug ] !== $key ) { continue; } if ( ! isset( $fonts_in_use[ $slug ] ) && ! isset( $fonts_in_use[ $key ] ) ) { continue; } $catalogue_faces = $family['fontFace']; if ( isset( $native_faces[ $key ] ) ) { // Native files win; the catalogue only fills styles and weights they lack. $catalogue_faces = array_filter( $catalogue_faces, static function ( $face ) use ( $native_faces, $key ) { return ! self::is_face_provided( $face, $native_faces[ $key ] ); } ); if ( empty( $catalogue_faces ) ) { continue; } } $faces = array(); foreach ( $catalogue_faces as $face ) { $face['font-family'] = $name; $converted = array(); foreach ( $face as $property => $value ) { $converted[ _wp_to_kebab_case( $property ) ] = $value; } $faces[] = $converted; } $fonts_to_print[] = $faces; } if ( ! empty( $fonts_to_print ) ) { wp_print_font_faces( $fonts_to_print ); } } /** * Whether a native face already covers a catalogue face's style and weight. * * @param array $face Catalogue face. * @param array[] $native_faces Native faces for the same family. * @return bool */ private static function is_face_provided( $face, $native_faces ) { $style = $face['fontStyle'] ?? 'normal'; list( $min, $max ) = self::get_weight_range( $face['fontWeight'] ?? '400' ); foreach ( $native_faces as $native ) { if ( ( $native['fontStyle'] ?? 'normal' ) !== $style ) { continue; } list( $native_min, $native_max ) = self::get_weight_range( $native['fontWeight'] ?? '400' ); // Overlap, not containment: a later catalogue rule would win at a shared weight. if ( $native_min <= $max && $min <= $native_max ) { return true; } } return false; } /** * Parses a font-weight descriptor such as "400", "bold" or "100 900". * * @param string|int $weight Font weight. * @return int[] Minimum and maximum weight. */ private static function get_weight_range( $weight ) { $weight = str_replace( array( 'normal', 'bold' ), array( '400', '700' ), (string) $weight ); $parts = preg_split( '/\s+/', trim( $weight ) ); return array( (int) $parts[0], (int) ( $parts[1] ?? $parts[0] ) ); } /** * Collect fonts used for global styles settings. */ public function collect_global_styles_fonts() { $global_styles = function_exists( 'gutenberg_get_global_styles' ) ? gutenberg_get_global_styles() : wp_get_global_styles(); $global_styles_font_slug = $this->get_font_slug_from_setting( $global_styles ); if ( $global_styles_font_slug ) { $this->add_font( $global_styles_font_slug ); } if ( isset( $global_styles['blocks'] ) ) { foreach ( $global_styles['blocks'] as $setting ) { $font_slug = $this->get_font_slug_from_setting( $setting ); if ( $font_slug ) { $this->add_font( $font_slug ); } } } if ( isset( $global_styles['elements'] ) ) { foreach ( $global_styles['elements'] as $setting ) { $font_slug = $this->get_font_slug_from_setting( $setting ); if ( $font_slug ) { $this->add_font( $font_slug ); } } } } /** * Collect fonts used for block-level settings. * * @filter pre_render_block * * @param string|null $content The pre-rendered content. Default null. * @param array $parsed_block The block being rendered. */ public function collect_block_fonts( $content, $parsed_block ) { if ( ! is_admin() && isset( $parsed_block['attrs']['fontFamily'] ) ) { $block_font_family = $parsed_block['attrs']['fontFamily']; $this->add_font( $block_font_family ); } return $content; } /** * Add the specify font to the fonts_in_use list. * * @param string $font_slug The font slug. */ public function add_font( $font_slug ) { if ( is_string( $font_slug ) ) { $this->fonts_in_use[] = $this->format_font( $font_slug ); } } /** * Format the given font slug. * * @example "ABeeZee" formats to "abeezee" * @example "ADLaM Display" formats to "adlam-display" * @param string $font_slug The font slug. * @return string The formatted font slug. */ public function format_font( $font_slug ) { return _wp_to_kebab_case( strtolower( $font_slug ) ); } /** * Get the font family name from a font. * * @param array $font The font definition object. */ public static function get_font_family_name( $font ) { $font_family = $font['fontFamily']; if ( str_contains( $font_family, ',' ) ) { $font_family = explode( ',', $font_family )[0]; } return trim( $font_family, "\"'" ); } /** * Get the font family slug from a settings array. * * @param array $setting The settings object. * * @return string|null */ public function get_font_slug_from_setting( $setting ) { if ( ! isset( $setting['typography']['fontFamily'] ) ) { return null; } $font_family = $setting['typography']['fontFamily']; // The font family may be a reference to a path to the value stored at that location, // e.g.: { "ref": "styles.elements.heading.typography.fontFamily" }. // Ignore it as we also get the value stored at that location from the setting. if ( ! is_string( $font_family ) ) { return null; } // A preset reference can include a var() fallback and a trailing font stack. preg_match( '/^\s*var\(\s*--wp--preset--font-family--(?P[^\s,()]+)\s*[,)]/', $font_family, $matches ); if ( isset( $matches['slug'] ) ) { return $matches['slug']; } // Full string: var:preset|font-family|slug // We do not care about the origin of the font, only its slug. preg_match( '/^\s*var:preset\|font-family\|(?P[^\s,]+)/', $font_family, $matches ); if ( isset( $matches['slug'] ) ) { return $matches['slug']; } return self::get_font_family_name( $setting['typography'] ); } /** * Check if the current screen is the block editor. * * @return bool */ public function is_block_editor() { if ( function_exists( 'get_current_screen' ) ) { $current_screen = get_current_screen(); if ( ! empty( $current_screen ) && method_exists( $current_screen, 'is_block_editor' ) && $current_screen->is_block_editor() ) { return true; } } return false; } }