Skip to content
Closed
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
26 changes: 0 additions & 26 deletions src/BladeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,32 +210,6 @@ public static function preStoreUncompiledBlocks(string $input): string
return $storeVerbatimBlocks->invoke($compiler, $input);
}

/**
* Store only @verbatim blocks as raw block placeholders.
*/
public static function storeVerbatimBlocks(string $input): string
{
$compiler = app('blade.compiler');

$reflection = new \ReflectionClass($compiler);
$method = $reflection->getMethod('storeVerbatimBlocks');

return $method->invoke($compiler, $input);
}

/**
* Restore raw block placeholders to their original content.
*/
public static function restoreRawBlocks(string $input): string
{
$compiler = app('blade.compiler');

$reflection = new \ReflectionClass($compiler);
$method = $reflection->getMethod('restoreRawContent');

return $method->invoke($compiler, $input);
}

/**
* Invoke the Blade compiler's compileComments via reflection.
*/
Expand Down
11 changes: 6 additions & 5 deletions src/Compiler/Wrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,18 @@ public function wrap(string $compiled, string $path, ?string $source = null): st
$sourceUsesThis = str_contains($source, '$this') || str_contains($compiled, '@entangle') || str_contains($compiled, '@script');

$compiled = BladeService::compileUseStatements($compiled);
$compiled = BladeService::restoreRawBlocks($compiled);
$compiled = BladeService::storeVerbatimBlocks($compiled);

// Keep @php/@verbatim content as @__raw_block_N__@ placeholders so
// downstream precompilers can't match/modify patterns inside them.
// Laravel's restoreRawContent() handles restoration at the end of
// compileString().

$imports = '';

$compiled = $this->useExtractor->extract($compiled, function ($statement) use (&$imports) {
$imports .= $statement . "\n";
});

$compiled = BladeService::preStoreUncompiledBlocks($compiled);

$output = '';

$output .= '<'.'?php' . "\n";
Expand Down
109 changes: 59 additions & 50 deletions src/Parser/Tokenizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,45 +52,50 @@ class Tokenizer
/**
* Tokenize a Blade template into an array of tokens.
*/
public function tokenize(string $content): array
public function tokenize(string $template): array
{
$this->resetTokenizer($content);
$this->tokens = [];
$this->buffer = '';
$this->currentToken = null;
$this->tagStack = [];
$this->currentPrefix = '';
$this->currentSlotPrefix = '';

$state = TokenizerState::TEXT;

while (!$this->isAtEnd()) {
$state = match($state) {
TokenizerState::TEXT => $this->handleTextState(),
TokenizerState::TAG_OPEN => $this->handleTagOpenState(),
TokenizerState::TAG_CLOSE => $this->handleTagCloseState(),
TokenizerState::SLOT_OPEN => $this->handleSlotOpenState(),
TokenizerState::SLOT_CLOSE => $this->handleSlotCloseState(),
TokenizerState::SHORT_SLOT => $this->handleShortSlotState(),
default => throw new \RuntimeException("Unknown state: $state"),
};
foreach (token_get_all($template) as $token) {
if (is_array($token) && $token[0] === T_INLINE_HTML) {
$this->position = 0;
$this->content = $token[1];
$this->length = strlen($token[1]);

while (!$this->isAtEnd()) {
$state = match ($state) {
TokenizerState::TEXT => $this->handleTextState(),
TokenizerState::TAG_OPEN => $this->handleTagOpenState(),
TokenizerState::TAG_CLOSE => $this->handleTagCloseState(),
TokenizerState::SLOT_OPEN => $this->handleSlotOpenState(),
TokenizerState::SLOT_CLOSE => $this->handleSlotCloseState(),
TokenizerState::SHORT_SLOT => $this->handleShortSlotState(),
default => throw new \RuntimeException("Unknown state: $state"),
};
}
} else {
// If we hit a non-HTML code inside a tag token, we should discard that token
// and consider everything buffered so far as plain text.
$this->currentToken = null;

$state = TokenizerState::TEXT;

$this->buffer .= is_array($token) ? $token[1] : $token;
}
}

$this->flushBuffer();

return $this->tokens;
}

/**
* Reset all tokenizer state for a new tokenization pass.
*/
protected function resetTokenizer(string $content): void
{
$this->content = $content;
$this->position = 0;
$this->length = strlen($content);
$this->tokens = [];
$this->buffer = '';
$this->currentToken = null;
$this->tagStack = [];
$this->currentPrefix = '';
$this->currentSlotPrefix = '';
}

/**
* Process text state, detecting component/slot tag boundaries.
*/
Expand All @@ -99,9 +104,9 @@ protected function handleTextState(): TokenizerState
$char = $this->current();

if ($char === '<') {
if ($slotInfo = $this->matchSlotOpen()) {
$this->flushBuffer();
$this->flushBuffer();

if ($slotInfo = $this->matchSlotOpen()) {
$this->currentSlotPrefix = $slotInfo['prefix'];

if ($slotInfo['isShort']) {
Expand All @@ -116,8 +121,6 @@ protected function handleTextState(): TokenizerState
}

if ($slotInfo = $this->matchSlotClose()) {
$this->flushBuffer();

$this->currentToken = new SlotCloseToken();

$this->currentSlotPrefix = $slotInfo['prefix'];
Expand All @@ -130,8 +133,6 @@ protected function handleTextState(): TokenizerState
}

if ($prefixInfo = $this->matchComponentOpen()) {
$this->flushBuffer();

$this->currentPrefix = $prefixInfo['prefix'];

$this->currentToken = new TagOpenToken(
Expand All @@ -144,8 +145,6 @@ protected function handleTextState(): TokenizerState
}

if ($this->peek(1) === '/' && ($prefixInfo = $this->matchComponentClose())) {
$this->flushBuffer();

$this->currentPrefix = $prefixInfo['prefix'];

$this->currentToken = new TagCloseToken(
Expand All @@ -158,8 +157,6 @@ protected function handleTextState(): TokenizerState
}
}

$this->buffer .= $char;

$this->advance();

return TokenizerState::TEXT;
Expand Down Expand Up @@ -189,18 +186,18 @@ protected function handleTagOpenState(): TokenizerState

array_pop($this->tagStack);

$this->tokens[] = $this->currentToken;

$this->advance(2);

$this->emitToken();

return TokenizerState::TEXT;
}

if ($this->current() === '>') {
$this->tokens[] = $this->currentToken;

$this->advance();

$this->emitToken();

return TokenizerState::TEXT;
}
}
Expand All @@ -224,10 +221,10 @@ protected function handleTagCloseState(): TokenizerState
}

if ($this->current() === '>') {
$this->tokens[] = $this->currentToken;

$this->advance();

$this->emitToken();

return TokenizerState::TEXT;
}

Expand Down Expand Up @@ -257,10 +254,10 @@ protected function handleSlotOpenState(): TokenizerState
}

if ($this->current() === '>') {
$this->tokens[] = $this->currentToken;

$this->advance();

$this->emitToken();

return TokenizerState::TEXT;
}

Expand All @@ -281,10 +278,10 @@ protected function handleSlotCloseState(): TokenizerState
}

if ($this->current() === '>') {
$this->tokens[] = $this->currentToken;

$this->advance();

$this->emitToken();

return TokenizerState::TEXT;
}

Expand All @@ -306,10 +303,10 @@ protected function handleShortSlotState(): TokenizerState
$this->collectAttributes();

if ($this->current() === '>') {
$this->tokens[] = $this->currentToken;

$this->advance();

$this->emitToken();

return TokenizerState::TEXT;
}
}
Expand Down Expand Up @@ -525,6 +522,8 @@ protected function remaining(): string
*/
protected function advance(int $count = 1): void
{
$this->buffer .= substr($this->content, $this->position, $count);

$this->position += $count;
}

Expand All @@ -536,6 +535,16 @@ protected function isAtEnd(): bool
return $this->position >= $this->length;
}

/**
* Emit the current token and discard the raw buffer.
*/
protected function emitToken(): void
{
$this->tokens[] = $this->currentToken;

$this->buffer = '';
}

/**
* Emit any accumulated text buffer as a TextToken.
*/
Expand Down
7 changes: 5 additions & 2 deletions tests/Compiler/WrapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,12 @@
});

test('hoists use statements to top of output', function ($statement) {
$source = "{$statement}\n<div></div>";
// Replace raw @php blocks for placeholders. This normally happens in BlazeManager before the template gets to the Wrapper
$source = BladeService::preStoreUncompiledBlocks($statement);

expect(app(Wrapper::class)->wrap($source, '', $source))->toStartWith("<?php\nuse");
expect(app(Wrapper::class)->wrap($source, '', $source))->toStartWith("<?php\nuse \App\Models\User");
})->with([
['@use(\'App\Models\User\')'],
['@php use \App\Models\User; @endphp'],
['<?php use \App\Models\User; ?>'],
]);
88 changes: 88 additions & 0 deletions tests/IntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,94 @@
expect(Blade::render($input))->toBe('');
});

test('component inside PHP line comment is not compiled by Blaze', function () {
$compiled = compile('php-comment-parent.blade.php');

expect($compiled)->not->toContain('ensureCompiled');
expect($compiled)->not->toContain('$__blaze->pushData');
expect($compiled)->toContain('visible');
});

test('component inside Blade comment is correctly ignored', function () {
$output = blade(
view: <<<'BLADE'
{{-- <x-mycomp /> --}}
<div>visible</div>
BLADE,
components: [
'mycomp' => <<<'BLADE'
@blaze
<span>SHOULD NOT APPEAR</span>
BLADE,
],
);

expect($output)->toContain('visible');
expect($output)->not->toContain('SHOULD NOT APPEAR');
});

test('component inside @php block is correctly ignored', function () {
$output = blade(
view: <<<'BLADE'
@php // <x-mycomp /> @endphp
<div>visible</div>
BLADE,
components: [
'mycomp' => <<<'BLADE'
@blaze
<span>SHOULD NOT APPEAR</span>
BLADE,
],
);

expect($output)->toContain('visible');
expect($output)->not->toContain('SHOULD NOT APPEAR');
});

test('component inside PHP block comment renders correctly end-to-end', function () {
$output = blade(
view: <<<'BLADE'
<?php /* <x-mycomp /> */ ?>
<div>visible</div>
BLADE,
components: [
'mycomp' => <<<'BLADE'
@blaze
<span>SHOULD NOT APPEAR</span>
BLADE,
],
);

expect($output)->toContain('visible');
expect($output)->not->toContain('SHOULD NOT APPEAR');
});

test('protects php blocks from precompilers that inject php tags', function () {
// Simulate Livewire's SupportMorphAwareBladeCompilation precompiler which
// wraps morph markers in PHP open/close tags. The injected close-tag
// terminates PHP mode even inside // comments, causing @if to be
// compiled as a bare directive.
app('blade.compiler')->precompiler(function ($template) {
$open = '<' . '?php';
$close = '?' . '>';
$prefix = $open . ' if(true): ' . $close . '<!--[if BLOCK]><![endif]-->' . $open . ' endif; ' . $close;

return preg_replace(
'/(?<!\w)@if(?!\w)/',
$prefix . '@if',
$template
);
});

$compiled = compile('php-comment-with-directive.blade.php');

// If Blaze fails to protect the @php block content from precompilers,
// the close-tag inside the injected prefix ends the // comment's PHP
// context, leaving bare @if in inline HTML where compileStatements()
// turns it into invalid PHP (if with no condition).
expect($compiled)->not->toContain('<' . '?php if: ?' . '>');
});

// TODO: Install PHPStan, which probably would have caught this.
test('supports php engine', function () {
// Make sure our hooks do not break views
Expand Down
Loading
Loading