Skip to content
Open
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
1 change: 1 addition & 0 deletions src/Blaze.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* @method static void debug()
* @method static bool isDebugging()
* @method static string compileForDebug(string $template)
* @method static void registerPrefix(string $prefix, string $namespace = '', string $slotPrefix = 'x-slot')
*
* @see \Livewire\Blaze\BlazeManager
*/
Expand Down
12 changes: 11 additions & 1 deletion src/BlazeManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Livewire\Blaze\Memoizer\Memoizer;
use Livewire\Blaze\Parser\Nodes\ComponentNode;
use Livewire\Blaze\Parser\Parser;
use Livewire\Blaze\Parser\PrefixRegistry;
use Livewire\Blaze\Parser\Tokenizer;
use Livewire\Blaze\Parser\Walker;
use Livewire\Blaze\Support\Directives;
Expand Down Expand Up @@ -48,9 +49,10 @@ public function __construct(
protected BladeCompiler $bladeCompiler,
protected BlazeRuntime $runtime,
protected BladeService $blade,
protected PrefixRegistry $registry,
) {
$this->renderer = new BladeRenderer($bladeCompiler, app('view'), $this->runtime, $this);
$this->parser = new Parser(new Tokenizer($this->blade), new AttributeParser($this->blade));
$this->parser = new Parser(new Tokenizer($this->blade, $this->registry), new AttributeParser($this->blade));
$this->walker = new Walker;
$this->compiler = new Compiler($config, $this->blade, $this);
$this->folder = new Folder($config, $this->blade, $this->renderer, $this);
Expand Down Expand Up @@ -397,6 +399,14 @@ public function optimize(): Config
return $this->config;
}

/**
* Register a custom component tag prefix (e.g. 'tk:' => Blade namespace 'tk::').
*/
public function registerPrefix(string $prefix, string $namespace = '', string $slotPrefix = 'x-slot'): void
{
$this->registry->register($prefix, $namespace, $slotPrefix);
}

/**
* Recursively check if any descendant component uses @aware.
*/
Expand Down
1 change: 1 addition & 0 deletions src/BlazeServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function register(): void
$this->app->singleton(Config::class);
$this->app->singleton(Debugger::class);
$this->app->singleton(Instrumenter::class);
$this->app->singleton(\Livewire\Blaze\Parser\PrefixRegistry::class);
$this->app->singleton(BlazeManager::class);

$this->app->singleton(\PhpParser\Parser::class, function () {
Expand Down
17 changes: 5 additions & 12 deletions src/Parser/Nodes/ComponentNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class ComponentNode extends Node
public function __construct(
public string $name,
public string $prefix,
public string $namespace = '',
public string $attributeString = '',
public array $children = [],
public bool $selfClosing = false,
Expand Down Expand Up @@ -49,7 +50,7 @@ public function setParentsAttributes(array $parentsAttributes): void
/** {@inheritdoc} */
public function render(): string
{
$name = $this->stripNamespaceFromName($this->name, $this->prefix);
$name = $this->stripNamespaceFromName($this->name);

$output = "<{$this->prefix}{$name}";

Expand All @@ -76,18 +77,10 @@ public function render(): string
/**
* Strip the namespace prefix from a component name for tag rendering.
*/
protected function stripNamespaceFromName(string $name, string $prefix): string
protected function stripNamespaceFromName(string $name): string
{
$prefixes = [
'flux:' => ['namespace' => 'flux::'],
'x:' => ['namespace' => ''],
'x-' => ['namespace' => ''],
];
if (isset($prefixes[$prefix])) {
$namespace = $prefixes[$prefix]['namespace'];
if (! empty($namespace) && str_starts_with($name, $namespace)) {
return substr($name, strlen($namespace));
}
if ($this->namespace !== '' && str_starts_with($name, $this->namespace)) {
return substr($name, strlen($this->namespace));
}

return $name;
Expand Down
2 changes: 2 additions & 0 deletions src/Parser/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ protected function handleTagOpen(TagOpenToken $token, ParseStack $stack): void
$node = new ComponentNode(
name: $token->namespace . $token->name,
prefix: $token->prefix,
namespace: $token->namespace,
attributeString: $attributeString,
children: [],
selfClosing: false,
Expand All @@ -82,6 +83,7 @@ protected function handleTagSelfClose(TagSelfCloseToken $token, ParseStack $stac
$node = new ComponentNode(
name: $token->namespace . $token->name,
prefix: $token->prefix,
namespace: $token->namespace,
attributeString: $attributeString,
children: [],
selfClosing: true,
Expand Down
35 changes: 35 additions & 0 deletions src/Parser/PrefixRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Livewire\Blaze\Parser;

/**
* Holds the set of recognized component tag prefixes (e.g. 'x-', 'flux:')
* and their associated Blade view-namespace and slot-tag prefix.
*/
class PrefixRegistry
{
protected array $prefixes = [
'flux:' => ['namespace' => 'flux::', 'slot' => 'x-slot'],
'x:' => ['namespace' => '', 'slot' => 'x-slot'],
'x-' => ['namespace' => '', 'slot' => 'x-slot'],
];

/**
* Register (or overwrite) a component tag prefix.
*/
public function register(string $prefix, string $namespace = '', string $slotPrefix = 'x-slot'): void
{
$this->prefixes[$prefix] = [
'namespace' => $namespace,
'slot' => $slotPrefix,
];
}

/**
* Get all registered prefixes, keyed by prefix string.
*/
public function all(): array
{
return $this->prefixes;
}
}
24 changes: 5 additions & 19 deletions src/Parser/Tokenizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,10 @@ class Tokenizer
{
public function __construct(
protected BladeService $blade,
protected PrefixRegistry $registry,
) {
}

protected array $prefixes = [
'flux:' => [
'namespace' => 'flux::',
'slot' => 'x-slot',
],
'x:' => [
'namespace' => '',
'slot' => 'x-slot',
],
'x-' => [
'namespace' => '',
'slot' => 'x-slot',
],
];

protected string $content = '';

protected int $position = 0;
Expand Down Expand Up @@ -495,7 +481,7 @@ protected function collectAttributes(): void
*/
protected function matchSlotOpen(): ?array
{
foreach ($this->prefixes as $prefix => $config) {
foreach ($this->registry->all() as $prefix => $config) {
$slotPrefix = $config['slot'];

if ($this->match('<\s*' . $slotPrefix . ':')) {
Expand All @@ -515,7 +501,7 @@ protected function matchSlotOpen(): ?array
*/
protected function matchSlotClose(): ?array
{
foreach ($this->prefixes as $prefix => $config) {
foreach ($this->registry->all() as $prefix => $config) {
$slotPrefix = $config['slot'];

if ($this->match('<\/\s*' . $slotPrefix)) {
Expand All @@ -531,7 +517,7 @@ protected function matchSlotClose(): ?array
*/
protected function matchComponentOpen(): ?array
{
foreach ($this->prefixes as $prefix => $config) {
foreach ($this->registry->all() as $prefix => $config) {
if ($this->match('<\s*' . $prefix)) {
return [
'prefix' => $prefix,
Expand All @@ -548,7 +534,7 @@ protected function matchComponentOpen(): ?array
*/
protected function matchComponentClose(): ?array
{
foreach ($this->prefixes as $prefix => $config) {
foreach ($this->registry->all() as $prefix => $config) {
if ($this->match('<\/\s*' . $prefix)) {
return [
'prefix' => $prefix,
Expand Down
15 changes: 15 additions & 0 deletions tests/BlazeManagerTest.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<?php

use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Blade;
use Livewire\Blaze\Blaze;
use Livewire\Blaze\BlazeManager;
use Livewire\Blaze\Support\Utils;

beforeEach(fn () => Artisan::call('view:clear'));

Expand Down Expand Up @@ -54,3 +56,16 @@

expect($manager->viewContainsExpiredFrontMatter($view))->toBeFalse();
});

test('registerPrefix makes a custom tag prefix compilable', function () {
Blade::anonymousComponentNamespace('components', 'tk');

Blaze::registerPrefix('tk:', namespace: 'tk::');

$path = fixture_path('views/components/card.blade.php');
$hash = Utils::hash($path);

$output = Blaze::compileForFolding('<tk:card class="mt-8" />');

expect($output)->toContain("_{$hash}(");
});
15 changes: 12 additions & 3 deletions tests/Parser/ParserTest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use Livewire\Blaze\Blaze;
use Livewire\Blaze\Parser\Parser;
use Livewire\Blaze\Parser\Nodes\ComponentNode;
use Livewire\Blaze\Parser\Nodes\DirectiveNode;
Expand Down Expand Up @@ -109,17 +110,25 @@
]);
});

test('parses component prefixes', function ($input, $prefix, $name) {
test('parses component prefixes', function ($input, $prefix, $name, $namespace = '') {
expect(app(Parser::class)->parse($input))->toEqual([
new ComponentNode($name, $prefix),
new ComponentNode($name, $prefix, $namespace),
]);
})->with([
'standard' => ['<x-button>', 'x-', 'button'],
'short' => ['<x:button>', 'x:', 'button'],
'namespaced' => ['<x-ui::button>', 'x-', 'ui::button'],
'flux' => ['<flux:button>', 'flux:', 'flux::button'],
'flux' => ['<flux:button>', 'flux:', 'flux::button', 'flux::'],
]);

test('parses custom registered prefixes', function () {
Blaze::registerPrefix('tk:', namespace: 'tk::');

expect(app(Parser::class)->parse('<tk:icon />'))->toEqual([
new ComponentNode('tk::icon', 'tk:', 'tk::', selfClosing: true),
]);
});

test('preprocesses attributes using Laravel pipeline', function ($input, $expected) {
$result = app(Parser::class)->parse($input);

Expand Down
38 changes: 38 additions & 0 deletions tests/Parser/PrefixRegistryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

use Livewire\Blaze\Parser\PrefixRegistry;

test('has default built-in prefixes', function () {
$registry = new PrefixRegistry;

expect($registry->all())->toBe([
'flux:' => ['namespace' => 'flux::', 'slot' => 'x-slot'],
'x:' => ['namespace' => '', 'slot' => 'x-slot'],
'x-' => ['namespace' => '', 'slot' => 'x-slot'],
]);
});

test('registers a new prefix', function () {
$registry = new PrefixRegistry;

$registry->register('tk:', 'tk::');

expect($registry->all())->toHaveKey('tk:', ['namespace' => 'tk::', 'slot' => 'x-slot']);
});

test('registers a new prefix with a custom slot prefix', function () {
$registry = new PrefixRegistry;

$registry->register('tk:', 'tk::', 'tk-slot');

expect($registry->all())->toHaveKey('tk:', ['namespace' => 'tk::', 'slot' => 'tk-slot']);
});

test('overwrites an existing prefix without duplicating it', function () {
$registry = new PrefixRegistry;

$registry->register('x-', 'custom::');

expect($registry->all())->toHaveCount(3)
->and($registry->all())->toHaveKey('x-', ['namespace' => 'custom::', 'slot' => 'x-slot']);
});
14 changes: 14 additions & 0 deletions tests/Parser/TokenizerTest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use Livewire\Blaze\Parser\PrefixRegistry;
use Livewire\Blaze\Parser\Tokenizer;
use Livewire\Blaze\Parser\Tokens\SlotCloseToken;
use Livewire\Blaze\Parser\Tokens\SlotOpenToken;
Expand All @@ -20,6 +21,19 @@
]);
});

test('tokenizes custom registered prefixes', function () {
app(PrefixRegistry::class)->register('tk:', 'tk::');

$input = '<tk:icon></tk:icon>';

$result = app(Tokenizer::class)->tokenize($input);

expect($result)->toEqual([
new TagOpenToken(name: 'icon', prefix: 'tk:', namespace: 'tk::'),
new TagCloseToken(name: 'icon', prefix: 'tk:', namespace: 'tk::'),
]);
});

test('tokenizes self-closing tags', function () {
$input = '<x-button type="button" />';

Expand Down
Loading