Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,6 @@ claude.json


PRODUCTION_READY.md
DELETED_TESTS.md
HTML_TESTS_STATUS.md
HTML.md
1,518 changes: 444 additions & 1,074 deletions phpstan-baseline.neon

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
includes:
- vendor/larastan/larastan/extension.neon
- phpstan-baseline.neon

parameters:
level: 9
Expand Down
4 changes: 4 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
</source>

<php>
<!-- Disable OPcache to prevent stack overflow with large test files in PHP 8.4 -->
<ini name="opcache.enable" value="0"/>
<ini name="opcache.jit" value="disable"/>

<env name="APP_ENV" value="testing"/>
<env name="APP_KEY" value="base64:2fl1JRpkYXOX8JgKp/P6dXkAXvAKmPMnEtcJZ2nHqzw="/>
<env name="CACHE_DRIVER" value="array"/>
Expand Down
11 changes: 10 additions & 1 deletion resources/js/hyper.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion resources/js/hyper.js.map

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions run-all-html-tests.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
@echo off
setlocal enabledelayedexpansion

echo ==========================================
echo HTML Builder Test Suite - All Tests
echo ==========================================
echo.
echo Running 292 tests across 8 test files...
echo.

set TOTAL_TESTS=0
set TOTAL_PASSED=0
set TOTAL_FAILED=0

echo [1/8] ElementBasicsTest (36 tests)...
"C:\Users\pc\.config\herd\bin\php.bat" -d opcache.enable=0 vendor\bin\phpunit tests\Unit\Html\ElementBasicsTest.php --no-progress --colors=never 2>&1 | findstr /C:"OK" /C:"FAILURES"
echo.

echo [2/8] VoidElementTest (32 tests)...
"C:\Users\pc\.config\herd\bin\php.bat" -d opcache.enable=0 vendor\bin\phpunit tests\Unit\Html\VoidElementTest.php --no-progress --colors=never 2>&1 | findstr /C:"OK" /C:"FAILURES"
echo.

echo [3/8] TextElementTest (50 tests)...
"C:\Users\pc\.config\herd\bin\php.bat" -d opcache.enable=0 vendor\bin\phpunit tests\Unit\Html\TextElementTest.php --no-progress --colors=never 2>&1 | findstr /C:"OK" /C:"FAILURES"
echo.

echo [4/8] GlobalAttributesTest (63 tests)...
"C:\Users\pc\.config\herd\bin\php.bat" -d opcache.enable=0 vendor\bin\phpunit tests\Unit\Html\Attributes\GlobalAttributesTest.php --no-progress --colors=never 2>&1 | findstr /C:"OK" /C:"FAILURES"
echo.

echo [5/8] FormAttributesTest (51 tests)...
"C:\Users\pc\.config\herd\bin\php.bat" -d opcache.enable=0 vendor\bin\phpunit tests\Unit\Html\Attributes\FormAttributesTest.php --no-progress --colors=never 2>&1 | findstr /C:"OK" /C:"FAILURES"
echo.

echo [6/8] ContainerElement_BasicTest (18 tests)...
"C:\Users\pc\.config\herd\bin\php.bat" -d opcache.enable=0 vendor\bin\phpunit tests\Unit\Html\ContainerElement_BasicTest.php --no-progress --colors=never 2>&1 | findstr /C:"OK" /C:"FAILURES"
echo.

echo [7/8] ContainerElement_SecurityTest (18 tests)...
"C:\Users\pc\.config\herd\bin\php.bat" -d opcache.enable=0 vendor\bin\phpunit tests\Unit\Html\ContainerElement_SecurityTest.php --no-progress --colors=never 2>&1 | findstr /C:"OK" /C:"FAILURES"
echo.

echo [8/8] DataSignalsIntegrationTest (24 tests)...
"C:\Users\pc\.config\herd\bin\php.bat" -d opcache.enable=0 vendor\bin\phpunit tests\Unit\Html\DataSignalsIntegrationTest.php --no-progress --colors=never 2>&1 | findstr /C:"OK" /C:"FAILURES"
echo.

echo ==========================================
echo Test Suite Complete
echo ==========================================
echo.
echo Expected: 292 tests passing
echo See HTML_TESTS_STATUS.md for details
echo.
25 changes: 25 additions & 0 deletions run-html-tests.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@echo off
setlocal enabledelayedexpansion

echo ======================================
echo HTML Builder Test Suite Runner
echo ======================================
echo.

set TOTAL_TESTS=0
set TOTAL_PASSED=0
set TOTAL_FAILED=0

for %%F in (tests\Unit\Html\*.php tests\Unit\Html\Attributes\*.php) do (
echo Testing: %%~nF
"C:\Users\pc\.config\herd\bin\php.bat" -d opcache.enable=0 vendor\bin\phpunit "%%F" --no-progress > temp_test_output.txt 2>&1

findstr /C:"OK" /C:"FAILURES" temp_test_output.txt
echo.
)

del temp_test_output.txt

echo ======================================
echo Test Suite Complete
echo ======================================
6 changes: 4 additions & 2 deletions src/Exceptions/HyperValidationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ class HyperValidationException extends ValidationException
* constructor for standard Laravel validation exception initialization.
*
* @param \Illuminate\Contracts\Validation\Validator $validator Validator instance with errors
* @param array<string, array<int, string>> $errors cleared validation fields
* @param \Symfony\Component\HttpFoundation\Response|null $response Optional custom response
* @param string $errorBag Error bag name for multiple validation contexts
*/
public function __construct($validator, $response = null, $errorBag = 'default')
public function __construct(\Illuminate\Contracts\Validation\Validator $validator, array $errors = [], ?\Symfony\Component\HttpFoundation\Response $response = null, string $errorBag = 'default')
{
parent::__construct($validator, $response, $errorBag);
$this->errors = $validator->errors()->toArray();
$errors = [...$errors, ...$validator->errors()->toArray()];
$this->errors = $errors;
}

/**
Expand Down
140 changes: 140 additions & 0 deletions src/Html/Concerns/Assets/ManagesAssets.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php

namespace Dancycodes\Hyper\Html\Concerns\Assets;

use Closure;

/**
* Asset management helpers for framework integration
*
* Provides chainable methods to inject framework asset tags (Vite, Hyper, etc.)
* These methods replicate Blade directives but in pure PHP, enabling developers
* to use the HTML Builder without mixing Blade syntax.
*
* All methods inject raw HTML content and return the element for method chaining.
*/
trait ManagesAssets
{
/**
* Inject Laravel Vite asset tags (replicates @vite directive)
*
* Automatically includes the necessary script and link tags for Vite assets.
* In development, connects to the Vite dev server. In production, loads
* the built assets from the manifest.
*
* Example:
* ```php
* Html::head()
* ->vite(['resources/css/app.css', 'resources/js/app.js'])
* ->content(Html::title('My App'));
* ```
*
* @param string|array|Closure $entrypoints File paths to Vite entry points or closure returning paths
* @param string|null|Closure $buildDirectory Optional build directory or closure returning directory
*/
public function vite(string|array|Closure $entrypoints = [], string|null|Closure $buildDirectory = null): static
{
// Evaluate closures if provided
$entrypoints = $this->evaluate($entrypoints);
$buildDirectory = $buildDirectory ? $this->evaluate($buildDirectory) : null;

// Get Vite instance and generate HTML
$vite = app(\Illuminate\Foundation\Vite::class);
$html = $vite($entrypoints, $buildDirectory);

// Inject as raw HTML (accumulating if rawHtml already exists)
return $this->appendHtml($html);
}

/**
* Inject Laravel Hyper runtime assets (replicates @hyper directive)
*
* Injects the CSRF token meta tag and Hyper's JavaScript module.
* This is required for Hyper's reactive functionality to work.
*
* Generates:
* - <meta name="csrf-token" content="{token}">
* - <script type="module" src="/vendor/hyper/js/hyper.js"></script>
*
* Example:
* ```php
* Html::head()
* ->hyper()
* ->vite(['resources/css/app.css', 'resources/js/app.js'])
* ->content(Html::title('My App'));
* ```
*/
public function hyper(): static
{
// Build the HTML exactly as the @hyper directive does
$csrfToken = csrf_token();
$scriptSrc = asset('vendor/hyper/js/hyper.js');

// Escape values for XSS protection
$escapedToken = htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8');
$escapedSrc = htmlspecialchars($scriptSrc, ENT_QUOTES, 'UTF-8');

$html = '<meta name="csrf-token" content="' . $escapedToken . '">' . "\n" .
'<script type="module" src="' . $escapedSrc . '"></script>';

// Inject as raw HTML (accumulating if rawHtml already exists)
return $this->appendHtml($html);
}

/**
* Inject Tailwind CSS from CDN (development/prototyping only)
*
* This is a convenience method for quick prototyping. For production,
* use Tailwind CLI or Vite integration instead.
*
* Version 4 is the default and uses the new @tailwindcss/browser package.
* Version 3 uses the legacy Play CDN.
*
* Example:
* ```php
* Html::head()
* ->hyper()
* ->tailwindCdn() // Uses v4 by default
* ->content(Html::title('Prototype'));
*
* // Or specify version explicitly:
* Html::head()->tailwindCdn('3') // Uses v3 legacy CDN
* ```
*
* @param string|Closure $version Tailwind major version ('3' or '4', defaults to '4')
*/
public function tailwindCdn(string|Closure $version = '4'): static
{
$version = $this->evaluate($version);

// Generate correct CDN URL based on version
$html = match ($version) {
'3' => '<script src="https://cdn.tailwindcss.com"></script>',
'4' => '<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>',
default => '<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>',
};

// Inject as raw HTML (accumulating if rawHtml already exists)
return $this->appendHtml($html);
}

/**
* Append HTML to existing rawHtml instead of replacing it
*
* This allows multiple asset injection methods to be chained
* without overwriting each other.
*
* @param string $html HTML to append
*/
protected function appendHtml(string $html): static
{
// If rawHtml already exists, append with newline separator
if (isset($this->rawHtml) && $this->rawHtml !== null) {
$this->rawHtml .= "\n" . $html;
} else {
$this->rawHtml = $html;
}

return $this;
}
}
Loading
Loading