contributing.md 0000666 00000002504 13436747734 0007624 0 ustar 00 How to contribute & use the issue tracker
=========================================
Nette welcomes your contributions. There are several ways to help out:
* Create an issue on GitHub, if you have found a bug
* Write test cases for open bug issues
* Write fixes for open bug/feature issues, preferably with test cases included
* Contribute to the [documentation](https://nette.org/en/writing)
Issues
------
Please **do not use the issue tracker to ask questions**. We will be happy to help you
on [Nette forum](https://forum.nette.org) or chat with us on [Gitter](https://gitter.im/nette/nette).
A good bug report shouldn't leave others needing to chase you up for more
information. Please try to be as detailed as possible in your report.
**Feature requests** are welcome. But take a moment to find out whether your idea
fits with the scope and aims of the project. It's up to *you* to make a strong
case to convince the project's developers of the merits of this feature.
Contributing
------------
If you'd like to contribute, please take a moment to read [the contributing guide](https://nette.org/en/contributing).
The best way to propose a feature is to discuss your ideas on [Nette forum](https://forum.nette.org) before implementing them.
Please do not fix whitespace, format code, or make a purely cosmetic patch.
Thanks! :heart:
src/latte.php 0000666 00000003470 13436747734 0007207 0 ustar 00 'exceptions.php',
'Latte\\Compiler' => 'Compiler/Compiler.php',
'Latte\\Engine' => 'Engine.php',
'Latte\\Helpers' => 'Helpers.php',
'Latte\\HtmlNode' => 'Compiler/HtmlNode.php',
'Latte\\ILoader' => 'ILoader.php',
'Latte\\IMacro' => 'IMacro.php',
'Latte\\Loaders\\FileLoader' => 'Loaders/FileLoader.php',
'Latte\\Loaders\\StringLoader' => 'Loaders/StringLoader.php',
'Latte\\MacroNode' => 'Compiler/MacroNode.php',
'Latte\\Macros\\BlockMacros' => 'Macros/BlockMacros.php',
'Latte\\Macros\\CoreMacros' => 'Macros/CoreMacros.php',
'Latte\\Macros\\MacroSet' => 'Macros/MacroSet.php',
'Latte\\MacroTokens' => 'Compiler/MacroTokens.php',
'Latte\\Parser' => 'Compiler/Parser.php',
'Latte\\PhpHelpers' => 'Compiler/PhpHelpers.php',
'Latte\\PhpWriter' => 'Compiler/PhpWriter.php',
'Latte\\RegexpException' => 'exceptions.php',
'Latte\\Runtime\\CachingIterator' => 'Runtime/CachingIterator.php',
'Latte\\Runtime\\FilterExecutor' => 'Runtime/FilterExecutor.php',
'Latte\\Runtime\\FilterInfo' => 'Runtime/FilterInfo.php',
'Latte\\Runtime\\Filters' => 'Runtime/Filters.php',
'Latte\\Runtime\\Html' => 'Runtime/Html.php',
'Latte\\Runtime\\IHtmlString' => 'Runtime/IHtmlString.php',
'Latte\\Runtime\\ISnippetBridge' => 'Runtime/ISnippetBridge.php',
'Latte\\Runtime\\SnippetDriver' => 'Runtime/SnippetDriver.php',
'Latte\\Runtime\\Template' => 'Runtime/Template.php',
'Latte\\RuntimeException' => 'exceptions.php',
'Latte\\Strict' => 'Strict.php',
'Latte\\Token' => 'Compiler/Token.php',
'Latte\\TokenIterator' => 'Compiler/TokenIterator.php',
'Latte\\Tokenizer' => 'Compiler/Tokenizer.php',
];
if (isset($classMap[$className])) {
require __DIR__ . '/Latte/' . $classMap[$className];
}
});
src/Latte/IMacro.php 0000666 00000001266 13436747734 0010322 0 ustar 00 1,'hr' => 1,'br' => 1,'input' => 1,'meta' => 1,'area' => 1,'embed' => 1,'keygen' => 1,'source' => 1,'base' => 1,
'col' => 1,'link' => 1,'param' => 1,'basefont' => 1,'frame' => 1,'isindex' => 1,'wbr' => 1,'command' => 1,'track' => 1,
];
/**
* Checks callback.
* @return callable
*/
public static function checkCallback($callable)
{
if (!is_callable($callable, FALSE, $text)) {
throw new \InvalidArgumentException("Callback '$text' is not callable.");
}
return $callable;
}
/**
* Finds the best suggestion.
* @return string|NULL
*/
public static function getSuggestion(array $items, $value)
{
$best = NULL;
$min = (strlen($value) / 4 + 1) * 10 + .1;
foreach (array_unique($items, SORT_REGULAR) as $item) {
$item = is_object($item) ? $item->getName() : $item;
if (($len = levenshtein($item, $value, 10, 11, 10)) > 0 && $len < $min) {
$min = $len;
$best = $item;
}
}
return $best;
}
/**
* @return bool
*/
public static function removeFilter(&$modifier, $filter)
{
$modifier = preg_replace('#\|(' . $filter . ')\s?(?=\||\z)#i', '', $modifier, -1, $found);
return (bool) $found;
}
/**
* Starts the $haystack string with the prefix $needle?
* @return bool
*/
public static function startsWith($haystack, $needle)
{
return strncmp($haystack, $needle, strlen($needle)) === 0;
}
}
src/Latte/exceptions.php 0000666 00000003134 13436747734 0011325 0 ustar 00 sourceCode = (string) $code;
$this->sourceLine = (int) $line;
$this->sourceName = (string) $name;
if (@is_file($name)) { // @ - may trigger error
$this->message = rtrim($this->message, '.')
. ' in ' . str_replace(dirname(dirname($name)), '...', $name) . ($line ? ":$line" : '');
}
return $this;
}
}
/**
* The exception that indicates error of the last Regexp execution.
*/
class RegexpException extends \Exception
{
public static $messages = [
PREG_INTERNAL_ERROR => 'Internal error',
PREG_BACKTRACK_LIMIT_ERROR => 'Backtrack limit was exhausted',
PREG_RECURSION_LIMIT_ERROR => 'Recursion limit was exhausted',
PREG_BAD_UTF8_ERROR => 'Malformed UTF-8 data',
5 => 'Offset didn\'t correspond to the begin of a valid UTF-8 code point', // PREG_BAD_UTF8_OFFSET_ERROR
6 => 'Failed due to limited JIT stack space', // PREG_JIT_STACKLIMIT_ERROR
];
public function __construct($message, $code = NULL)
{
parent::__construct($message ?: (isset(self::$messages[$code]) ? self::$messages[$code] : 'Unknown error'), $code);
}
}
/**
* The exception that indicates error during rendering template.
*/
class RuntimeException extends \Exception
{
}
src/Latte/Loaders/StringLoader.php 0000666 00000002316 13436747734 0013133 0 ustar 00 content] */
private $templates;
public function __construct(array $templates = NULL)
{
$this->templates = $templates;
}
/**
* Returns template source code.
* @return string
*/
public function getContent($name)
{
if ($this->templates === NULL) {
return $name;
} elseif (isset($this->templates[$name])) {
return $this->templates[$name];
} else {
throw new \RuntimeException("Missing template '$name'.");
}
}
/**
* @return bool
*/
public function isExpired($name, $time)
{
return FALSE;
}
/**
* Returns referred template name.
* @return string
*/
public function getReferredName($name, $referringName)
{
if ($this->templates === NULL) {
throw new \LogicException("Missing template '$name'.");
}
return $name;
}
/**
* Returns unique identifier for caching.
* @return string
*/
public function getUniqueId($name)
{
return $this->getContent($name);
}
}
src/Latte/Loaders/FileLoader.php 0000666 00000004060 13436747734 0012542 0 ustar 00 baseDir = $baseDir ? $this->normalizePath("$baseDir/") : NULL;
}
/**
* Returns template source code.
* @return string
*/
public function getContent($file)
{
$file = $this->baseDir . $file;
if ($this->baseDir && !Latte\Helpers::startsWith($this->normalizePath($file), $this->baseDir)) {
throw new \RuntimeException("Template '$file' is not within the allowed path '$this->baseDir'.");
} elseif (!is_file($file)) {
throw new \RuntimeException("Missing template file '$file'.");
} elseif ($this->isExpired($file, time())) {
if (@touch($file) === FALSE) {
trigger_error("File's modification time is in the future. Cannot update it: " . error_get_last()['message'], E_USER_WARNING);
}
}
return file_get_contents($file);
}
/**
* @return bool
*/
public function isExpired($file, $time)
{
return @filemtime($this->baseDir . $file) > $time; // @ - stat may fail
}
/**
* Returns referred template name.
* @return string
*/
public function getReferredName($file, $referringFile)
{
if ($this->baseDir || !preg_match('#/|\\\\|[a-z][a-z0-9+.-]*:#iA', $file)) {
$file = $this->normalizePath($referringFile . '/../' . $file);
}
return $file;
}
/**
* Returns unique identifier for caching.
* @return string
*/
public function getUniqueId($file)
{
return $this->baseDir . strtr($file, '/', DIRECTORY_SEPARATOR);
}
/**
* @return string
*/
private static function normalizePath($path)
{
$res = [];
foreach (explode('/', strtr($path, '\\', '/')) as $part) {
if ($part === '..' && $res && end($res) !== '..') {
array_pop($res);
} elseif ($part !== '.') {
$res[] = $part;
}
}
return implode(DIRECTORY_SEPARATOR, $res);
}
}
src/Latte/ILoader.php 0000666 00000001154 13436747734 0010463 0 ustar 00 addMacro('include', [$me, 'macroInclude']);
$me->addMacro('includeblock', [$me, 'macroIncludeBlock']); // deprecated
$me->addMacro('import', [$me, 'macroImport'], NULL, NULL, self::ALLOWED_IN_HEAD);
$me->addMacro('extends', [$me, 'macroExtends'], NULL, NULL, self::ALLOWED_IN_HEAD);
$me->addMacro('layout', [$me, 'macroExtends'], NULL, NULL, self::ALLOWED_IN_HEAD);
$me->addMacro('snippet', [$me, 'macroBlock'], [$me, 'macroBlockEnd']);
$me->addMacro('block', [$me, 'macroBlock'], [$me, 'macroBlockEnd'], NULL, self::AUTO_CLOSE);
$me->addMacro('define', [$me, 'macroBlock'], [$me, 'macroBlockEnd']);
$me->addMacro('snippetArea', [$me, 'macroBlock'], [$me, 'macroBlockEnd']);
$me->addMacro('ifset', [$me, 'macroIfset'], '}');
$me->addMacro('elseifset', [$me, 'macroIfset']);
}
/**
* Initializes before template parsing.
* @return void
*/
public function initialize()
{
$this->namedBlocks = [];
$this->blockTypes = [];
$this->extends = NULL;
$this->imports = [];
}
/**
* Finishes template parsing.
*/
public function finalize()
{
$compiler = $this->getCompiler();
$functions = [];
foreach ($this->namedBlocks as $name => $code) {
$compiler->addMethod(
$functions[$name] = $this->generateMethodName($name),
'?>' . $compiler->expandTokens($code) . 'namedBlocks) {
$compiler->addProperty('blocks', $functions);
$compiler->addProperty('blockTypes', $this->blockTypes);
}
return [
($this->extends === NULL ? '' : '$this->parentName = ' . $this->extends . ';') . implode($this->imports)
];
}
/********************* macros ****************d*g**/
/**
* {include block}
*/
public function macroInclude(MacroNode $node, PhpWriter $writer)
{
$node->replaced = FALSE;
$destination = $node->tokenizer->fetchWord(); // destination [,] [params]
if (!preg_match('~#|[\w-]+\z~A', $destination)) {
return FALSE;
}
$destination = ltrim($destination, '#');
$parent = $destination === 'parent';
if ($destination === 'parent' || $destination === 'this') {
for ($item = $node->parentNode; $item && $item->name !== 'block' && !isset($item->data->name); $item = $item->parentNode);
if (!$item) {
throw new CompileException("Cannot include $destination block outside of any block.");
}
$destination = $item->data->name;
}
$noEscape = Helpers::removeFilter($node->modifiers, 'noescape');
if (!$noEscape && Helpers::removeFilter($node->modifiers, 'escape')) {
trigger_error('Macro ' . $node->getNotation() . ' provides auto-escaping, remove |escape.');
}
if ($node->modifiers && !$noEscape) {
$node->modifiers .= '|escape';
}
return $writer->write(
'$this->renderBlock' . ($parent ? 'Parent' : '') . '('
. (strpos($destination, '$') === FALSE ? var_export($destination, TRUE) : $destination)
. ', %node.array? + '
. (isset($this->namedBlocks[$destination]) || $parent ? 'get_defined_vars()' : '$this->params')
. ($node->modifiers
? ', function ($s, $type) { $_fi = new LR\FilterInfo($type); return %modifyContent($s); }'
: ($noEscape || $parent ? '' : ', ' . var_export(implode($node->context), TRUE)))
. ');'
);
}
/**
* {includeblock "file"}
* @deprecated
*/
public function macroIncludeBlock(MacroNode $node, PhpWriter $writer)
{
//trigger_error('Macro {includeblock} is deprecated, use similar macro {import}.', E_USER_DEPRECATED);
$node->replaced = FALSE;
if ($node->modifiers) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
}
return $writer->write(
'ob_start(function () {}); $this->createTemplate(%node.word, %node.array? + get_defined_vars(), "includeblock")->renderToContentType(%var); echo rtrim(ob_get_clean());',
implode($node->context)
);
}
/**
* {import "file"}
*/
public function macroImport(MacroNode $node, PhpWriter $writer)
{
if ($node->modifiers) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
}
$destination = $node->tokenizer->fetchWord();
$this->checkExtraArgs($node);
$code = $writer->write('$this->createTemplate(%word, $this->params, "import")->render();', $destination);
if ($this->getCompiler()->isInHead()) {
$this->imports[] = $code;
} else {
return $code;
}
}
/**
* {extends none | $var | "file"}
*/
public function macroExtends(MacroNode $node, PhpWriter $writer)
{
$notation = $node->getNotation();
if ($node->modifiers) {
throw new CompileException("Modifiers are not allowed in $notation");
} elseif (!$node->args) {
throw new CompileException("Missing destination in $notation");
} elseif ($node->parentNode) {
throw new CompileException("$notation must be placed outside any macro.");
} elseif ($this->extends !== NULL) {
throw new CompileException("Multiple $notation declarations are not allowed.");
} elseif ($node->args === 'none') {
$this->extends = 'FALSE';
} else {
$this->extends = $writer->write('%node.word%node.args');
}
if (!$this->getCompiler()->isInHead()) {
trigger_error("$notation must be placed in template head.", E_USER_WARNING);
}
}
/**
* {block [name]}
* {snippet [name [,]] [tag]}
* {snippetArea [name]}
* {define name}
*/
public function macroBlock(MacroNode $node, PhpWriter $writer)
{
$name = $node->tokenizer->fetchWord();
if ($node->name === 'block' && $name === FALSE) { // anonymous block
return $node->modifiers === '' ? '' : 'ob_start(function () {})';
} elseif ($node->name === 'define' && $node->modifiers) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
}
$node->data->name = $name = ltrim((string) $name, '#');
if ($name == NULL) {
if ($node->name === 'define') {
throw new CompileException('Missing block name.');
}
} elseif (strpos($name, '$') !== FALSE) { // dynamic block/snippet
if ($node->name === 'snippet') {
for ($parent = $node->parentNode; $parent && !($parent->name === 'snippet' || $parent->name === 'snippetArea'); $parent = $parent->parentNode);
if (!$parent) {
throw new CompileException('Dynamic snippets are allowed only inside static snippet/snippetArea.');
}
$parent->data->dynamic = TRUE;
$node->data->leave = TRUE;
$node->closingCode = "global->snippetDriver->leave(); ?>";
$enterCode = '$this->global->snippetDriver->enter(' . $writer->formatWord($name) . ', "' . SnippetDriver::TYPE_DYNAMIC . '");';
if ($node->prefix) {
$node->attrCode = $writer->write("global->snippetDriver->getHtmlId({$writer->formatWord($name)})) . '\"' ?>");
return $writer->write($enterCode);
}
$tag = trim((string) $node->tokenizer->fetchWord(), '<>');
if ($tag) {
trigger_error('HTML tag specified in {snippet} is deprecated, use n:snippet.', E_USER_DEPRECATED);
}
$tag = $tag ? $tag : 'div';
$node->closingCode .= "\n$tag>";
$this->checkExtraArgs($node);
return $writer->write("?>\n<$tag id=\"global->snippetDriver->getHtmlId({$writer->formatWord($name)})) ?>\">data->leave = TRUE;
$node->data->func = $this->generateMethodName($name);
$fname = $writer->formatWord($name);
if ($node->name === 'define') {
$node->closingCode = '';
} else {
if (Helpers::startsWith((string) $node->context[1], Latte\Compiler::CONTEXT_HTML_ATTRIBUTE)) {
$node->context[1] = '';
$node->modifiers .= '|escape';
} elseif ($node->modifiers) {
$node->modifiers .= '|escape';
}
$node->closingCode = $writer->write('renderBlock(%raw, get_defined_vars()'
. ($node->modifiers ? ', function ($s, $type) { $_fi = new LR\FilterInfo($type); return %modifyContent($s); }' : '') . '); ?>', $fname);
}
$blockType = var_export(implode($node->context), TRUE);
$this->checkExtraArgs($node);
return "\$this->checkBlockContentType($blockType, $fname);"
. "\$this->blockQueue[$fname][] = [\$this, '{$node->data->func}'];";
}
}
// static snippet/snippetArea
if ($node->name === 'snippet' || $node->name === 'snippetArea') {
if ($node->prefix && isset($node->htmlNode->attrs['id'])) {
throw new CompileException('Cannot combine HTML attribute id with n:snippet.');
}
$node->data->name = $name = '_' . $name;
}
if (isset($this->namedBlocks[$name])) {
throw new CompileException("Cannot redeclare static {$node->name} '$name'");
}
$extendsCheck = $this->namedBlocks ? '' : 'if ($this->getParentName()) return get_defined_vars();';
$this->namedBlocks[$name] = TRUE;
if (Helpers::removeFilter($node->modifiers, 'escape')) {
trigger_error('Macro ' . $node->getNotation() . ' provides auto-escaping, remove |escape.');
}
if (Helpers::startsWith((string) $node->context[1], Latte\Compiler::CONTEXT_HTML_ATTRIBUTE)) {
$node->context[1] = '';
$node->modifiers .= '|escape';
} elseif ($node->modifiers) {
$node->modifiers .= '|escape';
}
$this->blockTypes[$name] = implode($node->context);
$include = '$this->renderBlock(%var, ' . (($node->name === 'snippet' || $node->name === 'snippetArea') ? '$this->params' : 'get_defined_vars()')
. ($node->modifiers ? ', function ($s, $type) { $_fi = new LR\FilterInfo($type); return %modifyContent($s); }' : '') . ')';
if ($node->name === 'snippet') {
if ($node->prefix) {
if (isset($node->htmlNode->macroAttrs['foreach'])) {
trigger_error('Combination of n:snippet with n:foreach is invalid, use n:inner-foreach.', E_USER_WARNING);
}
$node->attrCode = $writer->write('global->snippetDriver->getHtmlId(%var)) . \'"\' ?>', (string) substr($name, 1));
return $writer->write($include, $name);
}
$tag = trim((string) $node->tokenizer->fetchWord(), '<>');
if ($tag) {
trigger_error('HTML tag specified in {snippet} is deprecated, use n:snippet.', E_USER_DEPRECATED);
}
$tag = $tag ? $tag : 'div';
$this->checkExtraArgs($node);
return $writer->write("?>\n<$tag id=\"global->snippetDriver->getHtmlId(%var)) ?>\">\n$tag>name === 'define') {
$tokens = $node->tokenizer;
$args = [];
while ($tokens->isNext()) {
$args[] = $tokens->expectNextValue($tokens::T_VARIABLE);
if ($tokens->isNext()) {
$tokens->expectNextValue(',');
}
}
if ($args) {
$node->data->args = 'list(' . implode(', ', $args) . ') = $_args + [' . str_repeat('NULL, ', count($args)) . '];';
}
return $extendsCheck;
} else { // block, snippetArea
$this->checkExtraArgs($node);
return $writer->write($extendsCheck . $include, $name);
}
}
/**
* {/block}
* {/snippet}
* {/snippetArea}
* {/define}
*/
public function macroBlockEnd(MacroNode $node, PhpWriter $writer)
{
if (isset($node->data->name)) { // block, snippet, define
if ($asInner = $node->name === 'snippet' && $node->prefix === MacroNode::PREFIX_NONE) {
$node->content = $node->innerContent;
}
if (($node->name === 'snippet' || $node->name === 'snippetArea') && strpos($node->data->name, '$') === FALSE) {
$type = $node->name === 'snippet' ? SnippetDriver::TYPE_STATIC : SnippetDriver::TYPE_AREA;
$node->content = 'global->snippetDriver->enter('
. $writer->formatWord(substr($node->data->name, 1))
. ', "' . $type . '"); ?>'
. preg_replace('#(?<=\n)[ \t]+\z#', '', $node->content) . 'global->snippetDriver->leave(); ?>';
}
if (empty($node->data->leave)) {
if (preg_match('#\$|n:#', $node->content)) {
$node->content = 'data->args) ? 'extract($this->params); ' . $node->data->args : 'extract($_args);') . ' ?>'
. $node->content;
}
$this->namedBlocks[$node->data->name] = $tmp = preg_replace('#^\n+|(?<=\n)[ \t]+\z#', '', $node->content);
$node->content = substr_replace($node->content, $node->openingCode . "\n", strspn($node->content, "\n"), strlen($tmp));
$node->openingCode = '';
} elseif (isset($node->data->func)) {
$node->content = rtrim($node->content, " \t");
$this->getCompiler()->addMethod(
$node->data->func,
$this->getCompiler()->expandTokens("extract(\$_args);\n?>$node->contentcontent = '';
}
if ($asInner) { // n:snippet -> n:inner-snippet
$node->innerContent = $node->openingCode . $node->content . $node->closingCode;
$node->closingCode = $node->openingCode = '';
}
return ' '; // consume next new line
} elseif ($node->modifiers) { // anonymous block with modifier
$node->modifiers .= '|escape';
return $writer->write('$_fi = new LR\FilterInfo(%var); echo %modifyContent(ob_get_clean());', $node->context[0]);
}
}
/**
* {ifset block}
* {elseifset block}
*/
public function macroIfset(MacroNode $node, PhpWriter $writer)
{
if ($node->modifiers) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
}
if (!preg_match('~#|[\w-]+\z~A', $node->args)) {
return FALSE;
}
$list = [];
while (($name = $node->tokenizer->fetchWord()) !== FALSE) {
$list[] = preg_match('~#|[\w-]+\z~A', $name)
? '$this->blockQueue["' . ltrim($name, '#') . '"]'
: $writer->formatArgs(new Latte\MacroTokens($name));
}
return ($node->name === 'elseifset' ? '} else' : '')
. 'if (isset(' . implode(', ', $list) . ')) {';
}
private function generateMethodName($blockName)
{
$clean = trim(preg_replace('#\W+#', '_', $blockName), '_');
$name = 'block' . ucfirst($clean);
$methods = array_keys($this->getCompiler()->getMethods());
if (!$clean || in_array(strtolower($name), array_map('strtolower', $methods))) {
$name .= '_' . substr(md5($blockName), 0, 5);
}
return $name;
}
}
src/Latte/Macros/MacroSet.php 0000666 00000007244 13436747734 0012113 0 ustar 00 compiler = $compiler;
}
public function addMacro($name, $begin, $end = NULL, $attr = NULL, $flags = NULL)
{
if (!$begin && !$end && !$attr) {
throw new \InvalidArgumentException("At least one argument must be specified for macro '$name'.");
}
foreach ([$begin, $end, $attr] as $arg) {
if ($arg && !is_string($arg)) {
Latte\Helpers::checkCallback($arg);
}
}
$this->macros[$name] = [$begin, $end, $attr];
$this->compiler->addMacro($name, $this, $flags);
return $this;
}
/**
* Initializes before template parsing.
* @return void
*/
public function initialize()
{
}
/**
* Finishes template parsing.
* @return array|NULL [prolog, epilog]
*/
public function finalize()
{
}
/**
* New node is found.
* @return bool|NULL
*/
public function nodeOpened(MacroNode $node)
{
list($begin, $end, $attr) = $this->macros[$node->name];
$node->empty = !$end;
if ($node->modifiers
&& (!$begin || (is_string($begin) && strpos($begin, '%modify') === FALSE))
&& (!$end || (is_string($end) && strpos($end, '%modify') === FALSE))
&& (!$attr || (is_string($attr) && strpos($attr, '%modify') === FALSE))
) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
}
if ($node->args
&& (!$begin || (is_string($begin) && strpos($begin, '%node') === FALSE))
&& (!$end || (is_string($end) && strpos($end, '%node') === FALSE))
&& (!$attr || (is_string($attr) && strpos($attr, '%node') === FALSE))
) {
throw new CompileException('Arguments are not allowed in ' . $node->getNotation());
}
if ($attr && $node->prefix === $node::PREFIX_NONE) {
$node->empty = TRUE;
$node->context[1] = Latte\Compiler::CONTEXT_HTML_ATTRIBUTE;
$res = $this->compile($node, $attr);
if ($res === FALSE) {
return FALSE;
} elseif (!$node->attrCode) {
$node->attrCode = "";
}
$node->context[1] = Latte\Compiler::CONTEXT_HTML_TEXT;
} elseif ($begin) {
$res = $this->compile($node, $begin);
if ($res === FALSE || ($node->empty && $node->prefix)) {
return FALSE;
} elseif (!$node->openingCode && is_string($res) && $res !== '') {
$node->openingCode = "";
}
} elseif (!$end) {
return FALSE;
}
}
/**
* Node is closed.
* @return void
*/
public function nodeClosed(MacroNode $node)
{
if (isset($this->macros[$node->name][1])) {
$res = $this->compile($node, $this->macros[$node->name][1]);
if (!$node->closingCode && is_string($res) && $res !== '') {
$node->closingCode = "";
}
}
}
/**
* Generates code.
* @return string|bool|NULL
*/
private function compile(MacroNode $node, $def)
{
$node->tokenizer->reset();
$writer = Latte\PhpWriter::using($node);
return is_string($def)
? $writer->write($def)
: call_user_func($def, $node, $writer);
}
/**
* @return Latte\Compiler
*/
public function getCompiler()
{
return $this->compiler;
}
/** @internal */
protected function checkExtraArgs(MacroNode $node)
{
if ($node->tokenizer->isNext()) {
$args = Latte\Runtime\Filters::truncate($node->tokenizer->joinAll(), 20);
trigger_error("Unexpected arguments '$args' in " . $node->getNotation());
}
}
}
src/Latte/Macros/CoreMacros.php 0000666 00000041701 13436747734 0012427 0 ustar 00 value} set template parameter
* - {default var => value} set default template parameter
* - {dump $var}
* - {debugbreak}
* - {contentType ...} HTTP Content-Type header
* - {status ...} HTTP status
* - {l} {r} to display { }
*/
class CoreMacros extends MacroSet
{
/** @var array */
private $overwrittenVars;
public static function install(Latte\Compiler $compiler)
{
$me = new static($compiler);
$me->addMacro('if', [$me, 'macroIf'], [$me, 'macroEndIf']);
$me->addMacro('elseif', '} elseif (%node.args) {');
$me->addMacro('else', [$me, 'macroElse']);
$me->addMacro('ifset', 'if (isset(%node.args)) {', '}');
$me->addMacro('elseifset', '} elseif (isset(%node.args)) {');
$me->addMacro('ifcontent', [$me, 'macroIfContent'], [$me, 'macroEndIfContent']);
$me->addMacro('switch', '$this->global->switch[] = (%node.args); if (FALSE) {', '} array_pop($this->global->switch)');
$me->addMacro('case', '} elseif (end($this->global->switch) === (%node.args)) {');
$me->addMacro('foreach', '', [$me, 'macroEndForeach']);
$me->addMacro('for', 'for (%node.args) {', '}');
$me->addMacro('while', [$me, 'macroWhile'], [$me, 'macroEndWhile']);
$me->addMacro('continueIf', [$me, 'macroBreakContinueIf']);
$me->addMacro('breakIf', [$me, 'macroBreakContinueIf']);
$me->addMacro('first', 'if ($iterator->isFirst(%node.args)) {', '}');
$me->addMacro('last', 'if ($iterator->isLast(%node.args)) {', '}');
$me->addMacro('sep', 'if (!$iterator->isLast(%node.args)) {', '}');
$me->addMacro('var', [$me, 'macroVar']);
$me->addMacro('default', [$me, 'macroVar']);
$me->addMacro('dump', [$me, 'macroDump']);
$me->addMacro('debugbreak', [$me, 'macroDebugbreak']);
$me->addMacro('l', '?>{addMacro('r', '?>}addMacro('_', [$me, 'macroTranslate'], [$me, 'macroTranslate']);
$me->addMacro('=', [$me, 'macroExpr']);
$me->addMacro('?', [$me, 'macroExpr']);
$me->addMacro('capture', [$me, 'macroCapture'], [$me, 'macroCaptureEnd']);
$me->addMacro('spaceless', [$me, 'macroSpaceless'], [$me, 'macroSpaceless']);
$me->addMacro('include', [$me, 'macroInclude']);
$me->addMacro('use', [$me, 'macroUse']);
$me->addMacro('contentType', [$me, 'macroContentType'], NULL, NULL, self::ALLOWED_IN_HEAD);
$me->addMacro('status', [$me, 'macroStatus']);
$me->addMacro('php', [$me, 'macroExpr']);
$me->addMacro('class', NULL, NULL, [$me, 'macroClass']);
$me->addMacro('attr', NULL, NULL, [$me, 'macroAttr']);
}
/**
* Initializes before template parsing.
* @return void
*/
public function initialize()
{
$this->overwrittenVars = [];
}
/**
* Finishes template parsing.
* @return array|NULL [prolog, epilog]
*/
public function finalize()
{
$code = '';
foreach ($this->overwrittenVars as $var => $lines) {
$s = var_export($var, TRUE);
$code .= 'if (isset($this->params[' . var_export($var, TRUE)
. "])) trigger_error('Variable $" . addcslashes($var, "'") . ' overwritten in foreach on line ' . implode(', ', $lines) . "'); ";
}
return [$code];
}
/********************* macros ****************d*g**/
/**
* {if ...}
*/
public function macroIf(MacroNode $node, PhpWriter $writer)
{
if ($node->modifiers) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
}
if ($node->data->capture = ($node->args === '')) {
return 'ob_start(function () {})';
}
if ($node->prefix === $node::PREFIX_TAG) {
return $writer->write($node->htmlNode->closing ? 'if (array_pop($this->global->ifs)) {' : 'if ($this->global->ifs[] = (%node.args)) {');
}
return $writer->write('if (%node.args) {');
}
/**
* {/if ...}
*/
public function macroEndIf(MacroNode $node, PhpWriter $writer)
{
if ($node->data->capture) {
if ($node->args === '') {
throw new CompileException('Missing condition in {if} macro.');
}
return $writer->write('if (%node.args) '
. (isset($node->data->else) ? '{ ob_end_clean(); echo ob_get_clean(); }' : 'echo ob_get_clean();')
. ' else '
. (isset($node->data->else) ? '{ $this->global->else = ob_get_clean(); ob_end_clean(); echo $this->global->else; }' : 'ob_end_clean();')
);
}
return '}';
}
/**
* {else}
*/
public function macroElse(MacroNode $node, PhpWriter $writer)
{
if ($node->modifiers) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
} elseif ($node->args) {
$hint = Helpers::startsWith($node->args, 'if') ? ', did you mean {elseif}?' : '';
throw new CompileException('Arguments are not allowed in ' . $node->getNotation() . $hint);
}
$ifNode = $node->parentNode;
if ($ifNode && $ifNode->name === 'if' && $ifNode->data->capture) {
if (isset($ifNode->data->else)) {
throw new CompileException('Macro {if} supports only one {else}.');
}
$ifNode->data->else = TRUE;
return 'ob_start(function () {})';
}
return '} else {';
}
/**
* n:ifcontent
*/
public function macroIfContent(MacroNode $node, PhpWriter $writer)
{
if (!$node->prefix || $node->prefix !== MacroNode::PREFIX_NONE) {
throw new CompileException('Unknown ' . $node->getNotation() . ", use n:{$node->name} attribute.");
}
}
/**
* n:ifcontent
*/
public function macroEndIfContent(MacroNode $node, PhpWriter $writer)
{
$node->openingCode = '';
$node->innerContent = '' . $node->innerContent . 'global->ifcontent = ob_get_flush(); ?>';
$node->closingCode = 'global->ifcontent) === "") ob_end_clean(); else echo ob_get_clean(); ?>';
}
/**
* {_$var |modifiers}
*/
public function macroTranslate(MacroNode $node, PhpWriter $writer)
{
if ($node->closing) {
if (strpos($node->content, 'content, TRUE);
$node->content = '';
} else {
$node->openingCode = '' . $node->openingCode;
$value = 'ob_get_clean()';
}
return $writer->write('$_fi = new LR\FilterInfo(%var); echo %modifyContent($this->filters->filterContent("translate", $_fi, %raw))', $node->context[0], $value);
} elseif ($node->empty = ($node->args !== '')) {
return $writer->write('echo %modify(call_user_func($this->filters->translate, %node.args))');
}
}
/**
* {include "file" [,] [params]}
*/
public function macroInclude(MacroNode $node, PhpWriter $writer)
{
$node->replaced = FALSE;
$noEscape = Helpers::removeFilter($node->modifiers, 'noescape');
if (!$noEscape && Helpers::removeFilter($node->modifiers, 'escape')) {
trigger_error('Macro {include} provides auto-escaping, remove |escape.');
}
if ($node->modifiers && !$noEscape) {
$node->modifiers .= '|escape';
}
return $writer->write(
'/* line ' . $node->startLine . ' */
$this->createTemplate(%node.word, %node.array? + $this->params, "include")->renderToContentType(%raw);',
$node->modifiers
? $writer->write('function ($s, $type) { $_fi = new LR\FilterInfo($type); return %modifyContent($s); }')
: var_export($noEscape ? NULL : implode($node->context), TRUE)
);
}
/**
* {use class MacroSet}
*/
public function macroUse(MacroNode $node, PhpWriter $writer)
{
trigger_error('Macro {use} is deprecated.', E_USER_DEPRECATED);
if ($node->modifiers) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
}
call_user_func(Helpers::checkCallback([$node->tokenizer->fetchWord(), 'install']), $this->getCompiler())
->initialize();
}
/**
* {capture $variable}
*/
public function macroCapture(MacroNode $node, PhpWriter $writer)
{
$variable = $node->tokenizer->fetchWord();
if (!Helpers::startsWith($variable, '$')) {
throw new CompileException("Invalid capture block variable '$variable'");
}
$this->checkExtraArgs($node);
$node->data->variable = $variable;
return 'ob_start(function () {})';
}
/**
* {/capture}
*/
public function macroCaptureEnd(MacroNode $node, PhpWriter $writer)
{
$body = in_array($node->context[0], [Engine::CONTENT_HTML, Engine::CONTENT_XHTML], TRUE)
? "ob_get_length() ? new LR\\Html(ob_get_clean()) : ob_get_clean()"
: 'ob_get_clean()';
return $writer->write("\$_fi = new LR\\FilterInfo(%var); %raw = %modifyContent($body);", $node->context[0], $node->data->variable);
}
/**
* {spaceless} ... {/spaceless}
*/
public function macroSpaceless(MacroNode $node, PhpWriter $writer)
{
if ($node->modifiers || $node->args) {
throw new CompileException('Modifiers and arguments are not allowed in ' . $node->getNotation());
}
$node->openingCode = in_array($node->context[0], [Engine::CONTENT_HTML, Engine::CONTENT_XHTML], TRUE)
? ''
: "";
$node->closingCode = '';
}
/**
* {while ...}
*/
public function macroWhile(MacroNode $node, PhpWriter $writer)
{
if ($node->modifiers) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
}
if ($node->data->do = ($node->args === '')) {
return 'do {';
}
return $writer->write('while (%node.args) {');
}
/**
* {/while ...}
*/
public function macroEndWhile(MacroNode $node, PhpWriter $writer)
{
if ($node->data->do) {
if ($node->args === '') {
throw new CompileException('Missing condition in {while} macro.');
}
return $writer->write('} while (%node.args);');
}
return '}';
}
/**
* {foreach ...}
*/
public function macroEndForeach(MacroNode $node, PhpWriter $writer)
{
$noCheck = Helpers::removeFilter($node->modifiers, 'nocheck');
$noIterator = Helpers::removeFilter($node->modifiers, 'noiterator');
if ($node->modifiers) {
throw new CompileException('Only modifiers |noiterator and |nocheck are allowed here.');
}
$node->openingCode = 'formatArgs();
if (!$noCheck) {
preg_match('#.+\s+as\s*\$(\w+)(?:\s*=>\s*\$(\w+))?#i', $args, $m);
for ($i = 1; $i < count($m); $i++) {
$this->overwrittenVars[$m[$i]][] = $node->startLine;
}
}
if (!$noIterator && preg_match('#\W(\$iterator|include|require|get_defined_vars)\W#', $this->getCompiler()->expandTokens($node->content))) {
$node->openingCode .= 'foreach ($iterator = $this->global->its[] = new LR\CachingIterator('
. preg_replace('#(.*)\s+as\s+#i', '$1) as ', $args, 1) . ') { ?>';
$node->closingCode = 'global->its); $iterator = end($this->global->its); ?>';
} else {
$node->openingCode .= 'foreach (' . $args . ') { ?>';
$node->closingCode = '';
}
}
/**
* {breakIf ...}
* {continueIf ...}
*/
public function macroBreakContinueIf(MacroNode $node, PhpWriter $writer)
{
if ($node->modifiers) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
}
$cmd = str_replace('If', '', $node->name);
if ($node->parentNode && $node->parentNode->prefix === $node::PREFIX_NONE) {
return $writer->write("if (%node.args) { echo \"{$node->parentNode->htmlNode->name}>\\n\"; $cmd; }");
}
return $writer->write("if (%node.args) $cmd;");
}
/**
* n:class="..."
*/
public function macroClass(MacroNode $node, PhpWriter $writer)
{
if (isset($node->htmlNode->attrs['class'])) {
throw new CompileException('It is not possible to combine class with n:class.');
}
return $writer->write('if ($_tmp = array_filter(%node.array)) echo \' class="\', %escape(implode(" ", array_unique($_tmp))), \'"\'');
}
/**
* n:attr="..."
*/
public function macroAttr(MacroNode $node, PhpWriter $writer)
{
return $writer->write('$_tmp = %node.array; echo LR\Filters::htmlAttributes(isset($_tmp[0]) && is_array($_tmp[0]) ? $_tmp[0] : $_tmp);');
}
/**
* {dump ...}
*/
public function macroDump(MacroNode $node, PhpWriter $writer)
{
if ($node->modifiers) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
}
$args = $writer->formatArgs();
return $writer->write(
'Tracy\Debugger::barDump(' . ($args ? "($args)" : 'get_defined_vars()'). ', %var);',
$args ?: 'variables'
);
}
/**
* {debugbreak ...}
*/
public function macroDebugbreak(MacroNode $node, PhpWriter $writer)
{
if ($node->modifiers) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
}
if (function_exists($func = 'debugbreak') || function_exists($func = 'xdebug_break')) {
return $writer->write($node->args == NULL ? "$func()" : "if (%node.args) $func();");
}
}
/**
* {var ...}
* {default ...}
*/
public function macroVar(MacroNode $node, PhpWriter $writer)
{
if ($node->modifiers) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
}
if ($node->args === '' && $node->parentNode && $node->parentNode->name === 'switch') {
return '} else {';
}
$var = TRUE;
$tokens = $writer->preprocess();
$res = new Latte\MacroTokens;
while ($tokens->nextToken()) {
if ($var && $tokens->isCurrent($tokens::T_SYMBOL, $tokens::T_VARIABLE)) {
if ($node->name === 'default') {
$res->append("'" . ltrim($tokens->currentValue(), '$') . "'");
} else {
$res->append('$' . ltrim($tokens->currentValue(), '$'));
}
$var = NULL;
} elseif ($tokens->isCurrent('=', '=>') && $tokens->depth === 0) {
$res->append($node->name === 'default' ? '=>' : '=');
$var = FALSE;
} elseif ($tokens->isCurrent(',') && $tokens->depth === 0) {
if ($var === NULL) {
$res->append($node->name === 'default' ? '=>NULL' : '=NULL');
}
$res->append($node->name === 'default' ? ',' : ';');
$var = TRUE;
} elseif ($var === NULL && $node->name === 'default' && !$tokens->isCurrent($tokens::T_WHITESPACE)) {
throw new CompileException("Unexpected '{$tokens->currentValue()}' in {default $node->args}");
} else {
$res->append($tokens->currentToken());
}
}
if ($var === NULL) {
$res->append($node->name === 'default' ? '=>NULL' : '=NULL');
}
$out = $writer->quotingPass($res)->joinAll();
return $node->name === 'default' ? "extract([$out], EXTR_SKIP)" : "$out;";
}
/**
* {= ...}
* {php ...}
*/
public function macroExpr(MacroNode $node, PhpWriter $writer)
{
if ($node->name === '?') {
trigger_error('Macro {? ...} is deprecated, use {php ...}.', E_USER_DEPRECATED);
}
return $writer->write($node->name === '='
? "echo %modify(%node.args) /* line $node->startLine */"
: '%modify(%node.args);'
);
}
/**
* {contentType ...}
*/
public function macroContentType(MacroNode $node, PhpWriter $writer)
{
if (!$this->getCompiler()->isInHead()
&& !($node->htmlNode && strtolower($node->htmlNode->name) === 'script' && strpos($node->args, 'html') !== FALSE)
) {
throw new CompileException($node->getNotation() . ' is allowed only in template header.');
}
$compiler = $this->getCompiler();
if ($node->modifiers) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
} elseif (strpos($node->args, 'xhtml') !== FALSE) {
$type = $compiler::CONTENT_XHTML;
} elseif (strpos($node->args, 'html') !== FALSE) {
$type = $compiler::CONTENT_HTML;
} elseif (strpos($node->args, 'xml') !== FALSE) {
$type = $compiler::CONTENT_XML;
} elseif (strpos($node->args, 'javascript') !== FALSE) {
$type = $compiler::CONTENT_JS;
} elseif (strpos($node->args, 'css') !== FALSE) {
$type = $compiler::CONTENT_CSS;
} elseif (strpos($node->args, 'calendar') !== FALSE) {
$type = $compiler::CONTENT_ICAL;
} else {
$type = $compiler::CONTENT_TEXT;
}
$compiler->setContentType($type);
if (strpos($node->args, '/') && !$node->htmlNode) {
return $writer->write('if (empty($this->global->coreCaptured) && in_array($this->getReferenceType(), ["extends", NULL], TRUE)) header(%var);', "Content-Type: $node->args");
}
}
/**
* {status ...}
*/
public function macroStatus(MacroNode $node, PhpWriter $writer)
{
trigger_error('Macro {status} is deprecated.', E_USER_DEPRECATED);
if ($node->modifiers) {
throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
}
return $writer->write((substr($node->args, -1) === '?' ? 'if (!headers_sent()) ' : '') .
'http_response_code(%0.var);', (int) $node->args
);
}
}
src/Latte/Strict.php 0000666 00000004512 13436747734 0010415 0 ustar 00 getMethods(\ReflectionMethod::IS_PUBLIC);
$hint = ($t = Helpers::getSuggestion($items, $name)) ? ", did you mean $t()?" : '.';
throw new LogicException("Call to undefined method $class::$name()$hint");
}
/**
* Call to undefined static method.
* @throws LogicException
*/
public static function __callStatic($name, $args)
{
$rc = new \ReflectionClass(get_called_class());
$items = array_intersect($rc->getMethods(\ReflectionMethod::IS_PUBLIC), $rc->getMethods(\ReflectionMethod::IS_STATIC));
$hint = ($t = Helpers::getSuggestion($items, $name)) ? ", did you mean $t()?" : '.';
throw new LogicException("Call to undefined static method {$rc->getName()}::$name()$hint");
}
/**
* Access to undeclared property.
* @throws LogicException
*/
public function &__get($name)
{
$rc = new \ReflectionClass($this);
$items = array_diff($rc->getProperties(\ReflectionProperty::IS_PUBLIC), $rc->getProperties(\ReflectionProperty::IS_STATIC));
$hint = ($t = Helpers::getSuggestion($items, $name)) ? ", did you mean $$t?" : '.';
throw new LogicException("Attempt to read undeclared property {$rc->getName()}::$$name$hint");
}
/**
* Access to undeclared property.
* @throws LogicException
*/
public function __set($name, $value)
{
$rc = new \ReflectionClass($this);
$items = array_diff($rc->getProperties(\ReflectionProperty::IS_PUBLIC), $rc->getProperties(\ReflectionProperty::IS_STATIC));
$hint = ($t = Helpers::getSuggestion($items, $name)) ? ", did you mean $$t?" : '.';
throw new LogicException("Attempt to write to undeclared property {$rc->getName()}::$$name$hint");
}
/**
* @return bool
*/
public function __isset($name)
{
return FALSE;
}
/**
* Access to undeclared property.
* @throws LogicException
*/
public function __unset($name)
{
$class = get_class($this);
throw new LogicException("Attempt to unset undeclared property $class::$$name.");
}
}
src/Latte/Engine.php 0000666 00000016531 13436747734 0010356 0 ustar 00 filters = new Runtime\FilterExecutor;
}
/**
* Renders template to output.
* @return void
*/
public function render($name, array $params = [], $block = NULL)
{
$this->createTemplate($name, $params + ['_renderblock' => $block])
->render();
}
/**
* Renders template to string.
* @return string
*/
public function renderToString($name, array $params = [], $block = NULL)
{
$template = $this->createTemplate($name, $params + ['_renderblock' => $block]);
return $template->capture([$template, 'render']);
}
/**
* Creates template object.
* @return Runtime\Template
*/
public function createTemplate($name, array $params = [])
{
$class = $this->getTemplateClass($name);
if (!class_exists($class, FALSE)) {
$this->loadTemplate($name);
}
return new $class($this, $params, $this->filters, $this->providers, $name);
}
/**
* Compiles template to PHP code.
* @return string
*/
public function compile($name)
{
foreach ($this->onCompile ?: [] as $cb) {
call_user_func(Helpers::checkCallback($cb), $this);
}
$this->onCompile = [];
$source = $this->getLoader()->getContent($name);
try {
$tokens = $this->getParser()->setContentType($this->contentType)
->parse($source);
$code = $this->getCompiler()->setContentType($this->contentType)
->compile($tokens, $this->getTemplateClass($name));
} catch (\Exception $e) {
if (!$e instanceof CompileException) {
$e = new CompileException("Thrown exception '{$e->getMessage()}'", NULL, $e);
}
$line = isset($tokens) ? $this->getCompiler()->getLine() : $this->getParser()->getLine();
throw $e->setSource($source, $line, $name);
}
if (!preg_match('#\n|\?#', $name)) {
$code = "" . $code;
}
$code = PhpHelpers::reformatCode($code);
return $code;
}
/**
* Compiles template to cache.
* @param string
* @return void
* @throws \LogicException
*/
public function warmupCache($name)
{
if (!$this->tempDirectory) {
throw new \LogicException('Path to temporary directory is not set.');
}
$class = $this->getTemplateClass($name);
if (!class_exists($class, FALSE)) {
$this->loadTemplate($name);
}
}
/**
* @return void
*/
private function loadTemplate($name)
{
if (!$this->tempDirectory) {
$code = $this->compile($name);
if (@eval('?>' . $code) === FALSE) { // @ is escalated to exception
throw (new CompileException('Error in template: ' . error_get_last()['message']))
->setSource($code, error_get_last()['line'], "$name (compiled)");
}
return;
}
$file = $this->getCacheFile($name);
if (!$this->isExpired($file, $name) && (@include $file) !== FALSE) { // @ - file may not exist
return;
}
if (!is_dir($this->tempDirectory)) {
@mkdir($this->tempDirectory); // @ - directory may already exist
}
$handle = fopen("$file.lock", 'c+');
if (!$handle || !flock($handle, LOCK_EX)) {
throw new \RuntimeException("Unable to acquire exclusive lock '$file.lock'.");
}
if (!is_file($file) || $this->isExpired($file, $name)) {
$code = $this->compile($name);
if (file_put_contents("$file.tmp", $code) !== strlen($code) || !rename("$file.tmp", $file)) {
@unlink("$file.tmp"); // @ - file may not exist
throw new \RuntimeException("Unable to create '$file'.");
} elseif (function_exists('opcache_invalidate')) {
@opcache_invalidate($file, TRUE); // @ can be restricted
}
}
if ((include $file) === FALSE) {
throw new \RuntimeException("Unable to load '$file'.");
}
flock($handle, LOCK_UN);
fclose($handle);
@unlink("$file.lock"); // @ file may become locked on Windows
}
/**
* @param string
* @param string
* @return bool
*/
private function isExpired($file, $name)
{
return $this->autoRefresh && $this->getLoader()->isExpired($name, (int) @filemtime($file)); // @ - file may not exist
}
/**
* @return string
*/
public function getCacheFile($name)
{
$hash = substr($this->getTemplateClass($name), 8);
$base = preg_match('#([/\\\\][\w@.-]{3,35}){1,3}\z#', $name, $m)
? preg_replace('#[^\w@.-]+#', '-', substr($m[0], 1)) . '--'
: '';
return "$this->tempDirectory/$base$hash.php";
}
/**
* @return string
*/
public function getTemplateClass($name)
{
$key = $this->getLoader()->getUniqueId($name) . "\00" . self::VERSION;
return 'Template' . substr(md5($key), 0, 10);
}
/**
* Registers run-time filter.
* @param string|NULL
* @param callable
* @return static
*/
public function addFilter($name, $callback)
{
$this->filters->add($name, $callback);
return $this;
}
/**
* Returns all run-time filters.
* @return string[]
*/
public function getFilters()
{
return $this->filters->getAll();
}
/**
* Call a run-time filter.
* @param string filter name
* @param array arguments
* @return mixed
*/
public function invokeFilter($name, array $args)
{
return call_user_func_array($this->filters->$name, $args);
}
/**
* Adds new macro.
* @return static
*/
public function addMacro($name, IMacro $macro)
{
$this->getCompiler()->addMacro($name, $macro);
return $this;
}
/**
* Adds new provider.
* @return static
*/
public function addProvider($name, $value)
{
$this->providers[$name] = $value;
return $this;
}
/**
* Returns all providers.
* @return array
*/
public function getProviders()
{
return $this->providers;
}
/**
* @return static
*/
public function setContentType($type)
{
$this->contentType = $type;
return $this;
}
/**
* Sets path to temporary directory.
* @return static
*/
public function setTempDirectory($path)
{
$this->tempDirectory = $path;
return $this;
}
/**
* Sets auto-refresh mode.
* @return static
*/
public function setAutoRefresh($on = TRUE)
{
$this->autoRefresh = (bool) $on;
return $this;
}
/**
* @return Parser
*/
public function getParser()
{
if (!$this->parser) {
$this->parser = new Parser;
}
return $this->parser;
}
/**
* @return Compiler
*/
public function getCompiler()
{
if (!$this->compiler) {
$this->compiler = new Compiler;
Macros\CoreMacros::install($this->compiler);
Macros\BlockMacros::install($this->compiler);
}
return $this->compiler;
}
/**
* @return static
*/
public function setLoader(ILoader $loader)
{
$this->loader = $loader;
return $this;
}
/**
* @return ILoader
*/
public function getLoader()
{
if (!$this->loader) {
$this->loader = new Loaders\FileLoader;
}
return $this->loader;
}
}
src/Latte/Compiler/HtmlNode.php 0000666 00000001621 13436747734 0012427 0 ustar 00 name = $name;
$this->parentNode = $parentNode;
$this->isEmpty = &$this->empty;
}
}
src/Latte/Compiler/Token.php 0000666 00000002516 13436747734 0012001 0 ustar 00 ? used for types MACRO_TAG, HTML_TAG_BEGIN */
public $closing;
/** @var bool is tag empty {name/}? used for type MACRO_TAG */
public $empty;
}
src/Latte/Compiler/Parser.php 0000666 00000030637 13436747734 0012162 0 ustar 00 ['\{(?![\s\'"{}])', '\}'], // {...}
'double' => ['\{\{(?![\s\'"{}])', '\}\}'], // {{...}}
'off' => ['\{(?=/syntax\})', '\}'], // {/syntax}
];
/** @var string[] */
private $delimiters;
/** @var string source template */
private $input;
/** @var Token[] */
private $output;
/** @var int position on source template */
private $offset;
/** @var array */
private $context = [self::CONTEXT_HTML_TEXT, NULL];
/** @var string */
private $lastHtmlTag;
/** @var string used by filter() */
private $syntaxEndTag;
/** @var int */
private $syntaxEndLevel = 0;
/** @var bool */
private $xmlMode;
/** @internal states */
const
CONTEXT_NONE = 'none',
CONTEXT_MACRO = 'macro',
CONTEXT_HTML_TEXT = 'htmlText',
CONTEXT_HTML_TAG = 'htmlTag',
CONTEXT_HTML_ATTRIBUTE = 'htmlAttribute',
CONTEXT_HTML_COMMENT = 'htmlComment',
CONTEXT_HTML_CDATA = 'htmlCData';
/**
* Process all {macros} and .
* @param string
* @return Token[]
*/
public function parse($input)
{
if (Helpers::startsWith($input, "\xEF\xBB\xBF")) { // BOM
$input = substr($input, 3);
}
$this->input = $input = str_replace("\r\n", "\n", $input);
$this->offset = 0;
$this->output = [];
if (!preg_match('##u', $input)) {
preg_match('#(?:[\x00-\x7F]|[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3})*+#A', $input, $m);
$this->offset = strlen($m[0]) + 1;
throw new \InvalidArgumentException('Template is not valid UTF-8 stream.');
}
$this->setSyntax($this->defaultSyntax);
$this->lastHtmlTag = $this->syntaxEndTag = NULL;
$tokenCount = 0;
while ($this->offset < strlen($input)) {
if ($this->{'context' . $this->context[0]}() === FALSE) {
break;
}
while ($tokenCount < count($this->output)) {
$this->filter($this->output[$tokenCount++]);
}
}
if ($this->context[0] === self::CONTEXT_MACRO) {
throw new CompileException('Malformed macro');
}
if ($this->offset < strlen($input)) {
$this->addToken(Token::TEXT, substr($this->input, $this->offset));
}
return $this->output;
}
/**
* Handles CONTEXT_HTML_TEXT.
*/
private function contextHtmlText()
{
$matches = $this->match('~
(?:(?<=\n|^)[ \t]*)?<(?P/?)(?P[a-z][a-z0-9:]*)| ## begin of HTML tag !(?:--(?!>))?|\?(?!=|php))| ## begin of ' . $this->delimiters[0] . ')
~xsi');
if (!empty($matches['htmlcomment'])) { // addToken(Token::HTML_TAG_BEGIN, $matches[0]);
$end = $matches['htmlcomment'] === '!--' ? '--' : ($matches['htmlcomment'] === '?' && $this->xmlMode ? '\?' : '');
$this->setContext(self::CONTEXT_HTML_COMMENT, $end);
} elseif (!empty($matches['tag'])) { // addToken(Token::HTML_TAG_BEGIN, $matches[0]);
$token->name = $matches['tag'];
$token->closing = (bool) $matches['closing'];
$this->lastHtmlTag = $matches['closing'] . strtolower($matches['tag']);
$this->setContext(self::CONTEXT_HTML_TAG);
} else {
return $this->processMacro($matches);
}
}
/**
* Handles CONTEXT_HTML_CDATA.
*/
private function contextHtmlCData()
{
$matches = $this->match('~
(?P' . $this->lastHtmlTag . ')(?![a-z0-9:])| ## end HTML tag ' . $this->delimiters[0] . ')
~xsi');
if (!empty($matches['tag'])) { // addToken(Token::HTML_TAG_BEGIN, $matches[0]);
$token->name = $this->lastHtmlTag;
$token->closing = TRUE;
$this->lastHtmlTag = '/' . $this->lastHtmlTag;
$this->setContext(self::CONTEXT_HTML_TAG);
} else {
return $this->processMacro($matches);
}
}
/**
* Handles CONTEXT_HTML_TAG.
*/
private function contextHtmlTag()
{
$matches = $this->match('~
(?P\s?/?>)([ \t]*\n)?| ## end of HTML tag
(?P' . $this->delimiters[0] . ')|
\s*(?P[^\s"\'>/={]+)(?:\s*=\s*(?P["\']|[^\s"\'=<>`{]+))? ## beginning of HTML attribute
~xsi');
if (!empty($matches['end'])) { // end of HTML tag />
$this->addToken(Token::HTML_TAG_END, $matches[0]);
$this->setContext(!$this->xmlMode && in_array($this->lastHtmlTag, ['script', 'style'], TRUE) ? self::CONTEXT_HTML_CDATA : self::CONTEXT_HTML_TEXT);
} elseif (isset($matches['attr']) && $matches['attr'] !== '') { // HTML attribute
$token = $this->addToken(Token::HTML_ATTRIBUTE_BEGIN, $matches[0]);
$token->name = $matches['attr'];
$token->value = isset($matches['value']) ? $matches['value'] : '';
if ($token->value === '"' || $token->value === "'") { // attribute = "'
if (Helpers::startsWith($token->name, self::N_PREFIX)) {
$token->value = '';
if ($m = $this->match('~(.*?)' . $matches['value'] . '~xsi')) {
$token->value = $m[1];
$token->text .= $m[0];
}
} else {
$this->setContext(self::CONTEXT_HTML_ATTRIBUTE, $matches['value']);
}
}
} else {
return $this->processMacro($matches);
}
}
/**
* Handles CONTEXT_HTML_ATTRIBUTE.
*/
private function contextHtmlAttribute()
{
$matches = $this->match('~
(?P' . $this->context[1] . ')| ## end of HTML attribute
(?P' . $this->delimiters[0] . ')
~xsi');
if (!empty($matches['quote'])) { // (attribute end) '"
$this->addToken(Token::HTML_ATTRIBUTE_END, $matches[0]);
$this->setContext(self::CONTEXT_HTML_TAG);
} else {
return $this->processMacro($matches);
}
}
/**
* Handles CONTEXT_HTML_COMMENT.
*/
private function contextHtmlComment()
{
$matches = $this->match('~
(?P' . $this->context[1] . '>)| ## end of HTML comment
(?P' . $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);
}
}
/**
* Handles CONTEXT_NONE.
*/
private function contextNone()
{
$matches = $this->match('~
(?P' . $this->delimiters[0] . ')
~xsi');
return $this->processMacro($matches);
}
/**
* Handles CONTEXT_MACRO.
*/
private function contextMacro()
{
$matches = $this->match('~
(?P\\*.*?\\*' . $this->delimiters[1] . '\n{0,2})|
(?P(?>
' . self::RE_STRING . '|
\{(?>' . self::RE_STRING . '|[^\'"{}])*+\}|
[^\'"{}]+
)++)
' . $this->delimiters[1] . '
(?P[ \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, $token->closing) = $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');
}
}
private function processMacro($matches)
{
if (!empty($matches['macro'])) { // {macro} or {* *}
$this->setContext(self::CONTEXT_MACRO, [$this->context, $matches['macro']]);
} else {
return FALSE;
}
}
/**
* Matches next token.
* @param string
* @return array
*/
private function match($re)
{
if (!preg_match($re, $this->input, $matches, PREG_OFFSET_CAPTURE, $this->offset)) {
if (preg_last_error()) {
throw new RegexpException(NULL, preg_last_error());
}
return [];
}
$value = substr($this->input, $this->offset, $matches[0][1] - $this->offset);
if ($value !== '') {
$this->addToken(Token::TEXT, $value);
}
$this->offset = $matches[0][1] + strlen($matches[0][0]);
foreach ($matches as $k => $v) {
$matches[$k] = $v[0];
}
return $matches;
}
/**
* @param string Parser::CONTENT_HTML, CONTENT_XHTML, CONTENT_XML or CONTENT_TEXT
* @return static
*/
public function setContentType($type)
{
if (in_array($type, [self::CONTENT_HTML, self::CONTENT_XHTML, self::CONTENT_XML], TRUE)) {
$this->setContext(self::CONTEXT_HTML_TEXT);
$this->xmlMode = $type === self::CONTENT_XML;
} else {
$this->setContext(self::CONTEXT_NONE);
}
return $this;
}
/**
* @return static
*/
public function setContext($context, $quote = NULL)
{
$this->context = [$context, $quote];
return $this;
}
/**
* Changes macro tag delimiters.
* @param string
* @return static
*/
public function setSyntax($type)
{
$type = $type ?: $this->defaultSyntax;
if (isset($this->syntaxes[$type])) {
$this->setDelimiters($this->syntaxes[$type][0], $this->syntaxes[$type][1]);
} else {
throw new \InvalidArgumentException("Unknown syntax '$type'");
}
return $this;
}
/**
* Changes macro tag delimiters.
* @param string left regular expression
* @param string right regular expression
* @return static
*/
public function setDelimiters($left, $right)
{
$this->delimiters = [$left, $right];
return $this;
}
/**
* Parses macro tag to name, arguments a modifiers parts.
* @param string {name arguments | modifiers}
* @return array|NULL
* @internal
*/
public function parseMacroTag($tag)
{
if (!preg_match('~^
(?P/?)
(
(?P\?|[a-z]\w*+(?:[.:]\w+)*+(?!::|\(|\\\\))| ## ?, name, /name, but not function( or class:: or namespace\
(?P!?)(?P[=\~#%^&_]?) ## !expression, !=expression, ...
)(?P(?:' . self::RE_STRING . '|[^\'"])*?)
(?P(?(?:' . self::RE_STRING . '|(?:\((?P>modArgs)\))|[^\'"/()]|/(?=.))*+))?
(?P/?\z)
()\z~isx', $tag, $match)) {
if (preg_last_error()) {
throw new RegexpException(NULL, preg_last_error());
}
return NULL;
}
if ($match['name'] === '') {
$match['name'] = $match['shortname'] ?: ($match['closing'] ? '' : '=');
if ($match['noescape']) {
trigger_error("The noescape shortcut {!...} is deprecated, use {...|noescape} modifier on line {$this->getLine()}.", E_USER_DEPRECATED);
$match['modifiers'] .= '|noescape';
}
}
return [$match['name'], trim($match['args']), $match['modifiers'], (bool) $match['empty'], (bool) $match['closing']];
}
private function addToken($type, $text)
{
$this->output[] = $token = new Token;
$token->type = $type;
$token->text = $text;
$token->line = $this->getLine() - substr_count(ltrim($text), "\n");
return $token;
}
public function getLine()
{
return $this->offset
? substr_count(substr($this->input, 0, $this->offset - 1), "\n") + 1
: 1;
}
/**
* Process low-level macros.
*/
protected function filter(Token $token)
{
if ($token->type === Token::MACRO_TAG && $token->name === '/syntax') {
$this->setSyntax($this->defaultSyntax);
$token->type = Token::COMMENT;
} elseif ($token->type === Token::MACRO_TAG && $token->name === 'syntax') {
$this->setSyntax($token->value);
$token->type = Token::COMMENT;
} elseif ($token->type === Token::HTML_ATTRIBUTE_BEGIN && $token->name === 'n:syntax') {
$this->setSyntax($token->value);
$this->syntaxEndTag = $this->lastHtmlTag;
$this->syntaxEndLevel = 1;
$token->type = Token::COMMENT;
} elseif ($token->type === Token::HTML_TAG_BEGIN && $this->lastHtmlTag === $this->syntaxEndTag) {
$this->syntaxEndLevel++;
} elseif ($token->type === Token::HTML_TAG_END && $this->lastHtmlTag === ('/' . $this->syntaxEndTag) && --$this->syntaxEndLevel === 0) {
$this->setSyntax($this->defaultSyntax);
} elseif ($token->type === Token::MACRO_TAG && $token->name === 'contentType') {
if (strpos($token->value, 'html') !== FALSE) {
$this->setContentType(self::CONTENT_HTML);
} elseif (strpos($token->value, 'xml') !== FALSE) {
$this->setContentType(self::CONTENT_XML);
} else {
$this->setContentType(self::CONTENT_TEXT);
}
}
}
}
src/Latte/Compiler/PhpWriter.php 0000666 00000036136 13436747734 0012652 0 ustar 00 tokenizer, NULL, $node->context);
$me->modifiers = &$node->modifiers;
return $me;
}
public function __construct(MacroTokens $tokens, $modifiers = NULL, array $context = NULL)
{
$this->tokens = $tokens;
$this->modifiers = $modifiers;
$this->context = $context;
}
/**
* Expands %node.word, %node.array, %node.args, %escape(), %modify(), %var, %raw, %word in code.
* @param string
* @return string
*/
public function write($mask)
{
$mask = preg_replace('#%(node|\d+)\.#', '%$1_', $mask);
$mask = preg_replace_callback('#%escape(\(([^()]*+|(?1))+\))#', function ($m) {
return $this->escapePass(new MacroTokens(substr($m[1], 1, -1)))->joinAll();
}, $mask);
$mask = preg_replace_callback('#%modify(Content)?(\(([^()]*+|(?2))+\))#', function ($m) {
return $this->formatModifiers(substr($m[2], 1, -1), (bool) $m[1]);
}, $mask);
$args = func_get_args();
$pos = $this->tokens->position;
$word = strpos($mask, '%node_word') === FALSE ? NULL : $this->tokens->fetchWord();
$code = preg_replace_callback('#([,+]\s*)?%(node_|\d+_|)(word|var|raw|array|args)(\?)?(\s*\+\s*)?()#',
function ($m) use ($word, &$args) {
list(, $l, $source, $format, $cond, $r) = $m;
switch ($source) {
case 'node_':
$arg = $word; break;
case '':
$arg = next($args); break;
default:
$arg = $args[(int) $source + 1]; break;
}
switch ($format) {
case 'word':
$code = $this->formatWord($arg); break;
case 'args':
$code = $this->formatArgs(); break;
case 'array':
$code = $this->formatArray();
$code = $cond && $code === '[]' ? '' : $code; break;
case 'var':
$code = var_export($arg, TRUE); break;
case 'raw':
$code = (string) $arg; break;
}
if ($cond && $code === '') {
return $r ? $l : $r;
} else {
return $l . $code . $r;
}
}, $mask);
$this->tokens->position = $pos;
return $code;
}
/**
* Formats modifiers calling.
* @param string
* @return string
*/
public function formatModifiers($var, $isContent = FALSE)
{
$tokens = new MacroTokens(ltrim($this->modifiers, '|'));
$tokens = $this->preprocess($tokens);
$tokens = $this->modifierPass($tokens, $var, $isContent);
$tokens = $this->quotingPass($tokens);
return $tokens->joinAll();
}
/**
* Formats macro arguments to PHP code. (It advances tokenizer to the end as a side effect.)
* @return string
*/
public function formatArgs(MacroTokens $tokens = NULL)
{
$tokens = $this->preprocess($tokens);
$tokens = $this->quotingPass($tokens);
return $tokens->joinAll();
}
/**
* Formats macro arguments to PHP array. (It advances tokenizer to the end as a side effect.)
* @return string
*/
public function formatArray(MacroTokens $tokens = NULL)
{
$tokens = $this->preprocess($tokens);
$tokens = $this->expandCastPass($tokens);
$tokens = $this->quotingPass($tokens);
return $tokens->joinAll();
}
/**
* Formats parameter to PHP string.
* @param string
* @return string
*/
public function formatWord($s)
{
return (is_numeric($s) || preg_match('#^\$|[\'"]|^(true|TRUE)\z|^(false|FALSE)\z|^(null|NULL)\z|^[\w\\\\]{3,}::[A-Z0-9_]{2,}\z#', $s))
? $this->formatArgs(new MacroTokens($s))
: '"' . $s . '"';
}
/**
* Preprocessor for tokens. (It advances tokenizer to the end as a side effect.)
* @return MacroTokens
*/
public function preprocess(MacroTokens $tokens = NULL)
{
$tokens = $tokens === NULL ? $this->tokens : $tokens;
$this->validateTokens($tokens);
$tokens = $this->removeCommentsPass($tokens);
$tokens = $this->shortTernaryPass($tokens);
$tokens = $this->inlineModifierPass($tokens);
$tokens = $this->inOperatorPass($tokens);
return $tokens;
}
/**
* @throws CompileException
* @return void
*/
public function validateTokens(MacroTokens $tokens)
{
$deprecatedVars = array_flip(['$template', '$_b', '$_l', '$_g', '$_args', '$_fi', '$_control', '$_presenter', '$_form', '$_input', '$_label', '$_snippetMode']);
$brackets = [];
$pos = $tokens->position;
while ($tokens->nextToken()) {
if ($tokens->isCurrent('?>')) {
throw new CompileException('Forbidden ?> inside macro');
} elseif ($tokens->isCurrent($tokens::T_VARIABLE) && isset($deprecatedVars[$tokens->currentValue()])) {
trigger_error("Variable {$tokens->currentValue()} is deprecated.", E_USER_DEPRECATED);
} elseif ($tokens->isCurrent($tokens::T_SYMBOL)
&& !$tokens->isPrev('::') && !$tokens->isNext('::') && !$tokens->isPrev('->') && !$tokens->isNext('\\')
&& preg_match('#^[A-Z0-9]{3,}$#', $val = $tokens->currentValue())
) {
trigger_error("Replace literal $val with constant('$val')", E_USER_DEPRECATED);
} elseif ($tokens->isCurrent('(', '[', '{')) {
static $counterpart = ['(' => ')', '[' => ']', '{' => '}'];
$brackets[] = $counterpart[$tokens->currentValue()];
} elseif ($tokens->isCurrent(')', ']', '}') && $tokens->currentValue() !== array_pop($brackets)) {
throw new CompileException('Unexpected ' . $tokens->currentValue());
} elseif ($tokens->isCurrent('function', 'class', 'interface', 'trait') && $tokens->isNext($tokens::T_SYMBOL, '&')
|| $tokens->isCurrent('return', 'yield') && !$brackets
) {
throw new CompileException("Forbidden keyword '{$tokens->currentValue()}' inside macro.");
}
}
if ($brackets) {
throw new CompileException('Missing ' . array_pop($brackets));
}
$tokens->position = $pos;
}
/**
* Removes PHP comments.
* @return MacroTokens
*/
public function removeCommentsPass(MacroTokens $tokens)
{
$res = new MacroTokens;
while ($tokens->nextToken()) {
if (!$tokens->isCurrent($tokens::T_COMMENT)) {
$res->append($tokens->currentToken());
}
}
return $res;
}
/**
* Simplified ternary expressions without third part.
* @return MacroTokens
*/
public function shortTernaryPass(MacroTokens $tokens)
{
$res = new MacroTokens;
$inTernary = [];
while ($tokens->nextToken()) {
if ($tokens->isCurrent('?')) {
$inTernary[] = $tokens->depth;
} elseif ($tokens->isCurrent(':')) {
array_pop($inTernary);
} elseif ($tokens->isCurrent(',', ')', ']', '|') && end($inTernary) === $tokens->depth + $tokens->isCurrent(')', ']')) {
$res->append(' : NULL');
array_pop($inTernary);
}
$res->append($tokens->currentToken());
}
if ($inTernary) {
$res->append(' : NULL');
}
return $res;
}
/**
* Pseudocast (expand).
* @return MacroTokens
*/
public function expandCastPass(MacroTokens $tokens)
{
$res = new MacroTokens('[');
$expand = NULL;
while ($tokens->nextToken()) {
if ($tokens->isCurrent('(expand)') && $tokens->depth === 0) {
$expand = TRUE;
$res->append('],');
} elseif ($expand && $tokens->isCurrent(',') && !$tokens->depth) {
$expand = FALSE;
$res->append(', [');
} else {
$res->append($tokens->currentToken());
}
}
if ($expand === NULL) {
$res->append(']');
} else {
$res->prepend('array_merge(')->append($expand ? ', [])' : '])');
}
return $res;
}
/**
* Quotes symbols to strings.
* @return MacroTokens
*/
public function quotingPass(MacroTokens $tokens)
{
$res = new MacroTokens;
while ($tokens->nextToken()) {
$res->append($tokens->isCurrent($tokens::T_SYMBOL)
&& (!$tokens->isPrev() || $tokens->isPrev(',', '(', '[', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', '=', 'and', 'or', 'xor', '??'))
&& (!$tokens->isNext() || $tokens->isNext(',', ';', ')', ']', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', 'and', 'or', 'xor', '??'))
&& !preg_match('#^[A-Z_][A-Z0-9_]{2,}$#', $tokens->currentValue())
? "'" . $tokens->currentValue() . "'"
: $tokens->currentToken()
);
}
return $res;
}
/**
* Syntax $entry in [item1, item2].
* @return MacroTokens
*/
public function inOperatorPass(MacroTokens $tokens)
{
while ($tokens->nextToken()) {
if ($tokens->isCurrent($tokens::T_VARIABLE)) {
$start = $tokens->position;
$depth = $tokens->depth;
$expr = $arr = [];
$expr[] = $tokens->currentToken();
while ($tokens->isNext($tokens::T_VARIABLE, $tokens::T_SYMBOL, $tokens::T_NUMBER, $tokens::T_STRING, '[', ']', '(', ')', '->')
&& !$tokens->isNext('in')) {
$expr[] = $tokens->nextToken();
}
if ($depth === $tokens->depth && $tokens->nextValue('in') && ($arr[] = $tokens->nextToken('['))) {
while ($tokens->isNext()) {
$arr[] = $tokens->nextToken();
if ($tokens->isCurrent(']') && $tokens->depth === $depth) {
$new = array_merge($tokens->parse('in_array('), $expr, $tokens->parse(', '), $arr, $tokens->parse(', TRUE)'));
array_splice($tokens->tokens, $start, $tokens->position - $start + 1, $new);
$tokens->position = $start + count($new) - 1;
continue 2;
}
}
}
$tokens->position = $start;
}
}
return $tokens->reset();
}
/**
* Process inline filters ($var|filter)
* @return MacroTokens
*/
public function inlineModifierPass(MacroTokens $tokens)
{
$result = new MacroTokens;
while ($tokens->nextToken()) {
if ($tokens->isCurrent('(', '[')) {
$result->tokens = array_merge($result->tokens, $this->inlineModifierInner($tokens));
} else {
$result->append($tokens->currentToken());
}
}
return $result;
}
private function inlineModifierInner(MacroTokens $tokens)
{
$isFunctionOrArray = $tokens->isPrev($tokens::T_VARIABLE, $tokens::T_SYMBOL) || $tokens->isCurrent('[');
$result = new MacroTokens;
$args = new MacroTokens;
$modifiers = new MacroTokens;
$current = $args;
$anyModifier = FALSE;
$result->append($tokens->currentToken());
while ($tokens->nextToken()) {
if ($tokens->isCurrent('(', '[')) {
$current->tokens = array_merge($current->tokens, $this->inlineModifierInner($tokens));
} elseif ($current !== $modifiers && $tokens->isCurrent('|')) {
$anyModifier = TRUE;
$current = $modifiers;
} elseif ($tokens->isCurrent(')', ']') || ($isFunctionOrArray && $tokens->isCurrent(','))) {
$partTokens = count($modifiers->tokens)
? $this->modifierPass($modifiers, $args->tokens)->tokens
: $args->tokens;
$result->tokens = array_merge($result->tokens, $partTokens);
if ($tokens->isCurrent(',')) {
$result->append($tokens->currentToken());
$args = new MacroTokens;
$modifiers = new MacroTokens;
$current = $args;
continue;
} elseif ($isFunctionOrArray || !$anyModifier) {
$result->append($tokens->currentToken());
} else {
array_shift($result->tokens);
}
return $result->tokens;
} else {
$current->append($tokens->currentToken());
}
}
throw new CompileException('Unbalanced brackets.');
}
/**
* Formats modifiers calling.
* @param MacroTokens
* @param string|array
* @throws CompileException
* @return MacroTokens
*/
public function modifierPass(MacroTokens $tokens, $var, $isContent = FALSE)
{
$inside = FALSE;
$res = new MacroTokens($var);
while ($tokens->nextToken()) {
if ($tokens->isCurrent($tokens::T_WHITESPACE)) {
$res->append(' ');
} elseif ($inside) {
if ($tokens->isCurrent(':', ',')) {
$res->append(', ');
$tokens->nextAll($tokens::T_WHITESPACE);
} elseif ($tokens->isCurrent('|')) {
$res->append(')');
$inside = FALSE;
} else {
$res->append($tokens->currentToken());
}
} else {
if ($tokens->isCurrent($tokens::T_SYMBOL)) {
if ($tokens->isCurrent('escape')) {
if ($isContent) {
$res->prepend('LR\Filters::convertTo($_fi, ' . var_export(implode($this->context), TRUE) . ', ')
->append(')');
} else {
$res = $this->escapePass($res);
}
$tokens->nextToken('|');
} elseif (!strcasecmp($tokens->currentValue(), 'checkurl')) {
$res->prepend('LR\Filters::safeUrl(');
$inside = TRUE;
} else {
$name = strtolower($tokens->currentValue());
$res->prepend($isContent
? '$this->filters->filterContent('. var_export($name, TRUE) . ', $_fi, '
: 'call_user_func($this->filters->' . $name . ', '
);
$inside = TRUE;
}
} else {
throw new CompileException("Modifier name must be alphanumeric string, '{$tokens->currentValue()}' given.");
}
}
}
if ($inside) {
$res->append(')');
}
return $res;
}
/**
* Escapes expression in tokens.
* @return MacroTokens
*/
public function escapePass(MacroTokens $tokens)
{
$tokens = clone $tokens;
list($contentType, $context) = $this->context;
switch ($contentType) {
case Compiler::CONTENT_XHTML:
case Compiler::CONTENT_HTML:
switch ($context) {
case Compiler::CONTEXT_HTML_TEXT:
return $tokens->prepend('LR\Filters::escapeHtmlText(')->append(')');
case Compiler::CONTEXT_HTML_TAG:
case Compiler::CONTEXT_HTML_ATTRIBUTE_UNQUOTED_URL:
return $tokens->prepend('LR\Filters::escapeHtmlAttrUnquoted(')->append(')');
case Compiler::CONTEXT_HTML_ATTRIBUTE:
case Compiler::CONTEXT_HTML_ATTRIBUTE_URL:
return $tokens->prepend('LR\Filters::escapeHtmlAttr(')->append(')');
case Compiler::CONTEXT_HTML_ATTRIBUTE_JS:
return $tokens->prepend('LR\Filters::escapeHtmlAttr(LR\Filters::escapeJs(')->append('))');
case Compiler::CONTEXT_HTML_ATTRIBUTE_CSS:
return $tokens->prepend('LR\Filters::escapeHtmlAttr(LR\Filters::escapeCss(')->append('))');
case Compiler::CONTEXT_HTML_COMMENT:
return $tokens->prepend('LR\Filters::escapeHtmlComment(')->append(')');
case Compiler::CONTEXT_HTML_BOGUS_COMMENT:
return $tokens->prepend('LR\Filters::escapeHtml(')->append(')');
case Compiler::CONTEXT_HTML_JS:
case Compiler::CONTEXT_HTML_CSS:
return $tokens->prepend('LR\Filters::escape' . ucfirst($context) . '(')->append(')');
default:
throw new CompileException("Unknown context $contentType, $context.");
}
case Compiler::CONTENT_XML:
switch ($context) {
case Compiler::CONTEXT_XML_TEXT:
case Compiler::CONTEXT_XML_ATTRIBUTE:
case Compiler::CONTEXT_XML_BOGUS_COMMENT:
return $tokens->prepend('LR\Filters::escapeXml(')->append(')');
case Compiler::CONTEXT_XML_COMMENT:
return $tokens->prepend('LR\Filters::escapeHtmlComment(')->append(')');
case Compiler::CONTEXT_XML_TAG:
return $tokens->prepend('LR\Filters::escapeXmlAttrUnquoted(')->append(')');
default:
throw new CompileException("Unknown context $contentType, $context.");
}
case Compiler::CONTENT_JS:
case Compiler::CONTENT_CSS:
case Compiler::CONTENT_ICAL:
return $tokens->prepend('LR\Filters::escape' . ucfirst($contentType) . '(')->append(')');
case Compiler::CONTENT_TEXT:
return $tokens;
case NULL:
return $tokens->prepend('call_user_func($this->filters->escape, ')->append(')');
default:
throw new CompileException("Unknown context $contentType.");
}
}
}
src/Latte/Compiler/MacroTokens.php 0000666 00000006072 13436747734 0013147 0 ustar 00 parse($input));
$this->ignored = [self::T_COMMENT, self::T_WHITESPACE];
}
public function parse($s)
{
self::$tokenizer = self::$tokenizer ?: new Tokenizer([
self::T_WHITESPACE => '\s+',
self::T_COMMENT => '(?s)/\*.*?\*/',
self::T_STRING => Parser::RE_STRING,
self::T_KEYWORD => '(?:true|false|null|TRUE|FALSE|NULL|INF|NAN|and|or|xor|clone|new|instanceof|return|continue|break)(?![\w\pL_])', // keyword
self::T_CAST => '\((?:expand|string|array|int|integer|float|bool|boolean|object)\)', // type casting
self::T_VARIABLE => '\$[\w\pL_]+',
self::T_NUMBER => '[+-]?[0-9]+(?:\.[0-9]+)?(?:e[0-9]+)?',
self::T_SYMBOL => '[\w\pL_]+(?:-+[\w\pL_]+)*',
self::T_CHAR => '::|=>|->|\+\+|--|<<|>>|<=>|<=|>=|===|!==|==|!=|<>|&&|\|\||\?\?|\?>|\*\*|\.\.\.|[^"\']', // =>, any char except quotes
], 'u');
return self::$tokenizer->tokenize($s);
}
/**
* Appends simple token or string (will be parsed).
* @return static
*/
public function append($val, $position = NULL)
{
if ($val != NULL) { // intentionally @
array_splice(
$this->tokens,
$position === NULL ? count($this->tokens) : $position,
0,
is_array($val) ? [$val] : $this->parse($val)
);
}
return $this;
}
/**
* Prepends simple token or string (will be parsed).
* @return static
*/
public function prepend($val)
{
if ($val != NULL) { // intentionally @
array_splice($this->tokens, 0, 0, is_array($val) ? [$val] : $this->parse($val));
}
return $this;
}
/**
* Reads single token (optionally delimited by comma) from string.
* @return string
*/
public function fetchWord()
{
$words = $this->fetchWords();
return $words ? implode(':', $words) : FALSE;
}
/**
* Reads single tokens delimited by colon from string.
* @return array
*/
public function fetchWords()
{
do {
$words[] = $this->joinUntil(self::T_WHITESPACE, ',', ':');
} while ($this->nextToken(':'));
if (count($words) === 1 && ($space = $this->nextValue(self::T_WHITESPACE))
&& (($dot = $this->nextValue('.')) || $this->isPrev('.')))
{
$words[0] .= $space . $dot . $this->joinUntil(',');
}
$this->nextToken(',');
$this->nextAll(self::T_WHITESPACE, self::T_COMMENT);
return $words === [''] ? [] : $words;
}
public function reset()
{
$this->depth = 0;
return parent::reset();
}
protected function next()
{
parent::next();
if ($this->isCurrent('[', '(', '{')) {
$this->depth++;
} elseif ($this->isCurrent(']', ')', '}')) {
$this->depth--;
}
}
}
src/Latte/Compiler/Compiler.php 0000666 00000055354 13436747734 0012503 0 ustar 00 IMacro[]] */
private $macros;
/** @var int[] IMacro flags */
private $flags;
/** @var HtmlNode */
private $htmlNode;
/** @var MacroNode */
private $macroNode;
/** @var string[] */
private $placeholders = [];
/** @var string */
private $contentType = self::CONTENT_HTML;
/** @var string|NULL */
private $context;
/** @var mixed */
private $lastAttrValue;
/** @var int */
private $tagOffset;
/** @var bool */
private $inHead;
/** @var array of [name => [body, arguments]] */
private $methods = [];
/** @var array of [name => serialized value] */
private $properties = [];
/** Context-aware escaping content types */
const
CONTENT_HTML = Engine::CONTENT_HTML,
CONTENT_XHTML = Engine::CONTENT_XHTML,
CONTENT_XML = Engine::CONTENT_XML,
CONTENT_JS = Engine::CONTENT_JS,
CONTENT_CSS = Engine::CONTENT_CSS,
CONTENT_ICAL = Engine::CONTENT_ICAL,
CONTENT_TEXT = Engine::CONTENT_TEXT;
/** @internal Context-aware escaping HTML contexts */
const
CONTEXT_HTML_TEXT = NULL,
CONTEXT_HTML_TAG = 'Tag',
CONTEXT_HTML_ATTRIBUTE = 'Attr',
CONTEXT_HTML_ATTRIBUTE_JS = 'AttrJs',
CONTEXT_HTML_ATTRIBUTE_CSS = 'AttrCss',
CONTEXT_HTML_ATTRIBUTE_URL = 'AttrUrl',
CONTEXT_HTML_ATTRIBUTE_UNQUOTED_URL = 'AttrUnquotedUrl',
CONTEXT_HTML_COMMENT = 'Comment',
CONTEXT_HTML_BOGUS_COMMENT = 'Bogus',
CONTEXT_HTML_CSS = 'Css',
CONTEXT_HTML_JS = 'Js',
CONTEXT_XML_TEXT = self::CONTEXT_HTML_TEXT,
CONTEXT_XML_TAG = self::CONTEXT_HTML_TAG,
CONTEXT_XML_ATTRIBUTE = self::CONTEXT_HTML_ATTRIBUTE,
CONTEXT_XML_COMMENT = self::CONTEXT_HTML_COMMENT,
CONTEXT_XML_BOGUS_COMMENT = self::CONTEXT_HTML_BOGUS_COMMENT;
/**
* Adds new macro with IMacro flags.
* @param string
* @return static
*/
public function addMacro($name, IMacro $macro, $flags = NULL)
{
if (!isset($this->flags[$name])) {
$this->flags[$name] = $flags ?: IMacro::DEFAULT_FLAGS;
} elseif ($flags && $this->flags[$name] !== $flags) {
throw new \LogicException("Incompatible flags for macro $name.");
}
$this->macros[$name][] = $macro;
return $this;
}
/**
* Compiles tokens to PHP code.
* @param Token[]
* @return string
*/
public function compile(array $tokens, $className)
{
$this->tokens = $tokens;
$output = '';
$this->output = &$output;
$this->inHead = TRUE;
$this->htmlNode = $this->macroNode = $this->context = NULL;
$this->placeholders = $this->properties = [];
$this->methods = ['main' => NULL, 'prepare' => NULL];
$macroHandlers = new \SplObjectStorage;
array_map([$macroHandlers, 'attach'], call_user_func_array('array_merge', $this->macros));
foreach ($macroHandlers as $handler) {
$handler->initialize($this);
}
foreach ($tokens as $this->position => $token) {
if ($this->inHead && !($token->type === $token::COMMENT
|| $token->type === $token::MACRO_TAG && isset($this->flags[$token->name]) && $this->flags[$token->name] & IMacro::ALLOWED_IN_HEAD
|| $token->type === $token::TEXT && trim($token->text) === ''
)) {
$this->inHead = FALSE;
}
$this->{"process$token->type"}($token);
}
while ($this->htmlNode) {
if (!empty($this->htmlNode->macroAttrs)) {
throw new CompileException('Missing ' . self::printEndTag($this->htmlNode));
}
$this->htmlNode = $this->htmlNode->parentNode;
}
while ($this->macroNode) {
if (~$this->flags[$this->macroNode->name] & IMacro::AUTO_CLOSE) {
throw new CompileException('Missing ' . self::printEndTag($this->macroNode));
}
$this->closeMacro($this->macroNode->name);
}
$prepare = $epilogs = '';
foreach ($macroHandlers as $handler) {
$res = $handler->finalize();
$prepare .= empty($res[0]) ? '' : "";
$epilogs = (empty($res[1]) ? '' : "") . $epilogs;
}
$this->addMethod('main', $this->expandTokens("extract(\$this->params);?>\n$output$epilogsaddMethod('prepare', "extract(\$this->params);?>$preparecontentType !== self::CONTENT_HTML) {
$this->addProperty('contentType', $this->contentType);
}
foreach ($this->properties as $name => $value) {
$members[] = "\tpublic $$name = " . PhpHelpers::dump($value) . ';';
}
foreach (array_filter($this->methods) as $name => $method) {
$members[] = "\n\tfunction $name($method[arguments])\n\t{\n" . ($method['body'] ? "\t\t$method[body]\n" : '') . "\t}";
}
return "contentType = $type;
$this->context = NULL;
return $this;
}
/**
* @deprecated
*/
public function getContentType()
{
trigger_error(__METHOD__ . ' is deprecated.', E_USER_DEPRECATED);
return $this->contentType;
}
/**
* @internal
*/
public function setContext($context)
{
trigger_error(__METHOD__ . ' is deprecated.', E_USER_DEPRECATED);
$this->context = $context;
return $this;
}
/**
* @deprecated
*/
public function getContext()
{
trigger_error(__METHOD__ . ' is deprecated.', E_USER_DEPRECATED);
return $this->context;
}
/**
* @return MacroNode|NULL
*/
public function getMacroNode()
{
return $this->macroNode;
}
/**
* Returns current line number.
* @return int|NULL
*/
public function getLine()
{
return isset($this->tokens[$this->position]) ? $this->tokens[$this->position]->line : NULL;
}
/**
* @return bool
*/
public function isInHead()
{
return $this->inHead;
}
/**
* Adds custom method to template.
* @return void
* @internal
*/
public function addMethod($name, $body, $arguments = '')
{
$this->methods[$name] = ['body' => trim($body), 'arguments' => $arguments];
}
/**
* Returns custom methods.
* @return array
* @internal
*/
public function getMethods()
{
return $this->methods;
}
/**
* Adds custom property to template.
* @return void
* @internal
*/
public function addProperty($name, $value)
{
$this->properties[$name] = $value;
}
/**
* Returns custom properites.
* @return array
* @internal
*/
public function getProperties()
{
return $this->properties;
}
/** @internal */
public function expandTokens($s)
{
return strtr($s, $this->placeholders);
}
private function processText(Token $token)
{
if ($this->lastAttrValue === '' && $this->context && Helpers::startsWith($this->context, self::CONTEXT_HTML_ATTRIBUTE)) {
$this->lastAttrValue = $token->text;
}
$this->output .= $this->escape($token->text);
}
private function processMacroTag(Token $token)
{
if ($this->context === self::CONTEXT_HTML_TAG || $this->context && Helpers::startsWith($this->context, self::CONTEXT_HTML_ATTRIBUTE)) {
$this->lastAttrValue = TRUE;
}
$isRightmost = !isset($this->tokens[$this->position + 1])
|| substr($this->tokens[$this->position + 1]->text, 0, 1) === "\n";
if ($token->closing) {
$this->closeMacro($token->name, $token->value, $token->modifiers, $isRightmost);
} else {
if (!$token->empty && isset($this->flags[$token->name]) && $this->flags[$token->name] & IMacro::AUTO_EMPTY) {
$pos = $this->position;
while (($t = isset($this->tokens[++$pos]) ? $this->tokens[$pos] : NULL)
&& ($t->type !== Token::MACRO_TAG || $t->name !== $token->name)
&& ($t->type !== Token::HTML_ATTRIBUTE_BEGIN || $t->name !== Parser::N_PREFIX . $token->name));
$token->empty = $t ? !$t->closing : TRUE;
}
$node = $this->openMacro($token->name, $token->value, $token->modifiers, $isRightmost);
if ($token->empty) {
if ($node->empty) {
throw new CompileException("Unexpected /} in tag {$token->text}");
}
$this->closeMacro($token->name, NULL, NULL, $isRightmost);
}
}
}
private function processHtmlTagBegin(Token $token)
{
if ($token->closing) {
while ($this->htmlNode) {
if (strcasecmp($this->htmlNode->name, $token->name) === 0) {
break;
}
if ($this->htmlNode->macroAttrs) {
throw new CompileException("Unexpected $token->name>, expecting " . self::printEndTag($this->htmlNode));
}
$this->htmlNode = $this->htmlNode->parentNode;
}
if (!$this->htmlNode) {
$this->htmlNode = new HtmlNode($token->name);
}
$this->htmlNode->closing = TRUE;
$this->htmlNode->endLine = $this->getLine();
$this->context = self::CONTEXT_HTML_TEXT;
} elseif ($token->text === '