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
30 changes: 26 additions & 4 deletions packages/router/src/Exceptions/DevelopmentException.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,30 @@ private function enhanceStacktraceForViewCompilation(ViewCompilationFailed $exce
return $stacktrace;
}

$lines = explode("\n", $exception->content);
$errorLine = $previous->getLine();
$hasSourceLocation = $exception->sourcePath && $exception->sourceLine && Filesystem\is_file($exception->sourcePath);

$errorPath = $hasSourceLocation
? $exception->sourcePath
: $exception->path;

$errorLine = $exception->sourceLine ?? $previous->getLine();

$firstFrame = $stacktrace->frames[0] ?? null;

if (
$firstFrame instanceof Frame
&& $firstFrame->absoluteFile === $errorPath
&& $firstFrame->line === $errorLine
&& $firstFrame->class === TempestViewRenderer::class
&& $firstFrame->function === 'renderCompiled'
) {
return $stacktrace;
}

$lines = $hasSourceLocation
? explode(PHP_EOL, Filesystem\read_file($exception->sourcePath))
: explode(PHP_EOL, $exception->content);

$contextLines = 5;
$startLine = max(1, $errorLine - $contextLines);
$endLine = min(count($lines), $errorLine + $contextLines);
Expand All @@ -111,8 +133,8 @@ function: 'renderCompiled',
lines: $snippetLines,
highlightedLine: $errorLine,
),
absoluteFile: $exception->path,
relativeFile: to_relative_path(root_path(), $exception->path),
absoluteFile: $errorPath,
relativeFile: to_relative_path(root_path(), $errorPath),
arguments: [],
index: 1,
));
Expand Down
17 changes: 17 additions & 0 deletions packages/view/src/CompiledView.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Tempest\View;

final readonly class CompiledView
{
/**
* @param array<int, array{compiledStartLine: int, compiledEndLine: int, sourcePath: string, sourceStartLine: int}> $lineMap
*/
public function __construct(
public string $content,
public ?string $sourcePath,
public array $lineMap,
) {}
}
5 changes: 5 additions & 0 deletions packages/view/src/Element.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,9 @@ public function getChildren(): array;
* @return T|null
*/
public function unwrap(string $elementClass): ?Element;

/**
* @return string[] An array of import statements
*/
public function getImports(): array;
}
12 changes: 4 additions & 8 deletions packages/view/src/Elements/CollectionElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,17 @@ final class CollectionElement implements Element
{
use IsElement;

public function __construct(
private readonly array $elements,
) {}

/** @return \Tempest\View\Element[] */
public function getElements(): array
/** @param Element[] $elements */
public function __construct(array $elements)
{
return $this->elements;
$this->setChildren($elements);
}

public function compile(): string
{
$compiled = [];

foreach ($this->elements as $element) {
foreach ($this->getChildren() as $element) {
$compiled[] = $element->compile();
}

Expand Down
72 changes: 30 additions & 42 deletions packages/view/src/Elements/ElementFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Tempest\View\Parser\Token;
use Tempest\View\Parser\TokenType;
use Tempest\View\Slot;
use Tempest\View\ViewCache;
use Tempest\View\ViewConfig;

final class ElementFactory
Expand All @@ -22,6 +23,7 @@ final class ElementFactory
public function __construct(
private readonly ViewConfig $viewConfig,
private readonly Environment $environment,
private readonly ViewCache $viewCache,
) {}

public function setViewCompiler(TempestViewCompiler $compiler): self
Expand All @@ -40,15 +42,7 @@ public function withIsHtml(bool $isHtml): self
return $clone;
}

public function make(Token $token): ?Element
{
return $this->makeElement(
token: $token,
parent: null,
);
}

private function makeElement(Token $token, ?Element $parent): ?Element
public function make(Token $token, Element $parent): ?Element
Copy link
Member Author

Choose a reason for hiding this comment

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

ElementFactory now always requires a parent/root element

{
if (
$token->type === TokenType::OPEN_TAG_END
Expand All @@ -59,44 +53,46 @@ private function makeElement(Token $token, ?Element $parent): ?Element
return null;
}

$attributes = $token->htmlAttributes;

foreach ($token->phpAttributes as $index => $content) {
$attributes[] = new PhpAttribute((string) $index, $content);
}

if ($token->type === TokenType::CONTENT) {
$text = $token->compile();

if (trim($text) === '') {
return null;
}

return new TextElement(text: $text);
}

if ($token->type === TokenType::WHITESPACE) {
return new WhitespaceElement($token->content);
}

if (! $token->tag || $token->type === TokenType::COMMENT || $token->type === TokenType::PHP) {
return new RawElement(token: $token, tag: null, content: $token->compile());
}

$attributes = $token->htmlAttributes;

foreach ($token->phpAttributes as $index => $content) {
$attributes[] = new PhpAttribute((string) $index, $content);
}

if ($token->tag === 'code' || $token->tag === 'pre') {
return new RawElement(
$element = new TextElement(text: $text);
} elseif ($token->type === TokenType::WHITESPACE) {
$element = new WhitespaceElement($token->content);
} elseif ($token->type !== TokenType::PHP && (! $token->tag || $token->type === TokenType::COMMENT)) {
$element = new RawElement(
token: $token,
tag: null,
content: $token->compile(),
);
} elseif ($token->tag === 'code' || $token->tag === 'pre') {
$element = new RawElement(
token: $token,
tag: $token->tag,
content: $token->compileChildren(),
attributes: $attributes,
);
}

if ($viewComponentClass = $this->viewConfig->viewComponents[$token->tag] ?? null) {
} elseif ($token->type === TokenType::PHP) {
$element = new PhpElement(
token: $token,
content: $token->compile(),
);
} elseif ($viewComponentClass = $this->viewConfig->viewComponents[$token->tag] ?? null) {
$element = new ViewComponentElement(
token: $token,
environment: $this->environment,
compiler: $this->compiler,
viewCache: $this->viewCache,
viewComponent: $viewComponentClass,
attributes: $attributes,
);
Expand All @@ -120,23 +116,15 @@ private function makeElement(Token $token, ?Element $parent): ?Element
);
}

$children = [];
$element->setParent($parent);
Copy link
Member Author

Choose a reason for hiding this comment

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

Refactored parent/child setting because not all elements have children, but all have a parent.


foreach ($token->children as $child) {
$childElement = $this->clone()->makeElement(
$this->clone()->make(
token: $child,
parent: $parent,
parent: $element,
);

if ($childElement === null) {
continue;
}

$children[] = $childElement;
}

$element->setChildren($children);

return $element;
}

Expand Down
16 changes: 12 additions & 4 deletions packages/view/src/Elements/IsElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ public function setParent(?Element $parent): self
{
$this->parent = $parent;

$this->parent->setChildren([...$this->parent->getChildren(), $this]);
Copy link
Member Author

Choose a reason for hiding this comment

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

See before: not all elements have children but all have a parent


return $this;
}

Expand All @@ -141,10 +143,7 @@ public function setChildren(array $children): self
$previous = null;

foreach ($children as $child) {
$child
->setParent($this)
->setPrevious($previous);

$child->setPrevious($previous);
$previous = $child;
}

Expand All @@ -170,4 +169,13 @@ public function unwrap(string $elementClass): ?Element

return null;
}

public function getImports(): array
{
if ($this->parent) {
return $this->parent->getImports();
}

return [];
}
}
31 changes: 31 additions & 0 deletions packages/view/src/Elements/PhpElement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Tempest\View\Elements;

use Tempest\View\Element;
use Tempest\View\Parser\Token;
use Tempest\View\WithToken;

final class PhpElement implements Element, WithToken
{
use IsElement;

public function __construct(
public readonly Token $token,
private readonly string $content,
) {}

public function compile(): string
{
return $this->content;
}

public function getImports(): array
{
preg_match_all('/^\s*use .*;/m', $this->content, $matches);
Copy link
Member Author

Choose a reason for hiding this comment

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

Currently we're still using a regex, we could improve if needed once this refactor works


return $matches[0] ?? [];

Check failure on line 29 in packages/view/src/Elements/PhpElement.php

View workflow job for this annotation

GitHub Actions / Run static analysis: PHPStan

Offset 0 on array{list<string>} on left side of ?? always exists and is not nullable.
}
}
59 changes: 59 additions & 0 deletions packages/view/src/Elements/RootElement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);

namespace Tempest\View\Elements;

use Tempest\View\Element;

final class RootElement implements Element
{
use IsElement;

private array $inheritedImports = [];

public function compile(): string
{
$compiled = [];

foreach ($this->children as $element) {
$compiled[] = $element->compile();
}

return implode($compiled);
}

public function getImports(): array
{
$imports = [];

$this->mergeImports($imports, $this->inheritedImports);

foreach ($this->children as $child) {
Copy link
Member Author

Choose a reason for hiding this comment

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

We only want to resolve imports from PhpElements, not from sibling ViewComponent elements. That's why we check instanceof PhpElement here

if ($child instanceof PhpElement) {
$this->mergeImports($imports, $child->getImports());
}
}

return array_values($imports);
}

public function setInheritedImports(array $imports): self
{
$this->inheritedImports = $imports;

return $this;
}

private function mergeImports(array &$imports, array $candidates): void
{
foreach ($candidates as $import) {
$import = trim($import);

if ($import === '') {
continue;
}

$imports[$import] = $import;
}
}
}
Loading
Loading