$cssFiles */ public function scanComponents(array $cssFiles): ComponentRegistry { $components = []; foreach ($cssFiles as $cssFile) { if (! $cssFile instanceof FilePath) { $cssFile = new FilePath($cssFile); } if (! $cssFile->exists()) { continue; } $cssContent = file_get_contents($cssFile->toString()); if ($cssContent === false) { continue; } $fileComponents = $this->extractComponentsFromCss($cssContent, $cssFile->toString()); $components = array_merge($components, $fileComponents); } return new ComponentRegistry($components); } /** * @return array */ private function extractComponentsFromCss(string $cssContent, string $filePath): array { $components = []; $processedComponents = []; // Remove comments $cleanedContent = preg_replace('/\/\*.*?\*\//s', '', $cssContent); $cssContent = $cleanedContent !== null ? $cleanedContent : $cssContent; // Find all CSS selectors with improved regex that handles nested braces preg_match_all('/([^{}]+)\s*{([^{}]*(?:{[^{}]*}[^{}]*)*)}/s', $cssContent, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $selectors = $match[1]; $cssRules = trim($match[2]); // Skip empty rules if (empty($cssRules)) { continue; } // Clean and split selectors $selectorList = array_map('trim', explode(',', $selectors)); foreach ($selectorList as $selector) { // Clean up the selector $selector = trim($selector); // Skip @rules, :root, and other non-component selectors if (strpos($selector, '@') === 0 || $selector === ':root' || empty($selector)) { continue; } $component = $this->analyzeSelector($selector, $cssRules, $filePath); if ($component !== null) { // Avoid duplicates based on component name $key = $component->name . '_' . $component->state->value; if (! isset($processedComponents[$key])) { $components[] = $component; $processedComponents[$key] = true; } } } } return $components; } private function analyzeSelector(string $selector, string $cssRules, string $filePath): ?Component { // Clean up selector - remove :where, :is wrappers $cleanedSelector = preg_replace('/:where\s*\((.*?)\)/', '$1', $selector); $selector = $cleanedSelector !== null ? $cleanedSelector : $selector; $cleanedSelector = preg_replace('/:is\s*\((.*?)\)/', '$1', $selector); $selector = $cleanedSelector !== null ? $cleanedSelector : $selector; // Ensure selector is non-empty string if (empty($selector)) { return null; } // Skip pseudo-elements and certain pseudo-classes if (preg_match('/::/', $selector) || preg_match('/:not\(/', $selector)) { return null; } // Skip complex selectors that are not component-like if (preg_match('/^\s*(html|body|main|header|footer|section|article|aside|nav|h1|h2|h3|h4|h5|h6|p|div|span|a|ul|ol|li|img|br|hr)\s*$/i', $selector)) { return null; } // Skip overly complex selectors (more than 3 parts) if (substr_count($selector, ' ') > 3) { return null; } // Skip selectors that are clearly not components if (preg_match('/^\s*(\*|::before|::after|\[|>|\+|~)/i', $selector)) { return null; } $componentName = null; // Extract main class name - improved regex to handle more cases if (preg_match('/\.([a-zA-Z][a-zA-Z0-9_-]*)(?:\s|:|$|>|\+|~|\[|,)/', $selector, $matches)) { $componentName = $matches[1]; // Skip utility-only classes if (strlen($componentName) <= 2) { return null; } } // Also handle element-based components (like button, input) - but be more selective elseif (preg_match('/^(button|input|select|textarea|table|form|dialog)(?:\s|:|$|\[)/', $selector, $matches)) { $componentName = $matches[1]; } // Handle attribute selectors like input[type="button"] elseif (preg_match('/(button|input|select|textarea)\[([^\]]+)\]/', $selector, $matches)) { $elementType = $matches[1]; $attributes = $matches[2]; // Create a meaningful name based on element and attributes if (preg_match('/type\s*=\s*["\']([^"\']+)["\']/', $attributes)) { preg_match('/type\s*=\s*["\']([^"\']+)["\']/', $attributes, $typeMatches); $componentName = $elementType . '-' . $typeMatches[1]; } else { $componentName = $elementType; } } if ($componentName !== null) { // Categorize component $category = $this->categorizeComponent($componentName); $pattern = $this->detectPattern($componentName); $state = $this->extractState($selector); return new Component( name: $componentName, selector: $selector, cssRules: $cssRules, category: $category, pattern: $pattern, state: $state, filePath: $filePath ); } return null; } private function categorizeComponent(string $className): ComponentCategory { $lowerName = strtolower($className); // Button components if (preg_match('/(btn|button)/i', $lowerName)) { return ComponentCategory::BUTTON; } // Navigation components if (preg_match('/(nav|menu|breadcrumb|tab|sidebar|aside)/i', $lowerName)) { return ComponentCategory::NAVIGATION; } // Form components if (preg_match('/(form|input|select|checkbox|radio|field|autosave|textarea)/i', $lowerName)) { return ComponentCategory::FORM; } // Card components if (preg_match('/(card|panel|box|tile)/i', $lowerName)) { return ComponentCategory::CARD; } // Alert/notification components if (preg_match('/(alert|notification|message|toast|status|error|warning|success)/i', $lowerName)) { return ComponentCategory::FEEDBACK; } // Layout components if (preg_match('/(container|grid|row|col|layout|wrapper|section|main|article)/i', $lowerName)) { return ComponentCategory::LAYOUT; } // Typography components if (preg_match('/(heading|title|text|font|headline|paragraph)/i', $lowerName)) { return ComponentCategory::TYPOGRAPHY; } // Additional specific components if (preg_match('/(header|footer|lightbox|modal|dialog)/i', $lowerName)) { return ComponentCategory::LAYOUT; } if (preg_match('/(csrf)/i', $lowerName)) { return ComponentCategory::FORM; } // Element-based components if (preg_match('/^(table)$/i', $lowerName)) { return ComponentCategory::LAYOUT; } return ComponentCategory::OTHER; } private function detectPattern(string $className): ComponentPattern { // BEM pattern (block__element--modifier) if (preg_match('/^[a-z][a-z0-9]*(__[a-z][a-z0-9]*)?(-{2}[a-z][a-z0-9]*)?$/', $className)) { return ComponentPattern::BEM; } // Utility pattern (single purpose, often prefixed) if (preg_match('/^(m|p|text|bg|border|flex|grid|w|h)-/', $className)) { return ComponentPattern::UTILITY; } // Traditional component pattern return ComponentPattern::TRADITIONAL; } private function extractState(string $selector): ComponentState { if (strpos($selector, ':hover') !== false) { return ComponentState::HOVER; } if (strpos($selector, ':focus') !== false) { return ComponentState::FOCUS; } if (strpos($selector, ':active') !== false) { return ComponentState::ACTIVE; } if (strpos($selector, ':disabled') !== false) { return ComponentState::DISABLED; } return ComponentState::DEFAULT; } }