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..ccafd0ec 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 dirname(__DIR__, 2) . '/tools/helpers.php'; + /** * Get all available font files from a directory. * @@ -185,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(), ); @@ -235,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 0975d172..3823d85e 100644 --- a/tools/font-detection.php +++ b/tools/font-detection.php @@ -8,92 +8,9 @@ // 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', - ); +namespace WebDevStudios\wdsbt; - $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. @@ -102,6 +19,7 @@ function wdsbt_parse_font_filename( $filename ) { * @return array Array of font files. */ function wdsbt_scan_font_directory( $directory ) { + $fonts = array(); $theme_dir = dirname( __DIR__, 1 ); $full_path = $theme_dir . '/' . $directory; @@ -119,7 +37,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 +65,8 @@ 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['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 @@ -157,8 +76,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..11159e90 --- /dev/null +++ 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/font-processor.php b/tools/font-processor.php index a8a44db2..f8abadaa 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() ); @@ -100,9 +109,20 @@ 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 ); - - // Always use the detected family from the filename. The folder name is only used for purpose. + $font_metadata = wdsbt_parse_font_meta_from_filename( $filename ); + + // 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(), @@ -119,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. @@ -216,7 +158,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 = wdsbt_get_font_role_slug( $font['path'] ); $family_dir = $full_output_dir . '/' . $standardized_slug; if ( ! is_dir( $family_dir ) ) { @@ -287,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_role_slug( $font['family'] ); $php .= " 'fonts/{$standardized_slug}/{$font['filename']}' => '{$format}',\n"; } @@ -313,21 +256,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 +263,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 +319,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..27a74fe8 100644 --- a/tools/generate-theme-json.php +++ b/tools/generate-theme-json.php @@ -9,230 +9,14 @@ namespace WebDevStudios\wdsbt; -/** - * 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; - } - - $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 = 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. - - $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 ); -} - -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. - * - * @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' => get_font_slug( $family ), - 'fontFamily' => $family . ', sans-serif', - 'fontFace' => array(), - ); - } +require_once __DIR__ . '/helpers.php'; - $grouped[ $family ]['fontFace'][] = array( - 'fontFamily' => $family, - 'fontStyle' => $font['style'], - 'fontWeight' => $font['weight'], - 'src' => array( "file:./{$font['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, '-' ); -} - -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. */ function generate_theme_json() { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped printf( "\nScanning for fonts...\n" ); @@ -250,27 +34,24 @@ 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' ); + // $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 = 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(); - - 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'] ) ) { @@ -299,13 +80,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. @@ -317,10 +98,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 new file mode 100644 index 00000000..bb3f4f61 --- /dev/null +++ b/tools/helpers.php @@ -0,0 +1,405 @@ + $family, + 'slug' => $slug, + 'fontFamily' => "{$family}, sans-serif", + 'fontFace' => array(), + ); + } + + $theme_slug = basename(dirname(__DIR__)); + + $grouped[ $family ]['fontFace'][] = array( + 'fontFamily' => $family, + 'fontStyle' => $font['style'], + 'fontWeight' => $font['weight'], + 'src' => [ "file:./{$font['relative_path']}" ], + ); + } + + return $grouped; + } +} + + +/** + * Sanitize a string into a URL/title-safe slug. + * + * @param string $text + * @return string + */ +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; + } +} + + +/** + * 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() ); + + // 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 ); + + $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. + * + * @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 ) { + + // 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', + ); + + $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 ); + + // 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 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 ) ); + } + } + } + + // 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; + } +}