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
21 changes: 12 additions & 9 deletions packages/view/src/Parser/TempestViewLexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

final class TempestViewLexer
{
private const string WHITESPACE = PHP_EOL . "\n\t\f ";
private const string WHITESPACE = "\r\n\t\f ";

private int $position = 0;

Expand Down Expand Up @@ -35,7 +35,7 @@ public function lex(): TokenCollection
$tokens = [...$tokens, ...$this->lexCharacterData()];
} elseif ($this->comesNext('<')) {
$tokens = [...$tokens, ...$this->lexTag()];
} elseif ($this->comesNext(' ') || $this->comesNext(PHP_EOL)) {
} elseif ($this->isWhitespace($this->current)) {
$tokens[] = $this->lexWhitespace();
} else {
$tokens[] = $this->lexContent();
Expand All @@ -61,6 +61,15 @@ private function seek(int $length = 1, int $offset = 0): ?string
return $seek;
}

private function isWhitespace(?string $value): bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the use of a function, but this will actually have a pretty significant performance impact. Anything done within the lexer should will be called thousands and thousands of times, and introducing a single function call actually has a measurable performance impact. Since the actual change is minimal, I'll close this PR and add it myself.

Let it be known though that I really appreciate the effort debugging this 🙏

{
if ($value === null) {
return false;
}

return str_contains(self::WHITESPACE, $value);
}

private function seekIgnoringWhitespace(int $length = 1): ?string
{
$offset = strspn($this->html, self::WHITESPACE, $this->position);
Expand Down Expand Up @@ -220,13 +229,7 @@ private function lexWhitespace(): Token
{
$buffer = '';

while ($this->current !== null) {
$seek = $this->seek();

if ($seek !== ' ' && $seek !== PHP_EOL) {
break;
}

while ($this->isWhitespace($this->seek())) {
$buffer .= $this->consume();
}

Expand Down
17 changes: 17 additions & 0 deletions packages/view/tests/TempestViewLexerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tempest\View\Tests;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;
use Tempest\View\Parser\TempestViewLexer;
Expand Down Expand Up @@ -316,6 +317,22 @@ public function test_attribute_with_new_line(): void
);
}

#[Test]
public function lexer_handles_crlf_attribute_boundaries(): void
{
$tokens = new TempestViewLexer("<div hidden\r\n></div>")->lex();

$this->assertTokens(
expected: [
new Token('<div', TokenType::OPEN_TAG_START),
new Token(' hidden', TokenType::ATTRIBUTE_NAME),
new Token("\r\n>", TokenType::OPEN_TAG_END),
new Token('</div>', TokenType::CLOSING_TAG),
],
actual: $tokens,
);
}

public function test_unclosed_php_tag(): void
{
$tokens = new TempestViewLexer('<?php echo "hi";')->lex();
Expand Down
Loading