#!/usr/bin/env php * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: moko-platform.CLI * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/theme_lint.php * BRIEF: Lint theme files — CSS syntax, image sizes, hardcoded URLs * * Usage: * php theme_lint.php --path /repo * php theme_lint.php --path /repo --max-image-kb 500 * php theme_lint.php --path /repo --github-output * * Options: * --path Repository root (default: .) * --max-image-kb Maximum image file size in KB (default: 500) * --github-output Export results to $GITHUB_OUTPUT * --strict Exit 1 on any warning (default: only on errors) */ declare(strict_types=1); $path = '.'; $maxImageKb = 500; $ghOutput = false; $strict = false; foreach ($argv as $i => $arg) { if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; if ($arg === '--max-image-kb' && isset($argv[$i + 1])) $maxImageKb = (int)$argv[$i + 1]; if ($arg === '--github-output') $ghOutput = true; if ($arg === '--strict') $strict = true; } $root = realpath($path) ?: $path; $errors = 0; $warnings = 0; // ── Find source directory ─────────────────────────────────────────────── $srcDir = null; foreach (['src', 'htdocs'] as $d) { if (is_dir("{$root}/{$d}")) { $srcDir = "{$root}/{$d}"; break; } } if ($srcDir === null) { fwrite(STDERR, "No src/ or htdocs/ directory in {$root}\n"); exit(1); } echo "Theme Lint: {$srcDir}\n\n"; // ── Check 1: CSS syntax validation ────────────────────────────────────── echo "--- CSS Syntax ---\n"; $cssFiles = findFiles($srcDir, '*.css'); $cssMinFiles = findFiles($srcDir, '*.min.css'); $cssToCheck = array_diff($cssFiles, $cssMinFiles); if (empty($cssToCheck)) { echo " No CSS files to check\n"; } else { foreach ($cssToCheck as $file) { $content = file_get_contents($file); $relPath = str_replace($root . '/', '', $file); // Check for unmatched braces $openBraces = substr_count($content, '{'); $closeBraces = substr_count($content, '}'); if ($openBraces !== $closeBraces) { echo " ERROR: {$relPath}: unmatched braces (open={$openBraces}, close={$closeBraces})\n"; $errors++; } // Check for empty rules if (preg_match_all('/\{[\s]*\}/', $content, $m)) { $count = count($m[0]); echo " WARN: {$relPath}: {$count} empty rule(s)\n"; $warnings++; } // Check for !important abuse (more than 10 in one file) $importantCount = substr_count($content, '!important'); if ($importantCount > 10) { echo " WARN: {$relPath}: {$importantCount} !important declarations (consider refactoring)\n"; $warnings++; } } if ($errors === 0) { echo " OK: " . count($cssToCheck) . " CSS file(s) checked\n"; } } // ── Check 2: Image file sizes ─────────────────────────────────────────── echo "\n--- Image Sizes (max {$maxImageKb}KB) ---\n"; $imageExts = ['*.jpg', '*.jpeg', '*.png', '*.gif', '*.webp', '*.svg', '*.bmp']; $images = []; foreach ($imageExts as $ext) { $images = array_merge($images, findFiles($srcDir, $ext)); } // Also check root images/ directory if (is_dir("{$root}/images")) { foreach ($imageExts as $ext) { $images = array_merge($images, findFiles("{$root}/images", $ext)); } } $oversized = 0; $totalSize = 0; foreach ($images as $file) { $size = filesize($file); $totalSize += $size; $relPath = str_replace($root . '/', '', $file); $sizeKb = round($size / 1024); if ($sizeKb > $maxImageKb) { echo " WARN: {$relPath}: {$sizeKb}KB (exceeds {$maxImageKb}KB limit)\n"; $oversized++; $warnings++; } } $totalMb = round($totalSize / 1024 / 1024, 1); echo " " . count($images) . " image(s), {$totalMb}MB total"; if ($oversized > 0) { echo ", {$oversized} oversized"; } echo "\n"; // ── Check 3: Hardcoded URLs in CSS/JS ─────────────────────────────────── echo "\n--- Hardcoded URLs ---\n"; $codeFiles = array_merge( findFiles($srcDir, '*.css'), findFiles($srcDir, '*.js') ); // Exclude minified files $codeFiles = array_filter($codeFiles, function($f) { return !preg_match('/\.min\.(css|js)$/', $f); }); $urlPatterns = [ '/https?:\/\/clarksvillefurs\.com/' => 'hardcoded production URL', '/https?:\/\/[a-z]+\.dev\.mokoconsulting\.tech/' => 'hardcoded dev URL', '/https?:\/\/localhost/' => 'localhost reference', ]; $urlIssues = 0; foreach ($codeFiles as $file) { $content = file_get_contents($file); $relPath = str_replace($root . '/', '', $file); foreach ($urlPatterns as $pattern => $desc) { if (preg_match_all($pattern, $content, $matches)) { $count = count($matches[0]); echo " WARN: {$relPath}: {$count} {$desc}\n"; $urlIssues++; $warnings++; } } } if ($urlIssues === 0) { echo " OK: No hardcoded URLs found\n"; } // ── Summary ───────────────────────────────────────────────────────────── echo "\n=== Summary ===\n"; echo "Errors: {$errors}\n"; echo "Warnings: {$warnings}\n"; if ($ghOutput) { $ghFile = getenv('GITHUB_OUTPUT'); if ($ghFile) { file_put_contents($ghFile, "lint_errors={$errors}\n", FILE_APPEND); file_put_contents($ghFile, "lint_warnings={$warnings}\n", FILE_APPEND); file_put_contents($ghFile, "lint_images=" . count($images) . "\n", FILE_APPEND); file_put_contents($ghFile, "lint_css=" . count($cssToCheck) . "\n", FILE_APPEND); } } if ($errors > 0) { exit(1); } if ($strict && $warnings > 0) { exit(1); } exit(0); // ── Helper: recursively find files matching a glob pattern ────────────── function findFiles(string $dir, string $pattern): array { $results = []; if (!is_dir($dir)) return $results; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS) ); foreach ($iterator as $file) { if (fnmatch($pattern, $file->getFilename())) { $results[] = $file->getPathname(); } } return $results; }