template = new ArrayNode(); $this->functions = array( 'count' => function($a) { return count($a); }, 'strlen' => function($a) { return mb_strlen($a); } ); self::parseTemplate($this->template, $string, 0); } /** * Enables or disables automatic escaping for template values. * Currently supported strategies: NULL, 'html', 'json', 'xml' * * @param string|callable $escape escape strategy or callback */ public function autoescape($escape) { if ($escape === 'html' || $escape === 'xml') { $this->escape = 'htmlspecialchars'; } else if ($escape === 'json') { $this->escape = 'json_encode'; } else if (is_callable($escape)) { $this->escape = $escape; } else if ($escape === NULL) { $this->escape = NULL; } else { throw new InvalidArgumentException("invalid escape strategy: $escape"); } } /** * Renders the template to a string using the given array of * bindings to resolve symbol references inside the template. * * @param array $bindings symbol table * * @return string string representation of the template */ public function render(array $bindings) { $context = new Context($bindings + $this->functions); $context->autoescape($this->escape); return $this->template->render($context); } /** * Skips tokens until the end of the current tag is reached. * * @param string $string template text * @param int $pos offset in string * * @return int new offset in the string */ private static function skipTokens($string, $pos) { for ($len = strlen($string); $pos < $len && substr_compare($string, self::$tag_end, $pos, strlen(self::$tag_end)); ++$pos) { $chr = $string[$pos]; if ($chr === '"' || $chr === "'") { while (++$pos < $len && $string[$pos] !== $chr) { if ($string[$pos] === '\\') { ++$pos; } } } } return $pos; } /** * Parses a template string into a template node tree, starting * at the specified offset. All created nodes are added to the * given sequence node. * * @param ArrayNode $node template node to build * @param string $string string to parse * @param int $pos offset in string * * @return int new offset in the string */ private static function parseTemplate(ArrayNode $node, $string, $pos) { $len = strlen($string); while ($pos < $len) { $next_pos = strpos($string, self::$tag_start, $pos); if ($next_pos === false) { $child = new TextNode(substr($string, $pos)); $node->addChild($child); break; } if ($next_pos > $pos) { $child = new TextNode(substr($string, $pos, $next_pos - $pos)); $node->addChild($child); } $pos = $next_pos + strlen(self::$tag_start); $next_pos = self::skipTokens($string, $pos); $scanner = new Scanner(substr($string, $pos, $next_pos - $pos)); $pos = $next_pos + strlen(self::$tag_end); switch ($scanner->nextToken()) { case T_FOREACH: $scanner->nextToken(); $expr = self::parseExpr($scanner); $key_name = 'index'; $val_name = 'this'; if ($scanner->tokenType() === T_AS) { $scanner->nextToken(); if ($scanner->tokenType() !== T_STRING) { throw new TemplateParserException('symbol expected', $scanner); } $val_name = $scanner->tokenValue(); $scanner->nextToken(); if ($scanner->tokenType() === T_DOUBLE_ARROW) { $scanner->nextToken(); if ($scanner->tokenType() !== T_STRING) { throw new TemplateParserException('symbol expected', $scanner); } $key_name = $val_name; $val_name = $scanner->tokenValue(); $scanner->nextToken(); } } $child = new IteratorNode($expr, $key_name, $val_name); $pos = self::parseTemplate($child, $string, $pos); $node->addChild($child); break; case T_ENDFOREACH: return $pos; case T_IF: $scanner->nextToken(); $child = new ConditionNode(self::parseExpr($scanner)); $pos = self::parseTemplate($child, $string, $pos); $node->addChild($child); break; case T_ELSEIF: $scanner->nextToken(); $child = new ConditionNode(self::parseExpr($scanner)); $node->addElse(); $node->addChild($child); return self::parseTemplate($child, $string, $pos); case T_ELSE: $scanner->nextToken(); $node->addElse(); break; case T_ENDIF: return $pos; default: $child = new ExpressionNode(self::parseExpr($scanner)); $node->addChild($child); } if ($scanner->tokenType() !== false) { throw new TemplateParserException('syntax error', $scanner); } } return $pos; } /** * value: NUMBER | STRING | SYMBOL | '(' expr ')' */ private static function parseValue(Scanner $scanner) { switch ($scanner->tokenType()) { case T_CONSTANT_ENCAPSED_STRING: case T_DNUMBER: case T_LNUMBER: $result = new ConstantExpression($scanner->tokenValue()); break; case T_STRING: $result = new SymbolExpression($scanner->tokenValue()); break; case '(': $scanner->nextToken(); $result = self::parseExpr($scanner); if ($scanner->tokenType() !== ')') { throw new TemplateParserException('missing ")"', $scanner); } break; default: throw new TemplateParserException('syntax error', $scanner); } $scanner->nextToken(); return $result; } /** * function: value | function '(' ')' | function '(' expr { ',' expr } ')' */ private static function parseFunction(Scanner $scanner) { $result = self::parseValue($scanner); $type = $scanner->tokenType(); while ($type === '(') { $scanner->nextToken(); $arguments = array(); if ($scanner->tokenType() !== ')') { $arguments[] = self::parseExpr($scanner); while ($scanner->tokenType() === ',') { $scanner->nextToken(); $arguments[] = self::parseExpr($scanner); } if ($scanner->tokenType() !== ')') { throw new TemplateParserException('missing ")"', $scanner); } } $scanner->nextToken(); $result = new FunctionExpression($result, $arguments); $type = $scanner->tokenType(); } return $result; } /** * index: function | index '[' expr ']' | index '.' SYMBOL */ private static function parseIndex(Scanner $scanner) { $result = self::parseFunction($scanner); $type = $scanner->tokenType(); while ($type === '[' || $type === '.') { $scanner->nextToken(); if ($type === '[') { $expr = self::parseExpr($scanner); if ($scanner->tokenType() !== ']') { throw new TemplateParserException('missing "]"', $scanner); } } else if ($scanner->tokenType() === T_STRING) { $expr = new ConstantExpression($scanner->tokenValue()); } else { throw new TemplateParserException('symbol expected', $scanner); } $scanner->nextToken(); $result = new IndexExpression($result, $expr, $type); $type = $scanner->tokenType(); } return $result; } /** * filter: index | filter '|' SYMBOL | filter '|' SYMBOL '(' expr { ',' expr } ')' */ private static function parseFilter(Scanner $scanner) { $result = self::parseIndex($scanner); $type = $scanner->tokenType(); while ($type === '|') { $scanner->nextToken(); if ($scanner->tokenType() !== T_STRING) { throw new TemplateParserException('symbol expected', $scanner); } $arguments = array($result); $symbol = new SymbolExpression($scanner->tokenValue()); $scanner->nextToken(); if ($scanner->tokenType() === '(') { $scanner->nextToken(); if ($scanner->tokenType() !== ')') { $arguments[] = self::parseExpr($scanner); while ($scanner->tokenType() === ',') { $scanner->nextToken(); $arguments[] = self::parseExpr($scanner); } if ($scanner->tokenType() !== ')') { throw new TemplateParserException('missing ")"', $scanner); } } $scanner->nextToken(); } if ($symbol->name() === 'raw') { $result = new RawExpression($result); } else { $result = new FunctionExpression($symbol, $arguments); } $type = $scanner->tokenType(); } return $result; } /** * sign: '!' sign | '+' sign | '-' sign | filter */ private static function parseSign(Scanner $scanner) { switch ($scanner->tokenType()) { case '!': $scanner->nextToken(); $result = new NotExpression(self::parseSign($scanner)); break; case '+': $scanner->nextToken(); $result = self::parseSign($scanner); break; case '-': $scanner->nextToken(); $result = new MinusExpression(self::parseSign($scanner)); break; default: $result = self::parseFilter($scanner); } return $result; } /** * product: sign | product '*' sign | product '/' sign | product '%' sign */ private static function parseProduct(Scanner $scanner) { $result = self::parseSign($scanner); $type = $scanner->tokenType(); while ($type === '*' || $type === '/' || $type === '%') { $scanner->nextToken(); $result = new ArithExpression($result, self::parseSign($scanner), $type); $type = $scanner->tokenType(); } return $result; } /** * sum: product | sum '+' product | sum '-' product | sum '~' product */ private static function parseSum(Scanner $scanner) { $result = self::parseProduct($scanner); $type = $scanner->tokenType(); while ($type === '+' || $type === '-' || $type === '~') { $scanner->nextToken(); $result = new ArithExpression($result, self::parseProduct($scanner), $type); $type = $scanner->tokenType(); } return $result; } /** * lt_gt: sum | lt_gt '<' concat | lt_gt IS_SMALLER_OR_EQUAL concat * | lt_gt '>' concat | lt_gt IS_GREATER_OR_EQUAL concat */ private static function parseLtGt(Scanner $scanner) { $result = self::parseSum($scanner); $type = $scanner->tokenType(); while ($type === '<' || $type === T_IS_SMALLER_OR_EQUAL || $type === '>' || $type === T_IS_GREATER_OR_EQUAL) { $scanner->nextToken(); $result = new BooleanExpression($result, self::parseSum($scanner), $type); $type = $scanner->tokenType(); } return $result; } /** * cmp: lt_gt | cmp IS_EQUAL lt_gt | cmp IS_NOT_EQUAL lt_gt */ private static function parseCmp(Scanner $scanner) { $result = self::parseLtGt($scanner); $type = $scanner->tokenType(); while ($type === T_IS_EQUAL || $type === T_IS_NOT_EQUAL) { $scanner->nextToken(); $result = new BooleanExpression($result, self::parseLtGt($scanner), $type); $type = $scanner->tokenType(); } return $result; } /** * and: cmp | and BOOLEAN_AND cmp */ private static function parseAnd(Scanner $scanner) { $result = self::parseCmp($scanner); $type = $scanner->tokenType(); while ($type === T_BOOLEAN_AND) { $scanner->nextToken(); $result = new BooleanExpression($result, self::parseCmp($scanner), $type); $type = $scanner->tokenType(); } return $result; } /** * or: and | or BOOLEAN_OR and */ private static function parseOr(Scanner $scanner) { $result = self::parseAnd($scanner); $type = $scanner->tokenType(); while ($type === T_BOOLEAN_OR) { $scanner->nextToken(); $result = new BooleanExpression($result, self::parseAnd($scanner), $type); $type = $scanner->tokenType(); } return $result; } /** * expr: or | or '?' expr ':' expr | or '?' ':' expr */ private static function parseExpr(Scanner $scanner) { $result = self::parseOr($scanner); if ($scanner->tokenType() === '?') { $scanner->nextToken(); if ($scanner->tokenType() !== ':') { $expr = self::parseExpr($scanner); } else { $expr = $result; } if ($scanner->tokenType() !== ':') { throw new TemplateParserException('missing ":"', $scanner); } $scanner->nextToken(); $result = new ConditionExpression($result, $expr, self::parseExpr($scanner)); } return $result; } } /** * Exception class used to report template parse errors. */ class TemplateParserException extends \Exception { public function __construct($message, $scanner) { $type = $scanner->tokenType(); $value = is_int($type) ? $scanner->tokenValue() : $type; return parent::__construct("$message at \"$value\""); } }