Skip to content

Commit

Permalink
added support for variable tags <{$foo}>
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Aug 13, 2023
1 parent db022f7 commit fbe268d
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 9 deletions.
4 changes: 2 additions & 2 deletions src/Latte/Compiler/TemplateLexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,9 @@ private function stateHtmlText(): \Generator
yield from $this->match('~(?J)
(?<Text>.+?)??
(
(?<Indentation>(?<=\n|^)[ \t]+)?(?<Html_TagOpen><)(?<Slash>/)?(?<Html_Name>' . self::ReTagName . ')| # <tag </tag
(?<Indentation>(?<=\n|^)[ \t]+)?(?<Html_TagOpen><)(?<Slash>/)?(?=[a-z]|' . $this->openDelimiter . ')| # < </ tag
(?<Html_CommentOpen><!--(?!>|->))| # <!-- comment
(?<Html_BogusOpen><[/?!])| # <!doctype <?xml or error
(?<Html_BogusOpen><[?!])| # <!doctype <?xml or error
(?<Indentation>(?<=\n|^)[ \t]+)?(?<Latte_TagOpen>' . $this->openDelimiter . '(?!\*))| # {tag
(?<Indentation>(?<=\n|^)[ \t]+)?(?<Latte_CommentOpen>' . $this->openDelimiter . '\*)| # {* comment
$
Expand Down
38 changes: 31 additions & 7 deletions src/Latte/Compiler/TemplateParserHtml.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,13 @@ private function parseTag(): ?Node
return $this->parseElement();
}

$name = $stream->peek(2)->text ?? '';
$name = $stream->peek(2);
if ($this->element
&& $this->parser->peekTag() === $this->element->data->tag
&& (strcasecmp($name, $this->element->name) === 0
|| !in_array($name, $this->element->data->unclosedTags ?? [], true))
&& ($name?->is(Token::Latte_TagOpen)
|| ($name?->is(Token::Html_Name)
&& (strcasecmp($name->text, $this->element->name) === 0
|| !in_array($name->text, $this->element->data->unclosedTags ?? [], true))))
) {
return null; // go to parseElement() one level up
}
Expand Down Expand Up @@ -122,17 +124,20 @@ private function parseElement(): Node

if ($stream->is(Token::Html_TagOpen) && $stream->peek(1)->is(Token::Slash)
&& ($endName = $stream->peek(2))
&& strcasecmp($elem->name, $endName->text) === 0
&& (($endName->is(Token::Html_Name) && (strcasecmp($elem->name, $endName->text) === 0))
|| ($endName->is(Token::Latte_TagOpen) && $elem->variableName))
) {
$elem->content = $content;
$elem->content->append($this->extractIndentation());
$this->parseElementEndTag();

} elseif ($outerNodes || $innerNodes || $tagNodes
|| $elem->variableName
|| ($this->parser->getContentType() === ContentType::Html && in_array(strtolower($elem->name), ['script', 'style'], true))
) {
$elemName = $elem->variableName ? '{...}' : $elem->name;
$stream->throwUnexpectedException(
addendum: ", expecting </{$elem->name}> for element started " . $elem->position->toWords(),
addendum: ", expecting </{$elemName}> for element started " . $elem->position->toWords(),
excerpt: isset($endName) ? "/{$endName->text}>" : '',
);
} else { // element collapsed to tags
Expand Down Expand Up @@ -172,10 +177,21 @@ private function parseStartTag(&$elem = null): Html\ElementNode
$stream = $this->parser->getStream();
$openToken = $stream->consume(Token::Html_TagOpen);
$this->parser->getLexer()->setState(TemplateLexer::StateHtmlTag);
$name = $stream->consume(Token::Html_Name)->text;

if ($stream->is(Token::Latte_TagOpen)) {
$name = '';
$variableName = $this->parser->parseLatteStatement();
if (!$variableName instanceof Latte\Essential\Nodes\PrintNode) {
throw new CompileException('Only expression can be used as a HTML tag name.', $variableName->position);
}
} else {
$name = $stream->consume(Token::Html_Name)->text;
$variableName = null;
}
if (!$stream->is(Token::Whitespace, Token::Slash, Token::Html_TagClose)) {
throw $stream->throwUnexpectedException();
}

$this->parser->lastIndentation = null;
$this->parser->inHead = false;
$elem = new Html\ElementNode(
Expand All @@ -186,6 +202,7 @@ private function parseStartTag(&$elem = null): Html\ElementNode
);
$elem->attributes = $this->parser->parseFragment([$this, 'inTagResolve']);
$elem->selfClosing = (bool) $stream->tryConsume(Token::Slash);
$elem->variableName = $variableName?->expression;
$stream->consume(Token::Html_TagClose);
$state = !$elem->selfClosing && $this->parser->getContentType() === ContentType::Html && in_array(strtolower($name), ['script', 'style'], true)
? TemplateLexer::StateHtmlRawText
Expand Down Expand Up @@ -224,7 +241,14 @@ private function parseElementEndTag(): void
$stream->consume(Token::Html_TagOpen);
$this->parser->getLexer()->setState(TemplateLexer::StateHtmlTag);
$stream->consume(Token::Slash);
$stream->consume(Token::Html_Name);
if ($stream->is(Token::Latte_TagOpen)) {
$node = $this->parser->parseLatteStatement();
if (!$node instanceof Latte\Essential\Nodes\PrintNode) {
throw new CompileException('Only expression can be used as a HTML tag name.', $node->position);
}
} else {
$stream->consume(Token::Html_Name);
}
$stream->tryConsume(Token::Whitespace);
$stream->consume(Token::Html_TagClose);
$this->parser->getLexer()->setState(TemplateLexer::StateHtmlText);
Expand Down
36 changes: 36 additions & 0 deletions tests/common/Compiler.errors.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,42 @@ Assert::exception(
'Attribute n:href must not appear inside {tags} (on line 1 at column 8)',
);

Assert::exception(
fn() => $latte->compile('<{if 1}{/if}>'),
Latte\CompileException::class,
'Only expression can be used as a HTML tag name (on line 1 at column 2)',
);

Assert::exception(
fn() => $latte->compile('<{$foo}>'),
Latte\CompileException::class,
'Unexpected end, expecting </{...}> for element started on line 1 at column 1 (on line 1 at column 9)',
);

Assert::exception(
fn() => $latte->compile('<{$foo}>...</{if 1}{/if}>'),
Latte\CompileException::class,
'Only expression can be used as a HTML tag name (on line 1 at column 14)',
);

Assert::exception(
fn() => $latte->compile('<a>...</{$foo}>'),
Latte\CompileException::class,
"Unexpected '{', expecting HTML name (on line 1 at column 9)",
);

Assert::exception(
fn() => $latte->compile('<{$foo}></span></{$foo}>'),
Latte\CompileException::class,
"Unexpected '</span>', expecting </{...}> for element started on line 1 at column 1 (on line 1 at column 9)",
);

Assert::exception(
fn() => $latte->compile('</{$foo}>'), // bogus tag
Latte\CompileException::class,
"Unexpected '{', expecting HTML name (on line 1 at column 3)",
);

Assert::exception(
fn() => $latte->compile('<span{$foo}>'),
Latte\CompileException::class,
Expand Down
36 changes: 36 additions & 0 deletions tests/common/Compiler.variableTags.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/**
* Test: variable tag names
*/

declare(strict_types=1);

use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


$latte = new Latte\Engine;
$latte->setLoader(new Latte\Loaders\StringLoader);

$template = <<<'EOD'
{var $tag = 'foo'}
<{$tag}>...</{$tag}>
<{$tag}><span></{$tag}>
<{$tag}>...</{$foo}> {* not checked *}
<{=a}><{=b}>...</{=x}></{=y}> {* not checked *}
EOD;

Assert::matchFile(
__DIR__ . '/expected/Compiler.variable.tags.php',
$latte->compile($template),
);
Assert::matchFile(
__DIR__ . '/expected/Compiler.variable.tags.html',
$latte->renderToString($template, ['x' => '\' & "']),
);
8 changes: 8 additions & 0 deletions tests/common/expected/Compiler.variable.tags.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

<foo>...</foo>

<foo><span></foo>

<foo>...</foo>

<a><b>...</b></a>
52 changes: 52 additions & 0 deletions tests/common/expected/Compiler.variable.tags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php
%A%
final class Template%a% extends Latte\Runtime\Template
{

public function main(array $ʟ_args): void
{
%A%
echo "\n";
$ʟ_tag[0] = '';
echo '<';
echo $ʟ_tmp = LR\Filters::safeTag($tag);
$ʟ_tag[0] = '</' . $ʟ_tmp . '>' . $ʟ_tag[0];
echo '>...';
echo $ʟ_tag[0];
echo '
';
$ʟ_tag[1] = '';
echo '<';
echo $ʟ_tmp = LR\Filters::safeTag($tag);
$ʟ_tag[1] = '</' . $ʟ_tmp . '>' . $ʟ_tag[1];
echo '><span>';
echo $ʟ_tag[1];
echo '
';
$ʟ_tag[2] = '';
echo '<';
echo $ʟ_tmp = LR\Filters::safeTag($tag);
$ʟ_tag[2] = '</' . $ʟ_tmp . '>' . $ʟ_tag[2];
echo '>...';
echo $ʟ_tag[2];
echo '
';
$ʟ_tag[3] = '';
echo '<';
echo $ʟ_tmp = LR\Filters::safeTag('a');
$ʟ_tag[3] = '</' . $ʟ_tmp . '>' . $ʟ_tag[3];
echo '>';
$ʟ_tag[4] = '';
echo '<';
echo $ʟ_tmp = LR\Filters::safeTag('b');
$ʟ_tag[4] = '</' . $ʟ_tmp . '>' . $ʟ_tag[4];
echo '>...';
echo $ʟ_tag[4];
echo $ʟ_tag[3];
echo ' ';
}
%A%
}

0 comments on commit fbe268d

Please sign in to comment.