diff --git a/src/Blaze.php b/src/Blaze.php
index f373d4f..bf99044 100644
--- a/src/Blaze.php
+++ b/src/Blaze.php
@@ -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
*/
diff --git a/src/BlazeManager.php b/src/BlazeManager.php
index 2ed5db9..e7c2401 100644
--- a/src/BlazeManager.php
+++ b/src/BlazeManager.php
@@ -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;
@@ -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);
@@ -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.
*/
diff --git a/src/BlazeServiceProvider.php b/src/BlazeServiceProvider.php
index 5f58647..b3f6c5c 100644
--- a/src/BlazeServiceProvider.php
+++ b/src/BlazeServiceProvider.php
@@ -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 () {
diff --git a/src/Parser/Nodes/ComponentNode.php b/src/Parser/Nodes/ComponentNode.php
index bd82485..5ef78ea 100644
--- a/src/Parser/Nodes/ComponentNode.php
+++ b/src/Parser/Nodes/ComponentNode.php
@@ -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,
@@ -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}";
@@ -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;
diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php
index 8f3751c..1f0dc7d 100644
--- a/src/Parser/Parser.php
+++ b/src/Parser/Parser.php
@@ -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,
@@ -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,
diff --git a/src/Parser/PrefixRegistry.php b/src/Parser/PrefixRegistry.php
new file mode 100644
index 0000000..4b5631f
--- /dev/null
+++ b/src/Parser/PrefixRegistry.php
@@ -0,0 +1,35 @@
+ ['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;
+ }
+}
diff --git a/src/Parser/Tokenizer.php b/src/Parser/Tokenizer.php
index f96efbf..a21ee61 100644
--- a/src/Parser/Tokenizer.php
+++ b/src/Parser/Tokenizer.php
@@ -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;
@@ -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 . ':')) {
@@ -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)) {
@@ -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,
@@ -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,
diff --git a/tests/BlazeManagerTest.php b/tests/BlazeManagerTest.php
index 52a5947..2715780 100644
--- a/tests/BlazeManagerTest.php
+++ b/tests/BlazeManagerTest.php
@@ -1,8 +1,10 @@
Artisan::call('view:clear'));
@@ -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('');
+
+ expect($output)->toContain("_{$hash}(");
+});
diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php
index 4c214ee..371e73d 100644
--- a/tests/Parser/ParserTest.php
+++ b/tests/Parser/ParserTest.php
@@ -1,5 +1,6 @@
parse($input))->toEqual([
- new ComponentNode($name, $prefix),
+ new ComponentNode($name, $prefix, $namespace),
]);
})->with([
'standard' => ['', 'x-', 'button'],
'short' => ['', 'x:', 'button'],
'namespaced' => ['', 'x-', 'ui::button'],
- 'flux' => ['', 'flux:', 'flux::button'],
+ 'flux' => ['', 'flux:', 'flux::button', 'flux::'],
]);
+test('parses custom registered prefixes', function () {
+ Blaze::registerPrefix('tk:', namespace: 'tk::');
+
+ expect(app(Parser::class)->parse(''))->toEqual([
+ new ComponentNode('tk::icon', 'tk:', 'tk::', selfClosing: true),
+ ]);
+});
+
test('preprocesses attributes using Laravel pipeline', function ($input, $expected) {
$result = app(Parser::class)->parse($input);
diff --git a/tests/Parser/PrefixRegistryTest.php b/tests/Parser/PrefixRegistryTest.php
new file mode 100644
index 0000000..dc241c9
--- /dev/null
+++ b/tests/Parser/PrefixRegistryTest.php
@@ -0,0 +1,38 @@
+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']);
+});
diff --git a/tests/Parser/TokenizerTest.php b/tests/Parser/TokenizerTest.php
index f47b8d3..9fe6a4a 100644
--- a/tests/Parser/TokenizerTest.php
+++ b/tests/Parser/TokenizerTest.php
@@ -1,5 +1,6 @@
register('tk:', 'tk::');
+
+ $input = '';
+
+ $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 = '';