From fda7bd1d123cc979a23707bcb20ac8bea9386b0a Mon Sep 17 00:00:00 2001 From: rodolfomartinez Date: Mon, 2 Feb 2026 11:04:00 -0500 Subject: [PATCH] font handling fix --- .../wordpress/maple-fonts-wp/assets/admin.css | 27 ++ .../includes/class-mlf-admin-page.php | 59 ++++ .../includes/class-mlf-font-downloader.php | 4 +- .../includes/class-mlf-font-registry.php | 157 ++++++++- .../includes/class-mlf-font-server.php | 182 ++++++++++ .../maple-fonts-wp/maple-local-fonts.php | 320 +++++++++++++++++- 6 files changed, 727 insertions(+), 22 deletions(-) create mode 100644 native/wordpress/maple-fonts-wp/includes/class-mlf-font-server.php diff --git a/native/wordpress/maple-fonts-wp/assets/admin.css b/native/wordpress/maple-fonts-wp/assets/admin.css index 1a923f3..f250843 100644 --- a/native/wordpress/maple-fonts-wp/assets/admin.css +++ b/native/wordpress/maple-fonts-wp/assets/admin.css @@ -520,6 +520,33 @@ margin: 12px 0; } +/* Settings Toggle */ +.mlf-setting-toggle { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + background: #fff; + border: 1px solid #dcdcde; + border-radius: 4px; + cursor: pointer; + font-weight: normal !important; + transition: background-color 0.15s ease; +} + +.mlf-setting-toggle:hover { + background: #f6f7f7; +} + +.mlf-setting-toggle input[type="checkbox"] { + margin: 0; +} + +.mlf-settings-section .description { + margin-top: 8px; + max-width: 600px; +} + /* Loading State */ .mlf-loading { opacity: 0.6; diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-admin-page.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-admin-page.php index 725df74..2236e9a 100644 --- a/native/wordpress/maple-fonts-wp/includes/class-mlf-admin-page.php +++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-admin-page.php @@ -23,6 +23,39 @@ class MLF_Admin_Page { // Empty constructor - class is instantiated for rendering } + /** + * Handle settings form submission. + */ + private function handle_settings_save() { + if (!isset($_POST['mlf_save_settings'])) { + return; + } + + // Verify nonce + if (!isset($_POST['mlf_settings_nonce']) || !wp_verify_nonce($_POST['mlf_settings_nonce'], 'mlf_save_settings')) { + add_settings_error('mlf_settings', 'nonce_error', __('Security check failed.', 'maple-local-fonts'), 'error'); + return; + } + + // Verify capability + $capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options'; + if (!current_user_can($capability)) { + return; + } + + // Save compatibility mode setting + $compatibility_mode = isset($_POST['mlf_compatibility_mode']) && $_POST['mlf_compatibility_mode'] === '1'; + update_option('mlf_compatibility_mode', $compatibility_mode); + + // Clear font caches to apply new URL format + delete_transient('mlf_imported_fonts_list'); + if (class_exists('WP_Theme_JSON_Resolver')) { + WP_Theme_JSON_Resolver::clean_cached_data(); + } + + add_settings_error('mlf_settings', 'settings_saved', __('Settings saved.', 'maple-local-fonts'), 'success'); + } + /** * Render the admin page. */ @@ -32,11 +65,15 @@ class MLF_Admin_Page { wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'maple-local-fonts')); } + // Handle settings save + $this->handle_settings_save(); + $registry = new MLF_Font_Registry(); $installed_fonts = $registry->get_imported_fonts(); ?>

+

@@ -162,6 +199,28 @@ class MLF_Admin_Page {
+ +
+

+
+ +
+ +

+ +

+
+
+ +
+
+
+
diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php index d83f15d..46e74b0 100644 --- a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php +++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php @@ -170,6 +170,7 @@ class MLF_Font_Downloader { [300, 400, 500, 600, 700], // Common range [400, 500, 600, 700], // Without light [400, 700], // Just regular and bold + [400], // Single weight (display fonts) ]; $css = null; @@ -227,7 +228,8 @@ class MLF_Font_Downloader { $family = str_replace(' ', '+', $font_name); // Try different weight ranges - fonts support varying ranges - $weight_ranges = ['300..900', '300..800', '300..700', '400..700']; + // Also try single weight 400 for fonts that only have one weight + $weight_ranges = ['300..900', '300..800', '300..700', '400..700', '400']; foreach ($weight_ranges as $range) { if ($italic) { diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php index 171c1f8..d43fee8 100644 --- a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php +++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php @@ -144,6 +144,10 @@ class MLF_Font_Registry { return new WP_Error('not_ours', 'Cannot delete fonts not imported by this plugin'); } + // Get font info before deletion for cleanup + $settings = json_decode($family->post_content, true); + $font_slug = $settings['slug'] ?? $family->post_name; + // Get font faces (children) $faces = get_children([ 'post_parent' => $family_id, @@ -154,11 +158,11 @@ class MLF_Font_Registry { // Delete font face files and posts foreach ($faces as $face) { - $settings = json_decode($face->post_content, true); + $face_settings = json_decode($face->post_content, true); - if (isset($settings['src'])) { + if (isset($face_settings['src'])) { // Convert file:. URL to path - $src = $settings['src']; + $src = $face_settings['src']; $src = str_replace('file:./', '', $src); $file_path = trailingslashit($font_dir['path']) . basename($src); @@ -176,12 +180,83 @@ class MLF_Font_Registry { // Delete family post wp_delete_post($family_id, true); + // Clean up global styles references to this font + $this->remove_font_from_global_styles($font_slug); + // Clear all font-related caches $this->clear_font_caches(); return true; } + /** + * Remove references to a deleted font from global styles. + * + * This prevents block errors when a font is deleted but still referenced. + * + * @param string $font_slug The font slug to remove. + */ + private function remove_font_from_global_styles($font_slug) { + // Get the global styles post for the current theme + $global_styles = get_posts([ + 'post_type' => 'wp_global_styles', + 'posts_per_page' => 1, + 'post_status' => 'publish', + 'tax_query' => [ + [ + 'taxonomy' => 'wp_theme', + 'field' => 'name', + 'terms' => get_stylesheet(), + ], + ], + ]); + + if (empty($global_styles)) { + return; + } + + $global_styles_post = $global_styles[0]; + $content = json_decode($global_styles_post->post_content, true); + + if (empty($content)) { + return; + } + + $modified = false; + + // Helper function to recursively remove font references + $remove_font_refs = function (&$data) use ($font_slug, &$modified, &$remove_font_refs) { + if (!is_array($data)) { + return; + } + + foreach ($data as $key => &$value) { + // Check for fontFamily references using the slug pattern + if ($key === 'fontFamily' && is_string($value)) { + // WordPress uses format like: var(--wp--preset--font-family--font-slug) + if (strpos($value, '--font-family--' . $font_slug) !== false) { + unset($data[$key]); + $modified = true; + } + } + + // Check for typography.fontFamily in element/block styles + if (is_array($value)) { + $remove_font_refs($value); + } + } + }; + + $remove_font_refs($content); + + if ($modified) { + wp_update_post([ + 'ID' => $global_styles_post->ID, + 'post_content' => wp_json_encode($content), + ]); + } + } + /** * Clear all font-related caches. */ @@ -320,6 +395,82 @@ class MLF_Font_Registry { return $result; } + /** + * Get all fonts imported by this plugin, including actual filenames. + * + * This method includes the actual filename stored in the database, + * which is needed to correctly reference variable vs static font files. + * + * @return array Array of font data with filenames. + */ + public function get_imported_fonts_with_src() { + $fonts = get_posts([ + 'post_type' => 'wp_font_family', + 'posts_per_page' => 100, + 'post_status' => 'publish', + 'meta_key' => '_mlf_imported', + 'meta_value' => '1', + ]); + + if (empty($fonts)) { + return []; + } + + // Collect all font IDs for batch query + $font_ids = wp_list_pluck($fonts, 'ID'); + + // Single query to get ALL font faces for ALL fonts + $all_faces = get_posts([ + 'post_type' => 'wp_font_face', + 'posts_per_page' => 1000, + 'post_status' => 'publish', + 'post_parent__in' => $font_ids, + ]); + + // Group faces by parent font ID + $faces_by_font = []; + foreach ($all_faces as $face) { + $parent_id = $face->post_parent; + if (!isset($faces_by_font[$parent_id])) { + $faces_by_font[$parent_id] = []; + } + $faces_by_font[$parent_id][] = $face; + } + + $result = []; + + foreach ($fonts as $font) { + $settings = json_decode($font->post_content, true); + + // Get variants from pre-fetched data + $faces = $faces_by_font[$font->ID] ?? []; + + $variants = []; + foreach ($faces as $face) { + $face_settings = json_decode($face->post_content, true); + + // Extract filename from src (format: "file:./filename.woff2") + $src = $face_settings['src'] ?? ''; + $filename = str_replace('file:./', '', $src); + + $variants[] = [ + 'weight' => $face_settings['fontWeight'] ?? '400', + 'style' => $face_settings['fontStyle'] ?? 'normal', + 'filename' => $filename, + ]; + } + + $result[] = [ + 'id' => $font->ID, + 'name' => $settings['name'] ?? $font->post_title, + 'slug' => $settings['slug'] ?? $font->post_name, + 'variants' => $variants, + ]; + } + + return $result; + } + /** * Validate that a path is within the WordPress fonts directory. * diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-server.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-server.php new file mode 100644 index 0000000..eab2c36 --- /dev/null +++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-server.php @@ -0,0 +1,182 @@ +[a-zA-Z0-9_\-]+\.woff2)', [ + 'methods' => 'GET', + 'callback' => [$this, 'serve_font'], + 'permission_callback' => '__return_true', + 'args' => [ + 'filename' => [ + 'required' => true, + 'validate_callback' => [$this, 'validate_filename'], + ], + ], + ]); + + // Serve CSS file (like Google Fonts does) + register_rest_route('mlf/v1', '/css', [ + 'methods' => 'GET', + 'callback' => [$this, 'serve_css'], + 'permission_callback' => '__return_true', + ]); + } + + /** + * Validate font filename. + * + * @param string $filename The filename to validate. + * @return bool True if valid. + */ + public function validate_filename($filename) { + // Only allow alphanumeric, underscore, hyphen, and .woff2 extension + if (!preg_match('/^[a-zA-Z0-9_\-]+\.woff2$/', $filename)) { + return false; + } + + // Prevent directory traversal + if (strpos($filename, '..') !== false || strpos($filename, '/') !== false) { + return false; + } + + return true; + } + + /** + * Serve a font file with proper headers. + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|WP_Error Response or error. + */ + public function serve_font($request) { + $filename = $request->get_param('filename'); + + // Get font directory + $font_dir = wp_get_font_dir(); + $file_path = trailingslashit($font_dir['path']) . $filename; + + // Validate file exists and is within fonts directory + $real_path = realpath($file_path); + $fonts_path = realpath($font_dir['path']); + + if (!$real_path || !$fonts_path || strpos($real_path, $fonts_path) !== 0) { + return new WP_Error('not_found', 'Font not found', ['status' => 404]); + } + + if (!file_exists($real_path)) { + return new WP_Error('not_found', 'Font not found', ['status' => 404]); + } + + // Verify it's a woff2 file + if (pathinfo($real_path, PATHINFO_EXTENSION) !== 'woff2') { + return new WP_Error('invalid_type', 'Invalid file type', ['status' => 400]); + } + + // Read file content + $content = file_get_contents($real_path); + + if ($content === false) { + return new WP_Error('read_error', 'Could not read font file', ['status' => 500]); + } + + // Verify WOFF2 magic bytes + if (substr($content, 0, 4) !== 'wOF2') { + return new WP_Error('invalid_format', 'Invalid font format', ['status' => 400]); + } + + // Send font with proper headers + $response = new WP_REST_Response($content); + $response->set_status(200); + $response->set_headers([ + 'Content-Type' => 'font/woff2', + 'Content-Length' => strlen($content), + 'Access-Control-Allow-Origin' => '*', + 'Access-Control-Allow-Methods' => 'GET, OPTIONS', + 'Cross-Origin-Resource-Policy' => 'cross-origin', + 'Cache-Control' => 'public, max-age=31536000, immutable', + 'Vary' => 'Accept-Encoding', + ]); + + return $response; + } + + /** + * Serve CSS file with @font-face rules (mimics Google Fonts). + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response Response with CSS content. + */ + public function serve_css($request) { + $registry = new MLF_Font_Registry(); + $fonts = $registry->get_imported_fonts(); + + if (empty($fonts)) { + $response = new WP_REST_Response('/* No fonts installed */'); + $response->set_headers(['Content-Type' => 'text/css; charset=UTF-8']); + return $response; + } + + $css = "/* Maple Local Fonts - Generated CSS */\n\n"; + + foreach ($fonts as $font) { + $font_slug = sanitize_title($font['name']); + + foreach ($font['variants'] as $variant) { + $weight = $variant['weight']; + $style = $variant['style']; + + // Build filename + if (strpos($weight, ' ') !== false) { + $filename = sprintf('%s_%s_variable.woff2', $font_slug, $style); + } else { + $filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight); + } + + // Use REST API URL for font file + $font_url = rest_url('mlf/v1/font/' . $filename); + + // Generate @font-face rule (format like Google Fonts) + $css .= "/* {$style} {$weight} */\n"; + $css .= "@font-face {\n"; + $css .= " font-family: '{$font['name']}';\n"; + $css .= " font-style: {$style};\n"; + $css .= " font-weight: {$weight};\n"; + $css .= " font-display: swap;\n"; + $css .= " src: url({$font_url}) format('woff2');\n"; + $css .= "}\n\n"; + } + } + + $response = new WP_REST_Response($css); + $response->set_status(200); + $response->set_headers([ + 'Content-Type' => 'text/css; charset=UTF-8', + 'Access-Control-Allow-Origin' => '*', + 'Cache-Control' => 'public, max-age=86400', + ]); + + return $response; + } +} diff --git a/native/wordpress/maple-fonts-wp/maple-local-fonts.php b/native/wordpress/maple-fonts-wp/maple-local-fonts.php index 82cc2fe..ebb087e 100644 --- a/native/wordpress/maple-fonts-wp/maple-local-fonts.php +++ b/native/wordpress/maple-fonts-wp/maple-local-fonts.php @@ -32,6 +32,20 @@ define('MLF_MAX_CSS_SIZE', 512 * 1024); // 512KB max CSS response define('MLF_MAX_FONT_FILE_SIZE', 5 * 1024 * 1024); // 5MB max font file define('MLF_MAX_FONT_FACES', 20); // Max font faces per import (9 weights × 2 styles + buffer) +/** + * Ensure WOFF2 MIME type is registered (fixes Safari font loading). + * + * @param array $mimes Existing MIME types. + * @return array Modified MIME types. + */ +function mlf_add_woff2_mime_type($mimes) { + $mimes['woff2'] = 'font/woff2'; + $mimes['woff'] = 'font/woff'; + return $mimes; +} +add_filter('mime_types', 'mlf_add_woff2_mime_type'); + + /** * Check WordPress version on activation. */ @@ -50,9 +64,46 @@ function mlf_activate() { if (!file_exists($font_dir['path'])) { wp_mkdir_p($font_dir['path']); } + + // Create .htaccess for proper MIME types and CORS (Apache servers) + $htaccess_path = trailingslashit($font_dir['path']) . '.htaccess'; + if (!file_exists($htaccess_path)) { + $htaccess_content = "# Maple Local Fonts - Font MIME types and CORS\n"; + $htaccess_content .= "\n"; + $htaccess_content .= " AddType font/woff2 .woff2\n"; + $htaccess_content .= " AddType font/woff .woff\n"; + $htaccess_content .= "\n\n"; + $htaccess_content .= "\n"; + $htaccess_content .= " \n"; + $htaccess_content .= " Header set Access-Control-Allow-Origin \"*\"\n"; + $htaccess_content .= " \n"; + $htaccess_content .= "\n"; + + global $wp_filesystem; + if (empty($wp_filesystem)) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + } + + if ($wp_filesystem) { + $wp_filesystem->put_contents($htaccess_path, $htaccess_content, FS_CHMOD_FILE); + } + } + + // Flush rewrite rules for fonts CSS endpoint + mlf_add_rewrite_rules(); + flush_rewrite_rules(); } register_activation_hook(__FILE__, 'mlf_activate'); +/** + * Flush rewrite rules on deactivation. + */ +function mlf_deactivate() { + flush_rewrite_rules(); +} +register_deactivation_hook(__FILE__, 'mlf_deactivate'); + /** * Check WordPress version on admin init (in case WP was downgraded). */ @@ -64,6 +115,44 @@ function mlf_check_version() { } add_action('admin_init', 'mlf_check_version'); +/** + * Ensure fonts directory htaccess exists (for MIME types and CORS). + */ +function mlf_ensure_htaccess() { + $font_dir = wp_get_font_dir(); + $htaccess_path = trailingslashit($font_dir['path']) . '.htaccess'; + + // Always update htaccess to ensure latest headers + $htaccess_content = "# Maple Local Fonts - Font MIME types and CORS\n"; + $htaccess_content .= "# Required for cross-browser font loading including iOS Safari\n\n"; + $htaccess_content .= "\n"; + $htaccess_content .= " AddType font/woff2 .woff2\n"; + $htaccess_content .= " AddType font/woff .woff\n"; + $htaccess_content .= "\n\n"; + $htaccess_content .= "\n"; + $htaccess_content .= " \n"; + $htaccess_content .= " Header set Access-Control-Allow-Origin \"*\"\n"; + $htaccess_content .= " Header set Access-Control-Allow-Methods \"GET, OPTIONS\"\n"; + $htaccess_content .= " Header set Access-Control-Allow-Headers \"Origin, Content-Type\"\n"; + $htaccess_content .= " Header set Cross-Origin-Resource-Policy \"cross-origin\"\n"; + $htaccess_content .= " Header set Timing-Allow-Origin \"*\"\n"; + $htaccess_content .= " \n"; + $htaccess_content .= "\n"; + + if (file_exists($font_dir['path'])) { + global $wp_filesystem; + if (empty($wp_filesystem)) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + } + + if ($wp_filesystem) { + $wp_filesystem->put_contents($htaccess_path, $htaccess_content, FS_CHMOD_FILE); + } + } +} +add_action('admin_init', 'mlf_ensure_htaccess'); + /** * Display version notice. */ @@ -133,9 +222,41 @@ add_action('plugins_loaded', 'mlf_init', 20); function mlf_register_rest_routes() { $controller = new MLF_Rest_Controller(); $controller->register_routes(); + + // Register font server routes for cross-browser compatibility + $font_server = new MLF_Font_Server(); + $font_server->register_routes(); } add_action('rest_api_init', 'mlf_register_rest_routes'); +/** + * Check if we should use REST API for font serving (compatibility mode). + * + * @return bool True if using REST API font serving. + */ +function mlf_use_rest_font_serving() { + return get_option('mlf_compatibility_mode', true); // Default to true for maximum compatibility +} + +/** + * Get the URL for a font file. + * + * Uses REST API endpoint for compatibility, or direct file URL for performance. + * + * @param string $filename The font filename. + * @return string The font URL. + */ +function mlf_get_font_url($filename) { + if (mlf_use_rest_font_serving()) { + // Use REST API endpoint (guaranteed proper headers) + return rest_url('mlf/v1/font/' . $filename); + } else { + // Use direct file URL (faster but depends on server config) + $font_dir = wp_get_font_dir(); + return trailingslashit($font_dir['url']) . $filename; + } +} + /** * Get the required capability for managing fonts. * @@ -154,20 +275,24 @@ function mlf_get_capability() { /** * Add imported fonts to the theme.json typography settings. * - * This makes fonts appear in the Site Editor typography dropdown. + * This makes fonts appear in the Site Editor typography dropdown + * and generates the @font-face CSS for the frontend. * * @param WP_Theme_JSON_Data $theme_json The theme.json data. * @return WP_Theme_JSON_Data Modified theme.json data. */ function mlf_add_fonts_to_theme_json($theme_json) { $registry = new MLF_Font_Registry(); - $fonts = $registry->get_imported_fonts(); + $fonts = $registry->get_imported_fonts_with_src(); if (empty($fonts)) { return $theme_json; } + // Build our font families $font_families = []; + $font_dir = wp_get_font_dir(); + $font_base_url = trailingslashit($font_dir['url']); foreach ($fonts as $font) { $font_faces = []; @@ -175,24 +300,17 @@ function mlf_add_fonts_to_theme_json($theme_json) { foreach ($font['variants'] as $variant) { $weight = $variant['weight']; $style = $variant['style']; + $filename = $variant['filename']; - // Build filename based on our naming convention - $font_slug = sanitize_title($font['name']); - if (strpos($weight, ' ') !== false) { - // Variable font - $filename = sprintf('%s_%s_variable.woff2', $font_slug, $style); - } else { - $filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight); - } - - $font_dir = wp_get_font_dir(); - $font_url = trailingslashit($font_dir['url']) . $filename; + // Use direct file URL + $font_url = $font_base_url . $filename; $font_faces[] = [ - 'fontFamily' => $font['name'], - 'fontWeight' => $weight, - 'fontStyle' => $style, - 'src' => [$font_url], + 'fontFamily' => $font['name'], + 'fontWeight' => $weight, + 'fontStyle' => $style, + 'fontDisplay' => 'swap', + 'src' => [$font_url], ]; } @@ -204,6 +322,7 @@ function mlf_add_fonts_to_theme_json($theme_json) { ]; } + // Use update_with to merge - WordPress handles the merging logic $new_data = [ 'version' => 2, 'settings' => [ @@ -215,7 +334,172 @@ function mlf_add_fonts_to_theme_json($theme_json) { return $theme_json->update_with($new_data); } -add_filter('wp_theme_json_data_user', 'mlf_add_fonts_to_theme_json'); +add_filter('wp_theme_json_data_user', 'mlf_add_fonts_to_theme_json', 10); + +/** + * Generate @font-face CSS for imported fonts. + * + * @return string CSS content. + */ +function mlf_get_font_face_css() { + $registry = new MLF_Font_Registry(); + $fonts = $registry->get_imported_fonts(); + + if (empty($fonts)) { + return ''; + } + + $css = ''; + + foreach ($fonts as $font) { + $font_slug = sanitize_title($font['name']); + // Escape font name for CSS (handle quotes and special chars) + $css_font_name = addcslashes($font['name'], "'\\"); + + foreach ($font['variants'] as $variant) { + $weight = $variant['weight']; + $style = $variant['style']; + + // Build filename based on our naming convention + if (strpos($weight, ' ') !== false) { + // Variable font + $filename = sprintf('%s_%s_variable.woff2', $font_slug, $style); + } else { + $filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight); + } + + // Get font URL (uses REST API or direct based on setting) + $font_url = mlf_get_font_url($filename); + + // Ensure HTTPS if site uses HTTPS (important for reverse proxy setups) + if (is_ssl() && strpos($font_url, 'http://') === 0) { + $font_url = str_replace('http://', 'https://', $font_url); + } + + $css .= "@font-face {"; + $css .= "font-family: '{$css_font_name}';"; + $css .= "font-style: {$style};"; + $css .= "font-weight: {$weight};"; + $css .= "font-display: swap;"; + $css .= "src: url('" . esc_url($font_url) . "') format('woff2');"; + $css .= "}\n"; + } + } + + return $css; +} + +/** + * Enqueue font face CSS on frontend. + * + * Uses external CSS file just like Google Fonts does. + */ +function mlf_enqueue_font_faces() { + $registry = new MLF_Font_Registry(); + $fonts = $registry->get_imported_fonts(); + + if (empty($fonts)) { + return; + } + + // Load CSS from custom endpoint (like Google Fonts serves CSS) + wp_enqueue_style( + 'mlf-local-fonts', + home_url('/mlf-fonts.css'), + [], + null // No version string, like Google Fonts + ); +} +add_action('wp_enqueue_scripts', 'mlf_enqueue_font_faces'); + +/** + * Enqueue font face CSS in block editor. + */ +function mlf_enqueue_editor_font_faces() { + $registry = new MLF_Font_Registry(); + $fonts = $registry->get_imported_fonts(); + + if (empty($fonts)) { + return; + } + + // Load CSS from custom endpoint (like Google Fonts serves CSS) + wp_enqueue_style( + 'mlf-local-fonts-editor', + home_url('/mlf-fonts.css'), + [], + null + ); +} +add_action('enqueue_block_editor_assets', 'mlf_enqueue_editor_font_faces'); + +/** + * Add rewrite rule for fonts CSS endpoint. + */ +function mlf_add_rewrite_rules() { + add_rewrite_rule('^mlf-fonts\.css$', 'index.php?mlf_fonts_css=1', 'top'); +} +add_action('init', 'mlf_add_rewrite_rules'); + +/** + * Add query var for fonts CSS. + */ +function mlf_add_query_vars($vars) { + $vars[] = 'mlf_fonts_css'; + return $vars; +} +add_filter('query_vars', 'mlf_add_query_vars'); + +/** + * Handle fonts CSS request. + */ +function mlf_handle_fonts_css_request() { + if (!get_query_var('mlf_fonts_css')) { + return; + } + + // Send CSS headers + header('Content-Type: text/css; charset=UTF-8'); + header('Access-Control-Allow-Origin: *'); + header('Cache-Control: public, max-age=86400'); + header('X-Content-Type-Options: nosniff'); + + $registry = new MLF_Font_Registry(); + $fonts = $registry->get_imported_fonts_with_src(); + + if (empty($fonts)) { + echo "/* No fonts installed */\n"; + exit; + } + + echo "/* Maple Local Fonts */\n\n"; + + // Use direct font file URLs + $font_dir = wp_get_font_dir(); + $font_base_url = trailingslashit($font_dir['url']); + + foreach ($fonts as $font) { + foreach ($font['variants'] as $variant) { + $weight = $variant['weight']; + $style = $variant['style']; + + // Use the actual filename from database (stored in src) + $filename = $variant['filename']; + $file_url = $font_base_url . $filename; + + echo "@font-face {\n"; + echo " font-family: '{$font['name']}';\n"; + echo " font-style: {$style};\n"; + echo " font-weight: {$weight};\n"; + echo " font-display: swap;\n"; + echo " src: url({$file_url}) format('woff2');\n"; + echo "}\n\n"; + } + } + + exit; +} +add_action('template_redirect', 'mlf_handle_fonts_css_request'); /** * Register admin menu.