- Fixed little bug with escaping variables and buffer tag
[haanga.git] / lib / Haanga / Compiler.php
blob82507413739bd01a2b627136ea157fbf7ede168c
1 <?php
2 /*
3 +---------------------------------------------------------------------------------+
4 | Copyright (c) 2010 César Rodas and Menéame Comunicacions S.L. |
5 +---------------------------------------------------------------------------------+
6 | Redistribution and use in source and binary forms, with or without |
7 | modification, are permitted provided that the following conditions are met: |
8 | 1. Redistributions of source code must retain the above copyright |
9 | notice, this list of conditions and the following disclaimer. |
10 | |
11 | 2. Redistributions in binary form must reproduce the above copyright |
12 | notice, this list of conditions and the following disclaimer in the |
13 | documentation and/or other materials provided with the distribution. |
14 | |
15 | 3. All advertising materials mentioning features or use of this software |
16 | must display the following acknowledgement: |
17 | This product includes software developed by César D. Rodas. |
18 | |
19 | 4. Neither the name of the César D. Rodas nor the |
20 | names of its contributors may be used to endorse or promote products |
21 | derived from this software without specific prior written permission. |
22 | |
23 | THIS SOFTWARE IS PROVIDED BY CÉSAR D. RODAS ''AS IS'' AND ANY |
24 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
25 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
26 | DISCLAIMED. IN NO EVENT SHALL CÉSAR D. RODAS BE LIABLE FOR ANY |
27 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
28 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
29 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
30 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
31 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
32 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE |
33 +---------------------------------------------------------------------------------+
34 | Authors: César Rodas <crodas@php.net> |
35 +---------------------------------------------------------------------------------+
38 class Haanga_Compiler
41 // properties {{{
42 protected static $block_var=NULL;
43 protected $generator;
44 protected $forloop = array();
45 protected $forid = 0;
46 protected $sub_template = FALSE;
47 protected $name;
48 protected $check_function = FALSE;
49 protected $blocks=array();
50 protected $line=0;
51 protected $file;
52 /**
53 * number of blocks :-)
55 protected $in_block=0;
56 /**
57 * output buffers :-)
59 protected $ob_start=0;
60 protected $append;
61 protected $prepend_op;
62 /**
63 * Context at compile time
65 protected $context;
66 /**
67 * Table which contains all variables
68 * aliases defined in the template
70 protected $var_alias;
71 /**
72 * Flag the current variable as safe. This means
73 * that escape won't be called if autoescape is
74 * activated (which is activated by default)
76 public $var_is_safe=FALSE;
77 public $safes;
79 /* compiler options */
80 static protected $autoescape = TRUE;
81 static protected $if_empty = TRUE;
82 static protected $dot_as_object = TRUE;
83 static protected $strip_whitespace = FALSE;
84 static protected $is_exec_enabled = FALSE;
85 static protected $global_context = array();
87 /**
88 * Debug file
90 protected $debug;
91 // }}}
93 function __construct()
95 $this->generator = new Haanga_Generator_PHP;
96 if (self::$block_var===NULL) {
97 self::$block_var = '{{block.'.md5('super').'}}';
101 // getOption($option) {{{
102 public static function getOption($option)
104 $value = NULL;
105 switch (strtolower($option)) {
106 case 'if_empty':
107 $value = self::$if_empty;
108 break;
109 case 'autoescape':
110 $value = self::$autoescape;
111 break;
112 case 'dot_as_object':
113 $value = self::$dot_as_object;
114 break;
115 case 'strip_whitespace':
116 $value = self::$strip_whitespace;
117 break;
118 case 'is_exec_enabled':
119 case 'allow_exec':
120 $value = self::$is_exec_enabled;
121 break;
122 case 'global':
123 $value = self::$global_context;
124 break;
126 return $value;
128 // }}}
130 // setOption($option, $value) {{{
132 * Set Compiler option.
134 * @return void
136 public static function setOption($option, $value)
138 switch (strtolower($option)) {
139 case 'if_empty':
140 self::$if_empty = (bool)$value;
141 break;
142 case 'autoescape':
143 self::$autoescape = (bool)$value;
144 break;
145 case 'dot_as_object':
146 self::$dot_as_object = (bool)$value;
147 break;
148 case 'strip_whitespace':
149 self::$strip_whitespace = (bool)$value;
150 break;
151 case 'is_exec_enabled':
152 case 'allow_exec':
153 self::$is_exec_enabled = (bool)$value;
154 break;
155 case 'global':
156 if (!is_array($value)) {
157 $value = array($value);
159 self::$global_context = $value;
160 break;
163 // }}}
165 // setDebug($file) {{{
166 function setDebug($file)
168 $this->debug = $file;
170 // }}}
172 // reset() {{{
173 function reset()
175 foreach (array_keys(get_object_vars($this)) as $key) {
176 if (isset($avoid_cleaning[$key])) {
177 continue;
179 $this->$key = NULL;
181 $this->generator = new Haanga_Generator_PHP;
182 $this->blocks = array();
183 $this->cycle = array();
185 // }}}
187 // get_template_name() {{{
188 final function get_template_name()
190 return $this->name;
192 // }}}
194 // Set template name {{{
195 function set_template_name($path)
197 $file = basename($path);
198 $pos = strpos($file,'.');
199 return ($this->name = substr($file, 0, $pos));
201 // }}}
203 // get_function_name(string $name) {{{
204 function get_function_name($name)
206 return "{$name}_template";
208 // }}}
210 // Compile ($code, $name=NULL) {{{
211 final function compile($code, $name=NULL, $file=NULL)
213 $this->name = $name;
215 $parsed = Haanga_Compiler_Lexer::init($code, $this, $file);
216 $code = "";
217 $this->subtemplate = FALSE;
219 $body = new Haanga_AST;
220 $this->prepend_op = hcode();
222 if (isset($parsed[0]) && $parsed[0]['operation'] == 'base') {
223 /* {% base ... %} found */
224 $base = $parsed[0][0];
225 $code .= $this->get_base_template($base);
226 unset($parsed[0]);
229 if (defined('HAANGA_VERSION')) {
230 $body->decl('HAANGA_VERSION', HAANGA_VERSION);
233 if ($name) {
234 $func_name = $this->get_function_name($name);
235 if ($this->check_function) {
236 $body->do_if(hexpr(hexec('function_exists', $func_name), '===', FALSE));
238 if (!empty($this->file)) {
239 $body->comment("Generated from ".$this->file);
242 $body->declare_function($func_name);
243 if (count(self::$global_context) > 0) {
244 $body->do_global(self::$global_context);
246 $body->do_exec('extract', hvar('vars'));
247 $body->do_if(hexpr(hvar('return'), '==', TRUE));
248 $body->do_exec('ob_start');
249 $body->do_endif();
253 $this->generate_op_code($parsed, $body);
254 if ($this->subtemplate) {
255 $expr = $this->expr_call_base_template();
256 $this->do_print($body, $expr);
259 $body->do_if(hexpr(hvar('return'), '==', TRUE));
260 $body->do_return(hexec('ob_get_clean'));
261 $body->do_endif();
263 if ($name) {
264 $body->do_endfunction();
265 if ($this->check_function) {
266 $body->do_endif();
270 if ($this->prepend_op->stack_size() > 0) {
271 $this->prepend_op->append_ast($body);
272 $body = $this->prepend_op;
275 $op_code = $body->getArray(TRUE);
278 $code .= $this->generator->getCode($op_code);
279 if (!empty($this->append)) {
280 $code .= $this->append;
283 if (!empty($this->debug)) {
284 $op_code['php'] = $code;
285 file_put_contents($this->debug, print_r($op_code, TRUE), LOCK_EX);
287 return $code;
289 // }}}
291 // compile_file($file) {{{
293 * Compile a file
295 * @param string $file File path
296 * @param bool $safe Whether or not add check if the function is already defined
298 * @return Generated PHP code
300 final function compile_file($file, $safe=FALSE, $context=array())
302 if (!is_readable($file)) {
303 throw new Haanga_Compiler_Exception("$file is not a file");
306 if (count(self::$global_context) > 0) {
307 /* add global variables (if any) to the current context */
308 foreach (self::$global_context as $var) {
309 $context[$var] = &$GLOBALS[$var];
313 $this->_base_dir = dirname($file);
314 $this->file = realpath($file);
315 $this->line = 0;
316 $this->check_function = $safe;
317 $this->context = $context;
318 $name = $this->set_template_name($file);
319 return $this->compile(file_get_contents($file), $name, $file);
321 // }}}
323 // getOpCodes($code, $file='') {{{
325 * Compile the $code and return the "opcodes"
326 * (the Abstract syntax tree).
328 * @param string $code Template content
329 * @param string $file File path (used for erro reporting)
331 * @return Haanga_AST
334 public function getOpCodes($code, $file)
336 $oldfile = $this->file;
337 $this->file = $file;
338 $parsed = Haanga_Compiler_Lexer::init($code, $this, $file);
339 $body = new Haanga_AST;
340 if (isset($parsed[0]) && $parsed[0]['operation'] == 'base') {
341 $this->Error("{% base is not supported on inlines %}");
343 $body = new Haanga_AST;
344 $this->generate_op_code($parsed, $body);
345 $this->file = $oldfile;
346 return $body;
348 // }}}
350 // Error($errtxt) {{{
352 * Throw an exception and appends information about the template (the path and
353 * the last processed line).
357 public function Error($err)
359 throw new Haanga_Compiler_Exception("{$err} in {$this->file}:$this->line");
361 // }}}
363 // is_expr methods {{{
364 function is_var_filter($cmd)
366 return isset($cmd['var_filter']);
369 // }}}
371 // expr_call_base_template() {{{
373 * Generate code to call base template
376 function expr_call_base_template()
378 return hexec(
379 $this->get_function_name($this->subtemplate),
380 hvar('vars'), TRUE,
381 hvar('blocks')
384 // }}}
386 // get_base_template($base) {{{
388 * Handle {% base "" %} definition. By default only
389 * static (string) are supported, but this can be overrided
390 * on subclasses.
392 * This method load the base class, compile it and return
393 * the generated code.
395 * @param array $base Base structure
397 * @return string Generated source code
399 function get_base_template($base)
401 if (!Haanga_AST::is_str($base)) {
402 $this->Error("Dynamic inheritance is not supported for compilated templates");
404 $file = $base['string'];
405 list($this->subtemplate, $new_code) = $this->compile_required_template($file);
406 return $new_code."\n\n";
408 // }}}
410 // {% base "foo.html" %} {{{
411 protected function generate_op_base()
413 $this->Error("{% base %} can be only as first statement");
415 // }}}
417 // Main Loop {{{
418 protected function generate_op_code($parsed, &$body)
420 if (!is_array($parsed)) {
421 $this->Error("Invalid \$parsed array");
423 foreach ($parsed as $op) {
424 if (!is_array($op)) {
425 continue;
427 if (!isset($op['operation'])) {
428 $this->Error("Malformed array:".print_r($op, TRUE));
430 if (isset($op['line'])) {
431 $this->line = $op['line'];
434 if ($this->subtemplate && $this->in_block == 0 && $op['operation'] != 'block') {
435 /* ignore most of tokens in subtemplates */
436 continue;
439 $method = "generate_op_".$op['operation'];
440 if (!is_callable(array($this, $method))) {
441 $this->Error("Compiler: Missing method $method");
443 $this->$method($op, $body);
446 // }}}
448 // Check the current expr {{{
449 protected function check_expr(&$expr)
451 if (Haanga_AST::is_expr($expr)) {
452 if ($expr['op_expr'] == 'in') {
453 for ($id=0; $id < 2; $id++) {
454 if ($this->is_var_filter($expr[$id])) {
455 $expr[$id] = $this->get_filtered_var($expr[$id]['var_filter'], $var);
458 if (Haanga_AST::is_str($expr[1])) {
459 $expr = hexpr(hexec('strpos', $expr[1], $expr[0]), '!==', FALSE);
460 } else {
461 $expr = hexpr(
462 hexpr_cond(
463 hexec('is_array', $expr[1]),
464 hexec('array_search', $expr[0], $expr[1]),
465 hexec('strpos', $expr[1], $expr[0])
467 ,'!==', FALSE
471 if (is_object($expr)) {
472 $expr = $expr->getArray();
474 $this->check_expr($expr[0]);
475 $this->check_expr($expr[1]);
476 } else if (is_array($expr)) {
477 if ($this->is_var_filter($expr)) {
478 $expr = $this->get_filtered_var($expr['var_filter'], $var);
479 } else if (isset($expr['args'])) {
480 /* check every arguments */
481 foreach ($expr['args'] as &$v) {
482 $this->check_expr($v);
484 unset($v);
485 } else if (isset($expr['expr_cond'])) {
486 /* Check expr conditions */
487 $this->check_expr($expr['expr_cond']);
488 $this->check_expr($expr['true']);
489 $this->check_expr($expr['false']);
493 // }}}
495 // buffer <varname> {{{
496 public function generate_op_buffer($details, &$body)
498 $this->ob_start($body);
499 $this->generate_op_code($details['body'], $body);
500 $body->decl($details['name'], hvar('buffer'.$this->ob_start));
501 $this->ob_start--;
502 $this->set_safe($details['name']);
504 // }}}
506 // ifequal|ifnot equal <var_filtered|string|number> <var_fitlered|string|number> ... else ... {{{
507 protected function generate_op_ifequal($details, &$body)
509 $if['expr'] = hexpr($details[1], $details['cmp'], $details[2])->getArray();
510 $if['body'] = $details['body'];
511 if (isset($details['else'])) {
512 $if['else'] = $details['else'];
514 $this->generate_op_if($if, $body);
516 // }}}
518 // {% if <expr> %} HTML {% else %} TWO {% endif $} {{{
519 protected function generate_op_if($details, &$body)
521 if (self::$if_empty && $this->is_var_filter($details['expr']) && count($details['expr']['var_filter']) == 1) {
522 /* if we are doing if <Variable> it should check
523 if it exists without throw any warning */
524 $expr = $details['expr'];
525 $expr['var_filter'][] = 'empty';
527 $variable = $this->get_filtered_var($expr['var_filter'], $var);
529 $details['expr'] = hexpr($variable, '===', FALSE)->getArray();
531 $this->check_expr($details['expr']);
532 $expr = Haanga_AST::fromArrayGetAST($details['expr']);
533 $body->do_if($expr);
534 $this->generate_op_code($details['body'], $body);
535 if (isset($details['else'])) {
536 $body->do_else();
537 $this->generate_op_code($details['else'], $body);
539 $body->do_endif();
541 // }}}
543 // Override template {{{
544 protected function compile_required_template($file)
546 if (!is_file($file)) {
547 if (isset($this->_base_dir)) {
548 $file = $this->_base_dir.'/'.$file;
551 if (!is_file($file)) {
552 $this->Error("can't find {$file} file template");
554 $class = get_class($this);
555 $comp = new $class;
556 $comp->reset();
557 $code = $comp->compile_file($file, $this->check_function);
558 return array($comp->get_template_name(), $code);
560 // }}}
562 // include "file.html" | include <var1> {{{
563 protected function generate_op_include($details, &$body)
565 if (!$details[0]['string']) {
566 $this->Error("Dynamic inheritance is not supported for compilated templates");
568 list($name,$code) = $this->compile_required_template($details[0]['string']);
569 $this->append .= "\n\n{$code}";
570 $this->do_print($body,
571 hexec($this->get_function_name($name),
572 hvar('vars'), TRUE, hvar('blocks'))
575 // }}}
577 // Handle HTML code {{{
578 protected function generate_op_html($details, &$body)
580 $string = Haanga_AST::str($details['html']);
581 $this->do_print($body, $string);
583 // }}}
585 // get_var_filtered {{{
587 * This method handles all the filtered variable (piped_list(X)'s
588 * output in the parser.
591 * @param array $variable (Output of piped_list(B) (parser))
592 * @param array &$varname Variable name
593 * @param bool $accept_string TRUE is string output are OK (ie: block.parent)
595 * @return expr
598 function get_filtered_var($variable, &$varname, $accept_string=FALSE)
600 $this->var_is_safe = FALSE;
602 if (count($variable) > 1) {
603 $count = count($variable);
604 $target = $this->generate_variable_name($variable[0]);
606 if (!Haanga_AST::is_var($target)) {
607 /* block.super can't have any filter */
608 $this->Error("This variable can't have any filter");
611 for ($i=1; $i < $count; $i++) {
612 $func_name = $variable[$i];
613 if ($func_name == 'escape') {
614 /* to avoid double cleaning */
615 $this->var_is_safe = TRUE;
617 $args = array(isset($exec) ? $exec : $target);
618 $exec = $this->do_filtering($func_name, $args);
620 unset($variable);
621 $varname = $args[0];
622 $details = $exec;
623 } else {
624 $details = $this->generate_variable_name($variable[0]);
625 $varname = $variable[0];
627 if (!Haanga_AST::is_var($details) && !$accept_string) {
628 /* generate_variable_name didn't replied a variable, weird case
629 currently just used for {{block.super}}.
631 $this->Error("Invalid variable name {$variable[0]}");
635 return $details;
637 // }}}
639 // generate_op_print_var {{{
641 * Generate code to print a variable with its filters, if there is any.
643 * All variable (except those flagged as |safe) are automatically
644 * escaped if autoescape is "on".
647 protected function generate_op_print_var($details, &$body)
650 $details = $this->get_filtered_var($details['variable'], $variable, TRUE);
652 if (!Haanga_AST::is_var($details) && !Haanga_AST::is_exec($details)) {
653 /* generate_variable_name didn't replied a variable, weird case
654 currently just used for {{block.super}}.
656 $this->do_print($body, $details);
657 return;
660 if (!$this->is_safe($details) && self::$autoescape) {
661 $args = array($details);
662 $details = $this->do_filtering('escape', $args);
666 if (is_array($details)) {
667 $details = Haanga_AST::fromArrayGetAST($details);
669 $this->do_print($body, $details);
671 // }}}
673 // {# something #} {{{
674 protected function generate_op_comment($details, &$body)
676 /* comments are annoying */
677 //$body->comment($details['comment']);
679 // }}}
681 // {% block 'name' %} ... {% endblock %} {{{
682 protected function generate_op_block($details, &$body)
684 if (is_array($details['name'])) {
685 $name = "";
686 foreach ($details['name'] as $part) {
687 if (is_string($part)) {
688 $name .= "{$part}";
689 } else if (is_array($part)) {
690 if (Haanga_AST::is_str($part)) {
691 $name .= "{$part['string']}";
692 } elseif (isset($part['object'])) {
693 $name .= "{$part['object']}";
694 } else {
695 $this->Error("Invalid blockname");
698 $name .= ".";
700 $details['name'] = substr($name, 0, -1);
702 $this->in_block++;
703 $this->blocks[] = $details['name'];
704 $block_name = hvar('blocks', $details['name']);
706 $this->ob_start($body);
707 $buffer_var = 'buffer'.$this->ob_start;
709 $content = hcode();
710 $this->generate_op_code($details['body'], $content);
712 $body->append_ast($content);
713 $this->ob_start--;
715 $buffer = hvar($buffer_var);
717 /* {{{ */
719 * isset previous block (parent block)?
720 * TRUE
721 * has reference to self::$block_var ?
722 * TRUE
723 * replace self::$block_var for current block value (buffer)
724 * FALSE
725 * print parent block
726 * FALSE
727 * print current block
730 $declare = hexpr_cond(
731 hexec('isset', $block_name),
732 hexpr_cond(
733 hexpr(hexec('strpos', $block_name, self::$block_var), '===', FALSE),
734 $block_name,
735 hexec('str_replace', self::$block_var, $buffer, $block_name)
736 ), $buffer);
737 /* }}} */
739 if (!$this->subtemplate) {
740 $this->do_print($body, $declare);
741 } else {
742 $body->decl($block_name, $declare);
743 if ($this->in_block > 1) {
744 $this->do_print($body, $block_name);
747 array_pop($this->blocks);
748 $this->in_block--;
751 // }}}
753 // regroup <var1> by <field> as <foo> {{{
754 protected function generate_op_regroup($details, &$body)
756 $body->comment("Temporary sorting");
758 $array = $this->get_filtered_var($details['array'], $varname);
760 if (Haanga_AST::is_exec($array)) {
761 $varname = hvar($details['as']);
762 $body->decl($varname, $array);
764 $var = hvar('item', $details['row']);
766 $body->decl('temp_group', array());
768 $body->do_foreach($varname, 'item', NULL,
769 hcode()->decl(hvar('temp_group', $var, NULL), hvar('item'))
772 $body->comment("Proper format");
773 $body->decl($details['as'], array());
774 $body->do_foreach('temp_group', 'item', 'group',
775 hcode()->decl(
776 hvar($details['as'], NULL),
777 array("grouper" => hvar('group'), "list" => hvar('item'))
780 $body->comment("Sorting done");
782 // }}}
784 // variable context {{{
786 * Variables context
788 * These two functions are useful to detect if a variable
789 * separated by dot (foo.bar) is an array or object. To avoid
790 * overhead we decide it at compile time, rather than
791 * ask over and over at rendering time.
793 * foo.bar:
794 * + If foo exists at compile time,
795 * and it is an array, it would be foo['bar']
796 * otherwise it'd be foo->bar.
797 * + If foo don't exists at compile time,
798 * it would be foo->bar if the compiler option
799 * dot_as_object is TRUE (by default) otherwise
800 * it'd be foo['bar']
802 * @author crodas
803 * @author gallir (ideas)
806 function set_context($varname, $value)
808 $this->context[$varname] = $value;
811 function get_context($variable)
813 if (!is_array($variable)) {
814 $variable = array($variable);
816 $varname = $variable[0];
817 if (isset($this->context[$varname])) {
818 if (count($variable) == 1) {
819 return $this->context[$varname];
821 $var = & $this->context[$varname];
822 foreach ($variable as $id => $part) {
823 if ($id != 0) {
824 if (is_array($part) && isset($part['object'])) {
825 $var = &$var->$part['object'];
826 } else if (is_object($var)) {
827 $var = &$var->$part;
828 } else {
829 $var = &$var[$part];
833 $variable = $var;
834 unset($var);
835 return $variable;
838 return NULL;
841 function var_is_object(Array $variable, $default=NULL)
843 $varname = $variable[0];
844 switch ($varname) {
845 case 'GLOBALS':
846 case '_SERVER':
847 case '_GET':
848 case '_POST':
849 case '_FILES':
850 case '_COOKIE':
851 case '_SESSION':
852 case '_REQUEST':
853 case '_ENV':
854 case 'forloop':
855 case 'block':
856 return FALSE; /* these are arrays */
859 if (isset($this->context[$varname])) {
860 return is_object($this->get_context($variable));
863 return $default===NULL ? self::$dot_as_object : $default;
865 // }}}
867 // Get variable name {{{
868 protected function generate_variable_name($variable)
870 if (is_array($variable)) {
871 switch ($variable[0]) {
872 case 'forloop':
873 if (!$this->forid) {
874 $this->Error("Invalid forloop reference outside of a loop");
876 switch ($variable[1]) {
877 case 'counter':
878 $this->forloop[$this->forid]['counter'] = TRUE;
879 $variable = 'forcounter1_'.$this->forid;
880 break;
881 case 'counter0':
882 $this->forloop[$this->forid]['counter0'] = TRUE;
883 $variable = 'forcounter0_'.$this->forid;
884 break;
885 case 'last':
886 $this->forloop[$this->forid]['counter'] = TRUE;
887 $this->forloop[$this->forid]['last'] = TRUE;
888 $variable = 'islast_'.$this->forid;
889 break;
890 case 'first':
891 $this->forloop[$this->forid]['first'] = TRUE;
892 $variable = 'isfirst_'.$this->forid;
893 break;
894 case 'revcounter':
895 $this->forloop[$this->forid]['revcounter'] = TRUE;
896 $variable = 'revcount_'.$this->forid;
897 break;
898 case 'revcounter0':
899 $this->forloop[$this->forid]['revcounter0'] = TRUE;
900 $variable = 'revcount0_'.$this->forid;
901 break;
902 case 'parentloop':
903 unset($variable[1]);
904 $this->forid--;
905 $variable = $this->generate_variable_name(array_values($variable));
906 $variable = $variable['var'];
907 $this->forid++;
908 break;
909 default:
910 $this->Error("Unexpected forloop.{$variable[1]}");
912 /* no need to escape it */
913 $this->var_is_safe = TRUE;
914 break;
915 case 'block':
916 if ($this->in_block == 0) {
917 $this->Error("Can't use block.super outside a block");
919 if (!$this->subtemplate) {
920 $this->Error("Only subtemplates can call block.super");
922 /* no need to escape it */
923 $this->var_is_safe = TRUE;
924 return Haanga_AST::str(self::$block_var);
925 break;
928 } else if (isset($this->var_alias[$variable])) {
929 $variable = $this->var_alias[$variable];
932 return hvar($variable)->getArray();
934 // }}}
936 // Print {{{
937 public function do_print(Haanga_AST $code, $stmt)
939 /* Flag this object as a printing one */
940 $code->doesPrint = TRUE;
942 if (self::$strip_whitespace && Haanga_AST::is_str($stmt)) {
943 $stmt['string'] = preg_replace('/\s+/', ' ', $stmt['string']);
946 if ($this->ob_start == 0) {
947 $code->do_echo($stmt);
948 return;
951 $buffer = hvar('buffer'.$this->ob_start);
952 $code->append($buffer, $stmt);
956 // }}}
958 // for [<key>,]<val> in <array> {{{
959 protected function generate_op_loop($details, &$body)
961 if (isset($details['empty'])) {
962 $body->do_if(hexpr(hexec('count', hvar($details['array'])), '==', 0));
963 $this->generate_op_code($details['empty'], $body);
964 $body->do_else();
967 /* ForID */
968 $oldid = $this->forid;
969 $this->forid = $oldid+1;
970 $this->forloop[$this->forid] = array();
972 /* Check if the array to iterate is an object */
973 $var = &$details['array'][0];
974 if (is_string($var) && $this->var_is_object(array($var), FALSE)) {
975 /* It is an object, call to get_object_vars */
976 $body->decl($var.'_arr', hexec('get_object_vars', hvar($var)));
977 $var .= '_arr';
979 unset($var);
981 /* variables */
982 $array = $this->get_filtered_var($details['array'], $varname);
984 /* Loop body */
985 if ($this->is_safe(hvar($varname))) {
986 $this->set_safe(hvar($details['variable']));
989 /* check if the elements in the array is an array or object */
991 $for_body = hcode();
992 $this->generate_op_code($details['body'], $for_body);
994 if ($this->is_safe(hvar($varname))) {
995 $this->set_unsafe($details['variable']);
998 $oid = $this->forid;
999 $size = hvar('psize_'.$oid);
1001 // counter {{{
1002 if (isset($this->forloop[$oid]['counter'])) {
1003 $var = hvar('forcounter1_'.$oid);
1004 $body->decl($var, 1);
1005 $for_body->decl($var, hexpr($var, '+', 1));
1007 // }}}
1009 // counter0 {{{
1010 if (isset($this->forloop[$oid]['counter0'])) {
1011 $var = hvar('forcounter0_'.$oid);
1012 $body->decl($var, 0);
1013 $for_body->decl($var, hexpr($var, '+', 1));
1015 // }}}
1017 // last {{{
1018 if (isset($this->forloop[$oid]['last'])) {
1019 if (!isset($cnt)) {
1020 $body->decl('psize_'.$oid, hexec('count', hvar_ex($details['array'])));
1021 $cnt = TRUE;
1023 $var = 'islast_'.$oid;
1024 $body->decl($var, hexpr(hvar('forcounter1_'.$oid), '==', $size));
1025 $for_body->decl($var, hexpr(hvar('forcounter1_'.$oid), '==', $size));
1027 // }}}
1029 // first {{{
1030 if (isset($this->forloop[$oid]['first'])) {
1031 $var = hvar('isfirst_'.$oid);
1032 $body->decl($var, TRUE);
1033 $for_body->decl($var, FALSE);
1035 // }}}
1037 // revcounter {{{
1038 if (isset($this->forloop[$oid]['revcounter'])) {
1039 if (!isset($cnt)) {
1040 $body->decl('psize_'.$oid, hexec('count', hvar_ex($details['array'])));
1041 $cnt = TRUE;
1043 $var = hvar('revcount_'.$oid);
1044 $body->decl($var, $size);
1045 $for_body->decl($var, hexpr($var, '-', 1));
1047 // }}}
1049 // revcounter0 {{{
1050 if (isset($this->forloop[$oid]['revcounter0'])) {
1051 if (!isset($cnt)) {
1052 $body->decl('psize_'.$oid, hexec('count', hvar_ex($details['array'])));
1053 $cnt = TRUE;
1055 $var = hvar('revcount0_'.$oid);
1056 $body->decl($var, hexpr($size, "-", 1));
1057 $for_body->decl($var, hexpr($var, '-', 1));
1059 // }}}
1061 /* Restore old ForID */
1062 $this->forid = $oldid;
1064 /* Merge loop body */
1065 $body->do_foreach($array, $details['variable'], $details['index'], $for_body);
1067 if (isset($details['empty'])) {
1068 $body->do_endif();
1071 // }}}
1073 // ifchanged [<var1> <var2] {{{
1074 protected function generate_op_ifchanged($details, &$body)
1076 static $ifchanged = 0;
1078 $ifchanged++;
1079 $var1 = 'ifchanged'.$ifchanged;
1080 if (!isset($details['check'])) {
1081 /* ugly */
1082 $this->ob_start($body);
1083 $var2 = hvar('buffer'.$this->ob_start);
1086 $this->generate_op_code($details['body'], $body);
1087 $this->ob_start--;
1088 $body->do_if(hexpr(hexec('isset', hvar($var1)), '==', FALSE, '||', hvar($var1), '!=', $var2));
1089 $this->do_print($body, $var2);
1090 $body->decl($var1, $var2);
1091 } else {
1092 /* beauty :-) */
1093 foreach ($details['check'] as $id=>$type) {
1094 if (!Haanga_AST::is_var($type)) {
1095 $this->Error("Unexpected string {$type['string']}, expected a varabile");
1098 $this_expr = hexpr(hexpr(
1099 hexec('isset', hvar($var1, $id)), '==', FALSE,
1100 '||', hvar($var1, $id), '!=', $type
1103 if (isset($expr)) {
1104 $this_expr = hexpr($expr, '&&', $this_expr);
1107 $expr = $this_expr;
1110 $body->do_if($expr);
1111 $this->generate_op_code($details['body'], $body);
1112 $body->decl($var1, $details['check']);
1115 if (isset($details['else'])) {
1116 $body->do_else();
1117 $this->generate_op_code($details['else'], $body);
1119 $body->do_endif();
1121 // }}}
1123 // autoescape ON|OFF {{{
1124 function generate_op_autoescape($details, &$body)
1126 $old_autoescape = self::$autoescape;
1127 self::$autoescape = strtolower($details['value']) == 'on';
1128 $this->generate_op_code($details['body'], $body);
1129 self::$autoescape = $old_autoescape;
1131 // }}}
1133 // {% spacefull %} Set to OFF strip_whitespace for a block (the compiler option) {{{
1134 function generate_op_spacefull($details, &$body)
1136 $old_strip_whitespace = self::$strip_whitespace;
1137 self::$strip_whitespace = FALSE;
1138 $this->generate_op_code($details['body'], $body);
1139 self::$strip_whitespace = $old_strip_whitespace;
1141 // }}}
1143 // ob_Start(array &$body) {{{
1145 * Start a new buffering
1148 function ob_start(&$body)
1150 $this->ob_start++;
1151 $body->decl('buffer'.$this->ob_start, "");
1153 // }}}
1155 // Custom Tags {{{
1156 function get_custom_tag($name)
1158 $function = $this->get_function_name($this->name).'_tag_'.$name;
1159 $this->append .= "\n\n".Haanga_Extension::getInstance('Tag')->getFunctionBody($name, $function);
1160 return $function;
1164 * Generate needed code for custom tags (tags that aren't
1165 * handled by the compiler).
1168 function generate_op_custom_tag($details, &$body)
1170 static $tags;
1171 if (!$tags) {
1172 $tags = Haanga_Extension::getInstance('Tag');
1175 $tag_name = $details['name'];
1176 $tagFunction = $tags->getFunctionAlias($tag_name);
1178 if (!$tagFunction && !$tags->hasGenerator($tag_name)) {
1179 $function = $this->get_custom_tag($tag_name, isset($details['as']));
1180 } else {
1181 $function = $tagFunction;
1184 if (isset($details['body'])) {
1186 if the custom tag has 'body'
1187 then it behave the same way as a filter
1189 $this->ob_start($body);
1190 $this->generate_op_code($details['body'], $body);
1191 $target = hvar('buffer'.$this->ob_start);
1192 if ($tags->hasGenerator($tag_name)) {
1193 $exec = $tags->generator($tag_name, $this, array($target));
1194 if (!$exec InstanceOf Haanga_AST) {
1195 $this->Error("Invalid output of custom filter {$tag_name}");
1197 } else {
1198 $exec = hexec($function, $target);
1200 $this->ob_start--;
1201 $this->do_print($body, $exec);
1202 return;
1205 $var = isset($details['as']) ? $details['as'] : NULL;
1206 $args = array_merge(array($function), $details['list']);
1208 if ($tags->hasGenerator($tag_name)) {
1209 $exec = $tags->generator($tag_name, $this, $details['list'], $var);
1210 if ($exec InstanceOf Haanga_AST) {
1211 if ($exec->stack_size() >= 2 || $exec->doesPrint || $var !== NULL) {
1213 The generator returned more than one statement,
1214 so we assume the output is already handled
1215 by one of those stmts.
1217 $body->append_ast($exec);
1218 return;
1220 } else {
1221 $this->Error("Invalid output of the custom tag {$tag_name}");
1223 } else {
1224 $fnc = array_shift($args);
1225 $exec = hexec($fnc);
1226 foreach ($args as $arg) {
1227 $exec->param($arg);
1231 if ($var) {
1232 $body->decl($var, $exec);
1233 } else {
1234 $this->do_print($body, $exec);
1237 // }}}
1239 // with <variable> as <var> {{{
1244 function generate_op_alias($details, &$body)
1246 $this->var_alias[ $details['as'] ] = $details['var'];
1247 $this->generate_op_code($details['body'], $body);
1248 unset($this->var_alias[ $details['as'] ] );
1250 // }}}
1252 // Custom Filters {{{
1253 function get_custom_filter($name)
1255 $function = $this->get_function_name($this->name).'_filter_'.$name;
1256 $this->append .= "\n\n".Haanga_Extension::getInstance('Filter')->getFunctionBody($name, $function);
1257 return $function;
1261 function do_filtering($name, $args)
1263 static $filter;
1264 if (!$filter) {
1265 $filter = Haanga_Extension::getInstance('Filter');
1268 if (is_array($name)) {
1270 prepare array for ($func_name, $arg1, $arg2 ... )
1271 where $arg1 = last expression and $arg2.. $argX is
1272 defined in the template
1274 $args = array_merge($args, $name['args']);
1275 $name = $name[0];
1278 if (!$filter->isValid($name)) {
1279 $this->Error("{$name} is an invalid filter");
1282 if ($filter->hasGenerator($name)) {
1283 return $filter->generator($name, $this, $args);
1285 $fnc = $filter->getFunctionAlias($name);
1286 if (!$fnc) {
1287 $fnc = $this->get_custom_filter($name);
1290 $args = array_merge(array($fnc), $args);
1291 $exec = call_user_func_array('hexec', $args);
1293 return $exec;
1296 function generate_op_filter($details, &$body)
1298 $this->ob_start($body);
1299 $this->generate_op_code($details['body'], $body);
1300 $target = hvar('buffer'.$this->ob_start);
1301 foreach ($details['functions'] as $f) {
1302 $param = (isset($exec) ? $exec : $target);
1303 $exec = $this->do_filtering($f, array($param));
1305 $this->ob_start--;
1306 $this->do_print($body, $exec);
1308 // }}}
1310 /* variable safety {{{ */
1311 function set_safe($name)
1313 if (!Haanga_AST::is_Var($name)) {
1314 $name = hvar($name)->getArray();
1316 $this->safes[serialize($name)] = TRUE;
1319 function set_unsafe($name)
1321 if (!Haanga_AST::is_Var($name)) {
1322 $name = hvar($name)->getArray();
1324 unset($this->safes[serialize($name)]);
1327 function is_safe($name)
1329 if ($this->var_is_safe) {
1330 return TRUE;
1332 if (isset($this->safes[serialize($name)])) {
1333 return TRUE;
1335 return FALSE;
1337 /* }}} */
1339 final static function main_cli()
1341 $argv = $GLOBALS['argv'];
1342 $haanga = new Haanga_Compiler;
1343 $code = $haanga->compile_file($argv[1], TRUE);
1344 if (!isset($argv[2]) || $argv[2] != '--notags') {
1345 $code = "<?php\n\n$code";
1347 echo $code;
1353 * Local variables:
1354 * tab-width: 4
1355 * c-basic-offset: 4
1356 * End:
1357 * vim600: sw=4 ts=4 fdm=marker
1358 * vim<600: sw=4 ts=4