*/ class Template extends Render { const VAR_NAME = '$var'; const TPL_NAME = '$tpl'; const COMPILE_STAGE_LOADED = 1; const COMPILE_STAGE_PRE_FILTERED = 2; const COMPILE_STAGE_PARSED = 3; const COMPILE_STAGE_PROCESSED = 4; const COMPILE_STAGE_POST_FILTERED = 5; /** * Disable array parser. */ const DENY_ARRAY = 1; /** * Disable modifier parser. */ const DENY_MODS = 2; /** * Allow parse modifiers with term */ const TERM_MODS = 1; /** * Allow parse range with term */ const TERM_RANGE = 1; /** * @var int shared counter */ public $i = 1; /** * @var array of macros */ public $macros = array(); /** * @var array of blocks */ public $blocks = array(); /** * @var string|null */ public $extends; /** * @var string|null */ public $extended; /** * Stack of extended templates * @var array */ public $ext_stack = array(); public $extend_body = false; /** * Parent template * @var Template */ public $parent; /** * Template PHP code * @var string */ private $_body; private $_compile_stage = 0; /** * Call stack * @var Tag[] */ private $_stack = array(); /** * Template source * @var string */ private $_src; /** * @var int */ private $_line = 1; private $_post = array(); /** * @var bool|string */ private $_ignore = false; private $_before = array(); private $_filters = array(); /** * @var int crc32 of the template name */ private $_crc = 0; /** * @param Fenom $fenom Template storage * @param int $options * @param Template $parent */ public function __construct(Fenom $fenom, $options, Template $parent = null) { $this->parent = $parent; $this->_fenom = $fenom; $this->_options = $options; $this->_filters = $this->_fenom->getFilters(); $this->_tag_filters = $this->_fenom->getTagFilters(); } /** * Get tag stack size * @return int */ public function getStackSize() { return count($this->_stack); } /** * @param string $tag * @return bool|\Fenom\Tag */ public function getParentScope($tag) { for ($i = count($this->_stack) - 1; $i >= 0; $i--) { if ($this->_stack[$i]->name == $tag) { return $this->_stack[$i]; } } return false; } /** * Load source from provider * @param string $name * @param bool $compile * @return self */ public function load($name, $compile = true) { $this->_name = $name; $this->_crc = crc32($this->_name); if ($provider = strstr($name, ':', true)) { $this->_scm = $provider; $this->_base_name = substr($name, strlen($provider) + 1); } else { $this->_base_name = $name; } $this->_provider = $this->_fenom->getProvider($provider); $this->_src = $this->_provider->getSource($this->_base_name, $this->_time); $this->_compile_stage = self::COMPILE_STAGE_LOADED; if ($compile) { $this->compile(); } return $this; } /** * Load custom source * @param string $name template name * @param string $src template source * @param bool $compile * @return \Fenom\Template */ public function source($name, $src, $compile = true) { $this->_name = $name; $this->_src = $src; if ($compile) { $this->compile(); } return $this; } /** * Convert template to PHP code * * @throws CompileException */ public function compile() { $end = $pos = 0; foreach ($this->_fenom->getPreFilters() as $filter) { $this->_src = call_user_func($filter, $this, $this->_src); } $this->_compile_stage = self::COMPILE_STAGE_PRE_FILTERED; while (($start = strpos($this->_src, '{', $pos)) !== false) { // search open-symbol of tags switch (substr($this->_src, $start + 1, 1)) { // check next character case "\n": case "\r": case "\t": case " ": case "}": // ignore the tag $this->_appendText(substr($this->_src, $pos, $start - $pos + 2)); $end = $start + 1; break; case "*": // comment block $end = strpos($this->_src, '*}', $start); // find end of the comment block if ($end === false) { throw new CompileException("Unclosed comment block in line {$this->_line}", 0, 1, $this->_name, $this->_line); } $end++; $this->_appendText(substr($this->_src, $pos, $start - $pos)); $comment = substr($this->_src, $start, $end - $start); // read the comment block for processing $this->_line += substr_count($comment, "\n"); // count lines in comments unset($comment); // cleanup break; default: $this->_appendText(substr($this->_src, $pos, $start - $pos)); $end = $start + 1; do { $need_more = false; $end = strpos($this->_src, '}', $end + 1); // search close-symbol of the tag if ($end === false) { // if unexpected end of template throw new CompileException("Unclosed tag in line {$this->_line}", 0, 1, $this->_name, $this->_line); } $tag = substr( $this->_src, $start + 1, // skip '{' $end - $start - 1 // skip '}' ); if ($this->_ignore) { // check ignore if ($tag === '/' . $this->_ignore) { // turn off ignore $this->_ignore = false; } else { // still ignore $this->_appendText('{' . $tag . '}'); continue; } } if ($this->_tag_filters) { foreach ($this->_tag_filters as $filter) { $tag = call_user_func($filter, $tag, $this); } } $tokens = new Tokenizer($tag); // tokenize the tag if ($tokens->isIncomplete()) { // all strings finished? $need_more = true; } else { $this->_appendCode($this->parseTag($tokens), '{' . $tag . '}'); // start the tag lexer if ($tokens->key()) { // if tokenizer have tokens - throws exceptions throw new CompileException("Unexpected token '" . $tokens->current() . "' in {$this} line {$this->_line}, near '{" . $tokens->getSnippetAsString(0, 0) . "' <- there", 0, E_ERROR, $this->_name, $this->_line); } } } while ($need_more); unset($tag); // cleanup break; } $pos = $end + 1; // move search-pointer to end of the tag } $this->_compile_stage = self::COMPILE_STAGE_PARSED; gc_collect_cycles(); $this->_appendText(substr($this->_src, $end ? $end + 1 : 0)); // append tail of the template if ($this->_stack) { $_names = array(); foreach ($this->_stack as $scope) { $_names[] = '{' . $scope->name . '} opened on line ' . $scope->line; } /* @var Tag $scope */ $message = "Unclosed tag" . (count($_names) > 1 ? "s" : "") . ": " . implode(", ", $_names); throw new CompileException($message, 0, 1, $this->_name, $scope->line); } $this->_src = ""; // cleanup if ($this->_post) { foreach ($this->_post as $cb) { call_user_func_array($cb, array($this, &$this->_body)); } } $this->_compile_stage = self::COMPILE_STAGE_PROCESSED; $this->addDepend($this); // for 'verify' performance foreach ($this->_fenom->getPostFilters() as $filter) { $this->_body = call_user_func($filter, $this, $this->_body); } $this->_compile_stage = self::COMPILE_STAGE_POST_FILTERED; } public function isStageDone($stage_no) { return $this->_compile_stage >= $stage_no; } /** * Set or unset the option * @param int $option * @param bool $value */ public function setOption($option, $value) { if ($value) { $this->_options |= $option; } else { $this->_options &= ~$option; } } /** * Execute some code at loading cache * @param $code * @return void */ public function before($code) { $this->_before[] = $code; } /** * Generate name of temporary internal template variable (may be random) * @return string */ public function tmpVar() { return sprintf('$t%x_%x', $this->_crc ? $this->_crc : mt_rand(0, 0x7FFFFFFF), $this->i++); } /** * Append plain text to template body * * @param string $text */ private function _appendText($text) { $this->_line += substr_count($text, "\n"); $strip = $this->_options & Fenom::AUTO_STRIP; if ($this->_filters) { if (strpos($text, "_filters as $filter) { $text = call_user_func($filter, $this, $text); } } else { $fragments = explode("_filters as $filter) { $fragment = call_user_func($filter, $this, $fragment); } } } $text = implode('' . ($strip ? '' : PHP_EOL), $fragments); } } else { $text = str_replace("' . ($strip ? '' : PHP_EOL), $text); } if ($strip) { $text = preg_replace('/\s+/uS', ' ', str_replace(array("\r", "\n"), " ", $text)); $text = str_replace("> <", "><", $text); } $this->_body .= $text; } /** * Append PHP code to template body * * @param string $code * @param $source */ private function _appendCode($code, $source) { if (!$code) { return; } else { $this->_line += substr_count($source, "\n"); $this->_body .= "_name}:{$this->_line}: {$source} */\n $code ?>"; } } /** * @param $tag_name */ public function ignore($tag_name) { $this->_ignore = $tag_name; } /** * @param callable[] $cb */ public function addPostCompile($cb) { $this->_post[] = $cb; } /** * Return PHP code of template * * @return string */ public function getBody() { return $this->_body; } /** * Return PHP code for saving to file * * @return string */ public function getTemplateCode() { $before = $this->_before ? implode("\n", $this->_before) . "\n" : ""; return "_name . "' compiled at " . date('Y-m-d H:i:s') . " */\n" . $before . // some code 'before' template "return new Fenom\\Render(\$fenom, " . $this->_getClosureSource() . ", array(\n" . "\t'options' => {$this->_options},\n" . "\t'provider' => " . var_export($this->_scm, true) . ",\n" . "\t'name' => " . var_export($this->_name, true) . ",\n" . "\t'base_name' => " . var_export($this->_base_name, true) . ",\n" . "\t'time' => {$this->_time},\n" . "\t'depends' => " . var_export($this->_depends, true) . ",\n" . "\t'macros' => " . $this->_getMacrosArray() . ",\n ));\n"; } /** * Make array with macros code * @return string */ private function _getMacrosArray() { if ($this->macros) { $macros = array(); foreach ($this->macros as $name => $m) { if ($m["recursive"]) { $macros[] = "\t\t'" . $name . "' => function (\$var, \$tpl) {\n?>" . $m["body"] . "{$this->_body}_code) { // evaluate template's code eval("\$this->_code = " . $this->_getClosureSource() . ";\n\$this->_macros = " . $this->_getMacrosArray() . ';'); if (!$this->_code) { throw new CompileException("Fatal error while creating the template"); } } return parent::display($values); } /** * Add depends * @param Render $tpl */ public function addDepend(Render $tpl) { $this->_depends[$tpl->getScm()][$tpl->getBaseName()] = $tpl->getTime(); } /** * Output the value * * @param string $data * @param null|bool $escape * @return string */ public function out($data, $escape = null) { if ($escape === null) { $escape = $this->_options & Fenom::AUTO_ESCAPE; } if ($escape) { return "echo htmlspecialchars($data, ENT_COMPAT, " . var_export(Fenom::$charset, true) . ");"; } else { return "echo $data;"; } } /** * Import block from another template * @param string $tpl */ public function importBlocks($tpl) { $donor = $this->_fenom->compile($tpl, false); foreach ($donor->blocks as $name => $block) { if (!isset($this->blocks[$name])) { $block['import'] = $this->getName(); $this->blocks[$name] = $block; } } $this->addDepend($donor); } /** * Extends the template * @param string $tpl * @return \Fenom\Template parent */ public function extend($tpl) { if (!$this->isStageDone(self::COMPILE_STAGE_PARSED)) { $this->compile(); } $parent = $this->_fenom->getRawTemplate()->load($tpl, false); $parent->blocks = &$this->blocks; $parent->macros = &$this->macros; $parent->_before = &$this->_before; $parent->extended = $this->getName(); if (!$this->ext_stack) { $this->ext_stack[] = $this->getName(); } $this->ext_stack[] = $parent->getName(); $parent->_options = $this->_options; $parent->ext_stack = $this->ext_stack; $parent->compile(); $this->_body = $parent->_body; $this->_src = $parent->_src; $this->addDepend($parent); return $parent; } /** * Tag router * @param Tokenizer $tokens * * @throws SecurityException * @throws CompileException * @return string executable PHP code */ public function parseTag(Tokenizer $tokens) { try { if ($tokens->is(Tokenizer::MACRO_STRING)) { return $this->parseAct($tokens); } elseif ($tokens->is('/')) { return $this->parseEndTag($tokens); } else { return $this->out($this->parseExpr($tokens)); } } catch (InvalidUsageException $e) { throw new CompileException($e->getMessage() . " in {$this->_name} line {$this->_line}", 0, E_ERROR, $this->_name, $this->_line, $e); } catch (\LogicException $e) { throw new SecurityException($e->getMessage() . " in {$this->_name} line {$this->_line}, near '{" . $tokens->getSnippetAsString(0, 0) . "' <- there", 0, E_ERROR, $this->_name, $this->_line, $e); } catch (\Exception $e) { throw new CompileException($e->getMessage() . " in {$this->_name} line {$this->_line}, near '{" . $tokens->getSnippetAsString(0, 0) . "' <- there", 0, E_ERROR, $this->_name, $this->_line, $e); } catch (\Throwable $e) { throw new CompileException($e->getMessage() . " in {$this->_name} line {$this->_line}, near '{" . $tokens->getSnippetAsString(0, 0) . "' <- there", 0, E_ERROR, $this->_name, $this->_line, $e); } } /** * Close tag handler * * @param Tokenizer $tokens * @return string * @throws TokenizeException */ public function parseEndTag(Tokenizer $tokens) { $name = $tokens->getNext(Tokenizer::MACRO_STRING); $tokens->next(); if (!$this->_stack) { throw new TokenizeException("Unexpected closing of the tag '$name', the tag hasn't been opened"); } /** @var Tag $tag */ $tag = array_pop($this->_stack); if ($tag->name !== $name) { throw new TokenizeException("Unexpected closing of the tag '$name' (expecting closing of the tag {$tag->name}, opened in line {$tag->line})"); } return $tag->end($tokens); } /** * Parse action {action ...} or {action(...) ...} * * @static * @param Tokenizer $tokens * @throws \LogicException * @throws \RuntimeException * @throws Error\TokenizeException * @return string */ public function parseAct(Tokenizer $tokens) { $action = $tokens->get(Tokenizer::MACRO_STRING); $tokens->next(); if ($tokens->is("(", T_DOUBLE_COLON, T_NS_SEPARATOR) && !$tokens->isWhiteSpaced() ) { // just invoke function or static method $tokens->back(); return $this->out($this->parseExpr($tokens)); } elseif ($tokens->is('.')) { $name = $tokens->skip()->get(Tokenizer::MACRO_STRING); if ($action !== "macro") { $name = $action . "." . $name; } return $this->parseMacroCall($tokens, $name); } if ($info = $this->_fenom->getTag($action, $this)) { $tag = new Tag($action, $this, $info, $this->_body); if ($tokens->is(':')) { // parse tag options do { $tag->tagOption($tokens->next()->need(T_STRING)->getAndNext()); } while ($tokens->is(':')); } $code = $tag->start($tokens); if ($tag->isClosed()) { $tag->restoreAll(); } else { array_push($this->_stack, $tag); } return $code; } for ($j = $i = count($this->_stack) - 1; $i >= 0; $i--) { // call function's internal tag if ($this->_stack[$i]->hasTag($action, $j - $i)) { return $this->_stack[$i]->tag($action, $tokens); } } if ($tags = $this->_fenom->getTagOwners($action)) { // unknown template tag throw new TokenizeException( "Unexpected tag '$action' (this tag can be used with '" . implode( "', '", $tags ) . "')" ); } else { throw new TokenizeException("Unexpected tag '$action'"); } } /** * Get current template line * @return int */ public function getLine() { return $this->_line; } /** * Parse expressions. The mix of operators and terms. * * @param Tokenizer $tokens * @param bool $is_var * @throws \Exception * @return string */ public function parseExpr(Tokenizer $tokens, &$is_var = false) { $exp = array(); $var = false; // last term was: true - variable, false - mixed $op = false; // last exp was operator $cond = false; // was comparison operator while ($tokens->valid()) { // parse term $term = $this->parseTerm($tokens, $var, -1); // term of the expression if ($term !== false) { if ($tokens->is('?', '!')) { if ($cond) { $term = array_pop($exp) . ' ' . $term; $term = '(' . array_pop($exp) . ' ' . $term . ')'; $var = false; } $term = $this->parseTernary($tokens, $term, $var); $var = false; } $exp[] = $term; $op = false; } else { break; } if (!$tokens->valid()) { break; } // parse operator if ($tokens->is(Tokenizer::MACRO_BINARY)) { // binary operator: $a + $b, $a <= $b, ... if ($tokens->is(Tokenizer::MACRO_COND)) { // comparison operator if ($cond) { break; } $cond = true; } elseif ($tokens->is(Tokenizer::MACRO_BOOLEAN)) { $cond = false; } $op = $tokens->getAndNext(); } elseif ($tokens->is(Tokenizer::MACRO_EQUALS, '[')) { // assignment operator: $a = 4, $a += 3, ... if (!$var) { break; } $op = $tokens->getAndNext(); if ($op == '[') { $tokens->need(']')->next()->need('=')->next(); $op = '[]='; } } elseif ($tokens->is(T_STRING)) { // test or containment operator: $a in $b, $a is set, ... if (!$exp) { break; } $operator = $tokens->current(); if ($operator == "is") { $item = array_pop($exp); $exp[] = $this->parseIs($tokens, $item, $var); } elseif ($operator == "in" || ($operator == "not" && $tokens->isNextToken("in"))) { $item = array_pop($exp); $exp[] = $this->parseIn($tokens, $item); } else { break; } } elseif ($tokens->is('~')) { // string concatenation operator: 'asd' ~ $var if ($tokens->isNext('=')) { // ~= $exp[] = ".="; $tokens->next()->next(); } else { $concat = array(array_pop($exp)); while ($tokens->is('~')) { $tokens->next(); if ($tokens->is(T_LNUMBER, T_DNUMBER)) { $concat[] = "strval(" . $this->parseTerm($tokens) . ")"; } else { if ($tokens->is('~')) { $tokens->next(); $concat[] = "' '"; } if (!$term2 = "strval(" . $this->parseTerm($tokens) . ")") { throw new UnexpectedTokenException($tokens); } $concat[] = $term2; } } $exp[] = "(" . implode(".", $concat) . ")"; } } else { break; } if ($op) { $exp[] = $op; } } if ($op || !$exp) { throw new UnexpectedTokenException($tokens); } if (count($exp) == 1 && $var) { $is_var = true; } return implode(' ', $exp); } /** * Parse any term of expression: -2, ++$var, 'adf' * * @param Tokenizer $tokens * @param bool $is_var is parsed term - plain variable * @param int $allows * @throws \Exception * @return bool|string */ public function parseTerm(Tokenizer $tokens, &$is_var = false, $allows = -1) { $is_var = false; if ($tokens->is(Tokenizer::MACRO_UNARY)) { $unary = $tokens->getAndNext(); } else { $unary = ""; } switch ($tokens->key()) { case T_LNUMBER: case T_DNUMBER: $code = $unary . $this->parseScalar($tokens); break; case T_CONSTANT_ENCAPSED_STRING: case '"': case T_ENCAPSED_AND_WHITESPACE: if ($unary) { throw new UnexpectedTokenException($tokens->back()); } $code = $this->parseScalar($tokens); break; /** @noinspection PhpMissingBreakStatementInspection */ case '$': $code = $this->parseAccessor($tokens, $is_var); if (!$is_var) { if($tokens->is(T_OBJECT_OPERATOR)) { if ($this->_options & Fenom::DENY_METHODS) { throw new \LogicException("Forbidden to call methods"); } $code = $unary . $this->parseChain($tokens, $code); } else { $code = $unary . $code; } break; } /* no break */ case T_VARIABLE: if (!isset($code)) { $code = $this->parseVariable($tokens); } if ($tokens->is("(") && $tokens->hasBackList(T_STRING, T_OBJECT_OPERATOR)) { if ($this->_options & Fenom::DENY_METHODS) { throw new \LogicException("Forbidden to call methods"); } $code = $unary . $this->parseChain($tokens, $code); } elseif ($tokens->is(Tokenizer::MACRO_INCDEC)) { if ($this->_options & Fenom::FORCE_VERIFY) { $code = $unary . '(isset(' . $code . ') ? ' . $code . $tokens->getAndNext() . ' : null)'; } else { $code = $unary . $code . $tokens->getAndNext(); } } else { if ($this->_options & Fenom::FORCE_VERIFY) { $code = $unary . '(isset(' . $code . ') ? ' . $code . ' : null)'; } else { $is_var = true; $code = $unary . $code; } } break; case T_DEC: case T_INC: if ($this->_options & Fenom::FORCE_VERIFY) { $var = $this->parseVariable($tokens); $code = $unary . '(isset(' . $var . ') ? ' . $tokens->getAndNext() . $this->parseVariable($tokens) . ' : null)'; } else { $code = $unary . $tokens->getAndNext() . $this->parseVariable($tokens); } break; case '(': $tokens->next(); $code = $unary . "(" . $this->parseExpr($tokens) . ")"; $tokens->need(")")->next(); break; case T_STRING: if ($tokens->isSpecialVal()) { $code = $unary . $tokens->getAndNext(); } elseif ($tokens->isNext("(") && !$tokens->getWhitespace()) { $func = $this->_fenom->getModifier($modifier = $tokens->current(), $this); if (!$func) { throw new \Exception("Function " . $tokens->getAndNext() . " not found"); } if (!is_string($func)) { // dynamic modifier $call = 'call_user_func_array($tpl->getStorage()->getModifier("' . $modifier . '"), array' . $this->parseArgs($tokens->next()) . ')'; // @todo optimize } else { $call = $func . $this->parseArgs($tokens->next()); } $code = $unary . $this->parseChain($tokens, $call); } elseif ($tokens->isNext(T_NS_SEPARATOR, T_DOUBLE_COLON)) { $method = $this->parseStatic($tokens); $args = $this->parseArgs($tokens); $code = $unary . $this->parseChain($tokens, $method . $args); } else { return false; } break; case T_ISSET: case T_EMPTY: $func = $tokens->getAndNext(); if ($tokens->is("(") && $tokens->isNext(T_VARIABLE)) { $code = $unary . $func . "(" . $this->parseVariable($tokens->next()) . ")"; $tokens->need(')')->next(); } else { throw new TokenizeException("Unexpected token " . $tokens->getNext() . ", isset() and empty() accept only variables"); } break; case '[': if ($unary) { throw new UnexpectedTokenException($tokens->back()); } $code = $this->parseArray($tokens); break; default: if ($unary) { throw new UnexpectedTokenException($tokens->back()); } else { return false; } } if (($allows & self::TERM_MODS) && $tokens->is('|')) { $code = $this->parseModifier($tokens, $code); $is_var = false; } if (($allows & self::TERM_RANGE) && $tokens->is('.') && $tokens->isNext('.')) { $tokens->next()->next(); $code = '(new \Fenom\RangeIterator(' . $code . ', ' . $this->parseTerm($tokens, $var, self::TERM_MODS) . '))'; $is_var = false; } return $code; } /** * Parse call-chunks: $var->func()->func()->prop->func()->... * @param Tokenizer $tokens * @param string $code start point (it is $var) * @return string */ public function parseChain(Tokenizer $tokens, $code) { do { if ($tokens->is('(')) { $code .= $this->parseArgs($tokens); } if ($tokens->is(T_OBJECT_OPERATOR) && $tokens->isNext(T_STRING)) { $code .= '->' . $tokens->next()->getAndNext(); } } while ($tokens->is('(', T_OBJECT_OPERATOR)); return $code; } /** * Parse variable name: $a, $a.b, $a.b['c'], $a:index * @param Tokenizer $tokens * @param $var * @return string * @throws Error\UnexpectedTokenException */ public function parseVariable(Tokenizer $tokens, $var = null) { if (!$var) { if ($tokens->isNext('@')) { // $v = $tokens->get(T_VARIABLE); $prop = $tokens->next()->next()->get(T_STRING); if ($tag = $this->getParentScope("foreach")) { $tokens->next(); return Compiler::foreachProp($tag, $prop); } else { throw new UnexpectedTokenException($tokens); } } else { $var = '$var["' . substr($tokens->get(T_VARIABLE), 1) . '"]'; $tokens->next(); } } while ($t = $tokens->key()) { if ($t === ".") { $tokens->next(); if ($tokens->is(T_VARIABLE)) { $key = '[ $var["' . substr($tokens->getAndNext(), 1) . '"] ]'; } elseif ($tokens->is(Tokenizer::MACRO_STRING)) { $key = '["' . $tokens->getAndNext() . '"]'; } elseif ($tokens->is(Tokenizer::MACRO_SCALAR)) { $key = "[" . $tokens->getAndNext() . "]"; } elseif ($tokens->is('"')) { $key = "[" . $this->parseQuote($tokens) . "]"; } elseif ($tokens->is('.')) { $tokens->back(); break; } else { throw new UnexpectedTokenException($tokens); } $var .= $key; } elseif ($t === "[") { if ($tokens->isNext(']')) { break; } $tokens->next(); if ($tokens->is(Tokenizer::MACRO_STRING)) { if ($tokens->isNext("(")) { $key = "[" . $this->parseExpr($tokens) . "]"; } else { $key = '["' . $tokens->current() . '"]'; $tokens->next(); } } else { $key = "[" . $this->parseExpr($tokens) . "]"; } $tokens->get("]"); $tokens->next(); $var .= $key; } elseif ($t === T_DNUMBER) { $var .= '[' . substr($tokens->getAndNext(), 1) . ']'; } elseif ($t === T_OBJECT_OPERATOR) { $var .= "->" . $tokens->getNext(T_STRING); $tokens->next(); } else { break; } } return $var; } /** * Parse accessor * @param Tokenizer $tokens * @param bool $is_var * @return string */ public function parseAccessor(Tokenizer $tokens, &$is_var = false) { $accessor = $tokens->need('$')->next()->need('.')->next()->current(); $parser = $this->getStorage()->getAccessor($accessor); $is_var = false; if ($parser) { if (is_array($parser)) { if (isset($parser['callback'])) { $tokens->next(); return 'call_user_func($tpl->getStorage()->getAccessor(' . var_export($accessor, true) . ', "callback"), ' . var_export($accessor, true) . ', $tpl, $var)'; } else { return call_user_func_array( $parser['parser'], array( $parser['accessor'], $tokens->next(), $this, &$is_var ) ); } } else { return call_user_func_array($parser, array($tokens->next(), $this, &$is_var)); } } else { throw new \RuntimeException("Unknown accessor '\$.$accessor'"); } } /** * Parse ternary operator * * @param Tokenizer $tokens * @param $var * @param $is_var * @return string * @throws UnexpectedTokenException */ public function parseTernary(Tokenizer $tokens, $var, $is_var) { $empty = $tokens->is('?'); $tokens->next(); if ($tokens->is(":", "?")) { $tokens->next(); if ($empty) { if ($is_var) { return '(empty(' . $var . ') ? (' . $this->parseExpr($tokens) . ') : ' . $var . ')'; } else { return '(' . $var . ' ?: (' . $this->parseExpr($tokens) . '))'; } } else { if ($is_var) { return '(isset(' . $var . ') ? ' . $var . ' : (' . $this->parseExpr($tokens) . '))'; } else { return '((' . $var . ' !== null) ? ' . $var . ' : (' . $this->parseExpr($tokens) . '))'; } } } elseif ($tokens->is( Tokenizer::MACRO_BINARY, Tokenizer::MACRO_BOOLEAN, Tokenizer::MACRO_MATH ) || !$tokens->valid() ) { if ($empty) { if ($is_var) { return '!empty(' . $var . ')'; } else { return '(' . $var . ')'; } } else { if ($is_var) { return 'isset(' . $var . ')'; } else { return '(' . $var . ' !== null)'; } } } else { $expr1 = $this->parseExpr($tokens); $tokens->need(':')->skip(); $expr2 = $this->parseExpr($tokens); if ($empty) { if ($is_var) { return '(empty(' . $var . ') ? ' . $expr2 . ' : ' . $expr1 . ')'; } else { return '(' . $var . ' ? ' . $expr1 . ' : ' . $expr2 . ')'; } } else { if ($is_var) { return '(isset(' . $var . ') ? ' . $expr1 . ' : ' . $expr2 . ')'; } else { return '((' . $var . ' !== null) ? ' . $expr1 . ' : ' . $expr2 . ')'; } } } } /** * Parse 'is' and 'is not' operators * @see _tests * @param Tokenizer $tokens * @param string $value * @param bool $variable * @throws InvalidUsageException * @return string */ public function parseIs(Tokenizer $tokens, $value, $variable = false) { $tokens->next(); if ($tokens->current() == 'not') { $invert = '!'; $equal = '!='; $tokens->next(); } else { $invert = ''; $equal = '=='; } if ($tokens->is(Tokenizer::MACRO_STRING)) { $action = $tokens->current(); if (!$variable && ($action == "set" || $action == "empty")) { $action = "_$action"; $tokens->next(); return $invert . sprintf($this->_fenom->getTest($action), $value); } elseif ($test = $this->_fenom->getTest($action)) { $tokens->next(); return $invert . sprintf($test, $value); } elseif ($tokens->isSpecialVal()) { $tokens->next(); return '(' . $value . ' ' . $equal . '= ' . $action . ')'; } return $invert . '(' . $value . ' instanceof \\' . $this->parseName($tokens) . ')'; } elseif ($tokens->is(T_VARIABLE, '[', Tokenizer::MACRO_SCALAR, '"')) { return '(' . $value . ' ' . $equal . '= ' . $this->parseTerm($tokens) . ')'; } elseif ($tokens->is(T_NS_SEPARATOR)) { // return $invert . '(' . $value . ' instanceof \\' . $this->parseName($tokens) . ')'; } else { throw new InvalidUsageException("Unknown argument"); } } /** * Parse 'in' and 'not in' operators * @param Tokenizer $tokens * @param string $value * @throws InvalidUsageException * @throws UnexpectedTokenException * @return string */ public function parseIn(Tokenizer $tokens, $value) { $checkers = array( "string" => 'is_int(strpos(%2$s, %1$s))', "list" => "in_array(%s, %s)", "keys" => "array_key_exists(%s, %s)", "auto" => '\Fenom\Modifier::in(%s, %s)' ); $checker = null; $invert = ''; if ($tokens->current() == 'not') { $invert = '!'; $tokens->next(); } if ($tokens->current() !== "in") { throw new UnexpectedTokenException($tokens); } $tokens->next(); if ($tokens->is(Tokenizer::MACRO_STRING)) { $checker = $tokens->current(); if (!isset($checkers[$checker])) { throw new UnexpectedTokenException($tokens); } $tokens->next(); } if ($tokens->is('[')) { if ($checker == "string") { throw new InvalidUsageException("Can not use string operation for array"); } elseif (!$checker) { $checker = "list"; } return $invert . sprintf($checkers[$checker], $value, $this->parseArray($tokens)); } elseif ($tokens->is('"', T_ENCAPSED_AND_WHITESPACE, T_CONSTANT_ENCAPSED_STRING)) { if (!$checker) { $checker = "string"; } elseif ($checker != "string") { throw new InvalidUsageException("Can not use array operation for string"); } return $invert . sprintf($checkers[$checker], "strval($value)", $this->parseScalar($tokens)); } elseif ($tokens->is(T_VARIABLE, Tokenizer::MACRO_INCDEC)) { if (!$checker) { $checker = "auto"; } return $invert . sprintf($checkers[$checker], $value, $this->parseTerm($tokens)); } else { throw new UnexpectedTokenException($tokens); } } /** * Parse method, class or constant name * * @param Tokenizer $tokens * @return string */ public function parseName(Tokenizer $tokens) { $tokens->skipIf(T_NS_SEPARATOR); $name = ""; if ($tokens->is(T_STRING)) { $name .= $tokens->getAndNext(); while ($tokens->is(T_NS_SEPARATOR)) { $name .= '\\' . $tokens->next()->get(T_STRING); $tokens->next(); } } return $name; } /** * Parse scalar values * * @param Tokenizer $tokens * @throws Error\UnexpectedTokenException * @return string */ public function parseScalar(Tokenizer $tokens) { $token = $tokens->key(); switch ($token) { case T_CONSTANT_ENCAPSED_STRING: case T_LNUMBER: case T_DNUMBER: return $tokens->getAndNext(); case T_ENCAPSED_AND_WHITESPACE: case '"': return $this->parseQuote($tokens); default: throw new UnexpectedTokenException($tokens); } } /** * Parse string with or without variable * * @param Tokenizer $tokens * @throws UnexpectedTokenException * @return string */ public function parseQuote(Tokenizer $tokens) { if ($tokens->is('"')) { $stop = $tokens->current(); $_str = '"'; $tokens->next(); while ($t = $tokens->key()) { if ($t === T_ENCAPSED_AND_WHITESPACE) { $_str .= $tokens->current(); $tokens->next(); } elseif ($t === T_VARIABLE) { if (strlen($_str) > 1) { $_str .= '".'; } else { $_str = ""; } $_str .= '$var["' . substr($tokens->current(), 1) . '"]'; $tokens->next(); if ($tokens->is($stop)) { $tokens->skip(); return $_str; } else { $_str .= '."'; } } elseif ($t === T_CURLY_OPEN) { if (strlen($_str) > 1) { $_str .= '".'; } else { $_str = ""; } $tokens->getNext(T_VARIABLE); $_str .= '(' . $this->parseExpr($tokens) . ')'; if ($tokens->is($stop)) { $tokens->next(); return $_str; } else { $_str .= '."'; } } elseif ($t === "}") { $tokens->next(); } elseif ($t === $stop) { $tokens->next(); return $_str . '"'; } else { break; } } throw new UnexpectedTokenException($tokens); } elseif ($tokens->is(T_CONSTANT_ENCAPSED_STRING)) { return $tokens->getAndNext(); } elseif ($tokens->is(T_ENCAPSED_AND_WHITESPACE)) { throw new UnexpectedTokenException($tokens); } else { return ""; } } /** * Parse modifiers * |modifier:1:2.3:'string':false:$var:(4+5*$var3)|modifier2:"str {$var+3} ing":$arr.item * * @param Tokenizer $tokens * @param $value * @throws \LogicException * @throws \Exception * @return string */ public function parseModifier(Tokenizer $tokens, $value) { while ($tokens->is("|")) { $modifier = $tokens->getNext(Tokenizer::MACRO_STRING); if ($tokens->isNext(T_DOUBLE_COLON, T_NS_SEPARATOR)) { $mods = $this->parseStatic($tokens); } else { $mods = $this->_fenom->getModifier($modifier, $this); if (!$mods) { throw new \Exception("Modifier " . $tokens->current() . " not found"); } $tokens->next(); } $args = array(); while ($tokens->is(":")) { if (($args[] = $this->parseTerm($tokens->next(), $is_var, 0)) === false) { throw new UnexpectedTokenException($tokens); } } if (!is_string($mods)) { // dynamic modifier $mods = 'call_user_func($tpl->getStorage()->getModifier("' . $modifier . '"), '; } else { $mods .= "("; } if ($args) { $value = $mods . $value . ', ' . implode(", ", $args) . ')'; } else { $value = $mods . $value . ')'; } } return $value; } /** * Parse array * [1, 2.3, 5+7/$var, 'string', "str {$var+3} ing", $var2, []] * * @param Tokenizer $tokens * @param int $count amount of elements * @throws Error\UnexpectedTokenException * @return string */ public function parseArray(Tokenizer $tokens, &$count = 0) { if ($tokens->is("[")) { $arr = array(); $tokens->next(); while ($tokens->valid()) { if ($tokens->is(']')) { $tokens->next(); return 'array(' . implode(', ', $arr) . ')'; } if ($tokens->is('[')) { $arr[] = $this->parseArray($tokens); $count++; } else { $expr = $this->parseExpr($tokens); if ($tokens->is(T_DOUBLE_ARROW)) { $tokens->next(); $arr[] = $expr . ' => ' . $this->parseExpr($tokens); } else { $arr[] = $expr; } $count++; } if ($tokens->is(',')) { $tokens->next(); } } } throw new UnexpectedTokenException($tokens); } /** * @param Tokenizer $tokens * @param $name * @return string * @throws InvalidUsageException */ public function parseMacroCall(Tokenizer $tokens, $name) { $recursive = false; $macro = false; if (isset($this->macros[$name])) { $macro = $this->macros[$name]; $recursive = $macro['recursive']; } else { foreach ($this->_stack as $scope) { if ($scope->name == 'macro' && $scope['name'] == $name) { // invoke recursive $recursive = $scope; $macro = $scope['macro']; break; } } if (!$macro) { throw new InvalidUsageException("Undefined macro '$name'"); } } $tokens->next(); $p = $this->parseParams($tokens); $args = array(); foreach ($macro['args'] as $arg) { if (isset($p[$arg])) { $args[$arg] = $p[$arg]; } elseif (isset($macro['defaults'][$arg])) { $args[$arg] = $macro['defaults'][$arg]; } else { throw new InvalidUsageException("Macro '$name' require '$arg' argument"); } } if ($recursive) { if ($recursive instanceof Tag) { $recursive['recursive'] = true; } return '$tpl->getMacro("' . $name . '")->__invoke(' . Compiler::toArray($args) . ', $tpl);'; } else { $vars = $this->tmpVar(); return $vars . ' = $var; $var = ' . Compiler::toArray($args) . ';' . PHP_EOL . '?>' . $macro["body"] . '_options & Fenom::DENY_STATICS) { throw new \LogicException("Static methods are disabled"); } $tokens->skipIf(T_NS_SEPARATOR); $name = ""; if ($tokens->is(T_STRING)) { $name .= $tokens->getAndNext(); while ($tokens->is(T_NS_SEPARATOR)) { $name .= '\\' . $tokens->next()->get(T_STRING); $tokens->next(); } } $tokens->need(T_DOUBLE_COLON)->next()->need(T_STRING); $static = $name . "::" . $tokens->getAndNext(); if (!is_callable($static)) { throw new \RuntimeException("Method $static doesn't exist"); } return $static; } /** * Parse argument list * (1 + 2.3, 'string', $var, [2,4]) * * @param Tokenizer $tokens * @return string */ public function parseArgs(Tokenizer $tokens) { $_args = "("; $tokens->next(); $arg = $colon = false; while ($tokens->valid()) { if (!$arg && $tokens->is( T_VARIABLE, T_STRING, "$", "(", Tokenizer::MACRO_SCALAR, '"', Tokenizer::MACRO_UNARY, Tokenizer::MACRO_INCDEC ) ) { $_args .= $this->parseExpr($tokens); $arg = true; $colon = false; } elseif (!$arg && $tokens->is('[')) { $_args .= $this->parseArray($tokens); $arg = true; $colon = false; } elseif ($arg && $tokens->is(',')) { $_args .= $tokens->getAndNext() . ' '; $arg = false; $colon = true; } elseif (!$colon && $tokens->is(')')) { $tokens->next(); return $_args . ')'; } else { break; } } throw new TokenizeException("Unexpected token '" . $tokens->current() . "' in argument list"); } /** * Parse first unnamed argument * * @param Tokenizer $tokens * @param string $static * @return mixed|string */ public function parsePlainArg(Tokenizer $tokens, &$static) { if ($tokens->is(T_CONSTANT_ENCAPSED_STRING)) { if ($tokens->isNext('|')) { return $this->parseExpr($tokens); } else { $str = $tokens->getAndNext(); $static = stripslashes(substr($str, 1, -1)); return $str; } } else { return $this->parseExpr($tokens); } } /** * Parse parameters as $key=$value * param1=$var param2=3 ... * * @static * @param Tokenizer $tokens * @param array $defaults * @throws \Exception * @return array */ public function parseParams(Tokenizer $tokens, array $defaults = null) { $params = array(); while ($tokens->valid()) { if ($tokens->is(Tokenizer::MACRO_STRING)) { $key = $tokens->getAndNext(); if ($defaults && !isset($defaults[$key])) { throw new InvalidUsageException("Unknown parameter '$key'"); } if ($tokens->is("=")) { $tokens->next(); $params[$key] = $this->parseExpr($tokens); } else { throw new InvalidUsageException("Invalid value for parameter '$key'"); } } elseif ($tokens->is(Tokenizer::MACRO_SCALAR, '"', T_VARIABLE, "[", '(')) { $params[] = $this->parseExpr($tokens); } else { break; } } if ($defaults) { $params += $defaults; } return $params; } }