- Fixed little issue with autoescape
[haanga.git] / haanga / haanga.php
blob0dafd5a74a20918249d95cc9d55b45b9327e2acb
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 define('HAANGA_DIR', dirname(__FILE__));
40 // Load needed files {{{
41 require HAANGA_DIR."/lexer.php";
42 require HAANGA_DIR."/generator.php";
43 require HAANGA_DIR."/extensions.php";
44 require HAANGA_DIR."/tags.php";
45 require HAANGA_DIR."/filters.php";
46 // }}}
48 // Exception Class {{{
49 /**
50 * Exception class
53 class CompilerException extends Exception
56 // }}}
59 class Haanga_Main
62 // properties {{{
63 protected static $block_var=NULL;
64 protected $generator;
65 protected $forloop = array();
66 protected $forid = 0;
67 protected $sub_template = FALSE;
68 protected $name;
69 protected $blocks=array();
70 /**
71 * number of blocks :-)
73 protected $in_block=0;
74 /**
75 * output buffers :-)
77 protected $ob_start=0;
78 protected $append;
79 protected $prepend_op;
80 /**
81 * Table which contains all variables
82 * aliases defined in the template
84 protected $var_alias;
85 /**
86 * Flag the current variable as safe. This means
87 * that escape won't be called if autoescape is
88 * activated (which is activated by default)
90 public $var_is_safe=FALSE;
91 protected $autoescape=TRUE;
92 protected $strip_whitespaces=FALSE;
93 protected $force_whitespaces=0;
94 /**
95 * Debug file
97 protected $debug;
98 // }}}
100 function __construct()
102 $this->generator = new Haanga_CodeGenerator;
103 if (self::$block_var===NULL) {
104 self::$block_var = '{{block.'.md5('super').'}}';
108 // setDebug($file) {{{
109 function setDebug($file)
111 $this->debug = $file;
113 // }}}
115 // reset() {{{
116 function reset()
118 $avoid_cleaning = array('strip_whitespaces' => 1, 'block_var' => 1, 'autoescape'=>1);
119 foreach (array_keys(get_object_vars($this)) as $key) {
120 if (isset($avoid_cleaning[$key])) {
121 continue;
123 $this->$key = NULL;
125 $this->generator = new Haanga_CodeGenerator;
126 $this->blocks = array();
127 $this->cycle = array();
129 // }}}
131 // get_template_name() {{{
132 final function get_template_name()
134 return $this->name;
136 // }}}
138 // Set template name {{{
139 function set_template_name($path)
141 return ($this->name = strstr(basename($path),'.', TRUE));
143 // }}}
145 // get_function_name(string $name) {{{
146 function get_function_name($name)
148 return "{$name}_template";
150 // }}}
152 // Compile ($code, $name=NULL) {{{
153 final function compile($code, $name=NULL)
155 $this->name = $name;
157 $parsed = do_parsing($code);
158 $code = "";
159 $this->subtemplate = FALSE;
161 if ($parsed[0]['operation'] == 'base') {
162 /* {% base ... %} found */
163 $base = $parsed[0][0];
164 $code .= $this->get_base_template($base);
165 unset($parsed[0]);
168 if ($name) {
169 if (isset($this->_file)) {
170 $op_code[] = $this->op_comment("Generated from {$this->_base_dir}/{$this->_file}");
172 $op_code[] = $this->op_declare_function($this->get_function_name($name));
173 $op_code[] = $this->op_expr($this->expr_exec('extract', $this->expr_var('vars')));
176 $this->ob_start($op_code);
177 $this->generate_op_code($parsed, $op_code);
178 if ($this->subtemplate) {
179 $expr = $this->expr_call_base_template();
180 $this->generate_op_print(array('expr' => $expr), $op_code);
182 $this->ob_start--;
184 /* Add last part */
185 $expr = $this->expr('==', $this->expr_var('return'), TRUE);
186 $op_code[] = $this->op_if($expr);
187 $op_code[] = $this->op_return($this->expr_var('buffer1'));
188 $op_code[] = $this->op_else();
189 $this->generate_op_print(array('variable' => 'buffer1'), $op_code);
190 $op_code[] = $this->op_end('if');
192 if ($name) {
193 $op_code[] = $this->op_end('function');
196 if (count($this->prepend_op)) {
197 $op_code = array_merge($this->prepend_op, $op_code);
200 $code .= $this->generator->getCode($op_code);
201 if (!empty($this->append)) {
202 $code .= $this->append;
204 if (!empty($this->debug)) {
205 file_put_contents($this->debug, print_r($op_code, TRUE));
207 return $code;
209 // }}}
211 // compile_file($file) {{{
213 * Compile a file
215 * @param string $file File path
217 * @return Generated PHP code
219 final function compile_file($file)
221 if (!is_readable($file)) {
222 throw new CompilerException("$file is not a file");
224 $this->_base_dir = dirname($file);
225 $this->_file = basename($file);
226 $name = $this->set_template_name($file);
227 return $this->compile(file_get_contents($file), $name);
229 // }}}
231 // is_expr methods {{{
232 function is_expr(Array $cmd, $type=NULL)
234 if (isset($cmd['op_expr'])) {
235 if (!$type || $type == $cmd['op_expr']) {
236 return TRUE;
239 return FALSE;
242 function is_exec(Array $cmd)
244 return $this->is_expr($cmd, 'exec');
246 // }}}
248 // op_* helper methods {{{
250 * Return an stand alone expression
253 function op_expr($expr)
255 return array('op' => 'expr', $expr);
258 function op_comment($comment)
260 return array('op' => 'comment', 'comment' => $comment);
263 function op_foreach($array, $value, $key=NULL)
265 foreach (array('array', 'value', 'key') as $var) {
266 $var = &$$var;
267 if (is_array($var) && isset($var['var'])) {
268 $var = $var['var'];
270 unset($var);
272 $def = array('op' => 'foreach', 'array' => $array, 'value' => $value);
273 if ($key) {
274 $def['key'] = $key;
276 return $def;
279 function op_if($expr)
281 return array('op' => 'if', 'expr' => $expr);
284 function op_else()
286 return array('op' => 'else');
289 function op_return($expr)
291 return array('op' => 'return', $expr);
294 function op_end($op)
296 return array('op' => "end_{$op}");
299 function op_declare($name, $value)
301 if (is_array($name)) {
302 if (isset($name['var'])) {
303 $name = $name['var'];
307 if (is_array($value)) {
308 if (isset($value['op_expr'])) {
309 $value = array('expr' => $value);
313 return array('op' => 'declare', 'name' => $name, $value);
316 function op_append($name, $expr)
318 return array('op' => 'append_var', 'name' => $name, $expr);
322 function op_inc($name)
324 return array('op' => 'inc', 'name' => $name);
327 function op_declare_function($name)
329 return array('op' => 'function', 'name' => $name);
332 //}}}
334 // expr_* helper methods {{{
335 function expr_cond($expr, $true, $false)
337 return array('expr_cond' => $expr, 'true' => $true, 'false' => $false);
340 function expr_const($name)
342 return array('constant' => $name);
346 * Generate code to call base template
349 function expr_call_base_template()
351 return $this->expr_exec(
352 $this->get_function_name($this->subtemplate),
353 $this->expr_var('vars'),
354 $this->expr_TRUE(),
355 $this->expr_var('blocks')
360 * return a function call for isset($var) === $isset
362 * @return array
364 final function expr_isset($var, $isset=TRUE)
366 return $this->expr('==', $this->expr_exec('isset', $this->expr_var($var)), $isset);
369 final function expr_isset_ex($var, $isset=TRUE)
371 return $this->expr('==', $this->expr_exec('isset', $var), $isset);
374 final function expr_array()
376 $def = array();
377 foreach (func_get_args() as $arg) {
378 if (count($arg) == 2) {
379 if (!is_array($arg[0])) {
380 $arg[0] = $this->expr_str($arg[0]);
382 $arg = array('key' => $arg);
384 $def[] = $arg;
386 return array("array" => $def);
389 final function expr_array_first($values)
391 $def = array();
392 foreach ($values as $arg) {
393 if (count($arg) == 2) {
394 if (!is_array($arg[0])) {
395 $arg[0] = $this->expr_str($arg[0]);
397 $arg = array('key' => $arg);
399 $def[] = $arg;
401 return array("array" => $def);
406 * return an number definition of $num
408 * @return array
410 final function expr_number($num=0)
412 return array('number' =>$num);
416 * return an string definition of $str
418 * @return array
420 final function expr_str($str='')
422 return array('string' => $str);
426 * Generate expression that for
427 * boolean TRUE
429 * @return array
431 final function expr_TRUE()
433 return array('expr' => TRUE);
437 * Generate expression that for
438 * boolean FALSE
440 * @return array
442 final function expr_FALSE()
444 return array('expr' => FALSE);
448 * Return expr for variable reference
450 * @return array
452 final function expr_var($var)
454 return array('var' => func_get_args());
458 * Generate expression for
459 * a function calling inside an expression
461 * @return array
463 final function expr_exec($function)
465 $args = func_get_args();
466 unset($args[0]);
467 $args = array_values($args);
469 return array(
470 'exec' => $function,
471 'args' => $args
478 * Generate a generic expression
480 * @return array
482 final function expr($operation, $expr1, $expr2=NULL)
484 $expr = array('op_expr' => $operation, $expr1);
485 if ($expr2 !== NULL) {
486 $expr[] = $expr2;
489 return $expr;
491 // }}}
493 // get_base_template($base) {{{
495 * Handle {% base "" %} definition. By default only
496 * static (string) are supported, but this can be overrided
497 * on subclasses.
499 * This method load the base class, compile it and return
500 * the generated code.
502 * @param array $base Base structure
504 * @return string Generated source code
506 function get_base_template($base)
508 if (!isset($base['string'])) {
509 throw new CompilerException("Dynamic inheritance is not supported for compilated templates");
511 $file = $base['string'];
512 list($this->subtemplate, $new_code) = $this->compile_required_template($file);
513 return $new_code."\n\n";
515 // }}}
517 // {% base "foo.html" %} {{{
518 protected function generate_op_base()
520 throw new exception("{% base %} can be only as first statement");
522 // }}}
524 // Main Loop {{{
525 protected function generate_op_code($parsed, &$out)
527 if (!is_array($parsed)) {
528 throw new CompilerException("Invalid \$parsed array");
530 foreach ($parsed as $op) {
531 if (!isset($op['operation'])) {
532 throw new CompilerException("Malformed $parsed array");
534 if ($this->subtemplate && $this->in_block == 0 && $op['operation'] != 'block') {
535 /* ignore most of tokens in subtemplates */
536 continue;
538 $method = "generate_op_".$op['operation'];
539 if (!is_callable(array($this, $method))) {
540 throw new CompilerException("Compiler: Missing method $method");
542 $this->$method($op, $out);
545 // }}}
547 // Check the current expr {{{
548 protected function check_expr(&$expr)
550 if (is_array($expr) && isset($expr['op_expr'])) {
551 if ($expr['op_expr'] == 'in') {
552 if (isset($expr[1]['string'])) {
553 $expr = $this->expr("!==",
554 $this->expr_exec("strpos", $expr[1], $expr[0]),
555 FALSE
557 } else {
558 $expr = $this->expr("!==", $this->expr_cond(
559 $this->expr("==", $this->expr_exec("is_array", $expr[1]), TRUE),
560 $this->expr_exec("array_search", $expr[0], $expr[1]),
561 $this->expr_exec("strpos", $expr[1], $expr[0])
562 ), FALSE);
566 $this->check_expr($expr[0]);
567 $this->check_expr($expr[1]);
568 } else {
569 if (is_array($expr)) {
570 if (isset($expr['var'])) {
571 $expr = $this->generate_variable_name($expr['var']);
572 } else if (isset($expr['var_filter'])) {
573 foreach ($expr['var_filter'] as $id => $f) {
574 if ($id == 0) {
575 $exec = $this->generate_variable_name($f);
576 } else {
577 $exec = $this->expr_exec($f, $exec);
580 $expr = $exec;
581 } else if (isset($expr['args'])) {
582 /* check every arguments */
583 foreach ($expr['args'] as &$v) {
584 $this->check_expr($v);
586 unset($v);
587 } else if (isset($expr['expr_cond'])) {
588 /* Check expr conditions */
589 $this->check_expr($expr['expr_cond']);
590 $this->check_expr($expr['true']);
591 $this->check_expr($expr['false']);
596 // }}}
598 // {% if <expr> %} HTML {% else %} TWO {% endif $} {{{
599 protected function generate_op_if($details, &$out)
601 $this->check_expr($details['expr']);
602 $out[] = $this->op_if($details['expr']);
603 $this->generate_op_code($details['body'], $out);
604 if (isset($details['else'])) {
605 $out[] = $this->op_else();
606 $this->generate_op_code($details['else'], $out);
608 $out[] = $this->op_end('if');
610 // }}}
612 // Overload template {{{
613 protected function compile_required_template($file)
615 if (!is_file($file)) {
616 if (isset($this->_base_dir)) {
617 $file = $this->_base_dir.'/'.$file;
620 if (!is_file($file)) {
621 throw new CompilerException("can't find {$file} file template");
623 $class = get_class($this);
624 $comp = new $class;
625 $comp->reset();
626 $code = $comp->compile_file($file);
627 return array($comp->get_template_name(), $code);
629 // }}}
631 // include "file.html" | include <var1> {{{
632 protected function generate_op_include($details, &$out)
634 if (!$details[0]['string']) {
635 throw new CompilerException("Dynamic inheritance is not supported for compilated templates");
637 list($name,$code) = $this->compile_required_template($details[0]['string']);
638 $this->append .= "\n\n{$code}";
639 $expr = $this->expr_exec(
640 $this->get_function_name($this->subtemplate),
641 $this->expr_var('vars'),
642 $this->expr_var('blocks'),
643 $this->expr_TRUE()
645 $this->generate_op_print($expr, $op_code);
646 $this->generate_op_print($expr, $out);
648 // }}}
650 // Handle HTML code {{{
651 protected function generate_op_html($details, &$out)
653 $this->generate_op_print($details, $out);
655 // }}}
657 // get_var_filtered {{{
659 * This method handles all the filtered variable (piped_list(X)'s
660 * output in the parser.
663 * @param array $variable (Output of piped_list(B) (parser))
664 * @param array &$varname Variable name
666 * @return expr
669 function get_var_filtering($variable, &$varname)
671 $this->var_is_safe = FALSE;
672 if (count($variable) > 1) {
673 $count = count($variable);
674 $target = $this->generate_variable_name($variable[0]);
676 if (!isset($target['var'])) {
677 /* block.super can't have any filter */
678 throw new CompilerException("This variable can't have any filter");
681 for ($i=1; $i < $count; $i++) {
682 $func_name = $variable[$i];
683 if ($func_name == 'escape') {
684 /* to avoid double cleaning */
685 $this->var_is_safe = TRUE;
687 $args = array(isset($exec) ? $exec : $target);
688 $exec = $this->do_filtering($func_name, $args);
690 unset($variable);
691 $varname = $args[0];
692 $details = $exec;
693 } else {
694 $details = $this->generate_variable_name($variable[0]);
695 $varname = $variable[0];
699 return $details;
701 // }}}
703 // generate_op_print_var {{{
705 * Generate code to print a variable with its filters, if there is any.
707 * All variable (except those flagged as |safe) are automatically
708 * escaped if autoescape is "on".
711 protected function generate_op_print_var($details, &$out)
714 $details = $this->get_var_filtering($details['variable'], $variable);
716 if (!isset($details['var']) && !isset($details['exec'])) {
717 /* generate_variable_name didn't replied a variable, weird case
718 currently just used for {{block.super}}.
720 $this->generate_op_print($details, $out);
721 return;
725 if (!$this->var_is_safe && $this->autoescape) {
726 $args = array($details);
727 $details = $this->do_filtering('escape', $args);
730 $this->generate_op_print($details, $out);
732 // }}}
734 // is_last_op_print($out) {{{
736 * Return TRUE if the last stacked operation
737 * is a print (declare or append_var).
739 * @param array $out Stack of operations
741 * @return bool
743 protected function is_last_op_print($out)
745 $last = count($out)-1;
746 $sprint = array('print', 'declare', 'append_var');
747 return $last >= 0 && array_search($out[$last]['op'], $sprint) !== FALSE;
749 // }}}
751 // first_of var1 var2 'name' {{{
752 protected function generate_op_first_of($details, &$out)
754 $texpr = array();
755 foreach ($details['vars'] as $var) {
756 if (isset($var['string'])) {
757 $texpr[] = $var;
758 break;
760 $texpr[] = $this->expr_cond(
761 $this->expr_isset($var['var']),
762 $var,
766 $texpr = array_reverse($texpr);
767 for ($i=1; $i < count($texpr); $i++) {
768 $texpr[$i]['false'] = $texpr[$i-1];
770 $expr = $texpr[$i-1];
772 $this->generate_op_print($expr, $out);
774 // }}}
776 // {# something #} {{{
777 protected function generate_op_comment($details, &$out)
779 if ($this->is_last_op_print($out)) {
780 /* If there is a print declared previously, we pop it
781 and add it after the cycle declaration
783 $old_print = array_pop($out);
785 $out[] = $this->op_comment($details['comment']);
786 if (isset($old_print)) {
787 $out[] = $old_print;
790 // }}}
792 // {% block 'name' %} ... {% endblock %} {{{
793 protected function generate_op_block($details, &$out)
795 $this->in_block++;
796 $this->blocks[] = $details['name'];
797 $block_name = $this->expr_var('blocks', $details['name']);
799 $this->ob_start($out);
800 $buffer_var = 'buffer'.$this->ob_start;
802 $this->generate_op_code($details['body'], $body);
804 $out = array_merge($out, $body);
805 $this->ob_start--;
807 $buffer = $this->expr_var($buffer_var);
809 /* {{{ */
811 * isset previous block (parent block)?
812 * TRUE
813 * has reference to self::$block_var ?
814 * TRUE
815 * replace self::$block_var for current block value (buffer)
816 * FALSE
817 * print parent block
818 * FALSE
819 * print current block
822 $declare = $this->expr_cond(
823 $this->expr_isset_ex($block_name),
824 $this->expr_cond(
825 $this->expr("===", $this->expr_exec('strpos', $block_name,
826 $this->expr_str(self::$block_var)
827 ), $this->expr_FALSE()
829 $block_name,
830 $this->expr_exec('str_replace',
831 $this->expr_str(self::$block_var),
832 $buffer,
833 $block_name
836 $buffer
838 /* }}} */
840 if (!$this->subtemplate) {
841 $this->generate_op_print($declare, $out);
842 } else {
843 $out[] = $this->op_declare($block_name, $declare);
844 if ($this->in_block > 1) {
845 $this->generate_op_print($block_name, $out);
848 array_pop($this->blocks);
849 $this->in_block--;
852 // }}}
854 // regroup <var1> by <field> as <foo> {{{
855 protected function generate_op_regroup($details, &$out)
857 $out[] = $this->op_declare($details['as'], $this->expr_array_first(array()));
858 $array = $this->get_var_filtering($details['array'], $varname);
859 if (!isset($details['var']) && !isset($varname['var'])) {
860 /* generate_variable_name didn't replied a variable, weird case
861 currently just used for {{block.super}}.
863 throw new CompilerException("Invalid variable name {$details['array']}");
865 if (isset($array['exec'])) {
866 $out[] = $this->op_declare($varname, $array);
868 $var = $this->expr_var('item', $details['row']);
870 $out[] = $this->op_comment("Temporary sorting");
871 $out[] = $this->op_foreach($varname, 'item');
874 $out[] = $this->op_declare(array('temp_group', $var, NULL), $this->expr_var('item'));
875 $out[] = $this->op_end('foreach');
877 $out[] = $this->op_comment("Proper format");
879 $out[] = $this->op_foreach('temp_group', 'item', 'group');
881 $array = $this->expr_array(
882 array("grouper", $this->expr_var('group')),
883 array("list", $this->expr_var('item'))
886 $out[] = $this->op_declare(array($details['as'], NULL), $array );
888 $out[] = $this->op_end('foreach');
889 $out[] = $this->op_comment("Sorting done");
891 // }}}
893 // Get variable name {{{
894 protected function generate_variable_name($variable)
896 if (is_array($variable)) {
897 switch ($variable[0]) {
898 case 'forloop':
899 if (!$this->forid) {
900 throw new CompilerException("Invalid forloop reference outside of a loop");
902 switch ($variable[1]) {
903 case 'counter':
904 $this->forloop[$this->forid]['counter'] = TRUE;
905 $variable = 'forcounter1_'.$this->forid;
906 break;
907 case 'counter0':
908 $this->forloop[$this->forid]['counter0'] = TRUE;
909 $variable = 'forcounter0_'.$this->forid;
910 break;
911 case 'last':
912 $this->forloop[$this->forid]['counter'] = TRUE;
913 $this->forloop[$this->forid]['last'] = TRUE;
914 $variable = 'islast_'.$this->forid;
915 break;
916 case 'first':
917 $this->forloop[$this->forid]['first'] = TRUE;
918 $variable = 'isfirst_'.$this->forid;
919 break;
920 case 'revcounter':
921 $this->forloop[$this->forid]['revcounter'] = TRUE;
922 $variable = 'revcount_'.$this->forid;
923 break;
924 case 'revcounter0':
925 $this->forloop[$this->forid]['revcounter0'] = TRUE;
926 $variable = 'revcount0_'.$this->forid;
927 break;
928 case 'parentloop':
929 unset($variable[1]);
930 $this->forid--;
931 $variable = $this->generate_variable_name(array_values($variable));
932 $variable = $variable['var'];
933 $this->forid++;
934 break;
935 default:
936 throw new CompilerException("Unexpected forloop.{$variable[1]}");
938 /* no need to escape it */
939 $this->var_is_safe = TRUE;
940 break;
941 case 'block':
942 if ($this->in_block == 0) {
943 throw new CompilerException("Can't use block.super outside a block");
945 if (!$this->subtemplate) {
946 throw new CompilerException("Only subtemplates can call block.super");
948 /* no need to escape it */
949 $this->var_is_safe = TRUE;
950 return $this->expr_str(self::$block_var);
951 break;
954 } else if (isset($this->var_alias[$variable])) {
955 $variable = $this->var_alias[$variable];
958 return $this->expr_var($variable);
960 // }}}
962 // Print {{{
963 public function generate_op_print($details, &$out)
965 $last = count($out)-1;
966 if (isset($details['variable'])) {
967 $content = $this->generate_variable_name($details['variable']);
968 } else if (isset($details['html'])) {
969 $html = $details['html'];
970 if ($this->strip_whitespaces && $this->force_whitespaces == 0) {
971 $html = str_replace("\n", " ", $html);
972 $html = preg_replace("/\s\s+/", " ", $html);
974 $content = $this->expr_str($html);
975 } else if (isset($details['function'])) {
976 $content = $this->expr_exec($details['function'][0], $details['function'][1]);
977 } else {
978 $content = $details;
981 $var_name = 'buffer'.$this->ob_start;
983 if ($this->ob_start == 0) {
984 $operation = 'print';
985 } else {
986 $operation = 'append_var';
989 if ($last >= 0 && $out[$last]['op'] == $operation && ($operation != 'append_var' || $out[$last]['name'] === $var_name)) {
990 /* try to append this to the previous print if it exists */
991 $out[$last][] = $content;
992 } else {
993 if ($this->ob_start == 0) {
994 $out[] = array('op' => 'print', $content);
995 } else {
996 if (isset($out[$last]) && $out[$last]['op'] == 'declare' && $out[$last]['name'] == $var_name) {
997 /* override an empty declaration of a empty buffer
998 if the next operation is an 'append'
1000 $out[$last][] = $content;
1001 } else {
1002 $out[] = $this->op_append('buffer'.$this->ob_start, $content);
1007 // }}}
1009 // for [<key>,]<val> in <array> {{{
1010 protected function generate_op_loop($details, &$out)
1012 if (isset($details['empty'])) {
1013 $expr = $this->expr('==',
1014 $this->expr_exec('count', $this->expr_var($details['array'])),
1018 $out[] = $this->op_if($expr);
1019 $this->generate_op_code($details['empty'], $out);
1020 $out[] = $this->op_else();
1023 /* ForID */
1024 $oldid = $this->forid;
1025 $this->forid = $oldid+1;
1027 $this->forloop[$this->forid] = array();
1029 /* Loop body */
1030 $for_loop_body = array();
1031 $this->generate_op_code($details['body'], $for_loop_body);
1033 $oid = $this->forid;
1034 $size = $this->expr_var('psize_'.$oid);
1036 // counter {{{
1037 if (isset($this->forloop[$oid]['counter'])) {
1038 $var = 'forcounter1_'.$oid;
1039 $out[] = $this->op_declare($var, $this->expr_number(1));
1040 $for_loop_body[] = $this->op_inc($var);
1042 // }}}
1044 // counter0 {{{
1045 if (isset($this->forloop[$oid]['counter0'])) {
1046 $var = 'forcounter0_'.$oid;
1047 $out[] = $this->op_declare($var, $this->expr_number(0) );
1048 $for_loop_body[] = $this->op_inc($var);
1050 // }}}
1052 // last {{{
1053 if (isset($this->forloop[$oid]['last'])) {
1054 if (!isset($cnt)) {
1055 $cnt = $this->op_declare('psize_'.$oid, $this->expr_exec('count', $this->expr_var($details['array'])));
1056 $out[] = $cnt;
1058 $var = 'islast_'.$oid;
1059 $expr = $this->op_declare($var, $this->expr("==", $this->expr_var('forcounter1_'.$oid), $size));
1061 $out[] = $expr;
1063 $for_loop_body[] = $expr;
1065 // }}}
1067 // first {{{
1068 if (isset($this->forloop[$oid]['first'])) {
1069 $out[] = $this->op_declare('isfirst_'.$oid, $this->expr_TRUE());
1071 $for_loop_body[] = $this->op_declare('isfirst_'.$oid, $this->expr_FALSE());
1073 // }}}
1075 // revcounter {{{
1076 if (isset($this->forloop[$oid]['revcounter'])) {
1077 if (!isset($cnt)) {
1078 $cnt = $this->op_declare('psize_'.$oid, $this->expr_exec('count', $this->expr_var($details['array'])));
1079 $out[] = $cnt;
1081 $var = $this->expr_var('revcount_'.$oid);
1082 $out[] = $this->op_declare($var, $size );
1084 $for_loop_body[] = $this->op_declare($var, $this->expr("-", $var, $this->expr_number(1)));
1086 // }}}
1088 // revcounter0 {{{
1089 if (isset($this->forloop[$oid]['revcounter0'])) {
1090 if (!isset($cnt)) {
1091 $cnt = $this->op_declare('psize_'.$oid, $this->expr_exec('count', $this->expr_var($details['array'])));
1092 $out[] = $cnt;
1094 $var = $this->expr_var('revcount0_'.$oid);
1095 $out[] = $this->op_declare($var, $this->expr("-", $size, $this->expr_number(1)));
1097 $for_loop_body[] = $this->op_declare($var, $this->expr("-", $var, $this->expr_number(1)));
1099 // }}}
1101 /* Restore old ForID */
1102 $this->forid = $oldid;
1104 /* Merge loop body */
1105 $loop = $this->op_foreach($details['array'], $details['variable'], $details['index']);
1107 $out[] = $loop;
1108 $out = array_merge($out, $for_loop_body);
1109 $out[] = $this->op_end('foreach');
1110 if (isset($details['empty'])) {
1111 $out[] = $this->op_end('if');
1114 // }}}
1116 // ifchanged [<var1> <var2] {{{
1117 protected function generate_op_ifchanged($details, &$out)
1119 static $ifchanged = 0;
1121 $ifchanged++;
1122 $var1 = 'ifchanged'.$ifchanged;
1123 if (!isset($details['check'])) {
1124 /* ugly */
1125 $this->ob_start($out);
1126 $var2 = 'buffer'.$this->ob_start;
1128 $expr = $this->expr('OR',
1129 $this->expr_isset($var1, FALSE),
1130 $this->expr('!=',
1131 $this->expr_var($var1),
1132 $this->expr_var($var2)
1136 $this->generate_op_code($details['body'], $out);
1137 $this->ob_start--;
1138 $out[] = $this->op_if($expr);
1139 $this->generate_op_print(array('variable' => $var2), $out);
1140 $out[] = $this->op_declare($var1, $this->expr_var($var2));
1141 } else {
1142 /* beauty :-) */
1143 foreach ($details['check'] as $id=>$type) {
1144 if (!isset($type['var'])) {
1145 throw new CompilerException("Unexpected string {$type['string']}, expected a varabile");
1147 $this_expr = $this->expr('OR',
1148 $this->expr_isset("{$var1}[{$id}]", FALSE),
1149 $this->expr('!=',
1150 $this->expr_var("{$var1}[{$id}]"),
1151 $this->expr_var($type['var'])
1154 if (isset($expr)) {
1155 $this_expr = $this->expr('AND',
1156 $this->expr('expr', $this_expr),
1157 $expr
1161 $expr = $this_expr;
1164 $out[] = $this->op_if($expr);
1165 $this->generate_op_code($details['body'], $out);
1166 $out[] = $this->op_declare($var1, $this->expr_array_first($details['check']));
1169 if (isset($details['else'])) {
1170 $out[] = $this->op_else();
1171 $this->generate_op_code($details['else'], $out);
1173 $out[] = $this->op_end('if');
1175 // }}}
1177 // autoescape ON|OFF {{{
1178 function generate_op_autoescape($details, &$out)
1180 $old_autoescape = $this->autoescape;
1181 $this->autoescape = strtolower($details['value']) == 'on';
1182 $this->generate_op_code($details['body'], $out);
1183 $this->autoescape = $old_autoescape;
1185 // }}}
1187 // ob_Start(array &$out) {{{
1189 * Start a new buffering
1192 function ob_start(&$out)
1194 $this->ob_start++;
1195 $out[] = $this->op_declare('buffer'.$this->ob_start, array('string' => ''));
1197 // }}}
1199 // Custom Tags {{{
1200 function get_custom_tag($name)
1202 $function = $this->get_function_name().'_tag_'.$name;
1203 $this->append .= "\n\n".Extensions::getInstance('Haanga_Tag')->getFunctionBody($name, $function);
1204 return $function;
1208 * Generate needed code for custom tags (tags that aren't
1209 * handled by the compiler).
1212 function generate_op_custom_tag($details, &$out)
1214 static $tags;
1215 if (!$tags) {
1216 $tags = Extensions::getInstance('Haanga_Tag');
1219 $tag_name = $details['name'];
1220 $tagFunction = $tags->getFunctionAlias($tag_name);
1222 if (!$tagFunction && !$tags->hasGenerator($tag_name)) {
1223 $function = $this->get_custom_tag($tag_name, isset($details['as']));
1224 } else {
1225 $function = $tagFunction;
1228 if (isset($details['body'])) {
1230 if the custom tag has 'body'
1231 then it behave the same way as a filter
1233 $this->ob_start($out);
1234 $this->generate_op_code($details['body'], $out);
1235 $target = $this->expr_var('buffer'.$this->ob_start);
1236 if ($tags->hasGenerator($tag_name)) {
1237 $exec = $tags->generator($tag_name, $this, array($target));
1238 } else {
1239 $exec = $this->expr_exec($function, $target);
1241 $this->ob_start--;
1242 $this->generate_op_print($exec, $out);
1243 return;
1246 $var = isset($details['as']) ? $details['as'] : NULL;
1247 $args = array_merge(array($function), $details['list']);
1249 if ($tags->hasGenerator($tag_name)) {
1250 $exec = $tags->generator($tag_name, $this, $details['list'], $var);
1251 if ($exec InstanceOf ArrayIterator) {
1253 The generator returned more than one statement,
1254 so we assume the output is already handled
1255 by one of those stmts.
1257 $out = array_merge($out, $exec->getArrayCopy());
1258 return;
1260 } else {
1261 $exec = call_user_func_array(array($this, 'expr_exec'), $args);
1264 if (isset($details['for'])) {
1265 $new_args = array($this->expr_var('var'));
1266 $print['function'][1] = $new_args;
1267 $arr['args'] = $new_args;
1269 if ($var) {
1270 $out[] = $this->op_declare($var, $this->expr_str());
1273 $out[] = $this->op_foreach($details['for'], 'var');
1274 if ($var) {
1275 $out[] = $this->append_var($var, $exec);
1276 } else {
1277 $this->generate_op_print($exec, $out);
1279 $out[] = $this->op_end('foreach');
1280 } else {
1281 if ($var) {
1282 $out[] = $this->op_declare($var, $exec);
1283 } else {
1284 $this->generate_op_print($exec, $out);
1288 // }}}
1290 // with <variable> as <var> {{{
1295 function generate_op_alias($details, &$out)
1297 $this->var_alias[ $details['as'] ] = $details['var'];
1298 $this->generate_op_code($details['body'], $out);
1299 unset($this->var_alias[ $details['as'] ] );
1301 // }}}
1303 // Custom Filters {{{
1304 function get_custom_filter($name)
1306 $function = $this->get_function_name().'_filter_'.$name;
1307 $this->append .= "\n\n".Extensions::getInstance('Haanga_Filter')->getFunctionBody($name, $function);
1308 return $function;
1312 function do_filtering($name, $args)
1314 static $filter;
1315 if (!$filter) {
1316 $filter = Extensions::getInstance('Haanga_Filter');
1319 if (is_array($name)) {
1321 prepare array for ($func_name, $arg1, $arg2 ... )
1322 where $arg1 = last expression and $arg2.. $argX is
1323 defined in the template
1325 $args = array_merge($args, $name['args']);
1326 $name = $name[0];
1329 if (!$filter->isValid($name)) {
1330 throw new CompilerException("{$name} is an invalid filter");
1332 if ($filter->hasGenerator($name)) {
1333 return $filter->generator($name, $this, $args);
1335 $fnc = $filter->getFunctionAlias($name);
1336 if (!$fnc) {
1337 $fnc = $this->get_custom_filter($name);
1339 $args = array_merge(array($fnc), $args);
1340 $exec = call_user_func_array(array($this, 'expr_exec'), $args);
1342 return $exec;
1345 function generate_op_filter($details, &$out)
1347 $this->ob_start($out);
1348 $this->generate_op_code($details['body'], $out);
1349 $target = $this->expr_var('buffer'.$this->ob_start);
1350 foreach ($details['functions'] as $f) {
1351 $param = (isset($exec) ? $exec : $target);
1352 $exec = $this->do_filtering($f, array($param));
1354 $this->ob_start--;
1355 $this->generate_op_print($exec, $out);
1357 // }}}
1359 final static function main_cli()
1361 $argv = $GLOBALS['argv'];
1362 $haanga = new Haanga_Main;
1363 $code = $haanga->compile_file($argv[1]);
1364 echo "<?php\n\n$code\n";
1371 * Runtime compiler
1374 final class Haanga_Main_Runtime extends Haanga_Main
1377 // get_function_name($name=NULL) {{{
1382 function get_function_name($name=NULL)
1384 if ($name === NULL) {
1385 $name = $this->name;
1387 return "haanga_".sha1($name);
1389 // }}}
1391 // set_template_name($path) {{{
1392 function set_template_name($path)
1394 return $path;
1396 // }}}
1398 // Override {% include %} {{{
1399 protected function generate_op_include($details, &$out)
1401 $expr = $this->expr_exec(
1402 'Haanga::Load',
1403 $details[0],
1404 $this->expr_var('vars'),
1405 $this->expr_TRUE(),
1406 $this->expr_var('blocks')
1408 $this->generate_op_print(array('expr' => $expr), $out);
1410 // }}}
1412 // {% base "" %} {{{
1413 function expr_call_base_template()
1415 return $this->expr_exec(
1416 'Haanga::Load',
1417 $this->subtemplate,
1418 $this->expr_var('vars'),
1419 $this->expr_TRUE(),
1420 $this->expr_var('blocks')
1423 // }}}
1425 // get_base_template($base) {{{
1426 function get_base_template($base)
1428 $this->subtemplate = $base;
1430 // }}}
1432 // Override get_Custom_tag {{{
1437 function get_custom_tag($name)
1439 $loaded = &$this->tags;
1441 if (!isset($loaded[$name])) {
1442 $this->prepend_op[] = $this->op_comment("Load tag {$name} definition");
1443 $this->prepend_op[] = $this->op_expr($this->expr_exec("Haanga::doInclude", $this->Expr_str(Extensions::getInstance('Haanga_Tag')->getFilePath($name, FALSE))));
1444 $loaded[$name] = TRUE;
1447 $name = ucfirst($name);
1449 return "{$name}_Tag::main";
1451 // }}}
1453 // Override get_custom_filter {{{
1454 function get_custom_filter($name)
1456 $loaded = &$this->filters;
1458 if (!isset($loaded[$name])) {
1459 $this->prepend_op[] = $this->op_comment("Load filter {$name} definition");
1460 $this->prepend_op[] = $this->op_expr($this->expr_exec("Haanga::doInclude", $this->Expr_str(Extensions::getInstance('Haanga_Filter')->getFilePath($name, FALSE))));
1461 $loaded[$name] = TRUE;
1464 $name = ucfirst($name);
1466 return "{$name}_Filter::main";
1468 // }}}
1473 * Local variables:
1474 * tab-width: 4
1475 * c-basic-offset: 4
1476 * End:
1477 * vim600: sw=4 ts=4 fdm=marker
1478 * vim<600: sw=4 ts=4