Skip to content
Draft
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
52 changes: 15 additions & 37 deletions src/Compiler/SlotCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,33 @@ public function __construct(
public function compile(string $slotsVariableName, array $children): string
{
$output = '';
$hasExplicitDefault = $this->hasExplicitDefaultSlot($children);

// Compile implicit default slot from loose content (non-SlotNode children)
if (! $this->hasExplicitDefaultSlot($children)) {
$output .= $this->compileSlot('slot', $this->renderLooseContent($children), '[]', $slotsVariableName) . "\n";
if (! $hasExplicitDefault) {
$output .= '<' . '?php ob_start(); ?>';
}

// Compile each named slot
foreach ($children as $child) {
if ($child instanceof SlotNode) {
$output .= $this->compileSlot(
$output .= ' ' . $this->compileSlot(
$this->resolveSlotName($child),
$this->renderChildren($child->children),
$this->compileSlotAttributes($child),
$slotsVariableName,
) . "\n";
);
} else {
$output .= $child->render();
}
}

if (! $hasExplicitDefault) {
$contentHandler = $this->manager->isFolding()
? '$__blaze->processPassthroughContent(\'trim\', trim(ob_get_clean()))'
: 'trim(ob_get_clean())';

$output .= '<' . '?php ' . $slotsVariableName . '[\'slot\'] = new \Illuminate\View\ComponentSlot(' . $contentHandler . ', []); ?>' . "\n";
}

return $output;
}

Expand All @@ -63,37 +72,6 @@ protected function hasExplicitDefaultSlot(array $children): bool
return false;
}

/**
* Render non-SlotNode children as the default slot content.
*
* @param array<Node> $children
*/
protected function renderLooseContent(array $children): string
{
$content = '';
$previousWasSlot = false;

foreach ($children as $child) {
if ($child instanceof SlotNode) {
$previousWasSlot = true;
continue;
}

$rendered = $child->render();

// Laravel's slot compilation consumes the newline after </x-slot> and adds a leading space.
// We match this by prepending a space and stripping any leading newline.
if ($previousWasSlot) {
$rendered = ' ' . preg_replace('/^\n/', '', $rendered);
}

$content .= $rendered;
$previousWasSlot = false;
}

return $content;
}

/**
* Compile a slot into ob_start/ob_get_clean code.
*/
Expand Down
59 changes: 59 additions & 0 deletions src/Folder/Folder.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ public function fold(Node $node): Node
return $component;
}

if ($this->hasConditionallyRenderedSlots($component)) {
return $component;
}

$this->checkProblematicPatterns($source);

try {
Expand Down Expand Up @@ -171,6 +175,61 @@ protected function isSafeToFold(ComponentSource $source, ComponentNode $node): b
return true;
}

/**
* Determine if any slot is wrapped in a runtime control structure (e.g. @if ... @endif).
*
* Folding extracts slots at compile time, so a slot inside an unclosed conditional
* or loop would always render. When we detect one, we skip the fold and let the
* regular compiler handle the component, which evaluates conditionals at runtime.
*/
protected function hasConditionallyRenderedSlots(ComponentNode $node): bool
{
$depth = 0;

foreach ($node->children as $child) {
if ($child instanceof SlotNode) {
if ($depth > 0) {
return true;
}

continue;
}

if ($child instanceof TextNode) {
$depth += $this->controlStructureDepthChange($child->content);
}
}

return false;
}

/**
* Calculate the net change in Blade control structure nesting within a chunk of text.
*/
protected function controlStructureDepthChange(string $content): int
{
$pattern = '/\B@(end)?(if|unless|isset|switch|foreach|forelse|for|while|empty)\b(\s*\()?/';

if (! preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) {
return 0;
}

$depth = 0;

foreach ($matches as $match) {
if ($match[1] === 'end') {
$depth--;
} elseif ($match[2] === 'empty' && ! isset($match[3])) {
// Bare @empty is the @forelse separator, not a conditional opener...
continue;
} else {
$depth++;
}
}

return $depth;
}

/**
* Check if a slot has any dynamically-bound attributes.
*/
Expand Down
46 changes: 46 additions & 0 deletions tests/ComparisonTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,49 @@
</x-card>
BLADE
));

test('conditional slots', fn () => compare(<<<'BLADE'
<x-card>
@if(false)
<x-slot name="header">
Header
</x-slot>
@endif
</x-card>
BLADE
));

test('foldable conditional slots', fn () => compare(<<<'BLADE'
<x-foldable.card>
@if(false)
<x-slot name="header">
Header
</x-slot>
@endif
</x-foldable.card>
BLADE
));

test('truthy conditional slots', fn () => compare(<<<'BLADE'
<x-card>
@if(true)
<x-slot name="header">
Header
</x-slot>
@endif
Body
</x-card>
BLADE
));

test('foldable truthy conditional slots', fn () => compare(<<<'BLADE'
<x-foldable.card>
@if(true)
<x-slot name="header">
Header
</x-slot>
@endif
Body
</x-foldable.card>
BLADE
));
4 changes: 3 additions & 1 deletion tests/Compiler/CompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@
'<?php $__attrs'. $hash .' = [\'class\' => \'mt-8\']; ?> ',
'<?php $__slots'. $hash .' = []; ?> ',
'<?php $__blaze->pushData($__attrs'. $hash .'); ?> ',
'<?php ob_start(); ?> Body <?php $__slots'. $hash .'[\'slot\'] = new \Illuminate\View\ComponentSlot(trim(ob_get_clean()), []); ?> ',
'<?php ob_start(); ?> ',
'<?php ob_start(); ?> Header <?php $__slots'. $hash .'[\'header\'] = new \Illuminate\View\ComponentSlot(trim(ob_get_clean()), [\'class\' => \'p-2\']); ?> ',
'Body ',
'<?php ob_start(); ?> Footer <?php $__slots'. $hash .'[\'footer\'] = new \Illuminate\View\ComponentSlot(trim(ob_get_clean()), [\'class\' => \'mt-4\']); ?> ',
'<?php $__slots'. $hash .'[\'slot\'] = new \Illuminate\View\ComponentSlot(trim(ob_get_clean()), []); ?> ',
'<?php $__blaze->pushSlots($__slots'. $hash .'); ?> ',
'<?php _'. $hash .'($__blaze, $__attrs'. $hash .', $__slots'. $hash .', [], [], $__this ?? (isset($this) ? $this : null)); ?> ',
'<?php if (! empty($__slotsStack'. $hash .')) { $__slots'. $hash .' = array_pop($__slotsStack'. $hash .'); } ?> ',
Expand Down
Loading