- Added more filters and custom tags
[haanga.git] / lib / haanga.php
blobbd17bbc3ef8d9c3493d47bbcaf7102752790b05b
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 Haanga_CompilerException extends Exception
56 // }}}
59 class Haanga_Compiler
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 $force_whitespaces=0;
93 /**
94 * Debug file
96 protected $debug;
97 // }}}
99 function __construct()
101 $this->generator = new Haanga_CodeGenerator;
102 if (self::$block_var===NULL) {
103 self::$block_var = '{{block.'.md5('super').'}}';
107 // setDebug($file) {{{
108 function setDebug($file)
110 $this->debug = $file;
112 // }}}
114 // reset() {{{
115 function reset()
117 $avoid_cleaning = array('strip_whitespaces' => 1, 'block_var' => 1, 'autoescape'=>1);
118 foreach (array_keys(get_object_vars($this)) as $key) {
119 if (isset($avoid_cleaning[$key])) {
120 continue;
122 $this->$key = NULL;
124 $this->generator = new Haanga_CodeGenerator;
125 $this->blocks = array();
126 $this->cycle = array();
128 // }}}
130 // get_template_name() {{{
131 final function get_template_name()
133 return $this->name;
135 // }}}
137 // Set template name {{{
138 function set_template_name($path)
140 return ($this->name = strstr(basename($path),'.', TRUE));
142 // }}}
144 // get_function_name(string $name) {{{
145 function get_function_name($name)
147 return "{$name}_template";
149 // }}}
151 // Compile ($code, $name=NULL) {{{
152 final function compile($code, $name=NULL)
154 $this->name = $name;
156 $parsed = do_parsing($code);
157 $code = "";
158 $this->subtemplate = FALSE;
160 if ($parsed[0]['operation'] == 'base') {
161 /* {% base ... %} found */
162 $base = $parsed[0][0];
163 $code .= $this->get_base_template($base);
164 unset($parsed[0]);
167 if ($name) {
168 if (isset($this->_file)) {
169 $op_code[] = $this->op_comment("Generated from {$this->_base_dir}/{$this->_file}");
171 $op_code[] = $this->op_declare_function($this->get_function_name($name));
172 $op_code[] = $this->op_expr($this->expr_exec('extract', $this->expr_var('vars')));
175 $this->ob_start($op_code);
176 $this->generate_op_code($parsed, $op_code);
177 if ($this->subtemplate) {
178 $expr = $this->expr_call_base_template();
179 $this->generate_op_print(array('expr' => $expr), $op_code);
181 $this->ob_start--;
183 /* Add last part */
184 $expr = $this->expr('==', $this->expr_var('return'), TRUE);
185 $op_code[] = $this->op_if($expr);
186 $op_code[] = $this->op_return($this->expr_var('buffer1'));
187 $op_code[] = $this->op_else();
188 $this->generate_op_print(array('variable' => 'buffer1'), $op_code);
189 $op_code[] = $this->op_end('if');
191 if ($name) {
192 $op_code[] = $this->op_end('function');
195 if (count($this->prepend_op)) {
196 $op_code = array_merge($this->prepend_op, $op_code);
199 $code .= $this->generator->getCode($op_code);
200 if (!empty($this->append)) {
201 $code .= $this->append;
203 if (!empty($this->debug)) {
204 file_put_contents($this->debug, print_r($op_code, TRUE));
206 return $code;
208 // }}}
210 // compile_file($file) {{{
212 * Compile a file
214 * @param string $file File path
216 * @return Generated PHP code
218 final function compile_file($file)
220 if (!is_readable($file)) {
221 throw new Haanga_CompilerException("$file is not a file");
223 $this->_base_dir = dirname($file);
224 $this->_file = basename($file);
225 $name = $this->set_template_name($file);
226 return $this->compile(file_get_contents($file), $name);
228 // }}}
230 // is_expr methods {{{
231 function is_expr(Array $cmd, $type=NULL)
233 if (isset($cmd['op_expr'])) {
234 if (!$type || $type == $cmd['op_expr']) {
235 return TRUE;
238 return FALSE;
241 function is_exec(Array $cmd)
243 return $this->is_expr($cmd, 'exec');
246 function is_var(Array $cmd)
248 return isset($cmd['var']);
250 // }}}
252 // op_* helper methods {{{
254 * Return an stand alone expression
257 function op_expr($expr)
259 return array('op' => 'expr', $expr);
262 function op_comment($comment)
264 return array('op' => 'comment', 'comment' => $comment);
267 function op_foreach($array, $value, $key=NULL)
269 foreach (array('array', 'value', 'key') as $var) {
270 $var = &$$var;
271 if (is_array($var) && isset($var['var'])) {
272 $var = $var['var'];
274 unset($var);
276 $def = array('op' => 'foreach', 'array' => $array, 'value' => $value);
277 if ($key) {
278 $def['key'] = $key;
280 return $def;
283 function op_if($expr)
285 return array('op' => 'if', 'expr' => $expr);
288 function op_else()
290 return array('op' => 'else');
293 function op_return($expr)
295 return array('op' => 'return', $expr);
298 function op_end($op)
300 return array('op' => "end_{$op}");
303 function op_declare($name, $value)
305 if (is_array($name)) {
306 if (isset($name['var'])) {
307 $name = $name['var'];
311 if (is_array($value)) {
312 if (isset($value['op_expr'])) {
313 $value = array('expr' => $value);
317 return array('op' => 'declare', 'name' => $name, $value);
320 function op_append($name, $expr)
322 return array('op' => 'append_var', 'name' => $name, $expr);
326 function op_inc($name)
328 return array('op' => 'inc', 'name' => $name);
331 function op_declare_function($name)
333 return array('op' => 'function', 'name' => $name);
336 //}}}
338 // expr_* helper methods {{{
339 function expr_cond($expr, $true, $false)
341 return array('expr_cond' => $expr, 'true' => $true, 'false' => $false);
344 function expr_const($name)
346 return array('constant' => $name);
350 * Generate code to call base template
353 function expr_call_base_template()
355 return $this->expr_exec(
356 $this->get_function_name($this->subtemplate),
357 $this->expr_var('vars'),
358 $this->expr_TRUE(),
359 $this->expr_var('blocks')
364 * return a function call for isset($var) === $isset
366 * @return array
368 final function expr_isset($var, $isset=TRUE)
370 return $this->expr('==', $this->expr_exec('isset', $this->expr_var($var)), $isset);
373 final function expr_isset_ex($var, $isset=TRUE)
375 return $this->expr('==', $this->expr_exec('isset', $var), $isset);
378 final function expr_array()
380 $def = array();
381 foreach (func_get_args() as $arg) {
382 if (count($arg) == 2) {
383 if (!is_array($arg[0])) {
384 $arg[0] = $this->expr_str($arg[0]);
386 $arg = array('key' => $arg);
388 $def[] = $arg;
390 return array("array" => $def);
393 final function expr_array_first($values)
395 $def = array();
396 foreach ($values as $arg) {
397 if (count($arg) == 2) {
398 if (!is_array($arg[0])) {
399 $arg[0] = $this->expr_str($arg[0]);
401 $arg = array('key' => $arg);
403 $def[] = $arg;
405 return array("array" => $def);
410 * return an number definition of $num
412 * @return array
414 final function expr_number($num=0)
416 return array('number' =>$num);
420 * return an string definition of $str
422 * @return array
424 final function expr_str($str='')
426 return array('string' => $str);
430 * Generate expression that for
431 * boolean TRUE
433 * @return array
435 final function expr_TRUE()
437 return array('expr' => TRUE);
441 * Generate expression that for
442 * boolean FALSE
444 * @return array
446 final function expr_FALSE()
448 return array('expr' => FALSE);
452 * Return expr for variable reference
454 * @return array
456 final function expr_var($var)
458 return array('var' => func_get_args());
462 * Return expr for variable reference
464 * @return array
466 final function expr_var_ex(Array $var)
468 return array('var' => $var);
472 * Generate expression for
473 * a function calling inside an expression
475 * @return array
477 final function expr_exec($function)
479 $args = func_get_args();
480 unset($args[0]);
481 $args = array_values($args);
483 return array(
484 'exec' => $function,
485 'args' => $args
492 * Generate a generic expression
494 * @return array
496 final function expr($operation, $expr1, $expr2=NULL)
498 $expr = array('op_expr' => $operation, $expr1);
499 if ($expr2 !== NULL) {
500 $expr[] = $expr2;
503 return $expr;
505 // }}}
507 // get_base_template($base) {{{
509 * Handle {% base "" %} definition. By default only
510 * static (string) are supported, but this can be overrided
511 * on subclasses.
513 * This method load the base class, compile it and return
514 * the generated code.
516 * @param array $base Base structure
518 * @return string Generated source code
520 function get_base_template($base)
522 if (!isset($base['string'])) {
523 throw new Haanga_CompilerException("Dynamic inheritance is not supported for compilated templates");
525 $file = $base['string'];
526 list($this->subtemplate, $new_code) = $this->compile_required_template($file);
527 return $new_code."\n\n";
529 // }}}
531 // {% base "foo.html" %} {{{
532 protected function generate_op_base()
534 throw new exception("{% base %} can be only as first statement");
536 // }}}
538 // Main Loop {{{
539 protected function generate_op_code($parsed, &$out)
541 if (!is_array($parsed)) {
542 throw new Haanga_CompilerException("Invalid \$parsed array");
544 foreach ($parsed as $op) {
545 if (!isset($op['operation'])) {
546 throw new Haanga_CompilerException("Malformed $parsed array");
548 if ($this->subtemplate && $this->in_block == 0 && $op['operation'] != 'block') {
549 /* ignore most of tokens in subtemplates */
550 continue;
552 $method = "generate_op_".$op['operation'];
553 if (!is_callable(array($this, $method))) {
554 throw new Haanga_CompilerException("Compiler: Missing method $method");
556 $this->$method($op, $out);
559 // }}}
561 // Check the current expr {{{
562 protected function check_expr(&$expr)
564 if (is_array($expr) && isset($expr['op_expr'])) {
565 if ($expr['op_expr'] == 'in') {
566 if (isset($expr[1]['string'])) {
567 $expr = $this->expr("!==",
568 $this->expr_exec("strpos", $expr[1], $expr[0]),
569 FALSE
571 } else {
572 $expr = $this->expr("!==", $this->expr_cond(
573 $this->expr("==", $this->expr_exec("is_array", $expr[1]), TRUE),
574 $this->expr_exec("array_search", $expr[0], $expr[1]),
575 $this->expr_exec("strpos", $expr[1], $expr[0])
576 ), FALSE);
580 $this->check_expr($expr[0]);
581 $this->check_expr($expr[1]);
582 } else {
583 if (is_array($expr)) {
584 if (isset($expr['var'])) {
585 $expr = $this->generate_variable_name($expr['var']);
586 } else if (isset($expr['var_filter'])) {
587 foreach ($expr['var_filter'] as $id => $f) {
588 if ($id == 0) {
589 $exec = $this->generate_variable_name($f);
590 } else {
591 $exec = $this->expr_exec($f, $exec);
594 $expr = $exec;
595 } else if (isset($expr['args'])) {
596 /* check every arguments */
597 foreach ($expr['args'] as &$v) {
598 $this->check_expr($v);
600 unset($v);
601 } else if (isset($expr['expr_cond'])) {
602 /* Check expr conditions */
603 $this->check_expr($expr['expr_cond']);
604 $this->check_expr($expr['true']);
605 $this->check_expr($expr['false']);
610 // }}}
612 // {% if <expr> %} HTML {% else %} TWO {% endif $} {{{
613 protected function generate_op_if($details, &$out)
615 $this->check_expr($details['expr']);
616 $out[] = $this->op_if($details['expr']);
617 $this->generate_op_code($details['body'], $out);
618 if (isset($details['else'])) {
619 $out[] = $this->op_else();
620 $this->generate_op_code($details['else'], $out);
622 $out[] = $this->op_end('if');
624 // }}}
626 // Overload template {{{
627 protected function compile_required_template($file)
629 if (!is_file($file)) {
630 if (isset($this->_base_dir)) {
631 $file = $this->_base_dir.'/'.$file;
634 if (!is_file($file)) {
635 throw new Haanga_CompilerException("can't find {$file} file template");
637 $class = get_class($this);
638 $comp = new $class;
639 $comp->reset();
640 $code = $comp->compile_file($file);
641 return array($comp->get_template_name(), $code);
643 // }}}
645 // include "file.html" | include <var1> {{{
646 protected function generate_op_include($details, &$out)
648 if (!$details[0]['string']) {
649 throw new Haanga_CompilerException("Dynamic inheritance is not supported for compilated templates");
651 list($name,$code) = $this->compile_required_template($details[0]['string']);
652 $this->append .= "\n\n{$code}";
653 $expr = $this->expr_exec(
654 $this->get_function_name($this->subtemplate),
655 $this->expr_var('vars'),
656 $this->expr_var('blocks'),
657 $this->expr_TRUE()
659 $this->generate_op_print($expr, $op_code);
660 $this->generate_op_print($expr, $out);
662 // }}}
664 // Handle HTML code {{{
665 protected function generate_op_html($details, &$out)
667 $this->generate_op_print($details, $out);
669 // }}}
671 // get_var_filtered {{{
673 * This method handles all the filtered variable (piped_list(X)'s
674 * output in the parser.
677 * @param array $variable (Output of piped_list(B) (parser))
678 * @param array &$varname Variable name
679 * @param bool $accept_string TRUE is string output are OK (ie: block.parent)
681 * @return expr
684 function get_filtered_var($variable, &$varname, $accept_string=FALSE)
686 $this->var_is_safe = FALSE;
687 if (count($variable) > 1) {
688 $count = count($variable);
689 $target = $this->generate_variable_name($variable[0]);
691 if (!isset($target['var'])) {
692 /* block.super can't have any filter */
693 throw new Haanga_CompilerException("This variable can't have any filter");
696 for ($i=1; $i < $count; $i++) {
697 $func_name = $variable[$i];
698 if ($func_name == 'escape') {
699 /* to avoid double cleaning */
700 $this->var_is_safe = TRUE;
702 $args = array(isset($exec) ? $exec : $target);
703 $exec = $this->do_filtering($func_name, $args);
705 unset($variable);
706 $varname = $args[0];
707 $details = $exec;
708 } else {
709 $details = $this->generate_variable_name($variable[0]);
710 $varname = $variable[0];
712 if (!isset($details['var']) && !$accept_string) {
713 /* generate_variable_name didn't replied a variable, weird case
714 currently just used for {{block.super}}.
716 throw new Haanga_CompilerException("Invalid variable name {$variable[0]}");
720 return $details;
722 // }}}
724 // generate_op_print_var {{{
726 * Generate code to print a variable with its filters, if there is any.
728 * All variable (except those flagged as |safe) are automatically
729 * escaped if autoescape is "on".
732 protected function generate_op_print_var($details, &$out)
735 $details = $this->get_filtered_var($details['variable'], $variable, TRUE);
737 if (!isset($details['var']) && !isset($details['exec'])) {
738 /* generate_variable_name didn't replied a variable, weird case
739 currently just used for {{block.super}}.
741 $this->generate_op_print($details, $out);
742 return;
746 if (!$this->var_is_safe && $this->autoescape) {
747 $args = array($details);
748 $details = $this->do_filtering('escape', $args);
751 $this->generate_op_print($details, $out);
753 // }}}
755 // is_last_op_print($out) {{{
757 * Return TRUE if the last stacked operation
758 * is a print (declare or append_var).
760 * @param array $out Stack of operations
762 * @return bool
764 protected function is_last_op_print($out)
766 $last = count($out)-1;
767 $sprint = array('print', 'declare', 'append_var');
768 return $last >= 0 && array_search($out[$last]['op'], $sprint) !== FALSE;
770 // }}}
772 // {# something #} {{{
773 protected function generate_op_comment($details, &$out)
775 if ($this->is_last_op_print($out)) {
776 /* If there is a print declared previously, we pop it
777 and add it after the cycle declaration
779 $old_print = array_pop($out);
781 $out[] = $this->op_comment($details['comment']);
782 if (isset($old_print)) {
783 $out[] = $old_print;
786 // }}}
788 // {% block 'name' %} ... {% endblock %} {{{
789 protected function generate_op_block($details, &$out)
791 $this->in_block++;
792 $this->blocks[] = $details['name'];
793 $block_name = $this->expr_var('blocks', $details['name']);
795 $this->ob_start($out);
796 $buffer_var = 'buffer'.$this->ob_start;
798 $this->generate_op_code($details['body'], $body);
800 $out = array_merge($out, $body);
801 $this->ob_start--;
803 $buffer = $this->expr_var($buffer_var);
805 /* {{{ */
807 * isset previous block (parent block)?
808 * TRUE
809 * has reference to self::$block_var ?
810 * TRUE
811 * replace self::$block_var for current block value (buffer)
812 * FALSE
813 * print parent block
814 * FALSE
815 * print current block
818 $declare = $this->expr_cond(
819 $this->expr_isset_ex($block_name),
820 $this->expr_cond(
821 $this->expr("===", $this->expr_exec('strpos', $block_name,
822 $this->expr_str(self::$block_var)
823 ), $this->expr_FALSE()
825 $block_name,
826 $this->expr_exec('str_replace',
827 $this->expr_str(self::$block_var),
828 $buffer,
829 $block_name
832 $buffer
834 /* }}} */
836 if (!$this->subtemplate) {
837 $this->generate_op_print($declare, $out);
838 } else {
839 $out[] = $this->op_declare($block_name, $declare);
840 if ($this->in_block > 1) {
841 $this->generate_op_print($block_name, $out);
844 array_pop($this->blocks);
845 $this->in_block--;
848 // }}}
850 // regroup <var1> by <field> as <foo> {{{
851 protected function generate_op_regroup($details, &$out)
853 $out[] = $this->op_comment("Temporary sorting");
854 $array = $this->get_filtered_var($details['array'], $varname);
856 if (isset($array['exec'])) {
857 $varname = $this->expr_var($details['as']);
858 $out[] = $this->op_declare($varname, $array);
860 $var = $this->expr_var('item', $details['row']);
862 $out[] = $this->op_declare('temp_group', $this->expr_array());
863 $out[] = $this->op_foreach($varname, 'item');
866 $out[] = $this->op_declare(array('temp_group', $var, NULL), $this->expr_var('item'));
867 $out[] = $this->op_end('foreach');
869 $out[] = $this->op_comment("Proper format");
870 $out[] = $this->op_declare($details['as'], $this->expr_array_first(array()));
871 $out[] = $this->op_foreach('temp_group', 'item', 'group');
873 $array = $this->expr_array(
874 array("grouper", $this->expr_var('group')),
875 array("list", $this->expr_var('item'))
878 $out[] = $this->op_declare(array($details['as'], NULL), $array );
880 $out[] = $this->op_end('foreach');
881 $out[] = $this->op_comment("Sorting done");
883 // }}}
885 // Get variable name {{{
886 protected function generate_variable_name($variable)
888 if (is_array($variable)) {
889 switch ($variable[0]) {
890 case 'forloop':
891 if (!$this->forid) {
892 throw new Haanga_CompilerException("Invalid forloop reference outside of a loop");
894 switch ($variable[1]) {
895 case 'counter':
896 $this->forloop[$this->forid]['counter'] = TRUE;
897 $variable = 'forcounter1_'.$this->forid;
898 break;
899 case 'counter0':
900 $this->forloop[$this->forid]['counter0'] = TRUE;
901 $variable = 'forcounter0_'.$this->forid;
902 break;
903 case 'last':
904 $this->forloop[$this->forid]['counter'] = TRUE;
905 $this->forloop[$this->forid]['last'] = TRUE;
906 $variable = 'islast_'.$this->forid;
907 break;
908 case 'first':
909 $this->forloop[$this->forid]['first'] = TRUE;
910 $variable = 'isfirst_'.$this->forid;
911 break;
912 case 'revcounter':
913 $this->forloop[$this->forid]['revcounter'] = TRUE;
914 $variable = 'revcount_'.$this->forid;
915 break;
916 case 'revcounter0':
917 $this->forloop[$this->forid]['revcounter0'] = TRUE;
918 $variable = 'revcount0_'.$this->forid;
919 break;
920 case 'parentloop':
921 unset($variable[1]);
922 $this->forid--;
923 $variable = $this->generate_variable_name(array_values($variable));
924 $variable = $variable['var'];
925 $this->forid++;
926 break;
927 default:
928 throw new Haanga_CompilerException("Unexpected forloop.{$variable[1]}");
930 /* no need to escape it */
931 $this->var_is_safe = TRUE;
932 break;
933 case 'block':
934 if ($this->in_block == 0) {
935 throw new Haanga_CompilerException("Can't use block.super outside a block");
937 if (!$this->subtemplate) {
938 throw new Haanga_CompilerException("Only subtemplates can call block.super");
940 /* no need to escape it */
941 $this->var_is_safe = TRUE;
942 return $this->expr_str(self::$block_var);
943 break;
946 } else if (isset($this->var_alias[$variable])) {
947 $variable = $this->var_alias[$variable];
950 return $this->expr_var($variable);
952 // }}}
954 // Print {{{
955 public function generate_op_print($details, &$out)
957 $last = count($out)-1;
958 if (isset($details['variable'])) {
959 $content = $this->generate_variable_name($details['variable']);
960 } else if (isset($details['html'])) {
961 $html = $details['html'];
962 $content = $this->expr_str($html);
963 } else {
964 $content = $details;
967 $var_name = 'buffer'.$this->ob_start;
969 if ($this->ob_start == 0) {
970 $operation = 'print';
971 } else {
972 $operation = 'append_var';
975 if ($last >= 0 && $out[$last]['op'] == $operation && ($operation != 'append_var' || $out[$last]['name'] === $var_name)) {
976 /* try to append this to the previous print if it exists */
977 $out[$last][] = $content;
978 } else {
979 if ($this->ob_start == 0) {
980 $out[] = array('op' => 'print', $content);
981 } else {
982 if (isset($out[$last]) && $out[$last]['op'] == 'declare' && $out[$last]['name'] == $var_name) {
983 /* override an empty declaration of a empty buffer
984 if the next operation is an 'append'
986 $out[$last][] = $content;
987 } else {
988 $out[] = $this->op_append('buffer'.$this->ob_start, $content);
993 // }}}
995 // for [<key>,]<val> in <array> {{{
996 protected function generate_op_loop($details, &$out)
998 if (isset($details['empty'])) {
999 $expr = $this->expr('==',
1000 $this->expr_exec('count', $this->expr_var($details['array'])),
1004 $out[] = $this->op_if($expr);
1005 $this->generate_op_code($details['empty'], $out);
1006 $out[] = $this->op_else();
1009 /* ForID */
1010 $oldid = $this->forid;
1011 $this->forid = $oldid+1;
1013 $this->forloop[$this->forid] = array();
1015 /* Loop body */
1016 $for_loop_body = array();
1017 $this->generate_op_code($details['body'], $for_loop_body);
1019 $oid = $this->forid;
1020 $size = $this->expr_var('psize_'.$oid);
1022 // counter {{{
1023 if (isset($this->forloop[$oid]['counter'])) {
1024 $var = 'forcounter1_'.$oid;
1025 $out[] = $this->op_declare($var, $this->expr_number(1));
1026 $for_loop_body[] = $this->op_inc($var);
1028 // }}}
1030 // counter0 {{{
1031 if (isset($this->forloop[$oid]['counter0'])) {
1032 $var = 'forcounter0_'.$oid;
1033 $out[] = $this->op_declare($var, $this->expr_number(0) );
1034 $for_loop_body[] = $this->op_inc($var);
1036 // }}}
1038 // last {{{
1039 if (isset($this->forloop[$oid]['last'])) {
1040 if (!isset($cnt)) {
1041 $cnt = $this->op_declare('psize_'.$oid, $this->expr_exec('count', $this->expr_var($details['array'])));
1042 $out[] = $cnt;
1044 $var = 'islast_'.$oid;
1045 $expr = $this->op_declare($var, $this->expr("==", $this->expr_var('forcounter1_'.$oid), $size));
1047 $out[] = $expr;
1049 $for_loop_body[] = $expr;
1051 // }}}
1053 // first {{{
1054 if (isset($this->forloop[$oid]['first'])) {
1055 $out[] = $this->op_declare('isfirst_'.$oid, $this->expr_TRUE());
1057 $for_loop_body[] = $this->op_declare('isfirst_'.$oid, $this->expr_FALSE());
1059 // }}}
1061 // revcounter {{{
1062 if (isset($this->forloop[$oid]['revcounter'])) {
1063 if (!isset($cnt)) {
1064 $cnt = $this->op_declare('psize_'.$oid, $this->expr_exec('count', $this->expr_var($details['array'])));
1065 $out[] = $cnt;
1067 $var = $this->expr_var('revcount_'.$oid);
1068 $out[] = $this->op_declare($var, $size );
1070 $for_loop_body[] = $this->op_declare($var, $this->expr("-", $var, $this->expr_number(1)));
1072 // }}}
1074 // revcounter0 {{{
1075 if (isset($this->forloop[$oid]['revcounter0'])) {
1076 if (!isset($cnt)) {
1077 $cnt = $this->op_declare('psize_'.$oid, $this->expr_exec('count', $this->expr_var($details['array'])));
1078 $out[] = $cnt;
1080 $var = $this->expr_var('revcount0_'.$oid);
1081 $out[] = $this->op_declare($var, $this->expr("-", $size, $this->expr_number(1)));
1083 $for_loop_body[] = $this->op_declare($var, $this->expr("-", $var, $this->expr_number(1)));
1085 // }}}
1087 /* Restore old ForID */
1088 $this->forid = $oldid;
1090 /* Merge loop body */
1091 $array = $this->get_filtered_var($details['array'], $varname);
1092 $loop = $this->op_foreach($array, $details['variable'], $details['index']);
1094 $out[] = $loop;
1095 $out = array_merge($out, $for_loop_body);
1096 $out[] = $this->op_end('foreach');
1097 if (isset($details['empty'])) {
1098 $out[] = $this->op_end('if');
1101 // }}}
1103 // ifchanged [<var1> <var2] {{{
1104 protected function generate_op_ifchanged($details, &$out)
1106 static $ifchanged = 0;
1108 $ifchanged++;
1109 $var1 = 'ifchanged'.$ifchanged;
1110 if (!isset($details['check'])) {
1111 /* ugly */
1112 $this->ob_start($out);
1113 $var2 = 'buffer'.$this->ob_start;
1115 $expr = $this->expr('OR',
1116 $this->expr_isset($var1, FALSE),
1117 $this->expr('!=',
1118 $this->expr_var($var1),
1119 $this->expr_var($var2)
1123 $this->generate_op_code($details['body'], $out);
1124 $this->ob_start--;
1125 $out[] = $this->op_if($expr);
1126 $this->generate_op_print(array('variable' => $var2), $out);
1127 $out[] = $this->op_declare($var1, $this->expr_var($var2));
1128 } else {
1129 /* beauty :-) */
1130 foreach ($details['check'] as $id=>$type) {
1131 if (!isset($type['var'])) {
1132 throw new Haanga_CompilerException("Unexpected string {$type['string']}, expected a varabile");
1134 $this_expr = $this->expr('OR',
1135 $this->expr_isset("{$var1}[{$id}]", FALSE),
1136 $this->expr('!=',
1137 $this->expr_var("{$var1}[{$id}]"),
1138 $this->expr_var($type['var'])
1141 if (isset($expr)) {
1142 $this_expr = $this->expr('AND',
1143 $this->expr('expr', $this_expr),
1144 $expr
1148 $expr = $this_expr;
1151 $out[] = $this->op_if($expr);
1152 $this->generate_op_code($details['body'], $out);
1153 $out[] = $this->op_declare($var1, $this->expr_array_first($details['check']));
1156 if (isset($details['else'])) {
1157 $out[] = $this->op_else();
1158 $this->generate_op_code($details['else'], $out);
1160 $out[] = $this->op_end('if');
1162 // }}}
1164 // autoescape ON|OFF {{{
1165 function generate_op_autoescape($details, &$out)
1167 $old_autoescape = $this->autoescape;
1168 $this->autoescape = strtolower($details['value']) == 'on';
1169 $this->generate_op_code($details['body'], $out);
1170 $this->autoescape = $old_autoescape;
1172 // }}}
1174 // ob_Start(array &$out) {{{
1176 * Start a new buffering
1179 function ob_start(&$out)
1181 $this->ob_start++;
1182 $out[] = $this->op_declare('buffer'.$this->ob_start, array('string' => ''));
1184 // }}}
1186 // Custom Tags {{{
1187 function get_custom_tag($name)
1189 $function = $this->get_function_name().'_tag_'.$name;
1190 $this->append .= "\n\n".Haanga_Extensions::getInstance('Haanga_Tag')->getFunctionBody($name, $function);
1191 return $function;
1195 * Generate needed code for custom tags (tags that aren't
1196 * handled by the compiler).
1199 function generate_op_custom_tag($details, &$out)
1201 static $tags;
1202 if (!$tags) {
1203 $tags = Haanga_Extensions::getInstance('Haanga_Tag');
1206 $tag_name = $details['name'];
1207 $tagFunction = $tags->getFunctionAlias($tag_name);
1209 if (!$tagFunction && !$tags->hasGenerator($tag_name)) {
1210 $function = $this->get_custom_tag($tag_name, isset($details['as']));
1211 } else {
1212 $function = $tagFunction;
1215 if (isset($details['body'])) {
1217 if the custom tag has 'body'
1218 then it behave the same way as a filter
1220 $this->ob_start($out);
1221 $this->generate_op_code($details['body'], $out);
1222 $target = $this->expr_var('buffer'.$this->ob_start);
1223 if ($tags->hasGenerator($tag_name)) {
1224 $exec = $tags->generator($tag_name, $this, array($target));
1225 } else {
1226 $exec = $this->expr_exec($function, $target);
1228 $this->ob_start--;
1229 $this->generate_op_print($exec, $out);
1230 return;
1233 $var = isset($details['as']) ? $details['as'] : NULL;
1234 $args = array_merge(array($function), $details['list']);
1236 if ($tags->hasGenerator($tag_name)) {
1237 $exec = $tags->generator($tag_name, $this, $details['list'], $var);
1238 if ($exec InstanceOf ArrayIterator) {
1240 The generator returned more than one statement,
1241 so we assume the output is already handled
1242 by one of those stmts.
1244 $out = array_merge($out, $exec->getArrayCopy());
1245 return;
1247 } else {
1248 $exec = call_user_func_array(array($this, 'expr_exec'), $args);
1251 if ($var) {
1252 $out[] = $this->op_declare($var, $exec);
1253 } else {
1254 $this->generate_op_print($exec, $out);
1257 // }}}
1259 // with <variable> as <var> {{{
1264 function generate_op_alias($details, &$out)
1266 $this->var_alias[ $details['as'] ] = $details['var'];
1267 $this->generate_op_code($details['body'], $out);
1268 unset($this->var_alias[ $details['as'] ] );
1270 // }}}
1272 // Custom Filters {{{
1273 function get_custom_filter($name)
1275 $function = $this->get_function_name().'_filter_'.$name;
1276 $this->append .= "\n\n".Haanga_Extensions::getInstance('Haanga_Filter')->getFunctionBody($name, $function);
1277 return $function;
1281 function do_filtering($name, $args)
1283 static $filter;
1284 if (!$filter) {
1285 $filter = Haanga_Extensions::getInstance('Haanga_Filter');
1288 if (is_array($name)) {
1290 prepare array for ($func_name, $arg1, $arg2 ... )
1291 where $arg1 = last expression and $arg2.. $argX is
1292 defined in the template
1294 $args = array_merge($args, $name['args']);
1295 $name = $name[0];
1298 if (!$filter->isValid($name)) {
1299 throw new Haanga_CompilerException("{$name} is an invalid filter");
1301 if ($filter->hasGenerator($name)) {
1302 return $filter->generator($name, $this, $args);
1304 $fnc = $filter->getFunctionAlias($name);
1305 if (!$fnc) {
1306 $fnc = $this->get_custom_filter($name);
1308 $args = array_merge(array($fnc), $args);
1309 $exec = call_user_func_array(array($this, 'expr_exec'), $args);
1311 return $exec;
1314 function generate_op_filter($details, &$out)
1316 $this->ob_start($out);
1317 $this->generate_op_code($details['body'], $out);
1318 $target = $this->expr_var('buffer'.$this->ob_start);
1319 foreach ($details['functions'] as $f) {
1320 $param = (isset($exec) ? $exec : $target);
1321 $exec = $this->do_filtering($f, array($param));
1323 $this->ob_start--;
1324 $this->generate_op_print($exec, $out);
1326 // }}}
1328 final static function main_cli()
1330 $argv = $GLOBALS['argv'];
1331 $haanga = new Haanga_Compiler;
1332 $code = $haanga->compile_file($argv[1]);
1333 echo "<?php\n\n$code\n";
1340 * Runtime compiler
1343 final class Haanga_Compiler_Runtime extends Haanga_Compiler
1346 // get_function_name($name=NULL) {{{
1351 function get_function_name($name=NULL)
1353 if ($name === NULL) {
1354 $name = $this->name;
1356 return "haanga_".sha1($name);
1358 // }}}
1360 // set_template_name($path) {{{
1361 function set_template_name($path)
1363 return $path;
1365 // }}}
1367 // Override {% include %} {{{
1368 protected function generate_op_include($details, &$out)
1370 $expr = $this->expr_exec(
1371 'Haanga::Load',
1372 $details[0],
1373 $this->expr_var('vars'),
1374 $this->expr_TRUE(),
1375 $this->expr_var('blocks')
1377 $this->generate_op_print(array('expr' => $expr), $out);
1379 // }}}
1381 // {% base "" %} {{{
1382 function expr_call_base_template()
1384 return $this->expr_exec(
1385 'Haanga::Load',
1386 $this->subtemplate,
1387 $this->expr_var('vars'),
1388 $this->expr_TRUE(),
1389 $this->expr_var('blocks')
1392 // }}}
1394 // get_base_template($base) {{{
1395 function get_base_template($base)
1397 $this->subtemplate = $base;
1399 // }}}
1401 // Override get_Custom_tag {{{
1406 function get_custom_tag($name)
1408 $loaded = &$this->tags;
1410 if (!isset($loaded[$name])) {
1411 $this->prepend_op[] = $this->op_comment("Load tag {$name} definition");
1412 $this->prepend_op[] = $this->op_expr($this->expr_exec("Haanga::doInclude", $this->Expr_str(Haanga_Extensions::getInstance('Haanga_Tag')->getFilePath($name, FALSE))));
1413 $loaded[$name] = TRUE;
1416 $name = ucfirst($name);
1418 return "{$name}_Tag::main";
1420 // }}}
1422 // Override get_custom_filter {{{
1423 function get_custom_filter($name)
1425 $loaded = &$this->filters;
1427 if (!isset($loaded[$name])) {
1428 $this->prepend_op[] = $this->op_comment("Load filter {$name} definition");
1429 $this->prepend_op[] = $this->op_expr($this->expr_exec("Haanga::doInclude", $this->Expr_str(Haanga_Extensions::getInstance('Haanga_Filter')->getFilePath($name, FALSE))));
1430 $loaded[$name] = TRUE;
1433 $name = ucfirst($name);
1435 return "{$name}_Filter::main";
1437 // }}}
1442 * Local variables:
1443 * tab-width: 4
1444 * c-basic-offset: 4
1445 * End:
1446 * vim600: sw=4 ts=4 fdm=marker
1447 * vim<600: sw=4 ts=4