Skip to content

Commit

Permalink
Parser: macros are parsed in two phases [Closes nette/nette#711]
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Sep 10, 2014
1 parent b2f8f25 commit e5d01a0
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 59 deletions.
110 changes: 70 additions & 40 deletions src/Latte/Parser.php
Expand Up @@ -36,8 +36,8 @@ class Parser extends Object
'off' => array('[^\x00-\xFF]', ''),
);

/** @var string */
private $macroRe;
/** @var string[] */
private $delimiters;

/** @var string source template */
private $input;
Expand Down Expand Up @@ -66,7 +66,8 @@ class Parser extends Object
CONTEXT_HTML_TAG = 'htmlTag',
CONTEXT_HTML_ATTRIBUTE = 'htmlAttribute',
CONTEXT_RAW = 'raw',
CONTEXT_HTML_COMMENT = 'htmlComment';
CONTEXT_HTML_COMMENT = 'htmlComment',
CONTEXT_MACRO = 'macro';


/**
Expand All @@ -92,19 +93,9 @@ public function parse($input)
$this->lastHtmlTag = $this->syntaxEndTag = NULL;

while ($this->offset < strlen($input)) {
$matches = $this->{"context".$this->context[0]}();

if (!$matches) { // EOF
if ($this->{"context".$this->context[0]}() === FALSE) {
break;

} elseif (!empty($matches['comment'])) { // {* *}
$this->addToken(Token::COMMENT, $matches[0]);

} elseif (!empty($matches['macro'])) { // {macro}
$token = $this->addToken(Token::MACRO_TAG, $matches[0]);
list($token->name, $token->value, $token->modifiers, $token->empty) = $this->parseMacroTag($matches['macro']);
}

$this->filter();
}

Expand All @@ -123,7 +114,7 @@ private function contextHtmlText()
$matches = $this->match('~
(?:(?<=\n|^)[ \t]*)?<(?P<closing>/?)(?P<tag>[a-z0-9:]+)| ## begin of HTML tag <tag </tag - ignores <!DOCTYPE
<(?P<htmlcomment>!--(?!>))| ## begin of HTML comment <!--, but not <!-->
'.$this->macroRe.' ## macro tag
(?P<macro>' . $this->delimiters[0] . ')
~xsi');

if (!empty($matches['htmlcomment'])) { // <!--
Expand All @@ -136,8 +127,10 @@ private function contextHtmlText()
$token->closing = (bool) $matches['closing'];
$this->lastHtmlTag = $matches['closing'] . strtolower($matches['tag']);
$this->setContext(self::CONTEXT_HTML_TAG);

} else {
return $this->processMacro($matches);
}
return $matches;
}


Expand All @@ -147,8 +140,8 @@ private function contextHtmlText()
private function contextCData()
{
$matches = $this->match('~
</(?P<tag>'.$this->lastHtmlTag.')(?![a-z0-9:])| ## end HTML tag </tag
'.$this->macroRe.' ## macro tag
</(?P<tag>' . $this->lastHtmlTag . ')(?![a-z0-9:])| ## end HTML tag </tag
(?P<macro>' . $this->delimiters[0] . ')
~xsi');

if (!empty($matches['tag'])) { // </tag
Expand All @@ -157,8 +150,9 @@ private function contextCData()
$token->closing = TRUE;
$this->lastHtmlTag = '/' . $this->lastHtmlTag;
$this->setContext(self::CONTEXT_HTML_TAG);
} else {
return $this->processMacro($matches);
}
return $matches;
}


Expand All @@ -169,7 +163,7 @@ private function contextHtmlTag()
{
$matches = $this->match('~
(?P<end>\ ?/?>)([ \t]*\n)?| ## end of HTML tag
'.$this->macroRe.'| ## macro tag
(?P<macro>' . $this->delimiters[0] . ')|
\s*(?P<attr>[^\s/>={]+)(?:\s*=\s*(?P<value>["\']|[^\s/>{]+))? ## beginning of HTML attribute
~xsi');

Expand All @@ -193,8 +187,9 @@ private function contextHtmlTag()
$this->setContext(self::CONTEXT_HTML_ATTRIBUTE, $matches['value']);
}
}
} else {
return $this->processMacro($matches);
}
return $matches;
}


Expand All @@ -204,15 +199,16 @@ private function contextHtmlTag()
private function contextHtmlAttribute()
{
$matches = $this->match('~
(?P<quote>'.$this->context[1].')| ## end of HTML attribute
'.$this->macroRe.' ## macro tag
(?P<quote>' . $this->context[1] . ')| ## end of HTML attribute
(?P<macro>' . $this->delimiters[0] . ')
~xsi');

if (!empty($matches['quote'])) { // (attribute end) '"
$this->addToken(Token::TEXT, $matches[0]);
$this->setContext(self::CONTEXT_HTML_TAG);
} else {
return $this->processMacro($matches);
}
return $matches;
}


Expand All @@ -223,14 +219,15 @@ private function contextHtmlComment()
{
$matches = $this->match('~
(?P<htmlcomment>-->)| ## end of HTML comment
'.$this->macroRe.' ## macro tag
(?P<macro>' . $this->delimiters[0] . ')
~xsi');

if (!empty($matches['htmlcomment'])) { // -->
$this->addToken(Token::HTML_TAG_END, $matches[0]);
$this->setContext(self::CONTEXT_HTML_TEXT);
} else {
return $this->processMacro($matches);
}
return $matches;
}


Expand All @@ -240,9 +237,50 @@ private function contextHtmlComment()
private function contextRaw()
{
$matches = $this->match('~
'.$this->macroRe.' ## macro tag
(?P<macro>' . $this->delimiters[0] . ')
~xsi');
return $matches;
return $this->processMacro($matches);
}


/**
* Handles CONTEXT_MACRO.
*/
private function contextMacro()
{
$matches = $this->match('~
(?P<comment>\\*.*?\\*' . $this->delimiters[1] . '\n{0,2})|
(?P<macro>(?:
' . self::RE_STRING . '|
\{(?:' . self::RE_STRING . '|[^\'"{}])*+\}|
[^\'"{}]
)+?)
' . $this->delimiters[1] . '
(?P<rmargin>[ \t]*(?=\n))?
~xsiA');

if (!empty($matches['macro'])) {
$token = $this->addToken(Token::MACRO_TAG, $this->context[1][1] . $matches[0]);
list($token->name, $token->value, $token->modifiers, $token->empty) = $this->parseMacroTag($matches['macro']);
$this->context = $this->context[1][0];

} elseif (!empty($matches['comment'])) {
$this->addToken(Token::COMMENT, $this->context[1][1] . $matches[0]);
$this->context = $this->context[1][0];

} else {
throw new CompileException("Malformed macro on line {$this->getLine()}.");
}
}


private function processMacro($matches)
{
if (!empty($matches['macro'])) { // {macro} or {* *}
$this->setContext(self::CONTEXT_MACRO, array($this->context, $matches['macro']));
} else {
return FALSE;
}
}


Expand Down Expand Up @@ -325,17 +363,7 @@ public function setSyntax($type)
*/
public function setDelimiters($left, $right)
{
$this->macroRe = '
(?P<comment>' . $left . '\\*.*?\\*' . $right . '\n{0,2})|
' . $left . '
(?P<macro>(?:
' . self::RE_STRING . '|
\{(?:' . self::RE_STRING . '|[^\'"{}])*+\}|
[^\'"{}]
)+?)
' . $right . '
(?P<rmargin>[ \t]*(?=\n))?
';
$this->delimiters = array($left, $right);
return $this;
}

Expand Down Expand Up @@ -396,7 +424,9 @@ private function getLine()
protected function filter()
{
$token = end($this->output);
if ($token->type === Token::MACRO_TAG && $token->name === '/syntax') {
if (!$token) {

} elseif ($token->type === Token::MACRO_TAG && $token->name === '/syntax') {
$this->setSyntax($this->defaultSyntax);
$token->type = Token::COMMENT;

Expand Down
29 changes: 29 additions & 0 deletions tests/Latte/Parser.errors.phpt
@@ -0,0 +1,29 @@
<?php

/**
* Test: Latte\Parser errors.
*/

use Tester\Assert,
Latte\Parser;


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


Assert::exception(function() {
$parser = new Parser;
$parser->parse("\xA0\xA0");
}, 'InvalidArgumentException', 'Template is not valid UTF-8 stream.');


Assert::exception(function() {
$parser = new Parser;
$parser->parse("{var \n'abc}");
}, 'Latte\CompileException', 'Malformed macro on line 1.');


Assert::exception(function() {
$parser = new Parser;
$parser->parse("\n{* \n'abc}");
}, 'Latte\CompileException', 'Malformed macro on line 2.');
2 changes: 1 addition & 1 deletion tests/Latte/Parser.parseMacroTag.phpt
Expand Up @@ -10,7 +10,7 @@ use Tester\Assert;
require __DIR__ . '/../bootstrap.php';


$parser = new Latte\Parser();
$parser = new Latte\Parser;


Assert::same( array('?', 'echo', '', FALSE), $parser->parseMacroTag('? echo') );
Expand Down
18 changes: 0 additions & 18 deletions tests/Latte/Parser.utf8.phpt

This file was deleted.

4 comments on commit e5d01a0

@JanTvrdik
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a BC break. Maybe even major BC break. And also compiling $latte->compile('{') is broken.

@JanTvrdik
Copy link
Contributor

Choose a reason for hiding this comment

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

On the other hand it may make certain things easier for PhpStorm's Latte plugin.

@dg
Copy link
Member Author

@dg dg commented on e5d01a0 Sep 11, 2014

Choose a reason for hiding this comment

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

Fixed

@TomasVotruba
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Please sign in to comment.