- Changed Lexer and Parser
[haanga.git] / lib / Haanga / Compiler.php
blob9a80b4b67e48d1e8cac8e6e7036d00716218a5c8
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();
86 static protected $echo_concat = '.';
87 static protected $enable_load = TRUE;
89 /**
90 * Debug file
92 protected $debug;
93 // }}}
95 function __construct()
97 $this->generator = new Haanga_Generator_PHP;
98 if (self::$block_var===NULL) {
99 self::$block_var = '{{block.'.md5('super').'}}';
103 // getOption($option) {{{
104 public static function getOption($option)
106 $value = NULL;
107 switch (strtolower($option)) {
108 case 'enable_load':
109 $value = self::$enable_load;
110 break;
111 case 'if_empty':
112 $value = self::$if_empty;
113 break;
114 case 'autoescape':
115 $value = self::$autoescape;
116 break;
117 case 'dot_as_object':
118 $value = self::$dot_as_object;
119 break;
120 case 'echo_concat':
121 $value = self::$echo_concat;
122 break;
123 case 'strip_whitespace':
124 $value = self::$strip_whitespace;
125 break;
126 case 'is_exec_enabled':
127 case 'allow_exec':
128 $value = self::$is_exec_enabled;
129 break;
130 case 'global':
131 $value = self::$global_context;
132 break;
134 return $value;
136 // }}}
138 // setOption($option, $value) {{{
140 * Set Compiler option.
142 * @return void
144 public static function setOption($option, $value)
146 switch (strtolower($option)) {
147 case 'if_empty':
148 self::$if_empty = (bool)$value;
149 break;
150 case 'enable_load':
151 self::$enable_load = (bool)$value;
152 case 'echo_concat':
153 if ($value == '.' || $value == ',') {
154 self::$echo_concat = $value;
156 break;
158 case 'autoescape':
159 self::$autoescape = (bool)$value;
160 break;
161 case 'dot_as_object':
162 self::$dot_as_object = (bool)$value;
163 break;
164 case 'strip_whitespace':
165 self::$strip_whitespace = (bool)$value;
166 break;
167 case 'is_exec_enabled':
168 case 'allow_exec':
169 self::$is_exec_enabled = (bool)$value;
170 break;
171 case 'global':
172 if (!is_array($value)) {
173 $value = array($value);
175 self::$global_context = $value;
176 break;
179 // }}}
181 // setDebug($file) {{{
182 function setDebug($file)
184 $this->debug = $file;
186 // }}}
188 // reset() {{{
189 function reset()
191 foreach (array_keys(get_object_vars($this)) as $key) {
192 if (isset($avoid_cleaning[$key])) {
193 continue;
195 $this->$key = NULL;
197 $this->generator = new Haanga_Generator_PHP;
198 $this->blocks = array();
199 $this->cycle = array();
201 // }}}
203 // get_template_name() {{{
204 final function get_template_name()
206 return $this->name;
208 // }}}
210 // Set template name {{{
211 function set_template_name($path)
213 $file = basename($path);
214 $pos = strpos($file,'.');
215 return ($this->name = substr($file, 0, $pos));
217 // }}}
219 // get_function_name(string $name) {{{
220 function get_function_name($name)
222 return "{$name}_template";
224 // }}}
226 // Compile ($code, $name=NULL) {{{
227 final function compile($code, $name=NULL, $file=NULL)
229 $this->name = $name;
231 $parsed = Haanga_Compiler_Lexer::init($code, $this, $file);
232 $code = "";
233 $this->subtemplate = FALSE;
235 $body = new Haanga_AST;
236 $this->prepend_op = hcode();
238 if (isset($parsed[0]) && $parsed[0]['operation'] == 'base') {
239 /* {% base ... %} found */
240 $base = $parsed[0][0];
241 $code .= $this->get_base_template($base);
242 unset($parsed[0]);
245 if (defined('HAANGA_VERSION')) {
246 $body->decl('HAANGA_VERSION', HAANGA_VERSION);
249 if ($name) {
250 $func_name = $this->get_function_name($name);
251 if ($this->check_function) {
252 $body->do_if(hexpr(hexec('function_exists', $func_name), '===', FALSE));
254 if (!empty($this->file)) {
255 $body->comment("Generated from ".$this->file);
258 $body->declare_function($func_name);
259 if (count(self::$global_context) > 0) {
260 $body->do_global(self::$global_context);
262 $body->do_exec('extract', hvar('vars'));
263 $body->do_if(hexpr(hvar('return'), '==', TRUE));
264 $body->do_exec('ob_start');
265 $body->do_endif();
269 $this->generate_op_code($parsed, $body);
270 if ($this->subtemplate) {
271 $expr = $this->expr_call_base_template();
272 $this->do_print($body, $expr);
275 $body->do_if(hexpr(hvar('return'), '==', TRUE));
276 $body->do_return(hexec('ob_get_clean'));
277 $body->do_endif();
279 if ($name) {
280 $body->do_endfunction();
281 if ($this->check_function) {
282 $body->do_endif();
286 if ($this->prepend_op->stack_size() > 0) {
287 $this->prepend_op->append_ast($body);
288 $body = $this->prepend_op;
291 $op_code = $body->getArray(TRUE);
294 $code .= $this->generator->getCode($op_code);
295 if (!empty($this->append)) {
296 $code .= $this->append;
299 if (!empty($this->debug)) {
300 $op_code['php'] = $code;
301 file_put_contents($this->debug, print_r($op_code, TRUE), LOCK_EX);
303 return $code;
305 // }}}
307 // compile_file($file) {{{
309 * Compile a file
311 * @param string $file File path
312 * @param bool $safe Whether or not add check if the function is already defined
314 * @return Generated PHP code
316 final function compile_file($file, $safe=FALSE, $context=array())
318 if (!is_readable($file)) {
319 throw new Haanga_Compiler_Exception("$file is not a file");
322 if (count(self::$global_context) > 0) {
323 /* add global variables (if any) to the current context */
324 foreach (self::$global_context as $var) {
325 $context[$var] = &$GLOBALS[$var];
329 $this->_base_dir = dirname($file);
330 $this->file = realpath($file);
331 $this->line = 0;
332 $this->check_function = $safe;
333 $this->context = $context;
334 $name = $this->set_template_name($file);
335 return $this->compile(file_get_contents($file), $name, $file);
337 // }}}
339 // getOpCodes($code, $file='') {{{
341 * Compile the $code and return the "opcodes"
342 * (the Abstract syntax tree).
344 * @param string $code Template content
345 * @param string $file File path (used for erro reporting)
347 * @return Haanga_AST
350 public function getOpCodes($code, $file)
352 $oldfile = $this->file;
353 $this->file = $file;
354 $parsed = Haanga_Compiler_Lexer::init($code, $this, $file);
355 $body = new Haanga_AST;
356 if (isset($parsed[0]) && $parsed[0]['operation'] == 'base') {
357 $this->Error("{% base is not supported on inlines %}");
359 $body = new Haanga_AST;
360 $this->generate_op_code($parsed, $body);
361 $this->file = $oldfile;
362 return $body;
364 // }}}
366 // Error($errtxt) {{{
368 * Throw an exception and appends information about the template (the path and
369 * the last processed line).
373 public function Error($err)
375 throw new Haanga_Compiler_Exception("{$err} in {$this->file}:$this->line");
377 // }}}
379 // is_expr methods {{{
380 function is_var_filter($cmd)
382 return isset($cmd['var_filter']);
385 // }}}
387 // expr_call_base_template() {{{
389 * Generate code to call base template
392 function expr_call_base_template()
394 return hexec(
395 $this->get_function_name($this->subtemplate),
396 hvar('vars'), TRUE,
397 hvar('blocks')
400 // }}}
402 // get_base_template($base) {{{
404 * Handle {% base "" %} definition. By default only
405 * static (string) are supported, but this can be overrided
406 * on subclasses.
408 * This method load the base class, compile it and return
409 * the generated code.
411 * @param array $base Base structure
413 * @return string Generated source code
415 function get_base_template($base)
417 if (!Haanga_AST::is_str($base)) {
418 $this->Error("Dynamic inheritance is not supported for compilated templates");
420 $file = $base['string'];
421 list($this->subtemplate, $new_code) = $this->compile_required_template($file);
422 return $new_code."\n\n";
424 // }}}
426 // {% base "foo.html" %} {{{
427 protected function generate_op_base()
429 $this->Error("{% base %} can be only as first statement");
431 // }}}
433 // Main Loop {{{
434 protected function generate_op_code($parsed, &$body)
436 if (!is_array($parsed)) {
437 $this->Error("Invalid \$parsed array");
439 foreach ($parsed as $op) {
440 if (!is_array($op)) {
441 continue;
443 if (!isset($op['operation'])) {
444 $this->Error("Malformed array:".print_r($op, TRUE));
446 if (isset($op['line'])) {
447 $this->line = $op['line'];
450 if ($this->subtemplate && $this->in_block == 0 && $op['operation'] != 'block') {
451 /* ignore most of tokens in subtemplates */
452 continue;
455 $method = "generate_op_".$op['operation'];
456 if (!is_callable(array($this, $method))) {
457 $this->Error("Compiler: Missing method $method");
459 $this->$method($op, $body);
462 // }}}
464 // Check the current expr {{{
465 protected function check_expr(&$expr)
467 if (Haanga_AST::is_expr($expr)) {
468 if ($expr['op_expr'] == 'in') {
469 for ($id=0; $id < 2; $id++) {
470 if ($this->is_var_filter($expr[$id])) {
471 $expr[$id] = $this->get_filtered_var($expr[$id]['var_filter'], $var);
474 if (Haanga_AST::is_str($expr[1])) {
475 $expr = hexpr(hexec('strpos', $expr[1], $expr[0]), '!==', FALSE);
476 } else {
477 $expr = hexpr(
478 hexpr_cond(
479 hexec('is_array', $expr[1]),
480 hexec('array_search', $expr[0], $expr[1]),
481 hexec('strpos', $expr[1], $expr[0])
483 ,'!==', FALSE
487 if (is_object($expr)) {
488 $expr = $expr->getArray();
490 $this->check_expr($expr[0]);
491 $this->check_expr($expr[1]);
492 } else if (is_array($expr)) {
493 if ($this->is_var_filter($expr)) {
494 $expr = $this->get_filtered_var($expr['var_filter'], $var);
495 } else if (isset($expr['args'])) {
496 /* check every arguments */
497 foreach ($expr['args'] as &$v) {
498 $this->check_expr($v);
500 unset($v);
501 } else if (isset($expr['expr_cond'])) {
502 /* Check expr conditions */
503 $this->check_expr($expr['expr_cond']);
504 $this->check_expr($expr['true']);
505 $this->check_expr($expr['false']);
509 // }}}
511 // ifequal|ifnot equal <var_filtered|string|number> <var_fitlered|string|number> ... else ... {{{
512 protected function generate_op_ifequal($details, &$body)
514 $if['expr'] = hexpr($details[1], $details['cmp'], $details[2])->getArray();
515 $if['body'] = $details['body'];
516 if (isset($details['else'])) {
517 $if['else'] = $details['else'];
519 $this->generate_op_if($if, $body);
521 // }}}
523 // {% if <expr> %} HTML {% else %} TWO {% endif $} {{{
524 protected function generate_op_if($details, &$body)
526 if (self::$if_empty && $this->is_var_filter($details['expr']) && count($details['expr']['var_filter']) == 1) {
527 /* if we are doing if <Variable> it should check
528 if it exists without throw any warning */
529 $expr = $details['expr'];
530 $expr['var_filter'][] = 'empty';
532 $variable = $this->get_filtered_var($expr['var_filter'], $var);
534 $details['expr'] = hexpr($variable, '===', FALSE)->getArray();
536 $this->check_expr($details['expr']);
537 $expr = Haanga_AST::fromArrayGetAST($details['expr']);
538 $body->do_if($expr);
539 $this->generate_op_code($details['body'], $body);
540 if (isset($details['else'])) {
541 $body->do_else();
542 $this->generate_op_code($details['else'], $body);
544 $body->do_endif();
546 // }}}
548 // Override template {{{
549 protected function compile_required_template($file)
551 if (!is_file($file)) {
552 if (isset($this->_base_dir)) {
553 $file = $this->_base_dir.'/'.$file;
556 if (!is_file($file)) {
557 $this->Error("can't find {$file} file template");
559 $class = get_class($this);
560 $comp = new $class;
561 $comp->reset();
562 $code = $comp->compile_file($file, $this->check_function);
563 return array($comp->get_template_name(), $code);
565 // }}}
567 // include "file.html" | include <var1> {{{
568 protected function generate_op_include($details, &$body)
570 if (!$details[0]['string']) {
571 $this->Error("Dynamic inheritance is not supported for compilated templates");
573 list($name,$code) = $this->compile_required_template($details[0]['string']);
574 $this->append .= "\n\n{$code}";
575 $this->do_print($body,
576 hexec($this->get_function_name($name),
577 hvar('vars'), TRUE, hvar('blocks'))
580 // }}}
582 // Handle HTML code {{{
583 protected function generate_op_html($details, &$body)
585 $string = Haanga_AST::str($details['html']);
586 $this->do_print($body, $string);
588 // }}}
590 // get_var_filtered {{{
592 * This method handles all the filtered variable (piped_list(X)'s
593 * output in the parser.
596 * @param array $variable (Output of piped_list(B) (parser))
597 * @param array &$varname Variable name
598 * @param bool $accept_string TRUE is string output are OK (ie: block.parent)
600 * @return expr
603 function get_filtered_var($variable, &$varname, $accept_string=FALSE)
605 $this->var_is_safe = FALSE;
607 if (count($variable) > 1) {
608 $count = count($variable);
609 $target = $this->generate_variable_name($variable[0]);
611 if (!Haanga_AST::is_var($target)) {
612 /* block.super can't have any filter */
613 $this->Error("This variable can't have any filter");
616 for ($i=1; $i < $count; $i++) {
617 $func_name = $variable[$i];
618 if ($func_name == 'escape') {
619 /* to avoid double cleaning */
620 $this->var_is_safe = TRUE;
622 $args = array(isset($exec) ? $exec : $target);
623 $exec = $this->do_filtering($func_name, $args);
625 unset($variable);
626 $varname = $args[0];
627 $details = $exec;
628 } else {
629 $details = $this->generate_variable_name($variable[0]);
630 $varname = $variable[0];
632 if (!Haanga_AST::is_var($details) && !$accept_string) {
633 /* generate_variable_name didn't replied a variable, weird case
634 currently just used for {{block.super}}.
636 $this->Error("Invalid variable name {$variable[0]}");
640 return $details;
642 // }}}
644 // generate_op_print_var {{{
646 * Generate code to print a variable with its filters, if there is any.
648 * All variable (except those flagged as |safe) are automatically
649 * escaped if autoescape is "on".
652 protected function generate_op_print_var($details, &$body)
655 $details = $this->get_filtered_var($details['variable'], $variable, TRUE);
657 if (!Haanga_AST::is_var($details) && !Haanga_AST::is_exec($details)) {
658 /* generate_variable_name didn't replied a variable, weird case
659 currently just used for {{block.super}}.
661 $this->do_print($body, $details);
662 return;
665 if (!$this->is_safe($details) && self::$autoescape) {
666 $args = array($details);
667 $details = $this->do_filtering('escape', $args);
671 if (is_array($details)) {
672 $details = Haanga_AST::fromArrayGetAST($details);
674 $this->do_print($body, $details);
676 // }}}
678 // {# something #} {{{
679 protected function generate_op_comment($details, &$body)
681 /* comments are annoying */
682 //$body->comment($details['comment']);
684 // }}}
686 // {% block 'name' %} ... {% endblock %} {{{
687 protected function generate_op_block($details, &$body)
689 if (is_array($details['name'])) {
690 $name = "";
691 foreach ($details['name'] as $part) {
692 if (is_string($part)) {
693 $name .= "{$part}";
694 } else if (is_array($part)) {
695 if (Haanga_AST::is_str($part)) {
696 $name .= "{$part['string']}";
697 } elseif (isset($part['object'])) {
698 $name .= "{$part['object']}";
699 } else {
700 $this->Error("Invalid blockname");
703 $name .= ".";
705 $details['name'] = substr($name, 0, -1);
707 $this->in_block++;
708 $this->blocks[] = $details['name'];
709 $block_name = hvar('blocks', $details['name']);
711 $this->ob_start($body);
712 $buffer_var = 'buffer'.$this->ob_start;
714 $content = hcode();
715 $this->generate_op_code($details['body'], $content);
717 $body->append_ast($content);
718 $this->ob_start--;
720 $buffer = hvar($buffer_var);
722 /* {{{ */
724 * isset previous block (parent block)?
725 * TRUE
726 * has reference to self::$block_var ?
727 * TRUE
728 * replace self::$block_var for current block value (buffer)
729 * FALSE
730 * print parent block
731 * FALSE
732 * print current block
735 $declare = hexpr_cond(
736 hexec('isset', $block_name),
737 hexpr_cond(
738 hexpr(hexec('strpos', $block_name, self::$block_var), '===', FALSE),
739 $block_name,
740 hexec('str_replace', self::$block_var, $buffer, $block_name)
741 ), $buffer);
742 /* }}} */
744 if (!$this->subtemplate) {
745 $this->do_print($body, $declare);
746 } else {
747 $body->decl($block_name, $declare);
748 if ($this->in_block > 1) {
749 $this->do_print($body, $block_name);
752 array_pop($this->blocks);
753 $this->in_block--;
756 // }}}
758 // regroup <var1> by <field> as <foo> {{{
759 protected function generate_op_regroup($details, &$body)
761 $body->comment("Temporary sorting");
763 $array = $this->get_filtered_var($details['array'], $varname);
765 if (Haanga_AST::is_exec($array)) {
766 $varname = hvar($details['as']);
767 $body->decl($varname, $array);
769 $var = hvar('item', $details['row']);
771 $body->decl('temp_group', array());
773 $body->do_foreach($varname, 'item', NULL,
774 hcode()->decl(hvar('temp_group', $var, NULL), hvar('item'))
777 $body->comment("Proper format");
778 $body->decl($details['as'], array());
779 $body->do_foreach('temp_group', 'item', 'group',
780 hcode()->decl(
781 hvar($details['as'], NULL),
782 array("grouper" => hvar('group'), "list" => hvar('item'))
785 $body->comment("Sorting done");
787 // }}}
789 // variable context {{{
791 * Variables context
793 * These two functions are useful to detect if a variable
794 * separated by dot (foo.bar) is an array or object. To avoid
795 * overhead we decide it at compile time, rather than
796 * ask over and over at rendering time.
798 * foo.bar:
799 * + If foo exists at compile time,
800 * and it is an array, it would be foo['bar']
801 * otherwise it'd be foo->bar.
802 * + If foo don't exists at compile time,
803 * it would be foo->bar if the compiler option
804 * dot_as_object is TRUE (by default) otherwise
805 * it'd be foo['bar']
807 * @author crodas
808 * @author gallir (ideas)
811 function set_context($varname, $value)
813 $this->context[$varname] = $value;
816 function get_context($variable)
818 if (!is_array($variable)) {
819 $variable = array($variable);
821 $varname = $variable[0];
822 if (isset($this->context[$varname])) {
823 if (count($variable) == 1) {
824 return $this->context[$varname];
826 $var = & $this->context[$varname];
827 foreach ($variable as $id => $part) {
828 if ($id != 0) {
829 if (is_array($part) && isset($part['object'])) {
830 $var = &$var->$part['object'];
831 } else if (is_object($var)) {
832 $var = &$var->$part;
833 } else {
834 $var = &$var[$part];
838 $variable = $var;
839 unset($var);
840 return $variable;
843 return NULL;
846 function var_is_object(Array $variable, $default=NULL)
848 $varname = $variable[0];
849 switch ($varname) {
850 case 'GLOBALS':
851 case '_SERVER':
852 case '_GET':
853 case '_POST':
854 case '_FILES':
855 case '_COOKIE':
856 case '_SESSION':
857 case '_REQUEST':
858 case '_ENV':
859 case 'forloop':
860 case 'block':
861 return FALSE; /* these are arrays */
864 if (isset($this->context[$varname])) {
865 return is_object($this->get_context($variable));
868 return $default===NULL ? self::$dot_as_object : $default;
870 // }}}
872 // Get variable name {{{
873 protected function generate_variable_name($variable)
875 if (is_array($variable)) {
876 switch ($variable[0]) {
877 case 'forloop':
878 if (!$this->forid) {
879 $this->Error("Invalid forloop reference outside of a loop");
881 switch ($variable[1]) {
882 case 'counter':
883 $this->forloop[$this->forid]['counter'] = TRUE;
884 $variable = 'forcounter1_'.$this->forid;
885 break;
886 case 'counter0':
887 $this->forloop[$this->forid]['counter0'] = TRUE;
888 $variable = 'forcounter0_'.$this->forid;
889 break;
890 case 'last':
891 $this->forloop[$this->forid]['counter'] = TRUE;
892 $this->forloop[$this->forid]['last'] = TRUE;
893 $variable = 'islast_'.$this->forid;
894 break;
895 case 'first':
896 $this->forloop[$this->forid]['first'] = TRUE;
897 $variable = 'isfirst_'.$this->forid;
898 break;
899 case 'revcounter':
900 $this->forloop[$this->forid]['revcounter'] = TRUE;
901 $variable = 'revcount_'.$this->forid;
902 break;
903 case 'revcounter0':
904 $this->forloop[$this->forid]['revcounter0'] = TRUE;
905 $variable = 'revcount0_'.$this->forid;
906 break;
907 case 'parentloop':
908 unset($variable[1]);
909 $this->forid--;
910 $variable = $this->generate_variable_name(array_values($variable));
911 $variable = $variable['var'];
912 $this->forid++;
913 break;
914 default:
915 $this->Error("Unexpected forloop.{$variable[1]}");
917 /* no need to escape it */
918 $this->var_is_safe = TRUE;
919 break;
920 case 'block':
921 if ($this->in_block == 0) {
922 $this->Error("Can't use block.super outside a block");
924 if (!$this->subtemplate) {
925 $this->Error("Only subtemplates can call block.super");
927 /* no need to escape it */
928 $this->var_is_safe = TRUE;
929 return Haanga_AST::str(self::$block_var);
930 break;
933 } else if (isset($this->var_alias[$variable])) {
934 $variable = $this->var_alias[$variable];
937 return hvar($variable)->getArray();
939 // }}}
941 // Print {{{
942 public function do_print(Haanga_AST $code, $stmt)
944 /* Flag this object as a printing one */
945 $code->doesPrint = TRUE;
947 if (self::$strip_whitespace && Haanga_AST::is_str($stmt)) {
948 $stmt['string'] = preg_replace('/\s+/', ' ', $stmt['string']);
951 if ($this->ob_start == 0) {
952 $code->do_echo($stmt);
953 return;
956 $buffer = hvar('buffer'.$this->ob_start);
957 $code->append($buffer, $stmt);
961 // }}}
963 // for [<key>,]<val> in <array> {{{
964 protected function generate_op_loop($details, &$body)
966 if (isset($details['empty'])) {
967 $body->do_if(hexpr(hexec('count', hvar($details['array'])), '==', 0));
968 $this->generate_op_code($details['empty'], $body);
969 $body->do_else();
972 /* ForID */
973 $oldid = $this->forid;
974 $this->forid = $oldid+1;
975 $this->forloop[$this->forid] = array();
977 /* Check if the array to iterate is an object */
978 $var = &$details['array'][0];
979 if (is_string($var) && $this->var_is_object(array($var), FALSE)) {
980 /* It is an object, call to get_object_vars */
981 $body->decl($var.'_arr', hexec('get_object_vars', hvar($var)));
982 $var .= '_arr';
984 unset($var);
986 /* variables */
987 $array = $this->get_filtered_var($details['array'], $varname);
989 /* Loop body */
990 if ($this->is_safe(hvar($varname))) {
991 $this->set_safe(hvar($details['variable']));
994 /* check if the elements in the array is an array or object */
996 $for_body = hcode();
997 $this->generate_op_code($details['body'], $for_body);
999 if ($this->is_safe(hvar($varname))) {
1000 $this->set_unsafe($details['variable']);
1003 $oid = $this->forid;
1004 $size = hvar('psize_'.$oid);
1006 // counter {{{
1007 if (isset($this->forloop[$oid]['counter'])) {
1008 $var = hvar('forcounter1_'.$oid);
1009 $body->decl($var, 1);
1010 $for_body->decl($var, hexpr($var, '+', 1));
1012 // }}}
1014 // counter0 {{{
1015 if (isset($this->forloop[$oid]['counter0'])) {
1016 $var = hvar('forcounter0_'.$oid);
1017 $body->decl($var, 0);
1018 $for_body->decl($var, hexpr($var, '+', 1));
1020 // }}}
1022 // last {{{
1023 if (isset($this->forloop[$oid]['last'])) {
1024 if (!isset($cnt)) {
1025 $body->decl('psize_'.$oid, hexec('count', hvar_ex($details['array'])));
1026 $cnt = TRUE;
1028 $var = 'islast_'.$oid;
1029 $body->decl($var, hexpr(hvar('forcounter1_'.$oid), '==', $size));
1030 $for_body->decl($var, hexpr(hvar('forcounter1_'.$oid), '==', $size));
1032 // }}}
1034 // first {{{
1035 if (isset($this->forloop[$oid]['first'])) {
1036 $var = hvar('isfirst_'.$oid);
1037 $body->decl($var, TRUE);
1038 $for_body->decl($var, FALSE);
1040 // }}}
1042 // revcounter {{{
1043 if (isset($this->forloop[$oid]['revcounter'])) {
1044 if (!isset($cnt)) {
1045 $body->decl('psize_'.$oid, hexec('count', hvar_ex($details['array'])));
1046 $cnt = TRUE;
1048 $var = hvar('revcount_'.$oid);
1049 $body->decl($var, $size);
1050 $for_body->decl($var, hexpr($var, '-', 1));
1052 // }}}
1054 // revcounter0 {{{
1055 if (isset($this->forloop[$oid]['revcounter0'])) {
1056 if (!isset($cnt)) {
1057 $body->decl('psize_'.$oid, hexec('count', hvar_ex($details['array'])));
1058 $cnt = TRUE;
1060 $var = hvar('revcount0_'.$oid);
1061 $body->decl($var, hexpr($size, "-", 1));
1062 $for_body->decl($var, hexpr($var, '-', 1));
1064 // }}}
1066 /* Restore old ForID */
1067 $this->forid = $oldid;
1069 /* Merge loop body */
1070 $body->do_foreach($array, $details['variable'], $details['index'], $for_body);
1072 if (isset($details['empty'])) {
1073 $body->do_endif();
1076 // }}}
1078 // ifchanged [<var1> <var2] {{{
1079 protected function generate_op_ifchanged($details, &$body)
1081 static $ifchanged = 0;
1083 $ifchanged++;
1084 $var1 = 'ifchanged'.$ifchanged;
1085 if (!isset($details['check'])) {
1086 /* ugly */
1087 $this->ob_start($body);
1088 $var2 = hvar('buffer'.$this->ob_start);
1091 $this->generate_op_code($details['body'], $body);
1092 $this->ob_start--;
1093 $body->do_if(hexpr(hexec('isset', hvar($var1)), '==', FALSE, '||', hvar($var1), '!=', $var2));
1094 $this->do_print($body, $var2);
1095 $body->decl($var1, $var2);
1096 } else {
1097 /* beauty :-) */
1098 foreach ($details['check'] as $id=>$type) {
1099 if (!Haanga_AST::is_var($type)) {
1100 $this->Error("Unexpected string {$type['string']}, expected a varabile");
1103 $this_expr = hexpr(hexpr(
1104 hexec('isset', hvar($var1, $id)), '==', FALSE,
1105 '||', hvar($var1, $id), '!=', $type
1108 if (isset($expr)) {
1109 $this_expr = hexpr($expr, '&&', $this_expr);
1112 $expr = $this_expr;
1115 $body->do_if($expr);
1116 $this->generate_op_code($details['body'], $body);
1117 $body->decl($var1, $details['check']);
1120 if (isset($details['else'])) {
1121 $body->do_else();
1122 $this->generate_op_code($details['else'], $body);
1124 $body->do_endif();
1126 // }}}
1128 // autoescape ON|OFF {{{
1129 function generate_op_autoescape($details, &$body)
1131 $old_autoescape = self::$autoescape;
1132 self::$autoescape = strtolower($details['value']) == 'on';
1133 $this->generate_op_code($details['body'], $body);
1134 self::$autoescape = $old_autoescape;
1136 // }}}
1138 // {% spacefull %} Set to OFF strip_whitespace for a block (the compiler option) {{{
1139 function generate_op_spacefull($details, &$body)
1141 $old_strip_whitespace = self::$strip_whitespace;
1142 self::$strip_whitespace = FALSE;
1143 $this->generate_op_code($details['body'], $body);
1144 self::$strip_whitespace = $old_strip_whitespace;
1146 // }}}
1148 // ob_Start(array &$body) {{{
1150 * Start a new buffering
1153 function ob_start(&$body)
1155 $this->ob_start++;
1156 $body->decl('buffer'.$this->ob_start, "");
1158 // }}}
1160 // Custom Tags {{{
1161 function get_custom_tag($name)
1163 $function = $this->get_function_name($this->name).'_tag_'.$name;
1164 $this->append .= "\n\n".Haanga_Extension::getInstance('Tag')->getFunctionBody($name, $function);
1165 return $function;
1169 * Generate needed code for custom tags (tags that aren't
1170 * handled by the compiler).
1173 function generate_op_custom_tag($details, &$body)
1175 static $tags;
1176 if (!$tags) {
1177 $tags = Haanga_Extension::getInstance('Tag');
1180 $tag_name = $details['name'];
1181 $tagFunction = $tags->getFunctionAlias($tag_name);
1183 if (!$tagFunction && !$tags->hasGenerator($tag_name)) {
1184 $function = $this->get_custom_tag($tag_name, isset($details['as']));
1185 } else {
1186 $function = $tagFunction;
1189 if (isset($details['body'])) {
1191 if the custom tag has 'body'
1192 then it behave the same way as a filter
1194 $this->ob_start($body);
1195 $this->generate_op_code($details['body'], $body);
1196 $target = hvar('buffer'.$this->ob_start);
1197 if ($tags->hasGenerator($tag_name)) {
1198 $args = array_merge(array($target), $details['list']);
1199 $exec = $tags->generator($tag_name, $this, $args);
1200 if (!$exec InstanceOf Haanga_AST) {
1201 $this->Error("Invalid output of custom filter {$tag_name}");
1203 if ($exec->stack_size() >= 2 || $exec->doesPrint) {
1205 The generator returned more than one statement,
1206 so we assume the output is already handled
1207 by one of those stmts.
1209 $body->append_ast($exec);
1210 $this->ob_start--;
1211 return;
1213 } else {
1214 $exec = hexec($function, $target);
1216 $this->ob_start--;
1217 $this->do_print($body, $exec);
1218 return;
1221 $var = isset($details['as']) ? $details['as'] : NULL;
1222 $args = array_merge(array($function), $details['list']);
1224 if ($tags->hasGenerator($tag_name)) {
1225 $exec = $tags->generator($tag_name, $this, $details['list'], $var);
1226 if ($exec InstanceOf Haanga_AST) {
1227 if ($exec->stack_size() >= 2 || $exec->doesPrint || $var !== NULL) {
1229 The generator returned more than one statement,
1230 so we assume the output is already handled
1231 by one of those stmts.
1233 $body->append_ast($exec);
1234 return;
1236 } else {
1237 $this->Error("Invalid output of the custom tag {$tag_name}");
1239 } else {
1240 $fnc = array_shift($args);
1241 $exec = hexec($fnc);
1242 foreach ($args as $arg) {
1243 $exec->param($arg);
1247 if ($var) {
1248 $body->decl($var, $exec);
1249 } else {
1250 $this->do_print($body, $exec);
1253 // }}}
1255 // with <variable> as <var> {{{
1260 function generate_op_alias($details, &$body)
1262 $this->var_alias[ $details['as'] ] = $details['var'];
1263 $this->generate_op_code($details['body'], $body);
1264 unset($this->var_alias[ $details['as'] ] );
1266 // }}}
1268 // Custom Filters {{{
1269 function get_custom_filter($name)
1271 $function = $this->get_function_name($this->name).'_filter_'.$name;
1272 $this->append .= "\n\n".Haanga_Extension::getInstance('Filter')->getFunctionBody($name, $function);
1273 return $function;
1277 function do_filtering($name, $args)
1279 static $filter;
1280 if (!$filter) {
1281 $filter = Haanga_Extension::getInstance('Filter');
1284 if (is_array($name)) {
1286 prepare array for ($func_name, $arg1, $arg2 ... )
1287 where $arg1 = last expression and $arg2.. $argX is
1288 defined in the template
1290 $args = array_merge($args, $name['args']);
1291 $name = $name[0];
1294 if (!$filter->isValid($name)) {
1295 $this->Error("{$name} is an invalid filter");
1298 if ($filter->hasGenerator($name)) {
1299 return $filter->generator($name, $this, $args);
1301 $fnc = $filter->getFunctionAlias($name);
1302 if (!$fnc) {
1303 $fnc = $this->get_custom_filter($name);
1306 $args = array_merge(array($fnc), $args);
1307 $exec = call_user_func_array('hexec', $args);
1309 return $exec;
1312 function generate_op_filter($details, &$body)
1314 $this->ob_start($body);
1315 $this->generate_op_code($details['body'], $body);
1316 $target = hvar('buffer'.$this->ob_start);
1317 foreach ($details['functions'] as $f) {
1318 $param = (isset($exec) ? $exec : $target);
1319 $exec = $this->do_filtering($f, array($param));
1321 $this->ob_start--;
1322 $this->do_print($body, $exec);
1324 // }}}
1326 /* variable safety {{{ */
1327 function set_safe($name)
1329 if (!Haanga_AST::is_Var($name)) {
1330 $name = hvar($name)->getArray();
1332 $this->safes[serialize($name)] = TRUE;
1335 function set_unsafe($name)
1337 if (!Haanga_AST::is_Var($name)) {
1338 $name = hvar($name)->getArray();
1340 unset($this->safes[serialize($name)]);
1343 function is_safe($name)
1345 if ($this->var_is_safe) {
1346 return TRUE;
1348 if (isset($this->safes[serialize($name)])) {
1349 return TRUE;
1351 return FALSE;
1353 /* }}} */
1355 final static function main_cli()
1357 $argv = $GLOBALS['argv'];
1358 $haanga = new Haanga_Compiler;
1359 $code = $haanga->compile_file($argv[1], TRUE);
1360 if (!isset($argv[2]) || $argv[2] != '--notags') {
1361 $code = "<?php\n\n$code";
1363 echo $code;
1369 * Local variables:
1370 * tab-width: 4
1371 * c-basic-offset: 4
1372 * End:
1373 * vim600: sw=4 ts=4 fdm=marker
1374 * vim<600: sw=4 ts=4