From ea6fde8f2e9ec2d397ee9070327cbbd881c14721 Mon Sep 17 00:00:00 2001 From: Lee Date: Thu, 9 Oct 2025 00:28:25 -0500 Subject: [PATCH 1/4] Font Processing for Custom Fonts --- CONTRIBUTING.md | 4 +- inc/setup/dynamic-theme-json.php | 23 +-------- tools/font-processor.php | 44 ++++++++++------- tools/generate-theme-json.php | 39 ++++++++------- tools/helpers.php | 83 ++++++++++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 58 deletions(-) create mode 100644 tools/helpers.php diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00273878..476c947d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,8 +5,8 @@ WebDevStudios welcomes contributions and bug fixes from third-parties. Here are - Create an [Issue](https://github.com/WebDevStudios/wds-bt/issues) so we can all discuss your idea - Fork wds-bt - Create a feature/hotfix branch off [main](https://github.com/WebDevStudios/wds-bt/tree/main) -- Commit code changes to your feature/hotifx branch -- Continue to merge master into your feature/hotifx branch so it stays current +- Commit code changes to your feature/hotfix branch +- Continue to merge master into your feature/hotfix branch so it stays current - Test across all major browsers - Accessibility testing (both WCAG 2.2AA and Section 508) - Must pass PHPCS, ESLint, and Stylelint assertions diff --git a/inc/setup/dynamic-theme-json.php b/inc/setup/dynamic-theme-json.php index c26c128a..11f8f972 100644 --- a/inc/setup/dynamic-theme-json.php +++ b/inc/setup/dynamic-theme-json.php @@ -10,6 +10,8 @@ use RecursiveIteratorIterator; use RecursiveDirectoryIterator; +require_once __DIR__ . '/helpers.php'; + /** * Get all available font files from a directory. * @@ -202,28 +204,7 @@ function group_fonts_by_family( $fonts ) { return $grouped; } -/** - * Map font family to standardized slug. - * - * @param string $family Font family name. - * @return string Standardized slug. - */ -function get_font_slug( $family ) { - $slug_mapping = array( - 'Oxygen' => 'body', - 'Inter' => 'headline', - 'Roboto Mono' => 'mono', - 'Open Sans' => 'body', - 'Lato' => 'body', - 'Poppins' => 'headline', - 'Montserrat' => 'headline', - 'Raleway' => 'headline', - 'Playfair Display' => 'headline', - 'Roboto' => 'body', - ); - return $slug_mapping[ $family ] ?? sanitize_title( $family ); -} /** * Filter theme.json data to include dynamically detected fonts. diff --git a/tools/font-processor.php b/tools/font-processor.php index a8a44db2..5ee0d402 100644 --- a/tools/font-processor.php +++ b/tools/font-processor.php @@ -22,6 +22,8 @@ namespace WebDevStudios\wdsbt; +require_once __DIR__ . '/helpers.php'; + // Configuration. $wdsbt_config = array( 'input_dir' => 'assets/fonts', @@ -93,6 +95,13 @@ function scan_font_files( $input_dir ) { new \RecursiveDirectoryIterator( $full_path, \RecursiveDirectoryIterator::SKIP_DOTS ) ); + // Keywords to ignore when detecting family name from filename. + $ignore_keywords = [ + '100','200','300','400','500','600','700','800','900', + 'thin','extralight','light','regular','medium','semibold', + 'bold','extrabold','black','italic','oblique','normal' + ]; + foreach ( $iterator as $file ) { if ( $file->isFile() && in_array( strtolower( $file->getExtension() ), array( 'woff2', 'woff', 'ttf', 'otf' ), true ) ) { $relative_path = str_replace( $theme_dir . '/', '', $file->getPathname() ); @@ -102,7 +111,18 @@ function scan_font_files( $input_dir ) { $folder_name = basename( dirname( $file->getPathname() ) ); $font_metadata = parse_font_filename( $filename ); - // Always use the detected family from the filename. The folder name is only used for purpose. + // Smart fallback for Unknown family + if ( 'Unknown' === $font_metadata['family'] ) { + // Remove extension + $base = preg_replace('/\.[^.]+$/','', $filename); + // Split by - or _ + $parts = preg_split('/[-_]/', $base); + // Keep parts that are not in ignore_keywords + $clean_parts = array_filter($parts, function($part) use ($ignore_keywords) { + return ! in_array(strtolower($part), $ignore_keywords, true); + }); + $font_metadata['family'] = ucwords( implode(' ', $clean_parts) ); + } $fonts[] = array( 'path' => $file->getPathname(), @@ -216,7 +236,8 @@ function copy_fonts_to_output( $fonts, $output_dir ) { } foreach ( $fonts as $font ) { - $standardized_slug = get_font_slug( $font['family'] ); + //$standardized_slug = get_font_slug( $font['family'] ); + $standardized_slug = get_font_slug( $font['path'] ); $family_dir = $full_output_dir . '/' . $standardized_slug; if ( ! is_dir( $family_dir ) ) { @@ -313,21 +334,6 @@ function generate_font_preload( $fonts, $output_file ) { } } -/** - * Map font family to standardized slug. - * - * @param string $family Font family name. - * @return string Standardized slug. - */ -function get_font_slug( $family ) { - $slug_mapping = array( - 'Oxygen' => 'body', - 'Inter' => 'headline', - 'Roboto Mono' => 'mono', - ); - - return $slug_mapping[ $family ] ?? sanitize_title( $family ); -} /** * Main function. @@ -335,6 +341,9 @@ function get_font_slug( $family ) { function main() { global $wdsbt_config; + // Include the theme.json generator at the start of this function to ensure it's available. + include_once __DIR__ . '/generate-theme-json.php'; + printf( "Font Processor Tool\n" ); printf( "=====================\n\n" ); @@ -388,7 +397,6 @@ function main() { // Update theme.json. printf( "\nUpdating theme.json...\n" ); - include_once __DIR__ . '/generate-theme-json.php'; generate_theme_json(); printf( "\nFont processing complete!\n" ); diff --git a/tools/generate-theme-json.php b/tools/generate-theme-json.php index 6f02836a..c41baa22 100644 --- a/tools/generate-theme-json.php +++ b/tools/generate-theme-json.php @@ -9,6 +9,8 @@ namespace WebDevStudios\wdsbt; +require_once __DIR__ . '/helpers.php'; + /** * Scan directory for font files. * @@ -20,10 +22,18 @@ function scan_font_directory( $directory ) { $theme_dir = dirname( __DIR__, 1 ); $full_path = $theme_dir . '/' . $directory; + if ( ! is_dir( $full_path ) ) { return $fonts; } + // Keywords to ignore when detecting font family from filename. + $ignore_keywords = [ + '100','200','300','400','500','600','700','800','900', + 'thin','extralight','light','regular','medium','semibold', + 'bold','extrabold','black','italic','oblique','normal' + ]; + $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $full_path, \RecursiveDirectoryIterator::SKIP_DOTS ) ); @@ -34,10 +44,22 @@ function scan_font_directory( $directory ) { $filename = $file->getBasename(); $font_metadata = parse_font_filename( $filename ); + // Detect font family from folder name (headline, body, mono). $folder_name = basename( dirname( $file->getPathname() ) ); // Always use the detected family from the filename. The folder name is only used for purpose. + if ( 'Unknown' === $font_metadata['family'] ) { + // Remove extension + $base = preg_replace('/\.[^.]+$/','', $filename); + // Split by - or _ + $parts = preg_split('/[-_]/', $base); + // Keep parts that are not in ignore_keywords + $clean_parts = array_filter($parts, function($part) use ($ignore_keywords) { + return ! in_array(strtolower($part), $ignore_keywords, true); + }); + $font_metadata['family'] = ucwords( implode(' ', $clean_parts) ); + } $variant_key = $font_metadata['family'] . '-' . $font_metadata['weight'] . '-' . $font_metadata['style']; @@ -211,23 +233,6 @@ function sanitize_title( $title ) { return trim( $title, '-' ); } -if ( ! function_exists( __NAMESPACE__ . '\\get_font_slug' ) ) { - /** - * Map font family to standardized slug. - * - * @param string $family Font family name. - * @return string Standardized slug. - */ - function get_font_slug( $family ) { - $slug_mapping = array( - 'Oxygen' => 'body', - 'Inter' => 'headline', - 'Roboto Mono' => 'mono', - ); - - return $slug_mapping[ $family ] ?? sanitize_title( $family ); - } -} /** * Generate theme.json with detected fonts. diff --git a/tools/helpers.php b/tools/helpers.php new file mode 100644 index 00000000..98ce9d77 --- /dev/null +++ b/tools/helpers.php @@ -0,0 +1,83 @@ +#!/usr/bin/env php + 'body', + 'Inter' => 'headline', + 'Roboto Mono' => 'mono', + ); + + if ( is_array( $family_or_path ) ) { + if ( ! empty( $family_or_path['path'] ) ) { + $candidate = $family_or_path['path']; + } elseif ( ! empty( $family_or_path['family'] ) ) { + $candidate = $family_or_path['family']; + } else { + $candidate = ''; + } + } else { + $candidate = (string) $family_or_path; + } + + // Normalize separators + $normalized = str_replace( '\\', '/', $candidate ); + + // Folder-based slug + if ( false !== strpos( $normalized, '/fonts/' ) ) { + $after = explode( '/fonts/', $normalized, 2 )[1]; + $segments = explode( '/', trim( $after, '/' ) ); + if ( ! empty( $segments[0] ) ) { + return sanitize_title( $segments[0] ); + } + } + + // Family name mapping + foreach ( $slug_mapping as $key => $value ) { + if ( 0 === strcasecmp( $candidate, $key ) ) { + return $value; + } + } + + // Fallback: strip extension from filename and sanitize + $base = $normalized; + if ( false !== strpos( $base, '/' ) ) { + $base = basename( $base ); + } + $base = preg_replace( '/\.[^.]+$/', '', $base ); + + return ! empty( $base ) ? sanitize_title( $base ) : 'unknown'; + } +} + +/** + * Sanitize a string into a URL/title-safe slug. + * + * @param string $text + * @return string + */ +if ( ! function_exists( __NAMESPACE__ . '\\sanitize_title' ) ) { + function sanitize_title( $text ) { + $text = strtolower( trim( $text ) ); + $text = preg_replace( '/[^a-z0-9]+/', '-', $text ); + $text = trim( $text, '-' ); + return $text; + } +} From 807602c424a924970d18645bc938153db7dcec85 Mon Sep 17 00:00:00 2001 From: Lee Date: Fri, 10 Oct 2025 01:23:50 -0500 Subject: [PATCH 2/4] Current version --- tools/font-detection.php | 91 +--------------------- tools/font-processor.php | 82 +------------------- tools/generate-theme-json.php | 2 +- tools/helpers.php | 138 ++++++++++++++++++++++++++++++++-- 4 files changed, 138 insertions(+), 175 deletions(-) diff --git a/tools/font-detection.php b/tools/font-detection.php index 0975d172..d4556f48 100644 --- a/tools/font-detection.php +++ b/tools/font-detection.php @@ -8,92 +8,7 @@ // Prefix: wdsbt_. -/** - * Parse font filename. - * - * @param string $filename Font filename. - * @return array Font metadata. - */ -function wdsbt_parse_font_filename( $filename ) { - $metadata = array( - 'family' => 'Unknown', - 'weight' => '400', - 'style' => 'normal', - ); - - $family_patterns = array( - 'inter' => 'Inter', - 'oxygen' => 'Oxygen', - 'roboto-mono' => 'Roboto Mono', - 'roboto' => 'Roboto', - 'open-sans' => 'Open Sans', - 'lato' => 'Lato', - 'poppins' => 'Poppins', - 'montserrat' => 'Montserrat', - 'raleway' => 'Raleway', - 'playfair' => 'Playfair Display', - ); - - $weight_patterns = array( - '-100' => '100', - '-200' => '200', - '-300' => '300', - '-regular' => '400', - '-normal' => '400', - '-400' => '400', - '-500' => '500', - '-600' => '600', - '-700' => '700', - '-800' => '800', - '-900' => '900', - 'thin' => '100', - 'extralight' => '200', - 'light' => '300', - 'regular' => '400', - 'medium' => '500', - 'semibold' => '600', - 'bold' => '700', - 'extrabold' => '800', - 'black' => '900', - ); - - $style_patterns = array( - 'italic' => 'italic', - 'oblique' => 'oblique', - ); - - $lowercase_filename = strtolower( $filename ); - - foreach ( $family_patterns as $pattern => $family ) { - if ( strpos( $lowercase_filename, $pattern ) !== false ) { - $metadata['family'] = $family; - break; - } - } - - if ( 'Unknown' === $metadata['family'] ) { - $parts = preg_split( '/[-_\s]+/', $filename ); - if ( ! empty( $parts[0] ) ) { - $metadata['family'] = ucwords( str_replace( '-', ' ', $parts[0] ) ); - } - } - - foreach ( $weight_patterns as $pattern => $weight ) { - if ( strpos( $lowercase_filename, $pattern ) !== false ) { - $metadata['weight'] = $weight; - break; - } - } - - foreach ( $style_patterns as $pattern => $style ) { - if ( strpos( $lowercase_filename, $pattern ) !== false ) { - $metadata['style'] = $style; - break; - } - } - - return $metadata; -} +require_once __DIR__ . '/helpers.php'; /** * Scan directory for font files. @@ -119,7 +34,7 @@ function wdsbt_scan_font_directory( $directory ) { $relative_path = str_replace( $theme_dir . '/', '', $file->getPathname() ); $extension = $file->getExtension(); $filename = $file->getBasename( '.' . $extension ); - $font_metadata = wdsbt_parse_font_filename( $filename ); + $font_metadata = wdsbt_parse_font_meta_from_filename( $filename ); $fonts[] = array( 'path' => $file->getPathname(), 'relative_path' => $relative_path, @@ -147,7 +62,7 @@ function wdsbt_print_fonts( $fonts, $label = 'Fonts' ) { foreach ( $fonts as $font ) { printf( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CLI output, escaping not required. - " - %s.%s | %s %s %s\n", + " - %s.%s | %s | Weight: %s | Style: %s\n", $font['filename'], // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CLI output $font['extension'], // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CLI output $font['family'], // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CLI output diff --git a/tools/font-processor.php b/tools/font-processor.php index 5ee0d402..dc1dfe78 100644 --- a/tools/font-processor.php +++ b/tools/font-processor.php @@ -139,85 +139,7 @@ function scan_font_files( $input_dir ) { return $fonts; } -/** - * Parse font filename. - * - * @param string $filename Font filename to parse. - * @return array Font metadata. - */ -function parse_font_filename( $filename ) { - $metadata = array( - 'family' => 'Unknown', - 'weight' => '400', - 'style' => 'normal', - ); - - $family_patterns = array( - 'inter' => 'Inter', - 'oxygen' => 'Oxygen', - 'roboto-mono' => 'Roboto Mono', - 'roboto' => 'Roboto', - 'open-sans' => 'Open Sans', - 'lato' => 'Lato', - 'poppins' => 'Poppins', - 'montserrat' => 'Montserrat', - 'raleway' => 'Raleway', - 'playfair' => 'Playfair Display', - ); - - $weight_patterns = array( - '-100' => '100', - '-200' => '200', - '-300' => '300', - '-regular' => '400', - '-normal' => '400', - '-400' => '400', - '-500' => '500', - '-600' => '600', - '-700' => '700', - '-800' => '800', - '-900' => '900', - 'thin' => '100', - 'extralight' => '200', - 'light' => '300', - 'regular' => '400', - 'medium' => '500', - 'semibold' => '600', - 'bold' => '700', - 'extrabold' => '800', - 'black' => '900', - ); - - $style_patterns = array( - 'italic' => 'italic', - 'oblique' => 'oblique', - ); - - $lowercase_filename = strtolower( $filename ); - foreach ( $family_patterns as $pattern => $family ) { - if ( strpos( $lowercase_filename, $pattern ) !== false ) { - $metadata['family'] = $family; - break; - } - } - - foreach ( $weight_patterns as $pattern => $weight ) { - if ( strpos( $lowercase_filename, $pattern ) !== false ) { - $metadata['weight'] = $weight; - break; - } - } - - foreach ( $style_patterns as $pattern => $style ) { - if ( strpos( $lowercase_filename, $pattern ) !== false ) { - $metadata['style'] = $style; - break; - } - } - - return $metadata; -} /** * Copy fonts to output directory. @@ -237,7 +159,7 @@ function copy_fonts_to_output( $fonts, $output_dir ) { foreach ( $fonts as $font ) { //$standardized_slug = get_font_slug( $font['family'] ); - $standardized_slug = get_font_slug( $font['path'] ); + $standardized_slug = wdsbt_get_font_slug( $font['path'] ); $family_dir = $full_output_dir . '/' . $standardized_slug; if ( ! is_dir( $family_dir ) ) { @@ -308,7 +230,7 @@ function generate_font_preload( $fonts, $output_file ) { foreach ( $preloaded as $font ) { $format = 'woff2' === $font['extension'] ? 'font/woff2' : 'font/woff'; - $standardized_slug = get_font_slug( $font['family'] ); + $standardized_slug = wdsbt_get_font_slug( $font['family'] ); $php .= " 'fonts/{$standardized_slug}/{$font['filename']}' => '{$format}',\n"; } diff --git a/tools/generate-theme-json.php b/tools/generate-theme-json.php index c41baa22..74bab9ff 100644 --- a/tools/generate-theme-json.php +++ b/tools/generate-theme-json.php @@ -203,7 +203,7 @@ function group_fonts_by_family( $fonts ) { if ( ! isset( $grouped[ $family ] ) ) { $grouped[ $family ] = array( 'name' => $family, - 'slug' => get_font_slug( $family ), + 'slug' => wdsbt_get_font_slug( $family ), 'fontFamily' => $family . ', sans-serif', 'fontFace' => array(), ); diff --git a/tools/helpers.php b/tools/helpers.php index 98ce9d77..6d445999 100644 --- a/tools/helpers.php +++ b/tools/helpers.php @@ -17,8 +17,8 @@ * @return string Standardized slug (e.g. 'body', 'headline', 'mono', or sanitized name). */ -if ( ! function_exists( __NAMESPACE__ . '\\get_font_slug' ) ) { - function get_font_slug( $family_or_path ) { +if ( ! function_exists( __NAMESPACE__ . '\\wdsbt_get_font_slug' ) ) { + function wdsbt_get_font_slug( $family_or_path ) { static $slug_mapping = array( 'Oxygen' => 'body', 'Inter' => 'headline', @@ -45,7 +45,7 @@ function get_font_slug( $family_or_path ) { $after = explode( '/fonts/', $normalized, 2 )[1]; $segments = explode( '/', trim( $after, '/' ) ); if ( ! empty( $segments[0] ) ) { - return sanitize_title( $segments[0] ); + return wdsbt_sanitize_title( $segments[0] ); } } @@ -63,21 +63,147 @@ function get_font_slug( $family_or_path ) { } $base = preg_replace( '/\.[^.]+$/', '', $base ); - return ! empty( $base ) ? sanitize_title( $base ) : 'unknown'; + return ! empty( $base ) ? wdsbt_sanitize_title( $base ) : 'unknown'; } } + + + /** * Sanitize a string into a URL/title-safe slug. * * @param string $text * @return string */ -if ( ! function_exists( __NAMESPACE__ . '\\sanitize_title' ) ) { - function sanitize_title( $text ) { +if ( ! function_exists( __NAMESPACE__ . '\\wdsbt_sanitize_title' ) ) { + function wdsbt_sanitize_title( $text ) { $text = strtolower( trim( $text ) ); $text = preg_replace( '/[^a-z0-9]+/', '-', $text ); $text = trim( $text, '-' ); return $text; } } + + +/** + * Parse font filename. + * + * @param string $filename Font filename to parse. + * @return array Font metadata. + */ +if ( ! function_exists( __NAMESPACE__ . '\\wdsbt_parse_font_meta_from_filename' ) ) { + + function wdsbt_parse_font_meta_from_filename( $filename ) { + + $metadata = array( + 'family' => 'Unknown', + 'weight' => '400', + 'style' => 'normal', + ); + + $family_patterns = array( + 'inter' => 'Inter', + 'oxygen' => 'Oxygen', + 'roboto-mono' => 'Roboto Mono', + 'roboto' => 'Roboto', + 'open-sans' => 'Open Sans', + 'lato' => 'Lato', + 'poppins' => 'Poppins', + 'montserrat' => 'Montserrat', + 'raleway' => 'Raleway', + 'playfair' => 'Playfair Display', + ); + + $weight_patterns = array( + '-100' => '100', + '-200' => '200', + '-300' => '300', + '-regular' => '400', + '-normal' => '400', + '-400' => '400', + '-500' => '500', + '-600' => '600', + '-700' => '700', + '-800' => '800', + '-900' => '900', + 'thin' => '100', + 'extralight' => '200', + 'light' => '300', + 'regular' => '400', + 'medium' => '500', + 'semibold' => '600', + 'bold' => '700', + 'extrabold' => '800', + 'black' => '900', + ); + + $style_patterns = array( + 'italic' => 'italic', + 'oblique' => 'oblique', + ); + + // Create a list of keywords to ignore when parsing the family name in the case of unknown fonts + // This includes all keys from the family, weight, and style patterns + // Normalize by removing dashes and leading hyphens + $all_keys = array_merge( + array_keys($family_patterns), + array_keys($weight_patterns), + array_keys($style_patterns) + ); + + $ignore_keywords = array_map(function($key) { + return ltrim(str_replace('-', '', $key), '-'); + }, $all_keys); + + // Remove duplicates and re-index the array + $ignore_keywords = array_unique($ignore_keywords); + $ignore_keywords = array_values($ignore_keywords); + + + + $lowercase_filename = strtolower( $filename ); + + foreach ( $family_patterns as $pattern => $family ) { + if ( strpos( $lowercase_filename, $pattern ) !== false ) { + $metadata['family'] = $family; + break; + } + } + + // If family name is not in the above list, try to extract from filename + // This accounts for multi-part names and ignores common weight/style keywords + if ( 'Unknown' === $metadata['family'] ) { + // Split filename into parts (by -, _, or space) + $parts = preg_split( '/[-_\s]+/', strtolower( $filename ) ); + + if ( ! empty( $parts ) ) { + // Remove ignored words + $filtered = array_filter( $parts, function( $part ) use ( $ignore_keywords ) { + return ! in_array( $part, $ignore_keywords, true ); + }); + + if ( ! empty( $filtered ) ) { + // Combine remaining parts and capitalize properly + $metadata['family'] = ucwords( implode( ' ', $filtered ) ); + } + } + } + + foreach ( $weight_patterns as $pattern => $weight ) { + if ( strpos( $lowercase_filename, $pattern ) !== false ) { + $metadata['weight'] = $weight; + break; + } + } + + foreach ( $style_patterns as $pattern => $style ) { + if ( strpos( $lowercase_filename, $pattern ) !== false ) { + $metadata['style'] = $style; + break; + } + } + + return $metadata; + } +} From b0daaa4f456df36e2aa65a3db75cbd9f65e7bd3a Mon Sep 17 00:00:00 2001 From: Lee Date: Tue, 6 Jan 2026 00:09:32 -0600 Subject: [PATCH 3/4] Progress on font pipeline Making progress on the font pipeline. Consolidating several duplicate functions into a helpers file, and abstracting functionality. --- .../body/silly-font-name-900-italic.woff2 | Bin 0 -> 16348 bytes inc/setup/dynamic-theme-json.php | 200 +++--- tools/font-detection.php | 13 +- tools/font-generator.php | 613 ------------------ tools/font-pipeline-config.php | 0 tools/font-processor.php | 6 +- tools/generate-theme-json.php | 176 +---- tools/helpers.php | 186 +++++- 8 files changed, 315 insertions(+), 879 deletions(-) create mode 100644 assets/fonts/body/silly-font-name-900-italic.woff2 delete mode 100644 tools/font-generator.php create mode 100644 tools/font-pipeline-config.php diff --git a/assets/fonts/body/silly-font-name-900-italic.woff2 b/assets/fonts/body/silly-font-name-900-italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b610f33fd18631fe531ffff54fcc89ac8f29c473 GIT binary patch literal 16348 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 ekD1#U{6yl3%sbT^bh~b=>3+yOaTDrK7MHc literal 0 HcmV?d00001 diff --git a/inc/setup/dynamic-theme-json.php b/inc/setup/dynamic-theme-json.php index 11f8f972..816b86a9 100644 --- a/inc/setup/dynamic-theme-json.php +++ b/inc/setup/dynamic-theme-json.php @@ -43,7 +43,7 @@ function scan_font_directory( $directory ) { $filename = $file->getBasename( '.' . $file->getExtension() ); // Parse font metadata from filename. - $font_metadata = parse_font_filename( $filename ); + $font_metadata = wdsbt_parse_font_meta_from_filename( $filename ); // Create a unique key for this font variant. $variant_key = $font_metadata['family'] . '-' . $font_metadata['weight'] . '-' . $font_metadata['style']; @@ -72,105 +72,105 @@ function scan_font_directory( $directory ) { * @param string $filename Font filename. * @return array Font metadata. */ -function parse_font_filename( $filename ) { - // Default values. - $metadata = array( - 'family' => 'Unknown', - 'weight' => '400', - 'style' => 'normal', - ); - - // Common font family patterns. - $family_patterns = array( - 'inter' => 'Inter', - 'oxygen' => 'Oxygen', - 'roboto-mono' => 'Roboto Mono', - 'roboto' => 'Roboto', - 'open-sans' => 'Open Sans', - 'lato' => 'Lato', - 'poppins' => 'Poppins', - 'montserrat' => 'Montserrat', - 'raleway' => 'Raleway', - 'playfair' => 'Playfair Display', - 'source-sans' => 'Source Sans Pro', - 'noto-sans' => 'Noto Sans', - 'nunito' => 'Nunito', - 'merriweather' => 'Merriweather', - 'ubuntu' => 'Ubuntu', - 'oswald' => 'Oswald', - ); - - // Common weight patterns with exact matches. - $weight_patterns = array( - '-100' => '100', - '-200' => '200', - '-300' => '300', - '-regular' => '400', - '-normal' => '400', - '-400' => '400', - '-500' => '500', - '-600' => '600', - '-700' => '700', - '-800' => '800', - '-900' => '900', - 'thin' => '100', - 'extralight' => '200', - 'light' => '300', - 'regular' => '400', - 'medium' => '500', - 'semibold' => '600', - 'bold' => '700', - 'extrabold' => '800', - 'black' => '900', - ); - - // Common style patterns. - $style_patterns = array( - 'italic' => 'italic', - 'oblique' => 'oblique', - ); - - $lowercase_filename = strtolower( $filename ); - - // Detect font family - use the longest matching pattern. - $matched_family = ''; - $longest_match = 0; - foreach ( $family_patterns as $pattern => $family ) { - if ( strpos( $lowercase_filename, $pattern ) !== false && strlen( $pattern ) > $longest_match ) { - $matched_family = $family; - $longest_match = strlen( $pattern ); - } - } - if ( $matched_family ) { - $metadata['family'] = $matched_family; - } - - // If no family detected, try to extract from filename. - if ( 'Unknown' === $metadata['family'] ) { - $parts = preg_split( '/[-_\s]+/', $filename ); - if ( ! empty( $parts[0] ) ) { - $metadata['family'] = ucwords( str_replace( '-', ' ', $parts[0] ) ); - } - } - - // Detect font weight - use exact matches. - foreach ( $weight_patterns as $pattern => $weight ) { - if ( strpos( $lowercase_filename, $pattern ) !== false ) { - $metadata['weight'] = $weight; - break; - } - } - - // Detect font style. - foreach ( $style_patterns as $pattern => $style ) { - if ( strpos( $lowercase_filename, $pattern ) !== false ) { - $metadata['style'] = $style; - break; - } - } - - return $metadata; -} +// function parse_font_filename( $filename ) { +// // Default values. +// $metadata = array( +// 'family' => 'Unknown', +// 'weight' => '400', +// 'style' => 'normal', +// ); + +// // Common font family patterns. +// $family_patterns = array( +// 'inter' => 'Inter', +// 'oxygen' => 'Oxygen', +// 'roboto-mono' => 'Roboto Mono', +// 'roboto' => 'Roboto', +// 'open-sans' => 'Open Sans', +// 'lato' => 'Lato', +// 'poppins' => 'Poppins', +// 'montserrat' => 'Montserrat', +// 'raleway' => 'Raleway', +// 'playfair' => 'Playfair Display', +// 'source-sans' => 'Source Sans Pro', +// 'noto-sans' => 'Noto Sans', +// 'nunito' => 'Nunito', +// 'merriweather' => 'Merriweather', +// 'ubuntu' => 'Ubuntu', +// 'oswald' => 'Oswald', +// ); + +// // Common weight patterns with exact matches. +// $weight_patterns = array( +// '-100' => '100', +// '-200' => '200', +// '-300' => '300', +// '-regular' => '400', +// '-normal' => '400', +// '-400' => '400', +// '-500' => '500', +// '-600' => '600', +// '-700' => '700', +// '-800' => '800', +// '-900' => '900', +// 'thin' => '100', +// 'extralight' => '200', +// 'light' => '300', +// 'regular' => '400', +// 'medium' => '500', +// 'semibold' => '600', +// 'bold' => '700', +// 'extrabold' => '800', +// 'black' => '900', +// ); + +// // Common style patterns. +// $style_patterns = array( +// 'italic' => 'italic', +// 'oblique' => 'oblique', +// ); + +// $lowercase_filename = strtolower( $filename ); + +// // Detect font family - use the longest matching pattern. +// $matched_family = ''; +// $longest_match = 0; +// foreach ( $family_patterns as $pattern => $family ) { +// if ( strpos( $lowercase_filename, $pattern ) !== false && strlen( $pattern ) > $longest_match ) { +// $matched_family = $family; +// $longest_match = strlen( $pattern ); +// } +// } +// if ( $matched_family ) { +// $metadata['family'] = $matched_family; +// } + +// // If no family detected, try to extract from filename. +// if ( 'Unknown' === $metadata['family'] ) { +// $parts = preg_split( '/[-_\s]+/', $filename ); +// if ( ! empty( $parts[0] ) ) { +// $metadata['family'] = ucwords( str_replace( '-', ' ', $parts[0] ) ); +// } +// } + +// // Detect font weight - use exact matches. +// foreach ( $weight_patterns as $pattern => $weight ) { +// if ( strpos( $lowercase_filename, $pattern ) !== false ) { +// $metadata['weight'] = $weight; +// break; +// } +// } + +// // Detect font style. +// foreach ( $style_patterns as $pattern => $style ) { +// if ( strpos( $lowercase_filename, $pattern ) !== false ) { +// $metadata['style'] = $style; +// break; +// } +// } + +// return $metadata; +// } /** * Group fonts by family. diff --git a/tools/font-detection.php b/tools/font-detection.php index d4556f48..9b9a374c 100644 --- a/tools/font-detection.php +++ b/tools/font-detection.php @@ -17,6 +17,7 @@ * @return array Array of font files. */ function wdsbt_scan_font_directory( $directory ) { + $fonts = array(); $theme_dir = dirname( __DIR__, 1 ); $full_path = $theme_dir . '/' . $directory; @@ -63,6 +64,7 @@ function wdsbt_print_fonts( $fonts, $label = 'Fonts' ) { printf( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CLI output, escaping not required. " - %s.%s | %s | Weight: %s | Style: %s\n", + $font['relative_path'], $font['filename'], // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CLI output $font['extension'], // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CLI output $font['family'], // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CLI output @@ -72,8 +74,11 @@ function wdsbt_print_fonts( $fonts, $label = 'Fonts' ) { } } + // Example usage for CLI debugging. -$wdsbt_build_fonts = wdsbt_scan_font_directory( 'build/fonts' ); -$wdsbt_assets_fonts = wdsbt_scan_font_directory( 'assets/fonts' ); -wdsbt_print_fonts( $wdsbt_build_fonts, 'Build Fonts' ); -wdsbt_print_fonts( $wdsbt_assets_fonts, 'Assets Fonts' ); +$theme_dir = dirname( __DIR__, 1 ); +$wdsbt_build_fonts = wdsbt_scan_font_directory_raw( 'build/fonts', $theme_dir ); +$wdsbt_assets_fonts = wdsbt_scan_font_directory_raw( 'assets/fonts', $theme_dir ); + +wdsbt_print_font_summary( $wdsbt_build_fonts, false, 'All Detected Build Fonts' ); +wdsbt_print_font_summary( $wdsbt_assets_fonts, false, 'All Detected Assets Fonts' ); diff --git a/tools/font-generator.php b/tools/font-generator.php deleted file mode 100644 index 515b23b0..00000000 --- a/tools/font-generator.php +++ /dev/null @@ -1,613 +0,0 @@ -#!/usr/bin/env php - 'assets/fonts', - 'output_dir' => 'build/fonts', - 'css_output' => 'assets/scss/base/_fonts.scss', - 'preload_output' => 'inc/setup/font-preload.php', - 'subset_text' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,!?@#$%^&*()_+-=[]{}|;:,.<>?/~`\'"\\', - 'formats' => array( 'woff2', 'woff' ), - 'weights' => array( - '100' => 'Thin', - '200' => 'ExtraLight', - '300' => 'Light', - '400' => 'Regular', - '500' => 'Medium', - '600' => 'SemiBold', - '700' => 'Bold', - '800' => 'ExtraBold', - '900' => 'Black', - ), -); - -/** - * Parse command line arguments. - */ -function parse_args() { - global $argv, $wdsbt_config; - - $options = array( - '--input-dir' => 'input_dir', - '--output-dir' => 'output_dir', - '--css-output' => 'css_output', - '--subset' => 'subset_text', - '--formats' => 'formats', - '--weights' => 'weights', - '--help' => 'help', - ); - - foreach ( $argv as $i => $arg ) { - if ( isset( $options[ $arg ] ) ) { - $key = $options[ $arg ]; - if ( 'help' === $key ) { - show_help(); - exit( 0 ); - } - if ( isset( $argv[ $i + 1 ] ) && ! str_starts_with( $argv[ $i + 1 ], '--' ) ) { - $wdsbt_config[ $key ] = $argv[ $i + 1 ]; - } - } - } -} - -/** - * Show help information. - */ -function show_help() { - echo "Font Generator Tool\n\n"; - echo "Usage: php tools/font-generator.php [options]\n\n"; - echo "Options:\n"; - echo " --input-dir DIR Input directory (default: assets/fonts)\n"; - echo " --output-dir DIR Output directory (default: build/fonts)\n"; - echo " --css-output FILE CSS output file (default: assets/scss/base/_fonts.scss)\n"; - echo " --subset TEXT Subset text for optimization\n"; - echo " --formats LIST Output formats (default: woff2,woff)\n"; - echo " --weights LIST Font weights to generate\n"; - echo " --help Show this help\n\n"; - echo "Examples:\n"; - echo " php tools/font-generator.php\n"; - echo " php tools/font-generator.php --input-dir custom/fonts --output-dir dist/fonts\n"; - echo " php tools/font-generator.php --formats woff2 --subset 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\n"; -} - -/** - * Check if required tools are available. - */ -function check_dependencies() { - $tools = array( 'fonttools', 'woff2_compress' ); - $missing = array(); - - foreach ( $tools as $tool ) { - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_shell_exec -- CLI tool - $output = shell_exec( "which $tool 2>/dev/null" ); - if ( empty( $output ) ) { - $missing[] = $tool; - } - } - - if ( ! empty( $missing ) ) { - printf( - 'Missing required tools: %s -', - implode( ', ', $missing ) - ); - printf( "Please install:\n" ); - printf( " - fonttools: pip install fonttools\n" ); - printf( " - woff2_compress: brew install woff2 (macOS) or apt-get install woff2 (Ubuntu)\n" ); - exit( 1 ); - } - - printf( "All required tools are available\n" ); -} - -/** - * Scan for source font files. - * - * @param string $input_dir Input directory. - * @return array Array of font files. - */ -function scan_source_fonts( $input_dir ) { - $fonts = array(); - $theme_dir = dirname( __DIR__, 1 ); - $full_path = $theme_dir . '/' . $input_dir; - - if ( ! is_dir( $full_path ) ) { - printf( - 'Input directory not found: %s -', - $full_path - ); - return $fonts; - } - - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator( $full_path, RecursiveDirectoryIterator::SKIP_DOTS ) - ); - - foreach ( $iterator as $file ) { - if ( $file->isFile() && in_array( strtolower( $file->getExtension() ), array( 'ttf', 'otf' ), true ) ) { - $relative_path = str_replace( $theme_dir . '/', '', $file->getPathname() ); - $filename = $file->getBasename(); - $font_metadata = parse_font_filename( $filename ); - - $fonts[] = array( - 'path' => $file->getPathname(), - 'relative_path' => $relative_path, - 'filename' => $filename, - 'family' => $font_metadata['family'], - 'weight' => $font_metadata['weight'], - 'style' => $font_metadata['style'], - ); - } - } - - return $fonts; -} - -/** - * Parse font filename (reuse from existing tool). - * - * @param string $filename Font filename. - * @return array Font metadata. - */ -function parse_font_filename( $filename ) { - $metadata = array( - 'family' => 'Unknown', - 'weight' => '400', - 'style' => 'normal', - ); - - $family_patterns = array( - 'inter' => 'Inter', - 'oxygen' => 'Oxygen', - 'roboto-mono' => 'Roboto Mono', - 'roboto' => 'Roboto', - 'open-sans' => 'Open Sans', - 'lato' => 'Lato', - 'poppins' => 'Poppins', - 'montserrat' => 'Montserrat', - 'raleway' => 'Raleway', - 'playfair' => 'Playfair Display', - ); - - $weight_patterns = array( - '-100' => '100', - '-200' => '200', - '-300' => '300', - '-regular' => '400', - '-normal' => '400', - '-400' => '400', - '-500' => '500', - '-600' => '600', - '-700' => '700', - '-800' => '800', - '-900' => '900', - 'thin' => '100', - 'extralight' => '200', - 'light' => '300', - 'regular' => '400', - 'medium' => '500', - 'semibold' => '600', - 'bold' => '700', - 'extrabold' => '800', - 'black' => '900', - ); - - $style_patterns = array( - 'italic' => 'italic', - 'oblique' => 'oblique', - ); - - $lowercase_filename = strtolower( $filename ); - - foreach ( $family_patterns as $pattern => $family ) { - if ( strpos( $lowercase_filename, $pattern ) !== false ) { - $metadata['family'] = $family; - break; - } - } - - foreach ( $weight_patterns as $pattern => $weight ) { - if ( strpos( $lowercase_filename, $pattern ) !== false ) { - $metadata['weight'] = $weight; - break; - } - } - - foreach ( $style_patterns as $pattern => $style ) { - if ( strpos( $lowercase_filename, $pattern ) !== false ) { - $metadata['style'] = $style; - break; - } - } - - return $metadata; -} - -/** - * Generate font subsets and optimized formats. - * - * @param array $source_fonts Array of source font files. - * @param string $output_dir Output directory. - * @param array $config Configuration array. - * @return array Array of generated fonts. - */ -function generate_font_files( $source_fonts, $output_dir, $config ) { - $generated_fonts = array(); - $theme_dir = dirname( __DIR__, 1 ); - $output_path = $theme_dir . '/' . $output_dir; - - // Create output directory. - if ( ! is_dir( $output_path ) ) { - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir -- CLI tool - mkdir( $output_path, 0755, true ); - } - - foreach ( $source_fonts as $font ) { - printf( 'Processing %s %s %s...\n', $font['family'], $font['weight'], $font['style'] ); - - $family_slug = sanitize_title( $font['family'] ); - $output_filename = "{$family_slug}-{$font['weight']}-{$font['style']}"; - - $generated_variants = array(); - - foreach ( $config['formats'] as $format ) { - $output_file = "{$output_path}/{$output_filename}.{$format}"; - - // Generate subset and convert format. - $success = generate_font_variant( $font['path'], $output_file, $format, $config['subset_text'] ); - - if ( $success ) { - $generated_variants[] = array( - 'path' => str_replace( $theme_dir . '/', '', $output_file ), - 'format' => $format, - 'filename' => basename( $output_file ), - ); - printf( - ' Generated %s -', - $format - ); - } else { - printf( - ' Failed to generate %s -', - $format - ); - } - } - - if ( ! empty( $generated_variants ) ) { - $generated_fonts[] = array( - 'family' => $font['family'], - 'weight' => $font['weight'], - 'style' => $font['style'], - 'variants' => $generated_variants, - ); - } - } - - return $generated_fonts; -} - -/** - * Generate a single font variant. - * - * @param string $input_file Input font file path. - * @param string $output_file Output font file path. - * @param string $format Output format. - * @param string $subset_text Subset text for optimization. - * @return bool Success status. - */ -function generate_font_variant( $input_file, $output_file, $format, $subset_text ) { - $temp_subset = tempnam( sys_get_temp_dir(), 'font_subset_' ) . '.ttf'; - - // Create subset. - $subset_cmd = "pyftsubset '{$input_file}' --text='{$subset_text}' --output-file='{$temp_subset}'"; - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_shell_exec -- CLI tool - $subset_result = shell_exec( $subset_cmd . ' 2>&1' ); - - if ( ! file_exists( $temp_subset ) ) { - return false; - } - - $success = false; - - switch ( $format ) { - case 'woff2': - $convert_cmd = "woff2_compress '{$temp_subset}'"; - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_shell_exec -- CLI tool - $success = shell_exec( $convert_cmd . ' 2>&1' ) !== null; - if ( $success ) { - WP_Filesystem::move( $temp_subset . '.woff2', $output_file ); - } - break; - - case 'woff': - $convert_cmd = "pyftsubset '{$temp_subset}' --flavor=woff --output-file='{$output_file}'"; - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_shell_exec -- CLI tool - $success = shell_exec( $convert_cmd . ' 2>&1' ) !== null; - break; - - default: - $success = copy( $temp_subset, $output_file ); - break; - } - - // Clean up temp file. - if ( file_exists( $temp_subset ) ) { - wp_delete_file( $temp_subset ); - } - - return $success; -} - -/** - * Generate CSS for font loading. - * - * @param array $generated_fonts Array of generated fonts. - * @param string $output_file Output CSS file path. - * @return bool Success status. - */ -function generate_font_css( $generated_fonts, $output_file ) { - $theme_dir = dirname( __DIR__, 1 ); - $css_path = $theme_dir . '/' . $output_file; - - // Create directory if it doesn't exist. - $css_dir = dirname( $css_path ); - if ( ! is_dir( $css_dir ) ) { - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir -- CLI tool - mkdir( $css_dir, 0755, true ); - } - - $css = "/* Auto-generated font CSS */\n\n"; - - // Group fonts by family. - $families = array(); - foreach ( $generated_fonts as $font ) { - $family = $font['family']; - if ( ! isset( $families[ $family ] ) ) { - $families[ $family ] = array(); - } - $families[ $family ][] = $font; - } - - foreach ( $families as $family => $fonts ) { - $css .= "/* {$family} */\n"; - - foreach ( $fonts as $font ) { - $css .= "@font-face {\n"; - $css .= " font-family: '{$family}';\n"; - $css .= " font-style: {$font['style']};\n"; - $css .= " font-weight: {$font['weight']};\n"; - $css .= " font-display: swap;\n"; - $css .= ' src: '; - - $src_parts = array(); - foreach ( $font['variants'] as $variant ) { - $format = 'woff2' === $variant['format'] ? 'woff2' : 'woff'; - $src_parts[] = "url('../fonts/{$variant['filename']}') format('{$format}')"; - } - - $css .= implode( ', ', $src_parts ) . ";\n"; - $css .= "}\n\n"; - } - } - - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- CLI tool - if ( file_put_contents( $css_path, $css ) ) { - printf( - 'Generated font CSS: %s -', - $output_file - ); - return true; - } else { - printf( - 'Failed to generate font CSS -' - ); - return false; - } -} - -/** - * Generate font preload links. - * - * @param array $generated_fonts Array of generated fonts. - * @param string $output_file Output PHP file path. - * @return bool Success status. - */ -function generate_font_preload( $generated_fonts, $output_file ) { - $theme_dir = dirname( __DIR__, 1 ); - $php_path = $theme_dir . '/' . $output_file; - - // Create directory if it doesn't exist. - $php_dir = dirname( $php_path ); - if ( ! is_dir( $php_dir ) ) { - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir -- CLI tool - mkdir( $php_dir, 0755, true ); - } - - $php = " 'font/{$first_variant['format']}',\n"; - } - - $php .= " ];\n\n"; - $php .= " foreach (\$preload_links as \$href => \$as) {\n"; - $php .= " echo '';\n"; - $php .= " }\n"; - $php .= "}\n"; - - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- CLI tool - if ( file_put_contents( $php_path, $php ) ) { - printf( - 'Generated font preload: %s -', - $output_file - ); - return true; - } else { - printf( - 'Failed to generate font preload -' - ); - return false; - } -} - -/** - * Sanitize title (reuse from existing tool). - * - * @param string $title Title to sanitize. - * @return string Sanitized title. - */ -function sanitize_title( $title ) { - $title = strtolower( $title ); - $title = preg_replace( '/[^a-z0-9\s-]/', '', $title ); - $title = preg_replace( '/[\s-]+/', '-', $title ); - return trim( $title, '-' ); -} - -/** - * Main function. - */ -function main() { - global $wdsbt_config; - - printf( "Font Generator Tool\n" ); - - printf( "=====================\n\n" ); - - // Parse arguments. - parse_args(); - - // Check dependencies. - check_dependencies(); - - // Scan for source fonts. - printf( "\nScanning for source fonts...\n" ); - $source_fonts = scan_source_fonts( $wdsbt_config['input_dir'] ); - - if ( empty( $source_fonts ) ) { - - printf( - "No source fonts found in %s\n", - $wdsbt_config['input_dir'] - ); - exit( 1 ); - } - - printf( - "Found %d source fonts:\n", - count( $source_fonts ) - ); - foreach ( $source_fonts as $font ) { - - printf( - " - %s %s %s\n", - $font['family'], - $font['weight'], - $font['style'] - ); - } - - // Generate optimized fonts. - - printf( "\nGenerating optimized fonts...\n" ); - $generated_fonts = generate_font_files( $source_fonts, $wdsbt_config['output_dir'], $wdsbt_config ); - - if ( empty( $generated_fonts ) ) { - - printf( "No fonts were generated\n" ); - exit( 1 ); - } - - printf( "\nGenerated %d font families\n", count( $generated_fonts ) ); - - // Generate CSS. - printf( "\nGenerating CSS...\n" ); - generate_font_css( $generated_fonts, $wdsbt_config['css_output'] ); - - // Generate preload links. - printf( "\nGenerating preload links...\n" ); - generate_font_preload( $generated_fonts, $wdsbt_config['preload_output'] ); - - // Update theme.json. - printf( "\nUpdating theme.json...\n" ); - include_once __DIR__ . '/generate-theme-json.php'; - generate_theme_json(); - - printf( "\nFont generation complete!\n" ); - - printf( "Generated files:\n" ); - - printf( - " - Fonts: %s/\n", - $wdsbt_config['output_dir'] - ); - - printf( - " - CSS: %s\n", - $wdsbt_config['css_output'] - ); - - printf( - " - Preload: %s\n", - $wdsbt_config['preload_output'] - ); - - printf( " - Theme JSON: theme.json\n" ); -} - -// Run the generator. -main(); - -// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped -// phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- CLI tool writing local file. -// phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_mkdir -- CLI tool -// phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_copy -- CLI tool -// phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_is_dir -- CLI tool -// phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_is_file -- CLI tool -// phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_unlink -- CLI tool -// phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_rename -- CLI tool diff --git a/tools/font-pipeline-config.php b/tools/font-pipeline-config.php new file mode 100644 index 00000000..e69de29b diff --git a/tools/font-processor.php b/tools/font-processor.php index dc1dfe78..f8abadaa 100644 --- a/tools/font-processor.php +++ b/tools/font-processor.php @@ -109,7 +109,7 @@ function scan_font_files( $input_dir ) { // Detect font family from folder name (headline, body, mono). $folder_name = basename( dirname( $file->getPathname() ) ); - $font_metadata = parse_font_filename( $filename ); + $font_metadata = wdsbt_parse_font_meta_from_filename( $filename ); // Smart fallback for Unknown family if ( 'Unknown' === $font_metadata['family'] ) { @@ -159,7 +159,7 @@ function copy_fonts_to_output( $fonts, $output_dir ) { foreach ( $fonts as $font ) { //$standardized_slug = get_font_slug( $font['family'] ); - $standardized_slug = wdsbt_get_font_slug( $font['path'] ); + $standardized_slug = wdsbt_get_font_role_slug( $font['path'] ); $family_dir = $full_output_dir . '/' . $standardized_slug; if ( ! is_dir( $family_dir ) ) { @@ -230,7 +230,7 @@ function generate_font_preload( $fonts, $output_file ) { foreach ( $preloaded as $font ) { $format = 'woff2' === $font['extension'] ? 'font/woff2' : 'font/woff'; - $standardized_slug = wdsbt_get_font_slug( $font['family'] ); + $standardized_slug = wdsbt_get_font_role_slug( $font['family'] ); $php .= " 'fonts/{$standardized_slug}/{$font['filename']}' => '{$format}',\n"; } diff --git a/tools/generate-theme-json.php b/tools/generate-theme-json.php index 74bab9ff..838b274b 100644 --- a/tools/generate-theme-json.php +++ b/tools/generate-theme-json.php @@ -18,6 +18,7 @@ * @return array Array of font files. */ function scan_font_directory( $directory ) { + $fonts = array(); $theme_dir = dirname( __DIR__, 1 ); $full_path = $theme_dir . '/' . $directory; @@ -42,25 +43,12 @@ function scan_font_directory( $directory ) { if ( $file->isFile() && in_array( strtolower( $file->getExtension() ), array( 'woff2', 'woff', 'ttf', 'otf' ), true ) ) { $relative_path = str_replace( $theme_dir . '/', '', $file->getPathname() ); $filename = $file->getBasename(); - $font_metadata = parse_font_filename( $filename ); + $font_metadata = wdsbt_parse_font_meta_from_filename( $filename ); // Detect font family from folder name (headline, body, mono). $folder_name = basename( dirname( $file->getPathname() ) ); - // Always use the detected family from the filename. The folder name is only used for purpose. - if ( 'Unknown' === $font_metadata['family'] ) { - // Remove extension - $base = preg_replace('/\.[^.]+$/','', $filename); - // Split by - or _ - $parts = preg_split('/[-_]/', $base); - // Keep parts that are not in ignore_keywords - $clean_parts = array_filter($parts, function($part) use ($ignore_keywords) { - return ! in_array(strtolower($part), $ignore_keywords, true); - }); - $font_metadata['family'] = ucwords( implode(' ', $clean_parts) ); - } - $variant_key = $font_metadata['family'] . '-' . $font_metadata['weight'] . '-' . $font_metadata['style']; if ( ! isset( $fonts[ $variant_key ] ) || @@ -80,113 +68,6 @@ function scan_font_directory( $directory ) { return array_values( $fonts ); } -if ( ! function_exists( __NAMESPACE__ . '\\parse_font_filename' ) ) { - /** - * Parse font metadata from filename. - * - * @param string $filename Font filename. - * @return array Font metadata. - */ - function parse_font_filename( $filename ) { - // Default values. - $metadata = array( - 'family' => 'Unknown', - 'weight' => '400', - 'style' => 'normal', - ); - - // Common font family patterns. - $family_patterns = array( - 'inter' => 'Inter', - 'oxygen' => 'Oxygen', - 'roboto-mono' => 'Roboto Mono', - 'roboto' => 'Roboto', - 'open-sans' => 'Open Sans', - 'lato' => 'Lato', - 'poppins' => 'Poppins', - 'montserrat' => 'Montserrat', - 'raleway' => 'Raleway', - 'playfair' => 'Playfair Display', - 'source-sans' => 'Source Sans Pro', - 'noto-sans' => 'Noto Sans', - 'nunito' => 'Nunito', - 'merriweather' => 'Merriweather', - 'ubuntu' => 'Ubuntu', - 'oswald' => 'Oswald', - ); - - // Common weight patterns with exact matches. - $weight_patterns = array( - '-100' => '100', - '-200' => '200', - '-300' => '300', - '-regular' => '400', - '-normal' => '400', - '-400' => '400', - '-500' => '500', - '-600' => '600', - '-700' => '700', - '-800' => '800', - '-900' => '900', - 'thin' => '100', - 'extralight' => '200', - 'light' => '300', - 'regular' => '400', - 'medium' => '500', - 'semibold' => '600', - 'bold' => '700', - 'extrabold' => '800', - 'black' => '900', - ); - - // Common style patterns. - $style_patterns = array( - 'italic' => 'italic', - 'oblique' => 'oblique', - ); - - $lowercase_filename = strtolower( $filename ); - - // Detect font family - use the longest matching pattern. - $matched_family = ''; - $longest_match = 0; - foreach ( $family_patterns as $pattern => $family ) { - if ( strpos( $lowercase_filename, $pattern ) !== false && strlen( $pattern ) > $longest_match ) { - $matched_family = $family; - $longest_match = strlen( $pattern ); - } - } - if ( $matched_family ) { - $metadata['family'] = $matched_family; - } - - // If no family detected, try to extract from filename. - if ( 'Unknown' === $metadata['family'] ) { - $parts = preg_split( '/[-_\s]+/', $filename ); - if ( ! empty( $parts[0] ) ) { - $metadata['family'] = ucwords( str_replace( '-', ' ', $parts[0] ) ); - } - } - - // Detect font weight - use exact matches. - foreach ( $weight_patterns as $pattern => $weight ) { - if ( strpos( $lowercase_filename, $pattern ) !== false ) { - $metadata['weight'] = $weight; - break; - } - } - - // Detect font style. - foreach ( $style_patterns as $pattern => $style ) { - if ( strpos( $lowercase_filename, $pattern ) !== false ) { - $metadata['style'] = $style; - break; - } - } - - return $metadata; - } -} /** * Group fonts by family. @@ -203,7 +84,7 @@ function group_fonts_by_family( $fonts ) { if ( ! isset( $grouped[ $family ] ) ) { $grouped[ $family ] = array( 'name' => $family, - 'slug' => wdsbt_get_font_slug( $family ), + 'slug' => wdsbt_get_font_role_slug( $family ), 'fontFamily' => $family . ', sans-serif', 'fontFace' => array(), ); @@ -213,7 +94,7 @@ function group_fonts_by_family( $fonts ) { 'fontFamily' => $family, 'fontStyle' => $font['style'], 'fontWeight' => $font['weight'], - 'src' => array( "file:./{$font['path']}" ), + 'src' => array( "file:./{$font['relative_path']}" ), ); } @@ -238,6 +119,7 @@ function sanitize_title( $title ) { * Generate theme.json with detected fonts. */ function generate_theme_json() { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped printf( "\nScanning for fonts...\n" ); @@ -255,24 +137,32 @@ function generate_theme_json() { printf( "No base theme.json found, creating new one\n" ); } + // Get theme root directory. + $theme_dir = dirname( __DIR__, 1 ); + + // Scan for fonts in both build and assets directories. + // $build_fonts = scan_font_directory( 'build/fonts' ); + // $assets_fonts = scan_font_directory( 'assets/fonts' ); + // Scan for fonts in both build and assets directories. - $build_fonts = scan_font_directory( 'build/fonts' ); - $assets_fonts = scan_font_directory( 'assets/fonts' ); + $build_fonts = wdsbt_scan_font_directory_raw( 'build/fonts', $theme_dir ); + $assets_fonts = wdsbt_scan_font_directory_raw( 'assets/fonts', $theme_dir ); // Merge fonts, preferring build fonts over assets fonts. $all_fonts = array_merge( $build_fonts, $assets_fonts ); + $unique_fonts = wdsbt_resolve_fonts( $all_fonts ); - // Remove duplicates (build fonts take precedence). - $unique_fonts = array(); - $seen_paths = array(); + // // Remove duplicates (build fonts take precedence). + // $unique_fonts = array(); + // $seen_paths = array(); - foreach ( $all_fonts as $font ) { - $key = $font['family'] . '-' . $font['weight'] . '-' . $font['style']; - if ( ! isset( $seen_paths[ $key ] ) ) { - $unique_fonts[] = $font; - $seen_paths[ $key ] = true; - } - } + // foreach ( $all_fonts as $font ) { + // $key = $font['family'] . '-' . $font['weight'] . '-' . $font['style']; + // if ( ! isset( $seen_paths[ $key ] ) ) { + // $unique_fonts[] = $font; + // $seen_paths[ $key ] = true; + // } + // } // Group fonts by family. $font_families = group_fonts_by_family( $unique_fonts ); @@ -304,13 +194,13 @@ function generate_theme_json() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped printf( "Total Fonts: %d\n", $font_count ); + // Print a clean summary of detected font families. if ( $family_count > 0 ) { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - printf( "\nDetected Families:\n" ); - foreach ( $base_theme_json['settings']['typography']['fontFamilies'] as $family ) { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - printf( "- %s: %d variants\n", $family['name'], count( $family['fontFace'] ) ); - } + wdsbt_print_font_summary( + $base_theme_json['settings']['typography']['fontFamilies'] ?? [], + true, // grouped = true to show one line per family + 'Detected Families' + ); } // Save the updated theme.json. @@ -322,10 +212,10 @@ function generate_theme_json() { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- CLI tool writing local file. if ( file_put_contents( $output_path, $json_content ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - printf( "\nSuccessfully generated theme.json with detected fonts\n" ); + printf( "\nāœ… Successfully generated theme.json with detected fonts\n" ); } else { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - printf( "\nFailed to write theme.json\n" ); + printf( "\nāŒ Failed to write theme.json\n" ); } } diff --git a/tools/helpers.php b/tools/helpers.php index 6d445999..ffa1ab28 100644 --- a/tools/helpers.php +++ b/tools/helpers.php @@ -1,24 +1,30 @@ -#!/usr/bin/env php 'body', 'Inter' => 'headline', @@ -52,6 +58,7 @@ function wdsbt_get_font_slug( $family_or_path ) { // Family name mapping foreach ( $slug_mapping as $key => $value ) { if ( 0 === strcasecmp( $candidate, $key ) ) { + // printf( "Default mapping found and used for " . $candidate . "\n" ); return $value; } } @@ -86,6 +93,145 @@ function wdsbt_sanitize_title( $text ) { } +/** + * Scan a directory for font files and extract metadata. + * Low-level helper: no deduplication or path policy. + * + * @param string $directory Relative directory to scan. + * @param string $theme_dir Absolute theme root. + * @return array List of font files with metadata. + */ +if ( ! function_exists( __NAMESPACE__ . '\\wdsbt_scan_font_directory_raw' ) ) { + function wdsbt_scan_font_directory_raw( $directory, $theme_dir = null ) { + + $theme_dir = $theme_dir ?: dirname( __DIR__, 1 ); + $fonts = []; + + $full_path = rtrim( $theme_dir, '/' ) . '/' . trim( $directory, '/' ); + if ( ! is_dir( $full_path ) ) { + return $fonts; + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( $full_path, \RecursiveDirectoryIterator::SKIP_DOTS ) + ); + + foreach ( $iterator as $file ) { + if ( + $file->isFile() && + in_array( strtolower( $file->getExtension() ), $GLOBALS['wdsbt_config']['formats'], true ) + ) + { + $filename = $file->getBasename( '.' . $file->getExtension() ); + $relative_path = str_replace( rtrim( $theme_dir, '/' ) . '/', '', $file->getPathname() ); + + $font_metadata = wdsbt_parse_font_meta_from_filename( $filename ); + + $fonts[] = [ + 'path' => $file->getPathname(), + 'relative_path' => $relative_path, + 'filename' => $filename, + 'extension' => $file->getExtension(), + 'family' => $font_metadata['family'], + 'weight' => $font_metadata['weight'], + 'style' => $font_metadata['style'], + ]; + } + } + + return $fonts; + } +} + + +/** + * Resolve font variants and apply precedence rules. + * + * @param array $fonts Raw font list. + * @return array Resolved font list. + */ +if ( ! function_exists( __NAMESPACE__ . '\\wdsbt_resolve_fonts' ) ) { + function wdsbt_resolve_fonts( array $fonts ) { + + $resolved = array(); + + foreach ( $fonts as $font ) { + + // Skip known bad paths (build/fonts/assets/fonts, etc). + if ( + isset( $font['relative_path'] ) && + preg_match( '/(build\/fonts\/assets\/fonts|assets\/fonts\/build\/fonts)/', $font['relative_path'] ) + ) { + continue; + } + + $variant_key = $font['family'] . '-' . $font['weight'] . '-' . $font['style']; + + if ( ! isset( $resolved[ $variant_key ] ) ) { + $resolved[ $variant_key ] = $font; + continue; + } + + // Prefer build/ over assets/ + $existing_path = $resolved[ $variant_key ]['relative_path'] ?? ''; + $new_path = $font['relative_path'] ?? ''; + + if ( + strpos( $new_path, 'build/' ) === 0 && + strpos( $existing_path, 'assets/' ) === 0 + ) { + $resolved[ $variant_key ] = $font; + } + } + + return array_values( $resolved ); + } +} + + +/** + * Print fonts for debugging or summary. + * Can display either per-file details or grouped family summary. + * + * @param array $fonts Array of font files or grouped font families. + * @param bool $grouped Whether to display as grouped families (true) or per-file (false). + * @param string $label Label for output. + */ +if ( ! function_exists( __NAMESPACE__ . '\\wdsbt_print_font_summary' ) ) { + function wdsbt_print_font_summary( $fonts, $grouped = false, $label = 'Fonts' ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CLI output, escaping not required. + printf( "%s\n", $label ); + + if ( $grouped ) { + foreach ( $fonts as $family ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CLI output + printf( + "- %s (%s): %d variants\n", + $family['name'], + $family['slug'], + count( $family['fontFace'] ?? [] ) + ); + } + } else { + foreach ( $fonts as $font ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CLI output + printf( + " - %s | %s | Weight: %s | Style: %s\n", + $font['relative_path'] ?? $font['path'], + $font['extension'] ?? '', + $font['weight'] ?? '', + $font['style'] ?? '' + ); + } + } + + printf( "\n\n"); + + } +} + + + /** * Parse font filename. * @@ -164,13 +310,21 @@ function wdsbt_parse_font_meta_from_filename( $filename ) { $lowercase_filename = strtolower( $filename ); + // Detect font family - use the longest matching pattern. + $matched_family = ''; + $longest_match = 0; + foreach ( $family_patterns as $pattern => $family ) { - if ( strpos( $lowercase_filename, $pattern ) !== false ) { - $metadata['family'] = $family; - break; + if ( strpos( $lowercase_filename, $pattern ) !== false && strlen( $pattern ) > $longest_match ) { + $matched_family = $family; + $longest_match = strlen( $pattern ); } } + if ( $matched_family ) { + $metadata['family'] = $matched_family; + } + // If family name is not in the above list, try to extract from filename // This accounts for multi-part names and ignores common weight/style keywords if ( 'Unknown' === $metadata['family'] ) { From 82c0f529963c26fafbe8727cae53b94e0898313f Mon Sep 17 00:00:00 2001 From: Lee Date: Mon, 12 Jan 2026 23:52:25 -0600 Subject: [PATCH 4/4] Font pipeline cleanup Cleaning up the font pipeline to actually respect the font build process. --- .../body/silly-font-name-900-italic.woff2 | Bin 16348 -> 0 bytes inc/setup/dynamic-theme-json.php | 259 ++++++++--------- package.json | 272 +++++++++--------- tools/font-detection.php | 2 + tools/font-pipeline-config.php | 38 +++ tools/generate-theme-json.php | 116 +------- tools/helpers.php | 150 ++++++---- 7 files changed, 397 insertions(+), 440 deletions(-) delete mode 100644 assets/fonts/body/silly-font-name-900-italic.woff2 diff --git a/assets/fonts/body/silly-font-name-900-italic.woff2 b/assets/fonts/body/silly-font-name-900-italic.woff2 deleted file mode 100644 index b610f33fd18631fe531ffff54fcc89ac8f29c473..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16348 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 ekD1#U{6yl3%sbT^bh~b=>3+yOaTDrK7MHc diff --git a/inc/setup/dynamic-theme-json.php b/inc/setup/dynamic-theme-json.php index 816b86a9..ccafd0ec 100644 --- a/inc/setup/dynamic-theme-json.php +++ b/inc/setup/dynamic-theme-json.php @@ -10,7 +10,7 @@ use RecursiveIteratorIterator; use RecursiveDirectoryIterator; -require_once __DIR__ . '/helpers.php'; +require_once dirname(__DIR__, 2) . '/tools/helpers.php'; /** * Get all available font files from a directory. @@ -43,7 +43,7 @@ function scan_font_directory( $directory ) { $filename = $file->getBasename( '.' . $file->getExtension() ); // Parse font metadata from filename. - $font_metadata = wdsbt_parse_font_meta_from_filename( $filename ); + $font_metadata = parse_font_filename( $filename ); // Create a unique key for this font variant. $variant_key = $font_metadata['family'] . '-' . $font_metadata['weight'] . '-' . $font_metadata['style']; @@ -72,105 +72,105 @@ function scan_font_directory( $directory ) { * @param string $filename Font filename. * @return array Font metadata. */ -// function parse_font_filename( $filename ) { -// // Default values. -// $metadata = array( -// 'family' => 'Unknown', -// 'weight' => '400', -// 'style' => 'normal', -// ); - -// // Common font family patterns. -// $family_patterns = array( -// 'inter' => 'Inter', -// 'oxygen' => 'Oxygen', -// 'roboto-mono' => 'Roboto Mono', -// 'roboto' => 'Roboto', -// 'open-sans' => 'Open Sans', -// 'lato' => 'Lato', -// 'poppins' => 'Poppins', -// 'montserrat' => 'Montserrat', -// 'raleway' => 'Raleway', -// 'playfair' => 'Playfair Display', -// 'source-sans' => 'Source Sans Pro', -// 'noto-sans' => 'Noto Sans', -// 'nunito' => 'Nunito', -// 'merriweather' => 'Merriweather', -// 'ubuntu' => 'Ubuntu', -// 'oswald' => 'Oswald', -// ); - -// // Common weight patterns with exact matches. -// $weight_patterns = array( -// '-100' => '100', -// '-200' => '200', -// '-300' => '300', -// '-regular' => '400', -// '-normal' => '400', -// '-400' => '400', -// '-500' => '500', -// '-600' => '600', -// '-700' => '700', -// '-800' => '800', -// '-900' => '900', -// 'thin' => '100', -// 'extralight' => '200', -// 'light' => '300', -// 'regular' => '400', -// 'medium' => '500', -// 'semibold' => '600', -// 'bold' => '700', -// 'extrabold' => '800', -// 'black' => '900', -// ); - -// // Common style patterns. -// $style_patterns = array( -// 'italic' => 'italic', -// 'oblique' => 'oblique', -// ); - -// $lowercase_filename = strtolower( $filename ); - -// // Detect font family - use the longest matching pattern. -// $matched_family = ''; -// $longest_match = 0; -// foreach ( $family_patterns as $pattern => $family ) { -// if ( strpos( $lowercase_filename, $pattern ) !== false && strlen( $pattern ) > $longest_match ) { -// $matched_family = $family; -// $longest_match = strlen( $pattern ); -// } -// } -// if ( $matched_family ) { -// $metadata['family'] = $matched_family; -// } - -// // If no family detected, try to extract from filename. -// if ( 'Unknown' === $metadata['family'] ) { -// $parts = preg_split( '/[-_\s]+/', $filename ); -// if ( ! empty( $parts[0] ) ) { -// $metadata['family'] = ucwords( str_replace( '-', ' ', $parts[0] ) ); -// } -// } - -// // Detect font weight - use exact matches. -// foreach ( $weight_patterns as $pattern => $weight ) { -// if ( strpos( $lowercase_filename, $pattern ) !== false ) { -// $metadata['weight'] = $weight; -// break; -// } -// } - -// // Detect font style. -// foreach ( $style_patterns as $pattern => $style ) { -// if ( strpos( $lowercase_filename, $pattern ) !== false ) { -// $metadata['style'] = $style; -// break; -// } -// } - -// return $metadata; -// } +function parse_font_filename( $filename ) { + // Default values. + $metadata = array( + 'family' => 'Unknown', + 'weight' => '400', + 'style' => 'normal', + ); + + // Common font family patterns. + $family_patterns = array( + 'inter' => 'Inter', + 'oxygen' => 'Oxygen', + 'roboto-mono' => 'Roboto Mono', + 'roboto' => 'Roboto', + 'open-sans' => 'Open Sans', + 'lato' => 'Lato', + 'poppins' => 'Poppins', + 'montserrat' => 'Montserrat', + 'raleway' => 'Raleway', + 'playfair' => 'Playfair Display', + 'source-sans' => 'Source Sans Pro', + 'noto-sans' => 'Noto Sans', + 'nunito' => 'Nunito', + 'merriweather' => 'Merriweather', + 'ubuntu' => 'Ubuntu', + 'oswald' => 'Oswald', + ); + + // Common weight patterns with exact matches. + $weight_patterns = array( + '-100' => '100', + '-200' => '200', + '-300' => '300', + '-regular' => '400', + '-normal' => '400', + '-400' => '400', + '-500' => '500', + '-600' => '600', + '-700' => '700', + '-800' => '800', + '-900' => '900', + 'thin' => '100', + 'extralight' => '200', + 'light' => '300', + 'regular' => '400', + 'medium' => '500', + 'semibold' => '600', + 'bold' => '700', + 'extrabold' => '800', + 'black' => '900', + ); + + // Common style patterns. + $style_patterns = array( + 'italic' => 'italic', + 'oblique' => 'oblique', + ); + + $lowercase_filename = strtolower( $filename ); + + // Detect font family - use the longest matching pattern. + $matched_family = ''; + $longest_match = 0; + foreach ( $family_patterns as $pattern => $family ) { + if ( strpos( $lowercase_filename, $pattern ) !== false && strlen( $pattern ) > $longest_match ) { + $matched_family = $family; + $longest_match = strlen( $pattern ); + } + } + if ( $matched_family ) { + $metadata['family'] = $matched_family; + } + + // If no family detected, try to extract from filename. + if ( 'Unknown' === $metadata['family'] ) { + $parts = preg_split( '/[-_\s]+/', $filename ); + if ( ! empty( $parts[0] ) ) { + $metadata['family'] = ucwords( str_replace( '-', ' ', $parts[0] ) ); + } + } + + // Detect font weight - use exact matches. + foreach ( $weight_patterns as $pattern => $weight ) { + if ( strpos( $lowercase_filename, $pattern ) !== false ) { + $metadata['weight'] = $weight; + break; + } + } + + // Detect font style. + foreach ( $style_patterns as $pattern => $style ) { + if ( strpos( $lowercase_filename, $pattern ) !== false ) { + $metadata['style'] = $style; + break; + } + } + + return $metadata; +} /** * Group fonts by family. @@ -187,7 +187,7 @@ function group_fonts_by_family( $fonts ) { if ( ! isset( $grouped[ $family ] ) ) { $grouped[ $family ] = array( 'name' => $family, - 'slug' => get_font_slug( $family ), + 'slug' => wdsbt_get_font_role_slug( $family ), 'fontFamily' => $family . ', sans-serif', 'fontFace' => array(), ); @@ -204,7 +204,28 @@ function group_fonts_by_family( $fonts ) { return $grouped; } +/** + * Map font family to standardized slug. + * + * @param string $family Font family name. + * @return string Standardized slug. + */ +function get_font_slug( $family ) { + $slug_mapping = array( + 'Oxygen' => 'body', + 'Inter' => 'headline', + 'Roboto Mono' => 'mono', + 'Open Sans' => 'body', + 'Lato' => 'body', + 'Poppins' => 'headline', + 'Montserrat' => 'headline', + 'Raleway' => 'headline', + 'Playfair Display' => 'headline', + 'Roboto' => 'body', + ); + return $slug_mapping[ $family ] ?? sanitize_title( $family ); +} /** * Filter theme.json data to include dynamically detected fonts. @@ -216,39 +237,7 @@ function filter_theme_json_data( $theme_json_data ) { // Get the current theme.json content. $theme_json = $theme_json_data->get_data(); - // Scan for fonts in both build and assets directories. - $build_fonts = scan_font_directory( 'build/fonts' ); - $assets_fonts = scan_font_directory( 'assets/fonts' ); - - // Merge fonts, preferring build fonts over assets fonts. - $all_fonts = array_merge( $build_fonts, $assets_fonts ); - - // Remove duplicates (build fonts take precedence). - $unique_fonts = array(); - $seen_paths = array(); - - foreach ( $all_fonts as $font ) { - $key = $font['family'] . '-' . $font['weight'] . '-' . $font['style']; - if ( ! isset( $seen_paths[ $key ] ) ) { - $unique_fonts[] = $font; - $seen_paths[ $key ] = true; - } - } - - // Group fonts by family. - $font_families = group_fonts_by_family( $unique_fonts ); - - // Ensure typography settings exist. - if ( ! isset( $theme_json['settings']['typography'] ) ) { - $theme_json['settings']['typography'] = array(); - } - - // Update or add font families. - if ( ! empty( $font_families ) ) { - $theme_json['settings']['typography']['fontFamilies'] = array_values( $font_families ); - } - - // Create new theme JSON data object with updated content. + // Theme.json is already generated by the font build pipeline, we just need to enqueue it. $updated_theme_json = new \WP_Theme_JSON_Data( $theme_json ); return $updated_theme_json; diff --git a/package.json b/package.json index 34f786db..64e534b8 100644 --- a/package.json +++ b/package.json @@ -1,138 +1,138 @@ { - "name": "wds-bt", - "version": "1.3.5", - "private": true, - "description": "A starter block theme from WebDevStudios.", - "author": "WebDevStudios ", - "license": "GPL-2.0-or-later", - "keywords": [ - "WordPress", - "Theme" - ], - "homepage": "https://github.com/WebDevStudios/wds-bt/#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/WebDevStudios/wds-bt.git" - }, - "bugs": { - "url": "https://github.com/WebDevStudios/wds-bt/issues" - }, - "engines": { - "node": ">=22.0.0", - "npm": ">=10" - }, - "dependencies": { - "@wordpress/block-editor": "^14.21.0", - "@wordpress/blocks": "^14.15.0", - "@wordpress/components": "^29.12.0", - "@wordpress/compose": "^7.26.0", - "@wordpress/hooks": "^4.26.0", - "@wordpress/i18n": "^5.26.0", - "@wordpress/token-list": "^3.26.0", - "body-parser": "^2.2.0", - "braces": "^3.0.3", - "cookie": "^1.0.2", - "dotenv": "^17.2.0", - "express": "^5.1.0", - "got": "^14.4.7", - "npm": "^11.4.2", - "postcss": "^8.5.6", - "send": "^1.2.0", - "serve-static": "^2.2.0", - "svgo": "^4.0.0", - "uuid": "^11.1.0", - "ws": "^8.18.3", - "xml2js": "^0.6.2" - }, - "devDependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-class-static-block": "7.27.1", - "@babel/preset-env": "^7.28.0", - "@babel/preset-react": "^7.27.1", - "@evilmartians/lefthook": "^1.12.2", - "@typescript-eslint/eslint-plugin": "^8.37.0", - "@wordpress/dependency-extraction-webpack-plugin": "^6.26.0", - "@wordpress/env": "^10.26.0", - "@wordpress/eslint-plugin": "^22.12.0", - "@wordpress/prettier-config": "^4.26.0", - "@wordpress/scripts": "^30.19.0", - "autoprefixer": "^10.4.21", - "babel-loader": "^10.0.0", - "clean-webpack-plugin": "^4.0.0", - "compression-webpack-plugin": "^11.1.0", - "copy-webpack-plugin": "^13.0.0", - "cross-env": "^7.0.3", - "css-minimizer-webpack-plugin": "^7.0.2", - "dotenv-cli": "^8.0.0", - "eslint": "8.57.1", - "eslint-plugin-eslint-comments": "3.2.0", - "eslint-plugin-prettier": "^5.5.1", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-webpack-plugin": "^5.0.2", - "glob": "^11.0.3", - "html_codesniffer": "2.5.1", - "image-minimizer-webpack-plugin": "^4.1.3", - "imagemin": "^9.0.1", - "imagemin-gifsicle": "^7.0.0", - "imagemin-jpegtran": "^8.0.0", - "imagemin-mozjpeg": "^10.0.0", - "imagemin-optipng": "^8.0.0", - "imagemin-svgo": "^11.0.1", - "inquirer": "^12.7.0", - "lint-staged": "16.1.2", - "mini-css-extract-plugin": "^2.9.2", - "npm-check-updates": "^18.0.1", - "npm-run-all": "4.1.5", - "pa11y-ci": "^3.1.0", - "postcss-loader": "^8.1.1", - "postcss-move-props-to-bg-image-query": "^4.0.0", - "postcss-preset-env": "^10.2.4", - "postcss-rtl": "^2.0.0", - "prettier": "3.6.2", - "puppeteer-core": "24.14.0", - "remove-files-webpack-plugin": "^1.5.0", - "rimraf": "^6.0.1", - "sass-loader": "^16.0.5", - "stylelint-webpack-plugin": "^5.0.1", - "svg-spritemap-webpack-plugin": "^4.7.0", - "svg-transform-loader": "^2.0.13", - "terser-webpack-plugin": "^5.3.14", - "thread-loader": "^4.0.4", - "webpack": "^5.100.2", - "webpack-cli": "^6.0.1", - "webpackbar": "^7.0.0" - }, - "scripts": { - "a11y": "node a11y.cjs", - "build": "rimraf blocks build && wp-scripts build --config webpack.config.js --progress && npm run fonts:generate", - "create-block": "run-s create-block:run", - "create-block:run": "cd ./assets/blocks && npx @wordpress/create-block --namespace=wdsbt --template ../../inc/block-template --no-plugin", - "format": "wp-scripts format && npm run format:php && npm run format:js", - "format:css": "wp-scripts format ./assets/**/*.scss", - "format:php": "composer run-script phpcs-fix", - "fonts": "rimraf build/fonts && php tools/font-processor.php", - "fonts:detect": "php tools/font-detection.php", - "fonts:generate": "php tools/generate-theme-json.php", - "lint": "run-p lint:*", - "lint:css": "wp-scripts lint-style", - "lint:js": "wp-scripts lint-js", - "lint:md:docs": "wp-scripts lint-md-docs", - "lint:php": "composer run-script phpcs", - "lint:pkg-json": "wp-scripts lint-pkg-json", - "packages-update": "npx npm-check-updates -u && npm install && npm dedupe && npm audit fix", - "postinstall": "npx lefthook install && git config --local core.hooksPath .git/hooks && ./scripts/update-cursorrules.sh", - "preinstall": "npx cross-env npm_config_legacy_peer_deps=true", - "reset": "rimraf node_modules vendor build blocks package-lock.json composer.lock", - "setup": "npm run reset && npm i && composer i && npm run build", - "start": "rimraf build blocks && npm run fonts && wp-scripts start", - "update-cursorrules": "./scripts/update-cursorrules.sh", - "version-update": "dotenv node updateVersion.js" - }, - "lint-staged": { - "*.js": "wp-scripts lint-js", - "*.php": "composer run lint", - "*.scss": "wp-scripts lint-style", - "*.json": "wp-scripts lint-pkg-json", - "*.md": "wp-scripts lint-md-docs" - } + "name": "wds-bt", + "version": "1.3.5", + "private": true, + "description": "A starter block theme from WebDevStudios.", + "author": "WebDevStudios ", + "license": "GPL-2.0-or-later", + "keywords": [ + "WordPress", + "Theme" + ], + "homepage": "https://github.com/WebDevStudios/wds-bt/#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/WebDevStudios/wds-bt.git" + }, + "bugs": { + "url": "https://github.com/WebDevStudios/wds-bt/issues" + }, + "engines": { + "node": ">=22.0.0", + "npm": ">=10" + }, + "dependencies": { + "@wordpress/block-editor": "^14.21.0", + "@wordpress/blocks": "^14.15.0", + "@wordpress/components": "^29.12.0", + "@wordpress/compose": "^7.26.0", + "@wordpress/hooks": "^4.26.0", + "@wordpress/i18n": "^5.26.0", + "@wordpress/token-list": "^3.26.0", + "body-parser": "^2.2.0", + "braces": "^3.0.3", + "cookie": "^1.0.2", + "dotenv": "^17.2.0", + "express": "^5.1.0", + "got": "^14.4.7", + "npm": "^11.4.2", + "postcss": "^8.5.6", + "send": "^1.2.0", + "serve-static": "^2.2.0", + "svgo": "^4.0.0", + "uuid": "^11.1.0", + "ws": "^8.18.3", + "xml2js": "^0.6.2" + }, + "devDependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-class-static-block": "7.27.1", + "@babel/preset-env": "^7.28.0", + "@babel/preset-react": "^7.27.1", + "@evilmartians/lefthook": "^1.12.2", + "@typescript-eslint/eslint-plugin": "^8.37.0", + "@wordpress/dependency-extraction-webpack-plugin": "^6.26.0", + "@wordpress/env": "^10.26.0", + "@wordpress/eslint-plugin": "^22.12.0", + "@wordpress/prettier-config": "^4.26.0", + "@wordpress/scripts": "^30.19.0", + "autoprefixer": "^10.4.21", + "babel-loader": "^10.0.0", + "clean-webpack-plugin": "^4.0.0", + "compression-webpack-plugin": "^11.1.0", + "copy-webpack-plugin": "^13.0.0", + "cross-env": "^7.0.3", + "css-minimizer-webpack-plugin": "^7.0.2", + "dotenv-cli": "^8.0.0", + "eslint": "8.57.1", + "eslint-plugin-eslint-comments": "3.2.0", + "eslint-plugin-prettier": "^5.5.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-webpack-plugin": "^5.0.2", + "glob": "^11.0.3", + "html_codesniffer": "2.5.1", + "image-minimizer-webpack-plugin": "^4.1.3", + "imagemin": "^9.0.1", + "imagemin-gifsicle": "^7.0.0", + "imagemin-jpegtran": "^8.0.0", + "imagemin-mozjpeg": "^10.0.0", + "imagemin-optipng": "^8.0.0", + "imagemin-svgo": "^11.0.1", + "inquirer": "^12.7.0", + "lint-staged": "16.1.2", + "mini-css-extract-plugin": "^2.9.2", + "npm-check-updates": "^18.0.1", + "npm-run-all": "4.1.5", + "pa11y-ci": "^3.1.0", + "postcss-loader": "^8.1.1", + "postcss-move-props-to-bg-image-query": "^4.0.0", + "postcss-preset-env": "^10.2.4", + "postcss-rtl": "^2.0.0", + "prettier": "3.6.2", + "puppeteer-core": "24.14.0", + "remove-files-webpack-plugin": "^1.5.0", + "rimraf": "^6.0.1", + "sass-loader": "^16.0.5", + "stylelint-webpack-plugin": "^5.0.1", + "svg-spritemap-webpack-plugin": "^4.7.0", + "svg-transform-loader": "^2.0.13", + "terser-webpack-plugin": "^5.3.14", + "thread-loader": "^4.0.4", + "webpack": "^5.100.2", + "webpack-cli": "^6.0.1", + "webpackbar": "^7.0.0" + }, + "scripts": { + "a11y": "node a11y.cjs", + "build": "rimraf blocks build && wp-scripts build --config webpack.config.js --progress && npm run fonts:generate", + "create-block": "run-s create-block:run", + "create-block:run": "cd ./assets/blocks && npx @wordpress/create-block --namespace=wdsbt --template ../../inc/block-template --no-plugin", + "format": "wp-scripts format && npm run format:php && npm run format:js", + "format:css": "wp-scripts format ./assets/**/*.scss", + "format:php": "composer run-script phpcs-fix", + "fonts": "rimraf build/fonts && php tools/font-processor.php", + "fonts:detect": "php tools/font-detection.php", + "fonts:generate": "rimraf build/fonts && php tools/generate-theme-json.php", + "lint": "run-p lint:*", + "lint:css": "wp-scripts lint-style", + "lint:js": "wp-scripts lint-js", + "lint:md:docs": "wp-scripts lint-md-docs", + "lint:php": "composer run-script phpcs", + "lint:pkg-json": "wp-scripts lint-pkg-json", + "packages-update": "npx npm-check-updates -u && npm install && npm dedupe && npm audit fix", + "postinstall": "npx lefthook install && git config --local core.hooksPath .git/hooks && ./scripts/update-cursorrules.sh", + "preinstall": "npx cross-env npm_config_legacy_peer_deps=true", + "reset": "rimraf node_modules vendor build blocks package-lock.json composer.lock", + "setup": "npm run reset && npm i && composer i && npm run build", + "start": "rimraf build blocks && npm run fonts && wp-scripts start", + "update-cursorrules": "./scripts/update-cursorrules.sh", + "version-update": "dotenv node updateVersion.js" + }, + "lint-staged": { + "*.js": "wp-scripts lint-js", + "*.php": "composer run lint", + "*.scss": "wp-scripts lint-style", + "*.json": "wp-scripts lint-pkg-json", + "*.md": "wp-scripts lint-md-docs" + } } diff --git a/tools/font-detection.php b/tools/font-detection.php index 9b9a374c..3823d85e 100644 --- a/tools/font-detection.php +++ b/tools/font-detection.php @@ -8,6 +8,8 @@ // Prefix: wdsbt_. +namespace WebDevStudios\wdsbt; + require_once __DIR__ . '/helpers.php'; /** diff --git a/tools/font-pipeline-config.php b/tools/font-pipeline-config.php index e69de29b..11159e90 100644 --- a/tools/font-pipeline-config.php +++ b/tools/font-pipeline-config.php @@ -0,0 +1,38 @@ +#!/usr/bin/env php + 'assets/fonts', + + // Directory to write processed fonts (relative to theme root) + 'output_dir' => 'build/fonts', + + // PHP file to generate for preloading fonts + 'preload_output' => 'inc/setup/font-preload.php', + + // List of font formats to process + 'formats' => ['woff2','woff'], +]; diff --git a/tools/generate-theme-json.php b/tools/generate-theme-json.php index 838b274b..27a74fe8 100644 --- a/tools/generate-theme-json.php +++ b/tools/generate-theme-json.php @@ -11,109 +11,6 @@ require_once __DIR__ . '/helpers.php'; -/** - * Scan directory for font files. - * - * @param string $directory Directory to scan. - * @return array Array of font files. - */ -function scan_font_directory( $directory ) { - - $fonts = array(); - $theme_dir = dirname( __DIR__, 1 ); - $full_path = $theme_dir . '/' . $directory; - - - if ( ! is_dir( $full_path ) ) { - return $fonts; - } - - // Keywords to ignore when detecting font family from filename. - $ignore_keywords = [ - '100','200','300','400','500','600','700','800','900', - 'thin','extralight','light','regular','medium','semibold', - 'bold','extrabold','black','italic','oblique','normal' - ]; - - $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator( $full_path, \RecursiveDirectoryIterator::SKIP_DOTS ) - ); - - foreach ( $iterator as $file ) { - if ( $file->isFile() && in_array( strtolower( $file->getExtension() ), array( 'woff2', 'woff', 'ttf', 'otf' ), true ) ) { - $relative_path = str_replace( $theme_dir . '/', '', $file->getPathname() ); - $filename = $file->getBasename(); - $font_metadata = wdsbt_parse_font_meta_from_filename( $filename ); - - - // Detect font family from folder name (headline, body, mono). - $folder_name = basename( dirname( $file->getPathname() ) ); - - $variant_key = $font_metadata['family'] . '-' . $font_metadata['weight'] . '-' . $font_metadata['style']; - - if ( ! isset( $fonts[ $variant_key ] ) || - ( strpos( $relative_path, 'build/' ) === 0 && strpos( $fonts[ $variant_key ]['path'], 'assets/' ) === 0 ) ) { - $fonts[ $variant_key ] = array( - 'path' => $relative_path, - 'filename' => $filename, - 'extension' => $file->getExtension(), - 'family' => $font_metadata['family'], - 'weight' => $font_metadata['weight'], - 'style' => $font_metadata['style'], - ); - } - } - } - - return array_values( $fonts ); -} - - -/** - * Group fonts by family. - * - * @param array $fonts Array of font files. - * @return array Fonts grouped by family. - */ -function group_fonts_by_family( $fonts ) { - $grouped = array(); - - foreach ( $fonts as $font ) { - $family = $font['family']; - - if ( ! isset( $grouped[ $family ] ) ) { - $grouped[ $family ] = array( - 'name' => $family, - 'slug' => wdsbt_get_font_role_slug( $family ), - 'fontFamily' => $family . ', sans-serif', - 'fontFace' => array(), - ); - } - - $grouped[ $family ]['fontFace'][] = array( - 'fontFamily' => $family, - 'fontStyle' => $font['style'], - 'fontWeight' => $font['weight'], - 'src' => array( "file:./{$font['relative_path']}" ), - ); - } - - return $grouped; -} - -/** - * Sanitize title (simple version without WordPress dependency). - * - * @param string $title Title to sanitize. - * @return string Sanitized title. - */ -function sanitize_title( $title ) { - $title = strtolower( $title ); - $title = preg_replace( '/[^a-z0-9\s-]/', '', $title ); - $title = preg_replace( '/[\s-]+/', '-', $title ); - return trim( $title, '-' ); -} - /** * Generate theme.json with detected fonts. @@ -152,20 +49,9 @@ function generate_theme_json() { $all_fonts = array_merge( $build_fonts, $assets_fonts ); $unique_fonts = wdsbt_resolve_fonts( $all_fonts ); - // // Remove duplicates (build fonts take precedence). - // $unique_fonts = array(); - // $seen_paths = array(); - - // foreach ( $all_fonts as $font ) { - // $key = $font['family'] . '-' . $font['weight'] . '-' . $font['style']; - // if ( ! isset( $seen_paths[ $key ] ) ) { - // $unique_fonts[] = $font; - // $seen_paths[ $key ] = true; - // } - // } // Group fonts by family. - $font_families = group_fonts_by_family( $unique_fonts ); + $font_families = wdsbt_group_fonts_by_family( $unique_fonts ); // Ensure typography settings exist. if ( ! isset( $base_theme_json['settings']['typography'] ) ) { diff --git a/tools/helpers.php b/tools/helpers.php index ffa1ab28..bb3f4f61 100644 --- a/tools/helpers.php +++ b/tools/helpers.php @@ -19,64 +19,94 @@ require_once __DIR__ . '/font-pipeline-config.php'; - +/** + * Determine a font role slug based on folder structure or family name. + * + * Folder-based roles are detected from 'assets/fonts' or 'build/fonts'. + * Falls back to sanitized font family/filename if folder detection fails. + * + * @param array|string $family_or_path Font array with 'relative_path'/'family' or a family string. + * @param bool $debug Optional. If true, outputs debug info (default false). + * @return string Font role slug (e.g., 'body', 'headline', 'mono', or sanitized name). + */ if ( ! function_exists( __NAMESPACE__ . '\\wdsbt_get_font_role_slug' ) ) { - function wdsbt_get_font_role_slug( $family_or_path ) { - - // Slug mapping is intentionally opinionated - // Unsure the purpose of this - Potentially remove in future versions! - static $slug_mapping = array( - 'Oxygen' => 'body', - 'Inter' => 'headline', - 'Roboto Mono' => 'mono', - ); + function wdsbt_get_font_role_slug( $family_or_path, $debug = false ) { - if ( is_array( $family_or_path ) ) { - if ( ! empty( $family_or_path['path'] ) ) { - $candidate = $family_or_path['path']; - } elseif ( ! empty( $family_or_path['family'] ) ) { - $candidate = $family_or_path['family']; - } else { - $candidate = ''; - } - } else { - $candidate = (string) $family_or_path; - } + // Determine candidate string + if ( is_array( $family_or_path ) ) { + $candidate = $family_or_path['relative_path'] ?? $family_or_path['path'] ?? $family_or_path['family'] ?? ''; + echo "[DEBUG] candidate path: $candidate\n"; + } else { + $candidate = (string) $family_or_path; + } + + // Normalize path separators (Windows → Unix style) + $normalized = str_replace('\\', '/', $candidate); - // Normalize separators - $normalized = str_replace( '\\', '/', $candidate ); - // Folder-based slug - if ( false !== strpos( $normalized, '/fonts/' ) ) { - $after = explode( '/fonts/', $normalized, 2 )[1]; - $segments = explode( '/', trim( $after, '/' ) ); - if ( ! empty( $segments[0] ) ) { - return wdsbt_sanitize_title( $segments[0] ); + if ( strpos($normalized, '/fonts/') !== false ) { + $after = explode('/fonts/', $normalized, 2)[1]; + $segments = explode('/', trim($after, '/')); + if ( ! empty($segments[0]) ) { + return wdsbt_sanitize_title($segments[0]); } } - // Family name mapping - foreach ( $slug_mapping as $key => $value ) { - if ( 0 === strcasecmp( $candidate, $key ) ) { - // printf( "Default mapping found and used for " . $candidate . "\n" ); - return $value; + // Fallback: strip extension and sanitize + $base = basename($normalized); + $base = preg_replace('/\.[^.]+$/', '', $base); + + $slug = $base ? wdsbt_sanitize_title($base) : 'unknown'; + if ( $debug ) echo "[DEBUG] returning fallback slug: $slug\n"; + + return $slug; + } +} + + +/** + * Group fonts by family. + * + * @param array $fonts List of resolved font variants. + * @return array Fonts grouped into theme.json family entries. + */ +if ( ! function_exists( __NAMESPACE__ . '\\wdsbt_group_fonts_by_family' ) ) { + function wdsbt_group_fonts_by_family( array $fonts ) { + + $grouped = array(); + + foreach ( $fonts as $font ) { + + $family = $font['family']; + + // Use family/path for role_slug inference + $slug = wdsbt_get_font_role_slug( $font, true ); // function will pick relative_path first + $relative_path = str_replace('\\', '/', $font['relative_path']); + + if ( ! isset( $grouped[ $family ] ) ) { + $grouped[ $family ] = array( + 'name' => $family, + 'slug' => $slug, + 'fontFamily' => "{$family}, sans-serif", + 'fontFace' => array(), + ); } - } - // Fallback: strip extension from filename and sanitize - $base = $normalized; - if ( false !== strpos( $base, '/' ) ) { - $base = basename( $base ); + $theme_slug = basename(dirname(__DIR__)); + + $grouped[ $family ]['fontFace'][] = array( + 'fontFamily' => $family, + 'fontStyle' => $font['style'], + 'fontWeight' => $font['weight'], + 'src' => [ "file:./{$font['relative_path']}" ], + ); } - $base = preg_replace( '/\.[^.]+$/', '', $base ); - return ! empty( $base ) ? wdsbt_sanitize_title( $base ) : 'unknown'; + return $grouped; } } - - /** * Sanitize a string into a URL/title-safe slug. * @@ -123,7 +153,9 @@ function wdsbt_scan_font_directory_raw( $directory, $theme_dir = null ) { ) { $filename = $file->getBasename( '.' . $file->getExtension() ); - $relative_path = str_replace( rtrim( $theme_dir, '/' ) . '/', '', $file->getPathname() ); + + // Normalize relative path to forward slashes + $relative_path = str_replace('\\', '/', str_replace( rtrim( $theme_dir, '/' ) . '/', '', $file->getPathname() )); $font_metadata = wdsbt_parse_font_meta_from_filename( $filename ); @@ -242,23 +274,31 @@ function wdsbt_print_font_summary( $fonts, $grouped = false, $label = 'Fonts' ) function wdsbt_parse_font_meta_from_filename( $filename ) { + // Default values. $metadata = array( 'family' => 'Unknown', 'weight' => '400', 'style' => 'normal', ); + // Common font family patterns. $family_patterns = array( - 'inter' => 'Inter', - 'oxygen' => 'Oxygen', - 'roboto-mono' => 'Roboto Mono', - 'roboto' => 'Roboto', - 'open-sans' => 'Open Sans', - 'lato' => 'Lato', - 'poppins' => 'Poppins', - 'montserrat' => 'Montserrat', - 'raleway' => 'Raleway', - 'playfair' => 'Playfair Display', + 'inter' => 'Inter', + 'oxygen' => 'Oxygen', + 'roboto-mono' => 'Roboto Mono', + 'roboto' => 'Roboto', + 'open-sans' => 'Open Sans', + 'lato' => 'Lato', + 'poppins' => 'Poppins', + 'montserrat' => 'Montserrat', + 'raleway' => 'Raleway', + 'playfair' => 'Playfair Display', + 'source-sans' => 'Source Sans Pro', + 'noto-sans' => 'Noto Sans', + 'nunito' => 'Nunito', + 'merriweather' => 'Merriweather', + 'ubuntu' => 'Ubuntu', + 'oswald' => 'Oswald', ); $weight_patterns = array( @@ -344,6 +384,7 @@ function wdsbt_parse_font_meta_from_filename( $filename ) { } } + // Detect font weight - use exact matches. foreach ( $weight_patterns as $pattern => $weight ) { if ( strpos( $lowercase_filename, $pattern ) !== false ) { $metadata['weight'] = $weight; @@ -351,6 +392,7 @@ function wdsbt_parse_font_meta_from_filename( $filename ) { } } + // Detect font style. foreach ( $style_patterns as $pattern => $style ) { if ( strpos( $lowercase_filename, $pattern ) !== false ) { $metadata['style'] = $style;