*/ class Compiler { /** * Tag {include ...} * * @static * @param Tokenizer $tokens * @param Template $tpl * @throws \LogicException * @return string */ public static function tagInclude(Tokenizer $tokens, Template $tpl) { $name = false; $cname = $tpl->parsePlainArg($tokens, $name); $p = $tpl->parseParams($tokens); if ($name) { if($tpl->getStorage()->getOptions() & \Fenom::FORCE_INCLUDE) { $inc = $tpl->getStorage()->compile($name, false); $tpl->addDepend($inc); $var = $tpl->tmpVar(); if($p) { return $var.' = $var; $var = ' . self::toArray($p) . ' + $var; ?>' . $inc->getBody() . '' . $inc->getBody() . 'getStorage()->templateExists($name)) { throw new \LogicException("Template $name not found"); } } if($p) { return '$tpl->getStorage()->getTemplate(' . $cname . ')->display(' . self::toArray($p) . ' + $var);'; } else { return '$tpl->getStorage()->getTemplate(' . $cname . ')->display($var);'; } } /** * Tag {insert ...} * @param Tokenizer $tokens * @param Template $tpl * @return string * @throws Error\InvalidUsageException */ public static function tagInsert(Tokenizer $tokens, Template $tpl) { $tpl->parsePlainArg($tokens, $name); if (!$name) { throw new InvalidUsageException("Tag {insert} accept only static template name"); } $inc = $tpl->getStorage()->compile($name, false); $tpl->addDepend($inc); return '?>' . $inc->getBody() . 'tpl->parseExpr($tokens) . ') {'; } /** * Tag {elseif ...} * * @static * @param Tokenizer $tokens * @param Scope $scope * @throws InvalidUsageException * @return string */ public static function tagElseIf(Tokenizer $tokens, Scope $scope) { if ($scope["else"]) { throw new InvalidUsageException('Incorrect use of the tag {elseif}'); } return '} elseif(' . $scope->tpl->parseExpr($tokens) . ') {'; } /** * Tag {else} * * @param Tokenizer $tokens * @param Scope $scope * @internal param $ * @param Scope $scope * @return string */ public static function tagElse(Tokenizer $tokens, Scope $scope) { $scope["else"] = true; return '} else {'; } /** * Open tag {foreach ...} * * @static * @param Tokenizer $tokens * @param Scope $scope * @throws UnexpectedTokenException * @throws InvalidUsageException * @return string */ public static function foreachOpen(Tokenizer $tokens, Scope $scope) { $p = array("index" => false, "first" => false, "last" => false); $key = null; $before = $body = array(); if ($tokens->is(T_VARIABLE)) { $from = $scope->tpl->parseTerm($tokens); $prepend = ""; } elseif ($tokens->is('[')) { $from = $scope->tpl->parseArray($tokens); $uid = '$v' . $scope->tpl->i++; $prepend = $uid . ' = ' . $from . ';'; $from = $uid; } else { throw new UnexpectedTokenException($tokens, null, "tag {foreach}"); } $tokens->get(T_AS); $tokens->next(); $value = $scope->tpl->parseVariable($tokens); if ($tokens->is(T_DOUBLE_ARROW)) { $tokens->next(); $key = $value; $value = $scope->tpl->parseVariable($tokens); } $scope["after"] = array(); $scope["else"] = false; while ($token = $tokens->key()) { $param = $tokens->get(T_STRING); if (!isset($p[$param])) { throw new InvalidUsageException("Unknown parameter '$param' in {foreach}"); } $tokens->getNext("="); $tokens->next(); $p[$param] = $scope->tpl->parseVariable($tokens); } if ($p["index"]) { $before[] = $p["index"] . ' = 0'; $scope["after"][] = $p["index"] . '++'; } if ($p["first"]) { $before[] = $p["first"] . ' = true'; $scope["after"][] = $p["first"] . ' && (' . $p["first"] . ' = false )'; } if ($p["last"]) { $before[] = $p["last"] . ' = false'; $scope["uid"] = "v" . $scope->tpl->i++; $before[] = '$' . $scope["uid"] . " = count($from)"; $body[] = 'if(!--$' . $scope["uid"] . ') ' . $p["last"] . ' = true'; } $before = $before ? implode("; ", $before) . ";" : ""; $body = $body ? implode("; ", $body) . ";" : ""; $scope["after"] = $scope["after"] ? implode("; ", $scope["after"]) . ";" : ""; if ($key) { return "$prepend if($from) { $before foreach($from as $key => $value) { $body"; } else { return "$prepend if($from) { $before foreach($from as $value) { $body"; } } /** * Tag {foreachelse} * * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function foreachElse($tokens, Scope $scope) { $scope["no-break"] = $scope["no-continue"] = $scope["else"] = true; return " {$scope['after']} } } else {"; } /** * Close tag {/foreach} * * @static * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function foreachClose($tokens, Scope $scope) { if ($scope["else"]) { return '}'; } else { return " {$scope['after']} } }"; } } /** * @static * @param Tokenizer $tokens * @param Scope $scope * @throws Error\UnexpectedTokenException * @throws Error\InvalidUsageException * @return string */ public static function forOpen(Tokenizer $tokens, Scope $scope) { $p = array("index" => false, "first" => false, "last" => false, "step" => 1, "to" => false, "max" => false, "min" => false); $scope["after"] = $before = $body = array(); $i = array('', ''); $c = ""; $var = $scope->tpl->parseTerm($tokens, $is_var); if (!$is_var) { throw new UnexpectedTokenException($tokens); } $tokens->get("="); $tokens->next(); $val = $scope->tpl->parseExpr($tokens); $p = $scope->tpl->parseParams($tokens, $p); if (is_numeric($p["step"])) { if ($p["step"] > 0) { $condition = "$var <= {$p['to']}"; if ($p["last"]) $c = "($var + {$p['step']}) > {$p['to']}"; } elseif ($p["step"] < 0) { $condition = "$var >= {$p['to']}"; if ($p["last"]) $c = "($var + {$p['step']}) < {$p['to']}"; } else { throw new InvalidUsageException("Invalid step value if {for}"); } } else { $condition = "({$p['step']} > 0 && $var <= {$p['to']} || {$p['step']} < 0 && $var >= {$p['to']})"; if ($p["last"]) $c = "({$p['step']} > 0 && ($var + {$p['step']}) <= {$p['to']} || {$p['step']} < 0 && ($var + {$p['step']}) >= {$p['to']})"; } if ($p["first"]) { $before[] = $p["first"] . ' = true'; $scope["after"][] = $p["first"] . ' && (' . $p["first"] . ' = false )'; } if ($p["last"]) { $before[] = $p["last"] . ' = false'; $body[] = "if($c) {$p['last']} = true"; } if ($p["index"]) { $i[0] .= $p["index"] . ' = 0,'; $i[1] .= $p["index"] . '++,'; } $scope["else"] = false; $scope["else_cond"] = "$var==$val"; $before = $before ? implode("; ", $before) . ";" : ""; $body = $body ? implode("; ", $body) . ";" : ""; $scope["after"] = $scope["after"] ? implode("; ", $scope["after"]) . ";" : ""; return "$before for({$i[0]} $var=$val; $condition;{$i[1]} $var+={$p['step']}) { $body"; } /** * @static * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function forElse(Tokenizer $tokens, Scope $scope) { $scope["no-break"] = $scope["no-continue"] = true; $scope["else"] = true; return " } if({$scope['else_cond']}) {"; } /** * @static * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function forClose($tokens, Scope $scope) { if ($scope["else"]) { return '}'; } else { return " {$scope['after']} }"; } } /** * @static * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function whileOpen(Tokenizer $tokens, Scope $scope) { return 'while(' . $scope->tpl->parseExpr($tokens) . ') {'; } /** * Open tag {switch} * * @static * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function switchOpen(Tokenizer $tokens, Scope $scope) { $expr = $scope->tpl->parseExpr($tokens); $scope["case"] = array(); $scope["last"] = array(); $scope["default"] = ''; $scope["var"] = $scope->tpl->tmpVar(); $scope["expr"] = $scope["var"].' = strval('.$expr.')'; // lazy init return ''; } /** * Resort cases for {switch} * @param Scope $scope */ private static function _caseResort(Scope $scope) { $content = $scope->cutContent(); if ($scope["last"] === false) { $scope["default"] .= $content; } else { foreach ($scope["last"] as $case) { if (!isset($scope["case"][$case])) { $scope["case"][$case] = ""; } $scope["case"][$case] .= $content; } } $scope["last"] = array(); } /** * Tag {case ...} * * @static * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function tagCase(Tokenizer $tokens, Scope $scope) { self::_caseResort($scope); do { $scope["last"][] = $scope->tpl->parseScalar($tokens, false); if ($tokens->is(',')) { $tokens->next(); } else { break; } } while (true); return ''; } /** * Tag {default} * * @static * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function tagDefault($tokens, Scope $scope) { self::_caseResort($scope); $scope["last"] = false; return ''; } /** * Close tag {switch} * * @static * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function switchClose(Tokenizer $tokens, Scope $scope) { self::_caseResort($scope); $expr = $scope["var"]; $code = $scope["expr"].";\n"; $default = $scope["default"]; foreach ($scope["case"] as $case => $content) { if(is_numeric($case)) { $case = "'$case'"; } $code .= "if($expr == $case) {\n?>$content$default_extends)) { throw new InvalidUsageException("Only one {extends} allowed"); } elseif ($tpl->getStackSize()) { throw new InvalidUsageException("Tags {extends} can not be nested"); } $tpl_name = $tpl->parsePlainArg($tokens, $name); if (empty($tpl->_extended)) { $tpl->addPostCompile(__CLASS__ . "::extendBody"); } if ($tpl->getOptions() & Template::DYNAMIC_EXTEND) { $tpl->_compatible = true; } if ($name) { // static extends $tpl->_extends = $tpl->getStorage()->getRawTemplate()->load($name, false); if (!isset($tpl->_compatible)) { $tpl->_compatible = & $tpl->_extends->_compatible; } $tpl->addDepend($tpl->_extends); return ""; } else { // dynamic extends if (!isset($tpl->_compatible)) { $tpl->_compatible = true; } $tpl->_extends = $tpl_name; return '$parent = $tpl->getStorage()->getTemplate(' . $tpl_name . ', \Fenom\Template::EXTENDED);'; } } /** * Post compile action for {extends ...} tag * @param string $body * @param Template $tpl */ public static function extendBody(&$body, $tpl) { $t = $tpl; if ($tpl->uses) { $tpl->blocks += $tpl->uses; } while (isset($t->_extends)) { $t = $t->_extends; if (is_object($t)) { /* @var \Fenom\Template $t */ $t->_extended = true; $tpl->addDepend($t); $t->_compatible = & $tpl->_compatible; $t->blocks = & $tpl->blocks; $t->compile(); if ($t->uses) { $tpl->blocks += $t->uses; } if (!isset($t->_extends)) { // last item => parent if (empty($tpl->_compatible)) { $body = $t->getBody(); } else { $body = '' . $body . '' . $t->getBody(); } return; } else { $body .= $t->getBody(); } } else { $body = '' . $body . 'b = &$tpl->b; $parent->display((array)$tpl); unset($tpl->b, $parent->b); ?>'; return; } } } /** * Tag {use ...} * @param Tokenizer $tokens * @param Template $tpl * @throws InvalidUsageException * @return string */ public static function tagUse(Tokenizer $tokens, Template $tpl) { if ($tpl->getStackSize()) { throw new InvalidUsageException("Tag {use} can not be nested"); } $tpl->parsePlainArg($tokens, $name); if ($name) { $tpl->importBlocks($name); } else { throw new InvalidUsageException('template name must be given explicitly yet'); } } /** * Tag {block ...} * @param Tokenizer $tokens * @param Scope $scope * @throws \RuntimeException * @return string */ public static function tagBlockOpen(Tokenizer $tokens, Scope $scope) { if ($scope->level > 0) { $scope->tpl->_compatible = true; } $scope["cname"] = $scope->tpl->parsePlainArg($tokens, $name); if(!$name) { throw new \RuntimeException("Only static names for blocks allowed"); } $scope["name"] = $name; $scope["use_parent"] = false; } /** * @param Tokenizer $tokens * @param Scope $scope */ public static function tagBlockClose($tokens, Scope $scope) { $tpl = $scope->tpl; $name = $scope["name"]; if(isset($tpl->blocks[$name])) { // block defined $block = &$tpl->blocks[$name]; if($block['use_parent']) { $parent = $scope->getContent(); $block['block'] = str_replace($block['use_parent']." ?>", "?>".$parent, $block['block']); } if(!$block["import"]) { // not from {use} - redefine block $scope->replaceContent($block["block"]); return; } elseif($block["import"] != $tpl->getName()) { // tag {use} was in another template $tpl->blocks[$scope["name"]]["import"] = false; $scope->replaceContent($block["block"]); } } $tpl->blocks[$scope["name"]] = [ "from" => $tpl->getName(), "import" => false, "use_parent" => $scope["use_parent"], "block" => $scope->getContent() ]; } /** * Tag {parent} * * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function tagParent($tokens, Scope $scope) { $block_scope = $scope->tpl->getParentScope('block'); if(!$block_scope['use_parent']) { $block_scope['use_parent'] = "/* %%parent#".mt_rand(0, 1e6)."%% */"; } return $block_scope['use_parent']; } /** * Standard close tag {/...} * * @static * @return string */ public static function stdClose() { return '}'; } /** * Standard function parser * * @static * @param mixed $function * @param Tokenizer $tokens * @param Template $tpl * @return string */ public static function stdFuncParser($function, Tokenizer $tokens, Template $tpl) { return "$function(" . self::toArray($tpl->parseParams($tokens)) . ', $tpl)'; } /** * Smart function parser * * @static * @param $function * @param Tokenizer $tokens * @param Template $tpl * @return string */ public static function smartFuncParser($function, Tokenizer $tokens, Template $tpl) { if (strpos($function, "::")) { list($class, $method) = explode("::", $function, 2); $ref = new \ReflectionMethod($class, $method); } else { $ref = new \ReflectionFunction($function); } $args = array(); $params = $tpl->parseParams($tokens); foreach ($ref->getParameters() as $param) { if (isset($params[$param->getName()])) { $args[] = $params[$param->getName()]; } elseif (isset($params[$param->getPosition()])) { $args[] = $params[$param->getPosition()]; } elseif ($param->isOptional()) { $args[] = var_export($param->getDefaultValue(), true); } } return "$function(" . implode(", ", $args) . ')'; } /** * Standard function open tag parser * * @static * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function stdFuncOpen(Tokenizer $tokens, Scope $scope) { $scope["params"] = self::toArray($scope->tpl->parseParams($tokens)); return 'ob_start();'; } /** * Standard function close tag parser * * @static * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function stdFuncClose($tokens, Scope $scope) { return $scope["function"] . '(' . $scope["params"] . ', ob_get_clean(), $tpl)'; } /** * Convert array of code to string array * @param $params * @return string */ public static function toArray($params) { $_code = array(); foreach ($params as $k => $v) { $_code[] = '"' . $k . '" => ' . $v; } return 'array(' . implode(",", $_code) . ')'; } /** * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function varOpen(Tokenizer $tokens, Scope $scope) { $var = $scope->tpl->parseVariable($tokens); if ($tokens->is('=')) { // inline tag {var ...} $scope->is_closed = true; $tokens->next(); if ($tokens->is("[")) { return $var . '=' . $scope->tpl->parseArray($tokens); } else { return $var . '=' . $scope->tpl->parseExpr($tokens); } } else { $scope["name"] = $var; if ($tokens->is('|')) { $scope["value"] = $scope->tpl->parseModifier($tokens, "ob_get_clean()"); } else { $scope["value"] = "ob_get_clean()"; } return 'ob_start();'; } } /** * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function varClose(Tokenizer $tokens, Scope $scope) { return $scope["name"] . '=' . $scope["value"] . ';'; } /** * @param Tokenizer $tokens * @param Scope $scope * @return string */ public static function filterOpen(Tokenizer $tokens, Scope $scope) { $scope["filter"] = $scope->tpl->parseModifier($tokens, "ob_get_clean()"); return "ob_start();"; } /** * @param $tokens * @param Scope $scope * @return string */ public static function filterClose($tokens, Scope $scope) { return "echo " . $scope["filter"] . ";"; } /** * Tag {cycle} * * @param Tokenizer $tokens * @param Template $tpl * @return string * @throws InvalidUsageException */ public static function tagCycle(Tokenizer $tokens, Template $tpl) { if ($tokens->is("[")) { $exp = $tpl->parseArray($tokens); } else { $exp = $tpl->parseExpr($tokens); } if ($tokens->valid()) { $p = $tpl->parseParams($tokens); if (empty($p["index"])) { throw new InvalidUsageException("Cycle may contain only index attribute"); } else { return 'echo ' . __CLASS__ . '::cycle(' . $exp . ', ' . $p["index"] . ')'; } } else { $var = $tpl->tmpVar(); return 'echo ' . __CLASS__ . '::cycle(' . $exp . ", isset($var) ? ++$var : ($var = 0) )"; } } /** * Runtime cycle callback * @param mixed $vals * @param $index * @return mixed */ public static function cycle($vals, $index) { return $vals[$index % count($vals)]; } /** * Import macros from templates * * @param Tokenizer $tokens * @param Template $tpl * @throws UnexpectedTokenException * @throws InvalidUsageException * @return string */ public static function tagImport(Tokenizer $tokens, Template $tpl) { $import = array(); if ($tokens->is('[')) { $tokens->next(); while ($tokens->valid()) { if ($tokens->is(Tokenizer::MACRO_STRING)) { $import[$tokens->current()] = true; $tokens->next(); } elseif ($tokens->is(']')) { $tokens->next(); break; } elseif ($tokens->is(',')) { $tokens->next(); } else { break; } } if ($tokens->current() != "from") { throw new UnexpectedTokenException($tokens); } $tokens->next(); } $tpl->parsePlainArg($tokens, $name); if (!$name) { throw new InvalidUsageException("Invalid usage tag {import}"); } if ($tokens->is(T_AS)) { $alias = $tokens->next()->get(Tokenizer::MACRO_STRING); if ($alias === "macro") { $alias = ""; } $tokens->next(); } else { $alias = ""; } $donor = $tpl->getStorage()->getRawTemplate()->load($name, true); if ($donor->macros) { foreach ($donor->macros as $name => $macro) { if ($p = strpos($name, ".")) { $name = substr($name, $p); } if ($import && !isset($import[$name])) { continue; } if ($alias) { $name = $alias . '.' . $name; } $tpl->macros[$name] = $macro; } $tpl->addDepend($donor); } return ''; } /** * Define macro * * @param Tokenizer $tokens * @param Scope $scope * @throws InvalidUsageException */ public static function macroOpen(Tokenizer $tokens, Scope $scope) { $scope["name"] = $tokens->get(Tokenizer::MACRO_STRING); $scope["recursive"] = false; $args = array(); $defaults = array(); if (!$tokens->valid()) { return; } $tokens->next()->need('(')->next(); if ($tokens->is(')')) { return; } while ($tokens->is(Tokenizer::MACRO_STRING, T_VARIABLE)) { $args[] = $param = $tokens->getAndNext(); if ($tokens->is('=')) { $tokens->next(); if ($tokens->is(T_CONSTANT_ENCAPSED_STRING, T_LNUMBER, T_DNUMBER) || $tokens->isSpecialVal()) { $defaults[$param] = $tokens->getAndNext(); } else { throw new InvalidUsageException("Macro parameters may have only scalar defaults"); } } $tokens->skipIf(','); } $tokens->skipIf(')'); $scope["macro"] = array( "name" => $scope["name"], "args" => $args, "defaults" => $defaults, "body" => "", "recursive" => false ); return; } /** * @param Tokenizer $tokens * @param Scope $scope */ public static function macroClose(Tokenizer $tokens, Scope $scope) { if ($scope["recursive"]) { $scope["macro"]["recursive"] = true; } $scope["macro"]["body"] = $scope->cutContent(); $scope->tpl->macros[$scope["name"]] = $scope["macro"]; } /** * Output value as is, without escaping * * @param Tokenizer $tokens * @param Template $tpl * @throws InvalidUsageException * @return string */ public static function tagRaw(Tokenizer $tokens, Template $tpl) { $escape = (bool)$tpl->escape; $tpl->escape = false; if ($tokens->is(':')) { $func = $tokens->getNext(Tokenizer::MACRO_STRING); $tag = $tpl->getStorage()->getTag($func, $tpl); if ($tag["type"] == \Fenom::INLINE_FUNCTION) { $code = $tpl->parseAct($tokens); // } elseif ($tag["type"] == \Fenom::BLOCK_FUNCTION) { // $code = $tpl->parseAct($tokens); // $tpl->getLastScope()->escape = false; // return $code; } else { throw new InvalidUsageException("Raw mode allow for expressions or functions"); } } else { $code = $tpl->out($tpl->parseExpr($tokens)); } $tpl->escape = $escape; return $code; } /** * @param Tokenizer $tokens * @param Scope $scope */ public static function autoescapeOpen(Tokenizer $tokens, Scope $scope) { $boolean = ($tokens->get(T_STRING) == "true" ? true : false); $scope["escape"] = $scope->tpl->escape; $scope->tpl->escape = $boolean; $tokens->next(); } /** * @param Tokenizer $tokens * @param Scope $scope */ public static function autoescapeClose(Tokenizer $tokens, Scope $scope) { $scope->tpl->escape = $scope["escape"]; } }