=?|[!=]=|[(){}.,%*\/+~|-]|\[|\]/A'; public function __construct($code, $filename=NULL) { $this->code = preg_replace('/(\r\n|\r|\n)/', '\n', $code); $this->filename = $filename; $this->cursor = 0; $this->lineno = 1; $this->pushedBack = array(); $this->end = strlen($this->code); $this->position = self::POSITION_DATA; } /** * parse the nex token and return it. */ public function nextToken() { // do we have tokens pushed back? get one if (!empty($this->pushedBack)) return array_shift($this->pushedBack); // have we reached the end of the code? if ($this->cursor >= $this->end) return Twig_Token::EOF($this->lineno); // otherwise dispatch to the lexing functions depending // on our current position in the code. switch ($this->position) { case self::POSITION_DATA: $tokens = $this->lexData(); break; case self::POSITION_BLOCK: $tokens = $this->lexBlock(); break; case self::POSITION_VAR: $tokens = $this->lexVar(); break; } // if the return value is not an array it's a token if (!is_array($tokens)) return $tokens; // empty array, call again else if (empty($tokens)) return $this->nextToken(); // if we have multiple items we push them to the buffer else if (count($tokens) > 1) { $first = array_shift($tokens); $this->pushedBack = $tokens; return $first; } // otherwise return the first item of the array. return $tokens[0]; } private function lexData() { $match = NULL; // if no matches are left we return the rest of the template // as simple text token if (!preg_match('/(.*?)(\{[%#]|\$(?!\$))/A', $this->code, $match, NULL, $this->cursor)) { $rv = Twig_Token::Text(substr($this->code, $this->cursor), $this->lineno); $this->cursor = $this->end; return $rv; } $this->cursor += strlen($match[0]); // update the lineno on the instance $lineno = $this->lineno; $this->lineno += substr_count($match[0], '\n'); // push the template text first $text = $match[1]; if (!empty($text)) { $result = array(Twig_Token::Text($text, $lineno)); $lineno += substr_count($text, '\n'); } else $result = array(); // block start token, let's return a token for that. if (($token = $match[2]) !== '$') { // if our section is a comment, just return the text if ($token[1] == '#') { if (!preg_match('/.*?#\}/A', $this->code, $match, NULL, $this->cursor)) throw new Twig_SyntaxError('unclosed comment', $this->lineno); $this->cursor += strlen($match[0]); $this->lineno += substr_count($match[0], '\n'); return $result; } $result[] = new Twig_Token(Twig_Token::BLOCK_START_TYPE, '', $lineno); $this->position = self::POSITION_BLOCK; } // quoted block else if (isset($this->code[$this->cursor]) && $this->code[$this->cursor] == '{') { $this->cursor++; $result[] = new Twig_Token(Twig_Token::VAR_START_TYPE, '', $lineno); $this->position = self::POSITION_VAR; } // inline variable expressions. If there is no name next we // fail silently. $ 42 could be common so no need to be a // dickhead. else if (preg_match(self::REGEX_NAME, $this->code, $match, NULL, $this->cursor)) { $result[] = new Twig_Token(Twig_Token::VAR_START_TYPE, '', $lineno); $result[] = Twig_Token::Name($match[0], $lineno); $this->cursor += strlen($match[0]); // allow attribute lookup while (isset($this->code[$this->cursor]) && $this->code[$this->cursor] === '.') { ++$this->cursor; $result[] = Twig_Token::Operator('.', $this->lineno); if (preg_match(self::REGEX_NAME, $this->code, $match, NULL, $this->cursor)) { $this->cursor += strlen($match[0]); $result[] = Twig_Token::Name($match[0], $this->lineno); } else if (preg_match(self::REGEX_NUMBER, $this->code, $match, NULL, $this->cursor)) { $this->cursor += strlen($match[0]); $result[] = Twig_Token::Number($match[0], $this->lineno); } else { --$this->cursor; break; } } $result[] = new Twig_Token(Twig_Token::VAR_END_TYPE, '', $lineno); } return $result; } private function lexBlock() { $match = NULL; if (preg_match('/\s*%\}/A', $this->code, $match, NULL, $this->cursor)) { $this->cursor += strlen($match[0]); $lineno = $this->lineno; $this->lineno += substr_count($match[0], '\n'); $this->position = self::POSITION_DATA; return new Twig_Token(Twig_Token::BLOCK_END_TYPE, '', $lineno); } return $this->lexExpression(); } private function lexVar() { $match = NULL; if (preg_match('/\s*\}/A', $this->code, $match, NULL, $this->cursor)) { $this->cursor += strlen($match[0]); $lineno = $this->lineno; $this->lineno += substr_count($match[0], '\n'); $this->position = self::POSITION_DATA; return new Twig_Token(Twig_Token::VAR_END_TYPE, '', $lineno); } return $this->lexExpression(); } private function lexExpression() { $match = NULL; // skip whitespace while (preg_match('/\s+/A', $this->code, $match, NULL, $this->cursor)) { $this->cursor += strlen($match[0]); $this->lineno += substr_count($match[0], '\n'); } // sanity check if ($this->cursor >= $this->end) throw new Twig_SyntaxError('unexpected end of stream', $this->lineno, $this->filename); // first parse operators if (preg_match(self::REGEX_OPERATOR, $this->code, $match, NULL, $this->cursor)) { $this->cursor += strlen($match[0]); return Twig_Token::Operator($match[0], $this->lineno); } // now names if (preg_match(self::REGEX_NAME, $this->code, $match, NULL, $this->cursor)) { $this->cursor += strlen($match[0]); return Twig_Token::Name($match[0], $this->lineno); } // then numbers else if (preg_match(self::REGEX_NUMBER, $this->code, $match, NULL, $this->cursor)) { $this->cursor += strlen($match[0]); $value = (float)$match[0]; if ((int)$value === $value) $value = (int)$value; return Twig_Token::Number($value, $this->lineno); } // and finally strings else if (preg_match(self::REGEX_STRING, $this->code, $match, NULL, $this->cursor)) { $this->cursor += strlen($match[0]); $this->lineno += substr_count($match[0], '\n'); $value = stripcslashes(substr($match[0], 1, strlen($match[0]) - 2)); return Twig_Token::String($value, $this->lineno); } // unlexable throw new Twig_SyntaxError("Unexpected character '" . $this->code[$this->cursor] . "'.", $this->lineno, $this->filename); } } /** * Wrapper around a lexer for simplified token access. */ class Twig_TokenStream { private $pushed; private $lexer; public $filename; public $current; public $eof; public function __construct($lexer, $filename) { $this->pushed = array(); $this->lexer = $lexer; $this->filename = $filename; $this->next(); } public function push($token) { $this->pushed[] = $token; } /** * set the pointer to the next token and return the old one. */ public function next() { if (!empty($this->pushed)) $token = array_shift($this->pushed); else $token = $this->lexer->nextToken(); $old = $this->current; $this->current = $token; $this->eof = $token->type === Twig_Token::EOF_TYPE; return $old; } /** * Look at the next token. */ public function look() { $old = $this->next(); $new = $this->current; $this->push($old); $this->push($new); return $new; } /** * Skip some tokens. */ public function skip($times=1) { for ($i = 0; $i < $times; ++$i) $this->next(); } /** * expect a token (like $token->test()) and return it or raise * a syntax error. */ public function expect($primary, $secondary=NULL) { $token = $this->current; if (!$token->test($primary, $secondary)) throw new Twig_SyntaxError('unexpected token', $this->current->lineno); $this->next(); return $token; } /** * Forward that call to the current token. */ public function test($primary, $secondary=NULL) { return $this->current->test($primary, $secondary); } } /** * Simple struct for tokens. */ class Twig_Token { public $type; public $value; public $lineno; const TEXT_TYPE = 0; const EOF_TYPE = -1; const BLOCK_START_TYPE = 1; const VAR_START_TYPE = 2; const BLOCK_END_TYPE = 3; const VAR_END_TYPE = 4; const NAME_TYPE = 5; const NUMBER_TYPE = 6; const STRING_TYPE = 7; const OPERATOR_TYPE = 8; public function __construct($type, $value, $lineno) { $this->type = $type; $this->value = $value; $this->lineno = $lineno; } /** * Test the current token for a type. The first argument is the type * of the token (if not given Twig_Token::NAME_NAME), the second the * value of the token (if not given value is not checked). * the token value can be an array if multiple checks shoudl be * performed. */ public function test($type, $values=NULL) { if (is_null($values) && !is_int($type)) { $values = $type; $type = self::NAME_TYPE; } return ($this->type === $type) && ( is_null($values) || (is_array($values) && in_array($this->value, $values)) || $this->value == $values ); } public static function Text($value, $lineno) { return new Twig_Token(self::TEXT_TYPE, $value, $lineno); } public static function EOF($lineno) { return new Twig_Token(self::EOF_TYPE, '', $lineno); } public static function Name($value, $lineno) { return new Twig_Token(self::NAME_TYPE, $value, $lineno); } public static function Number($value, $lineno) { return new Twig_Token(self::NUMBER_TYPE, $value, $lineno); } public static function String($value, $lineno) { return new Twig_Token(self::STRING_TYPE, $value, $lineno); } public static function Operator($value, $lineno) { return new Twig_Token(self::OPERATOR_TYPE, $value, $lineno); } }