Implement JQHTML function cache ID system and fix bundle compilation Implement underscore prefix for system tables Fix JS syntax linter to support decorators and grant exception to Task system SPA: Update planning docs and wishlists with remaining features SPA: Document Navigation API abandonment and future enhancements Implement SPA browser integration with History API (Phase 1) Convert contacts view page to SPA action Convert clients pages to SPA actions and document conversion procedure SPA: Merge GET parameters and update documentation Implement SPA route URL generation in JavaScript and PHP Implement SPA bootstrap controller architecture Add SPA routing manual page (rsx:man spa) Add SPA routing documentation to CLAUDE.md Phase 4 Complete: Client-side SPA routing implementation Update get_routes() consumers for unified route structure Complete SPA Phase 3: PHP-side route type detection and is_spa flag Restore unified routes structure and Manifest_Query class Refactor route indexing and add SPA infrastructure Phase 3 Complete: SPA route registration in manifest Implement SPA Phase 2: Extract router code and test decorators Rename Jqhtml_Component to Component and complete SPA foundation setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
289 lines
10 KiB
PHP
Executable File
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 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 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 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'] === '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;
|
|
}
|
|
}
|
|
} |