]> git.andy128k.dev Git - ipf-template.git/commitdiff
simplify directory layout
authorAndrey Kutejko <andy128k@gmail.com>
Tue, 30 Jul 2013 21:53:28 +0000 (00:53 +0300)
committerAndrey Kutejko <andy128k@gmail.com>
Tue, 30 Jul 2013 21:53:28 +0000 (00:53 +0300)
21 files changed:
ipf/exception.php [deleted file]
ipf/template.php [deleted file]
ipf/template/compiler.php [deleted file]
ipf/template/context.php [deleted file]
ipf/template/contextvars.php [deleted file]
ipf/template/environment.php [deleted file]
ipf/template/environment/filesystem.php [deleted file]
ipf/template/file.php [deleted file]
ipf/template/modifier.php [deleted file]
ipf/template/safestring.php [deleted file]
ipf/template/string.php [deleted file]
ipf/template/tag.php [deleted file]
lib/compiler.php [new file with mode: 0644]
lib/context.php [new file with mode: 0644]
lib/environment.php [new file with mode: 0644]
lib/environment_filesystem.php [new file with mode: 0644]
lib/exception.php [new file with mode: 0644]
lib/modifier.php [new file with mode: 0644]
lib/safestring.php [new file with mode: 0644]
lib/tag.php [new file with mode: 0644]
lib/template.php [new file with mode: 0644]

diff --git a/ipf/exception.php b/ipf/exception.php
deleted file mode 100644 (file)
index 2aa9755..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class IPF_Template_Exception extends Exception
-{
-}
-
diff --git a/ipf/template.php b/ipf/template.php
deleted file mode 100644 (file)
index 0c40fee..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-abstract class IPF_Template
-{
-    protected $environment;
-
-    public function __construct(IPF_Template_Environment $environment)
-    {
-        $this->environment = $environment;
-    }
-
-    abstract public function __toString();
-
-    abstract protected function content();
-
-    public function compile()
-    {
-        $compiler = new IPF_Template_Compiler($this->content(), $this->environment);
-        return $compiler->getCompiledTemplate();
-    }
-
-    public function render($c=null)
-    {
-        $compiled_template = $this->environment->getCompiledTemplateName($this);
-        ob_start();
-        $t = $c;
-        try {
-            include $compiled_template;
-        } catch (Exception $e) {
-            ob_clean();
-            throw $e;
-        }
-        $a = ob_get_contents();
-        ob_end_clean();
-        return $a;
-    }
-}
-
diff --git a/ipf/template/compiler.php b/ipf/template/compiler.php
deleted file mode 100644 (file)
index 003e1c2..0000000
+++ /dev/null
@@ -1,440 +0,0 @@
-<?php
-
-class IPF_Template_Compiler
-{
-    private static $allowedInVar = null,
-                   $allowedInExpr = null,
-                   $allowedAssign = null,
-                   $allowedForeach = null;
-
-    public static function init()
-    {
-        $vartype = array(T_CHARACTER, T_CONSTANT_ENCAPSED_STRING, T_DNUMBER,
-            T_ENCAPSED_AND_WHITESPACE, T_LNUMBER, T_OBJECT_OPERATOR, T_STRING,
-            T_WHITESPACE, T_ARRAY, T_VARIABLE);
-
-        $assignOp = array(T_AND_EQUAL, T_DIV_EQUAL, T_MINUS_EQUAL, T_MOD_EQUAL,
-            T_MUL_EQUAL, T_OR_EQUAL, T_PLUS_EQUAL, T_PLUS_EQUAL, T_SL_EQUAL,
-            T_SR_EQUAL, T_XOR_EQUAL);
-
-        $op = array(T_BOOLEAN_AND, T_BOOLEAN_OR, T_EMPTY, T_INC, T_ISSET,
-            T_IS_EQUAL, T_IS_GREATER_OR_EQUAL, T_IS_IDENTICAL, T_IS_NOT_EQUAL,
-            T_IS_NOT_IDENTICAL, T_IS_SMALLER_OR_EQUAL, T_LOGICAL_AND,
-            T_LOGICAL_OR, T_LOGICAL_XOR, T_SR, T_SL, T_DOUBLE_ARROW);
-
-        self::$allowedInVar   = array_merge($vartype, $op);
-        self::$allowedInExpr  = array_merge($vartype, $op);
-        self::$allowedAssign  = array_merge($vartype, $op, $assignOp);
-        self::$allowedForeach = array_merge(self::$allowedInExpr, array(T_AS));
-    }
-
-    protected $_literals;
-
-    protected $_blockStack = array();
-
-    protected $_transStack = array();
-    protected $_transPlural = false;
-
-    private $environment;
-
-    public $templateContent = '';
-
-    public $_extendBlocks = array();
-
-    function __construct($templateContent, $environment)
-    {
-        $this->templateContent = $templateContent;
-        $this->environment = $environment;
-    }
-
-    private function compile()
-    {
-        $this->compileBlocks();
-        $tplcontent = $this->templateContent;
-        $tplcontent = preg_replace('!{\*(.*?)\*}!s', '', $tplcontent);
-        $tplcontent = preg_replace('!<\?php(.*?)\?>!s', '', $tplcontent);
-        preg_match_all('!{literal}(.*?){/literal}!s', $tplcontent, $_match);
-        $this->_literals = $_match[1];
-        $tplcontent = preg_replace("!{literal}(.*?){/literal}!s", '{literal}', $tplcontent);
-        // Core regex to parse the template
-        $result = preg_replace_callback('/{((.).*?)}/s',
-                                        array($this, '_callback'),
-                                        $tplcontent);
-        if (count($this->_blockStack)) {
-            trigger_error(sprintf('End tag of a block missing: %s', end($this->_blockStack)), E_USER_ERROR);
-        }
-        return $result;
-    }
-
-    public function getCompiledTemplate()
-    {
-        $result = $this->compile();
-        $result = str_replace(array('?><?php', '<?php ?>', '<?php  ?>'), '', $result);
-        $result = str_replace("?>\n", "?>\n\n", $result);
-        return $result;
-    }
-
-    private static function toplevelBlocks($content)
-    {
-        preg_match_all("!{block\s*([^} \r\t\n]*)}|{/block}!", $content, $tags, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
-
-        $count = 0;
-        $result = array();
-        foreach ($tags as $tag) {
-            $text = $tag[0][0];
-
-            if (substr($text, 0, 6) === '{block') {
-                if ($count == 0) {
-                    $result[] = array(
-                        'name' => $tag[1][0],
-                        'start' => $tag[1][1] + strlen($tag[1][0]) + 1,
-                    );
-                }
-                $count++;
-            } elseif (substr($text, 0, 7) === '{/block') {
-                $count--;
-                if ($count == 0) {
-                    $result[count($result)-1]['finish'] = $tag[0][1];
-                }
-            }
-        }
-
-        if ($count != 0)
-            throw new IPF_Template_Exception('Blocks are not nested properly.');
-
-        return $result;
-    }
-
-    function compileBlocks()
-    {
-        $tplcontent = $this->templateContent;
-        $extendedTemplate = '';
-        // Match extends on the first line of the template
-        if (preg_match("!{extends\s['\"](.*?)['\"]}!", $tplcontent, $_match)) {
-            $extendedTemplate = $_match[1];
-        }
-        // Get the blocks in the current template
-        $blocks = self::toplevelBlocks($this->templateContent);
-        $cnt = count($blocks);
-        // Compile the blocks
-        for ($i=0; $i<$cnt; $i++) {
-            $blockName = $blocks[$i]['name'];
-            if (!isset($this->_extendBlocks[$blockName]) or false !== strpos($this->_extendBlocks[$blockName], '~~{~~superblock~~}~~')) {
-                $compiler = clone($this);
-                $compiler->templateContent = substr($this->templateContent, $blocks[$i]['start'], $blocks[$i]['finish'] - $blocks[$i]['start']);
-                $_tmp = $compiler->compile();
-                if (!isset($this->_extendBlocks[$blockName])) {
-                    $this->_extendBlocks[$blockName] = $_tmp;
-                } else {
-                    $this->_extendBlocks[$blockName] = str_replace('~~{~~superblock~~}~~', $_tmp, $this->_extendBlocks[$blockName]);
-                }
-            }
-        }
-        if (strlen($extendedTemplate) > 0) {
-            // The template of interest is now the extended template
-            // as we are not in a base template
-            $this->templateContent = $this->environment->loadTemplateFile($extendedTemplate);
-            $this->compileBlocks(); //It will recurse to the base template.
-        } else {
-            // Replace the current blocks by a place holder
-            if ($cnt) {
-                $this->templateContent = preg_replace("!{block\s(\S+?)}(.*?){/block}!s", "{block $1}", $tplcontent, -1);
-            }
-        }
-    }
-
-    private function _callback($matches)
-    {
-        list(,$tag, $firstcar) = $matches;
-        if (!preg_match('/^\$|[\'"]|[a-zA-Z\/]$/', $firstcar)) {
-            trigger_error(sprintf('Invalid tag syntax: %s', $tag), E_USER_ERROR);
-            return '';
-        }
-        if (in_array($firstcar, array('$', '\'', '"'))) {
-            if ('blocktrans' !== end($this->_blockStack)) {
-                return '<?php echo IPF_Template_SafeString::value('.$this->_parseVariable($tag).'); ?>';
-            } else {
-                $tok = explode('|', $tag);
-                $this->_transStack[substr($tok[0], 1)] = $this->_parseVariable($tag);
-                return '%%'.substr($tok[0], 1).'%%';
-            }
-        } else {
-            if (!preg_match('/^(\/?[a-zA-Z0-9_]+)(?:(?:\s+(.*))|(?:\((.*)\)))?$/', $tag, $m)) {
-                trigger_error(sprintf('Invalid function syntax: %s', $tag), E_USER_ERROR);
-                return '';
-            }
-            if (count($m) == 4){
-                $m[2] = $m[3];
-            }
-            if (!isset($m[2])) $m[2] = '';
-            if($m[1] == 'ldelim') return '{';
-            if($m[1] == 'rdelim') return '}';
-            if ($m[1] != 'include') {
-                return '<?php '.$this->_parseFunction($m[1], $m[2]).'?>';
-            } else {
-                return $this->_parseFunction($m[1], $m[2]);
-            }
-        }
-    }
-
-    private function _parseVariable($expr)
-    {
-        $tok = explode('|', $expr);
-        $res = $this->_parseFinal(array_shift($tok), self::$allowedInVar);
-        foreach ($tok as $modifier) {
-            if (!preg_match('/^(\w+)(?:\:(.*))?$/', $modifier, $m)) {
-                trigger_error(sprintf('Invalid modifier syntax: (%s) %s', $expr, $modifier), E_USER_ERROR);
-                return '';
-            }
-
-            if ($this->environment->hasModifier($m[1])) {
-                trigger_error(sprintf('Unknown modifier: (%s) %s', $expr, $m[1]), E_USER_ERROR);
-                return '';
-            }
-
-            $mod = $this->environment->getModifier($m[1]);
-
-            if (isset($m[2])) {
-                $res = $mod.'('.$res.','.$m[2].')';
-            } else {
-                $res = $mod.'('.$res.')';
-            }
-        }
-        return $res;
-    }
-
-    private function _parseFunction($name, $args)
-    {
-        switch ($name) {
-        case 'if':
-            $res = 'if ('.$this->_parseFinal($args, self::$allowedInExpr).'): ';
-            $this->_blockStack[] = 'if';
-            break;
-        case 'else':
-            if (end($this->_blockStack) != 'if') {
-                trigger_error(sprintf('End tag of a block missing: %s', end($this->_blockStack)), E_USER_ERROR);
-            }
-            $res = 'else: ';
-            break;
-        case 'elseif':
-            if (end($this->_blockStack) != 'if') {
-                trigger_error(sprintf('End tag of a block missing: %s', end($this->_blockStack)), E_USER_ERROR);
-            }
-            $res = 'elseif('.$this->_parseFinal($args, self::$allowedInExpr).'):';
-            break;
-        case 'foreach':
-            $tokens = $this->tokenize($args, self::$allowedForeach, array(';'));
-            $asFound = false;
-            $scopeVars = array('foreach_counter0', 'foreach_counter', 'foreach_first');
-            foreach ($tokens as $token) {
-                if (!is_array($token))
-                    continue;
-                if ($asFound) {
-                    if ($token[0] == T_VARIABLE)
-                        $scopeVars[] = substr($token[1], 1);
-                } else {
-                    if ($token[0] == T_AS)
-                        $asFound = true;
-                }
-            }
-            $res =
-                '$t->push(\'' . implode('\', \'', $scopeVars) . '\'); ' .
-                '$t->_vars[\'foreach_counter0\'] = 0;' .
-                '$t->_vars[\'foreach_counter\'] = 1;' .
-                '$t->_vars[\'foreach_first\'] = true;' .
-                'foreach ('.$this->compileExpression($tokens).'): ';
-            $this->_blockStack[] = 'foreach';
-            break;
-        case 'while':
-            $res = 'while('.$this->_parseFinal($args, self::$allowedInExpr).'):';
-            $this->_blockStack[] = 'while';
-            break;
-        case '/foreach':
-            if(end($this->_blockStack) != 'foreach'){
-                trigger_error(sprintf('End tag of a block missing: %s', end($this->_blockStack)), E_USER_ERROR);
-            }
-            array_pop($this->_blockStack);
-            $res =
-                '$t->_vars[\'foreach_counter0\'] = $t->_vars[\'foreach_counter0\'] + 1;' .
-                '$t->_vars[\'foreach_counter\'] = $t->_vars[\'foreach_counter\'] + 1;' .
-                '$t->_vars[\'foreach_first\'] = false;' .
-                'endforeach; ' .
-                '$t->pop(); ';
-            break;
-        case '/if':
-        case '/while':
-            $short = substr($name,1);
-            if(end($this->_blockStack) != $short){
-                trigger_error(sprintf('End tag of a block missing: %s', end($this->_blockStack)), E_USER_ERROR);
-            }
-            array_pop($this->_blockStack);
-            $res = 'end'.$short.'; ';
-            break;
-        case 'assign':
-            $res = $this->_parseFinal($args, self::$allowedAssign).'; ';
-            break;
-        case 'literal':
-            if (count($this->_literals)) {
-                $res = '?>'.array_shift($this->_literals).'<?php ';
-            } else {
-                trigger_error('End tag of a block missing: literal', E_USER_ERROR);
-            }
-            break;
-        case '/literal':
-            trigger_error('Start tag of a block missing: literal', E_USER_ERROR);
-            break;
-        case 'block':
-            if (isset($this->_extendBlocks[$args]))
-                $res = '?>'.$this->_extendBlocks[$args].'<?php ';
-            else
-                $res = '';
-            break;
-        case 'superblock':
-            $res = '?>~~{~~superblock~~}~~<?php ';
-            break;
-        case 'trans':
-            $argfct = $this->_parseFinal($args, self::$allowedAssign);
-            $res = 'echo(__('.$argfct.'));';
-            break;
-        case 'blocktrans':
-            $this->_blockStack[] = 'blocktrans';
-            $res = '';
-            $this->_transStack = array();
-            if ($args) {
-                $this->_transPlural = true;
-                $_args = $this->_parseFinal($args, self::$allowedAssign, array(';', '[', ']'));
-                $res .= '$_b_t_c='.trim($_args).'; ';
-            }
-            $res .= 'ob_start(); ';
-            break;
-        case '/blocktrans':
-            $short = substr($name,1);
-            if(end($this->_blockStack) != $short){
-                trigger_error(sprintf('End tag of a block missing: %s', end($this->_blockStack)), E_USER_ERROR);
-            }
-            $res = '';
-            if ($this->_transPlural) {
-                $res .= '$_b_t_p=ob_get_contents(); ob_end_clean(); echo(';
-                $res .= 'IPF_Translation::sprintf(_n($_b_t_s, $_b_t_p, $_b_t_c), array(';
-                $_tmp = array();
-                foreach ($this->_transStack as $key=>$_trans) {
-                    $_tmp[] = '\''.addslashes($key).'\' => IPF_Template_SafeString::value('.$_trans.')';
-                }
-                $res .= implode(', ', $_tmp);
-                unset($_trans, $_tmp);
-                $res .= ')));';
-                $this->_transStack = array();
-            } else {
-                $res .= '$_b_t_s=ob_get_contents(); ob_end_clean(); ';
-                if (count($this->_transStack) == 0) {
-                    $res .= 'echo(__($_b_t_s)); ';
-                } else {
-                    $res .= 'echo(IPF_Translation::sprintf(__($_b_t_s), array(';
-                    $_tmp = array();
-                    foreach ($this->_transStack as $key=>$_trans) {
-                        $_tmp[] = '\''.addslashes($key).'\' => IPF_Template_SafeString::value('.$_trans.')';
-                    }
-                    $res .= implode(', ', $_tmp);
-                    unset($_trans, $_tmp);
-                    $res .= '))); ';
-                    $this->_transStack = array();
-                }
-            }
-            $this->_transPlural = false;
-            array_pop($this->_blockStack);
-            break;
-        case 'plural':
-            $res = '$_b_t_s=ob_get_contents(); ob_end_clean(); ob_start(); ';
-            break;
-        case 'include':
-            $argfct = preg_replace('!^[\'"](.*)[\'"]$!', '$1', $args);
-            $includedTemplateContent = $this->environment->loadTemplateFile($argfct);
-            $_comp = new IPF_Template_Compiler($includedTemplateContent, $this->environment);
-            $res = $_comp->compile();
-            break;
-        default:
-            $_start = true;
-            $oname = $name;
-            if (substr($name, 0, 1) == '/') {
-                $_start = false;
-                $name = substr($name, 1);
-            }
-            // Here we should allow custom blocks.
-
-            // Here we start the template tag calls at the template tag
-            // {tag ...} is not a block, so it must be a function.
-            if (!$this->environment->isTagAllowed($name)) {
-                trigger_error(sprintf('The function tag "%s" is not allowed.', $name), E_USER_ERROR);
-            }
-            // $argfct is a string that can be copy/pasted in the PHP code
-            // but we need the array of args.
-            $argfct = $this->_parseFinal($args, self::$allowedAssign);
-
-            $res = '$_extra_tag = new '.$this->environment->getTag($name).'($t);';
-            if ($_start) {
-                $res .= '$_extra_tag->start('.$argfct.'); ';
-            } else {
-                $res .= '$_extra_tag->end('.$argfct.'); ';
-            }
-        }
-        return $res;
-    }
-
-    private function _parseFinal($string, &$allowed, $exceptchar=array(';'))
-    {
-        $tokens = $this->tokenize($string, $allowed, $exceptchar);
-        return $this->compileExpression($tokens);
-    }
-
-    private function tokenize($string, $allowed, $exceptchar)
-    {
-        $tokens = token_get_all('<?php '.$string.'?>');
-        $result = array();
-        foreach ($tokens as $tok) {
-            if (is_array($tok)) {
-                list($type, $str) = $tok;
-                if ($type == T_OPEN_TAG || $type == T_CLOSE_TAG) {
-                    // skip
-                } elseif (in_array($type, $allowed)) {
-                    $result[] = $tok;
-                } else {
-                    throw new IPF_Template_Exception(sprintf('Invalid syntax: (%s) %s.', $string, $str));
-                }
-            } else {
-                if (in_array($tok, $exceptchar)) {
-                    trigger_error(sprintf('Invalid character: (%s) %s.', $string, $tok), E_USER_ERROR);
-                } else {
-                    $result[] = $tok;
-                }
-            }
-        }
-        return $result;
-    }
-
-    private function compileExpression($tokens)
-    {
-        $result = '';
-        foreach ($tokens as $tok) {
-            if (is_array($tok)) {
-                list($type, $str) = $tok;
-                if ($type == T_VARIABLE) {
-                    $result .= '$t->_vars[\''.substr($str, 1).'\']';
-                } else {
-                    $result .= $str;
-                }
-            } else {
-                if ($tok == '.') {
-                    $result .= '->';
-                } elseif ($tok == '~') {
-                    $result .= '.';
-                } else {
-                    $result .= $tok;
-                }
-            }
-        }
-        return $result;
-    }
-}
-
-IPF_Template_Compiler::init();
-
diff --git a/ipf/template/context.php b/ipf/template/context.php
deleted file mode 100644 (file)
index c6fd6c0..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-class IPF_Template_Context
-{
-    public $_vars;
-    private $stack = array();
-
-    function __construct($vars=array())
-    {
-        $this->_vars = new IPF_Template_ContextVars($vars);
-    }
-
-    function get($var)
-    {
-        if (isset($this->_vars[$var])) {
-            return $this->_vars[$var];
-        }
-        return '';
-    }
-
-    function set($var, $value)
-    {
-        $this->_vars[$var] = $value;
-    }
-
-    public function push()
-    {
-        $vars = func_get_args();
-        $frame = array();
-        foreach ($vars as $var) {
-            $frame[$var] = $this->get($var);
-        }
-        $this->stack[] = $frame;
-    }
-
-    public function pop()
-    {
-        $frame = array_pop($this->stack);
-        foreach ($frame as $var => $val)
-            $this->set($var, $val);
-    }
-}
-
diff --git a/ipf/template/contextvars.php b/ipf/template/contextvars.php
deleted file mode 100644 (file)
index 637c12e..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-class IPF_Template_ContextVars extends ArrayObject
-{
-    function offsetGet($prop)
-    {
-        if (!$this->offsetExists($prop)) {
-            return '';
-        }
-        return parent::offsetGet($prop);
-    }
-
-    function __get($prop)
-    {
-        if (isset($this->$prop)) {
-            return $this->$prop;
-        } else {
-            return $this->offsetGet($prop);
-        }
-    }
-
-    function __toString()
-    {
-        return var_export($this, true);
-    }
-}
diff --git a/ipf/template/environment.php b/ipf/template/environment.php
deleted file mode 100644 (file)
index eb7834f..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-
-abstract class IPF_Template_Environment
-{
-    abstract public function loadTemplateFile($filename);
-
-    abstract public function getCompiledTemplateName($template);
-
-    // Dictionary of allowed tags (tag name => class)
-    public $tags = array();
-
-    public function isTagAllowed($name)
-    {
-        return isset($this->tags[$name]);
-    }
-
-    public function getTag($name)
-    {
-        if (isset($this->tags[$name]))
-            return $this->tags[$name];
-        else
-            throw new IPF_Template_Exception('Tag '.$name.' is not defined.');
-    }
-
-    // Dictionary of modifiers (modifier name => function)
-    public $modifiers = array(
-        'upper'       => 'strtoupper',
-        'lower'       => 'strtolower',
-        'escxml'      => 'htmlspecialchars',
-        'escape'      => 'IPF_Template_Modifier::escape',
-        'strip_tags'  => 'strip_tags',
-        'escurl'      => 'rawurlencode',
-        'capitalize'  => 'ucwords',
-        'debug'       => 'print_r', // Not var_export because of recursive issues.
-        'fulldebug'   => 'var_export',
-        'count'       => 'count',
-        'nl2br'       => 'nl2br',
-        'trim'        => 'trim',
-        'unsafe'      => 'IPF_Template_SafeString::markSafe',
-        'safe'        => 'IPF_Template_SafeString::markSafe',
-        'date'        => 'IPF_Template_Modifier::dateFormat',
-        'time'        => 'IPF_Template_Modifier::timeFormat',
-        'floatformat' => 'IPF_Template_Modifier::floatFormat',
-        'limit_words' => 'IPF_Template_Modifier::limitWords',
-        'limit_chars' => 'IPF_Template_Modifier::limitCharacters',
-    );
-
-    public function hasModifier($name)
-    {
-        return isset($this->modifiers[$name]);
-    }
-
-    public function getModifier($name)
-    {
-        if (isset($this->modifiers[$name]))
-            return $this->modifiers[$name];
-        else
-            throw new IPF_Template_Exception('Modifier '.$name.' is not defined.');
-    }
-}
-
diff --git a/ipf/template/environment/filesystem.php b/ipf/template/environment/filesystem.php
deleted file mode 100644 (file)
index 2d4f4b8..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-
-class IPF_Template_Environment_FileSystem extends IPF_Template_Environment
-{
-    public $folders = array();
-    public $cache = '/tmp';
-    public $debug = false;
-
-    public function loadTemplateFile($filename)
-    {
-        // FIXME: Very small security check, could be better.
-        if (strpos($filename, '..') !== false) {
-            throw new IPF_Template_Exception(sprintf('Template file contains invalid characters: %s', $filename));
-        }
-        foreach ($this->folders as $folder) {
-            if (file_exists($folder.'/'.$filename)) {
-                return file_get_contents($folder.'/'.$filename);
-            }
-        }
-        throw new IPF_Template_Exception(sprintf('Template file not found: %s', $filename));
-    }
-
-    public function getCompiledTemplateName($template)
-    {
-        $_tmp = var_export($this->folders, true);
-        $filename = $this->cache.'/IPF_Template-'.md5($_tmp.(string)$template).'.phps';
-        if ($this->debug or !file_exists($filename)) {
-            $this->write($filename, $template->compile());
-        }
-        return $filename;
-    }
-
-    private function write($filename, $content)
-    {
-        $fp = @fopen($filename, 'a');
-        if ($fp !== false) {
-            flock($fp, LOCK_EX);
-            ftruncate($fp, 0);
-            rewind($fp);
-            fwrite($fp, $content, strlen($content));
-            flock($fp, LOCK_UN);
-            fclose($fp);
-            @chmod($filename, 0777);
-            return true;
-        } else {
-            throw new IPF_Template_Exception(sprintf('Cannot write the compiled template: %s', $filename));
-        }
-        return false;
-    }
-}
-
diff --git a/ipf/template/file.php b/ipf/template/file.php
deleted file mode 100644 (file)
index 2c813eb..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-class IPF_Template_File extends IPF_Template
-{
-    private $tpl;
-
-    function __construct($template, IPF_Template_Environment $environment)
-    {
-        parent::__construct($environment);
-        $this->tpl = $template;
-    }
-
-    public function __toString()
-    {
-        return $this->tpl;
-    }
-
-    protected function content()
-    {
-        return $this->environment->loadTemplateFile($this->tpl);
-    }
-}
-
diff --git a/ipf/template/modifier.php b/ipf/template/modifier.php
deleted file mode 100644 (file)
index 0c837da..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-<?php
-
-final class IPF_Template_Modifier
-{
-    public static function escape($string)
-    {
-        return htmlspecialchars((string)$string, ENT_COMPAT, 'UTF-8');
-    }
-
-    public static function dateFormat($date, $format='%b %e, %Y')
-    {
-        if (substr(PHP_OS,0,3) == 'WIN') {
-            $_win_from = array ('%e',  '%T',       '%D');
-            $_win_to   = array ('%#d', '%H:%M:%S', '%m/%d/%y');
-            $format    = str_replace($_win_from, $_win_to, $format);
-        }
-        $date = date('Y-m-d H:i:s', strtotime($date.' GMT'));
-        return strftime($format, strtotime($date));
-    }
-
-    public static function timeFormat($time, $format='Y-m-d H:i:s')
-    {
-        return date($format, $time);
-    }
-
-    public static function floatFormat($number, $decimals=2, $dec_point='.', $thousands_sep=',')
-    {
-        return number_format($number, $decimals, $dec_point, $thousands_sep);
-    }
-
-    /**
-     * Word Limiter
-     *
-     * Limits a string to X number of words.
-     *
-     * @param  string
-     * @param  integer
-     * @param  string  the end character. Usually an ellipsis
-     * @return string
-     */
-    public static function limitWords($str, $limit=100, $end_char='&#8230;')
-    {
-        if (trim($str) == '')
-            return $str;
-        preg_match('/^\s*+(?:\S++\s*+){1,'.(int) $limit.'}/', $str, $matches);
-        if (strlen($str) == strlen($matches[0]))
-            $end_char = '';
-        return rtrim($matches[0]).$end_char;
-    }
-
-    /**
-     * Character Limiter
-     *
-     * Limits the string based on the character count.  Preserves complete words
-     * so the character count may not be exactly as specified.
-     *
-     * @param  string
-     * @param  integer
-     * @param  string  the end character. Usually an ellipsis
-     * @return string
-     */
-    function limitCharacters($str, $n=500, $end_char='&#8230;')
-    {
-        if (strlen($str) < $n)
-            return $str;
-
-        $str = preg_replace("/\s+/", ' ', str_replace(array("\r\n", "\r", "\n"), ' ', $str));
-
-        if (strlen($str) <= $n)
-            return $str;
-
-        $out = "";
-        foreach (explode(' ', trim($str)) as $val) {
-            $out .= $val.' ';
-            if (strlen($out) >= $n) {
-                $out = trim($out);
-                return (strlen($out) == strlen($str)) ? $out : $out.$end_char;
-            }
-        }
-    }
-}
-
diff --git a/ipf/template/safestring.php b/ipf/template/safestring.php
deleted file mode 100644 (file)
index 0bd145b..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-class IPF_Template_SafeString
-{
-    public $value = '';
-
-    public static function value($mixed, $safe=false)
-    {
-        if (is_object($mixed) and 'IPF_Template_SafeString' == get_class($mixed))
-            return $mixed->value;
-        if ($safe)
-            return $mixed;
-        return htmlspecialchars($mixed, ENT_COMPAT, 'UTF-8');
-    }
-
-    function __construct($mixed, $safe=false)
-    {
-        $this->value = self::value($mixed, $safe);
-    }
-
-    function __toString()
-    {
-        return $this->value;
-    }
-
-    public static function markSafe($string)
-    {
-        return new IPF_Template_SafeString($string, true);
-    }
-}
-
diff --git a/ipf/template/string.php b/ipf/template/string.php
deleted file mode 100644 (file)
index 835e55a..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-class IPF_Template_String extends IPF_Template
-{
-    private $tpl;
-
-    function __construct($template, IPF_Template_Environment $environment)
-    {
-        parent::__construct($environment);
-        $this->tpl = $template;
-    }
-
-    public function __toString()
-    {
-        return $this->tpl;
-    }
-
-    protected function content()
-    {
-        return $this->tpl;
-    }
-}
-
diff --git a/ipf/template/tag.php b/ipf/template/tag.php
deleted file mode 100644 (file)
index 1a1ced2..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-class IPF_Template_Tag
-{
-    protected $context;
-
-    function __construct($context=null)
-    {
-        $this->context = $context;
-    }
-
-    // variable list of arguments
-    function start()
-    {
-    }
-
-    // variable list of arguments
-    function end()
-    {
-    }
-}
-
diff --git a/lib/compiler.php b/lib/compiler.php
new file mode 100644 (file)
index 0000000..003e1c2
--- /dev/null
@@ -0,0 +1,440 @@
+<?php
+
+class IPF_Template_Compiler
+{
+    private static $allowedInVar = null,
+                   $allowedInExpr = null,
+                   $allowedAssign = null,
+                   $allowedForeach = null;
+
+    public static function init()
+    {
+        $vartype = array(T_CHARACTER, T_CONSTANT_ENCAPSED_STRING, T_DNUMBER,
+            T_ENCAPSED_AND_WHITESPACE, T_LNUMBER, T_OBJECT_OPERATOR, T_STRING,
+            T_WHITESPACE, T_ARRAY, T_VARIABLE);
+
+        $assignOp = array(T_AND_EQUAL, T_DIV_EQUAL, T_MINUS_EQUAL, T_MOD_EQUAL,
+            T_MUL_EQUAL, T_OR_EQUAL, T_PLUS_EQUAL, T_PLUS_EQUAL, T_SL_EQUAL,
+            T_SR_EQUAL, T_XOR_EQUAL);
+
+        $op = array(T_BOOLEAN_AND, T_BOOLEAN_OR, T_EMPTY, T_INC, T_ISSET,
+            T_IS_EQUAL, T_IS_GREATER_OR_EQUAL, T_IS_IDENTICAL, T_IS_NOT_EQUAL,
+            T_IS_NOT_IDENTICAL, T_IS_SMALLER_OR_EQUAL, T_LOGICAL_AND,
+            T_LOGICAL_OR, T_LOGICAL_XOR, T_SR, T_SL, T_DOUBLE_ARROW);
+
+        self::$allowedInVar   = array_merge($vartype, $op);
+        self::$allowedInExpr  = array_merge($vartype, $op);
+        self::$allowedAssign  = array_merge($vartype, $op, $assignOp);
+        self::$allowedForeach = array_merge(self::$allowedInExpr, array(T_AS));
+    }
+
+    protected $_literals;
+
+    protected $_blockStack = array();
+
+    protected $_transStack = array();
+    protected $_transPlural = false;
+
+    private $environment;
+
+    public $templateContent = '';
+
+    public $_extendBlocks = array();
+
+    function __construct($templateContent, $environment)
+    {
+        $this->templateContent = $templateContent;
+        $this->environment = $environment;
+    }
+
+    private function compile()
+    {
+        $this->compileBlocks();
+        $tplcontent = $this->templateContent;
+        $tplcontent = preg_replace('!{\*(.*?)\*}!s', '', $tplcontent);
+        $tplcontent = preg_replace('!<\?php(.*?)\?>!s', '', $tplcontent);
+        preg_match_all('!{literal}(.*?){/literal}!s', $tplcontent, $_match);
+        $this->_literals = $_match[1];
+        $tplcontent = preg_replace("!{literal}(.*?){/literal}!s", '{literal}', $tplcontent);
+        // Core regex to parse the template
+        $result = preg_replace_callback('/{((.).*?)}/s',
+                                        array($this, '_callback'),
+                                        $tplcontent);
+        if (count($this->_blockStack)) {
+            trigger_error(sprintf('End tag of a block missing: %s', end($this->_blockStack)), E_USER_ERROR);
+        }
+        return $result;
+    }
+
+    public function getCompiledTemplate()
+    {
+        $result = $this->compile();
+        $result = str_replace(array('?><?php', '<?php ?>', '<?php  ?>'), '', $result);
+        $result = str_replace("?>\n", "?>\n\n", $result);
+        return $result;
+    }
+
+    private static function toplevelBlocks($content)
+    {
+        preg_match_all("!{block\s*([^} \r\t\n]*)}|{/block}!", $content, $tags, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
+
+        $count = 0;
+        $result = array();
+        foreach ($tags as $tag) {
+            $text = $tag[0][0];
+
+            if (substr($text, 0, 6) === '{block') {
+                if ($count == 0) {
+                    $result[] = array(
+                        'name' => $tag[1][0],
+                        'start' => $tag[1][1] + strlen($tag[1][0]) + 1,
+                    );
+                }
+                $count++;
+            } elseif (substr($text, 0, 7) === '{/block') {
+                $count--;
+                if ($count == 0) {
+                    $result[count($result)-1]['finish'] = $tag[0][1];
+                }
+            }
+        }
+
+        if ($count != 0)
+            throw new IPF_Template_Exception('Blocks are not nested properly.');
+
+        return $result;
+    }
+
+    function compileBlocks()
+    {
+        $tplcontent = $this->templateContent;
+        $extendedTemplate = '';
+        // Match extends on the first line of the template
+        if (preg_match("!{extends\s['\"](.*?)['\"]}!", $tplcontent, $_match)) {
+            $extendedTemplate = $_match[1];
+        }
+        // Get the blocks in the current template
+        $blocks = self::toplevelBlocks($this->templateContent);
+        $cnt = count($blocks);
+        // Compile the blocks
+        for ($i=0; $i<$cnt; $i++) {
+            $blockName = $blocks[$i]['name'];
+            if (!isset($this->_extendBlocks[$blockName]) or false !== strpos($this->_extendBlocks[$blockName], '~~{~~superblock~~}~~')) {
+                $compiler = clone($this);
+                $compiler->templateContent = substr($this->templateContent, $blocks[$i]['start'], $blocks[$i]['finish'] - $blocks[$i]['start']);
+                $_tmp = $compiler->compile();
+                if (!isset($this->_extendBlocks[$blockName])) {
+                    $this->_extendBlocks[$blockName] = $_tmp;
+                } else {
+                    $this->_extendBlocks[$blockName] = str_replace('~~{~~superblock~~}~~', $_tmp, $this->_extendBlocks[$blockName]);
+                }
+            }
+        }
+        if (strlen($extendedTemplate) > 0) {
+            // The template of interest is now the extended template
+            // as we are not in a base template
+            $this->templateContent = $this->environment->loadTemplateFile($extendedTemplate);
+            $this->compileBlocks(); //It will recurse to the base template.
+        } else {
+            // Replace the current blocks by a place holder
+            if ($cnt) {
+                $this->templateContent = preg_replace("!{block\s(\S+?)}(.*?){/block}!s", "{block $1}", $tplcontent, -1);
+            }
+        }
+    }
+
+    private function _callback($matches)
+    {
+        list(,$tag, $firstcar) = $matches;
+        if (!preg_match('/^\$|[\'"]|[a-zA-Z\/]$/', $firstcar)) {
+            trigger_error(sprintf('Invalid tag syntax: %s', $tag), E_USER_ERROR);
+            return '';
+        }
+        if (in_array($firstcar, array('$', '\'', '"'))) {
+            if ('blocktrans' !== end($this->_blockStack)) {
+                return '<?php echo IPF_Template_SafeString::value('.$this->_parseVariable($tag).'); ?>';
+            } else {
+                $tok = explode('|', $tag);
+                $this->_transStack[substr($tok[0], 1)] = $this->_parseVariable($tag);
+                return '%%'.substr($tok[0], 1).'%%';
+            }
+        } else {
+            if (!preg_match('/^(\/?[a-zA-Z0-9_]+)(?:(?:\s+(.*))|(?:\((.*)\)))?$/', $tag, $m)) {
+                trigger_error(sprintf('Invalid function syntax: %s', $tag), E_USER_ERROR);
+                return '';
+            }
+            if (count($m) == 4){
+                $m[2] = $m[3];
+            }
+            if (!isset($m[2])) $m[2] = '';
+            if($m[1] == 'ldelim') return '{';
+            if($m[1] == 'rdelim') return '}';
+            if ($m[1] != 'include') {
+                return '<?php '.$this->_parseFunction($m[1], $m[2]).'?>';
+            } else {
+                return $this->_parseFunction($m[1], $m[2]);
+            }
+        }
+    }
+
+    private function _parseVariable($expr)
+    {
+        $tok = explode('|', $expr);
+        $res = $this->_parseFinal(array_shift($tok), self::$allowedInVar);
+        foreach ($tok as $modifier) {
+            if (!preg_match('/^(\w+)(?:\:(.*))?$/', $modifier, $m)) {
+                trigger_error(sprintf('Invalid modifier syntax: (%s) %s', $expr, $modifier), E_USER_ERROR);
+                return '';
+            }
+
+            if ($this->environment->hasModifier($m[1])) {
+                trigger_error(sprintf('Unknown modifier: (%s) %s', $expr, $m[1]), E_USER_ERROR);
+                return '';
+            }
+
+            $mod = $this->environment->getModifier($m[1]);
+
+            if (isset($m[2])) {
+                $res = $mod.'('.$res.','.$m[2].')';
+            } else {
+                $res = $mod.'('.$res.')';
+            }
+        }
+        return $res;
+    }
+
+    private function _parseFunction($name, $args)
+    {
+        switch ($name) {
+        case 'if':
+            $res = 'if ('.$this->_parseFinal($args, self::$allowedInExpr).'): ';
+            $this->_blockStack[] = 'if';
+            break;
+        case 'else':
+            if (end($this->_blockStack) != 'if') {
+                trigger_error(sprintf('End tag of a block missing: %s', end($this->_blockStack)), E_USER_ERROR);
+            }
+            $res = 'else: ';
+            break;
+        case 'elseif':
+            if (end($this->_blockStack) != 'if') {
+                trigger_error(sprintf('End tag of a block missing: %s', end($this->_blockStack)), E_USER_ERROR);
+            }
+            $res = 'elseif('.$this->_parseFinal($args, self::$allowedInExpr).'):';
+            break;
+        case 'foreach':
+            $tokens = $this->tokenize($args, self::$allowedForeach, array(';'));
+            $asFound = false;
+            $scopeVars = array('foreach_counter0', 'foreach_counter', 'foreach_first');
+            foreach ($tokens as $token) {
+                if (!is_array($token))
+                    continue;
+                if ($asFound) {
+                    if ($token[0] == T_VARIABLE)
+                        $scopeVars[] = substr($token[1], 1);
+                } else {
+                    if ($token[0] == T_AS)
+                        $asFound = true;
+                }
+            }
+            $res =
+                '$t->push(\'' . implode('\', \'', $scopeVars) . '\'); ' .
+                '$t->_vars[\'foreach_counter0\'] = 0;' .
+                '$t->_vars[\'foreach_counter\'] = 1;' .
+                '$t->_vars[\'foreach_first\'] = true;' .
+                'foreach ('.$this->compileExpression($tokens).'): ';
+            $this->_blockStack[] = 'foreach';
+            break;
+        case 'while':
+            $res = 'while('.$this->_parseFinal($args, self::$allowedInExpr).'):';
+            $this->_blockStack[] = 'while';
+            break;
+        case '/foreach':
+            if(end($this->_blockStack) != 'foreach'){
+                trigger_error(sprintf('End tag of a block missing: %s', end($this->_blockStack)), E_USER_ERROR);
+            }
+            array_pop($this->_blockStack);
+            $res =
+                '$t->_vars[\'foreach_counter0\'] = $t->_vars[\'foreach_counter0\'] + 1;' .
+                '$t->_vars[\'foreach_counter\'] = $t->_vars[\'foreach_counter\'] + 1;' .
+                '$t->_vars[\'foreach_first\'] = false;' .
+                'endforeach; ' .
+                '$t->pop(); ';
+            break;
+        case '/if':
+        case '/while':
+            $short = substr($name,1);
+            if(end($this->_blockStack) != $short){
+                trigger_error(sprintf('End tag of a block missing: %s', end($this->_blockStack)), E_USER_ERROR);
+            }
+            array_pop($this->_blockStack);
+            $res = 'end'.$short.'; ';
+            break;
+        case 'assign':
+            $res = $this->_parseFinal($args, self::$allowedAssign).'; ';
+            break;
+        case 'literal':
+            if (count($this->_literals)) {
+                $res = '?>'.array_shift($this->_literals).'<?php ';
+            } else {
+                trigger_error('End tag of a block missing: literal', E_USER_ERROR);
+            }
+            break;
+        case '/literal':
+            trigger_error('Start tag of a block missing: literal', E_USER_ERROR);
+            break;
+        case 'block':
+            if (isset($this->_extendBlocks[$args]))
+                $res = '?>'.$this->_extendBlocks[$args].'<?php ';
+            else
+                $res = '';
+            break;
+        case 'superblock':
+            $res = '?>~~{~~superblock~~}~~<?php ';
+            break;
+        case 'trans':
+            $argfct = $this->_parseFinal($args, self::$allowedAssign);
+            $res = 'echo(__('.$argfct.'));';
+            break;
+        case 'blocktrans':
+            $this->_blockStack[] = 'blocktrans';
+            $res = '';
+            $this->_transStack = array();
+            if ($args) {
+                $this->_transPlural = true;
+                $_args = $this->_parseFinal($args, self::$allowedAssign, array(';', '[', ']'));
+                $res .= '$_b_t_c='.trim($_args).'; ';
+            }
+            $res .= 'ob_start(); ';
+            break;
+        case '/blocktrans':
+            $short = substr($name,1);
+            if(end($this->_blockStack) != $short){
+                trigger_error(sprintf('End tag of a block missing: %s', end($this->_blockStack)), E_USER_ERROR);
+            }
+            $res = '';
+            if ($this->_transPlural) {
+                $res .= '$_b_t_p=ob_get_contents(); ob_end_clean(); echo(';
+                $res .= 'IPF_Translation::sprintf(_n($_b_t_s, $_b_t_p, $_b_t_c), array(';
+                $_tmp = array();
+                foreach ($this->_transStack as $key=>$_trans) {
+                    $_tmp[] = '\''.addslashes($key).'\' => IPF_Template_SafeString::value('.$_trans.')';
+                }
+                $res .= implode(', ', $_tmp);
+                unset($_trans, $_tmp);
+                $res .= ')));';
+                $this->_transStack = array();
+            } else {
+                $res .= '$_b_t_s=ob_get_contents(); ob_end_clean(); ';
+                if (count($this->_transStack) == 0) {
+                    $res .= 'echo(__($_b_t_s)); ';
+                } else {
+                    $res .= 'echo(IPF_Translation::sprintf(__($_b_t_s), array(';
+                    $_tmp = array();
+                    foreach ($this->_transStack as $key=>$_trans) {
+                        $_tmp[] = '\''.addslashes($key).'\' => IPF_Template_SafeString::value('.$_trans.')';
+                    }
+                    $res .= implode(', ', $_tmp);
+                    unset($_trans, $_tmp);
+                    $res .= '))); ';
+                    $this->_transStack = array();
+                }
+            }
+            $this->_transPlural = false;
+            array_pop($this->_blockStack);
+            break;
+        case 'plural':
+            $res = '$_b_t_s=ob_get_contents(); ob_end_clean(); ob_start(); ';
+            break;
+        case 'include':
+            $argfct = preg_replace('!^[\'"](.*)[\'"]$!', '$1', $args);
+            $includedTemplateContent = $this->environment->loadTemplateFile($argfct);
+            $_comp = new IPF_Template_Compiler($includedTemplateContent, $this->environment);
+            $res = $_comp->compile();
+            break;
+        default:
+            $_start = true;
+            $oname = $name;
+            if (substr($name, 0, 1) == '/') {
+                $_start = false;
+                $name = substr($name, 1);
+            }
+            // Here we should allow custom blocks.
+
+            // Here we start the template tag calls at the template tag
+            // {tag ...} is not a block, so it must be a function.
+            if (!$this->environment->isTagAllowed($name)) {
+                trigger_error(sprintf('The function tag "%s" is not allowed.', $name), E_USER_ERROR);
+            }
+            // $argfct is a string that can be copy/pasted in the PHP code
+            // but we need the array of args.
+            $argfct = $this->_parseFinal($args, self::$allowedAssign);
+
+            $res = '$_extra_tag = new '.$this->environment->getTag($name).'($t);';
+            if ($_start) {
+                $res .= '$_extra_tag->start('.$argfct.'); ';
+            } else {
+                $res .= '$_extra_tag->end('.$argfct.'); ';
+            }
+        }
+        return $res;
+    }
+
+    private function _parseFinal($string, &$allowed, $exceptchar=array(';'))
+    {
+        $tokens = $this->tokenize($string, $allowed, $exceptchar);
+        return $this->compileExpression($tokens);
+    }
+
+    private function tokenize($string, $allowed, $exceptchar)
+    {
+        $tokens = token_get_all('<?php '.$string.'?>');
+        $result = array();
+        foreach ($tokens as $tok) {
+            if (is_array($tok)) {
+                list($type, $str) = $tok;
+                if ($type == T_OPEN_TAG || $type == T_CLOSE_TAG) {
+                    // skip
+                } elseif (in_array($type, $allowed)) {
+                    $result[] = $tok;
+                } else {
+                    throw new IPF_Template_Exception(sprintf('Invalid syntax: (%s) %s.', $string, $str));
+                }
+            } else {
+                if (in_array($tok, $exceptchar)) {
+                    trigger_error(sprintf('Invalid character: (%s) %s.', $string, $tok), E_USER_ERROR);
+                } else {
+                    $result[] = $tok;
+                }
+            }
+        }
+        return $result;
+    }
+
+    private function compileExpression($tokens)
+    {
+        $result = '';
+        foreach ($tokens as $tok) {
+            if (is_array($tok)) {
+                list($type, $str) = $tok;
+                if ($type == T_VARIABLE) {
+                    $result .= '$t->_vars[\''.substr($str, 1).'\']';
+                } else {
+                    $result .= $str;
+                }
+            } else {
+                if ($tok == '.') {
+                    $result .= '->';
+                } elseif ($tok == '~') {
+                    $result .= '.';
+                } else {
+                    $result .= $tok;
+                }
+            }
+        }
+        return $result;
+    }
+}
+
+IPF_Template_Compiler::init();
+
diff --git a/lib/context.php b/lib/context.php
new file mode 100644 (file)
index 0000000..09dc03c
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+class IPF_Template_Context
+{
+    public $_vars;
+    private $stack = array();
+
+    function __construct($vars=array())
+    {
+        $this->_vars = new IPF_Template_ContextVars($vars);
+    }
+
+    function get($var)
+    {
+        if (isset($this->_vars[$var])) {
+            return $this->_vars[$var];
+        }
+        return '';
+    }
+
+    function set($var, $value)
+    {
+        $this->_vars[$var] = $value;
+    }
+
+    public function push()
+    {
+        $vars = func_get_args();
+        $frame = array();
+        foreach ($vars as $var) {
+            $frame[$var] = $this->get($var);
+        }
+        $this->stack[] = $frame;
+    }
+
+    public function pop()
+    {
+        $frame = array_pop($this->stack);
+        foreach ($frame as $var => $val)
+            $this->set($var, $val);
+    }
+}
+
+class IPF_Template_ContextVars extends ArrayObject
+{
+    function offsetGet($prop)
+    {
+        if (!$this->offsetExists($prop)) {
+            return '';
+        }
+        return parent::offsetGet($prop);
+    }
+
+    function __get($prop)
+    {
+        if (isset($this->$prop)) {
+            return $this->$prop;
+        } else {
+            return $this->offsetGet($prop);
+        }
+    }
+
+    function __toString()
+    {
+        return var_export($this, true);
+    }
+}
+
diff --git a/lib/environment.php b/lib/environment.php
new file mode 100644 (file)
index 0000000..eb7834f
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+
+abstract class IPF_Template_Environment
+{
+    abstract public function loadTemplateFile($filename);
+
+    abstract public function getCompiledTemplateName($template);
+
+    // Dictionary of allowed tags (tag name => class)
+    public $tags = array();
+
+    public function isTagAllowed($name)
+    {
+        return isset($this->tags[$name]);
+    }
+
+    public function getTag($name)
+    {
+        if (isset($this->tags[$name]))
+            return $this->tags[$name];
+        else
+            throw new IPF_Template_Exception('Tag '.$name.' is not defined.');
+    }
+
+    // Dictionary of modifiers (modifier name => function)
+    public $modifiers = array(
+        'upper'       => 'strtoupper',
+        'lower'       => 'strtolower',
+        'escxml'      => 'htmlspecialchars',
+        'escape'      => 'IPF_Template_Modifier::escape',
+        'strip_tags'  => 'strip_tags',
+        'escurl'      => 'rawurlencode',
+        'capitalize'  => 'ucwords',
+        'debug'       => 'print_r', // Not var_export because of recursive issues.
+        'fulldebug'   => 'var_export',
+        'count'       => 'count',
+        'nl2br'       => 'nl2br',
+        'trim'        => 'trim',
+        'unsafe'      => 'IPF_Template_SafeString::markSafe',
+        'safe'        => 'IPF_Template_SafeString::markSafe',
+        'date'        => 'IPF_Template_Modifier::dateFormat',
+        'time'        => 'IPF_Template_Modifier::timeFormat',
+        'floatformat' => 'IPF_Template_Modifier::floatFormat',
+        'limit_words' => 'IPF_Template_Modifier::limitWords',
+        'limit_chars' => 'IPF_Template_Modifier::limitCharacters',
+    );
+
+    public function hasModifier($name)
+    {
+        return isset($this->modifiers[$name]);
+    }
+
+    public function getModifier($name)
+    {
+        if (isset($this->modifiers[$name]))
+            return $this->modifiers[$name];
+        else
+            throw new IPF_Template_Exception('Modifier '.$name.' is not defined.');
+    }
+}
+
diff --git a/lib/environment_filesystem.php b/lib/environment_filesystem.php
new file mode 100644 (file)
index 0000000..2d4f4b8
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+class IPF_Template_Environment_FileSystem extends IPF_Template_Environment
+{
+    public $folders = array();
+    public $cache = '/tmp';
+    public $debug = false;
+
+    public function loadTemplateFile($filename)
+    {
+        // FIXME: Very small security check, could be better.
+        if (strpos($filename, '..') !== false) {
+            throw new IPF_Template_Exception(sprintf('Template file contains invalid characters: %s', $filename));
+        }
+        foreach ($this->folders as $folder) {
+            if (file_exists($folder.'/'.$filename)) {
+                return file_get_contents($folder.'/'.$filename);
+            }
+        }
+        throw new IPF_Template_Exception(sprintf('Template file not found: %s', $filename));
+    }
+
+    public function getCompiledTemplateName($template)
+    {
+        $_tmp = var_export($this->folders, true);
+        $filename = $this->cache.'/IPF_Template-'.md5($_tmp.(string)$template).'.phps';
+        if ($this->debug or !file_exists($filename)) {
+            $this->write($filename, $template->compile());
+        }
+        return $filename;
+    }
+
+    private function write($filename, $content)
+    {
+        $fp = @fopen($filename, 'a');
+        if ($fp !== false) {
+            flock($fp, LOCK_EX);
+            ftruncate($fp, 0);
+            rewind($fp);
+            fwrite($fp, $content, strlen($content));
+            flock($fp, LOCK_UN);
+            fclose($fp);
+            @chmod($filename, 0777);
+            return true;
+        } else {
+            throw new IPF_Template_Exception(sprintf('Cannot write the compiled template: %s', $filename));
+        }
+        return false;
+    }
+}
+
diff --git a/lib/exception.php b/lib/exception.php
new file mode 100644 (file)
index 0000000..2aa9755
--- /dev/null
@@ -0,0 +1,6 @@
+<?php
+
+class IPF_Template_Exception extends Exception
+{
+}
+
diff --git a/lib/modifier.php b/lib/modifier.php
new file mode 100644 (file)
index 0000000..0c837da
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+final class IPF_Template_Modifier
+{
+    public static function escape($string)
+    {
+        return htmlspecialchars((string)$string, ENT_COMPAT, 'UTF-8');
+    }
+
+    public static function dateFormat($date, $format='%b %e, %Y')
+    {
+        if (substr(PHP_OS,0,3) == 'WIN') {
+            $_win_from = array ('%e',  '%T',       '%D');
+            $_win_to   = array ('%#d', '%H:%M:%S', '%m/%d/%y');
+            $format    = str_replace($_win_from, $_win_to, $format);
+        }
+        $date = date('Y-m-d H:i:s', strtotime($date.' GMT'));
+        return strftime($format, strtotime($date));
+    }
+
+    public static function timeFormat($time, $format='Y-m-d H:i:s')
+    {
+        return date($format, $time);
+    }
+
+    public static function floatFormat($number, $decimals=2, $dec_point='.', $thousands_sep=',')
+    {
+        return number_format($number, $decimals, $dec_point, $thousands_sep);
+    }
+
+    /**
+     * Word Limiter
+     *
+     * Limits a string to X number of words.
+     *
+     * @param  string
+     * @param  integer
+     * @param  string  the end character. Usually an ellipsis
+     * @return string
+     */
+    public static function limitWords($str, $limit=100, $end_char='&#8230;')
+    {
+        if (trim($str) == '')
+            return $str;
+        preg_match('/^\s*+(?:\S++\s*+){1,'.(int) $limit.'}/', $str, $matches);
+        if (strlen($str) == strlen($matches[0]))
+            $end_char = '';
+        return rtrim($matches[0]).$end_char;
+    }
+
+    /**
+     * Character Limiter
+     *
+     * Limits the string based on the character count.  Preserves complete words
+     * so the character count may not be exactly as specified.
+     *
+     * @param  string
+     * @param  integer
+     * @param  string  the end character. Usually an ellipsis
+     * @return string
+     */
+    function limitCharacters($str, $n=500, $end_char='&#8230;')
+    {
+        if (strlen($str) < $n)
+            return $str;
+
+        $str = preg_replace("/\s+/", ' ', str_replace(array("\r\n", "\r", "\n"), ' ', $str));
+
+        if (strlen($str) <= $n)
+            return $str;
+
+        $out = "";
+        foreach (explode(' ', trim($str)) as $val) {
+            $out .= $val.' ';
+            if (strlen($out) >= $n) {
+                $out = trim($out);
+                return (strlen($out) == strlen($str)) ? $out : $out.$end_char;
+            }
+        }
+    }
+}
+
diff --git a/lib/safestring.php b/lib/safestring.php
new file mode 100644 (file)
index 0000000..0bd145b
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+class IPF_Template_SafeString
+{
+    public $value = '';
+
+    public static function value($mixed, $safe=false)
+    {
+        if (is_object($mixed) and 'IPF_Template_SafeString' == get_class($mixed))
+            return $mixed->value;
+        if ($safe)
+            return $mixed;
+        return htmlspecialchars($mixed, ENT_COMPAT, 'UTF-8');
+    }
+
+    function __construct($mixed, $safe=false)
+    {
+        $this->value = self::value($mixed, $safe);
+    }
+
+    function __toString()
+    {
+        return $this->value;
+    }
+
+    public static function markSafe($string)
+    {
+        return new IPF_Template_SafeString($string, true);
+    }
+}
+
diff --git a/lib/tag.php b/lib/tag.php
new file mode 100644 (file)
index 0000000..1a1ced2
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+class IPF_Template_Tag
+{
+    protected $context;
+
+    function __construct($context=null)
+    {
+        $this->context = $context;
+    }
+
+    // variable list of arguments
+    function start()
+    {
+    }
+
+    // variable list of arguments
+    function end()
+    {
+    }
+}
+
diff --git a/lib/template.php b/lib/template.php
new file mode 100644 (file)
index 0000000..269d435
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+
+abstract class IPF_Template
+{
+    protected $environment;
+
+    public function __construct(IPF_Template_Environment $environment)
+    {
+        $this->environment = $environment;
+    }
+
+    abstract public function __toString();
+
+    abstract protected function content();
+
+    public function compile()
+    {
+        $compiler = new IPF_Template_Compiler($this->content(), $this->environment);
+        return $compiler->getCompiledTemplate();
+    }
+
+    public function render($c=null)
+    {
+        $compiled_template = $this->environment->getCompiledTemplateName($this);
+        ob_start();
+        $t = $c;
+        try {
+            include $compiled_template;
+        } catch (Exception $e) {
+            ob_clean();
+            throw $e;
+        }
+        $a = ob_get_contents();
+        ob_end_clean();
+        return $a;
+    }
+}
+
+class IPF_Template_File extends IPF_Template
+{
+    private $filename;
+
+    function __construct($filename, IPF_Template_Environment $environment)
+    {
+        parent::__construct($environment);
+        $this->filename = $filename;
+    }
+
+    public function __toString()
+    {
+        return $this->filename;
+    }
+
+    protected function content()
+    {
+        return $this->environment->loadTemplateFile($this->filename);
+    }
+}
+
+class IPF_Template_String extends IPF_Template
+{
+    private $template;
+
+    function __construct($template, IPF_Template_Environment $environment)
+    {
+        parent::__construct($environment);
+        $this->template = $template;
+    }
+
+    public function __toString()
+    {
+        return $this->template;
+    }
+
+    protected function content()
+    {
+        return $this->template;
+    }
+}
+