- Added fixes to AST
[haanga.git] / lib / Haanga / Compiler.php
blob28edc79173a42023b148c90a6720dea3be5b090f
1 <?php
2 /*
3 +---------------------------------------------------------------------------------+
4 | Copyright (c) 2010 Haanga |
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 /**
51 * number of blocks :-)
53 protected $in_block=0;
54 /**
55 * output buffers :-)
57 protected $ob_start=0;
58 protected $append;
59 protected $prepend_op;
60 /**
61 * Context at compile time
63 protected $context;
64 /**
65 * Table which contains all variables
66 * aliases defined in the template
68 protected $var_alias;
69 /**
70 * Flag the current variable as safe. This means
71 * that escape won't be called if autoescape is
72 * activated (which is activated by default)
74 public $var_is_safe=FALSE;
75 public $safes;
77 /* compiler options */
78 protected $autoescape = TRUE;
79 protected $if_empty = TRUE;
80 protected $dot_as_object = TRUE;
81 protected $strip_whitespace = FALSE;
82 protected $is_exec_enabled = FALSE;
84 /**
85 * Debug file
87 protected $debug;
88 // }}}
90 function __construct()
92 $this->generator = new Haanga_Generator_PHP;
93 if (self::$block_var===NULL) {
94 self::$block_var = '{{block.'.md5('super').'}}';
98 // isExecEnabled() {{{
99 /**
100 * Return TRUE if the special tag 'exec' is enabled (FALSE by default)
103 function isExecAllowed()
105 return $this->is_exec_enabled;
107 // }}}
109 function setOption($option, $value)
111 switch (strtolower($option)) {
112 case 'if_empty':
113 $this->if_empty = (bool)$value;
114 break;
115 case 'autoescape':
116 $this->autoescape = (bool)$value;
117 break;
118 case 'dot_as_object':
119 $this->dot_as_object = (bool)$value;
120 break;
121 case 'strip_whitespace':
122 $this->strip_whitespace = (bool)$value;
123 break;
124 case 'is_exec_enabled':
125 case 'allow_exec':
126 $this->is_exec_enabled = (bool)$value;
127 break;
131 // setDebug($file) {{{
132 function setDebug($file)
134 $this->debug = $file;
136 // }}}
138 // reset() {{{
139 function reset()
141 $avoid_cleaning = array(
142 'strip_whitespace' => 1, 'block_var' => 1, 'autoescape'=>1,
143 'if_empty' => 1, 'dot_as_object' => 1, 'is_exec_enabled' => 1,
145 foreach (array_keys(get_object_vars($this)) as $key) {
146 if (isset($avoid_cleaning[$key])) {
147 continue;
149 $this->$key = NULL;
151 $this->generator = new Haanga_Generator_PHP;
152 $this->blocks = array();
153 $this->cycle = array();
155 // }}}
157 // get_template_name() {{{
158 final function get_template_name()
160 return $this->name;
162 // }}}
164 // Set template name {{{
165 function set_template_name($path)
167 $file = basename($path);
168 $pos = strpos($file,'.');
169 return ($this->name = substr($file, 0, $pos));
171 // }}}
173 // get_function_name(string $name) {{{
174 function get_function_name($name)
176 return "{$name}_template";
178 // }}}
180 // Compile ($code, $name=NULL) {{{
181 final function compile($code, $name=NULL)
183 $this->name = $name;
185 $parsed = Haanga_Compiler_Lexer::init($code, $this);
186 $code = "";
187 $this->subtemplate = FALSE;
189 $body = new Haanga_AST;
190 $this->prepend_op = hcode();
192 if (isset($parsed[0]) && $parsed[0]['operation'] == 'base') {
193 /* {% base ... %} found */
194 $base = $parsed[0][0];
195 $code .= $this->get_base_template($base);
196 unset($parsed[0]);
199 if ($name) {
200 $func_name = $this->get_function_name($name);
201 if ($this->check_function) {
202 $body->do_if(hexpr(hexec('function_exists', $func_name), '===', FALSE));
204 if (isset($this->_file)) {
205 $body->comment("Generated from {$this->_base_dir}/{$this->_file}");
208 $body->declare_function($func_name);
209 $body->do_exec('extract', hvar('vars'));
210 $body->do_if(hexpr(hvar('return'), '==', TRUE));
211 $body->do_exec('ob_start');
212 $body->do_endif();
216 $this->generate_op_code($parsed, $body);
217 if ($this->subtemplate) {
218 $expr = $this->expr_call_base_template();
219 $this->do_print($body, $expr);
222 $body->do_if(hexpr(hvar('return'), '==', TRUE));
223 $body->do_return(hexec('ob_get_clean'));
224 $body->do_endif();
226 if ($name) {
227 $body->do_endfunction();
228 if ($this->check_function) {
229 $body->do_endif();
233 if ($this->prepend_op->stack_size() > 0) {
234 $this->prepend_op->append_ast($body);
235 $body = $this->prepend_op;
238 $op_code = $body->getArray(TRUE);
239 $code .= $this->generator->getCode($op_code);
240 if (!empty($this->append)) {
241 $code .= $this->append;
244 if (!empty($this->debug)) {
245 $op_code['php'] = $code;
246 file_put_contents($this->debug, print_r($op_code, TRUE));
248 return $code;
250 // }}}
252 // compile_file($file) {{{
254 * Compile a file
256 * @param string $file File path
257 * @param bool $safe Whether or not add check if the function is already defined
259 * @return Generated PHP code
261 final function compile_file($file, $safe=FALSE, $context=array())
263 if (!is_readable($file)) {
264 throw new Haanga_Compiler_Exception("$file is not a file");
266 $this->_base_dir = dirname($file);
267 $this->_file = basename($file);
268 $this->check_function = $safe;
269 $this->context = $context;
270 $name = $this->set_template_name($file);
271 return $this->compile(file_get_contents($file), $name);
273 // }}}
275 // is_expr methods {{{
276 function is_var_filter($cmd)
278 return isset($cmd['var_filter']);
281 // }}}
283 // expr_call_base_template() {{{
285 * Generate code to call base template
288 function expr_call_base_template()
290 return hexec(
291 $this->get_function_name($this->subtemplate),
292 hvar('vars'), TRUE,
293 hvar('blocks')
296 // }}}
298 // get_base_template($base) {{{
300 * Handle {% base "" %} definition. By default only
301 * static (string) are supported, but this can be overrided
302 * on subclasses.
304 * This method load the base class, compile it and return
305 * the generated code.
307 * @param array $base Base structure
309 * @return string Generated source code
311 function get_base_template($base)
313 if (!Haanga_AST::is_str($base)) {
314 throw new Haanga_Compiler_Exception("Dynamic inheritance is not supported for compilated templates");
316 $file = $base['string'];
317 list($this->subtemplate, $new_code) = $this->compile_required_template($file);
318 return $new_code."\n\n";
320 // }}}
322 // {% base "foo.html" %} {{{
323 protected function generate_op_base()
325 throw new exception("{% base %} can be only as first statement");
327 // }}}
329 // Main Loop {{{
330 protected function generate_op_code($parsed, &$body)
332 if (!is_array($parsed)) {
333 throw new Haanga_Compiler_Exception("Invalid \$parsed array");
335 foreach ($parsed as $op) {
336 if (!is_array($op)) {
337 continue;
339 if (!isset($op['operation'])) {
340 throw new Haanga_Compiler_Exception("Malformed array:".print_r($op, TRUE));
342 if ($this->subtemplate && $this->in_block == 0 && $op['operation'] != 'block') {
343 /* ignore most of tokens in subtemplates */
344 continue;
346 $method = "generate_op_".$op['operation'];
347 if (!is_callable(array($this, $method))) {
348 throw new Haanga_Compiler_Exception("Compiler: Missing method $method");
350 $this->$method($op, $body);
353 // }}}
355 // Check the current expr {{{
356 protected function check_expr(&$expr)
358 if (Haanga_AST::is_expr($expr)) {
359 if ($expr['op_expr'] == 'in') {
360 for ($id=0; $id < 2; $id++) {
361 if ($this->is_var_filter($expr[$id])) {
362 $expr[$id] = $this->get_filtered_var($expr[$id]['var_filter'], $var);
365 if (Haanga_AST::is_str($expr[1])) {
366 $expr = hexpr(hexec('strpos', $expr[1], $expr[0]), '!==', FALSE);
367 } else {
368 $expr = hexpr(
369 hexpr_cond(
370 hexec('is_array', $expr[1]),
371 hexec('array_search', $expr[0], $expr[1]),
372 hexec('strpos', $expr[1], $expr[0])
374 ,'!==', FALSE
378 if (is_object($expr)) {
379 $expr = $expr->getArray();
381 $this->check_expr($expr[0]);
382 $this->check_expr($expr[1]);
383 } else if (is_array($expr)) {
384 if ($this->is_var_filter($expr)) {
385 $expr = $this->get_filtered_var($expr['var_filter'], $var);
386 } else if (isset($expr['args'])) {
387 /* check every arguments */
388 foreach ($expr['args'] as &$v) {
389 $this->check_expr($v);
391 unset($v);
392 } else if (isset($expr['expr_cond'])) {
393 /* Check expr conditions */
394 $this->check_expr($expr['expr_cond']);
395 $this->check_expr($expr['true']);
396 $this->check_expr($expr['false']);
400 // }}}
402 // buffer <varname> {{{
403 public function generate_op_buffer($details, &$body)
405 $this->ob_start($body);
406 $this->generate_op_code($details['body'], $body);
407 $body->decl($details['name'], hvar('buffer'.$this->ob_start));
408 $this->ob_start--;
410 // }}}
412 // ifequal|ifnot equal <var_filtered|string|number> <var_fitlered|string|number> ... else ... {{{
413 protected function generate_op_ifequal($details, &$body)
415 $if['expr'] = hexpr($details[1], $details['cmp'], $details[2])->getArray();
416 $if['body'] = $details['body'];
417 if (isset($details['else'])) {
418 $if['else'] = $details['else'];
420 $this->generate_op_if($if, $body);
422 // }}}
424 // {% if <expr> %} HTML {% else %} TWO {% endif $} {{{
425 protected function generate_op_if($details, &$body)
427 if ($this->if_empty && $this->is_var_filter($details['expr']) && count($details['expr']['var_filter']) == 1) {
428 /* if we are doing if <Variable> it should check
429 if it exists without throw any warning */
430 $expr = $details['expr'];
431 $expr['var_filter'][] = 'empty';
433 $variable = $this->get_filtered_var($expr['var_filter'], $var);
435 $details['expr'] = hexpr($variable, '===', FALSE)->getArray();
437 $this->check_expr($details['expr']);
438 $expr = Haanga_AST::fromArrayGetAST($details['expr']);
439 $body->do_if($expr);
440 $this->generate_op_code($details['body'], $body);
441 if (isset($details['else'])) {
442 $body->do_else();
443 $this->generate_op_code($details['else'], $body);
445 $body->do_endif();
447 // }}}
449 // Override template {{{
450 protected function compile_required_template($file)
452 if (!is_file($file)) {
453 if (isset($this->_base_dir)) {
454 $file = $this->_base_dir.'/'.$file;
457 if (!is_file($file)) {
458 throw new Haanga_Compiler_Exception("can't find {$file} file template");
460 $class = get_class($this);
461 $comp = new $class;
462 $comp->reset();
463 $code = $comp->compile_file($file, $this->check_function);
464 return array($comp->get_template_name(), $code);
466 // }}}
468 // include "file.html" | include <var1> {{{
469 protected function generate_op_include($details, &$body)
471 if (!$details[0]['string']) {
472 throw new Haanga_Compiler_Exception("Dynamic inheritance is not supported for compilated templates");
474 list($name,$code) = $this->compile_required_template($details[0]['string']);
475 $this->append .= "\n\n{$code}";
476 $this->do_print($body,
477 hexec($this->get_function_name($name),
478 hvar('vars'), TRUE, hvar('blocks'))
481 // }}}
483 // Handle HTML code {{{
484 protected function generate_op_html($details, &$body)
486 $string = Haanga_AST::str($details['html']);
487 $this->do_print($body, $string);
489 // }}}
491 // get_var_filtered {{{
493 * This method handles all the filtered variable (piped_list(X)'s
494 * output in the parser.
497 * @param array $variable (Output of piped_list(B) (parser))
498 * @param array &$varname Variable name
499 * @param bool $accept_string TRUE is string output are OK (ie: block.parent)
501 * @return expr
504 function get_filtered_var($variable, &$varname, $accept_string=FALSE)
506 $this->var_is_safe = FALSE;
508 if (count($variable) > 1) {
509 $count = count($variable);
510 $target = $this->generate_variable_name($variable[0]);
512 if (!Haanga_AST::is_var($target)) {
513 /* block.super can't have any filter */
514 throw new Haanga_Compiler_Exception("This variable can't have any filter");
517 for ($i=1; $i < $count; $i++) {
518 $func_name = $variable[$i];
519 if ($func_name == 'escape') {
520 /* to avoid double cleaning */
521 $this->var_is_safe = TRUE;
523 $args = array(isset($exec) ? $exec : $target);
524 $exec = $this->do_filtering($func_name, $args);
526 unset($variable);
527 $varname = $args[0];
528 $details = $exec;
529 } else {
530 $details = $this->generate_variable_name($variable[0]);
531 $varname = $variable[0];
533 if (!Haanga_AST::is_var($details) && !$accept_string) {
534 /* generate_variable_name didn't replied a variable, weird case
535 currently just used for {{block.super}}.
537 throw new Haanga_Compiler_Exception("Invalid variable name {$variable[0]}");
541 return $details;
543 // }}}
545 // generate_op_print_var {{{
547 * Generate code to print a variable with its filters, if there is any.
549 * All variable (except those flagged as |safe) are automatically
550 * escaped if autoescape is "on".
553 protected function generate_op_print_var($details, &$body)
556 $details = $this->get_filtered_var($details['variable'], $variable, TRUE);
558 if (!Haanga_AST::is_var($details) && !Haanga_AST::is_exec($details)) {
559 /* generate_variable_name didn't replied a variable, weird case
560 currently just used for {{block.super}}.
562 $this->do_print($body, $details);
563 return;
566 if (!$this->is_safe($details) && $this->autoescape) {
567 $args = array($details);
568 $details = $this->do_filtering('escape', $args);
572 if (is_array($details)) {
573 $details = Haanga_AST::fromArrayGetAST($details);
575 $this->do_print($body, $details);
577 // }}}
579 // {# something #} {{{
580 protected function generate_op_comment($details, &$body)
582 /* comments are annoying */
583 //$body->comment($details['comment']);
585 // }}}
587 // {% block 'name' %} ... {% endblock %} {{{
588 protected function generate_op_block($details, &$body)
590 if (is_array($details['name'])) {
591 $name = "";
592 foreach ($details['name'] as $part) {
593 if (is_string($part)) {
594 $name .= "{$part}";
595 } else if (is_array($part)) {
596 if (Haanga_AST::is_str($part)) {
597 $name .= "{$part['string']}";
598 } elseif (isset($part['object'])) {
599 $name .= "{$part['object']}";
600 } else {
601 throw new Haanga_Compiler_Exception("Invalid blockname");
604 $name .= ".";
606 $details['name'] = substr($name, 0, -1);
608 $this->in_block++;
609 $this->blocks[] = $details['name'];
610 $block_name = hvar('blocks', $details['name']);
612 $this->ob_start($body);
613 $buffer_var = 'buffer'.$this->ob_start;
615 $content = hcode();
616 $this->generate_op_code($details['body'], $content);
618 $body->append_ast($content);
619 $this->ob_start--;
621 $buffer = hvar($buffer_var);
623 /* {{{ */
625 * isset previous block (parent block)?
626 * TRUE
627 * has reference to self::$block_var ?
628 * TRUE
629 * replace self::$block_var for current block value (buffer)
630 * FALSE
631 * print parent block
632 * FALSE
633 * print current block
636 $declare = hexpr_cond(
637 hexec('isset', $block_name),
638 hexpr_cond(
639 hexpr(hexec('strpos', $block_name, self::$block_var), '===', FALSE),
640 $block_name,
641 hexec('str_replace', self::$block_var, $buffer, $block_name)
642 ), $buffer);
643 /* }}} */
645 if (!$this->subtemplate) {
646 $this->do_print($body, $declare);
647 } else {
648 $body->decl($block_name, $declare);
649 if ($this->in_block > 1) {
650 $this->do_print($body, $block_name);
653 array_pop($this->blocks);
654 $this->in_block--;
657 // }}}
659 // regroup <var1> by <field> as <foo> {{{
660 protected function generate_op_regroup($details, &$body)
662 $body->comment("Temporary sorting");
664 $array = $this->get_filtered_var($details['array'], $varname);
666 if (Haanga_AST::is_exec($array)) {
667 $varname = hvar($details['as']);
668 $body->decl($varname, $array);
670 $var = hvar('item', $details['row']);
672 $body->decl('temp_group', array());
674 $body->do_foreach($varname, 'item', NULL,
675 hcode()->decl(hvar('temp_group', $var, NULL), hvar('item'))
678 $body->comment("Proper format");
679 $body->decl($details['as'], array());
680 $body->do_foreach('temp_group', 'item', 'group',
681 hcode()->decl(
682 hvar($details['as'], NULL),
683 array("grouper" => hvar('group'), "list" => hvar('item'))
686 $body->comment("Sorting done");
688 // }}}
690 // variable context {{{
692 * Variables context
694 * These two functions are useful to detect if a variable
695 * separated by dot (foo.bar) is an array or object. To avoid
696 * overhead we decide it at compile time, rather than
697 * ask over and over at rendering time.
699 * foo.bar:
700 * + If foo exists at compile time,
701 * and it is an array, it would be foo['bar']
702 * otherwise it'd be foo->bar.
703 * + If foo don't exists at compile time,
704 * it would be foo->bar if the compiler option
705 * dot_as_object is TRUE (by default) otherwise
706 * it'd be foo['bar']
708 * @author crodas
709 * @author gallir (ideas)
712 function set_context($varname, $value)
714 $this->context[$varname] = $value;
717 function var_is_object(Array $variable)
719 $varname = $variable[0];
720 switch ($varname) {
721 case 'GLOBALS':
722 case '_SERVER':
723 case '_GET':
724 case '_POST':
725 case '_FILES':
726 case '_COOKIE':
727 case '_SESSION':
728 case '_REQUEST':
729 case '_ENV':
730 case 'forloop':
731 case 'block':
732 return FALSE; /* these are arrays */
735 if (isset($this->context[$varname])) {
736 if (count($variable) == 1) {
737 return is_object($this->context[$varname]);
739 $var = & $this->context[$varname];
740 foreach ($variable as $id => $part) {
741 if ($id != 0) {
742 if (is_array($part) && isset($part['object'])) {
743 $var = &$var->$part['object'];
744 } else if (is_object($var)) {
745 $var = &$var->$part;
746 } else {
747 $var = &$var[$part];
752 $type = is_object($var);
754 /* delete reference */
755 unset($var);
757 return $type;
760 return $this->dot_as_object;
762 // }}}
764 // Get variable name {{{
765 protected function generate_variable_name($variable)
767 if (is_array($variable)) {
768 switch ($variable[0]) {
769 case 'forloop':
770 if (!$this->forid) {
771 throw new Haanga_Compiler_Exception("Invalid forloop reference outside of a loop");
773 switch ($variable[1]) {
774 case 'counter':
775 $this->forloop[$this->forid]['counter'] = TRUE;
776 $variable = 'forcounter1_'.$this->forid;
777 break;
778 case 'counter0':
779 $this->forloop[$this->forid]['counter0'] = TRUE;
780 $variable = 'forcounter0_'.$this->forid;
781 break;
782 case 'last':
783 $this->forloop[$this->forid]['counter'] = TRUE;
784 $this->forloop[$this->forid]['last'] = TRUE;
785 $variable = 'islast_'.$this->forid;
786 break;
787 case 'first':
788 $this->forloop[$this->forid]['first'] = TRUE;
789 $variable = 'isfirst_'.$this->forid;
790 break;
791 case 'revcounter':
792 $this->forloop[$this->forid]['revcounter'] = TRUE;
793 $variable = 'revcount_'.$this->forid;
794 break;
795 case 'revcounter0':
796 $this->forloop[$this->forid]['revcounter0'] = TRUE;
797 $variable = 'revcount0_'.$this->forid;
798 break;
799 case 'parentloop':
800 unset($variable[1]);
801 $this->forid--;
802 $variable = $this->generate_variable_name(array_values($variable));
803 $variable = $variable['var'];
804 $this->forid++;
805 break;
806 default:
807 throw new Haanga_Compiler_Exception("Unexpected forloop.{$variable[1]}");
809 /* no need to escape it */
810 $this->var_is_safe = TRUE;
811 break;
812 case 'block':
813 if ($this->in_block == 0) {
814 throw new Haanga_Compiler_Exception("Can't use block.super outside a block");
816 if (!$this->subtemplate) {
817 throw new Haanga_Compiler_Exception("Only subtemplates can call block.super");
819 /* no need to escape it */
820 $this->var_is_safe = TRUE;
821 return Haanga_AST::str(self::$block_var);
822 break;
825 } else if (isset($this->var_alias[$variable])) {
826 $variable = $this->var_alias[$variable];
829 return hvar($variable)->getArray();
831 // }}}
833 // Print {{{
834 public function do_print(Haanga_AST $code, $stmt)
836 /* Flag this object as a printing one */
837 $code->doesPrint = TRUE;
839 $buffer = hvar('buffer'.$this->ob_start);
841 if ($this->strip_whitespace && Haanga_AST::is_str($stmt)) {
842 $stmt['string'] = preg_replace('/\s+/', ' ', $stmt['string']);
843 if (trim($stmt['string']) == "") {
844 return; /* avoid whitespaces */
848 if ($this->ob_start == 0) {
849 $code->do_echo($stmt);
850 return;
853 $code->append($buffer, $stmt);
857 // }}}
859 // for [<key>,]<val> in <array> {{{
860 protected function generate_op_loop($details, &$body)
862 if (isset($details['empty'])) {
863 $body->do_if(hexpr(hexec('count', hvar($details['array'])), '==', 0));
864 $this->generate_op_code($details['empty'], $body);
865 $body->do_else();
868 /* ForID */
869 $oldid = $this->forid;
870 $this->forid = $oldid+1;
871 $this->forloop[$this->forid] = array();
873 /* variables */
874 $array = $this->get_filtered_var($details['array'], $varname);
876 /* Loop body */
877 if ($this->is_safe(hvar($varname))) {
878 $this->set_safe(hvar($details['variable']));
881 $for_body = hcode();
882 $this->generate_op_code($details['body'], $for_body);
884 if ($this->is_safe(hvar($varname))) {
885 $this->set_unsafe($details['variable']);
888 $oid = $this->forid;
889 $size = hvar('psize_'.$oid);
891 // counter {{{
892 if (isset($this->forloop[$oid]['counter'])) {
893 $var = hvar('forcounter1_'.$oid);
894 $body->decl($var, 1);
895 $for_body->decl($var, hexpr($var, '+', 1));
897 // }}}
899 // counter0 {{{
900 if (isset($this->forloop[$oid]['counter0'])) {
901 $var = hvar('forcounter0_'.$oid);
902 $body->decl($var, 0);
903 $for_body->decl($var, hexpr($var, '+', 1));
905 // }}}
907 // last {{{
908 if (isset($this->forloop[$oid]['last'])) {
909 if (!isset($cnt)) {
910 $body->decl('psize_'.$oid, hexec('count', hvar_ex($details['array'])));
911 $cnt = TRUE;
913 $var = 'islast_'.$oid;
914 $body->decl($var, hexpr(hvar('forcounter1_'.$oid), '==', $size));
915 $for_body->decl($var, hexpr(hvar('forcounter1_'.$oid), '==', $size));
917 // }}}
919 // first {{{
920 if (isset($this->forloop[$oid]['first'])) {
921 $var = hvar('isfirst_'.$oid);
922 $body->decl($var, TRUE);
923 $for_body->decl($var, FALSE);
925 // }}}
927 // revcounter {{{
928 if (isset($this->forloop[$oid]['revcounter'])) {
929 if (!isset($cnt)) {
930 $body->decl('psize_'.$oid, hexec('count', hvar_ex($details['array'])));
931 $cnt = TRUE;
933 $var = hvar('revcount_'.$oid);
934 $body->decl($var, $size);
935 $for_body->decl($var, hexpr($var, '-', 1));
937 // }}}
939 // revcounter0 {{{
940 if (isset($this->forloop[$oid]['revcounter0'])) {
941 if (!isset($cnt)) {
942 $body->decl('psize_'.$oid, hexec('count', hvar_ex($details['array'])));
943 $cnt = TRUE;
945 $var = hvar('revcount0_'.$oid);
946 $body->decl($var, hexpr($size, "-", 1));
947 $for_body->decl($var, hexpr($var, '-', 1));
949 // }}}
951 /* Restore old ForID */
952 $this->forid = $oldid;
954 /* Merge loop body */
955 $body->do_foreach($array, $details['variable'], $details['index'], $for_body);
957 if (isset($details['empty'])) {
958 $body->do_endif();
961 // }}}
963 // ifchanged [<var1> <var2] {{{
964 protected function generate_op_ifchanged($details, &$body)
966 static $ifchanged = 0;
968 $ifchanged++;
969 $var1 = 'ifchanged'.$ifchanged;
970 if (!isset($details['check'])) {
971 /* ugly */
972 $this->ob_start($body);
973 $var2 = hvar('buffer'.$this->ob_start);
976 $this->generate_op_code($details['body'], $body);
977 $this->ob_start--;
978 $body->do_if(hexpr(hexec('isset', hvar($var1)), '==', FALSE, '||', hvar($var1), '!=', $var2));
979 $this->do_print($body, $var2);
980 $body->decl($var1, $var2);
981 } else {
982 /* beauty :-) */
983 foreach ($details['check'] as $id=>$type) {
984 if (!Haanga_AST::is_var($type)) {
985 throw new Haanga_Compiler_Exception("Unexpected string {$type['string']}, expected a varabile");
988 $this_expr = hexpr(hexpr(
989 hexec('isset', hvar($var1, $id)), '==', FALSE,
990 '||', hvar($var1, $id), '!=', $type
993 if (isset($expr)) {
994 $this_expr = hexpr($expr, '&&', $this_expr);
997 $expr = $this_expr;
1000 $body->do_if($expr);
1001 $this->generate_op_code($details['body'], $body);
1002 $body->decl($var1, $details['check']);
1005 if (isset($details['else'])) {
1006 $body->do_else();
1007 $this->generate_op_code($details['else'], $body);
1009 $body->do_endif();
1011 // }}}
1013 // autoescape ON|OFF {{{
1014 function generate_op_autoescape($details, &$body)
1016 $old_autoescape = $this->autoescape;
1017 $this->autoescape = strtolower($details['value']) == 'on';
1018 $this->generate_op_code($details['body'], $body);
1019 $this->autoescape = $old_autoescape;
1021 // }}}
1023 // ob_Start(array &$body) {{{
1025 * Start a new buffering
1028 function ob_start(&$body)
1030 $this->ob_start++;
1031 $body->decl('buffer'.$this->ob_start, "");
1033 // }}}
1035 // Custom Tags {{{
1036 function get_custom_tag($name)
1038 $function = $this->get_function_name($this->name).'_tag_'.$name;
1039 $this->append .= "\n\n".Haanga_Extension::getInstance('Tag')->getFunctionBody($name, $function);
1040 return $function;
1044 * Generate needed code for custom tags (tags that aren't
1045 * handled by the compiler).
1048 function generate_op_custom_tag($details, &$body)
1050 static $tags;
1051 if (!$tags) {
1052 $tags = Haanga_Extension::getInstance('Tag');
1055 $tag_name = $details['name'];
1056 $tagFunction = $tags->getFunctionAlias($tag_name);
1058 if (!$tagFunction && !$tags->hasGenerator($tag_name)) {
1059 $function = $this->get_custom_tag($tag_name, isset($details['as']));
1060 } else {
1061 $function = $tagFunction;
1064 if (isset($details['body'])) {
1066 if the custom tag has 'body'
1067 then it behave the same way as a filter
1069 $this->ob_start($body);
1070 $this->generate_op_code($details['body'], $body);
1071 $target = hvar('buffer'.$this->ob_start);
1072 if ($tags->hasGenerator($tag_name)) {
1073 $exec = $tags->generator($tag_name, $this, array($target));
1074 if (!$exec InstanceOf Haanga_AST) {
1075 throw new Haanga_Compiler_Exception("Invalid output of custom filter {$tag_name}");
1077 } else {
1078 $exec = hexec($function, $target);
1080 $this->ob_start--;
1081 $this->do_print($body, $exec);
1082 return;
1085 $var = isset($details['as']) ? $details['as'] : NULL;
1086 $args = array_merge(array($function), $details['list']);
1088 if ($tags->hasGenerator($tag_name)) {
1089 $exec = $tags->generator($tag_name, $this, $details['list'], $var);
1090 if ($exec InstanceOf Haanga_AST) {
1091 if ($exec->stack_size() >= 2 || $exec->doesPrint) {
1093 The generator returned more than one statement,
1094 so we assume the output is already handled
1095 by one of those stmts.
1097 $body->append_ast($exec);
1098 return;
1100 } else {
1101 throw new Haanga_Compiler_Exception("Invalid output of the custom tag {$tag_name}");
1103 } else {
1104 $fnc = array_shift($args);
1105 $exec = hexec($fnc);
1106 foreach ($args as $arg) {
1107 $exec->param($arg);
1111 if ($var) {
1112 $body->decl($var, $exec);
1113 } else {
1114 $this->do_print($body, $exec);
1117 // }}}
1119 // with <variable> as <var> {{{
1124 function generate_op_alias($details, &$body)
1126 $this->var_alias[ $details['as'] ] = $details['var'];
1127 $this->generate_op_code($details['body'], $body);
1128 unset($this->var_alias[ $details['as'] ] );
1130 // }}}
1132 // Custom Filters {{{
1133 function get_custom_filter($name)
1135 $function = $this->get_function_name($this->name).'_filter_'.$name;
1136 $this->append .= "\n\n".Haanga_Extension::getInstance('Filter')->getFunctionBody($name, $function);
1137 return $function;
1141 function do_filtering($name, $args)
1143 static $filter;
1144 if (!$filter) {
1145 $filter = Haanga_Extension::getInstance('Filter');
1148 if (is_array($name)) {
1150 prepare array for ($func_name, $arg1, $arg2 ... )
1151 where $arg1 = last expression and $arg2.. $argX is
1152 defined in the template
1154 $args = array_merge($args, $name['args']);
1155 $name = $name[0];
1158 if (!$filter->isValid($name)) {
1159 throw new Haanga_Compiler_Exception("{$name} is an invalid filter");
1162 if ($filter->hasGenerator($name)) {
1163 return $filter->generator($name, $this, $args);
1165 $fnc = $filter->getFunctionAlias($name);
1166 if (!$fnc) {
1167 $fnc = $this->get_custom_filter($name);
1170 $args = array_merge(array($fnc), $args);
1171 $exec = call_user_func_array('hexec', $args);
1173 return $exec;
1176 function generate_op_filter($details, &$body)
1178 $this->ob_start($body);
1179 $this->generate_op_code($details['body'], $body);
1180 $target = hvar('buffer'.$this->ob_start);
1181 foreach ($details['functions'] as $f) {
1182 $param = (isset($exec) ? $exec : $target);
1183 $exec = $this->do_filtering($f, array($param));
1185 $this->ob_start--;
1186 $this->do_print($body, $exec);
1188 // }}}
1190 /* variable safety {{{ */
1191 function set_safe($name)
1193 if (!Haanga_AST::is_Var($name)) {
1194 $name = hvar($name)->getArray();
1196 $this->safes[serialize($name)] = TRUE;
1199 function set_unsafe($name)
1201 if (!Haanga_AST::is_Var($name)) {
1202 $name = hvar($name)->getArray();
1204 unset($this->safes[serialize($name)]);
1207 function is_safe($name)
1209 if ($this->var_is_safe) {
1210 return TRUE;
1212 if (isset($this->safes[serialize($name)])) {
1213 return TRUE;
1215 return FALSE;
1217 /* }}} */
1219 final static function main_cli()
1221 $argv = $GLOBALS['argv'];
1222 $haanga = new Haanga_Compiler;
1223 $code = $haanga->compile_file($argv[1], TRUE);
1224 if (!isset($argv[2]) || $argv[2] != '--notags') {
1225 $code = "<?php\n\n$code";
1227 echo $code;
1233 * Local variables:
1234 * tab-width: 4
1235 * c-basic-offset: 4
1236 * End:
1237 * vim600: sw=4 ts=4 fdm=marker
1238 * vim<600: sw=4 ts=4