Files
rspade_system/app/RSpade/Integrations/Scss/Scss_ManifestModule.php
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 02:08:33 +00:00

289 lines
10 KiB
PHP
Executable File

<?php
namespace App\RSpade\Integrations\Scss;
use App\RSpade\Core\Manifest\ManifestModule_Abstract;
/**
* Module for processing SCSS/CSS files in the manifest
*/
class Scss_ManifestModule extends ManifestModule_Abstract
{
/**
* Get file extensions this module handles
*/
public function handles(): array
{
return ['scss', 'sass', 'css'];
}
/**
* Get processing priority
*/
public function priority(): int
{
return 1000; // Lowest priority, process after all other modules including Jqhtml
}
/**
* Process a SCSS/CSS file and extract metadata
*
* For SCSS files, also detects if the file has a single top-level class selector
* that matches a Blade view ID, JavaScript class extending Jqhtml_Component,
* or jqhtml template ID in the manifest.
*/
public function process(string $file_path, array $metadata): array
{
$content = file_get_contents($file_path);
$extension = pathinfo($file_path, PATHINFO_EXTENSION);
$metadata['type'] = 'stylesheet';
$metadata['format'] = $extension;
// Extract imports
$imports = [];
// SCSS/Sass @import
if (preg_match_all('/@import\s+[\'"]([^\'"]+)[\'"];/', $content, $matches)) {
$imports = array_merge($imports, $matches[1]);
}
// CSS @import
if (preg_match_all('/@import\s+url\s*\(\s*[\'"]?([^\'")\s]+)[\'"]?\s*\)/', $content, $matches)) {
$imports = array_merge($imports, $matches[1]);
}
// SCSS/Sass @use
if (preg_match_all('/@use\s+[\'"]([^\'"]+)[\'"]/', $content, $matches)) {
$imports = array_merge($imports, $matches[1]);
}
if (!empty($imports)) {
$metadata['imports'] = array_unique($imports);
}
// Extract variables (SCSS/Sass)
if ($extension === 'scss' || $extension === 'sass') {
$variables = [];
if (preg_match_all('/\$([a-zA-Z_][\w-]*)\s*:/', $content, $matches)) {
$variables = $matches[1];
}
if (!empty($variables)) {
$metadata['variables'] = array_unique($variables);
}
}
// Extract mixins (SCSS/Sass)
if ($extension === 'scss' || $extension === 'sass') {
$mixins = [];
if (preg_match_all('/@mixin\s+([a-zA-Z_][\w-]*)/', $content, $matches)) {
$mixins = $matches[1];
}
if (!empty($mixins)) {
$metadata['mixins'] = array_unique($mixins);
}
}
// Extract functions (SCSS/Sass)
if ($extension === 'scss' || $extension === 'sass') {
$functions = [];
if (preg_match_all('/@function\s+([a-zA-Z_][\w-]*)/', $content, $matches)) {
$functions = $matches[1];
}
if (!empty($functions)) {
$metadata['functions'] = array_unique($functions);
}
}
// Extract extends/placeholders (SCSS/Sass)
if ($extension === 'scss' || $extension === 'sass') {
$placeholders = [];
if (preg_match_all('/%([a-zA-Z_][\w-]*)/', $content, $matches)) {
$placeholders = $matches[1];
}
if (!empty($placeholders)) {
$metadata['placeholders'] = array_unique($placeholders);
}
}
// Extract main selectors (top-level classes/IDs)
$selectors = [];
// Remove comments to avoid false positives
$clean_content = preg_replace('/\/\*.*?\*\//s', '', $content);
$clean_content = preg_replace('/\/\/.*$/m', '', $clean_content);
// Extract class selectors
if (preg_match_all('/^\.([a-zA-Z_][\w-]*)/m', $clean_content, $matches)) {
foreach ($matches[1] as $class) {
$selectors[] = '.' . $class;
}
}
// Extract ID selectors
if (preg_match_all('/^#([a-zA-Z_][\w-]*)/m', $clean_content, $matches)) {
foreach ($matches[1] as $id) {
$selectors[] = '#' . $id;
}
}
if (!empty($selectors)) {
$metadata['selectors'] = array_unique($selectors);
}
// Check for single top-level class selector pattern for SCSS files
if ($extension === 'scss') {
$this->detect_scss_id($clean_content, $metadata);
}
// Check if it's a partial (starts with underscore)
$filename = basename($file_path);
if (str_starts_with($filename, '_')) {
$metadata['is_partial'] = true;
}
// Determine scope based on path
$relative_path = str_replace(base_path() . '/', '', $file_path);
$metadata['relative_path'] = $relative_path;
if (str_contains($relative_path, '/pages/')) {
$metadata['scope'] = 'page';
} elseif (str_contains($relative_path, '/components/')) {
$metadata['scope'] = 'component';
} elseif (str_contains($relative_path, '/layouts/')) {
$metadata['scope'] = 'layout';
} elseif (str_contains($relative_path, '/utilities/') || str_contains($relative_path, '/utils/')) {
$metadata['scope'] = 'utility';
} elseif (str_contains($relative_path, '/base/') || str_contains($relative_path, '/foundation/')) {
$metadata['scope'] = 'base';
} else {
$metadata['scope'] = 'general';
}
// Check for media queries
if (preg_match_all('/@media\s+([^{]+)/', $content, $matches)) {
$media_queries = [];
foreach ($matches[1] as $query) {
$query = trim($query);
if (str_contains($query, 'min-width')) {
$media_queries[] = 'responsive';
}
if (str_contains($query, 'print')) {
$media_queries[] = 'print';
}
if (str_contains($query, 'prefers-color-scheme')) {
$media_queries[] = 'dark-mode';
}
}
if (!empty($media_queries)) {
$metadata['media_features'] = array_unique($media_queries);
}
}
// Check for CSS custom properties (CSS variables)
$css_vars = [];
if (preg_match_all('/--([a-zA-Z][\w-]*)/', $content, $matches)) {
$css_vars = $matches[1];
}
if (!empty($css_vars)) {
$metadata['css_variables'] = array_unique($css_vars);
}
// Check for Bootstrap usage
if (preg_match('/\.(btn|col-|row|container|navbar|modal|card|form-control)/', $content)) {
$metadata['uses_bootstrap'] = true;
}
// Check for Font Awesome usage
if (preg_match('/\.(fa-|fas|far|fab|fal|fad)/', $content)) {
$metadata['uses_fontawesome'] = true;
}
return $metadata;
}
/**
* Detect if SCSS file has a single top-level class that qualifies as an ID
*
* The SCSS file gets an 'id' if:
* 1. All rules are contained within a single top-level class selector
* 2. The class name matches a Blade view ID, JS class extending Jqhtml_Component, or jqhtml template
* 3. No other SCSS file already has this ID
*/
protected function detect_scss_id(string $clean_content, array &$metadata): void
{
// Remove all whitespace and newlines for easier parsing
$compact = preg_replace('/\s+/', ' ', trim($clean_content));
// Check if content starts with a single class selector and everything is inside it
// Pattern: .ClassName { ... everything ... }
if (!preg_match('/^\.([A-Z][a-zA-Z0-9_]+)\s*\{(.*)\}\s*$/', $compact, $matches)) {
return;
}
$class_name = $matches[1];
$inner_content = $matches[2];
// Verify there are no other top-level rules by checking for unmatched closing braces
// Count opening and closing braces in the inner content
$open_braces = substr_count($inner_content, '{');
$close_braces = substr_count($inner_content, '}');
// If braces are balanced, everything is contained within the main selector
if ($open_braces !== $close_braces) {
return;
}
// Now check if this class name matches something in the manifest
// During build, we need to access the in-memory manifest data
// The get_all() method returns the cached data, not the in-progress build
// We need a different approach - access the static data directly
// Get access to the Manifest class's internal data using reflection
$reflection = new \ReflectionClass(\App\RSpade\Core\Manifest\Manifest::class);
$data_property = $reflection->getProperty('data');
$data_property->setAccessible(true);
$manifest_state = $data_property->getValue();
if (!isset($manifest_state['data']['files'])) {
return;
}
$manifest_data = $manifest_state['data']['files'];
$found_match = false;
$scss_id_already_exists = false;
foreach ($manifest_data as $file_data) {
// Check if another SCSS file already has this ID
if (isset($file_data['id']) && $file_data['id'] === $class_name &&
isset($file_data['extension']) && $file_data['extension'] === 'scss') {
$scss_id_already_exists = true;
break;
}
// Check for matching ID in Blade view or jqhtml template
if (isset($file_data['id']) && $file_data['id'] === $class_name) {
$found_match = true;
}
// Check for JavaScript class extending Jqhtml_Component
if (isset($file_data['extension']) && $file_data['extension'] === 'js' &&
isset($file_data['class']) && $file_data['class'] === $class_name &&
isset($file_data['extends']) && $file_data['extends'] === 'Jqhtml_Component') {
$found_match = true;
}
}
// Only set ID if we found a match and no other SCSS has this ID
if ($found_match && !$scss_id_already_exists) {
$metadata['id'] = $class_name;
}
}
}