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,339 @@
# JQHTML Laravel Integration
The JQHTML Laravel Bridge is included with the `@jqhtml/core` npm package, making it easy to integrate JQHTML error handling into your Laravel application without requiring a separate Composer package.
## Installation
### Step 1: Install JQHTML via npm
```bash
npm install @jqhtml/core
```
### Step 2: Load the Laravel Bridge in your Laravel application
#### Option A: In `AppServiceProvider` (Recommended)
Add to `app/Providers/AppServiceProvider.php`:
```php
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
// Load JQHTML Laravel Bridge from node_modules
$jqhtmlBridge = base_path('node_modules/@jqhtml/core/laravel-bridge/autoload.php');
if (file_exists($jqhtmlBridge)) {
require_once $jqhtmlBridge;
}
}
}
```
#### Option B: In `composer.json` autoload
Add to your `composer.json`:
```json
{
"autoload": {
"files": [
"node_modules/@jqhtml/core/laravel-bridge/autoload.php"
]
}
}
```
Then run:
```bash
composer dump-autoload
```
#### Option C: Manual inclusion in `bootstrap/app.php`
For Laravel 11+, add to `bootstrap/app.php`:
```php
use Illuminate\Foundation\Application;
return Application::configure(basePath: dirname(__DIR__))
->withProviders([
// After creating the app, load JQHTML
function ($app) {
$jqhtmlBridge = base_path('node_modules/@jqhtml/core/laravel-bridge/autoload.php');
if (file_exists($jqhtmlBridge)) {
require_once $jqhtmlBridge;
}
}
])
->create();
```
## Configuration
### Publish the configuration file (optional)
Create `config/jqhtml.php`:
```php
<?php
return [
'source_maps_path' => storage_path('jqhtml-sourcemaps'),
'show_source_context' => env('APP_DEBUG', false),
'context_lines' => 5,
'cache_compiled' => !env('APP_DEBUG', false),
'compiled_path' => storage_path('jqhtml-compiled'),
'enable_source_maps' => env('APP_DEBUG', false),
'source_map_mode' => 'external', // 'inline', 'external', or 'both'
];
```
## Usage
### Basic Error Handling
The bridge automatically catches and formats JQHTML template errors. When a JQHTML error occurs, it will be displayed with:
- Template file location with line and column numbers
- Source code context with error highlighting
- Helpful suggestions for common mistakes
- Source map resolution (if available)
### In Your Blade Templates
If you're compiling JQHTML templates and want to catch errors:
```php
use Jqhtml\LaravelBridge\JqhtmlException;
Route::post('/compile-template', function (Request $request) {
$templatePath = resource_path('jqhtml/' . $request->input('template'));
// Use Node.js to compile (via shell_exec, Process, etc.)
$result = shell_exec("node compile-jqhtml.js " . escapeshellarg($templatePath));
$data = json_decode($result, true);
if (!$data['success']) {
// Create exception from JS error data
throw JqhtmlException::createFromJsError($data['error']);
}
return response()->json(['compiled' => $data['code']]);
});
```
### Middleware Setup
Add to your middleware groups in `app/Http/Kernel.php`:
```php
protected $middlewareGroups = [
'web' => [
// ... other middleware
\Jqhtml\LaravelBridge\Middleware\JqhtmlErrorMiddleware::class,
],
];
```
### Manual Exception Handling
```php
use Jqhtml\LaravelBridge\JqhtmlException;
try {
// Your JQHTML compilation or execution
$compiled = compileJqhtmlTemplate($source);
} catch (\Exception $e) {
// Wrap in JQHTML exception for better display
throw new JqhtmlException(
$e->getMessage(),
'templates/my-template.jqhtml',
$lineNumber,
$columnNumber,
$sourceCode,
'Check your template syntax'
);
}
```
## Node.js Integration
### Compilation Script
Create `compile-jqhtml.js` in your Laravel project root:
```javascript
#!/usr/bin/env node
import { parse, generate } from '@jqhtml/core';
import fs from 'fs';
const templatePath = process.argv[2];
try {
const source = fs.readFileSync(templatePath, 'utf8');
const ast = parse(source, templatePath);
const { code, map } = generate(ast, { sourceMap: true });
console.log(JSON.stringify({
success: true,
code,
map
}));
} catch (error) {
// Format error for Laravel
console.log(JSON.stringify({
success: false,
error: {
message: error.message,
filename: error.filename || templatePath,
line: error.line,
column: error.column,
source: error.source,
suggestion: error.suggestion
}
}));
process.exit(1);
}
```
### Using with Laravel Mix/Vite
In `vite.config.js`:
```javascript
import { defineConfig } from 'vite';
import { parse, generate } from '@jqhtml/core';
export default defineConfig({
plugins: [
{
name: 'jqhtml',
transform(source, id) {
if (id.endsWith('.jqhtml')) {
try {
const ast = parse(source, id);
const { code, map } = generate(ast, { sourceMap: true });
return { code, map };
} catch (error) {
// Error will be caught by Laravel bridge
throw error;
}
}
}
}
]
});
```
## API Endpoint Example
Create an API endpoint for compiling JQHTML templates:
```php
// routes/api.php
use Jqhtml\LaravelBridge\JqhtmlException;
Route::post('/api/jqhtml/compile', function (Request $request) {
$template = $request->input('template');
$filename = $request->input('filename', 'template.jqhtml');
// Execute Node.js compilation
$process = new Process([
'node',
base_path('compile-jqhtml.js'),
'--stdin'
]);
$process->setInput(json_encode([
'template' => $template,
'filename' => $filename
]));
$process->run();
if (!$process->isSuccessful()) {
$output = json_decode($process->getOutput(), true);
if (isset($output['error'])) {
throw JqhtmlException::createFromJsError($output['error']);
}
throw new \Exception('Compilation failed');
}
return response()->json(json_decode($process->getOutput(), true));
});
```
## Laravel Ignition Integration
The bridge automatically integrates with Laravel Ignition (if installed) to provide enhanced error display. No additional configuration needed.
## Troubleshooting
### Bridge not loading
Ensure `node_modules/@jqhtml/core` exists:
```bash
ls -la node_modules/@jqhtml/core/laravel-bridge/
```
### Class not found errors
Clear Laravel's cache:
```bash
php artisan cache:clear
php artisan config:clear
composer dump-autoload
```
### Source maps not working
Ensure the source map directory exists and is writable:
```bash
mkdir -p storage/jqhtml-sourcemaps
chmod 755 storage/jqhtml-sourcemaps
```
## Directory Structure
After installation, your Laravel project will have:
```
your-laravel-project/
├── node_modules/
│ └── @jqhtml/
│ └── core/
│ ├── dist/ # JavaScript runtime
│ └── laravel-bridge/ # PHP integration
│ ├── src/ # PHP classes
│ ├── config/ # Laravel config
│ └── autoload.php # Autoloader
├── config/
│ └── jqhtml.php # Your config (optional)
├── storage/
│ ├── jqhtml-compiled/ # Compiled templates
│ └── jqhtml-sourcemaps/ # Source maps
└── compile-jqhtml.js # Node compilation script
```
## Benefits of This Approach
1. **Single Package**: Everything comes from npm, no Composer package needed
2. **Version Sync**: PHP and JS code always match versions
3. **Simple Updates**: Just `npm update @jqhtml/core`
4. **No Private Packagist**: No need for private Composer repositories
5. **CI/CD Friendly**: Works with standard Node.js deployment pipelines
## License
MIT - Same as @jqhtml/core

242
node_modules/@jqhtml/core/laravel-bridge/README.md generated vendored Executable file
View File

@@ -0,0 +1,242 @@
# JQHTML Laravel Bridge
Laravel integration package for JQHTML template error reporting with source map support.
## Installation
```bash
composer require jqhtml/laravel-bridge
```
## Configuration
Publish the configuration file:
```bash
php artisan vendor:publish --tag=jqhtml-config
```
## Usage
### Basic Setup
The package automatically registers its service provider and exception handler extensions. No additional setup is required for basic functionality.
### Error Handling
When a JQHTML template error occurs, the package will:
1. Parse the error message to extract template location information
2. Load source maps if available
3. Format the error for Laravel's exception handler
4. Display enhanced error information in debug mode
### Middleware
To automatically catch and format JQHTML errors, add the middleware to your route groups:
```php
// In app/Http/Kernel.php
protected $middlewareGroups = [
'web' => [
// ...
\Jqhtml\LaravelBridge\Middleware\JqhtmlErrorMiddleware::class,
],
];
```
### Manual Exception Creation
```php
use Jqhtml\LaravelBridge\JqhtmlException;
// Create from JavaScript error data
$jsError = json_decode($errorJson, true);
$exception = JqhtmlException::createFromJsError($jsError);
// Create manually
$exception = new JqhtmlException(
'Unclosed component definition',
'templates/user-card.jqhtml', // template file
42, // line number
15, // column number
$sourceCode, // template source
'Did you forget </Define:UserCard>?' // suggestion
);
```
### Integration with Node.js Compiler
When compiling JQHTML templates with Node.js, catch errors and pass them to Laravel:
```javascript
// Node.js side
try {
const compiled = jqhtml.compile(template);
} catch (error) {
// Send error details to Laravel
const errorData = {
message: error.message,
filename: error.filename,
line: error.line,
column: error.column,
source: error.source,
suggestion: error.suggestion
};
// Pass to Laravel via API or process communication
sendToLaravel(errorData);
}
```
```php
// Laravel side - Best Practice
use Jqhtml\LaravelBridge\JqhtmlException;
// Parse compiler JSON output
$errorData = json_decode($compiler_output, true);
if ($errorData && isset($errorData['error'])) {
// JqhtmlException extends ViewException, so this will show
// the template file as the error source in Laravel/Ignition
$exception = JqhtmlException::createFromJsError(
$errorData['error'],
$templatePath // Pass the actual template path
);
throw $exception;
}
// Alternative: Direct ViewException usage (if not using the bridge)
if ($errorData && isset($errorData['error'])) {
$error = $errorData['error'];
$line = $error['line'] ?? 1;
throw new \Illuminate\View\ViewException(
$error['message'],
0, // code
1, // severity
$templatePath, // file
$line, // line
null // previous
);
}
```
### Source Map Support
The package supports source maps for mapping compiled JavaScript back to original JQHTML templates:
```php
// Configure source map storage location
config(['jqhtml.source_maps_path' => storage_path('sourcemaps')]);
// The formatter will automatically look for source maps
// when an error includes a compiled file reference
$exception->setCompiledFile('/path/to/compiled.js');
```
### Custom Error Views
Create a custom error view at `resources/views/jqhtml/error.blade.php`:
```blade
@extends('errors::layout')
@section('title', 'JQHTML Template Error')
@section('message')
<div class="jqhtml-error">
<h2>{{ $exception->getMessage() }}</h2>
@if($exception->getSuggestion())
<div class="suggestion">
💡 {{ $exception->getSuggestion() }}
</div>
@endif
@if($exception->getTemplateFile())
<div class="location">
📍 {{ $exception->getTemplateFile() }}:{{ $exception->getTemplateLine() }}:{{ $exception->getTemplateColumn() }}
</div>
@endif
@if(isset($error_data['source_context']))
<div class="source-context">
<pre><code>@foreach($error_data['source_context'] as $line)
@if($line['is_error_line'])<strong>> {{ $line['line_number'] }} | {{ $line['content'] }}
@if($line['error_column']){{ str_repeat(' ', $line['error_column'] + 8) }}{{ str_repeat('^', 20) }}@endif</strong>
@else {{ $line['line_number'] }} | {{ $line['content'] }}
@endif
@endforeach</code></pre>
</div>
@endif
</div>
@endsection
```
## Configuration Options
```php
return [
// Where to store source map files
'source_maps_path' => storage_path('jqhtml-sourcemaps'),
// Show source code context in errors
'show_source_context' => env('APP_DEBUG', false),
// Lines of context to show
'context_lines' => 5,
// Cache compiled templates
'cache_compiled' => !env('APP_DEBUG', false),
// Where to store compiled templates
'compiled_path' => storage_path('jqhtml-compiled'),
// Enable source map generation
'enable_source_maps' => env('APP_DEBUG', false),
// Source map mode: 'inline', 'external', or 'both'
'source_map_mode' => 'external',
];
```
## Laravel Ignition Integration
The package automatically integrates with Laravel Ignition (if installed) to provide enhanced error display:
- Template file location with line and column numbers
- Source code context with error highlighting
- Helpful suggestions for common mistakes
- Source map resolution for compiled files
## API Reference
### JqhtmlException
Main exception class for JQHTML template errors. **Extends Laravel's `ViewException`** for optimal integration with Laravel's error handling and Ignition error pages.
When thrown, this exception ensures:
- The template file appears as the error source in Laravel/Ignition
- Line numbers point to the actual template location
- The error page shows your JQHTML template, not the PHP processor
### JqhtmlErrorFormatter
Formats exceptions for display with source context and source map support.
### JqhtmlExceptionRenderer
Renders exceptions for web and JSON responses.
### JqhtmlServiceProvider
Registers services and extends Laravel's exception handler.
### JqhtmlErrorMiddleware
Middleware for catching and wrapping JQHTML errors.
## License
MIT

51
node_modules/@jqhtml/core/laravel-bridge/autoload.php generated vendored Executable file
View File

@@ -0,0 +1,51 @@
<?php
/**
* JQHTML Laravel Bridge Autoloader
*
* Include this file from your Laravel project to load the JQHTML error handling bridge:
*
* require_once base_path('node_modules/@jqhtml/core/laravel-bridge/autoload.php');
*
* Or if using in a service provider:
*
* require_once dirname(__DIR__, 3) . '/node_modules/@jqhtml/core/laravel-bridge/autoload.php';
*/
// This is a Laravel bridge - it REQUIRES Laravel to function
if (!class_exists('Illuminate\\Support\\ServiceProvider')) {
throw new \RuntimeException(
'JQHTML Laravel Bridge requires Laravel. ' .
'This file should only be included from within a Laravel application.'
);
}
// Register the autoloader for JQHTML Laravel Bridge classes
spl_autoload_register(function ($class) {
// Check if the class is in the Jqhtml\LaravelBridge namespace
$prefix = 'Jqhtml\\LaravelBridge\\';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
// Get the relative class name
$relative_class = substr($class, $len);
// Replace namespace separators with directory separators
$file = __DIR__ . '/src/' . str_replace('\\', '/', $relative_class) . '.php';
// If the file exists, require it
if (file_exists($file)) {
require $file;
}
});
// Auto-register the service provider if in Laravel application context
if (function_exists('app') && app() instanceof \Illuminate\Foundation\Application) {
app()->register(\Jqhtml\LaravelBridge\JqhtmlServiceProvider::class);
}
// Return the namespace for convenience
return 'Jqhtml\\LaravelBridge';

34
node_modules/@jqhtml/core/laravel-bridge/composer.json generated vendored Executable file
View File

@@ -0,0 +1,34 @@
{
"name": "jqhtml/laravel-bridge",
"description": "Laravel integration bridge for JQHTML template error reporting and source maps",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "JQHTML Team"
}
],
"require": {
"php": "^7.4|^8.0",
"illuminate/support": "^8.0|^9.0|^10.0|^11.0"
},
"autoload": {
"psr-4": {
"Jqhtml\\LaravelBridge\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Jqhtml\\LaravelBridge\\Tests\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"Jqhtml\\LaravelBridge\\JqhtmlServiceProvider"
]
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

82
node_modules/@jqhtml/core/laravel-bridge/config/jqhtml.php generated vendored Executable file
View File

@@ -0,0 +1,82 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| JQHTML Source Maps Path
|--------------------------------------------------------------------------
|
| This option defines where JQHTML source map files should be stored.
| Source maps help map compiled JavaScript back to original JQHTML templates
| for better error reporting and debugging.
|
*/
'source_maps_path' => storage_path('jqhtml-sourcemaps'),
/*
|--------------------------------------------------------------------------
| Show Source Context
|--------------------------------------------------------------------------
|
| When enabled, error messages will include the surrounding source code
| context to help identify the exact location and nature of the error.
|
*/
'show_source_context' => env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Error Context Lines
|--------------------------------------------------------------------------
|
| Number of lines to show before and after the error line when displaying
| source context in error messages.
|
*/
'context_lines' => 5,
/*
|--------------------------------------------------------------------------
| Cache Compiled Templates
|--------------------------------------------------------------------------
|
| When enabled, compiled JQHTML templates will be cached to improve
| performance. Disable during development for immediate template updates.
|
*/
'cache_compiled' => env('JQHTML_CACHE', !env('APP_DEBUG', false)),
/*
|--------------------------------------------------------------------------
| Compiled Templates Path
|--------------------------------------------------------------------------
|
| Directory where compiled JQHTML templates should be stored.
|
*/
'compiled_path' => storage_path('jqhtml-compiled'),
/*
|--------------------------------------------------------------------------
| Enable Source Maps
|--------------------------------------------------------------------------
|
| Whether to generate source maps for compiled templates. Source maps
| increase compilation time slightly but provide better debugging.
|
*/
'enable_source_maps' => env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Source Map Mode
|--------------------------------------------------------------------------
|
| How source maps should be generated:
| - 'inline': Embed source map directly in compiled file
| - 'external': Save source map as separate .map file
| - 'both': Generate both inline and external source maps
|
*/
'source_map_mode' => 'external',
];

View File

@@ -0,0 +1,201 @@
#!/usr/bin/env node
/**
* Example Node.js integration for sending JQHTML errors to Laravel
*
* This shows how to compile JQHTML templates and send any errors
* to a Laravel backend for proper error handling and display.
*/
import { Lexer, Parser, CodeGenerator, JQHTMLParseError } from '@jqhtml/parser';
import fs from 'fs';
import fetch from 'node-fetch'; // or axios, etc.
/**
* Compile a JQHTML template and handle errors
*/
async function compileTemplate(templatePath, laravelEndpoint) {
try {
// Read template file
const source = fs.readFileSync(templatePath, 'utf8');
const filename = templatePath;
// Compile with source maps
const lexer = new Lexer(source);
const tokens = lexer.tokenize();
const parser = new Parser(tokens, source, filename);
const ast = parser.parse();
const generator = new CodeGenerator();
const result = generator.generateWithSourceMap(ast, filename, source);
// Success - return compiled code
return {
success: true,
code: result.code,
sourceMap: result.map
};
} catch (error) {
// Format error for Laravel
const errorData = formatErrorForLaravel(error, templatePath);
// Send to Laravel if endpoint provided
if (laravelEndpoint) {
await sendErrorToLaravel(errorData, laravelEndpoint);
}
// Return error response
return {
success: false,
error: errorData
};
}
}
/**
* Format a JavaScript error for Laravel consumption
*/
function formatErrorForLaravel(error, templatePath) {
// Check if it's a JQHTML parse error with full details
if (error instanceof JQHTMLParseError || error.name === 'JQHTMLParseError') {
return {
message: error.message,
filename: error.filename || templatePath,
line: error.line,
column: error.column,
source: error.source,
suggestion: error.suggestion,
severity: error.severity || 'error',
endLine: error.endLine,
endColumn: error.endColumn
};
}
// Generic error - try to extract what we can
const errorData = {
message: error.message || String(error),
filename: templatePath,
severity: 'error'
};
// Try to parse location from error message
const locationMatch = error.message.match(/at line (\d+), column (\d+)/);
if (locationMatch) {
errorData.line = parseInt(locationMatch[1]);
errorData.column = parseInt(locationMatch[2]);
}
return errorData;
}
/**
* Send error data to Laravel backend
*/
async function sendErrorToLaravel(errorData, endpoint) {
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
error: errorData,
context: {
node_version: process.version,
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV || 'development'
}
})
});
if (!response.ok) {
console.error('Failed to send error to Laravel:', response.statusText);
}
return response.json();
} catch (err) {
console.error('Error communicating with Laravel:', err);
}
}
/**
* Express/HTTP endpoint for template compilation
*
* This could be used in a Node.js service that compiles templates
* for a Laravel application.
*/
export function createCompilationEndpoint(app, laravelErrorEndpoint) {
app.post('/compile-jqhtml', async (req, res) => {
const { template, filename, source } = req.body;
try {
// Compile template
const lexer = new Lexer(source || template);
const tokens = lexer.tokenize();
const parser = new Parser(tokens, source || template, filename);
const ast = parser.parse();
const generator = new CodeGenerator();
const result = generator.generateWithSourceMap(
ast,
filename || 'template.jqhtml',
source || template
);
// Success response
res.json({
success: true,
compiled: {
code: result.code,
map: result.map
}
});
} catch (error) {
// Format error for Laravel
const errorData = formatErrorForLaravel(error, filename);
// Send error to Laravel for logging/display
if (laravelErrorEndpoint) {
sendErrorToLaravel(errorData, laravelErrorEndpoint);
}
// Return error response
res.status(400).json({
success: false,
error: errorData
});
}
});
}
/**
* CLI usage example
*/
if (import.meta.url === `file://${process.argv[1]}`) {
const templatePath = process.argv[2];
const laravelEndpoint = process.argv[3] || process.env.LARAVEL_ERROR_ENDPOINT;
if (!templatePath) {
console.error('Usage: node-integration.js <template-file> [laravel-endpoint]');
process.exit(1);
}
compileTemplate(templatePath, laravelEndpoint).then(result => {
if (result.success) {
console.log('Compilation successful!');
console.log('Code length:', result.code.length);
console.log('Source map:', result.sourceMap ? 'Generated' : 'Not generated');
} else {
console.error('Compilation failed!');
console.error(result.error);
process.exit(1);
}
});
}
// Export for use as module
export { compileTemplate, formatErrorForLaravel, sendErrorToLaravel };

View File

@@ -0,0 +1,187 @@
<?php
namespace Jqhtml\LaravelBridge;
use Throwable;
class JqhtmlErrorFormatter
{
protected $sourceMapPath;
protected $showSourceContext;
protected $sourceMapCache = [];
public function __construct(?string $sourceMapPath = null, bool $showSourceContext = true)
{
$this->sourceMapPath = $sourceMapPath;
$this->showSourceContext = $showSourceContext;
}
/**
* Format a JQHTML exception for Laravel's error handler
*/
public function format(JqhtmlException $exception): array
{
$data = [
'message' => $exception->getMessage(),
'exception' => get_class($exception),
'file' => $exception->getTemplateFile() ?? $exception->getFile(),
'line' => $exception->getTemplateLine() ?? $exception->getLine(),
'column' => $exception->getTemplateColumn(),
'error_type' => $exception->getErrorType(),
];
if ($exception->getSuggestion()) {
$data['suggestion'] = $exception->getSuggestion();
}
if ($this->showSourceContext && $exception->getSourceCode()) {
$data['source_context'] = $this->getSourceContext($exception);
}
// Try to resolve source map if we have a compiled file location
if ($exception->getCompiledFile() && $this->sourceMapPath) {
$data['source_map'] = $this->resolveSourceMap($exception);
}
return $data;
}
/**
* Format for JSON responses
*/
public function formatForJson(JqhtmlException $exception): array
{
return [
'error' => true,
'type' => 'jqhtml_error',
'message' => $exception->getMessage(),
'details' => $this->format($exception),
];
}
/**
* Get source code context for display
*/
protected function getSourceContext(JqhtmlException $exception): array
{
$source = $exception->getSourceCode();
$line = $exception->getTemplateLine();
$column = $exception->getTemplateColumn();
if (!$source || !$line) {
return [];
}
$lines = explode("\n", $source);
$lineIndex = $line - 1;
// Get 5 lines before and after for context
$contextSize = 5;
$start = max(0, $lineIndex - $contextSize);
$end = min(count($lines) - 1, $lineIndex + $contextSize);
$context = [];
for ($i = $start; $i <= $end; $i++) {
$context[] = [
'line_number' => $i + 1,
'content' => $lines[$i],
'is_error_line' => $i === $lineIndex,
'error_column' => $i === $lineIndex ? $column : null,
];
}
return $context;
}
/**
* Attempt to resolve source map for better error location
*/
protected function resolveSourceMap(JqhtmlException $exception): ?array
{
$compiledFile = $exception->getCompiledFile();
if (!$compiledFile || !file_exists($compiledFile)) {
return null;
}
// Look for source map file
$mapFile = $compiledFile . '.map';
if (!file_exists($mapFile)) {
// Try in the configured source map directory
if ($this->sourceMapPath) {
$mapFile = $this->sourceMapPath . '/' . basename($compiledFile) . '.map';
if (!file_exists($mapFile)) {
return null;
}
} else {
return null;
}
}
// Load and parse source map
if (!isset($this->sourceMapCache[$mapFile])) {
$mapContent = file_get_contents($mapFile);
$this->sourceMapCache[$mapFile] = json_decode($mapContent, true);
}
$sourceMap = $this->sourceMapCache[$mapFile];
if (!$sourceMap) {
return null;
}
return [
'version' => $sourceMap['version'] ?? null,
'sources' => $sourceMap['sources'] ?? [],
'file' => $sourceMap['file'] ?? null,
'has_mappings' => !empty($sourceMap['mappings']),
];
}
/**
* Convert a generic exception to JQHTML exception if it contains JQHTML error data
*/
public function wrapException(Throwable $exception): Throwable
{
// Check if the exception message contains JQHTML error data
$message = $exception->getMessage();
// Look for JQHTML error patterns
if (strpos($message, 'JQHTMLParseError') !== false ||
strpos($message, 'at line') !== false && strpos($message, 'column') !== false) {
// Try to extract error details from the message
$templateFile = null;
$templateLine = null;
$templateColumn = null;
// Extract location info from "at filename:line:column" format
if (preg_match('/at\s+([^:]+):(\d+):(\d+)/', $message, $matches)) {
$templateFile = $matches[1];
$templateLine = (int)$matches[2];
$templateColumn = (int)$matches[3];
} elseif (preg_match('/at\s+line\s+(\d+),\s+column\s+(\d+)/', $message, $matches)) {
$templateLine = (int)$matches[1];
$templateColumn = (int)$matches[2];
}
// Extract suggestion if present
$suggestion = null;
if (preg_match('/Did you\s+(.+?)\?/', $message, $matches)) {
$suggestion = 'Did you ' . $matches[1] . '?';
}
return new JqhtmlException(
$message,
$templateFile,
$templateLine,
$templateColumn,
null,
$suggestion,
'parse',
$exception->getCode(),
$exception
);
}
return $exception;
}
}

View File

@@ -0,0 +1,173 @@
<?php
namespace Jqhtml\LaravelBridge;
use Illuminate\View\ViewException;
use Throwable;
class JqhtmlException extends ViewException
{
protected $templateFile;
protected $templateLine;
protected $templateColumn;
protected $sourceCode;
protected $suggestion;
protected $errorType;
protected $compiledFile;
public function __construct(
string $message,
?string $templateFile = null,
?int $templateLine = null,
?int $templateColumn = null,
?string $sourceCode = null,
?string $suggestion = null,
string $errorType = 'parse',
int $code = 0,
?Throwable $previous = null
) {
// Call ViewException constructor with template file and line info
// ViewException signature: __construct($message, $code = 0, $severity = 1, $filename = '', $lineno = 0, $previous = null)
parent::__construct(
$message,
$code,
1, // severity
$templateFile ?? '',
$templateLine ?? 0,
$previous
);
$this->templateFile = $templateFile;
$this->templateLine = $templateLine;
$this->templateColumn = $templateColumn;
$this->sourceCode = $sourceCode;
$this->suggestion = $suggestion;
$this->errorType = $errorType;
}
/**
* Create from a JavaScript error object or JSON string
*/
public static function createFromJsError($jsError, ?string $compiledFile = null): self
{
if (is_string($jsError)) {
$jsError = json_decode($jsError, true);
}
return new self(
$jsError['message'] ?? 'Unknown JQHTML error',
$jsError['filename'] ?? $jsError['templateFile'] ?? null,
$jsError['line'] ?? null,
$jsError['column'] ?? null,
$jsError['source'] ?? null,
$jsError['suggestion'] ?? null,
$jsError['severity'] ?? 'error'
);
}
public function getTemplateFile(): ?string
{
return $this->templateFile;
}
public function getTemplateLine(): ?int
{
return $this->templateLine;
}
public function getTemplateColumn(): ?int
{
return $this->templateColumn;
}
public function getSourceCode(): ?string
{
return $this->sourceCode;
}
public function getSuggestion(): ?string
{
return $this->suggestion;
}
public function getErrorType(): string
{
return $this->errorType;
}
public function getCompiledFile(): ?string
{
return $this->compiledFile;
}
public function setCompiledFile(string $file): self
{
$this->compiledFile = $file;
return $this;
}
/**
* Get formatted error message with context
*/
public function getFormattedMessage(): string
{
$message = $this->getMessage();
if ($this->suggestion) {
$message .= "\n" . $this->suggestion;
}
if ($this->templateFile) {
$message .= sprintf(
"\n at %s:%d:%d",
$this->templateFile,
$this->templateLine ?? 0,
$this->templateColumn ?? 0
);
}
if ($this->sourceCode && $this->templateLine) {
$message .= "\n\n" . $this->getCodeSnippet();
}
return $message;
}
/**
* Get code snippet with error highlighting
*/
protected function getCodeSnippet(): string
{
if (!$this->sourceCode || !$this->templateLine) {
return '';
}
$lines = explode("\n", $this->sourceCode);
$lineIndex = $this->templateLine - 1;
// Show 3 lines before and after for context
$contextLines = 3;
$startLine = max(0, $lineIndex - $contextLines);
$endLine = min(count($lines) - 1, $lineIndex + $contextLines);
$snippet = '';
for ($i = $startLine; $i <= $endLine; $i++) {
$lineNum = $i + 1;
$isErrorLine = $i === $lineIndex;
$prefix = $isErrorLine ? '>' : ' ';
// Line number with padding
$lineNumStr = str_pad((string)$lineNum, 5, ' ', STR_PAD_LEFT);
$snippet .= sprintf("%s %s | %s\n", $prefix, $lineNumStr, $lines[$i]);
// Add pointer to error column
if ($isErrorLine && $this->templateColumn) {
$spaces = str_repeat(' ', $this->templateColumn + 8);
$carets = str_repeat('^', min(strlen($lines[$i]) - $this->templateColumn + 1, 20));
$snippet .= $spaces . $carets . "\n";
}
}
return $snippet;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Jqhtml\LaravelBridge;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Throwable;
class JqhtmlExceptionRenderer
{
protected $formatter;
public function __construct(JqhtmlErrorFormatter $formatter)
{
$this->formatter = $formatter;
}
/**
* Render a JQHTML exception for display
*/
public function render(Request $request, JqhtmlException $exception): ?Response
{
if ($request->expectsJson()) {
return $this->renderJson($exception);
}
// For development, enhance the error page with JQHTML-specific info
if (app()->hasDebugModeEnabled()) {
return $this->renderDebugView($exception);
}
// In production, return null to let Laravel handle it normally
return null;
}
/**
* Render JSON error response
*/
protected function renderJson(JqhtmlException $exception): JsonResponse
{
return response()->json(
$this->formatter->formatForJson($exception),
500
);
}
/**
* Render debug view with enhanced JQHTML error information
*/
protected function renderDebugView(JqhtmlException $exception): ?Response
{
// Get formatted error data
$errorData = $this->formatter->format($exception);
// If using Laravel's Ignition error page, enhance it with our data
if (class_exists(\Spatie\LaravelIgnition\Facades\Flare::class)) {
\Spatie\LaravelIgnition\Facades\Flare::context('JQHTML Error', $errorData);
return null; // Let Ignition handle the rendering
}
// If using older Laravel or custom error handling
try {
// Check if we have a custom view
if (view()->exists('jqhtml::error')) {
return response()->view('jqhtml::error', [
'exception' => $exception,
'error_data' => $errorData,
], 500);
}
} catch (Throwable $e) {
// Fall back to letting Laravel handle it
}
return null;
}
/**
* Check if an exception should be handled by this renderer
*/
public function shouldHandle(Throwable $exception): bool
{
if ($exception instanceof JqhtmlException) {
return true;
}
// Check if it's a wrapped JQHTML error
$message = $exception->getMessage();
return strpos($message, 'JQHTMLParseError') !== false ||
strpos($message, 'JQHTML') !== false;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Jqhtml\LaravelBridge;
use Illuminate\Support\ServiceProvider;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class JqhtmlServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register()
{
// Register the error formatter as a singleton
$this->app->singleton(JqhtmlErrorFormatter::class, function ($app) {
return new JqhtmlErrorFormatter(
$app['config']->get('jqhtml.source_maps_path', storage_path('jqhtml-sourcemaps')),
$app['config']->get('jqhtml.show_source_context', true)
);
});
// Register the exception renderer
$this->app->singleton(JqhtmlExceptionRenderer::class, function ($app) {
return new JqhtmlExceptionRenderer(
$app->make(JqhtmlErrorFormatter::class)
);
});
}
/**
* Bootstrap any application services.
*/
public function boot()
{
// Publish configuration
$this->publishes([
__DIR__ . '/../config/jqhtml.php' => config_path('jqhtml.php'),
], 'jqhtml-config');
// Extend Laravel's exception handler
$this->extendExceptionHandler();
}
/**
* Extend Laravel's exception handler to handle JQHTML exceptions
*/
protected function extendExceptionHandler()
{
$this->app->extend(ExceptionHandler::class, function ($handler, $app) {
// Hook into the exception rendering process
$handler->reportable(function (JqhtmlException $e) {
// Custom reporting logic if needed
});
$handler->renderable(function (JqhtmlException $e, $request) use ($app) {
if ($request->expectsJson()) {
return response()->json(
$app->make(JqhtmlErrorFormatter::class)->formatForJson($e),
500
);
}
// For web requests, let Laravel's default HTML handler display it
// with our enhanced error information
return null;
});
return $handler;
});
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Jqhtml\LaravelBridge\Middleware;
use Closure;
use Illuminate\Http\Request;
use Jqhtml\LaravelBridge\JqhtmlErrorFormatter;
use Jqhtml\LaravelBridge\JqhtmlException;
use Throwable;
class JqhtmlErrorMiddleware
{
protected $formatter;
public function __construct(JqhtmlErrorFormatter $formatter)
{
$this->formatter = $formatter;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
try {
return $next($request);
} catch (Throwable $exception) {
// Check if this might be a JQHTML error
if ($this->isJqhtmlError($exception)) {
// Wrap it in our exception type for better handling
$wrapped = $this->formatter->wrapException($exception);
if ($wrapped instanceof JqhtmlException) {
// Add request context
$wrapped->setCompiledFile(
$this->getCompiledFileFromRequest($request)
);
throw $wrapped;
}
}
// Not a JQHTML error, rethrow as-is
throw $exception;
}
}
/**
* Check if an exception appears to be JQHTML-related
*/
protected function isJqhtmlError(Throwable $exception): bool
{
$message = $exception->getMessage();
return strpos($message, 'JQHTML') !== false ||
strpos($message, 'jqhtml') !== false ||
strpos($message, 'at line') !== false && strpos($message, 'column') !== false ||
strpos($message, 'Unclosed component') !== false ||
strpos($message, 'Mismatched tags') !== false;
}
/**
* Try to determine the compiled file from the request
*/
protected function getCompiledFileFromRequest(Request $request): ?string
{
// Check if the request has a reference to a compiled template
$route = $request->route();
if ($route && method_exists($route, 'getAction')) {
$action = $route->getAction();
// Look for JQHTML template reference in route action
if (isset($action['jqhtml_template'])) {
return $action['jqhtml_template'];
}
}
// Check request attributes
if ($request->has('_jqhtml_compiled')) {
return $request->get('_jqhtml_compiled');
}
return null;
}
}

View File

@@ -0,0 +1,219 @@
<?php
namespace Jqhtml\LaravelBridge\Tests;
use PHPUnit\Framework\TestCase;
use Jqhtml\LaravelBridge\JqhtmlException;
use Jqhtml\LaravelBridge\JqhtmlErrorFormatter;
class ExceptionFormattingTest extends TestCase
{
protected $formatter;
protected function setUp(): void
{
parent::setUp();
$this->formatter = new JqhtmlErrorFormatter();
}
public function testBasicExceptionCreation()
{
$exception = new JqhtmlException(
'Unclosed component definition',
'templates/test.jqhtml',
10,
15
);
$this->assertEquals('Unclosed component definition', $exception->getMessage());
$this->assertEquals('templates/test.jqhtml', $exception->getTemplateFile());
$this->assertEquals(10, $exception->getTemplateLine());
$this->assertEquals(15, $exception->getTemplateColumn());
}
public function testExceptionWithSuggestion()
{
$exception = new JqhtmlException(
'Unclosed component definition',
'templates/test.jqhtml',
10,
15,
null,
'Did you forget </Define:ComponentName>?'
);
$this->assertEquals('Did you forget </Define:ComponentName>?', $exception->getSuggestion());
$formatted = $exception->getFormattedMessage();
$this->assertStringContainsString('Did you forget', $formatted);
}
public function testExceptionFromJsError()
{
$jsError = [
'message' => 'Syntax error: unexpected token',
'filename' => 'app.jqhtml',
'line' => 42,
'column' => 8,
'suggestion' => 'Check for missing closing tags',
'severity' => 'error'
];
$exception = JqhtmlException::createFromJsError($jsError);
$this->assertEquals('Syntax error: unexpected token', $exception->getMessage());
$this->assertEquals('app.jqhtml', $exception->getTemplateFile());
$this->assertEquals(42, $exception->getTemplateLine());
$this->assertEquals(8, $exception->getTemplateColumn());
$this->assertEquals('Check for missing closing tags', $exception->getSuggestion());
}
public function testExceptionFromJsonString()
{
$json = json_encode([
'message' => 'Parse error',
'templateFile' => 'template.jqhtml',
'line' => 5,
'column' => 10
]);
$exception = JqhtmlException::createFromJsError($json);
$this->assertEquals('Parse error', $exception->getMessage());
$this->assertEquals('template.jqhtml', $exception->getTemplateFile());
$this->assertEquals(5, $exception->getTemplateLine());
$this->assertEquals(10, $exception->getTemplateColumn());
}
public function testFormatterBasicFormat()
{
$exception = new JqhtmlException(
'Test error',
'test.jqhtml',
20,
5
);
$formatted = $this->formatter->format($exception);
$this->assertArrayHasKey('message', $formatted);
$this->assertArrayHasKey('file', $formatted);
$this->assertArrayHasKey('line', $formatted);
$this->assertArrayHasKey('column', $formatted);
$this->assertArrayHasKey('error_type', $formatted);
$this->assertEquals('Test error', $formatted['message']);
$this->assertEquals('test.jqhtml', $formatted['file']);
$this->assertEquals(20, $formatted['line']);
$this->assertEquals(5, $formatted['column']);
}
public function testFormatterWithSourceContext()
{
$sourceCode = "line 1\nline 2\nline 3 with error\nline 4\nline 5";
$exception = new JqhtmlException(
'Error on line 3',
'test.jqhtml',
3,
10,
$sourceCode
);
$formatter = new JqhtmlErrorFormatter(null, true);
$formatted = $formatter->format($exception);
$this->assertArrayHasKey('source_context', $formatted);
$context = $formatted['source_context'];
$this->assertIsArray($context);
// Find the error line in context
$errorLine = null;
foreach ($context as $line) {
if ($line['is_error_line']) {
$errorLine = $line;
break;
}
}
$this->assertNotNull($errorLine);
$this->assertEquals(3, $errorLine['line_number']);
$this->assertEquals('line 3 with error', $errorLine['content']);
$this->assertEquals(10, $errorLine['error_column']);
}
public function testFormatterJsonFormat()
{
$exception = new JqhtmlException('JSON test error');
$json = $this->formatter->formatForJson($exception);
$this->assertArrayHasKey('error', $json);
$this->assertArrayHasKey('type', $json);
$this->assertArrayHasKey('message', $json);
$this->assertArrayHasKey('details', $json);
$this->assertTrue($json['error']);
$this->assertEquals('jqhtml_error', $json['type']);
$this->assertEquals('JSON test error', $json['message']);
}
public function testWrapGenericException()
{
$genericException = new \Exception(
'JQHTMLParseError: Unclosed tag at line 10, column 5'
);
$wrapped = $this->formatter->wrapException($genericException);
$this->assertInstanceOf(JqhtmlException::class, $wrapped);
$this->assertEquals(10, $wrapped->getTemplateLine());
$this->assertEquals(5, $wrapped->getTemplateColumn());
}
public function testWrapExceptionWithFilename()
{
$genericException = new \Exception(
'Error at component.jqhtml:15:20 - syntax error'
);
$wrapped = $this->formatter->wrapException($genericException);
$this->assertInstanceOf(JqhtmlException::class, $wrapped);
$this->assertEquals('component.jqhtml', $wrapped->getTemplateFile());
$this->assertEquals(15, $wrapped->getTemplateLine());
$this->assertEquals(20, $wrapped->getTemplateColumn());
}
public function testCodeSnippetGeneration()
{
$source = implode("\n", [
'line 1',
'line 2',
'line 3',
'error is here', // line 4
'line 5',
'line 6',
'line 7'
]);
$exception = new JqhtmlException(
'Error message',
'test.jqhtml',
4,
8,
$source
);
$formatted = $exception->getFormattedMessage();
// Should show context lines
$this->assertStringContainsString('line 3', $formatted);
$this->assertStringContainsString('error is here', $formatted);
$this->assertStringContainsString('line 5', $formatted);
// Should have error pointer
$this->assertStringContainsString('^', $formatted);
}
}