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>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
<?php
namespace App\RSpade\Integrations\Jqhtml;
use App\RSpade\Core\Manifest\Manifest;
/**
* Jqhtml - Public-facing JQHTML integration class
*
* This class provides the main public API for working with JQHTML components
* in PHP/Blade templates. It outputs initialization divs that are processed
* by client-side JavaScript to mount the actual components.
*/
class Jqhtml
{
/**
* Render a JQHTML component initialization div
*
* @param string $component_name Component class name (e.g., 'User_Card')
* @param array $args Arguments to pass to the component (becomes this.args)
* @param string $slot_content Inner HTML content for the component slot
* @return string HTML div with initialization attributes
*/
public static function component(string $component_name, array $args = [], string $slot_content = ''): string
{
if ($slot_content !== '') {
return sprintf(
'<div class="Jqhtml_Component_Init" data-component-init-name="%s" data-component-args="%s">%s</div>',
htmlspecialchars($component_name),
htmlspecialchars(json_encode($args), ENT_QUOTES, 'UTF-8'),
$slot_content
);
}
return sprintf(
'<div class="Jqhtml_Component_Init" data-component-init-name="%s" data-component-args="%s"></div>',
htmlspecialchars($component_name),
htmlspecialchars(json_encode($args), ENT_QUOTES, 'UTF-8')
);
}
/**
* Get JQHTML template metadata by component ID
*
* @param string $template_id Component name (e.g., 'Counter_Widget')
* @return array|null Template file metadata from manifest, or null if not found
*/
public static function get_jqhtml_template_by_id(string $template_id): ?array
{
$manifest = Manifest::get_full_manifest();
// Look up component in jqhtml index
if (!isset($manifest['data']['jqhtml']['components'][$template_id])) {
// INTENTIONAL DEVIATION: Return null for missing template instead of throwing exception
// This allows callers to gracefully handle missing components
return null;
}
$component = $manifest['data']['jqhtml']['components'][$template_id];
// Get template file path
if (!isset($component['template_file'])) {
shouldnt_happen("JQHTML component '{$template_id}' has no template_file in manifest");
}
$template_file = $component['template_file'];
// Get full file metadata
if (!isset($manifest['data']['files'][$template_file])) {
shouldnt_happen("JQHTML template file '{$template_file}' for component '{$template_id}' not found in manifest files");
}
return $manifest['data']['files'][$template_file];
}
}

View File

@@ -0,0 +1,365 @@
<?php
namespace App\RSpade\Integrations\Jqhtml;
/**
* Custom Blade precompiler for jqhtml components
*
* Transforms uppercase component tags into jqhtml component calls.
* Example: <User_Card name="John" /> becomes Jqhtml::component('User_Card', ['name' => 'John'])
*/
class JqhtmlBladeCompiler
{
/**
* Cached list of jqhtml component names from manifest
*/
private static ?array $jqhtml_components = null;
/**
* Get list of jqhtml components from manifest
*/
private static function __get_jqhtml_components(): array
{
if (self::$jqhtml_components === null) {
// Get the cached list of jqhtml components from the manifest support module
self::$jqhtml_components = \App\RSpade\Integrations\Jqhtml\Jqhtml_ManifestSupport::get_jqhtml_components();
}
return self::$jqhtml_components;
}
/**
* Precompile Blade template to transform jqhtml component tags
*
* @param string $value The Blade template content
* @return string Transformed content
*/
public static function precompile(string $value): string
{
// Pattern to match tags that start with uppercase letter
// Matches both self-closing and paired tags
$pattern = '/<([A-Z][a-zA-Z0-9_]*)((?:\s+\$?[a-zA-Z0-9_\-:]+(?:=(?:"[^"]*"|\'[^\']*\'|[^>\s]+))?)*)\s*(?:\/>|>(.*?)<\/\1>)/s';
$value = preg_replace_callback($pattern, function ($matches) {
$component_name = $matches[1];
$attributes = $matches[2] ?? '';
$slot_content = $matches[3] ?? null;
// Parse attributes into array
$parsed_attrs = self::__parse_attributes($attributes);
// Convert to PHP array syntax
$php_array = self::__to_php_array($parsed_attrs);
// If there's slot content, we need to output the div directly to allow blade processing of the content
if ($slot_content !== null && trim($slot_content) !== '') {
// Check for slot syntax - not allowed in Blade
if (preg_match('/<#[a-zA-Z0-9_]+/', $slot_content)) {
throw new \RuntimeException(
"JQHTML slot syntax (<#slotname>) is not allowed in Blade files.\n" .
"Component '{$component_name}' contains slot tags in its innerHTML.\n" .
"Use standard innerHTML with content() function instead.\n\n" .
"Blade usage:\n" .
" <{$component_name}>\n" .
" Your content here\n" .
" </{$component_name}>\n\n" .
"Template definition:\n" .
" <Define:{$component_name}>\n" .
" <%= content() %>\n" .
" </Define:{$component_name}>"
);
}
// Check for block syntax - not allowed in Blade
if (preg_match('/<\/?Block:([a-zA-Z0-9_]+)/i', $slot_content, $block_match)) {
$block_name = $block_match[1] ?? 'Unknown';
throw new \RuntimeException(
"JQHTML block/slot syntax (<Block:{$block_name}>) is not allowed in Blade files.\n" .
"Component '{$component_name}' contains block tags in its innerHTML.\n\n" .
"Block/slot content requires jqhtml template compilation and ONLY works in .jqhtml files.\n" .
"Blade renders components server-side as standard HTML - it cannot pass block content to jqhtml components.\n\n" .
"OPTION 1: Pass data via component arguments (recommended)\n" .
" Instead of using blocks, pass all data as component arguments:\n\n" .
" WRONG (Blade):\n" .
" <{$component_name}>\n" .
" <Block:{$block_name}>\n" .
" Custom content...\n" .
" </Block:{$block_name}>\n" .
" </{$component_name}>\n\n" .
" CORRECT (Blade):\n" .
" <{$component_name} \$custom_content=\"...\" />\n\n" .
" Then handle customization in the component's .jqhtml template file.\n\n" .
"OPTION 2: Move to .jqhtml template file\n" .
" If you need block/slot functionality:\n\n" .
" 1. Create a .jqhtml wrapper component that uses blocks\n" .
" 2. Use the wrapper in Blade as a self-closing tag\n\n" .
"BLADE RULE: Jqhtml components in Blade are ALWAYS self-closing or use standard innerHTML only.\n" .
"Blocks/slots only work in .jqhtml → .jqhtml relationships."
);
}
// Recursively process slot content for nested components
$slot_content = self::precompile($slot_content);
// Separate $-prefixed attributes (component args) from regular attributes
$component_args = [];
$html_attrs = [];
$wrapper_tag = self::__get_default_wrapper_tag($component_name);
foreach ($parsed_attrs as $key => $attr) {
if (str_starts_with($key, '$')) {
// Component arg - remove $ prefix
$arg_key = substr($key, 1);
if ($attr['type'] === 'expression') {
$component_args[$arg_key] = ['type' => 'expression', 'value' => $attr['value']];
} else {
$component_args[$arg_key] = ['type' => 'string', 'value' => $attr['value']];
}
} elseif (str_starts_with($key, 'data-')) {
// Component arg - remove data- prefix
$arg_key = substr($key, 5);
if ($attr['type'] === 'expression') {
$component_args[$arg_key] = ['type' => 'expression', 'value' => $attr['value']];
} else {
$component_args[$arg_key] = ['type' => 'string', 'value' => $attr['value']];
}
} elseif ($key === 'tag') {
// Special case: tag attribute becomes _tag component arg AND sets the wrapper element
if ($attr['type'] === 'expression') {
$component_args['_tag'] = ['type' => 'expression', 'value' => $attr['value']];
} else {
$component_args['_tag'] = ['type' => 'string', 'value' => $attr['value']];
// Use the tag value as the wrapper element (only for string literals, not expressions)
$tag_value = $attr['value'];
if (preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $tag_value)) {
$wrapper_tag = $tag_value;
}
}
} else {
// Regular HTML attribute
$html_attrs[$key] = $attr;
}
}
// Build component args JSON
$json_args = self::__to_php_array($component_args);
if (empty($json_args) || $json_args === '[]') {
$json_args = '[]';
} else {
$json_args = "json_encode({$json_args})";
}
// Build HTML attributes string
// Handle class attribute specially to merge with Jqhtml_Component_Init
$class_value = 'Jqhtml_Component_Init';
if (isset($html_attrs['class'])) {
if ($html_attrs['class']['type'] === 'expression') {
$class_value = "Jqhtml_Component_Init ' . {$html_attrs['class']['value']} . '";
} else {
$class_value = 'Jqhtml_Component_Init ' . $html_attrs['class']['value'];
}
}
$attrs_string = ' class="' . $class_value . '"';
foreach ($html_attrs as $key => $attr) {
if ($key === 'class') {
continue; // Already handled above
}
if ($attr['type'] === 'expression') {
$attrs_string .= ' :' . $key . '="' . htmlspecialchars($attr['value']) . '"';
} elseif ($attr['value'] === true) {
$attrs_string .= ' ' . $key;
} else {
$attrs_string .= ' ' . $key . '="' . htmlspecialchars($attr['value']) . '"';
}
}
// Use {!! !!} not {{ }} because htmlspecialchars is already encoding the value
$args_output = $json_args === '[]' ? '[]' : "{!! htmlspecialchars({$json_args}, ENT_QUOTES, 'UTF-8') !!}";
return sprintf(
'<%s data-component-init-name="%s" data-component-args="%s"%s>%s</%s>',
$wrapper_tag,
$component_name,
$args_output,
$attrs_string,
$slot_content,
$wrapper_tag
);
}
// Generate the wrapper element for self-closing tags
// Separate $-prefixed attributes (component args) from regular attributes
$component_args = [];
$html_attrs = [];
$wrapper_tag = self::__get_default_wrapper_tag($component_name);
foreach ($parsed_attrs as $key => $attr) {
if (str_starts_with($key, '$')) {
// Component arg - remove $ prefix
$arg_key = substr($key, 1);
$component_args[$arg_key] = $attr;
} elseif (str_starts_with($key, 'data-')) {
// Component arg - remove data- prefix
$arg_key = substr($key, 5);
$component_args[$arg_key] = $attr;
} elseif ($key === 'tag') {
// Special case: tag attribute becomes _tag component arg AND sets the wrapper element
$component_args['_tag'] = $attr;
// Use the tag value as the wrapper element (only for string literals, not expressions)
if ($attr['type'] === 'string') {
$tag_value = $attr['value'];
if (preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $tag_value)) {
$wrapper_tag = $tag_value;
}
}
} else {
// Regular HTML attribute
$html_attrs[$key] = $attr;
}
}
// Build component args JSON
$json_args = self::__to_php_array($component_args);
if (empty($json_args) || $json_args === '[]') {
$json_args = '[]';
} else {
$json_args = "json_encode({$json_args})";
}
// Build HTML attributes string
// Handle class attribute specially to merge with Jqhtml_Component_Init
$class_value = 'Jqhtml_Component_Init';
if (isset($html_attrs['class'])) {
if ($html_attrs['class']['type'] === 'expression') {
$class_value = "Jqhtml_Component_Init ' . {$html_attrs['class']['value']} . '";
} else {
$class_value = 'Jqhtml_Component_Init ' . $html_attrs['class']['value'];
}
}
$attrs_string = ' class="' . $class_value . '"';
foreach ($html_attrs as $key => $attr) {
if ($key === 'class') {
continue; // Already handled above
}
if ($attr['type'] === 'expression') {
$attrs_string .= ' :' . $key . '="' . htmlspecialchars($attr['value']) . '"';
} elseif ($attr['value'] === true) {
$attrs_string .= ' ' . $key;
} else {
$attrs_string .= ' ' . $key . '="' . htmlspecialchars($attr['value']) . '"';
}
}
// Use {!! !!} not {{ }} because htmlspecialchars is already encoding the value
$args_output = $json_args === '[]' ? '[]' : "{!! htmlspecialchars({$json_args}, ENT_QUOTES, 'UTF-8') !!}";
return sprintf(
'<%s data-component-init-name="%s" data-component-args="%s"%s></%s>',
$wrapper_tag,
$component_name,
$args_output,
$attrs_string,
$wrapper_tag
);
}, $value);
return $value;
}
/**
* Parse HTML attributes into key-value pairs
*
* @param string $attributes HTML attributes string
* @return array
*/
private static function __parse_attributes(string $attributes): array
{
$parsed = [];
// Match attribute patterns (including $ prefix for literal variable names)
preg_match_all('/(\$?[a-zA-Z0-9_\-:]+)(?:=(?:"([^"]*)"|\'([^\']*)\'|([^>\s]+)))?/', $attributes, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$key = $match[1];
// Handle different attribute value formats
if (isset($match[2]) && $match[2] !== '') {
// Double quoted value
$value = $match[2];
} elseif (isset($match[3]) && $match[3] !== '') {
// Single quoted value
$value = $match[3];
} elseif (isset($match[4]) && $match[4] !== '') {
// Unquoted value
$value = $match[4];
} else {
// Boolean attribute (no value)
$value = true;
}
// Handle Blade expressions (: prefix means it's a PHP expression)
if (str_starts_with($key, ':')) {
$key = substr($key, 1);
// Value is already a PHP expression, keep as is
$parsed[$key] = ['type' => 'expression', 'value' => $value];
} else {
// Regular string value
$parsed[$key] = ['type' => 'string', 'value' => $value];
}
}
return $parsed;
}
/**
* Convert parsed attributes to PHP array syntax
*
* @param array $attrs
* @return string
*/
private static function __to_php_array(array $attrs): string
{
if (empty($attrs)) {
return '[]';
}
$parts = [];
foreach ($attrs as $key => $attr) {
if ($attr['type'] === 'expression') {
// PHP expression, use as is
$parts[] = "'{$key}' => {$attr['value']}";
} elseif ($attr['value'] === true) {
// Boolean true
$parts[] = "'{$key}' => true";
} else {
// String value, escape it
$escaped = addslashes($attr['value']);
$parts[] = "'{$key}' => '{$escaped}'";
}
}
return '[' . implode(', ', $parts) . ']';
}
/**
* Get the default wrapper tag for a component from its template metadata
*
* @param string $component_name Component name
* @return string Default wrapper tag ('div' if not specified in template)
*/
private static function __get_default_wrapper_tag(string $component_name): string
{
$template_metadata = \App\RSpade\Integrations\Jqhtml\Jqhtml::get_jqhtml_template_by_id($component_name);
if ($template_metadata !== null && isset($template_metadata['tag_name'])) {
return $template_metadata['tag_name'];
}
return 'div';
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\RSpade\Integrations\Jqhtml;
use Illuminate\Contracts\Foundation\ExceptionRenderer;
use App\RSpade\Integrations\Jqhtml\Jqhtml_ErrorPageRenderer;
/**
* Custom exception renderer that uses our modified error page renderer
*/
#[Instantiatable]
class JqhtmlExceptionRenderer implements ExceptionRenderer
{
protected Jqhtml_ErrorPageRenderer $errorPageHandler;
public function __construct(Jqhtml_ErrorPageRenderer $errorPageHandler)
{
$this->errorPageHandler = $errorPageHandler;
}
public function render($throwable)
{
ob_start();
$this->errorPageHandler->render($throwable);
return ob_get_clean();
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\RSpade\Integrations\Jqhtml;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use App\RSpade\Integrations\Jqhtml\JqhtmlBladeCompiler;
/**
* Jqhtml Service Provider
*
* Registers jqhtml components with Laravel's Blade component system
*/
#[Instantiatable]
class JqhtmlServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services
*/
public function boot(): void
{
// Register the Blade precompiler for uppercase jqhtml component tags
Blade::precompiler(function ($value) {
return JqhtmlBladeCompiler::precompile($value);
});
// Register the generic x-jqhtml component for kebab-case usage
Blade::component('jqhtml', \App\RSpade\Integrations\Jqhtml\Jqhtml_View_Component::class);
// Also register a Blade directive for simpler syntax
Blade::directive('jqhtml', function ($expression) {
// Parse the expression to extract component and args
// Example: @jqhtml('User_Card', ['name' => 'John'])
return "<?php echo \\App\\RSpade\\Integrations\\Jqhtml\\Jqhtml::component({$expression}); ?>";
});
}
}

View File

@@ -0,0 +1,320 @@
<?php
namespace App\RSpade\Integrations\Jqhtml;
use App\RSpade\Integrations\Jqhtml\Jqhtml_Exception_ViewException;
/**
* JqhtmlWebpackCompiler - Compiles JQHTML templates to JavaScript during bundle build
*
* This service handles the compilation of .jqhtml template files into JavaScript
* during the bundle build process. It uses the @jqhtml/parser NPM package to
* perform the compilation and caches the results based on file modification times.
*
* Features:
* - Uses @jqhtml/parser for template compilation
* - Caches compiled templates based on file mtime
* - Throws fatal errors on compilation failures (fail loud)
* - Integrates with the bundle compilation pipeline
*/
#[Instantiatable]
class JqhtmlWebpackCompiler
{
/**
* Path to the jqhtml-compile binary
*/
protected string $compiler_path;
/**
* Cache directory for compiled templates
*/
protected string $cache_dir;
/**
* Constructor
*/
public function __construct()
{
// Use official jqhtml CLI compiler from npm package
$this->compiler_path = base_path('node_modules/@jqhtml/parser/bin/jqhtml-compile');
$this->cache_dir = storage_path('rsx-tmp/jqhtml-cache');
// Ensure cache directory exists
if (!is_dir($this->cache_dir)) {
mkdir($this->cache_dir, 0755, true);
}
// Validate compiler exists - MUST exist
if (!file_exists($this->compiler_path)) {
throw new \RuntimeException(
"Official JQHTML CLI compiler not found at: {$this->compiler_path}. " .
"Run 'npm install @jqhtml/parser@^2.2.59' to install the official CLI compiler."
);
}
}
/**
* Compile a single JQHTML template file
*
* @param string $file_path Path to .jqhtml file
* @return string Compiled JavaScript code
* @throws \RuntimeException On compilation failure
*/
public function compile_file(string $file_path): string
{
if (!file_exists($file_path)) {
throw new \RuntimeException("JQHTML template not found: {$file_path}");
}
// Get file modification time for cache key
$mtime = filemtime($file_path);
$cache_key = md5($file_path) . '_' . $mtime;
$cache_file = $this->cache_dir . '/' . $cache_key . '.js';
// Check if cached version exists
if (file_exists($cache_file)) {
console_debug("JQHTML", "Using cached JQHTML template: {$file_path}");
return file_get_contents($cache_file);
}
console_debug("JQHTML", "Compiling JQHTML template: {$file_path}");
// Execute official CLI compiler with IIFE format for self-registering templates
// CRITICAL: Must include --sourcemap for proper error mapping in bundles
// JQHTML v2.2.65+ uses Mozilla source-map library for reliable concatenation
// IMPORTANT: Using proc_open() instead of \exec_safe() to handle large template outputs
// \exec_safe() can truncate output for complex templates due to line-by-line buffering
$command = sprintf(
'%s compile %s --format iife --sourcemap',
escapeshellarg($this->compiler_path),
escapeshellarg($file_path)
);
$descriptors = [
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'] // stderr
];
$process = proc_open($command, $descriptors, $pipes);
if (!is_resource($process)) {
throw new \RuntimeException("Failed to execute jqhtml compiler");
}
// Close stdin
fclose($pipes[0]);
// Set blocking mode to ensure complete reads
stream_set_blocking($pipes[1], true);
stream_set_blocking($pipes[2], true);
// Read stdout and stderr completely in chunks
// CRITICAL: Use feof() as loop condition to prevent race condition truncation
// Checking feof() AFTER empty reads can cause 8192-byte truncation bug
$output_str = '';
$error_str = '';
// Read stdout until EOF
while (!feof($pipes[1])) {
$chunk = fread($pipes[1], 8192);
if ($chunk !== false) {
$output_str .= $chunk;
}
}
// Read stderr until EOF
while (!feof($pipes[2])) {
$chunk = fread($pipes[2], 8192);
if ($chunk !== false) {
$error_str .= $chunk;
}
}
fclose($pipes[1]);
fclose($pipes[2]);
// Get return code
$return_code = proc_close($process);
// Combine stdout and stderr for error messages
if ($return_code !== 0 && !empty($error_str)) {
$output_str = $error_str . "\n" . $output_str;
}
// Check for compilation errors
if ($return_code !== 0) {
// Official CLI outputs errors to stderr (captured in stdout with 2>&1)
// Try multiple error formats
// Format 1: "at filename:line:column" (newer format)
if (preg_match('/at [^:]+:(\d+):(\d+)/i', $output_str, $matches)) {
$line = (int)$matches[1];
$column = (int)$matches[2];
throw new Jqhtml_Exception_ViewException(
"JQHTML compilation failed:\n{$output_str}",
$file_path,
$line,
$column
);
}
// Format 2: "Error at line X, column Y:" (older format)
if (preg_match('/Error at line (\d+), column (\d+):/i', $output_str, $matches)) {
$line = (int)$matches[1];
$column = (int)$matches[2];
throw new Jqhtml_Exception_ViewException(
"JQHTML compilation failed:\n{$output_str}",
$file_path,
$line,
$column
);
}
// Format 3: No line number found - generic error
throw new \RuntimeException(
"JQHTML compilation failed for {$file_path}:\n{$output_str}"
);
}
// Success - the output is the compiled JavaScript
$compiled_js = $output_str;
// Don't add any comments - they break sourcemap line offsets
// Just use the compiler output as-is
$wrapped_js = $compiled_js;
// Ensure proper newline at end
if (!str_ends_with($wrapped_js, "\n")) {
$wrapped_js .= "\n";
}
// Cache the compiled result
file_put_contents($cache_file, $wrapped_js);
// Clean up old cache files for this template
$this->cleanup_old_cache($file_path, $cache_key);
return $wrapped_js;
}
/**
* Compile multiple JQHTML template files
*
* @param array $files Array of file paths
* @return array Compiled JavaScript code keyed by file path
*/
public function compile_files(array $files): array
{
$compiled = [];
foreach ($files as $file) {
try {
$compiled[$file] = $this->compile_file($file);
} catch (\Exception $e) {
// FAIL LOUD - don't continue on error
throw new \RuntimeException(
"Failed to compile JQHTML templates: " . $e->getMessage()
);
}
}
return $compiled;
}
/**
* Extract component name from file path
*
* @param string $file_path Path to .jqhtml file
* @return string Component name
*/
protected function extract_component_name(string $file_path): string
{
// Remove base path and extension
$relative = str_replace(base_path() . '/', '', $file_path);
$relative = preg_replace('/\.jqhtml$/i', '', $relative);
// Convert path to component name (e.g., rsx/app/components/MyComponent)
// to MyComponent or components/MyComponent
$parts = explode('/', $relative);
// Use the filename as the component name
return basename($relative);
}
/**
* Clean up old cache files for a template
*
* @param string $file_path Original template path
* @param string $current_cache_key Current cache key to keep
*/
protected function cleanup_old_cache(string $file_path, string $current_cache_key): void
{
$file_hash = md5($file_path);
$pattern = $this->cache_dir . '/' . $file_hash . '_*.js';
foreach (glob($pattern) as $cache_file) {
$cache_key = basename($cache_file, '.js');
if ($cache_key !== $current_cache_key) {
unlink($cache_file);
}
}
}
/**
* Clear all cached templates
*/
public function clear_cache(): void
{
$pattern = $this->cache_dir . '/*.js';
foreach (glob($pattern) as $cache_file) {
unlink($cache_file);
}
console_debug("JQHTML", "Cleared JQHTML template cache");
}
/**
* Get cache statistics
*
* @return array Cache statistics
*/
public function get_cache_stats(): array
{
$pattern = $this->cache_dir . '/*.js';
$files = glob($pattern);
$total_size = 0;
foreach ($files as $file) {
$total_size += filesize($file);
}
return [
'cache_dir' => $this->cache_dir,
'cached_files' => count($files),
'total_size' => $total_size,
'total_size_human' => $this->format_bytes($total_size)
];
}
/**
* Format bytes to human readable
*
* @param int $bytes
* @return string
*/
protected function format_bytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$i = 0;
while ($bytes >= 1024 && $i < count($units) - 1) {
$bytes /= 1024;
$i++;
}
return round($bytes, 2) . ' ' . $units[$i];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\RSpade\Integrations\Jqhtml;
use App\RSpade\Core\Bundle\Rsx_Bundle_Abstract;
/**
* JQHTML Runtime Bundle
*
* Provides the JQHTML runtime JavaScript files needed for template rendering.
* The Jqhtml_BundleProcessor handles compilation of .jqhtml files separately.
*/
class Jqhtml_Bundle extends Rsx_Bundle_Abstract
{
/**
* Define the bundle configuration
*
* @return array Bundle configuration
*/
public static function define(): array
{
return [
'include' => [
__DIR__,
],
'npm' => [
'jqhtml' => "import jqhtml from '@jqhtml/core'",
'_Base_Jqhtml_Component' => "import { Jqhtml_Component } from '@jqhtml/core'",
],
];
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace App\RSpade\Integrations\Jqhtml;
use App\RSpade\Core\Bundle\BundleIntegration_Abstract;
use App\RSpade\Integrations\Jqhtml\Jqhtml_BundleProcessor;
use App\RSpade\Integrations\Jqhtml\Jqhtml_ManifestModule;
/**
* JqhtmlIntegration - JQHTML template system integration
*
* This integration adds support for JQHTML templates to the RSX framework.
* It provides:
* - Discovery of .jqhtml template files
* - Compilation of templates to JavaScript
* - Runtime library inclusion
* - Automatic binding between templates and ES6 classes
*
* JQHTML is a jQuery-based component templating system that compiles
* HTML-like syntax to JavaScript functions for efficient rendering.
*/
class Jqhtml_BundleIntegration extends BundleIntegration_Abstract
{
/**
* Get the integration's unique identifier
*
* @return string
*/
public static function get_name(): string
{
return 'jqhtml';
}
/**
* Get the ManifestModule class for file discovery
*
* @return string|null
*/
public static function get_manifest_module(): ?string
{
return Jqhtml_ManifestModule::class;
}
/**
* Get the BundleModule_Abstract class for asset provision
*
* @return string|null
*/
public static function get_bundle_module(): ?string
{
return null; // BundleModule system is obsolete
}
/**
* Get the Processor class for file transformation
*
* @return string|null
*/
public static function get_processor(): ?string
{
return Jqhtml_BundleProcessor::class;
}
/**
* Get file extensions handled by this integration
*
* @return array
*/
public static function get_file_extensions(): array
{
return ['jqhtml', 'jqtpl'];
}
/**
* Get configuration options for this integration
*
* @return array
*/
public static function get_config(): array
{
return [
'compiler' => [
'cache' => env('JQHTML_CACHE', true),
'cache_ttl' => env('JQHTML_CACHE_TTL', 3600),
'source_maps' => env('JQHTML_SOURCE_MAPS', !app()->environment('production')),
],
'runtime' => [
'bundle' => 'node_modules/@jqhtml/core/dist/index.js',
],
'search_patterns' => [
'rsx/**/*.jqhtml',
'rsx/**/*.jqtpl',
],
];
}
/**
* Get priority for this integration
*
* @return int
*/
public static function get_priority(): int
{
return 300; // After jQuery (100) and Lodash (90), before most other integrations
}
/**
* Bootstrap the integration
*/
public static function bootstrap(): void
{
// JQHTML runtime MUST exist - fail loud if not
$runtime_path = base_path(static::get_config()['runtime']['bundle']);
if (!file_exists($runtime_path)) {
throw new \RuntimeException("JQHTML runtime not found at: {$runtime_path}. Run 'npm install' to install @jqhtml packages.");
}
// Register automatic template-to-class binding
// This would be implemented to automatically bind templates to matching ES6 classes
}
/**
* Get dependencies for this integration
*
* @return array
*/
public static function get_dependencies(): array
{
return ['jquery']; // JQHTML requires jQuery to be loaded first
}
}

View File

@@ -0,0 +1,183 @@
<?php
namespace App\RSpade\Integrations\Jqhtml;
use Exception;
use RuntimeException;
use App\RSpade\Core\Bundle\BundleProcessor_Abstract;
use App\RSpade\Integrations\Jqhtml\JqhtmlWebpackCompiler;
use App\RSpade\Integrations\Jqhtml\Jqhtml_Exception_ViewException;
/**
* JqhtmlProcessor - Processes JQHTML template files
*
* Compiles JQHTML templates into JavaScript code that can be
* included in bundles. Uses webpack-based compilation with caching.
*/
class Jqhtml_BundleProcessor extends BundleProcessor_Abstract
{
/**
* Compiler instance
*/
protected static ?JqhtmlWebpackCompiler $compiler = null;
/**
* Get processor name
*/
public static function get_name(): string
{
return 'jqhtml';
}
/**
* Get file extensions this processor handles
*/
public static function get_extensions(): array
{
return ['jqhtml'];
}
/**
* Process multiple files in batch
* Compiles JQHTML templates and appends the JavaScript output to the bundle
*/
public static function process_batch(array &$bundle_files): void
{
// Check for jqhtml files
$jqhtml_files = array_filter($bundle_files, function ($file) {
return pathinfo($file, PATHINFO_EXTENSION) === 'jqhtml';
});
console_debug('JQHTML', 'process_batch called with ' . count($bundle_files) . ' files, ' . count($jqhtml_files) . ' jqhtml files');
if (empty($jqhtml_files)) {
console_debug('JQHTML', 'No jqhtml files to process');
return;
}
// Initialize compiler if needed
if (!static::$compiler) {
static::$compiler = new JqhtmlWebpackCompiler();
}
// Process each JQHTML file
foreach ($bundle_files as $path) {
$ext = pathinfo($path, PATHINFO_EXTENSION);
// Only process JQHTML files
if ($ext !== 'jqhtml') {
continue; // Skip non-JQHTML files
}
console_debug('JQHTML', "Processing file: {$path}");
// Generate temp file path for compiled output
$cache_key = md5($path . ':' . filemtime($path) . ':' . filesize($path));
$temp_file = storage_path('rsx-tmp/jqhtml_' . substr($cache_key, 0, 16) . '.js');
// Check if we need to compile
$needs_compile = !file_exists($temp_file) ||
(filemtime($path) > filemtime($temp_file));
if ($needs_compile) {
console_debug('JQHTML', "Compiling: {$path}");
try {
// Compile the template using webpack compiler
$js_code = static::$compiler->compile_file($path);
// Strip the compiler's comment line if present (single-line // comment)
if (preg_match('/^\/\/[^\n]*\n(.*)$/s', $js_code, $matches)) {
$js_code = $matches[1];
}
// Add our comment inline without any newlines to preserve sourcemap line offsets
$relative_path = str_replace(base_path() . '/', '', $path);
$wrapped_code = "/* Compiled from: {$relative_path} */ {$js_code}";
// Ensure proper newline at end
if (!str_ends_with($wrapped_code, "\n")) {
$wrapped_code .= "\n";
}
// Write to temp file
file_put_contents($temp_file, $wrapped_code);
console_debug('JQHTML', "Compiled {$path} -> {$temp_file} (" . strlen($wrapped_code) . ' bytes)');
} catch (Jqhtml_Exception_ViewException $e) {
// Let JQHTML ViewExceptions pass through for proper Ignition display
throw $e;
} catch (\Illuminate\View\ViewException $e) {
// Let ViewExceptions pass through for proper display
throw $e;
} catch (Exception $e) {
// FAIL LOUD - re-throw other exceptions
throw new RuntimeException(
"Failed to process JQHTML template {$path}: " . $e->getMessage()
);
}
} else {
console_debug('JQHTML', "Using cached: {$temp_file}");
}
// ALWAYS append the compiled JS file to the bundle (whether freshly compiled or cached)
$bundle_files[] = $temp_file;
}
console_debug('JQHTML', 'Final bundle_files count: ' . count($bundle_files));
}
/**
* Post-processing hook - no longer generates manifest
* Templates self-register via their compiled code
*/
public static function after_processing(array $processed_files, array $options = []): array
{
// Templates now self-register when their compiled JS executes
// No need for separate manifest generation
return [];
}
/**
* Pre-processing hook - reset compiled templates cache
*/
public static function before_processing(array $all_files, array $options = []): void
{
}
/**
* Get processor priority (processes before JS)
*/
public static function get_priority(): int
{
return 400; // Process before JavaScript files
}
/**
* Validate processor configuration
*/
public static function validate(): void
{
// JqhtmlWebpackCompiler must exist - it's a required part of the jqhtml integration
// Check if @jqhtml/parser is installed
$package_path = base_path('node_modules/@jqhtml/parser/package.json');
if (!file_exists($package_path)) {
throw new RuntimeException(
"@jqhtml/parser NPM package not found. Run 'npm install' to install @jqhtml packages."
);
}
}
/**
* Check if processor should run in current environment
*
* @return bool True if processor should run
*/
public static function is_enabled(): bool
{
// Always enabled - bundles control inclusion via module dependencies
return true;
}
}

View File

@@ -0,0 +1,13 @@
/**
* Jqhtml_Component - Base class for JQHTML components in RSX framework
*
* This class wraps the jqhtml.Component from the npm package and provides
* the standard interface for RSX components following the Upper_Case naming convention.
*
* _Base_Jqhtml_Component is imported from npm via Jqhtml_Bundle.
*
* @Instantiatable
*/
class Jqhtml_Component extends _Base_Jqhtml_Component {}
// RSX manifest automatically makes classes global - no manual assignment needed

View File

@@ -0,0 +1,94 @@
<?php
namespace App\RSpade\Integrations\Jqhtml;
use Spatie\LaravelIgnition\Renderers\ErrorPageRenderer;
use Throwable;
/**
* Custom error page renderer that extends Ignition's error renderer
* to properly display multi-line error messages without truncation
*/
#[Instantiatable]
class Jqhtml_ErrorPageRenderer extends ErrorPageRenderer
{
/**
* Render the error page with custom JavaScript to fix multi-line display
*/
public function render(Throwable $throwable): void
{
// Create custom JavaScript to convert newlines to <br> tags in error messages
$customScript = <<<'JS'
<script>
// Fix multi-line error messages in Ignition by converting \n to <br>
document.addEventListener('DOMContentLoaded', function() {
// Wait for React to render
setTimeout(function() {
// Find all elements that might contain error messages
const selectors = [
'.message',
'.exception-message',
'[class*="message"]',
'.line-clamp-2',
'h1',
'title'
];
selectors.forEach(function(selector) {
document.querySelectorAll(selector).forEach(function(element) {
const text = element.textContent || element.innerText || '';
// Check if element contains newlines
if (text.includes('\n')) {
// Convert newlines to <br> tags
const html = text
.split('\n')
.map(line => {
// Escape HTML but preserve spaces at start of lines
const escaped = line
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
// Convert leading spaces to non-breaking spaces
return escaped.replace(/^( +)/, function(match) {
return '&nbsp;'.repeat(match.length);
});
})
.join('<br>');
element.innerHTML = html;
// Remove truncation styles
element.classList.remove('line-clamp-2', 'truncate');
element.style.overflow = 'visible';
element.style.display = 'block';
element.style.webkitLineClamp = 'none';
element.style.webkitBoxOrient = 'horizontal';
}
});
});
// Do it again after a longer delay for any lazy-loaded content
setTimeout(arguments.callee, 500);
}, 100);
});
</script>
JS;
// Call parent render method with our custom script added
app(\Spatie\Ignition\Ignition::class)
->resolveDocumentationLink(
fn (Throwable $throwable) => (new \Spatie\LaravelIgnition\Support\LaravelDocumentationLinkFinder())->findLinkForThrowable($throwable)
)
->setFlare(app(\Spatie\FlareClient\Flare::class))
->setConfig(app(\Spatie\Ignition\Config\IgnitionConfig::class))
->setSolutionProviderRepository(app(\Spatie\ErrorSolutions\Contracts\SolutionProviderRepository::class))
->setContextProviderDetector(new \Spatie\LaravelIgnition\ContextProviders\LaravelContextProviderDetector())
->setSolutionTransformerClass(\Spatie\LaravelIgnition\Solutions\SolutionTransformers\LaravelSolutionTransformer::class)
->applicationPath(base_path())
->addCustomHtmlToHead($customScript)
->renderException($throwable);
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace App\RSpade\Integrations\Jqhtml;
use Illuminate\View\ViewException;
/**
* Custom exception for JQHTML template compilation errors
* Extends ViewException to integrate with Laravel's error handling
*/
#[Instantiatable]
class Jqhtml_Exception_ViewException extends ViewException
{
protected ?int $column = null;
protected ?string $context = null;
protected ?string $suggestion = null;
protected string $rawMessage = '';
/**
* Create a new JQHTML exception
*/
public function __construct(string $message, string $file, int $line = 0, int $column = 0, ?\Throwable $previous = null)
{
// Store the raw message
$this->rawMessage = $message;
// Call parent constructor with the raw message
// ViewException will handle the display
parent::__construct($message, 0, 1, $file, $line, $previous);
$this->column = $column;
}
/**
* Get the column number
*/
public function getColumn(): ?int
{
return $this->column;
}
/**
* Set the column number
*/
public function setColumn(int $column): void
{
$this->column = $column;
}
/**
* Get the code context
*/
public function getContext(): ?string
{
return $this->context;
}
/**
* Set the code context
*/
public function setContext(string $context): void
{
$this->context = $context;
}
/**
* Get the suggestion
*/
public function getSuggestion(): ?string
{
return $this->suggestion;
}
/**
* Set the suggestion
*/
public function setSuggestion(string $suggestion): void
{
$this->suggestion = $suggestion;
}
/**
* Get the raw unformatted message
*/
public function getRawMessage(): string
{
return $this->rawMessage;
}
/**
* Get the view source - override to handle .jqhtml files
* Ignition uses this to display the source code preview
*/
public function getSourceCode(): string
{
if (file_exists($this->getFile())) {
return file_get_contents($this->getFile());
}
return '';
}
}

View File

@@ -0,0 +1,156 @@
/**
* JQHTML Integration - Automatic component registration and binding
*
* This module automatically:
* 1. Registers component classes that extend Jqhtml_Component
* 2. Binds templates to component classes when names match
* 3. Enables $(selector).component("Component_Name") syntax
*/
class Jqhtml_Integration {
/**
* Compiled Jqhtml templates self-register. The developer (the framework in this case) is still
* responsible for registering es6 component classes with jqhtml. This does so at an early stage
* of framework init.
*/
static _on_framework_modules_define() {
let jqhtml_components = Manifest.get_extending('Jqhtml_Component');
console_debug('JQHTML_INIT', 'Registering ' + jqhtml_components.length + ' Jqhtml Components');
for (let component of jqhtml_components) {
jqhtml.register_component(component.class_name, component.class_object);
}
}
/**
* Framework modules init phase - Bind components and initialize DOM
* This runs after templates are registered to bind component classes
* @param {jQuery} [$scope] Optional scope to search within (defaults to body)
* @returns {Array<Promise>|undefined} Array of promises for recursive calls, undefined for top-level
*/
static _on_framework_modules_init($scope) {
const is_top_level = !$scope;
const promises = [];
const components_needing_init = ($scope || $('body')).find('.Jqhtml_Component_Init');
if (components_needing_init.length > 0) {
console_debug('JQHTML_INIT', `Initializing ${components_needing_init.length} DOM components`);
}
components_needing_init.each(function () {
const $element = $(this);
// Check if any parent has Jqhtml_Component_Init class - skip nested components
let parent = $element[0].parentElement;
while (parent) {
if (parent.classList.contains('Jqhtml_Component_Init')) {
return; // Skip this element, it's nested
}
parent = parent.parentElement;
}
const component_name = $element.attr('data-component-init-name');
// jQuery's .data() doesn't auto-parse JSON - we need to parse it manually
let component_args = {};
const args_string = $element.attr('data-component-args');
// Unset component- php side initialization args, it is no longer needed as a compionent attribute
// Unsetting also prevents undesired access to this code in other parts of the program, prevening an
// unwanted future dependency on this paradigm
$element.removeAttr('data-component-init-name');
$element.removeAttr('data-component-args');
$element.removeData('component-init-name');
$element.removeData('component-args');
if (args_string) {
try {
component_args = JSON.parse(args_string);
} catch (e) {
console.error(`[JQHTML Integration] Failed to parse component args for ${component_name}:`, e);
component_args = {};
}
}
if (component_name) {
// Transform $ prefixed keys to data- attributes
let component_args_filtered = {};
for (const [key, value] of Object.entries(component_args)) {
// if (key.startsWith('$')) {
// component_args_filtered[key.substring(1)] = value;
// } else
if (key.startsWith('data-')) {
component_args_filtered[key.substring(5)] = value;
} else {
component_args_filtered[key] = value;
}
}
try {
// Store inner HTML as string for nested component processing
component_args_filtered._inner_html = $element.html();
$element.empty();
// Remove the init class before instantiation to prevent re-initialization
$element.removeClass('Jqhtml_Component_Init');
// Create promise for this component's initialization
const component_promise = new Promise((resolve) => {
// Use jQuery component plugin to create the component
// Plugin handles element internally, just pass args
// Get the updated $element from
let component = $element.component(component_name, component_args_filtered);
component.on('ready', function () {
// Recursively collect promises from nested components
// Getting the updated component here - if the tag name was not div, the element would have been recreated, so we need to get the element set on the component, not from our earlier selector
const nested_promises = Jqhtml_Integration._on_framework_modules_init(component.$);
promises.push(...nested_promises);
// Resolve this component's promise
resolve();
}).$;
});
promises.push(component_promise);
} catch (error) {
console.error(`[JQHTML Integration] Failed to initialize component ${component_name}:`, error);
console.error('Error details:', error.stack || error);
}
}
});
// Top-level call: spawn async handler to wait for all promises, then trigger event
if (is_top_level) {
(async () => {
await Promise.all(promises);
await Rsx._rsx_call_all_classes('on_jqhtml_ready');
Rsx.trigger('jqhtml_ready');
})();
return;
}
// Recursive call: return promises for parent to collect
return promises;
}
/**
* Get all registered component names
* @returns {Array<string>} Array of component names
*/
static get_component_names() {
return jqhtml.get_component_names();
}
/**
* Check if a component is registered
* @param {string} name Component name
* @returns {boolean} True if component is registered
*/
static has_component(name) {
return jqhtml.has_component(name);
}
}
// RSX manifest automatically makes classes global - no manual assignment needed

View File

@@ -0,0 +1,3 @@
.Jqhtml_Component_Init {
display:none;
}

View File

@@ -0,0 +1,265 @@
<?php
namespace App\RSpade\Integrations\Jqhtml;
use App\RSpade\Core\Manifest\ManifestModule_Abstract;
/**
* JqhtmlManifestModule - Manifest module for JQHTML template files
*
* This module handles discovery and metadata extraction for .jqhtml template files.
* It extracts:
* - Template name from <Define:ComponentName> syntax
* - Component references (PascalCase tags)
* - Dependencies (@import directives)
* - Template metadata for component binding
*/
class Jqhtml_ManifestModule extends ManifestModule_Abstract
{
/**
* Get file extensions this module handles
*
* @return array
*/
public function handles(): array
{
return ['jqhtml', 'jqtpl'];
}
/**
* Get processing priority (lower = earlier)
*
* @return int
*/
public function priority(): int
{
return 500; // Process after core types but before CSS
}
/**
* Process a file and extract metadata
*
* @param string $file_path Full path to the file
* @param array $metadata Existing metadata
* @return array Updated metadata
*/
public function process(string $file_path, array $metadata): array
{
// Read template content
$content = file_get_contents($file_path);
// Check if file has meaningful content after comment removal
$cleaned_content = $this->remove_comments($content);
$trimmed_content = trim($cleaned_content);
// If file is effectively empty after comments removed and trimmed, don't require a template name
if (empty($trimmed_content)) {
$metadata['type'] = 'jqhtml_template';
$metadata['components'] = [];
$metadata['dependencies'] = [];
$metadata['slots'] = [];
return $metadata;
}
// Extract template name
$template_name = $this->extract_template_name($content);
if (!$template_name) {
// Non-empty file must have a template name - fail loud
throw new \RuntimeException("JQHTML template in {$file_path} has content but no <Define:ComponentName> tag. Please add a template definition to the file.");
}
// Extract component references
$components = $this->extract_components($content);
// Extract dependencies
$dependencies = $this->extract_dependencies($content);
// Extract slots
$slots = $this->extract_slots($content);
// Extract tag name (defaults to 'div' if not specified)
$tag_name = $this->extract_tag_name($content);
// Add JQHTML-specific metadata
$metadata['template_name'] = $template_name;
$metadata['id'] = $template_name; // Set the ID to the component name
$metadata['type'] = 'jqhtml_template';
$metadata['tag_name'] = $tag_name;
$metadata['components'] = $components;
$metadata['dependencies'] = $dependencies;
$metadata['slots'] = $slots;
// Check if this template has a matching ES6 class
$class_name = $this->to_class_name($template_name);
$metadata['expected_class'] = $class_name;
return $metadata;
}
/**
* Remove comments from JQHTML content
*
* @param string $content Template content
* @return string Content with comments removed
*/
protected function remove_comments(string $content): string
{
// Remove <%-- --%> style comments
$content = preg_replace('/<%--.*?--%>/s', '', $content);
// Remove <!-- --> style comments
$content = preg_replace('/<!--.*?-->/s', '', $content);
return $content;
}
/**
* Extract template name from content
*
* @param string $content Template content
* @return string|null Template name
*/
protected function extract_template_name(string $content): ?string
{
// Remove comments before searching for Define:
$content = $this->remove_comments($content);
// Find the first instance of <Define: and capture alphanumeric+underscore characters
// The component name continues until we hit a non-alphanumeric/non-underscore character
if (preg_match('/<Define:(\w+)/', $content, $matches)) {
return $matches[1];
}
return null;
}
/**
* Extract tag name from Define statement
*
* @param string $content Template content
* @return string Tag name (defaults to 'div' if not specified)
*/
protected function extract_tag_name(string $content): string
{
// Remove comments before searching
$content = $this->remove_comments($content);
// Look for tag attribute in Define statement
// Matches: <Define:ComponentName tag="span"> or <Define:ComponentName tag='span'>
if (preg_match('/<Define:\w+\s+[^>]*tag=["\']([^"\']+)["\']/', $content, $matches)) {
return $matches[1];
}
return 'div'; // Default tag name
}
/**
* Extract component references from template
*
* @param string $content Template content
* @return array Component names
*/
protected function extract_components(string $content): array
{
$components = [];
// Match PascalCase tags (likely components)
preg_match_all('/<([A-Z][a-zA-Z0-9_]*)(?:\s+[^>]*)?\/?>|<\/([A-Z][a-zA-Z0-9_]*)>/', $content, $matches);
foreach ($matches[1] as $tag) {
if ($tag && !in_array($tag, $components)) {
$components[] = $tag;
}
}
foreach ($matches[2] as $tag) {
if ($tag && !in_array($tag, $components)) {
$components[] = $tag;
}
}
// Remove Define tags as they're not component references
$components = array_filter($components, function($c) {
return !str_starts_with($c, 'Define');
});
return array_values($components);
}
/**
* Extract dependencies from template
*
* @param string $content Template content
* @return array Dependency paths
*/
protected function extract_dependencies(string $content): array
{
$dependencies = [];
// Match @import directives
preg_match_all('/@import\s+[\'"]([^\'"]+)[\'"]/', $content, $matches);
foreach ($matches[1] as $dep) {
if ($dep && !in_array($dep, $dependencies)) {
$dependencies[] = $dep;
}
}
return $dependencies;
}
/**
* Extract slot definitions from template
*
* @param string $content Template content
* @return array Slot names
*/
protected function extract_slots(string $content): array
{
$slots = [];
// Match <#slotname> syntax
preg_match_all('/<#(\w+)>/', $content, $matches);
foreach ($matches[1] as $slot) {
if ($slot && !in_array($slot, $slots)) {
$slots[] = $slot;
}
}
// Also match <slot name="..."> syntax
preg_match_all('/<slot\s+name=[\'"](\w+)[\'"]/', $content, $matches);
foreach ($matches[1] as $slot) {
if ($slot && !in_array($slot, $slots)) {
$slots[] = $slot;
}
}
return $slots;
}
/**
* Convert template name to expected class name
*
* @param string $template_name Template name
* @return string Expected class name
*/
protected function to_class_name(string $template_name): string
{
// Handle snake_case to PascalCase
if (strpos($template_name, '_') !== false) {
$parts = explode('_', $template_name);
return implode('', array_map('ucfirst', $parts));
}
// Handle kebab-case to PascalCase
if (strpos($template_name, '-') !== false) {
$parts = explode('-', $template_name);
return implode('', array_map('ucfirst', $parts));
}
// Already PascalCase or needs first letter capitalized
return ucfirst($template_name);
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace App\RSpade\Integrations\Jqhtml;
use Exception;
use App\RSpade\Core\Manifest\Manifest;
use App\RSpade\Core\Manifest\ManifestSupport_Abstract;
/**
* Support module for extracting jqhtml component metadata
* This runs after the primary manifest is built to add jqhtml component metadata
*/
class Jqhtml_ManifestSupport extends ManifestSupport_Abstract
{
/**
* Process the manifest and add jqhtml component metadata
*
* Doesn't actually read any files, just collates the data in the manifest
*
* @param array &$manifest_data Reference to the manifest data array
* @return void
*/
public static function process(array &$manifest_data): void
{
// Initialize jqhtml key if it doesn't exist
if (!isset($manifest_data['data']['jqhtml'])) {
$manifest_data['data']['jqhtml'] = [];
}
$components = [];
// Build map of component_name => js_file for classes extending Jqhtml_Component
$js_classes = [];
try {
$extending_components = Manifest::js_get_extending('Jqhtml_Component');
foreach ($extending_components as $component_info) {
if (isset($component_info['class']) && isset($component_info['file'])) {
$js_classes[$component_info['class']] = $component_info['file'];
}
}
} catch (Exception $e) {
// Manifest not ready yet, skip
}
// Get all jqhtml template files from manifest and build component map
if (isset($manifest_data['data']['files'])) {
foreach ($manifest_data['data']['files'] as $file_path => $file_data) {
if (isset($file_data['type']) && $file_data['type'] === 'jqhtml_template') {
if (isset($file_data['template_name'])) {
$component_name = $file_data['template_name'];
$components[$component_name] = [
'name' => $component_name,
'template_file' => $file_path,
];
// Add JS file if component has a class
if (isset($js_classes[$component_name])) {
$components[$component_name]['js_file'] = $js_classes[$component_name];
}
}
}
}
}
// Store component map (associative array: component_name => metadata)
$manifest_data['data']['jqhtml']['components'] = $components;
$manifest_data['data']['jqhtml']['component_count'] = count($components);
}
/**
* Get the name of this support module
*
* @return string
*/
public static function get_name(): string
{
return 'Jqhtml Component Metadata';
}
/**
* Static method to get jqhtml components from cached manifest
* This is called by JqhtmlBladeCompiler to get the list of components
*
* @return array List of jqhtml component names
*/
public static function get_jqhtml_components(): array
{
try {
// Get the full manifest data
$manifest = Manifest::get_full_manifest();
if (isset($manifest['data']['jqhtml']['components'])) {
return $manifest['data']['jqhtml']['components'];
}
} catch (Exception $e) {
// Manifest not ready, return empty
}
return [];
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\RSpade\Integrations\Jqhtml;
use App\RSpade\Core\Integration_Service_Provider_Abstract;
use App\RSpade\Integrations\Jqhtml\Jqhtml_BundleIntegration;
/**
* Jqhtml_Service_Provider - Service provider for JQHTML integration
*
* This provider registers the JQHTML integration with the RSX framework.
* It handles:
* - Registering file extensions with ExtensionRegistry
* - Registering the processor with BundleCompiler
* - Registering the manifest module with ManifestKernel
* - Bootstrapping the JQHTML runtime
*
* To use this integration, register this provider in config/app.php:
* App\RSpade\Integrations\Jqhtml\Jqhtml_Service_Provider::class
*
* Or register it conditionally in AppServiceProvider if needed.
*/
class Jqhtml_Service_Provider extends Integration_Service_Provider_Abstract
{
/**
* Get the integration class for this provider
*
* @return string
*/
protected function get_integration_class(): string
{
return Jqhtml_BundleIntegration::class;
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Integrations\Jqhtml;
use Illuminate\View\Component;
use RuntimeException;
use App\RSpade\Integrations\Jqhtml\Jqhtml;
/**
* Generic Blade component for rendering jqhtml components
*
* Usage: <x-jqhtml component="User_Card" :args="['name' => 'Jim']" />
*/
#[Instantiatable]
class Jqhtml_View_Component extends Component
{
public string $component;
public array $args;
/**
* Create a new component instance
*
* @param string $component The jqhtml component name (e.g., 'User_Card')
* @param array $args Component arguments
*/
public function __construct(string $component, array $args = [])
{
// Validate component name starts with uppercase
if (!ctype_upper($component[0])) {
throw new RuntimeException(
"JQHTML component name '{$component}' must start with an uppercase letter. " .
'This is a hard requirement of the jqhtml library.'
);
}
$this->component = $component;
$this->args = $args;
}
/**
* Get the view / contents that represent the component
*
* @return \Illuminate\Contracts\View\View|string
*/
public function render()
{
// Use the existing Jqhtml helper to render the component
return Jqhtml::component($this->component, $this->args);
}
}

View File

@@ -0,0 +1,236 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JQHTML Compilation Error</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0;
padding: 0;
background: #f5f5f5;
color: #333;
}
.container {
max-width: 1200px;
margin: 40px auto;
padding: 0 20px;
}
.error-header {
background: #fff;
border: 1px solid #e1e1e1;
border-radius: 8px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.error-title {
color: #e74c3c;
font-size: 24px;
margin: 0 0 10px 0;
font-weight: 600;
}
.error-file {
color: #666;
font-size: 14px;
margin: 10px 0;
font-family: 'Courier New', monospace;
}
.error-location {
color: #999;
font-size: 13px;
}
.error-message {
background: #fff;
border: 1px solid #e1e1e1;
border-radius: 8px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.error-message h3 {
color: #555;
font-size: 16px;
margin: 0 0 15px 0;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 500;
}
.error-text {
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
padding: 20px;
background: #f8f8f8;
border-radius: 4px;
border-left: 4px solid #e74c3c;
color: #444;
}
.error-context {
background: #fff;
border: 1px solid #e1e1e1;
border-radius: 8px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.error-context h3 {
color: #555;
font-size: 16px;
margin: 0 0 15px 0;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 500;
}
.code-context {
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.5;
background: #2d2d2d;
color: #f8f8f2;
padding: 20px;
border-radius: 4px;
overflow-x: auto;
white-space: pre;
}
.error-line {
background: rgba(231, 76, 60, 0.2);
display: inline-block;
width: 100%;
padding: 0 5px;
margin: 0 -5px;
}
.line-number {
color: #999;
display: inline-block;
width: 40px;
text-align: right;
padding-right: 10px;
border-right: 1px solid #444;
margin-right: 10px;
}
.error-suggestion {
background: #fff;
border: 1px solid #e1e1e1;
border-radius: 8px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.error-suggestion h3 {
color: #27ae60;
font-size: 16px;
margin: 0 0 15px 0;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 500;
}
.suggestion-text {
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
padding: 20px;
background: #f0f9f4;
border-radius: 4px;
border-left: 4px solid #27ae60;
color: #2c5f2d;
}
.error-trace {
background: #fff;
border: 1px solid #e1e1e1;
border-radius: 8px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.error-trace h3 {
color: #555;
font-size: 16px;
margin: 0 0 15px 0;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 500;
}
.trace-list {
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.8;
}
.trace-item {
padding: 8px 12px;
border-bottom: 1px solid #f0f0f0;
}
.trace-item:last-child {
border-bottom: none;
}
.trace-number {
color: #999;
display: inline-block;
width: 30px;
}
.trace-file {
color: #666;
}
.trace-function {
color: #3498db;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<div class="error-header">
<h1 class="error-title">JQHTML Template Compilation Error</h1>
<div class="error-file">{{ $exception->getFile() }}</div>
@if($exception->getLine())
<div class="error-location">Line {{ $exception->getLine() }}@if(method_exists($exception, 'getColumn') && $exception->getColumn()), Column {{ $exception->getColumn() }}@endif</div>
@endif
</div>
<div class="error-message">
<h3>Error Message</h3>
<pre class="error-text">{{ $exception->getMessage() }}</pre>
</div>
@if(method_exists($exception, 'getContext') && $exception->getContext())
<div class="error-context">
<h3>Code Context</h3>
<pre class="code-context">{{ $exception->getContext() }}</pre>
</div>
@endif
@if(method_exists($exception, 'getSuggestion') && $exception->getSuggestion())
<div class="error-suggestion">
<h3>How to Fix</h3>
<pre class="suggestion-text">{{ $exception->getSuggestion() }}</pre>
</div>
@endif
@if(app()->environment('local', 'development'))
<div class="error-trace">
<h3>Stack Trace</h3>
<div class="trace-list">
@foreach($exception->getTrace() as $index => $trace)
@if($index < 10)
<div class="trace-item">
<span class="trace-number">#{{ $index }}</span>
<span class="trace-file">{{ $trace['file'] ?? 'unknown' }}:{{ $trace['line'] ?? '?' }}</span>
@if(isset($trace['class']))
<span class="trace-function">{{ $trace['class'] }}{{ $trace['type'] ?? '::' }}{{ $trace['function'] }}()</span>
@elseif(isset($trace['function']))
<span class="trace-function">{{ $trace['function'] }}()</span>
@endif
</div>
@endif
@endforeach
</div>
</div>
@endif
</div>
</body>
</html>