#!/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 */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; class ThemeLintCli extends CliFramework { protected function configure(): void { $this->setDescription('Lint theme files -- CSS syntax, image sizes, hardcoded URLs'); $this->addArgument('--path', 'Repository root', '.'); $this->addArgument('--max-image-kb', 'Maximum image file size in KB', '500'); $this->addArgument('--github-output', 'Export results to $GITHUB_OUTPUT', false); $this->addArgument('--strict', 'Exit 1 on any warning', false); } protected function run(): int { $path = $this->getArgument('--path'); $maxImageKb = (int) $this->getArgument('--max-image-kb'); $ghOutput = (bool) $this->getArgument('--github-output'); $strict = (bool) $this->getArgument('--strict'); $root = realpath($path) ?: $path; $errors = 0; $warnings = 0; $srcDir = null; foreach (['src', 'htdocs'] as $d) { if (is_dir("{$root}/{$d}")) { $srcDir = "{$root}/{$d}"; break; } } if ($srcDir === null) { $this->log('ERROR', "No src/ or htdocs/ directory in {$root}"); return 1; } echo "Theme Lint: {$srcDir}\n\n"; echo "--- CSS Syntax ---\n"; $cssFiles = $this->findFiles($srcDir, '*.css'); $cssMinFiles = $this->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); $openBraces = substr_count($content, '{'); $closeBraces = substr_count($content, '}'); if ($openBraces !== $closeBraces) { echo " ERROR: {$relPath}: unmatched braces (open={$openBraces}, close={$closeBraces})\n"; $errors++; } if (preg_match_all('/\{[\s]*\}/', $content, $m)) { $count = count($m[0]); echo " WARN: {$relPath}: {$count} empty rule(s)\n"; $warnings++; } $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"; } } 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, $this->findFiles($srcDir, $ext)); } if (is_dir("{$root}/images")) { foreach ($imageExts as $ext) { $images = array_merge($images, $this->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"; echo "\n--- Hardcoded URLs ---\n"; $codeFiles = array_merge($this->findFiles($srcDir, '*.css'), $this->findFiles($srcDir, '*.js')); $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"; } 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) { return 1; } if ($strict && $warnings > 0) { return 1; } return 0; } private 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; } } $app = new ThemeLintCli(); exit($app->execute());