diff --git a/src/Editor.php b/src/Editor.php index 56fa527f..357c2f02 100644 --- a/src/Editor.php +++ b/src/Editor.php @@ -15,9 +15,8 @@ namespace X3P0\Ideas; -use WP_Block_Editor_Context; use X3P0\Ideas\Contracts\Bootable; -use X3P0\Ideas\Tools\HookAnnotation; +use X3P0\Ideas\Tools\{FontFaceResolver, HookAnnotation}; class Editor implements Bootable { @@ -34,6 +33,17 @@ public function boot(): void $this->hookMethods(); } + /** + * Runs actions only when viewing the Site Editor screen. + * + * @hook load-site-editor.php + * @since 1.0.0 + */ + public function loadSiteEditor(): void + { + add_action('enqueue_block_assets', [$this, 'enqueueFonts']); + } + /** * Add editor stylesheets. * @@ -79,23 +89,33 @@ public function enqueueAssets(): void wp_set_script_translations('x3p0-ideas-editor', 'x3p0-ideas'); } + /** + * Enqueues local web fonts. This is necessary to fix the broken Site + * Editor preview with style variations in WordPress. + * + * @since 1.0.0 + * @link https://github.com/WordPress/gutenberg/issues/59965 + */ + public function enqueueFonts(): void + { + $fonts = FontFaceResolver::getFonts(); + + ob_start(); + wp_print_font_faces($fonts); + $content = ob_get_clean(); + + wp_register_style('x3p0-ideas-fonts', false); + wp_add_inline_style('x3p0-ideas-fonts', trim(strip_tags($content))); + wp_enqueue_style('x3p0-ideas-fonts'); + } + /** * Customizes the block editor settings. * * @hook block_editor_settings_all last * @since 1.0.0 */ - public function registerSettings( - array $settings, - WP_Block_Editor_Context $context - ): array { - // Load all theme fonts on the Site Editor screen. - if ('core/edit-site' === $context->name) { - $settings['styles'][] = [ - 'css' => $this->getFontCss() - ]; - } - + public function registerSettings(array $settings ): array { $settings['imageDefaultSize'] = 'x3p0-wide'; $settings['fontLibraryEnabled'] = false; @@ -105,54 +125,4 @@ public function registerSettings( return $settings; } - - /** - * Quick and hacky method of loading all fonts. - * - * @since 1.0.0 - * @todo Create a more functional method of loading these. - */ - private function getFontCss(): string - { - $path = get_parent_theme_file_uri(); - return " - @font-face{font-family:Jost;font-style:normal;font-weight:100 900;font-display:fallback;src:url('{$path}/public/fonts/jost/jost.woff2') format('woff2');font-feature-settings:'tnum', 'ss01' 1;font-stretch:normal;} - @font-face{font-family:Jost;font-style:italic;font-weight:100 900;font-display:fallback;src:url('{$path}/public/fonts/jost/jost-italic.woff2') format('woff2');font-feature-settings:'tnum', 'ss01' 1;font-stretch:normal;} - @font-face{font-family:'Roboto Flex';font-style:normal;font-weight:100 1000;font-display:fallback;src:url('{$path}/public/fonts/roboto/roboto-flex.woff2') format('woff2');font-stretch:25% 151%;} - @font-face{font-family:'Roboto Slab';font-style:normal;font-weight:100 900;font-display:fallback;src:url('{$path}/public/fonts/roboto/roboto-slab.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:Playfair;font-style:normal;font-weight:200 700;font-display:fallback;src:url('{$path}/public/fonts/playfair/playfair.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:Playfair;font-style:italic;font-weight:200 700;font-display:fallback;src:url('{$path}/public/fonts/playfair/playfair-italic.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Dancing Script';font-style:normal;font-weight:400 700;font-display:fallback;src:url('{$path}/public/fonts/dancing-script/dancing-script.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:Cabin;font-style:normal;font-weight:400 700;font-display:fallback;src:url('{$path}/public/fonts/cabin/cabin.woff2') format('woff2');font-stretch:75% 100%;} - @font-face{font-family:Cabin;font-style:italic;font-weight:400 700;font-display:fallback;src:url('{$path}/public/fonts/cabin/cabin-italic.woff2') format('woff2');font-stretch:75% 100%;} - @font-face{font-family:'Plus Jakarta Sans';font-style:normal;font-weight:200 800;font-display:fallback;src:url('{$path}/public/fonts/plus-jakarta-sans/plus-jakarta-sans.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Plus Jakarta Sans';font-style:italic;font-weight:200 800;font-display:fallback;src:url('{$path}/public/fonts/plus-jakarta-sans/plus-jakarta-sans-italic.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:100 800;font-display:fallback;src:url('{$path}/public/fonts/jetbrains-mono/jetbrains-mono.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'JetBrains Mono';font-style:italic;font-weight:100 800;font-display:fallback;src:url('{$path}/public/fonts/jetbrains-mono/jetbrains-mono-italic.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Source Serif';font-style:normal;font-weight:200 900;font-display:fallback;src:url('{$path}/public/fonts/source/source-serif.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Source Serif';font-style:italic;font-weight:200 900;font-display:fallback;src:url('{$path}/public/fonts/source/source-serif-italic.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Playfair Display';font-style:normal;font-weight:400 900;font-display:fallback;src:url('{$path}/public/fonts/playfair/playfair-display.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Playfair Display';font-style:italic;font-weight:400 900;font-display:fallback;src:url('{$path}/public/fonts/playfair/playfair-display-italic.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Source Sans';font-style:normal;font-weight:200 900;font-display:fallback;src:url('{$path}/public/fonts/source/source-sans.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Source Sans';font-style:italic;font-weight:200 900;font-display:fallback;src:url('{$path}/public/fonts/source/source-sans-italic.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:Satisfy;font-style:normal;font-weight:400;font-display:fallback;src:url('{$path}/public/fonts/satisfy/satisfy.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:Montserrat;font-style:normal;font-weight:100 900;font-display:fallback;src:url('{$path}/public/fonts/montserrat/montserrat.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:Montserrat;font-style:italic;font-weight:100 900;font-display:fallback;src:url('{$path}/public/fonts/montserrat/montserrat-italic.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:Caveat;font-style:normal;font-weight:400 700;font-display:fallback;src:url('{$path}/public/fonts/caveat/caveat.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:Raleway;font-style:normal;font-weight:100 900;font-display:fallback;src:url('{$path}/public/fonts/raleway/raleway.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:Raleway;font-style:italic;font-weight:100 900;font-display:fallback;src:url('{$path}/public/fonts/raleway/raleway-italic.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Work Sans';font-style:normal;font-weight:100 900;font-display:fallback;src:url('{$path}/public/fonts/work-sans/work-sans.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Work Sans';font-style:italic;font-weight:100 900;font-display:fallback;src:url('{$path}/public/fonts/work-sans/work-sans-italic.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Roboto Serif';font-style:normal;font-weight:100 900;font-display:fallback;src:url('{$path}/experimental/fonts/roboto/roboto-serif.woff2') format('woff2');font-stretch:100%;} - @font-face{font-family:'Roboto Serif';font-style:italic;font-weight:100 900;font-display:fallback;src:url('{$path}/experimental/fonts/roboto/roboto-serif-italic.woff2') format('woff2');font-stretch:100%;} - @font-face{font-family:Oswald;font-style:normal;font-weight:200 700;font-display:fallback;src:url('{$path}/public/fonts/oswald/oswald.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Permanent Marker';font-style:normal;font-weight:400;font-display:fallback;src:url('{$path}/public/fonts/permanent-marker/permanent-marker.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Send Flowers';font-style:normal;font-weight:400;font-display:fallback;src:url('{$path}/public/fonts/send-flowers/send-flowers.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:Quicksand;font-style:normal;font-weight:300 700;font-display:fallback;src:url('{$path}/public/fonts/quicksand/quicksand.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Elsie Swash Caps';font-style:normal;font-weight:400;font-display:fallback;src:url('{$path}/public/fonts/elsie/elsie-swash-caps.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:'Elsie Swash Caps';font-style:normal;font-weight:900;font-display:fallback;src:url('{$path}/public/fonts/elsie/elsie-swash-caps-900.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:Lora;font-style:normal;font-weight:400 700;font-display:fallback;src:url('{$path}/x3p0-ideas/public/fonts/lora/lora.woff2') format('woff2');font-stretch:normal;} - @font-face{font-family:Lora;font-style:italic;font-weight:400 700;font-display:fallback;src:url('{$path}/x3p0-ideas/public/fonts/lora/lora.woff2') format('woff2');font-stretch:normal;} - "; - } } diff --git a/src/Tools/FontFaceResolver.php b/src/Tools/FontFaceResolver.php new file mode 100644 index 00000000..984bd2f5 --- /dev/null +++ b/src/Tools/FontFaceResolver.php @@ -0,0 +1,186 @@ + + * @copyright Copyright (c) 2023-2024, Justin Tadlock + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL-3.0-or-later + * @link https://github.com/x3p0-dev/x3p0-ideas + */ + +declare(strict_types=1); + +namespace X3P0\Ideas\Tools; + +use WP_Theme_JSON_Resolver; + +class FontFaceResolver +{ + /** + * Returns an array of font definitions to be plugged into functions + * like `wp_print_font_faces()` for enqueueing font stylesheets. + * + * @since 1.0.0 + */ + public static function getFonts(): array + { + $families = []; + $settings = wp_get_global_settings(); + $variations = WP_Theme_JSON_Resolver::get_style_variations(); + + // Loop through and store each variation's font families. + foreach ($variations as $variation) { + if (empty($variation['settings']['typography']['fontFamilies'])) { + continue; + } + + foreach ($variation['settings']['typography']['fontFamilies'] as $group) { + $families = array_merge($families, $group); + } + } + + // Bail early if there are no defined font families. + if ([] === $families) { + return []; + } + + $fonts = static::parseFamilies($families); + + // Because Core could be potentially loading a font that's + // already defined in our variations, let's remove those to + // avoid loading them a second time. + if (! empty($settings['typography']['fontFamilies'])) { + $global_families = []; + + foreach ($settings['typography']['fontFamilies'] as $group) { + $global_families = array_merge($global_families, $group); + } + + if ([] !== $global_families) { + $fonts = array_diff_key( + $fonts, + static::parseFamilies($global_families) + ); + } + } + + return $fonts; + } + + /** + * Parses the an array of font families and grabs any font-face + * definitions from them. + * + * @since 1.0.0 + */ + private static function parseFamilies(array $families): array + { + $faces = []; + + foreach ($families as $family) { + if (! isset($family['fontFace']) || ! isset($family['fontFamily'])) { + continue; + } + + $name = static::parseName($family['fontFamily']); + + if (! $name || isset($faces[$name])) { + continue; + } + + $faces[$name] = static::convertProperties($family['fontFace'], $name); + } + + // Just a little nicer sorting. 😊 + ksort($faces); + + return $faces; + } + + /** + * Parses a font-family name. Ensures that we only use the first name if + * presented a comma-separated list. + * + * @since 1.0.0 + */ + private static function parseName(string $family): string + { + if (str_contains($family, ',')) { + $family = explode(',', $family)[0]; + } + + return trim($family, "\"'"); + } + + /** + * Ensures that font-face properties are converted to a form that can be + * used in CSS. + * + * @since 1.0.0 + */ + private static function convertProperties(array $definitions, string $name): array + { + $converted = []; + + foreach ($definitions as $properties) { + $properties['font-family'] = $name; + + if (! empty($properties['src'])) { + $properties['src'] = static::toThemeFileUri( + (array) $properties['src'] + ); + } + + $converted[] = static::toKebabCase($properties); + } + + return $converted; + } + + /** + * Replaces file URI references from JSON to the theme file URI. + * + * @since 1.0.0 + */ + private static function toThemeFileUri(array $src): array + { + $placeholder = 'file:./'; + + foreach ($src as $key => $url) { + if (str_starts_with($url, $placeholder)) { + $src[ $key ] = get_theme_file_uri( + str_replace($placeholder, '', $url) + ); + } + } + + return $src; + } + + /** + * Converts property names/keys to kebab-case. + * + * @since 1.0.0 + */ + private static function toKebabCase(array $data): array + { + foreach ($data as $key => $value) { + unset($data[$key]); + $data[ _wp_to_kebab_case($key) ] = $value; + } + + return $data; + } +}