Skip to content

Instantly share code, notes, and snippets.

@gcavanunez
Last active March 16, 2026 20:21
Show Gist options
  • Select an option

  • Save gcavanunez/16e302795aaabb26de0923e0b594065b to your computer and use it in GitHub Desktop.

Select an option

Save gcavanunez/16e302795aaabb26de0923e0b594065b to your computer and use it in GitHub Desktop.
LilBlaze - check it out on a full Laravel app here https://github.com/gcavanunez/lil-blaze
<?php
namespace App\LilBlaze;
class LilBlaze
{
private array $folded = [];
public function compile(string $template): string
{
// Protect @php/@verbatim blocks
[$template, $blocks] = $this->store($template);
// Split into tokens
$tokens = $this->tokenize($template);
// Build AST from tokens
$nodes = $this->parse($tokens);
// Apply optimization
$this->walk($nodes);
// Render AST back to string
$output = implode('', array_map(fn ($n) => $n->render(), $nodes));
// Stamp folded dependencies
$output = $this->stamp($output);
// Restore protected blocks
return $this->restore($output, $blocks);
}
private function store(string $template): array
{
$blocks = [];
$counter = 0;
$replace = function ($m) use (&$blocks, &$counter) {
$placeholder = "__LILBLAZE_BLOCK_{$counter}__";
$blocks[$placeholder] = $m[0];
$counter++;
return $placeholder;
};
$template = preg_replace_callback('/@verbatim(.*?)@endverbatim/s', $replace, $template);
// Same pattern for @php ... @endphp blocks
$template = preg_replace_callback('/@php(.*?)@endphp/s', $replace, $template);
return [$template, $blocks];
}
private function restore(string $template, array $blocks): string
{
foreach ($blocks as $placeholder => $original) {
$template = str_replace($placeholder, $original, $template);
}
return $template;
}
private function tokenize(string $template): array
{
preg_match_all(
'/<\/?x-[\w\-:.]+[^>]*?\/?>/i',
$template,
$rawMatches,
PREG_OFFSET_CAPTURE
);
$result = collect($rawMatches[0])
->reduce(function ($carry, $match) use ($template) {
[$raw, $pos] = $match;
if ($pos > $carry['offset']) {
$carry['tokens'][] = new Token('text', substr($template, $carry['offset'], $pos - $carry['offset']));
}
$carry['tokens'][] = $this->classifyToken($raw);
$carry['offset'] = $pos + strlen($raw);
return $carry;
}, ['tokens' => [], 'offset' => 0]);
if ($result['offset'] < strlen($template)) {
$result['tokens'][] = new Token('text', substr($template, $result['offset']));
}
return $result['tokens'];
}
private function classifyToken(string $raw): Token
{
if (str_starts_with($raw, '</')) {
preg_match('/<\/x-([\w\-:.]+)\s*>/', $raw, $matches);
return new Token('tag_close', $raw, $matches[1] ?? '');
}
if (str_ends_with(rtrim($raw, " \t>"), '/')) {
preg_match('/<x-([\w\-:.]+)\s*(.*?)\s*\/>/s', $raw, $matches);
return new Token('tag_self_close', $raw, $matches[1] ?? '', trim($matches[2] ?? ''));
}
preg_match('/<x-([\w\-:.]+)((?:\s[^>]*)?)>/s', $raw, $matches);
return new Token('tag_open', $raw, $matches[1] ?? '', trim($matches[2] ?? ''));
}
private function parse(array $tokens): array
{
$stack = [];
return collect($tokens)
->reduce(function ($root, Token $token) use (&$stack) {
if ($token->type === 'tag_open') {
$node = new ComponentNode($token->name, $token->attributes);
$this->addChild($root, $stack, $node);
$stack[] = $node;
return $root;
}
match ($token->type) {
'text' => $this->addChild($root, $stack, new TextNode($token->content)),
'tag_self_close' => $this->addChild($root, $stack, new ComponentNode(
$token->name, $token->attributes, [], true,
)),
'tag_close' => array_pop($stack),
default => null,
};
return $root;
}, []);
}
private function addChild(array &$root, array &$stack, TextNode|ComponentNode $node): void
{
if (empty($stack)) {
$root[] = $node;
} else {
$stack[count($stack) - 1]->children[] = $node;
}
}
private function walk(array &$nodes): void
{
foreach ($nodes as $i => $node) {
if (! ($node instanceof ComponentNode)) {
continue;
}
if (! $node->selfClosing) {
$this->walk($node->children);
}
$result = $this->fold($node) ?? $this->compileNode($node);
if ($result !== null) {
$nodes[$i] = $result;
}
}
}
private function fold(ComponentNode $node): ?TextNode
{
$path = resource_path("views/components/{$node->name}.blade.php");
if (! file_exists($path)) {
return null;
}
if (preg_match('/:\w+\s*=/', $node->attributes)) {
return null;
}
preg_match_all('/(\w+)="([^"]*)"/', $node->attributes, $matches, PREG_SET_ORDER);
$source = file_get_contents($path);
$source = preg_replace('/@\w+[^\n]*\n?/', '', $source);
// Replace attributes
foreach ($matches as $match) {
$source = str_replace('{{ $'.$match[1].' }}', $match[2], $source);
}
// Replace default slot from children
if (! $node->selfClosing) {
$slotContent = implode('', array_map(fn ($n) => $n->render(), $node->children));
$source = str_replace('{{ $slot }}', $slotContent, $source);
}
$source = preg_replace('/\{\{\s*\$\w+\s*\}\}/', '', $source);
$this->recordFolded($node->name, $path);
return new TextNode($source);
}
private function compileNode(ComponentNode $node): ?TextNode
{
$path = resource_path("views/components/{$node->name}.blade.php");
if (! file_exists($path)) {
return null;
}
$hash = substr(md5($path), 0, 12);
$funcName = "_lilblaze_{$hash}";
$this->wrapComponent($path, $funcName);
$attrs = $this->buildAttributeArray($node->attributes);
$requirePath = "storage_path('framework/views/{$funcName}.php')";
if ($node->selfClosing) {
return new TextNode(
"<?php require_once {$requirePath}; {$funcName}({$attrs}); ?>"
);
}
$slotContent = implode('', array_map(fn ($n) => $n->render(), $node->children));
return new TextNode(
"<?php ob_start(); ?>{$slotContent}<?php \$__slot = ob_get_clean(); "
."require_once {$requirePath}; "
."{$funcName}(array_merge({$attrs}, ['slot' => \$__slot])); ?>"
);
}
private function wrapComponent(string $sourcePath, string $funcName): void
{
$compiledPath = storage_path("framework/views/{$funcName}.php");
if (file_exists($compiledPath) && filemtime($compiledPath) >= filemtime($sourcePath)) {
return;
}
$source = file_get_contents($sourcePath);
$defaults = '[]';
if (preg_match('/@props\(\s*(\[.*?\])\s*\)/s', $source, $m)) {
$defaults = $m[1];
}
$source = preg_replace('/@\w+\(.*?\)\s*\n?/s', '', $source);
$source = preg_replace('/@\w+\s*\n?/', '', $source);
$source = preg_replace_callback(
'/\{\{\s*(.+?)\s*\}\}/s',
fn ($m) => '<?php echo e('.$m[1].'); '.'?>',
$source
);
$php = implode(PHP_EOL, [
"<?php if (!function_exists('{$funcName}')):",
"function {$funcName}(array \$__data = []) {",
" \$__data = array_merge({$defaults}, \$__data);",
' extract($__data);',
' ob_start();',
"?>{$source}<?php",
' echo ltrim(ob_get_clean());',
'}',
'endif; ?>',
'',
]);
file_put_contents($compiledPath, $php);
}
private function buildAttributeArray(string $attributes): string
{
if (empty(trim($attributes))) {
return '[]';
}
preg_match_all('/:?(\w+)="([^"]*)"/', $attributes, $matches, PREG_SET_ORDER);
if (empty($matches)) {
return '[]';
}
$pairs = array_map(function ($matching) {
$isDynamic = str_starts_with($matching[0], ':');
return $isDynamic
? "'".addslashes($matching[1])."' => ".$matching[2]
: "'".addslashes($matching[1])."' => '".addslashes($matching[2])."'";
}, $matches);
return '['.implode(', ', $pairs).']';
}
private function recordFolded(string $name, string $path): void
{
$this->folded[] = [
'name' => $name,
'path' => $path,
'mtime' => filemtime($path),
];
}
private function stamp(string $compiled): string
{
$header = '';
foreach ($this->folded as $dep) {
$header .= "<?php /* [LilBlazeFolded]:{$dep['name']}:{$dep['path']}:{$dep['mtime']} */ ?>".PHP_EOL;
}
$this->folded = [];
return $header.$compiled;
}
}
<?php
namespace App\LilBlaze;
class ComponentNode
{
public array $children;
public function __construct(
public readonly string $name,
public readonly string $attributes = '',
array $children = [],
public readonly bool $selfClosing = false,
) {
$this->children = $children;
}
public function render(): string
{
$attrs = $this->attributes ? " {$this->attributes}" : '';
if ($this->selfClosing) {
return "<x-{$this->name}{$attrs} />";
}
$children = implode('', array_map(fn ($n) => $n->render(), $this->children));
return "<x-{$this->name}{$attrs}>{$children}</x-{$this->name}>";
}
}
<?php
namespace App\LilBlaze;
class TextNode
{
public function __construct(
public readonly string $content,
) {}
public function render(): string
{
return $this->content;
}
}
<?php
namespace App\LilBlaze;
class Token
{
public function __construct(
// 'text', 'tag_open', 'tag_close', 'tag_self_close'
public readonly string $type,
// raw matched string
public readonly string $content,
// component name
public readonly string $name = '',
// attribute string (the 'type="error" class="bold"' in <x-alert type="error" class="bold">)
public readonly string $attributes = '',
) {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment