├── php ├── test │ ├── templates │ │ ├── helper.html │ │ ├── layout.html │ │ └── index.html │ └── index.php ├── Twig │ ├── exceptions.php │ ├── compiler.php │ ├── api.php │ ├── runtime.php │ ├── lexer.php │ ├── ast.php │ └── parser.php └── Twig.php ├── README └── spec ├── spec.txt └── spec.html /php/test/templates/helper.html: -------------------------------------------------------------------------------- 1 |

From the Helper

2 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Twig 2 | ==== 3 | 4 | A simple template engine for the Chyrp project. 5 | -------------------------------------------------------------------------------- /php/test/templates/layout.html: -------------------------------------------------------------------------------- 1 | {% block title %}{% endblock %} 2 |
3 | {% block body %}Foo{% endblock %} 4 |
5 | -------------------------------------------------------------------------------- /php/test/index.php: -------------------------------------------------------------------------------- 1 | getTemplate('index.html'); 8 | $index->display(array('seq' => array(1, 2, 3, 4, ''))); 9 | -------------------------------------------------------------------------------- /php/test/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}The Page Title{% endblock %} 3 | {% block body %} 4 |

Hello World

5 | 10 |

{% super %}

11 |
12 | {% include "helper.html" %} 13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /php/Twig/exceptions.php: -------------------------------------------------------------------------------- 1 | lineno = $lineno; 36 | $this->filename = $filename; 37 | } 38 | } 39 | 40 | 41 | /** 42 | * Thrown when Twig encounters an exception at runtime in the Twig 43 | * core. 44 | */ 45 | class Twig_RuntimeError extends Twig_Exception 46 | { 47 | public function __construct($message) 48 | { 49 | parent::__construct($message); 50 | } 51 | } 52 | 53 | 54 | /** 55 | * Raised if the loader is unable to find a template. 56 | */ 57 | class Twig_TemplateNotFound extends Twig_Exception 58 | { 59 | public $name; 60 | 61 | public function __construct($name) 62 | { 63 | parent::__construct('Template not found: ' . $name); 64 | $this->name = $name; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /php/Twig.php: -------------------------------------------------------------------------------- 1 | getTemplate('index.html'); 21 | * 22 | * You can render templates by using the render and display methods. display 23 | * works like render just that it prints the output whereas render returns 24 | * the generated source as string. Both accept an array as context:: 25 | * 26 | * echo $template->render(array('users' => get_list_of_users())); 27 | * $template->display(array('users' => get_list_of_users())); 28 | * 29 | * Custom Loaders 30 | * -------------- 31 | * 32 | * For many applications it's a good idea to subclass the loader to add 33 | * support for multiple template locations. For example many applications 34 | * support plugins and you want to allow plugins to ship themes. 35 | * 36 | * The easiest way is subclassing Twig_Loader and override the getFilename 37 | * method which calculates the path to the template on the file system. 38 | * 39 | * 40 | * :copyright: 2008 by Armin Ronacher. 41 | * :license: BSD. 42 | */ 43 | 44 | 45 | if (!defined('TWIG_BASE')) 46 | define('TWIG_BASE', dirname(__FILE__) . '/Twig'); 47 | define('TWIG_VERSION', '0.1-dev'); 48 | 49 | 50 | // the systems we load automatically on initialization. The compiler 51 | // and other stuff is loaded on first request. 52 | require TWIG_BASE . '/exceptions.php'; 53 | require TWIG_BASE . '/runtime.php'; 54 | require TWIG_BASE . '/api.php'; 55 | -------------------------------------------------------------------------------- /php/Twig/compiler.php: -------------------------------------------------------------------------------- 1 | compile($compiler); 28 | if (is_null($fp)) 29 | return $compiler->getCode(); 30 | } 31 | 32 | 33 | class Twig_Compiler 34 | { 35 | public function format() 36 | { 37 | $arguments = func_get_args(); 38 | $this->raw(call_user_func_array('sprintf', $arguments)); 39 | } 40 | 41 | public function string($value) 42 | { 43 | $this->format('"%s"', addcslashes($value, "\t\"")); 44 | } 45 | 46 | public function repr($value) 47 | { 48 | if (is_int($value) || is_float($value)) 49 | $this->raw($value); 50 | else if (is_null($value)) 51 | $this->raw('NULL'); 52 | else if (is_bool($value)) 53 | $this->raw(value ? 'true' : 'false'); 54 | else if (is_array($value)) { 55 | $this->raw('array('); 56 | $i = 0; 57 | foreach ($value as $key => $value) { 58 | if ($i++) 59 | $this->raw(', '); 60 | $this->repr($key); 61 | $this->raw(' => '); 62 | $this->repr($value); 63 | } 64 | $this->raw(')'); 65 | } 66 | else 67 | $this->string($value); 68 | } 69 | 70 | public function pushContext() 71 | { 72 | $this->raw('$context[\'::parent\'] = $context;'. "\n"); 73 | } 74 | 75 | public function popContext() 76 | { 77 | $this->raw('$context = $context[\'::parent\'];'. "\n"); 78 | } 79 | 80 | public function addDebugInfo($node) 81 | { 82 | $this->raw("/* LINE:$node->lineno */\n"); 83 | } 84 | } 85 | 86 | 87 | class Twig_FileCompiler extends Twig_Compiler 88 | { 89 | private $fp; 90 | 91 | public function __construct($fp) 92 | { 93 | $this->fp = $fp; 94 | } 95 | 96 | public function raw($string) 97 | { 98 | fwrite($this->fp, $string); 99 | } 100 | } 101 | 102 | 103 | class Twig_StringCompiler extends Twig_Compiler 104 | { 105 | private $source; 106 | 107 | public function __construct() 108 | { 109 | $this->source = ''; 110 | } 111 | 112 | public function raw($string) 113 | { 114 | $this->source .= $string; 115 | } 116 | 117 | public function getCode() 118 | { 119 | return $this->source; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /php/Twig/api.php: -------------------------------------------------------------------------------- 1 | instance = $instance; 53 | $this->charset = $charset; 54 | $this->loader = $loader; 55 | } 56 | 57 | /** 58 | * Render the template with the given context and return it 59 | * as string. 60 | */ 61 | public function render($context=NULL) 62 | { 63 | ob_start(); 64 | $this->display($context); 65 | return ob_end_clean(); 66 | } 67 | 68 | /** 69 | * Works like `render()` but prints the output. 70 | */ 71 | public function display($context=NULL) 72 | { 73 | global $twig_current_template; 74 | $old = $twig_current_template; 75 | $twig_current_template = $this; 76 | if (is_null($context)) 77 | $context = array(); 78 | $this->instance->render($context); 79 | $twig_current_template = $old; 80 | } 81 | } 82 | 83 | /** 84 | * Baseclass for custom loaders. Subclasses have to provide a 85 | * getFilename method. 86 | */ 87 | class Twig_BaseLoader 88 | { 89 | public $cache; 90 | public $charset; 91 | 92 | public function __construct($cache=NULL, $charset=NULL) 93 | { 94 | $this->cache = $cache; 95 | $this->charset = $charset; 96 | } 97 | 98 | public function getTemplate($name) 99 | { 100 | $cls = $this->requireTemplate($name); 101 | return new Twig_Template(new $cls, $this->charset, $this); 102 | } 103 | 104 | public function getCacheFilename($name) 105 | { 106 | return $this->cache . '/twig_' . md5($name) . '.cache'; 107 | } 108 | 109 | public function requireTemplate($name) 110 | { 111 | $cls = '__TwigTemplate_' . md5($name); 112 | if (!class_exists($cls)) { 113 | if (is_null($this->cache)) { 114 | $this->evalTemplate($name); 115 | return $cls; 116 | } 117 | $fn = $this->getFilename($name); 118 | if (!file_exists($fn)) 119 | throw new Twig_TemplateNotFound($name); 120 | $cache_fn = $this->getCacheFilename($name); 121 | if (!file_exists($cache_fn) || 122 | filemtime($cache_fn) < filemtime($fn)) { 123 | twig_load_compiler(); 124 | $fp = @fopen($cache_fn, 'wb'); 125 | if (!$fp) { 126 | $this->evalTemplate($name, $fn); 127 | return $cls; 128 | } 129 | $compiler = new Twig_FileCompiler($fp); 130 | $this->compileTemplate($name, $compiler, $fn); 131 | fclose($fp); 132 | } 133 | include $cache_fn; 134 | } 135 | return $cls; 136 | } 137 | 138 | public function compileTemplate($name, $compiler=NULL, $fn=NULL) 139 | { 140 | twig_load_compiler(); 141 | if (is_null($compiler)) { 142 | $compiler = new Twig_StringCompiler(); 143 | $returnCode = true; 144 | } 145 | else 146 | $returnCode = false; 147 | if (is_null($fn)) 148 | $fn = $this->getFilename($name); 149 | 150 | $node = twig_parse(file_get_contents($fn, $name), $name); 151 | $node->compile($compiler); 152 | if ($returnCode) 153 | return $compiler->getCode(); 154 | } 155 | 156 | private function evalTemplate($name, $fn=NULL) 157 | { 158 | $code = $this->compileTemplate($name, NULL, $fn); 159 | eval('?>' . $code); 160 | } 161 | } 162 | 163 | 164 | /** 165 | * Helper class that loads templates. 166 | */ 167 | class Twig_Loader extends Twig_BaseLoader 168 | { 169 | public $folder; 170 | 171 | public function __construct($folder, $cache) 172 | { 173 | parent::__construct($cache); 174 | $this->folder = $folder; 175 | } 176 | 177 | public function getFilename($name) 178 | { 179 | $path = array(); 180 | foreach (explode('/', $name) as $part) { 181 | if ($part[0] != '.') 182 | array_push($path, $part); 183 | } 184 | return $this->folder . '/' . implode('/', $path); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /php/Twig/runtime.php: -------------------------------------------------------------------------------- 1 | 'twig_date_format_filter', 16 | 'numberformat' => 'number_format', 17 | 'moneyformat' => 'money_format', 18 | 'filesizeformat' => 'twig_filesize_format_filter', 19 | 'format' => 'sprintf', 20 | 21 | // numbers 22 | 'even' => 'twig_is_even_filter', 23 | 'odd' => 'twig_is_odd_filter', 24 | 25 | // escaping and encoding 26 | 'escape' => 'htmlspecialchars', 27 | 'e' => 'htmlspecialchars', 28 | 'urlencode' => 'twig_urlencode_filter', 29 | 30 | // string filters 31 | 'title' => 'twig_title_string_filter', 32 | 'capitalize' => 'twig_capitalize_string_filter', 33 | 'upper' => 'strtoupper', 34 | 'lower' => 'strtolower', 35 | 'strip' => 'trim', 36 | 'rstrip' => 'rtrim', 37 | 'lstrip' => 'ltrim', 38 | 39 | // array helpers 40 | 'join' => 'twig_join_filter', 41 | 'reverse' => 'array_reverse', 42 | 'length' => 'count', 43 | 'count' => 'count', 44 | 45 | // iteration and runtime 46 | 'default' => 'twig_default_filter', 47 | 'keys' => 'array_keys', 48 | 'items' => 'twig_get_array_items_filter' 49 | ); 50 | 51 | 52 | class Twig_LoopContextIterator implements Iterator 53 | { 54 | public $context; 55 | public $seq; 56 | public $idx; 57 | public $length; 58 | public $parent; 59 | 60 | public function __construct(&$context, $seq, $parent) 61 | { 62 | $this->context = $context; 63 | $this->seq = $seq; 64 | $this->idx = 0; 65 | $this->length = count($seq); 66 | $this->parent = $parent; 67 | } 68 | 69 | public function rewind() {} 70 | 71 | public function key() {} 72 | 73 | public function valid() 74 | { 75 | return $this->idx < $this->length; 76 | } 77 | 78 | public function next() 79 | { 80 | $this->idx++; 81 | } 82 | 83 | public function current() 84 | { 85 | return $this; 86 | } 87 | } 88 | 89 | /** 90 | * This is called like an ordinary filter just with the name of the filter 91 | * as first argument. Currently we just raise an exception here but it 92 | * would make sense in the future to allow dynamic filter lookup for plugins 93 | * or something like that. 94 | */ 95 | function twig_missing_filter($name) 96 | { 97 | throw new Twig_RuntimeError("filter '$name' does not exist."); 98 | } 99 | 100 | function twig_get_attribute($context, $obj, $item) 101 | { 102 | if (is_array($obj) && isset($obj[$item])) 103 | return $obj[$item]; 104 | if (!is_object($obj)) 105 | return NULL; 106 | if (method_exists($obj, $item)) 107 | return call_user_func(array($obj, $item)); 108 | if (property_exists($obj, $item)) { 109 | $tmp = get_object_vars($obj); 110 | return $tmp[$item]; 111 | } 112 | $method = 'get' . ucfirst($item); 113 | if (method_exists($obj, $method)) 114 | return call_user_func(array($obj, $method)); 115 | return NULL; 116 | } 117 | 118 | function twig_iterate(&$context, $seq) 119 | { 120 | $parent = isset($context['loop']) ? $context['loop'] : null; 121 | $seq = twig_make_array($seq); 122 | $context['loop'] = array('parent' => $parent, 'iterated' => false); 123 | return new Twig_LoopContextIterator($context, $seq, $parent); 124 | } 125 | 126 | function twig_set_loop_context(&$context, $iterator, $target) 127 | { 128 | $context[$target] = $iterator->seq[$iterator->idx]; 129 | $context['loop'] = twig_make_loop_context($iterator); 130 | } 131 | 132 | function twig_set_loop_context_multitarget(&$context, $iterator, $targets) 133 | { 134 | $values = $iterator->seq[$iterator->idx]; 135 | if (!is_array($values)) 136 | $values = array($values); 137 | $idx = 0; 138 | foreach ($values as $value) { 139 | if (!isset($targets[$idx])) 140 | break; 141 | $context[$targets[$idx++]] = $value; 142 | } 143 | $context['loop'] = twig_make_loop_context($iterator); 144 | } 145 | 146 | function twig_make_loop_context($iterator) 147 | { 148 | return array( 149 | 'parent' => $iterator->parent, 150 | 'length' => $iterator->length, 151 | 'index0' => $iterator->idx, 152 | 'index' => $iterator->idx + 1, 153 | 'revindex0' => $iterator->length - $iterator->idx - 1, 154 | 'revindex '=> $iterator->length - $iterator->idx, 155 | 'first' => $iterator->idx == 0, 156 | 'last' => $iterator->idx - 1 == $iterator->length, 157 | 'iterated' => true 158 | ); 159 | } 160 | 161 | function twig_make_array($object) 162 | { 163 | if (is_array($object)) 164 | return array_values($object); 165 | elseif (is_object($object)) { 166 | $result = array(); 167 | foreach ($object as $value) 168 | $result[] = $value; 169 | return $result; 170 | } 171 | return array(); 172 | } 173 | 174 | function twig_date_format_filter($timestamp, $format='F j, Y, G:i') 175 | { 176 | return date($format, $timestamp); 177 | } 178 | 179 | function twig_urlencode_filter($string, $raw=false) 180 | { 181 | if ($raw) 182 | return rawurlencode($url); 183 | return urlencode($url); 184 | } 185 | 186 | function twig_join_filter($value, $glue='') 187 | { 188 | return implode($glue, $value); 189 | } 190 | 191 | function twig_default_filter($value, $default='') 192 | { 193 | return is_null($value) ? $default : $value; 194 | } 195 | 196 | function twig_get_array_items_filter($array) 197 | { 198 | $result = array(); 199 | foreach ($array as $key => $value) 200 | $result[] = array($key, $value); 201 | return $result; 202 | } 203 | 204 | function twig_filesize_format_filter($value) 205 | { 206 | $value = max(0, (int)$value); 207 | $places = strlen($value); 208 | if ($places <= 9 && $places >= 7) { 209 | $value = number_format($value / 1048576, 1); 210 | return "$value MB"; 211 | } 212 | if ($places >= 10) { 213 | $value = number_format($value / 1073741824, 1); 214 | return "$value GB"; 215 | } 216 | $value = number_format($value / 1024, 1); 217 | return "$value KB"; 218 | } 219 | 220 | function twig_is_even_filter($value) 221 | { 222 | return $value % 2 == 0; 223 | } 224 | 225 | function twig_is_odd_filter($value) 226 | { 227 | return $value % 2 == 1; 228 | } 229 | 230 | 231 | // add multibyte extensions if possible 232 | if (function_exists('mb_get_info')) { 233 | function twig_upper_filter($string) 234 | { 235 | $template = twig_get_current_template(); 236 | if (!is_null($template->charset)) 237 | return mb_strtoupper($string, $template->charset); 238 | return strtoupper($string); 239 | } 240 | 241 | function twig_lower_filter($string) 242 | { 243 | $template = twig_get_current_template(); 244 | if (!is_null($template->charset)) 245 | return mb_strtolower($string, $template->charset); 246 | return strtolower($string); 247 | } 248 | 249 | function twig_title_string_filter($string) 250 | { 251 | $template = twig_get_current_template(); 252 | if (is_null($template->charset)) 253 | return ucwords(strtolower($string)); 254 | return mb_convert_case($string, MB_CASE_TITLE, $template->charset); 255 | } 256 | 257 | function twig_capitalize_string_filter($string) 258 | { 259 | $template = twig_get_current_template(); 260 | if (is_null($template->charset)) 261 | return ucfirst(strtolower($string)); 262 | return mb_strtoupper(mb_substr($string, 0, 1, $template->charset)) . 263 | mb_strtolower(mb_substr($string, 1, null, $template->charset)); 264 | } 265 | 266 | // override the builtins 267 | $twig_filters['upper'] = 'twig_upper_filter'; 268 | $twig_filters['lower'] = 'twig_lower_filter'; 269 | } 270 | 271 | // and byte fallback 272 | else { 273 | function twig_title_string_filter($string) 274 | { 275 | return ucwords(strtolower($string)); 276 | } 277 | 278 | function twig_capitalize_string_filter($string) 279 | { 280 | return ucfirst(strtolower($string)); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /php/Twig/lexer.php: -------------------------------------------------------------------------------- 1 | =?|==|[(){}.,%*\/+~|-]|\[|\]/A'; 44 | 45 | public function __construct($code, $filename=NULL) 46 | { 47 | $this->code = preg_replace('/(\r\n|\r|\n)/', '\n', $code); 48 | $this->filename = $filename; 49 | $this->cursor = 0; 50 | $this->lineno = 1; 51 | $this->pushedBack = array(); 52 | $this->end = strlen($this->code); 53 | $this->position = self::POSITION_DATA; 54 | } 55 | 56 | /** 57 | * parse the nex token and return it. 58 | */ 59 | public function nextToken() 60 | { 61 | // do we have tokens pushed back? get one 62 | if (!empty($this->pushedBack)) 63 | return array_shift($this->pushedBack); 64 | // have we reached the end of the code? 65 | if ($this->cursor >= $this->end) 66 | return Twig_Token::EOF($this->lineno); 67 | // otherwise dispatch to the lexing functions depending 68 | // on our current position in the code. 69 | switch ($this->position) { 70 | case self::POSITION_DATA: 71 | $tokens = $this->lexData(); break; 72 | case self::POSITION_BLOCK: 73 | $tokens = $this->lexBlock(); break; 74 | case self::POSITION_VAR: 75 | $tokens = $this->lexVar(); break; 76 | } 77 | 78 | // if the return value is not an array it's a token 79 | if (!is_array($tokens)) 80 | return $tokens; 81 | // empty array, call again 82 | else if (empty($tokens)) 83 | return $this->nextToken(); 84 | // if we have multiple items we push them to the buffer 85 | else if (count($tokens) > 1) { 86 | $first = array_shift($tokens); 87 | $this->pushedBack = $tokens; 88 | return $first; 89 | } 90 | // otherwise return the first item of the array. 91 | return $tokens[0]; 92 | } 93 | 94 | private function lexData() 95 | { 96 | $match = NULL; 97 | 98 | // if no matches are left we return the rest of the template 99 | // as simple text token 100 | if (!preg_match('/(.*?)(\{[%#]|\$(?!\$))/A', $this->code, $match, 101 | NULL, $this->cursor)) { 102 | $rv = Twig_Token::Text(substr($this->code, $this->cursor), 103 | $this->lineno); 104 | $this->cursor = $this->end; 105 | return $rv; 106 | } 107 | $this->cursor += strlen($match[0]); 108 | 109 | // update the lineno on the instance 110 | $lineno = $this->lineno; 111 | $this->lineno += substr_count($match[0], '\n'); 112 | 113 | // push the template text first 114 | $text = $match[1]; 115 | if (!empty($text)) { 116 | $result = array(Twig_Token::Text($text, $lineno)); 117 | $lineno += substr_count($text, '\n'); 118 | } 119 | else 120 | $result = array(); 121 | 122 | // block start token, let's return a token for that. 123 | if (($token = $match[2]) !== '$') { 124 | // if our section is a comment, just return the text 125 | if ($token[1] == '#') { 126 | if (!preg_match('/.*?#\}/A', $this->code, $match, 127 | NULL, $this->cursor)) 128 | throw new Twig_SyntaxError('unclosed comment', 129 | $this->lineno); 130 | $this->cursor += strlen($match[0]); 131 | $this->lineno += substr_count($match[0], '\n'); 132 | return $result; 133 | } 134 | $result[] = new Twig_Token(Twig_Token::BLOCK_START_TYPE, 135 | '', $lineno); 136 | $this->position = self::POSITION_BLOCK; 137 | } 138 | 139 | // quoted block 140 | else if (isset($this->code[$this->cursor]) && 141 | $this->code[$this->cursor] == '{') { 142 | $this->cursor++; 143 | $result[] = new Twig_Token(Twig_Token::VAR_START_TYPE, 144 | '', $lineno); 145 | $this->position = self::POSITION_VAR; 146 | } 147 | 148 | // inline variable expressions. If there is no name next we 149 | // fail silently. $ 42 could be common so no need to be a 150 | // dickhead. 151 | else if (preg_match(self::REGEX_NAME, $this->code, $match, 152 | NULL, $this->cursor)) { 153 | $result[] = new Twig_Token(Twig_Token::VAR_START_TYPE, 154 | '', $lineno); 155 | $result[] = Twig_Token::Name($match[0], $lineno); 156 | $this->cursor += strlen($match[0]); 157 | 158 | // allow attribute lookup 159 | while (isset($this->code[$this->cursor]) && 160 | $this->code[$this->cursor] === '.') { 161 | ++$this->cursor; 162 | $result[] = Twig_Token::Operator('.', $this->lineno); 163 | if (preg_match(self::REGEX_NAME, $this->code, 164 | $match, NULL, $this->cursor)) { 165 | $this->cursor += strlen($match[0]); 166 | $result[] = Twig_Token::Name($match[0], 167 | $this->lineno); 168 | } 169 | else if (preg_match(self::REGEX_NUMBER, $this->code, 170 | $match, NULL, $this->cursor)) { 171 | $this->cursor += strlen($match[0]); 172 | $result[] = Twig_Token::Number($match[0], 173 | $this->lineno); 174 | } 175 | else { 176 | --$this->cursor; 177 | break; 178 | } 179 | } 180 | $result[] = new Twig_Token(Twig_Token::VAR_END_TYPE, 181 | '', $lineno); 182 | } 183 | 184 | return $result; 185 | } 186 | 187 | private function lexBlock() 188 | { 189 | $match = NULL; 190 | if (preg_match('/\s*%\}/A', $this->code, $match, NULL, $this->cursor)) { 191 | $this->cursor += strlen($match[0]); 192 | $lineno = $this->lineno; 193 | $this->lineno += substr_count($match[0], '\n'); 194 | $this->position = self::POSITION_DATA; 195 | return new Twig_Token(Twig_Token::BLOCK_END_TYPE, '', $lineno); 196 | } 197 | return $this->lexExpression(); 198 | } 199 | 200 | private function lexVar() 201 | { 202 | $match = NULL; 203 | if (preg_match('/\s*\}/A', $this->code, $match, NULL, $this->cursor)) { 204 | $this->cursor += strlen($match[0]); 205 | $lineno = $this->lineno; 206 | $this->lineno += substr_count($match[0], '\n'); 207 | $this->position = self::POSITION_DATA; 208 | return new Twig_Token(Twig_Token::VAR_END_TYPE, '', $lineno); 209 | } 210 | return $this->lexExpression(); 211 | } 212 | 213 | private function lexExpression() 214 | { 215 | $match = NULL; 216 | 217 | // skip whitespace 218 | while (preg_match('/\s+/A', $this->code, $match, NULL, 219 | $this->cursor)) { 220 | $this->cursor += strlen($match[0]); 221 | $this->lineno += substr_count($match[0], '\n'); 222 | } 223 | 224 | // sanity check 225 | if ($this->cursor >= $this->end) 226 | throw new Twig_SyntaxError('unexpected end of stream', 227 | $this->lineno, $this->filename); 228 | 229 | // first parse operators 230 | if (preg_match(self::REGEX_OPERATOR, $this->code, $match, NULL, 231 | $this->cursor)) { 232 | $this->cursor += strlen($match[0]); 233 | return Twig_Token::Operator($match[0], $this->lineno); 234 | } 235 | 236 | // now names 237 | if (preg_match(self::REGEX_NAME, $this->code, $match, NULL, 238 | $this->cursor)) { 239 | $this->cursor += strlen($match[0]); 240 | return Twig_Token::Name($match[0], $this->lineno); 241 | } 242 | 243 | // then numbers 244 | else if (preg_match(self::REGEX_NUMBER, $this->code, $match, 245 | NULL, $this->cursor)) { 246 | $this->cursor += strlen($match[0]); 247 | $value = (float)$match[0]; 248 | if ((int)$value === $value) 249 | $value = (int)$value; 250 | return Twig_Token::Number($value, $this->lineno); 251 | } 252 | 253 | // and finally strings 254 | else if (preg_match(self::REGEX_STRING, $this->code, $match, 255 | NULL, $this->cursor)) { 256 | $this->cursor += strlen($match[0]); 257 | $this->lineno += substr_count($match[0], '\n'); 258 | $value = stripcslashes(substr($match[0], 1, strlen($match[0]) - 2)); 259 | return Twig_Token::String($value, $this->lineno); 260 | } 261 | 262 | // unlexable 263 | throw new Twig_SyntaxError("Unexpected character '" . 264 | $this->code[$this->cursor] . "'.", 265 | $this->lineno, $this->filename); 266 | } 267 | } 268 | 269 | 270 | /** 271 | * Wrapper around a lexer for simplified token access. 272 | */ 273 | class Twig_TokenStream 274 | { 275 | private $pushed; 276 | private $lexer; 277 | public $filename; 278 | public $current; 279 | public $eof; 280 | 281 | public function __construct($lexer, $filename) 282 | { 283 | $this->pushed = array(); 284 | $this->lexer = $lexer; 285 | $this->filename = $filename; 286 | $this->next(); 287 | } 288 | 289 | public function push($token) 290 | { 291 | $this->pushed[] = $token; 292 | } 293 | 294 | /** 295 | * set the pointer to the next token and return the old one. 296 | */ 297 | public function next() 298 | { 299 | if (!empty($this->pushed)) 300 | $token = array_shift($this->pushed); 301 | else 302 | $token = $this->lexer->nextToken(); 303 | $old = $this->current; 304 | $this->current = $token; 305 | $this->eof = $token->type === Twig_Token::EOF_TYPE; 306 | return $old; 307 | } 308 | 309 | /** 310 | * Look at the next token. 311 | */ 312 | public function look() 313 | { 314 | $old = $this->next(); 315 | $new = $this->current; 316 | $this->push($old); 317 | $this->push($new); 318 | return $new; 319 | } 320 | 321 | /** 322 | * Skip some tokens. 323 | */ 324 | public function skip($times=1) 325 | { 326 | for ($i = 0; $i < $times; ++$i) 327 | $this->next(); 328 | } 329 | 330 | /** 331 | * expect a token (like $token->test()) and return it or raise 332 | * a syntax error. 333 | */ 334 | public function expect($primary, $secondary=NULL) 335 | { 336 | $token = $this->current; 337 | if (!$token->test($primary, $secondary)) 338 | throw new Twig_SyntaxError('unexpected token', 339 | $this->current->lineno); 340 | $this->next(); 341 | return $token; 342 | } 343 | 344 | /** 345 | * Forward that call to the current token. 346 | */ 347 | public function test($primary, $secondary=NULL) 348 | { 349 | return $this->current->test($primary, $secondary); 350 | } 351 | } 352 | 353 | 354 | /** 355 | * Simple struct for tokens. 356 | */ 357 | class Twig_Token 358 | { 359 | public $type; 360 | public $value; 361 | public $lineno; 362 | 363 | const TEXT_TYPE = 0; 364 | const EOF_TYPE = -1; 365 | const BLOCK_START_TYPE = 1; 366 | const VAR_START_TYPE = 2; 367 | const BLOCK_END_TYPE = 3; 368 | const VAR_END_TYPE = 4; 369 | const NAME_TYPE = 5; 370 | const NUMBER_TYPE = 6; 371 | const STRING_TYPE = 7; 372 | const OPERATOR_TYPE = 8; 373 | 374 | public function __construct($type, $value, $lineno) 375 | { 376 | $this->type = $type; 377 | $this->value = $value; 378 | $this->lineno = $lineno; 379 | } 380 | 381 | /** 382 | * Test the current token for a type. The first argument is the type 383 | * of the token (if not given Twig_Token::NAME_NAME), the second the 384 | * value of the token (if not given value is not checked). 385 | * the token value can be an array if multiple checks shoudl be 386 | * performed. 387 | */ 388 | public function test($type, $values=NULL) 389 | { 390 | if (is_null($values) && !is_int($type)) { 391 | $values = $type; 392 | $type = self::NAME_TYPE; 393 | } 394 | return ($this->type === $type) && ( 395 | is_null($values) || 396 | (is_array($values) && in_array($this->value, $values)) || 397 | $this->value == $values 398 | ); 399 | } 400 | 401 | public static function Text($value, $lineno) 402 | { 403 | return new Twig_Token(self::TEXT_TYPE, $value, $lineno); 404 | } 405 | 406 | public static function EOF($lineno) 407 | { 408 | return new Twig_Token(self::EOF_TYPE, '', $lineno); 409 | } 410 | 411 | public static function Name($value, $lineno) 412 | { 413 | return new Twig_Token(self::NAME_TYPE, $value, $lineno); 414 | } 415 | 416 | public static function Number($value, $lineno) 417 | { 418 | return new Twig_Token(self::NUMBER_TYPE, $value, $lineno); 419 | } 420 | 421 | public static function String($value, $lineno) 422 | { 423 | return new Twig_Token(self::STRING_TYPE, $value, $lineno); 424 | } 425 | 426 | public static function Operator($value, $lineno) 427 | { 428 | return new Twig_Token(self::OPERATOR_TYPE, $value, $lineno); 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /spec/spec.txt: -------------------------------------------------------------------------------- 1 | ================================== 2 | Twig Template Engine Specification 3 | ================================== 4 | 5 | 6 | This specification specifies a simple cross-language template engine for at least 7 | PHP, Python and Ruby. 8 | 9 | 10 | Purpose 11 | ======= 12 | 13 | A language independent and simple template engine is useful for applications that 14 | use code which is written in more than one programming language. Good Examples 15 | are portal systems which use a blog written in Ruby, a forum software written in 16 | PHP and a planet system written in Python. 17 | 18 | 19 | Inspiration 20 | =========== 21 | 22 | Twig uses a syntax similar to the Genshi text templates which in turn were 23 | inspired by django which also inspired Jinja (all three of them python template 24 | engines) which inspired the Twig runtime environment. 25 | 26 | 27 | Undefined Behavior 28 | ================== 29 | 30 | To simplify porting the template language to different platforms in a couple of 31 | situations the behavior is undefined. Template authors may never take advantage 32 | of such a situation! 33 | 34 | 35 | Syntax 36 | ====== 37 | 38 | I'm too lazy to write down the syntax as BNF diagram but the following snippet 39 | should explain the syntax elements:: 40 | 41 | 42 | {# This is a comment #} 43 | {% block title %}Page Title Goes Here{% endblock %} 44 | {% if show_navigation %} 45 | 52 | {% endif %} 53 |
{% block body %}{% endblock %}
54 | 55 | 56 | Comments and Whitespace 57 | ----------------------- 58 | 59 | Everything between ``{#`` and ``#}`` is ignored by the lexer. Inside blocks and 60 | variable sections the Lexer has to remove whitespace too. 61 | 62 | 63 | Output Expressions 64 | ------------------ 65 | 66 | To output expressions two syntaxes exists. Simple variable output or full 67 | expression output:: 68 | 69 | $this.is.a.variable.output 70 | ${expression|goes|here} 71 | 72 | The former is what we call a variable expression, the second a full expression. 73 | Variable expressions might not contain whitespace where as a full expression 74 | must print the output of the full wrapped expression. 75 | 76 | 77 | Expressions 78 | ----------- 79 | 80 | Expressions allow basic string manipulation and arithmetic calculations. It 81 | an infix syntax with the following operators in this precedence: 82 | 83 | =========== ============================================================== 84 | Operator Description 85 | =========== ============================================================== 86 | ``+`` Convert both arguments into a number and add them up. 87 | ``-`` Convert both arguments into a number and substract them. 88 | ``*`` Convert both arguments into a number and multiply them. 89 | ``/`` Convert both arguments into a number and divide them. 90 | ``%`` Convert both arguments into a number and calculate the rest 91 | of the integer division. 92 | ``~`` Convert both arguments into a string and concatenate them. 93 | ``or`` True if the left or the right expression is true. 94 | ``and`` True if the left and the right expression is true. 95 | ``not`` negate the expression 96 | =========== ============================================================== 97 | 98 | All number conversions have an undefined precision but the implementations 99 | should try to select the best possible type. For example if the implementation 100 | sees an integer and a float that looks like an integer it may convert the 101 | latter into a long and add them. 102 | 103 | Use parentheses to group expressions. 104 | 105 | If an object cannot be compared the implementation might raise an error or fail 106 | silently. Template authors may never apply mathematical operators to untrusted 107 | data. This is especially true for the php implementation where the following 108 | outputs ``42``:: 109 | 110 | ${"foo41" + 1} 111 | 112 | This is undefined behavior and will break on different implementations or 113 | return ``0`` as ``"foo41"`` is not a valid number. 114 | 115 | Types 116 | ~~~~~ 117 | 118 | The following types exist: 119 | 120 | =========== =============== ============================================== 121 | Type Literal Description 122 | =========== =============== ============================================== 123 | ``integer`` `\d+` one of the two numeric types. Which of them 124 | is used when is up to the implementation. 125 | ``float``` `\d+\.\d+` floating point values. 126 | ``string`` see below a unicode string. The PHP implementation has 127 | to use bytestrings here and may use mb_string 128 | ``bool`` `(true|false)` Represents boolean values 129 | ``none`` `none` This type is returned on missing variables or 130 | attributes. 131 | =========== =============== ============================================== 132 | 133 | string regex:: 134 | 135 | (?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')(?sm) 136 | 137 | Attribute Lookup 138 | ~~~~~~~~~~~~~~~~ 139 | 140 | There are two ways to look up attributes on objects. The dot and the 141 | subscript syntax, both inspired by JavaScript. Basically the following 142 | expressions do the very same:: 143 | 144 | foo.name.0 145 | foo['name'][0] 146 | 147 | This is useful to dynamically get attributes from objects:: 148 | 149 | foo[bar] 150 | 151 | The underlaying implementation is free to specify on it's own what an attribute 152 | lookup means. The PHP reference implementation for example performs those 153 | actions on ``foo.bar``: 154 | 155 | - try ``$foo['bar']`` 156 | - try ``$foo->bar()`` 157 | - try ``$foo->bar`` 158 | - try ``$foo->getBar()`` 159 | 160 | The first match returns the object, attribute access to not existing attributes 161 | returns `none`. 162 | 163 | Filtering 164 | ~~~~~~~~~ 165 | 166 | The template language does not specify function calls but filters can be used 167 | to further modify variables using functions the template engine provides. 168 | 169 | The following snippet shows how filters are translated to function calls:: 170 | 171 | ${42|foo(1, 2)|bar|baz} 172 | -> baz(bar(foo(42, 1, 2))) 173 | 174 | The following filters must be provided by the implementation: 175 | 176 | =================== ====================================================== 177 | Name Description 178 | =================== ====================================================== 179 | `date` Format the date using the PHP date formatting rules. 180 | This may sound like a nonstandard way of formatting 181 | dates but it's a way very popular among template 182 | designers and also used by django. 183 | `numberformat` Apply number formatting on the string. This may or 184 | may not use local specific rules. 185 | `moneyformat` Like `numberformat` but for money. 186 | `filesizeformat` Takes a number of bytes and displays it as KB/MB/GB 187 | `format` applies `sprintf` formatting on the string:: 188 | 189 | ${"%s %2f" | format(string, float)} 190 | `escape` apply HTML escaping on a string. This also has to 191 | convert `"` to `" but leave `'` unmodified. 192 | `e` alias for `escape` 193 | `urlencode` URL encode the string. If the second parameter is 194 | true this function should encode for path sections, 195 | otherwise for query strings. 196 | `title` Make the string lowercase and upper case the first 197 | characters of all words. 198 | `capitalize` Like `title` but capitalizes only the first char of 199 | the whole string. 200 | `upper` Convert the string to uppercase 201 | `lower` Convert the string to lowercase 202 | `strip` Trim leading and trailing whitespace 203 | `lstrip` Trim leading whitespace 204 | `rstrip` Trim trailing whitespace 205 | `join` Concatenate the array items and join them with the 206 | string provided or an empty string. 207 | `reverse` Reverse the Array items 208 | `count` Count the number of items in an array or string 209 | characters. 210 | `length` alias for `count`. 211 | `default` If the value is `none` the first argument is returned 212 | `even` Is the number even? 213 | `odd` Is the number odd? 214 | =================== ====================================================== 215 | 216 | 217 | For Loops 218 | --------- 219 | 220 | Iteration works via for loops. Loops work a bit like their python counterparts 221 | just that they don't support multilevel tuple unpacking and that they add a new 222 | layer to the context. Thus at the end of the iteration all the modifications on 223 | the context disappear. Additionally inside loops you have access to a special 224 | `loop` object which provides runtime information:: 225 | 226 | ====================== =================================================== 227 | Variable Description 228 | ====================== =================================================== 229 | ``loop.index`` The current iteration of the loop (1-indexed) 230 | ``loop.index0`` The current iteration of the loop (0-indexed) 231 | ``loop.revindex`` The number of iterations from the end of the 232 | loop (1-indexed) 233 | ``loop.revindex0`` The number of iterations from the end of the 234 | loop (0-indexed) 235 | ``loop.first`` True if this is the first time through the loop 236 | ``loop.last`` True if this is the last time through the loop 237 | ``loop.parent`` For nested loops, this is the loop "above" the 238 | current one 239 | ====================== =================================================== 240 | 241 | Additionally for loops can have an `else` section that is executed if no 242 | iteration took place. 243 | 244 | Example 245 | ~~~~~~~ 246 | 247 | :: 248 | 249 | 256 | 257 | 258 | Notes on Iteration 259 | ~~~~~~~~~~~~~~~~~~ 260 | 261 | Because we have to cope with PHP too which has problematic arrays which are 262 | neither hashmaps nor lists we have a no support for associativ array iteration 263 | at all. How to iterate over associative array then? Using a filter:: 264 | 265 | {% for key, value in array|items %} 266 | ... 267 | {% endfor %} 268 | 269 | To iterate over the keys only:: 270 | 271 | {% for key in array|key %} 272 | ... 273 | {% endfor %} 274 | 275 | 276 | If Conditions 277 | ------------- 278 | 279 | If conditions work like like Ruby, PHP and Python just that we use PHP 280 | keywords. And it's `elseif` and not `else if`:: 281 | 282 | {% if expr1 %} 283 | ... 284 | {% elseif expr2 %} 285 | ... 286 | {% else %} 287 | ... 288 | {% endif %} 289 | 290 | 291 | Inheritance 292 | ----------- 293 | 294 | emplate inheritance allows you to build a base "skeleton" template that 295 | contains all the common elements of your site and defines **blocks** that 296 | child templates can override. 297 | 298 | Here a small template inheritance example:: 299 | 300 | 301 | 302 | 303 | {% block title %}My site{% endblock %} 304 | 312 |
313 | {% block content %}{% endblock %} 314 |
315 | 316 | 317 | If we call that template "base.html" a "index.html" template could override 318 | it and fill in the blocks:: 319 | 320 | {% extends "base.html" %} 321 | {% block title %}Foo — {% super %}{% endblock %} 322 | {% block content %} 323 | This is the content 324 | {% endblock %} 325 | 326 | By using `{% super %}` you can render the parent's block. The template 327 | filenames must be constant strings (we don't support dynamic inheritance 328 | for simplicity) and are relative to the loader folder, not the current 329 | template. 330 | -------------------------------------------------------------------------------- /php/Twig/ast.php: -------------------------------------------------------------------------------- 1 | lineno = $lineno; 20 | } 21 | 22 | public function compile($compiler) 23 | { 24 | } 25 | } 26 | 27 | 28 | class Twig_NodeList extends Twig_Node 29 | { 30 | public $nodes; 31 | 32 | public function __construct($nodes, $lineno) 33 | { 34 | parent::__construct($lineno); 35 | $this->nodes = $nodes; 36 | } 37 | 38 | public function compile($compiler) 39 | { 40 | foreach ($this->nodes as $node) 41 | $node->compile($compiler); 42 | } 43 | 44 | public static function fromArray($array, $lineno) 45 | { 46 | if (count($array) == 1) 47 | return $array[0]; 48 | return new Twig_NodeList($array, $lineno); 49 | } 50 | } 51 | 52 | 53 | class Twig_Module extends Twig_Node 54 | { 55 | public $body; 56 | public $extends; 57 | public $blocks; 58 | public $filename; 59 | public $id; 60 | 61 | public function __construct($body, $extends, $blocks, $filename) 62 | { 63 | parent::__construct(1); 64 | $this->body = $body; 65 | $this->extends = $extends; 66 | $this->blocks = $blocks; 67 | $this->filename = $filename; 68 | } 69 | 70 | public function compile($compiler) 71 | { 72 | $compiler->raw("extends)) { 74 | $compiler->raw('$this->requireTemplate('); 75 | $compiler->repr($this->extends); 76 | $compiler->raw(");\n"); 77 | } 78 | $compiler->raw('class __TwigTemplate_' . md5($this->filename)); 79 | if (!is_null($this->extends)) { 80 | $parent = md5($this->extends); 81 | $compiler->raw(" extends __TwigTemplate_$parent {\n"); 82 | } 83 | else { 84 | $compiler->raw(" {\npublic function render(&\$context) {\n"); 85 | $this->body->compile($compiler); 86 | $compiler->raw("}\n"); 87 | } 88 | 89 | foreach ($this->blocks as $node) 90 | $node->compile($compiler); 91 | 92 | $compiler->raw("}\n"); 93 | } 94 | } 95 | 96 | 97 | class Twig_Print extends Twig_Node 98 | { 99 | public $expr; 100 | 101 | public function __construct($expr, $lineno) 102 | { 103 | parent::__construct($lineno); 104 | $this->expr = $expr; 105 | } 106 | 107 | public function compile($compiler) 108 | { 109 | $compiler->addDebugInfo($this); 110 | $compiler->raw('echo '); 111 | $this->expr->compile($compiler); 112 | $compiler->raw(";\n"); 113 | } 114 | } 115 | 116 | 117 | class Twig_Text extends Twig_Node 118 | { 119 | public $data; 120 | 121 | public function __construct($data, $lineno) 122 | { 123 | parent::__construct($lineno); 124 | $this->data = $data; 125 | } 126 | 127 | public function compile($compiler) 128 | { 129 | $compiler->addDebugInfo($this); 130 | $compiler->raw('echo '); 131 | $compiler->string($this->data); 132 | $compiler->raw(";\n"); 133 | } 134 | } 135 | 136 | 137 | class Twig_ForLoop extends Twig_Node 138 | { 139 | public $is_multitarget; 140 | public $item; 141 | public $seq; 142 | public $body; 143 | public $else; 144 | 145 | public function __construct($is_multitarget, $item, $seq, $body, $else, 146 | $lineno) 147 | { 148 | parent::__construct($lineno); 149 | $this->is_multitarget = $is_multitarget; 150 | $this->item = $item; 151 | $this->seq = $seq; 152 | $this->body = $body; 153 | $this->else = $else; 154 | $this->lineno = $lineno; 155 | } 156 | 157 | public function compile($compiler) 158 | { 159 | $compiler->addDebugInfo($this); 160 | $compiler->pushContext(); 161 | $compiler->raw('foreach (twig_iterate($context, '); 162 | $this->seq->compile($compiler); 163 | $compiler->raw(") as \$iterator) {\n"); 164 | if ($this->is_multitarget) { 165 | $compiler->raw('twig_set_loop_context_multitarget($context, ' . 166 | '$iterator, array('); 167 | $idx = 0; 168 | foreach ($this->item as $node) { 169 | if ($idx++) 170 | $compiler->raw(', '); 171 | $compiler->repr($node->name); 172 | } 173 | $compiler->raw("));\n"); 174 | } 175 | else { 176 | $compiler->raw('twig_set_loop_context($context, $iterator, '); 177 | $compiler->repr($this->item->name); 178 | $compiler->raw(");\n"); 179 | } 180 | $this->body->compile($compiler); 181 | $compiler->raw("}\n"); 182 | if (!is_null($this->else)) { 183 | $compiler->raw("if (!\$context['loop']['iterated']) {\n"); 184 | $this->else->compile($compiler); 185 | $compiler->raw('}'); 186 | } 187 | $compiler->popContext(); 188 | } 189 | } 190 | 191 | 192 | class Twig_IfCondition extends Twig_Node 193 | { 194 | public $tests; 195 | public $else; 196 | 197 | public function __construct($tests, $else, $lineno) 198 | { 199 | parent::__construct($lineno); 200 | $this->tests = $tests; 201 | $this->else = $else; 202 | } 203 | 204 | public function compile($compiler) 205 | { 206 | $compiler->addDebugInfo($this); 207 | $idx = 0; 208 | foreach ($this->tests as $test) { 209 | $compiler->raw(($idx++ ? "}\n else " : '') . 'if ('); 210 | $test[0]->compile($compiler); 211 | $compiler->raw(") {\n"); 212 | $test[1]->compile($compiler); 213 | } 214 | if (!is_null($this->else)) { 215 | $compiler->raw("} else {\n"); 216 | $this->else->compile($compiler); 217 | } 218 | $compiler->raw("}\n"); 219 | } 220 | } 221 | 222 | 223 | class Twig_Block extends Twig_Node 224 | { 225 | public $name; 226 | public $body; 227 | public $parent; 228 | 229 | public function __construct($name, $body, $lineno, $parent=NULL) 230 | { 231 | parent::__construct($lineno); 232 | $this->name = $name; 233 | $this->body = $body; 234 | $this->parent = $parent; 235 | } 236 | 237 | public function replace($other) 238 | { 239 | $this->body = $other->body; 240 | } 241 | 242 | public function compile($compiler) 243 | { 244 | $compiler->addDebugInfo($this); 245 | $compiler->format('public function block_%s($context) {' . "\n", 246 | $this->name); 247 | if (!is_null($this->parent)) 248 | $compiler->raw('$context[\'::superblock\'] = array($this, ' . 249 | "'parent::block_$this->name');\n"); 250 | $this->body->compile($compiler); 251 | $compiler->format("}\n\n"); 252 | } 253 | } 254 | 255 | 256 | class Twig_BlockReference extends Twig_Node 257 | { 258 | public $name; 259 | 260 | public function __construct($name, $lineno) 261 | { 262 | parent::__construct($lineno); 263 | $this->name = $name; 264 | } 265 | 266 | public function compile($compiler) 267 | { 268 | $compiler->addDebugInfo($this); 269 | $compiler->format('$this->block_%s($context);' . "\n", $this->name); 270 | } 271 | } 272 | 273 | 274 | class Twig_Super extends Twig_Node 275 | { 276 | public $block_name; 277 | 278 | public function __construct($block_name, $lineno) 279 | { 280 | parent::__construct($lineno); 281 | $this->block_name = $block_name; 282 | } 283 | 284 | public function compile($compiler) 285 | { 286 | $compiler->addDebugInfo($this); 287 | $compiler->raw('parent::block_' . $this->block_name . '($context);' . "\n"); 288 | } 289 | } 290 | 291 | 292 | class Twig_Include extends Twig_Node 293 | { 294 | public $expr; 295 | 296 | public function __construct($expr, $lineno) 297 | { 298 | parent::__construct($lineno); 299 | $this->expr = $expr; 300 | } 301 | 302 | public function compile($compiler) 303 | { 304 | $compiler->addDebugInfo($this); 305 | $compiler->raw('twig_get_current_template()->loader->getTemplate('); 306 | $this->expr->compile($compiler); 307 | $compiler->raw(')->display($context);' . "\n"); 308 | } 309 | } 310 | 311 | 312 | class Twig_Expression extends Twig_Node 313 | { 314 | 315 | } 316 | 317 | 318 | class Twig_ConditionalExpression extends Twig_Expression 319 | { 320 | public $expr1; 321 | public $expr2; 322 | public $expr3; 323 | 324 | public function __construct($expr1, $expr2, $expr3, $lineno) 325 | { 326 | parent::__construct($lineno); 327 | $this->expr1 = $expr1; 328 | $this->expr2 = $expr2; 329 | $this->expr3 = $expr3; 330 | } 331 | 332 | public function compile($compiler) 333 | { 334 | $compiler->raw('('); 335 | $this->expr1->compile($compiler); 336 | $compiler->raw(') ? ('); 337 | $this->expr2->compile($compiler); 338 | $compiler->raw(') ; ('); 339 | $this->expr3->compile($compiler); 340 | $compiler->raw(')'); 341 | } 342 | } 343 | 344 | 345 | class Twig_BinaryExpression extends Twig_Expression 346 | { 347 | public $left; 348 | public $right; 349 | 350 | public function __construct($left, $right, $lineno) 351 | { 352 | parent::__construct($lineno); 353 | $this->left = $left; 354 | $this->right = $right; 355 | } 356 | 357 | public function compile($compiler) 358 | { 359 | $this->raw('('); 360 | $this->left->compile($compiler); 361 | $this->raw(') '); 362 | $this->operator($compiler); 363 | $this->raw(' ('); 364 | $this->right->compile($compiler); 365 | $this->raw(')'); 366 | } 367 | } 368 | 369 | 370 | class Twig_OrExpression extends Twig_BinaryExpression 371 | { 372 | public function operator($compiler) 373 | { 374 | return $compiler->raw('||'); 375 | } 376 | } 377 | 378 | 379 | class Twig_AndExpression extends Twig_BinaryExpression 380 | { 381 | public function operator($compiler) 382 | { 383 | return $compiler->raw('&&'); 384 | } 385 | } 386 | 387 | 388 | class Twig_AddExpression extends Twig_BinaryExpression 389 | { 390 | public function operator($compiler) 391 | { 392 | return $compiler->raw('+'); 393 | } 394 | } 395 | 396 | 397 | class Twig_SubExpression extends Twig_BinaryExpression 398 | { 399 | public function operator($compiler) 400 | { 401 | return $compiler->raw('-'); 402 | } 403 | } 404 | 405 | 406 | class Twig_ConcatExpression extends Twig_BinaryExpression 407 | { 408 | public function operator($compiler) 409 | { 410 | return $compiler->raw('.'); 411 | } 412 | } 413 | 414 | 415 | class Twig_MulExpression extends Twig_BinaryExpression 416 | { 417 | public function operator($compiler) 418 | { 419 | return $compiler->raw('*'); 420 | } 421 | } 422 | 423 | 424 | class Twig_DivExpression extends Twig_BinaryExpression 425 | { 426 | public function operator($compiler) 427 | { 428 | return $compiler->raw('/'); 429 | } 430 | } 431 | 432 | 433 | class Twig_ModExpression extends Twig_BinaryExpression 434 | { 435 | public function operator($compiler) 436 | { 437 | return $compiler->raw('%'); 438 | } 439 | } 440 | 441 | 442 | class Twig_CompareExpression extends Twig_Expression 443 | { 444 | public $expr; 445 | public $ops; 446 | 447 | public function __construct($expr, $ops, $lineno) 448 | { 449 | parent::__construct($lineno); 450 | $this->expr = $expr; 451 | $this->ops = $ops; 452 | } 453 | 454 | public function compile($compiler) 455 | { 456 | $this->expr->compile($compiler); 457 | $i = 0; 458 | foreach ($this->ops as $op) { 459 | if ($i) 460 | $compiler->raw(') && ($tmp' . $i); 461 | list($op, $node) = $op; 462 | $compiler->raw(' ' . $op . ' '); 463 | $compiler->raw('($tmp' . ++$i . ' = '); 464 | $node->compile($compiler); 465 | $compiler->raw(')'); 466 | } 467 | $compiler->raw(')'); 468 | } 469 | } 470 | 471 | 472 | class Twig_UnaryExpression extends Twig_Expression 473 | { 474 | public $node; 475 | 476 | public function __construct($node, $lineno) 477 | { 478 | parent::__construct($lineno); 479 | $this->node = $node; 480 | } 481 | 482 | public function compile($compiler) 483 | { 484 | $this->raw('('); 485 | $this->operator($compiler); 486 | $this->node->compile($compiler); 487 | $this->raw(')'); 488 | } 489 | } 490 | 491 | 492 | class Twig_NotExpression extends Twig_UnaryExpression 493 | { 494 | public function operator($compiler) 495 | { 496 | $compiler->raw('!'); 497 | } 498 | } 499 | 500 | 501 | class Twig_NegExpression extends Twig_UnaryExpression 502 | { 503 | public function operator($compiler) 504 | { 505 | $compiler->raw('-'); 506 | } 507 | } 508 | 509 | 510 | class Twig_PosExpression extends Twig_UnaryExpression 511 | { 512 | public function operator($compiler) 513 | { 514 | $compiler->raw('+'); 515 | } 516 | } 517 | 518 | 519 | class Twig_Constant extends Twig_Expression 520 | { 521 | public $value; 522 | 523 | public function __construct($value, $lineno) 524 | { 525 | parent::__construct($lineno); 526 | $this->value = $value; 527 | } 528 | 529 | public function compile($compiler) 530 | { 531 | $compiler->repr($this->value); 532 | } 533 | } 534 | 535 | 536 | class Twig_NameExpression extends Twig_Expression 537 | { 538 | public $name; 539 | 540 | public function __construct($name, $lineno) 541 | { 542 | parent::__construct($lineno); 543 | $this->name = $name; 544 | } 545 | 546 | public function compile($compiler) 547 | { 548 | $compiler->format('(isset($context[\'%s\']) ? $context[\'%s\'] ' . 549 | ': NULL)', $this->name, $this->name); 550 | } 551 | } 552 | 553 | 554 | class Twig_AssignNameExpression extends Twig_NameExpression 555 | { 556 | 557 | public function compile($compiler) 558 | { 559 | $compiler->format('$context[\'%s\']', $this->name); 560 | } 561 | } 562 | 563 | 564 | class Twig_GetAttrExpression extends Twig_Expression 565 | { 566 | public $node; 567 | public $attr; 568 | 569 | public function __construct($node, $attr, $lineno) 570 | { 571 | parent::__construct($lineno); 572 | $this->node = $node; 573 | $this->attr = $attr; 574 | } 575 | 576 | public function compile($compiler) 577 | { 578 | $compiler->raw('twig_get_attribute($context, '); 579 | $this->node->compile($compiler); 580 | $compiler->raw(', '); 581 | $this->attr->compile($compiler); 582 | $compiler->raw(')'); 583 | } 584 | } 585 | 586 | 587 | class Twig_FilterExpression extends Twig_Expression 588 | { 589 | public $node; 590 | public $filters; 591 | 592 | public function __construct($node, $filters, $lineno) 593 | { 594 | parent::__construct($lineno); 595 | $this->node = $node; 596 | $this->filters = $filters; 597 | } 598 | 599 | public function compile($compiler) 600 | { 601 | global $twig_filters; 602 | $postponed = array(); 603 | for ($i = count($this->filters) - 1; $i >= 0; --$i) { 604 | list($name, $attrs) = $this->filters[$i]; 605 | if (!isset($twig_filters[$name])) { 606 | $compiler->raw('twig_missing_filter('); 607 | $compiler->repr($name); 608 | $compiler->raw(', '); 609 | } 610 | else 611 | $compiler->raw($twig_filters[$name] . '('); 612 | $postponed[] = $attrs; 613 | } 614 | $this->node->compile($compiler); 615 | foreach ($postponed as $attributes) { 616 | foreach ($attributes as $node) { 617 | $compiler->raw(', '); 618 | $node->compile($compiler); 619 | } 620 | $compiler->raw(')'); 621 | } 622 | } 623 | } 624 | -------------------------------------------------------------------------------- /php/Twig/parser.php: -------------------------------------------------------------------------------- 1 | parse(); 18 | } 19 | 20 | 21 | class Twig_Parser 22 | { 23 | public $stream; 24 | public $blocks; 25 | public $extends; 26 | public $current_block; 27 | private $handlers; 28 | 29 | public function __construct($stream) 30 | { 31 | $this->stream = $stream; 32 | $this->extends = NULL; 33 | $this->blocks = array(); 34 | $this->current_block = NULL; 35 | $this->handlers = array( 36 | 'for' => array($this, 'parseForLoop'), 37 | 'if' => array($this, 'parseIfCondition'), 38 | 'extends' => array($this, 'parseExtends'), 39 | 'include' => array($this, 'parseInclude'), 40 | 'block' => array($this, 'parseBlock'), 41 | 'super' => array($this, 'parseSuper') 42 | ); 43 | } 44 | 45 | public function parseForLoop($token) 46 | { 47 | $lineno = $token->lineno; 48 | list($is_multitarget, $item) = $this->parseAssignmentExpression(); 49 | $this->stream->expect('in'); 50 | $seq = $this->parseExpression(); 51 | $this->stream->expect(Twig_Token::BLOCK_END_TYPE); 52 | $body = $this->subparse(array($this, 'decideForFork')); 53 | if ($this->stream->next()->value == 'else') { 54 | $this->stream->expect(Twig_Token::BLOCK_END_TYPE); 55 | $else = $this->subparse(array($this, 'decideForEnd'), true); 56 | } 57 | else 58 | $else = NULL; 59 | $this->stream->expect(Twig_Token::BLOCK_END_TYPE); 60 | return new Twig_ForLoop($is_multitarget, $item, $seq, $body, $else, 61 | $lineno); 62 | } 63 | 64 | public function decideForFork($token) 65 | { 66 | return $token->test(array('else', 'endfor')); 67 | } 68 | 69 | public function decideForEnd($token) 70 | { 71 | return $token->test('endfor'); 72 | } 73 | 74 | public function parseIfCondition($token) 75 | { 76 | $lineno = $token->lineno; 77 | $expr = $this->parseExpression(); 78 | $this->stream->expect(Twig_Token::BLOCK_END_TYPE); 79 | $body = $this->subparse(array($this, 'decideIfFork')); 80 | $tests = array(array($expr, $body)); 81 | $else = NULL; 82 | 83 | while (true) { 84 | switch ($this->stream->current->value) { 85 | case 'else': 86 | $this->stream->next(); 87 | $this->stream->expect(Twig_Token::BLOCK_END_TYPE); 88 | $else = $this->subparse(array($this, 'decideIfEnd')); 89 | break; 90 | case 'elseif': 91 | $this->stream->next(); 92 | $expr = $this->parseExpression(); 93 | $this->stream->expect(Twig_Token::BLOCK_END_TYPE); 94 | $body = $this->subparse(array($this, 'decideIfFork')); 95 | $tests[] = array($expr, $body); 96 | continue; 97 | default: 98 | $this->stream->next(); 99 | } 100 | break; 101 | } 102 | 103 | $this->stream->expect(Twig_Token::BLOCK_END_TYPE); 104 | return new Twig_IfCondition($tests, $else, $lineno); 105 | } 106 | 107 | public function decideIfFork($token) 108 | { 109 | return $token->test(array('elseif', 'else', 'endif')); 110 | } 111 | 112 | public function decideIfEnd($token) 113 | { 114 | return $token->test('endif'); 115 | } 116 | 117 | public function parseBlock($token) 118 | { 119 | $lineno = $token->lineno; 120 | $name = $this->stream->expect(Twig_Token::NAME_TYPE)->value; 121 | if (isset($this->blocks[$name])) 122 | throw new Twig_SyntaxError("block '$name' defined twice.", 123 | $lineno); 124 | $this->current_block = $name; 125 | $this->stream->expect(Twig_Token::BLOCK_END_TYPE); 126 | $body = $this->subparse(array($this, 'decideBlockEnd'), true); 127 | $this->stream->expect(Twig_Token::BLOCK_END_TYPE); 128 | $block = new Twig_Block($name, $body, $lineno); 129 | $this->blocks[$name] = $block; 130 | $this->current_block = NULL; 131 | return new Twig_BlockReference($name, $lineno); 132 | } 133 | 134 | public function decideBlockEnd($token) 135 | { 136 | return $token->test('endblock'); 137 | } 138 | 139 | public function parseExtends($token) 140 | { 141 | $lineno = $token->lineno; 142 | if (!is_null($this->extends)) 143 | throw new Twig_SyntaxError('multiple extend tags', $lineno); 144 | $this->extends = $this->stream->expect(Twig_Token::STRING_TYPE)->value; 145 | $this->stream->expect(Twig_Token::BLOCK_END_TYPE); 146 | return NULL; 147 | } 148 | 149 | public function parseInclude($token) 150 | { 151 | $expr = $this->parseExpression(); 152 | $this->stream->expect(Twig_Token::BLOCK_END_TYPE); 153 | return new Twig_Include($expr, $token->lineno); 154 | } 155 | 156 | public function parseSuper($token) 157 | { 158 | if (is_null($this->current_block)) 159 | throw new Twig_SyntaxError('super outside block', $token->lineno); 160 | $this->stream->expect(Twig_Token::BLOCK_END_TYPE); 161 | return new Twig_Super($this->current_block, $token->lineno); 162 | } 163 | 164 | public function parseExpression() 165 | { 166 | return $this->parseConditionalExpression(); 167 | } 168 | 169 | public function parseConditionalExpression() 170 | { 171 | $lineno = $this->stream->current->lineno; 172 | $expr1 = $this->parseOrExpression(); 173 | while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '?')) { 174 | $this->stream->next(); 175 | $expr2 = $this->parseOrExpression(); 176 | $this->stream->expect(Twig_Token::OPERATOR_TYPE, ':'); 177 | $expr3 = $this->parseConditionalExpression(); 178 | $expr1 = new Twig_ConditionalExpression($expr1, $expr2, $expr3, 179 | $this->lineno); 180 | $lineno = $this->stream->current->lineno; 181 | } 182 | return $expr1; 183 | } 184 | 185 | public function parseOrExpression() 186 | { 187 | $lineno = $this->stream->current->lineno; 188 | $left = $this->parseAndExpression(); 189 | while ($this->stream->test('or')) { 190 | $this->stream->next(); 191 | $right = $this->parseAndExpression(); 192 | $left = new Twig_OrExpression($left, $right, $lineno); 193 | $lineno = $this->stream->current->lineno; 194 | } 195 | return $left; 196 | } 197 | 198 | public function parseAndExpression() 199 | { 200 | $lineno = $this->stream->current->lineno; 201 | $left = $this->parseCompareExpression(); 202 | while ($this->stream->test('and')) { 203 | $this->stream->next(); 204 | $right = $this->parseCompareExpression(); 205 | $left = new Twig_AndExpression($left, $right, $lineno); 206 | $lineno = $this->stream->current->lineno; 207 | } 208 | return $left; 209 | } 210 | 211 | public function parseCompareExpression() 212 | { 213 | static $operators = array('==', '!=', '<', '>', '>=', '<='); 214 | $lineno = $this->stream->current->lineno; 215 | $expr = $this->parseAddExpression(); 216 | $ops = array(); 217 | while ($this->stream->test(Twig_Token::OPERATOR_TYPE, $operators)) 218 | $ops[] = array($this->stream->next()->value, 219 | $this->parseAddExpression()); 220 | 221 | if (empty($ops)) 222 | return $expr; 223 | return new Twig_CompareExpression($expr, $ops, $lineno); 224 | } 225 | 226 | public function parseAddExpression() 227 | { 228 | $lineno = $this->stream->current->lineno; 229 | $left = $this->parseSubExpression(); 230 | while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '+')) { 231 | $this->stream->next(); 232 | $right = $this->parseSubExpression(); 233 | $left = new Twig_AddExpression($left, $right, $lineno); 234 | $lineno = $this->stream->current->lineno; 235 | } 236 | return $left; 237 | } 238 | 239 | public function parseSubExpression() 240 | { 241 | $lineno = $this->stream->current->lineno; 242 | $left = $this->parseConcatExpression(); 243 | while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '-')) { 244 | $this->stream->next(); 245 | $right = $this->parseConcatExpression(); 246 | $left = new Twig_SubExpression($left, $right, $lineno); 247 | $lineno = $this->stream->current->lineno; 248 | } 249 | return $left; 250 | } 251 | 252 | public function parseConcatExpression() 253 | { 254 | $lineno = $this->stream->current->lineno; 255 | $left = $this->parseMulExpression(); 256 | while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '~')) { 257 | $this->stream->next(); 258 | $right = $this->parseMulExpression(); 259 | $left = new Twig_ConcatExpression($left, $right, $lineno); 260 | $lineno = $this->stream->current->lineno; 261 | } 262 | return $left; 263 | } 264 | 265 | public function parseMulExpression() 266 | { 267 | $lineno = $this->stream->current->lineno; 268 | $left = $this->parseDivExpression(); 269 | while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '*')) { 270 | $this->stream->next(); 271 | $right = $this->parseDivExpression(); 272 | $left = new Twig_MulExpression($left, $right, $lineno); 273 | $lineno = $this->stream->current->lineno; 274 | } 275 | return $left; 276 | } 277 | 278 | public function parseDivExpression() 279 | { 280 | $lineno = $this->stream->current->lineno; 281 | $left = $this->parseModExpression(); 282 | while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '/')) { 283 | $this->stream->next(); 284 | $right = $this->parseModExpression(); 285 | $left = new Twig_DivExpression($left, $right, $lineno); 286 | $lineno = $this->stream->current->lineno; 287 | } 288 | return $left; 289 | } 290 | 291 | public function parseModExpression() 292 | { 293 | $lineno = $this->stream->current->lineno; 294 | $left = $this->parseUnaryExpression(); 295 | while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '%')) { 296 | $this->stream->next(); 297 | $right = $this->parseUnaryExpression(); 298 | $left = new Twig_ModExpression($left, $right, $lineno); 299 | $lineno = $this->stream->current->lineno; 300 | } 301 | return $left; 302 | } 303 | 304 | public function parseUnaryExpression() 305 | { 306 | if ($this->stream->test('not')) 307 | return $this->parseNotExpression(); 308 | if ($this->stream->current->type == Twig_Token::OPERATOR_TYPE) { 309 | switch ($this->stream->current->value) { 310 | case '-': 311 | return $this->parseNegExpression(); 312 | case '+': 313 | return $this->parsePosExpression(); 314 | } 315 | } 316 | return $this->parsePrimaryExpression(); 317 | } 318 | 319 | public function parseNotExpression() 320 | { 321 | $token = $this->stream->next(); 322 | $node = $this->parseUnaryExpression(); 323 | return new Twig_NotExpression($node, $token->lineno); 324 | } 325 | 326 | public function parseNegExpression() 327 | { 328 | $token = $this->stream->next(); 329 | $node = $this->parseUnaryExpression(); 330 | return new Twig_NegExpression($node, $token->lineno); 331 | } 332 | 333 | public function parsePosExpression() 334 | { 335 | $token = $this->stream->next(); 336 | $node = $this->parseUnaryExpression(); 337 | return new Twig_PosExpression($node, $token->lineno); 338 | } 339 | 340 | public function parsePrimaryExpression($assignment=false) 341 | { 342 | $token = $this->stream->current; 343 | switch ($token->type) { 344 | case Twig_Token::NAME_TYPE: 345 | $this->stream->next(); 346 | switch ($token->value) { 347 | case 'true': 348 | $node = new Twig_Constant(true, $token->lineno); 349 | break; 350 | case 'false': 351 | $node = new Twig_Constant(false, $token->lineno); 352 | break; 353 | case 'none': 354 | $node = new Twig_Constant(NULL, $token->lineno); 355 | break; 356 | default: 357 | $cls = $assignment ? 'Twig_AssignNameExpression' 358 | : 'Twig_NameExpression'; 359 | $node = new $cls($token->value, $token->lineno); 360 | } 361 | break; 362 | case Twig_Token::NUMBER_TYPE: 363 | case Twig_Token::STRING_TYPE: 364 | $this->stream->next(); 365 | $node = new Twig_Constant($token->value, $token->lineno); 366 | break; 367 | default: 368 | if ($token->test(Twig_Token::OPERATOR_TYPE, '(')) { 369 | $this->stream->next(); 370 | $node = $this->parseExpression(); 371 | $this->stream->expect(Twig_Token::OPERATOR_TYPE, ')'); 372 | } 373 | else 374 | throw new Twig_SyntaxError('unexpected token', 375 | $token->lineno); 376 | } 377 | if (!$assignment) 378 | $node = $this->parsePostfixExpression($node); 379 | return $node; 380 | } 381 | 382 | public function parsePostfixExpression($node) 383 | { 384 | $stop = false; 385 | while (!$stop && $this->stream->current->type == 386 | Twig_Token::OPERATOR_TYPE) 387 | switch ($this->stream->current->value) { 388 | case '.': 389 | case '[': 390 | $node = $this->parseSubscriptExpression($node); 391 | break; 392 | case '|': 393 | $node = $this->parseFilterExpression($node); 394 | break; 395 | default: 396 | $stop = true; 397 | break; 398 | } 399 | return $node; 400 | } 401 | 402 | public function parseSubscriptExpression($node) 403 | { 404 | $token = $this->stream->next(); 405 | $lineno = $token->lineno; 406 | if ($token->value == '.') { 407 | $token = $this->stream->next(); 408 | if ($token->type == Twig_Token::NAME_TYPE || 409 | $token->type == Twig_Token::NUMBER_TYPE) 410 | $arg = new Twig_Constant($token->value, $lineno); 411 | else 412 | throw new Twig_SyntaxError('expected name or number', 413 | $lineno); 414 | 415 | } 416 | else { 417 | $token = $this->stream->next(); 418 | $arg = $this->parseExpression(); 419 | $this->stream->expect(Twig_Token::OPERATOR_TYPE, ']'); 420 | } 421 | return new Twig_GetAttrExpression($node, $arg, $lineno, 422 | $this->stream->current->lineno); 423 | } 424 | 425 | public function parseFilterExpression($node) 426 | { 427 | $lineno = $this->stream->current->lineno; 428 | $filters = array(); 429 | while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '|')) { 430 | $this->stream->next(); 431 | $token = $this->stream->expect(Twig_Token::NAME_TYPE); 432 | $args = array(); 433 | if ($this->stream->test( 434 | Twig_Token::OPERATOR_TYPE, '(')) { 435 | $this->stream->next(); 436 | while (!$this->stream->test( 437 | Twig_Token::OPERATOR_TYPE, ')')) { 438 | if (!empty($args)) 439 | $this->stream->expect( 440 | Twig_Token::OPERATOR_TYPE, ','); 441 | $args[] = $this->parseExpression(); 442 | } 443 | } 444 | $filters[] = array($token->value, $args); 445 | } 446 | return new Twig_FilterExpression($node, $filters, $lineno); 447 | } 448 | 449 | public function parseAssignmentExpression() 450 | { 451 | $lineno = $this->stream->current->lineno; 452 | $targets = array(); 453 | $is_multitarget = false; 454 | while (true) { 455 | if (!empty($targets)) 456 | $this->stream->expect(Twig_Token::OPERATOR_TYPE, ','); 457 | if ($this->stream->test(Twig_Token::OPERATOR_TYPE, ')') || 458 | $this->stream->test(Twig_Token::VAR_END_TYPE) || 459 | $this->stream->test(Twig_Token::BLOCK_END_TYPE) || 460 | $this->stream->test('in')) 461 | break; 462 | $targets[] = $this->parsePrimaryExpression(true); 463 | if (!$this->stream->test(Twig_Token::OPERATOR_TYPE, ',')) 464 | break; 465 | $is_multitarget = true; 466 | } 467 | if (!$is_multitarget && count($targets) == 1) 468 | return array(false, $targets[0]); 469 | return array(true, $targets); 470 | } 471 | 472 | public function subparse($test, $drop_needle=false) 473 | { 474 | $lineno = $this->stream->current->lineno; 475 | $rv = array(); 476 | while (!$this->stream->eof) { 477 | switch ($this->stream->current->type) { 478 | case Twig_Token::TEXT_TYPE: 479 | $token = $this->stream->next(); 480 | $rv[] = new Twig_Text($token->value, $token->lineno); 481 | break; 482 | case Twig_Token::VAR_START_TYPE: 483 | $token = $this->stream->next(); 484 | $expr = $this->parseExpression(); 485 | $this->stream->expect(Twig_Token::VAR_END_TYPE); 486 | $rv[] = new Twig_Print($expr, $token->lineno); 487 | break; 488 | case Twig_Token::BLOCK_START_TYPE: 489 | $this->stream->next(); 490 | $token = $this->stream->current; 491 | if ($token->type !== Twig_Token::NAME_TYPE) 492 | throw new Twig_SyntaxError('expected directive', 493 | $token->lineno); 494 | if (!is_null($test) && call_user_func($test, $token)) { 495 | if ($drop_needle) 496 | $this->stream->next(); 497 | return Twig_NodeList::fromArray($rv, $lineno); 498 | } 499 | if (!isset($this->handlers[$token->value])) 500 | throw new Twig_SyntaxError('unknown directive', 501 | $token->lineno); 502 | $this->stream->next(); 503 | $node = call_user_func($this->handlers[$token->value], 504 | $token); 505 | if (!is_null($node)) 506 | $rv[] = $node; 507 | break; 508 | default: 509 | assert(false, 'Lexer or parser ended up in ' . 510 | 'unsupported state.'); 511 | } 512 | } 513 | 514 | return Twig_NodeList::fromArray($rv, $lineno); 515 | } 516 | 517 | public function parse() 518 | { 519 | try { 520 | $body = $this->subparse(NULL); 521 | } 522 | catch (Twig_SyntaxError $e) { 523 | if (is_null($e->filename)) 524 | $e->filename = $this->stream->filename; 525 | throw $e; 526 | } 527 | if (!is_null($this->extends)) 528 | foreach ($this->blocks as $block) 529 | $block->parent = $this->extends; 530 | return new Twig_Module($body, $this->extends, $this->blocks, 531 | $this->stream->filename); 532 | } 533 | } 534 | -------------------------------------------------------------------------------- /spec/spec.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Twig Template Engine Specification 8 | 291 | 292 | 293 |
294 |

Twig Template Engine Specification

295 |

This specification specifies a simple cross-language template engine for at least 296 | PHP, Python and Ruby.

297 |
298 |

Purpose

299 |

A language independent and simple template engine is useful for applications that 300 | use code which is written in more than one programming language. Good Examples 301 | are portal systems which use a blog written in Ruby, a forum software written in 302 | PHP and a planet system written in Python.

303 |
304 |
305 |

Inspiration

306 |

Twig uses a syntax similar to the Genshi text templates which in turn were 307 | inspired by django which also inspired Jinja (all three of them python template 308 | engines) which inspired the Twig runtime environment.

309 |
310 |
311 |

Undefined Behavior

312 |

To simplify porting the template language to different platforms in a couple of 313 | situations the behavior is undefined. Template authors may never take advantage 314 | of such a situation!

315 |
316 |
317 |

Syntax

318 |

I'm too lazy to write down the syntax as BNF diagram but the following snippet 319 | should explain the syntax elements:

320 |
321 | <!DOCTYPE HTML>
322 | {# This is a comment #}
323 | <title>{% block title %}Page Title Goes Here{% endblock %}</title>
324 | {% if show_navigation %}
325 | <nav>
326 |   <ul>
327 |   {% for item in navigation %}
328 |     <li><a href="${item.href|e}">$item.caption</a></li>
329 |   {% endfor %}
330 |   </ul>
331 | </nav>
332 | {% endif %}
333 | <article>{% block body %}{% endblock %}</article>
334 | 
335 |
336 |

Comments and Whitespace

337 |

Everything between {# and #} is ignored by the lexer. Inside blocks and 338 | variable sections the Lexer has to remove whitespace too.

339 |
340 |
341 |

Output Expressions

342 |

To output expressions two syntaxes exists. Simple variable output or full 343 | expression output:

344 |
345 | $this.is.a.variable.output
346 | ${expression|goes|here}
347 | 
348 |

The former is what we call a variable expression, the second a full expression. 349 | Variable expressions might not contain whitespace where as a full expression 350 | must print the output of the full wrapped expression.

351 |
352 |
353 |

Expressions

354 |

Expressions allow basic string manipulation and arithmetic calculations. It 355 | an infix syntax with the following operators in this precedence:

356 |
357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 |
OperatorDescription
+Convert both arguments into a number and add them up.
-Convert both arguments into a number and substract them.
*Convert both arguments into a number and multiply them.
/Convert both arguments into a number and divide them.
%Convert both arguments into a number and calculate the rest 382 | of the integer division.
~Convert both arguments into a string and concatenate them.
orTrue if the left or the right expression is true.
andTrue if the left and the right expression is true.
notnegate the expression
398 |
399 |

All number conversions have an undefined precision but the implementations 400 | should try to select the best possible type. For example if the implementation 401 | sees an integer and a float that looks like an integer it may convert the 402 | latter into a long and add them.

403 |

Use parentheses to group expressions.

404 |

If an object cannot be compared the implementation might raise an error or fail 405 | silently. Template authors may never apply mathematical operators to untrusted 406 | data. This is especially true for the php implementation where the following 407 | outputs 42:

408 |
409 | ${"foo41" + 1}
410 | 
411 |

This is undefined behavior and will break on different implementations or 412 | return 0 as "foo41" is not a valid number.

413 |
414 |

Types

415 |

The following types exist:

416 |
417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 452 | 453 | 454 |
TypeLiteralDescription
integerd+one of the two numeric types. Which of them 433 | is used when is up to the implementation.
float`d+.d+floating point values.
stringsee belowa unicode string. The PHP implementation has 442 | to use bytestrings here and may use mb_string
bool(true|false)Represents boolean values
nonenoneThis type is returned on missing variables or 451 | attributes.
455 |
456 |

string regex:

457 |
458 | (?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')(?sm)
459 | 
460 |
461 |
462 |

Attribute Lookup

463 |

There are two ways to look up attributes on objects. The dot and the 464 | subscript syntax, both inspired by JavaScript. Basically the following 465 | expressions do the very same:

466 |
467 | foo.name.0
468 | foo['name'][0]
469 | 
470 |

This is useful to dynamically get attributes from objects:

471 |
472 | foo[bar]
473 | 
474 |

The underlaying implementation is free to specify on it's own what an attribute 475 | lookup means. The PHP reference implementation for example performs those 476 | actions on foo.bar:

477 |
    478 |
  • try $foo['bar']
  • 479 |
  • try $foo->bar()
  • 480 |
  • try $foo->bar
  • 481 |
  • try $foo->getBar()
  • 482 |
483 |

The first match returns the object, attribute access to not existing attributes 484 | returns none.

485 |
486 |
487 |

Filtering

488 |

The template language does not specify function calls but filters can be used 489 | to further modify variables using functions the template engine provides.

490 |

The following snippet shows how filters are translated to function calls:

491 |
492 | ${42|foo(1, 2)|bar|baz}
493 |     ->  baz(bar(foo(42, 1, 2)))
494 | 
495 |

The following filters must be provided by the implementation:

496 |
497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 513 | 514 | 515 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 530 | 531 | 532 | 534 | 535 | 536 | 537 | 538 | 539 | 542 | 543 | 544 | 546 | 547 | 548 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 569 | 570 | 571 | 572 | 573 | 574 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 |
NameDescription
dateFormat the date using the PHP date formatting rules. 510 | This may sound like a nonstandard way of formatting 511 | dates but it's a way very popular among template 512 | designers and also used by django.
numberformatApply number formatting on the string. This may or 516 | may not use local specific rules.
moneyformatLike numberformat but for money.
filesizeformatTakes a number of bytes and displays it as KB/MB/GB
format

applies sprintf formatting on the string:

526 |
527 | ${"%s %2f" | format(string, float)}
528 | 
529 |
escapeapply HTML escaping on a string. This also has to 533 | convert " to &quot; but leave `' unmodified.
ealias for escape
urlencodeURL encode the string. If the second parameter is 540 | true this function should encode for path sections, 541 | otherwise for query strings.
titleMake the string lowercase and upper case the first 545 | characters of all words.
capitalizeLike title but capitalizes only the first char of 549 | the whole string.
upperConvert the string to uppercase
lowerConvert the string to lowercase
stripTrim leading and trailing whitespace
lstripTrim leading whitespace
rstripTrim trailing whitespace
joinConcatenate the array items and join them with the 568 | string provided or an empty string.
reverseReverse the Array items
countCount the number of items in an array or string 575 | characters.
lengthalias for count.
defaultIf the value is none the first argument is returned
evenIs the number even?
oddIs the number odd?
591 |
592 |
593 |
594 |
595 |

For Loops

596 |

Iteration works via for loops. Loops work a bit like their python counterparts 597 | just that they don't support multilevel tuple unpacking and that they add a new 598 | layer to the context. Thus at the end of the iteration all the modifications on 599 | the context disappear. Additionally inside loops you have access to a special 600 | loop object which provides runtime information:

601 |
602 | ====================== ===================================================
603 | Variable               Description
604 | ====================== ===================================================
605 | ``loop.index``         The current iteration of the loop (1-indexed)
606 | ``loop.index0``        The current iteration of the loop (0-indexed)
607 | ``loop.revindex``      The number of iterations from the end of the
608 |                        loop (1-indexed)
609 | ``loop.revindex0``     The number of iterations from the end of the
610 |                        loop (0-indexed)
611 | ``loop.first``         True if this is the first time through the loop
612 | ``loop.last``          True if this is the last time through the loop
613 | ``loop.parent``        For nested loops, this is the loop "above" the
614 |                        current one
615 | ====================== ===================================================
616 | 
617 |

Additionally for loops can have an else section that is executed if no 618 | iteration took place.

619 |
620 |

Example

621 |
622 | <ul>
623 | {% for user in users %}
624 |     <li><a href="$user.href">${user.username|e}</a></li>
625 | {% else %}
626 |     <li><em>no users found!</em></li>
627 | {% endfor %}
628 | </ul>
629 | 
630 |
631 |
632 |

Notes on Iteration

633 |

Because we have to cope with PHP too which has problematic arrays which are 634 | neither hashmaps nor lists we have a no support for associativ array iteration 635 | at all. How to iterate over associative array then? Using a filter:

636 |
637 | {% for key, value in array|items %}
638 |     ...
639 | {% endfor %}
640 | 
641 |

To iterate over the keys only:

642 |
643 | {% for key in array|key %}
644 |     ...
645 | {% endfor %}
646 | 
647 |
648 |
649 |
650 |

If Conditions

651 |

If conditions work like like Ruby, PHP and Python just that we use PHP 652 | keywords. And it's elseif and not else if:

653 |
654 | {% if expr1 %}
655 |     ...
656 | {% elseif expr2 %}
657 |     ...
658 | {% else %}
659 |     ...
660 | {% endif %}
661 | 
662 |
663 |
664 |

Inheritance

665 |

emplate inheritance allows you to build a base "skeleton" template that 666 | contains all the common elements of your site and defines blocks that 667 | child templates can override.

668 |

Here a small template inheritance example:

669 |
670 | <!DOCTYPE HTML>
671 | <html lang="en">
672 |   <link rel="stylesheet" href="style.css">
673 |   <title>{% block title %}My site{% endblock %}</title>
674 |   <div id="sidebar">
675 |     {% block sidebar %}
676 |      <ul>
677 |         <li><a href="/">Home</a></li>
678 |         <li><a href="/blog/">Blog</a></li>
679 |      </ul>
680 |      {% endblock %}
681 |   </div>
682 |   <div id="content">
683 |      {% block content %}{% endblock %}
684 |   </div>
685 | </html>
686 | 
687 |

If we call that template "base.html" a "index.html" template could override 688 | it and fill in the blocks:

689 |
690 | {% extends "base.html" %}
691 | {% block title %}Foo &mdash; {% super %}{% endblock %}
692 | {% block content %}
693 |     This is the content
694 | {% endblock %}
695 | 
696 |
697 |
698 |
699 | 700 | 701 | --------------------------------------------------------------------------------