cvs: pear /HTML_Template_Sigma Sigma.php package.xml /HTML_Template_Sigma/docs readme.txt /HTML_Template_Sigma/tests Console_TestListener.php
Sigma_api_testcase.php Sigma_cache_testcase.php Sigma_usage_testcase.php test.php /HTML_Template_Sigma/tests/templates __include.html addblock.html
blockiteration.html blocks.html globals.html include.html loadtemplatefile.html replaceblock.html
| From: | Alexey Borzov | Date: | Fri, 04 Apr 2003 14:02:40 +0000 |
| Subject: | cvs: pear /HTML_Template_Sigma Sigma.php package.xml /HTML_Template_Sigma/docs readme.txt /HTML_Template_Sigma/tests Console_TestListener.php Sigma_api_testcase.php Sigma_cache_testcase.php Sigma_usage_testcase.php test.php /HTML_Template_Sigma/tests/templates __include.html addblock.html blockiteration.html blocks.html globals.html include.html loadtemplatefile.html replaceblock.html |
||
| Groups: | php.pear.cvs | ||
| Request: | Send a blank email to pear-cvs+get-9740@lists.php.net to get a copy of this message | ||
avb Fri Apr 4 09:02:40 2003 EDT
Added files:
/pear/HTML_Template_Sigma package.xml Sigma.php
/pear/HTML_Template_Sigma/docs readme.txt
/pear/HTML_Template_Sigma/tests Console_TestListener.php
Sigma_api_testcase.php
Sigma_cache_testcase.php
Sigma_usage_testcase.php test.php
/pear/HTML_Template_Sigma/tests/templates __include.html
addblock.html
blockiteration.html
blocks.html globals.html
include.html
loadtemplatefile.html
replaceblock.html
Log:
Initial revision
Index: pear/HTML_Template_Sigma/package.xml +++ pear/HTML_Template_Sigma/package.xml <?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE package SYSTEM "../package.dtd"> <package version="1.0"> <name>HTML_Template_Sigma</name> <summary>An implementation of Integrated Templates API with template 'compilation' added</summary> <description> HTML_Template_Sigma implements Integrated Templates API designed by Ulf Wendel. Features: * Nested blocks. Nesting is controlled by the engine. * Ability to include files from within template: &lt;!-- INCLUDE --&gt; * Automatic removal of empty blocks and unknown variables (methods to manually tweak/override this are also available) * Methods for runtime addition and replacement of blocks in templates * Callback functions (NOT RECOMMENDED, API may change in future release) * 'Compiled' templates: the engine has to parse a template file using regular expressions to find all the blocks and variable placeholders. This is a very "expensive" operation and is an overkill to do on every page request: templates seldom change on production websites. Thus this feature: an internal representation of the template structure is saved into a file and this file gets loaded instead of the source one on subsequent requests (unless the source changes) </description> <maintainers> <maintainer> <user>avb</user> <role>lead</role> <name>Alexey Borzov</name> <email>avb@php.net</email> </maintainer> </maintainers> <license>PHP License</license> <release> <version>0.9</version> <state>stable</state> <date>2003-04-04</date> <notes>Initial PEAR release</notes> <filelist> <dir name="/" baseinstalldir="HTML/Template"> <file role="php" name="Sigma.php"/> <dir name="tests"> <dir name="templates"> <file role="data" name="addblock.html"/> <file role="data" name="blockiteration.html"/> <file role="data" name="blocks.html"/> <file role="data" name="globals.html"/> <file role="data" name="__include.html"/> <file role="data" name="include.html"/> <file role="data" name="loadtemplatefile.html"/> <file role="data" name="replaceblock.html"/> </dir> <file role="test" name="Console_TestListener.php"/> <file role="test" name="Sigma_api_testcase.php"/> <file role="test" name="Sigma_cache_testcase.php"/> <file role="test" name="Sigma_usage_testcase.php"/> <file role="test" name="test.php"/> </dir> <dir name="docs"> <file role="doc" name="readme.txt"></file> </dir> </dir> </filelist> </release> </package> Index: pear/HTML_Template_Sigma/Sigma.php +++ pear/HTML_Template_Sigma/Sigma.php <?php // // +----------------------------------------------------------------------+ // | PHP Version 4 | // +----------------------------------------------------------------------+ // | Copyright (c) 1997-2003 The PHP Group | // +----------------------------------------------------------------------+ // | This source file is subject to version 2.02 of the PHP license, | // | that is bundled with this package in the file LICENSE, and is | // | available at through the world-wide-web at | // | http://www.php.net/license/2_02.txt. | // | If you did not receive a copy of the PHP license and are unable to | // | obtain it through the world-wide-web, please send a note to | // | license@php.net so we can mail you a copy immediately. | // +----------------------------------------------------------------------+ // | Authors: Ulf Wendel <ulf.wendel@phpdoc.de> | // | Alexey Borzov <avb@php.net> | // +----------------------------------------------------------------------+ // // $Id: Sigma.php,v 1.1 2003/04/04 14:02:39 avb Exp $ // require_once 'PEAR.php'; define('SIGMA_OK', 1); define('SIGMA_ERROR', -1); define('SIGMA_TPL_NOT_FOUND', -2); define('SIGMA_BLOCK_NOT_FOUND', -3); define('SIGMA_BLOCK_DUPLICATE', -4); define('SIGMA_CACHE_ERROR', -5); define('SIGMA_UNKNOWN_OPTION', -6); define('SIGMA_PLACEHOLDER_NOT_FOUND', -10); define('SIGMA_PLACEHOLDER_DUPLICATE', -11); define('SIGMA_BLOCK_EXISTS', -12); define('SIGMA_CONFLICT', -100); /** * HTML_Template_Sigma: implementation of Integrated Templates API with * template 'compilation' added. * * The main new feature in Sigma is the template 'compilation'. Consider the * following: when loading a template file the engine has to parse it using * regular expressions to find all the blocks and variable placeholders. This * is a very "expensive" operation and is definitely an overkill to do on * every page request: templates seldom change on production websites. This is * where the cache kicks in: it saves an internal representation of the * template structure into a file and this file gets loaded instead of the * source one on subsequent requests (unless the source changes, of course). * * While HTML_Template_Sigma inherits PHPLib Template's template syntax, it has * an API which is easier to understand. When using HTML_Template_PHPLIB, you * have to explicitly name a source and a target the block gets parsed into. * This gives maximum flexibility but requires full knowledge of template * structure from the programmer. * * Integrated Template on the other hands manages block nesting and parsing * itself. The engine knows that inner1 is a child of block2, there's * no need to tell it about this: * * + __global__ (hidden and automatically added) * + block1 * + block2 * + inner1 * + inner2 * * To add content to block1 you simply type: * <code>$tpl->setCurrentBlock("block1");</code> * and repeat this as often as needed: * <code> * $tpl->setVariable(...); * $tpl->parseCurrentBlock(); * </code> * * To add content to block2 you would type something like: * <code> * $tpl->setCurrentBlock("inner1"); * $tpl->setVariable(...); * $tpl->parseCurrentBlock(); * * $tpl->setVariable(...); * $tpl->parseCurrentBlock(); * * $tpl->parse("block2"); * </code> * * This will result in one repetition of block2 which contains two repetitions * of inner1. inner2 will be removed if $removeEmptyBlock is set to true (which * is the default). * * Usage: * <code> * $tpl = new HTML_Template_Sigma( [string filerootdir], [string cacherootdir] ); * * // load a template or set it with setTemplate() * $tpl->loadTemplatefile( string filename [, boolean removeUnknownVariables, boolean removeEmptyBlocks] ) * * // set "global" Variables meaning variables not beeing within a (inner) block * $tpl->setVariable( string variablename, mixed value ); * * // like with the HTML_Template_PHPLIB there's a second way to use setVariable() * $tpl->setVariable( array ( string varname => mixed value ) ); * * // Let's use any block, even a deeply nested one * $tpl->setCurrentBlock( string blockname ); * * // repeat this as often as you need it. * $tpl->setVariable( array ( string varname => mixed value ) ); * $tpl->parseCurrentBlock(); * * // get the parsed template or print it: $tpl->show() * $html = $tpl->get(); * </code> * * @author Ulf Wendel <uw@netuse.de> * @author Alexey Borzov <avb@php.net> * @version $Id: Sigma.php,v 1.1 2003/04/04 14:02:39 avb Exp $ * @access public * @package HTML_Template_Sigma */ class HTML_Template_Sigma extends PEAR { /** * First character of a variable placeholder ( _{_VARIABLE} ). * @var string * @access public * @see $closingDelimiter, $blocknameRegExp, $variablenameRegExp */ var $openingDelimiter = '{'; /** * Last character of a variable placeholder ( {VARIABLE_}_ ) * @var string * @access public * @see $openingDelimiter, $blocknameRegExp, $variablenameRegExp */ var $closingDelimiter = '}'; /** * RegExp for matching the block names in the template * Per default "sm" is used as the regexp modifier, "i" is missing. * That means a case sensitive search is done. * @var string * @access public * @see $variablenameRegExp, $openingDelimiter, $closingDelimiter */ var $blocknameRegExp = '[0-9A-Za-z_-]+'; /** * RegExp matching a variable placeholder in the template * Per default "sm" is used as the regexp modifier, "i" is missing. * That means a case sensitive search is done. * @var string * @access public * @see $blocknameRegExp, $openingDelimiter, $closingDelimiter */ var $variablenameRegExp = '[0-9A-Za-z_-]+'; /** * RegExp used to find variable placeholder, filled by the constructor * @var string Looks somewhat like @(delimiter varname delimiter)@ * @see HTML_Template_Sigma() */ var $variablesRegExp = ''; /** * RegExp used to strip unused variable placeholders * @brother $variablesRegExp * @see HTML_Template_Sigma() */ var $removeVariablesRegExp = ''; /** * RegExp used to find blocks and their content, filled by the constructor * @var string * @see HTML_Template_Sigma() */ var $blockRegExp = ''; /** * Controls the handling of unknown variables, default is remove * @var boolean * @access public */ var $removeUnknownVariables = true; /** * Controls the handling of empty blocks, default is remove * @var boolean * @access public */ var $removeEmptyBlocks = true; /** * Name of the current block * @var string */ var $currentBlock = '__global__'; /** * Template blocks and their content * @var array * @see _buildBlocks() */ var $_blocks = array(); /** * Content of parsed blocks * @var array */ var $_parsedBlocks = array(); /** * Variable names that appear in the block * @var array */ var $_blockVariables = array(); /** * Inner blocks inside the block * @var array */ var $_children = array(); /** * List of blocks to preserve even if they are "empty" * @var array $_touchedBlocks * @see touchBlock(), $removeEmptyBlocks */ var $_touchedBlocks = array(); /** * List of blocks which should not be shown even if not "empty" * @var array $_hiddenBlocks * @see hideBlock(), $removeEmptyBlocks */ var $_hiddenBlocks = array(); /** * Variables for substitution * * Variables are kept in this array before the replacements are done. * This allows automatic removal of empty blocks. * * @var array * @see setVariable() */ var $_variables = array(); /** * Global variables for substitution * * These are substituted into all blocks, are not cleared on * block parsing and do not trigger "non-empty" logic. I.e. if * only global variables are substituted into the block, it is * still considered "empty". * * @var array * @see setVariable(), setGlobalVariable() */ var $_globalVariables = array(); /** * Root directory for "source" templates * * @var string * @see HTML_Template_Sigma(), setRoot() */ var $fileRoot = ''; /** * Directory for "prepared" templates cache * * @var string * @see HTML_Template_Sigma(), setCacheRoot() */ var $_cacheRoot = null; /** * Flag indicating that the global block was parsed * @var boolean */ var $flagGlobalParsed = false; /** * $_options['preserve_data'] If false, then substitute variables and remove empty * placeholders also in data passed through setVariable (see also bugs #20199, #21951) * $_options['use_preg'] Whether to use preg_replace instead of str_replace in parse() * (this is a HTML_Template_IT backwards compatibility feature, see also bugs #21951, #20392) * $_options['trim_on_save'] Whether to trim extra whitespace from template on cache save. * Generally safe to have this on, unless you have <pre></pre> in templates or want to * preserve HTML indentantion */ var $_options = array( 'preserve_data' => false, 'use_preg' => false, 'trim_on_save' => true ); /** * Function name prefix used when searching for function calls in the template * @var string */ var $functionPrefix = 'func_'; /** * Function name RegExp * @var string */ var $functionnameRegExp = '[_a-zA-Z]+[A-Za-z_0-9]*'; /** * RegExp used to grep function calls in the template (set by the constructor) * * @var string * @see HTML_Template_Sigma() */ var $functionRegExp = ''; /** * List of functions found in the template. * @var array */ var $_functions = array(); /** * List of callback functions specified by the user * @var array */ var $_callback = array(); /** * RegExp used to find file inclusion calls in the template (should have 'e' modifier) * @var string */ var $includeRegExp = '#<!--\s+INCLUDE\s+(\S+)\s+-->#ime'; /** * Files queued for inclusion * @var array */ var $_triggers = array(); /** * Constructor: builds some complex regular expressions and optionally * sets the root directories. * * Make sure that you call this constructor if you derive your template * class from this one. * * @param $root string root directory for templates * @param $cacheRoot string directory to cache "prepared" templates in * @see setRoot() */ function HTML_Template_Sigma($root = '', $cacheRoot = '') { // the class is inherited from PEAR to be able to use $this->setErrorHandling() $this->PEAR(); $this->variablesRegExp = '@'.$this->openingDelimiter.'('.$this->variablenameRegExp.')'.$this->closingDelimiter.'@sm'; $this->removeVariablesRegExp = '@'.$this->openingDelimiter.'\s*('.$this->variablenameRegExp.')\s*'.$this->closingDelimiter.'@sm'; $this->blockRegExp = '@<!--\s+BEGIN\s+('.$this->blocknameRegExp.')\s+-->(.*)<!--\s+END\s+\1\s+-->@sm'; $this->functionRegExp = '@' . $this->functionPrefix . '(' . $this->functionnameRegExp . ')\s*\(@sm'; $this->setRoot($root); $this->setCacheRoot($cacheRoot); } /** * Sets the file root for templates. The file root gets prefixed to all * filenames passed to the object. * * @param string directory name * @see HTML_Template_Sigma() * @access public */ function setRoot($root) { if (('' != $root) && ('/' != substr($root, -1))) { $root .= '/'; } $this->fileRoot = $root; } /** * Sets the directory to cache "prepared" templates in. * * The directory should be writable to PHP, of course. The "prepared" * template is essentially a serialized array of $_blocks, $_blockVariables * and $_children. This allows to bypass expensive calls to * _buildBlockVariables() and especially _buildBlocks() when reading the * "prepared" template instead of the "source" one. * * @param string directory name * @see HTML_Template_Sigma(), _getCached(), _writeCache() * @access public */ function setCacheRoot($root) { if (empty($root)) { return true; } elseif (('' != $root) && ('/' != substr($root, -1))) { $root .= '/'; } $this->_cacheRoot = $root; } /** * Sets the option for the template class * * @access public * @param string option name * @param mixed option value * @return mixed SIGMA_OK on success, error object on failure */ function setOption($option, $value) { if (isset($this->_options[$option])) { $this->_options[$option] = $value; return SIGMA_OK; } return $this->raiseError($this->errorMessage(SIGMA_UNKNOWN_OPTION, $option), SIGMA_UNKNOWN_OPTION); } /** * Returns a textual error message for an error code * * @access public * @param integer error code * @param string additional data to insert into message * @return string error message */ function errorMessage($code, $data = null) { static $errorMessages; if (!isset($errorMessages)) { $errorMessages = array( SIGMA_ERROR => 'unknown error', SIGMA_OK => '', SIGMA_TPL_NOT_FOUND => 'Cannot read the template file \'%s\'', SIGMA_BLOCK_NOT_FOUND => 'Cannot find block \'%s\'', SIGMA_BLOCK_DUPLICATE => 'The name of a block must be unique within a template. Block \'%s\' found twice.', SIGMA_CACHE_ERROR => 'Cannot save template file \'%s\'', SIGMA_UNKNOWN_OPTION => 'Unknown option \'%s\'', SIGMA_PLACEHOLDER_NOT_FOUND => 'Variable placeholder \'%s\' not found', SIGMA_PLACEHOLDER_DUPLICATE => 'Placeholder \'%s\' should be unique, found in multiple blocks', SIGMA_BLOCK_EXISTS => 'Block \'%s\' already exists', SIGMA_CONFLICT => 'Sorry, you cannot use both callbacks and template caching' ); } if (PEAR::isError($code)) { $code = $code->getCode(); } if (!isset($errorMessages[$code])) { return $errorMessages[SIGMA_ERROR]; } else { return (null === $data)? $errorMessages[$code]: sprintf($errorMessages[$code], $data); } } /** * Prints a block with all replacements done. * * @access public * @param string block name * @brother get() */ function show($block = '__global__') { print $this->get($block); } /** * Returns a block with all replacements done. * * @param string block name * @param bool whether to clear parsed block contents * @return string block with all replacements done * @throws PEAR_Error * @access public * @see show() */ function get($block = '__global__', $clear = true) { if (!isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } if ('__global__' == $block && !$this->flagGlobalParsed) { $this->parse('__global__'); } // return the parsed block, removing the unknown placeholders if needed if (!isset($this->_parsedBlocks[$block])) { return ''; } else { $ret = $this->_parsedBlocks[$block]; if ($clear) { unset($this->_parsedBlocks[$block]); } if ($this->removeUnknownVariables) { $ret = preg_replace($this->removeVariablesRegExp, '', $ret); } if ($this->_options['preserve_data']) { $ret = str_replace($this->openingDelimiter . '%preserved%' . $this->closingDelimiter, $this->openingDelimiter, $ret); } return $ret; } } /** * Parses the given block. * * @param string block name * @param boolean true if the function is called recursively * @param boolean true if we are inside a "hidden" block * @access public * @see parseCurrentBlock() * @throws PEAR_Error */ function parse($block = '__global__', $flagRecursion = false, $fakeParse = false) { static $vars; if (!isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } if ('__global__' == $block) { $this->flagGlobalParsed = true; } if (!isset($this->_parsedBlocks[$block])) { $this->_parsedBlocks[$block] = ''; } $outer = $this->_blocks[$block]; if (!$flagRecursion) { $vars = array(); } // block is not empty if its local var is substituted $empty = true; foreach ($this->_blockVariables[$block] as $allowedvar => $v) { if (isset($this->_variables[$allowedvar])) { $vars[$this->openingDelimiter . $allowedvar . $this->closingDelimiter] = $this->_variables[$allowedvar]; $empty = false; // vital for checking "empty/nonempty" status unset($this->_variables[$allowedvar]); } } // processing of the inner blocks if (isset($this->_children[$block])) { foreach ($this->_children[$block] as $innerblock => $v) { $placeholder = $this->openingDelimiter.'__'.$innerblock.'__'.$this->closingDelimiter; if (isset($this->_hiddenBlocks[$innerblock])) { // don't bother actually parsing this inner block; but we _have_ // to go through its local vars to prevent problems on next iteration $this->parse($innerblock, true, true); unset($this->_hiddenBlocks[$innerblock]); $outer = str_replace($placeholder, '', $outer); } else { $this->parse($innerblock, true, $fakeParse); // block is not empty if its inner block is not empty if ('' != $this->_parsedBlocks[$innerblock]) { $empty = false; } $outer = str_replace($placeholder, $this->_parsedBlocks[$innerblock], $outer); $this->_parsedBlocks[$innerblock] = ''; } } } // add "global" variables to the static array foreach ($this->_globalVariables as $allowedvar => $value) { if (isset($this->_blockVariables[$block][$allowedvar])) { $vars[$this->openingDelimiter . $allowedvar . $this->closingDelimiter] = $value; } } // if we are inside a hidden block, don't bother if (!$fakeParse) { // substitute variables only on non-recursive call, thus all // variables from all inner blocks get substituted if (!$flagRecursion && 0 != count($vars)) { $varKeys = $this->_options['use_preg']? array_map(array(&$this, '_addPregDelimiters'), array_keys($vars)): array_keys($vars); $varValues = $this->_options['preserve_data']? array_map(array(&$this, '_preserveOpeningDelimiter'), array_values($vars)): array_values($vars); $funcReplace = $this->_options['use_preg']? 'preg_replace': 'str_replace'; $outer = $funcReplace($varKeys, $varValues, $outer); } // check whether the block is considered "empty" and append parsed content if not if (!$empty || ('__global__' == $block) || !$this->removeEmptyBlocks || isset($this->_touchedBlocks[$block])) { $this->_parsedBlocks[$block] .= $outer; if (isset($this->_touchedBlocks[$block])) { unset($this->_touchedBlocks[$block]); } } } return $empty; } /** * Sets a variable value. * * The function can be used either like setVariable("varname", "value") * or with one array $variables["varname"] = "value" given setVariable($variables) * * @access public * @param mixed $variable variable name or array ('varname'=>'value') * @param string $value variable value if $variable is not an array */ function setVariable($variable, $value = '') { if (is_array($variable)) { $this->_variables = array_merge($this->_variables, $variable); } else { $this->_variables[$variable] = $value; } } /** * Sets a global variable value. * * @access public * @param mixed $variable variable name or array ('varname'=>'value') * @param string $value variable value if $variable is not an array * @see setVariable() */ function setGlobalVariable($variable, $value = '') { if (is_array($variable)) { $this->_globalVariables = array_merge($this->_globalVariables, $variable); } else { $this->_globalVariables[$variable] = $value; } } /** * Sets the name of the current block: the block where variables are added * * @param string block name * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error * @access public */ function setCurrentBlock($block = '__global__') { if (!isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } $this->currentBlock = $block; return SIGMA_OK; } /** * Parses the current block * * @see parse(), setCurrentBlock(), $currentBlock * @access public */ function parseCurrentBlock() { return $this->parse($this->currentBlock); } /** * Returns the current block name * * @return string block name * @access public */ function getCurrentBlock() { return $this->currentBlock; } /** * Preserves the block even if empty blocks should be removed * * This is something special. Sometimes you have blocks that * should be preserved although they are empty (no placeholder replaced). * Think of a shopping basket. If it's empty you have to drop a message to * the user. If it's filled you have to show the contents of the shopping baseket. * Now where do you place the message that the basket is empty? It's no good * idea to place it in you applications as customers tend to like unecessary minor * text changes. Having another template file for an empty basket means that it's * very likely that one fine day the filled and empty basket templates have different * layout. I decided to introduce blocks that to not contain any placeholder but only * text such as the message "Your shopping basked is empty". * * Now if there is no replacement done in such a block the block will be recognized * as "empty" and by default ($removeEmptyBlocks = true) be stripped off. To avoid this * you can call touchBlock() * * @param string block name * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error * @access public * @see $removeEmptyBlocks, $_touchedBlocks */ function touchBlock($block) { if (!isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } if (isset($this->_hiddenBlocks[$block])) { unset($this->_hiddenBlocks[$block]); } $this->_touchedBlocks[$block] = true; return SIGMA_OK; } /** * Hides the block even if it is not "empty" * * Is somewhat an opposite to touchBlock(). * * Consider a block that should be visible to registered/"special" users only, but * its visibility is triggered by some little field passed in a large array * into setVariable(). You can either carefully juggle your variables to prevent the * block from appearing (a fragile solution) or simply call hideBlock() * * @param string block name * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error * @access public */ function hideBlock($block) { if (!isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } if (isset($this->_touchedBlocks[$block])) { unset($this->_touchedBlocks[$block]); } $this->_hiddenBlocks[$block] = true; return SIGMA_OK; } /** * Sets the template. * * You can either load a template file from disk with LoadTemplatefile() or set the * template manually using this function. * * @param string template content * @param boolean remove unknown/unused variables? * @param boolean remove empty blocks? * @see loadTemplatefile() * @access public * @return mixed SIGMA_OK on success, error object on failure */ function setTemplate($template, $removeUnknownVariables = true, $removeEmptyBlocks = true) { $this->_resetTemplate($removeUnknownVariables, $removeEmptyBlocks); // parse the template for function calls only if not using the cache feature if (null === $this->_cacheRoot) { $template = $this->_buildFunctionlist($template); } $list = $this->_buildBlocks('<!-- BEGIN __global__ -->'.$template.'<!-- END __global__ -->'); if (PEAR::isError($list)) { return $list; } $this->_buildBlockVariables(); return SIGMA_OK; } /** * Loads a template file * * If caching is on, then it checks whether a "prepared" template exists * If it does, it gets loaded instead of the original, if it does not, then * the original gets loaded and prepared and then the prepared version is saved. * addBlockfile() and replaceBlockfile() implement quite the same logic. * * @param string filename * @param boolean remove unknown/unused variables? * @param boolean remove empty blocks? * @access public * @return mixed SIGMA_OK on success, error object on failure * @see setTemplate(), $removeUnknownVariables, $removeEmptyBlocks */ function loadTemplateFile($filename, $removeUnknownVariables = true, $removeEmptyBlocks = true) { if ($this->_isCached($filename)) { $this->_resetTemplate($removeUnknownVariables, $removeEmptyBlocks); return $this->_getCached($filename); } $template = $this->_getFile($this->_sourceName($filename)); if (PEAR::isError($template)) { return $template; } $this->_triggers = array(); $template = preg_replace($this->includeRegExp, "\$this->_makeTrigger('\\1', '__global__')", $template); if (PEAR::isError($res = $this->setTemplate($template, $removeUnknownVariables, $removeEmptyBlocks))) { return $res; } else { return $this->_writeCache($filename, '__global__'); } } /** * Adds a block to the template changing a variable placeholder to a block placeholder. * * Add means "replace a variable placeholder by a new block". * This is different to PHPLib templates. The function loads a * block, creates a handle for it and assigns it to a certain * variable placeholder. To to the same with PHPLib templates you would * call set_file() to create the handle and parse() to assign the * parsed block to a variable. By this PHPLibstemplates assume that you tend * to assign a block to more than one one placeholder. To assign a parsed block * to more than only the placeholder you specify in this function you have * to use a combination of get() and setVariable(). * * The block content must not start with <!-- BEGIN blockname --> and end with * <!-- END blockname --> this would cause overhead and produce an error. * * @param string name of the variable placeholder, the name must be unique within the template. * @param string name of the block to be added * @param string content of the block * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error * @see addBlockfile() * @access public */ function addBlock($placeholder, $block, $template) { if (isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_EXISTS, $block), SIGMA_BLOCK_EXISTS); } $parents = $this->_findParentBlocks($placeholder); if (0 == count($parents)) { return $this->raiseError($this->errorMessage(SIGMA_PLACEHOLDER_NOT_FOUND, $placeholder), SIGMA_PLACEHOLDER_NOT_FOUND); } elseif (count($parents) > 1) { return $this->raiseError($this->errorMessage(SIGMA_PLACEHOLDER_DUPLICATE, $placeholder), SIGMA_PLACEHOLDER_DUPLICATE); } $template = "<!-- BEGIN $block -->" . $template . "<!-- END $block -->"; $list = $this->_buildBlocks($template); if (PEAR::isError($list)) { return $list; } $this->_replacePlaceholder($parents[0], $placeholder, $block); $this->_buildBlockVariables($block); return SIGMA_OK; } /** * Adds a block taken from a file to the template, changing a variable placeholder * to a block placeholder. * * @param string name of the variable placeholder * @param string name of the block to be added * @param string template file that contains the block * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error * @brother addBlock() * @access public */ function addBlockfile($placeholder, $block, $filename) { if ($this->_isCached($filename)) { return $this->_getCached($filename, $block, $placeholder); } $template = $this->_getFile($this->_sourceName($filename)); if (PEAR::isError($template)) { return $template; } $template = preg_replace($this->includeRegExp, "\$this->_makeTrigger('\\1', '{$block}')", $template); if (PEAR::isError($res = $this->addBlock($placeholder, $block, $template))) { return $res; } else { return $this->_writeCache($filename, $block); } } /** * Replaces an existing block with new content. * * This function will replace a block of the template and all blocks contained in * the replaced block and add a new block insted, means you can dynamically change your * template. * * In contrast to other systems Sigma analyses the way you've nested blocks and knows * which block belongs into another block. The nesting information helps to make the API * short and simple. Replacing blocks does not only mean that Sigma has to update * the nesting information (relatively time consumpting task) but you have to * make sure that you do not get confused due to the template change itself. * * @param string name of a block to replace * @param string new content * @param boolean true if the parsed contents of the block should be kept * @access public * @see replaceBlockfile(), addBlock() * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error */ function replaceBlock($block, $template, $keepContent = false) { if (!isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } // should not throw a error as we already checked for block existance $this->_removeBlockData($block, $keepContent); $template = "<!-- BEGIN $block -->" . $template . "<!-- END $block -->"; $list = $this->_buildBlocks($template); if (PEAR::isError($list)) { return $list; } // renew the variables list $this->_buildBlockVariables($block); return SIGMA_OK; } /** * Replaces an existing block with new content from a file. * * @param string name of a block to replace * @param string template file that contains the block * @param boolean true if the parsed contents of the block should be kept * @access public * @brother replaceBlock() * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error */ function replaceBlockfile($block, $filename, $keepContent = false) { if ($this->_isCached($filename)) { if (PEAR::isError($res = $this->_removeBlockData($block, $keepContent))) { return $res; } else { return $this->_getCached($filename, $block); } } $template = $this->_getFile($this->_sourceName($filename)); if (PEAR::isError($template)) { return $template; } $template = preg_replace($this->includeRegExp, "\$this->_makeTrigger('\\1', '{$block}')", $template); if (PEAR::isError($res = $this->replaceBlock($block, $template, $keepContent))) { return $res; } else { return $this->_writeCache($filename, $block); } } /** * Checks if the block exists in the template * * @param string block name * @return bool yes/no * @access public */ function blockExists($block) { return isset($this->_blocks[$block]); } /** * Returns the name of the (first) block that contains the specified placeholder. * * @param string Name of the placeholder you're searching * @param string Name of the block to scan. If left out (default) all blocks are scanned. * @return string Name of the (first) block that contains the specified placeholder. * If the placeholder was not found an empty string is returned. * @access public * @throws PEAR_Error */ function placeholderExists($placeholder, $block = '') { if ('' != $block && !isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } if ('' != $block) { // if we search in the specific block, we should just check the array return isset($this->_blockVariables[$block][$placeholder])? $block: ''; } else { // _findParentBlocks returns an array, we need only the first element $parents = $this->_findParentBlocks($placeholder); return empty($parents)? '': $parents[0]; } } // end func placeholderExists //------------------------------------------------------------ // FIXME FIXME FIXME FIXME FIXME FIXME FIXME FIXME FIXME FIXME // // Okay, what follows is essentialy a copy/paste from the "stock" // HTML_Template_ITX. These are the functions that deal with // callbacks inside a template. They somewhat work, just like in // the "stock" version, but they were not cleaned/optimised and // they cannot be mixed with caching: if you do pass a // $cacheRoot to a constructor, then callbacks are disabled. // // Why do I say that callbacks "somewhat" work? Well, they get // parsed only on main template load, not on addBlockfile()/ // replaceBlockfile(). You cannot use placeholders/blocks as // function arguments. You have to explicitly call performCallback(). // To sum it up: DON'T USE 'EM. // // IMNSHO, function calls should be redone as special case of // blocks, _buildFunctionList() should be invoked from // _buildBlocks() and callbacks should be called automatically // on parse. But this is for the next major release... // // FIXME FIXME FIXME FIXME FIXME FIXME FIXME FIXME FIXME FIXME //------------------------------------------------------------ /** * Sets a callback function. * * Sigma templates can contain simple function calls. * "function call" means that the editor of the template can add * special placeholder to the template like 'func_h1("embedded in h1")'. * Sigma will grab this function calls and allow you to define a callback * function for them. * * This is an absolutely evil feature. If your application makes heavy * use of such callbacks and you're even implementing if-then etc. on * the level of a template engine you're reiventing the wheel... - that's * actually how PHP came into life. Anyway, sometimes it's handy. * * Consider also using XML/XSLT or native PHP. * * <?php * ... * function h_one($args) { * return sprintf('<h1>%s</h1>', $args[0]); * } * * ... * $tpl = new HTML_Template_Sigma( ... ); * ... * $tpl->setCallbackFunction('h1', 'h_one'); * $tpl->performCallback(); * ?> * * template: * func_h1('H1 Headline'); * * @param string Function name in the template * @param string Name of the callback function * @param string Name of the callback object * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error * @access public */ function setCallbackFunction($tplFunction, $callbackFunction, $callbackObject = '') { if (null !== $this->_cacheRoot) { return $this->raiseError($this->errorMessage(SIGMA_CONFLICT), SIGMA_CONFLICT); } $this->_callback[$tplFunction] = array( 'function' => $callbackFunction, 'object' => $callbackObject ); return SIGMA_OK; } // end func setCallbackFunction /** * Checks the list of function calls in the template and calls their callback function. * * @access public * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error */ function performCallback() { if (null !== $this->_cacheRoot) { return $this->raiseError($this->errorMessage(SIGMA_CONFLICT), SIGMA_CONFLICT); } foreach ($this->_functions as $funcId => $funcData) { if (isset($this->_callback[$funcData['name']])) { if ('' != $this->_callback[$funcData['name']]['object']) { $this->_variables['__function' . $funcId . '__'] = call_user_func( array( &$GLOBALS[$this->_callback[$funcData['name']]['object']], $this->_callback[$funcData['name']]['function'] ), $funcData['args'] ); } else { $this->_variables['__function' . $funcId . '__'] = call_user_func( $this->_callback[$funcData['name']]['function'], $funcData['args'] ); } } } return SIGMA_OK; } // end func performCallback //------------------------------------------------------------ // // Private methods follow // //------------------------------------------------------------ /** * Reads the file and returns its content * * @param string filename * @return string file content (or error object) */ function _getFile($filename) { if (!($fh = @fopen($filename, 'r'))) { return $this->raiseError($this->errorMessage(SIGMA_TPL_NOT_FOUND, $filename), SIGMA_TPL_NOT_FOUND); } $content = fread($fh, filesize($filename)); fclose($fh); return $content; } /** * Recursively builds a list of all variables within a block * * @param string block name */ function _buildBlockVariables($block = '__global__') { $this->_blockVariables[$block] = array(); preg_match_all($this->variablesRegExp, $this->_blocks[$block], $regs); if (0 != count($regs[1])) { foreach ($regs[1] as $k => $var) { $this->_blockVariables[$block][$var] = true; } } if (isset($this->_children[$block]) && is_array($this->_children[$block])) { foreach ($this->_children[$block] as $child => $v) { $this->_buildBlockVariables($child); } } } /** * Recusively builds a list of all blocks within the template. * * @param string template to be scanned * @see $_blocks * @throws PEAR_Error * @return mixed array of block names on success or error object on failure */ function _buildBlocks($string) { $blocks = array(); if (preg_match_all($this->blockRegExp, $string, $regs, PREG_SET_ORDER)) { foreach ($regs as $k => $match) { $blockname = $match[1]; $blockcontent = $match[2]; if (isset($this->_blocks[$blockname]) || isset($blocks[$blockname])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_DUPLICATE, $blockname), SIGMA_BLOCK_DUPLICATE); } $this->_blocks[$blockname] = $blockcontent; $blocks[$blockname] = true; $inner = $this->_buildBlocks($blockcontent); if (PEAR::isError($inner)) { return $inner; } foreach ($inner as $name => $v) { $pattern = sprintf('@<!--\s+BEGIN\s+%s\s+-->(.*)<!--\s+END\s+%s\s+-->@sm', $name, $name); $replacement = $this->openingDelimiter.'__'.$name.'__'.$this->closingDelimiter; $this->_blocks[$blockname] = preg_replace($pattern, $replacement, $this->_blocks[$blockname]); $this->_children[$blockname][$name] = true; } } } return $blocks; } /** * Resets the object's properties, used before processing a new template * * @access private * @param boolean remove unknown/unused variables? * @param boolean remove empty blocks? * @see setTemplate(), loadTemplateFile() */ function _resetTemplate($removeUnknownVariables = true, $removeEmptyBlocks = true) { $this->removeUnknownVariables = $removeUnknownVariables; $this->removeEmptyBlocks = $removeEmptyBlocks; $this->currentBlock = '__global__'; $this->_variables = array(); $this->_blocks = array(); $this->_children = array(); $this->_parsedBlocks = array(); $this->_touchedBlocks = array(); $this->flagGlobalParsed = false; } // _resetTemplate /** * Checks whether we have a "prepared" template cached * * If we do not do caching, always returns false * * @access private * @param $filename source filename * @return bool yes/no */ function _isCached($filename) { if (null === $this->_cacheRoot) { return false; } $cachedName = $this->_cachedName($filename); $sourceName = $this->_sourceName($filename); if (@file_exists($cachedName) && (filemtime($cachedName) > @filemtime($sourceName))) { return true; } else { return false; } } // _isCached /** * Loads a "prepared" template file * * @access private * @param string filename * @param string block name * @param string variable placeholder to replace by a block * @return mixed SIGMA_OK on success, error object on failure */ function _getCached($filename, $block = '__global__', $placeholder = '') { $content = $this->_getFile($this->_cachedName($filename)); if (PEAR::isError($content)) { return $content; } $cache = unserialize($content); if ('__global__' != $block) { $this->_blocks[$block] = $cache['blocks']['__global__']; $this->_blockVariables[$block] = $cache['variables']['__global__']; $this->_children[$block] = $cache['children']['__global__']; unset($cache['blocks']['__global__'], $cache['variables']['__global__'], $cache['children']['__global__']); } $this->_blocks = array_merge($this->_blocks, $cache['blocks']); $this->_blockVariables = array_merge($this->_blockVariables, $cache['variables']); $this->_children = array_merge($this->_children, $cache['children']); // the same thing gets done in addBlockfile() if (!empty($placeholder)) { $parents = $this->_findParentBlocks($placeholder); $this->_replacePlaceholder($parents[0], $placeholder, $block); } // pull the triggers, if any if (isset($cache['triggers'])) { return $this->_pullTriggers($cache['triggers']); } return SIGMA_OK; } // _getCached /** * Returns a full name of a "prepared" template file * * @access private * @param string source filename, relative to root directory * @return string filename */ function _cachedName($filename) { if ('/' == $filename{0} && '/' == substr($this->_cacheRoot, -1)) { $filename = substr($filename, 1); } $filename = str_replace('/', '__', $filename); return $this->_cacheRoot. $filename. '.it'; } // _cachedName /** * Returns a full name of a "source" template file * * @param string source filename, relative to root directory * @access private * @return string */ function _sourceName($filename) { if ('/' == $filename{0} && '/' == substr($this->fileRoot, -1)) { $filename = substr($filename, 1); } return $this->fileRoot . $filename; } // _sourceName /** * Writes a prepared template to disk * * @access private * @param string source filename, relative to root directory * @param string name of the block to save * @return mixed SIGMA_OK on success, error object on failure */ function _writeCache($filename, $block) { // do not save anything if no cache dir, but do pull triggers if (null !== $this->_cacheRoot) { $cache = array( 'blocks' => array(), 'variables' => array(), 'children' => array() ); $cachedName = $this->_cachedName($filename); $this->_buildCache($cache, $block); if ('__global__' != $block) { foreach (array_keys($cache) as $k) { $cache[$k]['__global__'] = $cache[$k][$block]; unset($cache[$k][$block]); } } if (isset($this->_triggers[$block])) { $cache['triggers'] = $this->_triggers[$block]; } if (!($fh = @fopen($cachedName, 'w'))) { return $this->raiseError($this->errorMessage(SIGMA_CACHE_ERROR, $cachedName), SIGMA_CACHE_ERROR); } fwrite($fh, serialize($cache)); fclose($fh); } // now pull triggers if (isset($this->_triggers[$block])) { if (PEAR::isError($res = $this->_pullTriggers($this->_triggers[$block]))) { return $res; } unset($this->_triggers[$block]); } return SIGMA_OK; } // _writeCache /** * Readies the prepared template for saving * * @access private * @param array prepared template * @param string block to add */ function _buildCache(&$cache, $block) { if (!$this->_options['trim_on_save']) { $cache['blocks'][$block] = $this->_blocks[$block]; } else { $cache['blocks'][$block] = preg_replace( array('/^\\s+/m', '/\\s+$/m', '/(\\r?\\n)+/'), array('', '', "\n"), $this->_blocks[$block] ); } $cache['variables'][$block] = $this->_blockVariables[$block]; if (!isset($this->_children[$block])) { $cache['children'][$block] = array(); } else { $cache['children'][$block] = $this->_children[$block]; foreach (array_keys($this->_children[$block]) as $child) { $this->_buildCache($cache, $child); } } } /** * Recursively removes all data belonging to a block * * @param string block name * @param boolean true if the parsed contents of the block should be kept * @return mixed SIGMA_OK on success, error object on failure * @see replaceBlock(), replaceBlockfile() */ function _removeBlockData($block, $keepContent = false) { if (!isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } if (!empty($this->_children[$block])) { foreach (array_keys($this->_children[$block]) as $child) { $this->_removeBlockData($child, false); } unset($this->_children[$block]); } unset($this->_blocks[$block]); unset($this->_blockVariables[$block]); unset($this->_hiddenBlocks[$block]); unset($this->_touchedBlocks[$block]); if (!$keepContent) { unset($this->_parsedBlocks[$block]); } return SIGMA_OK; } /** * Returns the names of the blocks where the variable placeholder appears * * @param string variable name * @return array block names */ function _findParentBlocks($variable) { $parents = array(); foreach ($this->_blockVariables as $blockname => $varnames) { if (!empty($varnames[$variable])) { $parents[] = $blockname; } } return $parents; } /** * Replaces a variable placeholder by a block placeholder * * Of course, it also updates the necessary arrays * * @param string name of the block containing the placeholder * @param string variable name * @param string block name */ function _replacePlaceholder($parent, $placeholder, $block) { $this->_children[$parent][$block] = true; $this->_blockVariables[$parent]['__'.$block.'__'] = true; $this->_blocks[$parent] = str_replace($this->openingDelimiter.$placeholder.$this->closingDelimiter, $this->openingDelimiter.'__'.$block.'__'.$this->closingDelimiter, $this->_blocks[$parent] ); unset($this->_blockVariables[$parent][$placeholder]); } /** * Generates a placeholder to replace an <!-- INCLUDE --> statement * * @access private * @param string filename * @param string current block name * @return string a placeholder */ function _makeTrigger($filename, $block) { $name = 'trigger_' . substr(md5($filename . ' ' . uniqid($block)), 0, 10); $this->_triggers[$block][$name] = $filename; return $this->openingDelimiter . $name . $this->closingDelimiter; } /** * Replaces the "trigger" placeholders by the matching file contents. * * @see addBlockfile() * @param array array ('trigger placeholder' => 'filename') * @return mixed SIGMA_OK on success, error object on failure */ function _pullTriggers($triggers) { foreach ($triggers as $placeholder => $filename) { if (PEAR::isError($res = $this->addBlockfile($placeholder, $placeholder, $filename))) { return $res; } // we actually do not need the resultant block... $parents = $this->_findParentBlocks('__' . $placeholder . '__'); // merge current block's children and variables with the parent's ones if (isset($this->_children[$placeholder])) { $this->_children[$parents[0]] = array_merge($this->_children[$parents[0]], $this->_children[$placeholder]); } $this->_blockVariables[$parents[0]] = array_merge($this->_blockVariables[$parents[0]], $this->_blockVariables[$placeholder]); // substitute the block's contents into parent's $this->_blocks[$parents[0]] = str_replace( $this->openingDelimiter . '__' . $placeholder . '__' . $this->closingDelimiter, $this->_blocks[$placeholder], $this->_blocks[$parents[0]] ); // remove the stuff that is no more needed unset($this->_blocks[$placeholder], $this->_blockVariables[$placeholder], $this->_children[$placeholder]); unset($this->_children[$parents[0]][$placeholder], $this->_blockVariables[$parents[0]]['__' . $placeholder . '__']); } return SIGMA_OK; } /** * Builds a list of functions from the template * * @param string template * @return string template with function calls replaced by placeholders */ function _buildFunctionlist($template) { $this->_functions = array(); $ret = $template; $funcIdx = 0; while (preg_match($this->functionRegExp, $template, $regs)) { $pos = strpos($template, $regs[0]); $template = substr($template, $pos + strlen($regs[0])); $head = $this->_getToken($template, ')'); $args = array(); $ret = str_replace($regs[0] . $head . ')', '{__function' . $funcIdx . '__}', $ret); $template = str_replace($regs[0] . $head . ')', '{__function' . $funcIdx . '__}', $template); while ('' != $head && $args2 = $this->_getToken($head, ',')) { $arg2 = trim($args2); $args[] = ('"' == $arg2{0} || "'" == $arg2{0}) ? substr($arg2, 1, -1) : $arg2; if ($arg2 == $head) { break; } $head = substr($head, strlen($arg2) + 1); } $this->_functions[$funcIdx++] = array( 'name' => $regs[1], 'args' => $args ); } return $ret; } // end func _buildFunctionlist /** * Returns a part of string up to a delimiter * * It should handle strings enclosed in single and double quotes, * thus the need for a non-trivial function * * @param string a string from which to extract * @param mixed a delimiter or an array of ('delimiter' => true) * @return string an extracted string */ function _getToken($code, $delimiter) { if ('' == $code) { return ''; } if (!is_array($delimiter)) { $delimiter = array( $delimiter => true ); } $len = strlen($code); $enclosed = false; $enclosed_by = ''; if (isset($delimiter[$code{0}])) { $i = 1; } else { for ($i = 0; $i < $len; $i++) { $char = $code{$i}; if (('"' == $char || "'" == $char) && ($char == $enclosed_by || '' == $enclosed_by) && (0 == $i || ($i > 0 && '\\' != $code{$i - 1}))) { $enclosed_by = $enclosed? '': $char; $enclosed = !$enclosed; } if (!$enclosed && isset($delimiter[$char])) { break; } } } return substr($code, 0, $i); } // end func _getToken /** * Adds delimiters to a string, so it can be used as a pattern in preg_* functions * * @param string * @return string */ function _addPregDelimiters($str) { return '@' . $str . '@'; } /** * Replaces an opening delimiter by a special string * * @param string * @return string */ function _preserveOpeningDelimiter($str) { return (false === strpos($str, $this->openingDelimiter))? $str: str_replace($this->openingDelimiter, $this->openingDelimiter . '%preserved%' . $this->closingDelimiter, $str); } } ?> Index: pear/HTML_Template_Sigma/docs/readme.txt +++ pear/HTML_Template_Sigma/docs/readme.txt HTML_Template_Sigma: implementation of Integrated Templates API with caching added. This template engine implements Integrated Templates API designed by Ulf Wendel. This API is also implemented by the original Integrated Template (now HTML_Template_IT in PEAR) and thus this package can (probably, YMMV) be used instead of it if you do not call any private methods in your programs and do not directly access objects' private properties. 1) New features and additions to API: a) Transparent caching of prepared templates. You just have to pass a second parameter to the constructor: a directory name (the directory should be writable for PHP). After the template gets parsed for blocks and variables, the resultant arrays are serialized and saved into files in this directory. The next time the template file needs to be loaded, the prepared one is loaded instead, with a cheap unserialize() instead of very expensive RegExp matching logic. b) Global variables. Variables set by a function setGlobalVariable() do not get cleared after first substitution, unlike ordinary ones, and do not trigger "block not empty" logic (block with only global variables is still considered "empty"). Can be used for directory prefixes, session identifiers and the stuff like that. c) hideBlock(): an opposite of touchBlock(). It prevents block from appearing in the result even if it is "not empty". 2) Removed features and incompatibilities: a) $flagCacheTemplatefile and related logic was dropped in favor of a more generic approach, see (1.a) b) $clearCache and $clearCacheOnParse were removed. $clearCache functionality is now controlled by passing a second parameter to get(), $clearCacheOnParse is assumed to always be false. c) Public functions (from IT) init() and free() were removed. Public functions (from ITX) getFunctioncalls(), setFunctioncontent(), getBlocklist(), setCallbackFuntiontable() (sic!), getBlockvariables(), BlockvariableExists() were removed. e) Callback functions and caching do not mix! If you pass a second parameter to the constructor, the template WILL NOT be parsed for function calls. If you attempt to call setCallbackFunction() of performCallback() afterwards, an error will be thrown. Alexey Borzov <avb@php.net> Index: pear/HTML_Template_Sigma/tests/Console_TestListener.php +++ pear/HTML_Template_Sigma/tests/Console_TestListener.php <?php class Console_TestListener extends PHPUnit_TestListener { function addError(&$test, &$t) { $this->_errors += 1; echo " Error $this->_errors in " . $test->getName() . " : $t\n"; } function addFailure(&$test, &$t) { $this->_fails += 1; if ($this->_fails == 1) { echo "\n"; } echo "Failure $this->_fails : $t\n"; } function endTest(&$test) { if ($this->_fails == 0 && $this->_errors == 0) { echo ' Test passed'; } else { echo "There were $this->_fails failures for " . $test->getName() . "\n"; echo "There were $this->_errors errors for " . $test->getName() . "\n"; } echo "\n"; } function startTest(&$test) { $this->_fails = 0; $this->_errors = 0; echo get_class($test) . " : Starting " . $test->getName() . " ..."; } } ?> Index: pear/HTML_Template_Sigma/tests/Sigma_api_testcase.php +++ pear/HTML_Template_Sigma/tests/Sigma_api_testcase.php <?php function _uppercaseCallback($ary) { return strtoupper($ary[0]); } class Sigma_api_TestCase extends PHPUnit_TestCase { /** * A template object * @var object */ var $tpl; function Sigma_api_TestCase($name) { $this->PHPUnit_TestCase($name); } function setUp() { $className = 'HTML_Template_' . $GLOBALS['IT_class']; $this->tpl =& new $className('./templates'); } function tearDown() { unset($this->tpl); } function _stripWhitespace($str) { return preg_replace('/\\s+/', '', $str); } function _methodExists($name) { if (in_array(strtolower($name), get_class_methods($this->tpl))) { return true; } $this->assertTrue(false, 'method '. $name . ' not implemented in ' . get_class($this->tpl)); return false; } /** * Tests a setTemplate method * */ function testSetTemplate() { $result = $this->tpl->setTemplate('A template', false, false); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error setting template: '. $result->getMessage()); } $this->assertEquals('A template', $this->tpl->get()); } /** * Tests a loadTemplatefile method * */ function testLoadTemplatefile() { $result = $this->tpl->loadTemplatefile('loadtemplatefile.html', false, false); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->assertEquals('A template', trim($this->tpl->get())); } /** * Tests a setVariable method * */ function testSetVariable() { $result = $this->tpl->setTemplate('{placeholder1} {placeholder2} {placeholder3}', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error setting template: '. $result->getMessage()); } // "scalar" call $this->tpl->setVariable('placeholder1', 'var1'); // array call $this->tpl->setVariable(array( 'placeholder2' => 'var2', 'placeholder3' => 'var3' )); $this->assertEquals('var1 var2 var3', $this->tpl->get()); } /** * Tests the <!-- INCLUDE --> functionality * */ function testInclude() { $result = $this->tpl->loadTemplateFile('include.html', false, false); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->assertEquals('Master file; Included file', trim($this->tpl->get())); } /** * */ function testCurrentBlock() { $result = $this->tpl->loadTemplateFile('blockiteration.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->tpl->setVariable('outer', 'a'); $this->tpl->setCurrentBlock('inner_block'); for ($i = 0; $i < 5; $i++) { $this->tpl->setVariable('inner', $i + 1); $this->tpl->parseCurrentBlock(); } // for $this->assertEquals('a|1|2|3|4|5#', $this->_stripWhitespace($this->tpl->get())); } /** * */ function testRemovePlaceholders() { $result = $this->tpl->setTemplate('{placeholder1},{placeholder2},{placeholder3}', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error setting template: '. $result->getMessage()); } // we do not set {placeholder3} $this->tpl->setVariable(array( 'placeholder1' => 'var1', 'placeholder2' => 'var2' )); $this->assertEquals('var1,var2,', $this->tpl->get()); // Now, we should really add a switch for keeping {stuff} in // data supplied to setVariable() safe. Until then, removing it should // be expected behaviour $result = $this->tpl->setTemplate('{placeholder1},{placeholder2},{placeholder3}', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error setting template: '. $result->getMessage()); } $this->tpl->setVariable(array( 'placeholder1' => 'var1', 'placeholder2' => 'var2', 'placeholder3' => 'var3{stuff}' )); $this->assertEquals('var1,var2,var3', $this->tpl->get()); } /** * */ function testTouchBlock() { $result = $this->tpl->loadTemplateFile('blockiteration.html', false, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->tpl->setVariable('outer', 'data'); // inner_block should be preserved in output, even if empty $this->tpl->touchBlock('inner_block'); $this->assertEquals('data|{inner}#', $this->_stripWhitespace($this->tpl->get())); } // Not available in stock class /** * */ function testHideBlock() { if (!$this->_methodExists('hideBlock')) { return; } $result = $this->tpl->loadTemplateFile('blockiteration.html', false, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->tpl->setVariable(array( 'outer' => 'data', 'inner' => 'stuff' )); // inner_block is not empty, but should be removed nonetheless $this->tpl->hideBlock('inner_block'); $this->assertEquals('data#', $this->_stripWhitespace($this->tpl->get())); } /** * */ function testSetGlobalVariable() { if (!$this->_methodExists('setGlobalVariable')) { return; } $result = $this->tpl->loadTemplateFile('globals.html', false, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->tpl->setGlobalVariable('glob', 'glob'); // {var2} is not, block_two should be removed $this->tpl->setVariable(array( 'var1' => 'one', 'var3' => 'three' )); for ($i = 0; $i < 3; $i++) { $this->tpl->setVariable('var4', $i + 1); $this->tpl->parse('block_four'); } // for $this->assertEquals('glob:one#glob:three|glob:1|glob:2|glob:3#', $this->_stripWhitespace($this->tpl->get())); } function testOptionUsePreg() { if (!$this->_methodExists('setOption')) { return; } $this->tpl->setTemplate('{var}', true, true); $this->tpl->setOption('use_preg', true); $this->tpl->setVariable('var', 'All templates for $1 or \\$2!'); $this->assertEquals('All templates for or $2!', $this->tpl->get()); } function testOptionPreserveData() { if (!$this->_methodExists('setOption')) { return; } $this->tpl->setTemplate('{placeholder1},{placeholder2},{placeholder3}', true, true); $this->tpl->setOption('preserve_data', true); $this->tpl->setVariable(array( 'placeholder1' => 'var1', 'placeholder2' => 'var2', 'placeholder3' => 'var3{stuff}' )); $this->assertEquals('var1,var2,var3{stuff}', $this->tpl->get()); } function testPlaceholderExists() { $this->tpl->setTemplate('{var}'); $this->assertTrue($this->tpl->placeholderExists('var'), 'Existing placeholder \'var\' reported as nonexistant'); $this->assertTrue(!$this->tpl->placeholderExists('foobar'), 'Nonexistant placeholder \'foobar\' reported as existing'); $this->assertTrue($this->tpl->placeholderExists('var', '__global__'), 'Existing in block \'__global__\' placeholder \'var\' reported as nonexistant'); $this->assertTrue(!$this->tpl->placeholderExists('foobar', '__global__'), 'Nonexistant in block \'__global__\' placeholder \'foobar\' reported as existing'); } function testBlockExists() { $this->tpl->setTemplate('{var}'); $this->assertTrue($this->tpl->blockExists('__global__'), 'Existing block \'__global__\' reported as nonexistant'); $this->assertTrue(!$this->tpl->blockExists('foobar'), 'Nonexistant block \'foobar\' reported as existing'); } function testAddBlock() { $result = $this->tpl->loadTemplatefile('blocks.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->tpl->addBlock('var', 'added', 'added:{new_var}'); $this->assertTrue($this->tpl->blockExists('added'), 'The new block seems to be missing'); $this->assertTrue(!$this->tpl->placeholderExists('var'), 'The old variable seems to be still present in the template'); $this->tpl->setVariable('new_var', 'new_value'); $this->assertEquals('added:new_value', $this->_stripWhitespace($this->tpl->get())); } function testAddBlockfile() { $result = $this->tpl->loadTemplatefile('blocks.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $result = $this->tpl->addBlockfile('var', 'added', 'addblock.html'); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error adding block from file: '. $result->getMessage()); } $this->assertTrue($this->tpl->blockExists('added'), 'The new block seems to be missing'); $this->assertTrue(!$this->tpl->placeholderExists('var'), 'The old variable seems to be still present in the template'); $this->tpl->setVariable('new_var', 'new_value'); $this->assertEquals('added:new_value', $this->_stripWhitespace($this->tpl->get())); } function testReplaceBlock() { $result = $this->tpl->loadTemplatefile('blocks.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->tpl->setVariable('old_var', 'old_value'); $this->tpl->parse('old_block'); // old_block's contents should be discarded $this->tpl->replaceBlock('old_block', 'replaced:{replaced_var}#', false); $this->assertTrue(!$this->tpl->blockExists('old_inner_block') && !$this->tpl->placeholderExists('old_var'), 'The replaced block\'s contents seem to be still present'); $this->tpl->setVariable('replaced_var', 'replaced_value'); $this->tpl->parse('old_block'); // this time old_block's contents should be preserved $this->tpl->replaceBlock('old_block', 'replaced_again:{brand_new_var}', true); $this->tpl->setVariable('brand_new_var', 'brand_new_value'); $this->assertEquals('replaced:replaced_value#replaced_again:brand_new_value', $this->_stripWhitespace($this->tpl->get())); } function testReplaceBlockfile() { $result = $this->tpl->loadTemplatefile('blocks.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->tpl->setVariable('old_var', 'old_value'); $this->tpl->parse('old_block'); // old_block's contents should be discarded $result = $this->tpl->replaceBlockfile('old_block', 'replaceblock.html', false); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error replacing block from file: '. $result->getMessage()); } $this->assertTrue(!$this->tpl->blockExists('old_inner_block') && !$this->tpl->placeholderExists('old_var'), 'The replaced block\'s contents seem to be still present'); $this->tpl->setVariable(array( 'replaced_var' => 'replaced_value', 'replaced_inner_var' => 'inner_value' )); $this->tpl->parse('old_block'); // this time old_block's contents should be preserved $result = $this->tpl->replaceBlockfile('old_block', 'addblock.html', true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error replacing block from file: '. $result->getMessage()); } $this->tpl->setVariable('new_var', 'again'); $this->assertEquals('replaced:replaced_value|inner_value#added:again', $this->_stripWhitespace($this->tpl->get())); } function testCallback() { $this->tpl->setTemplate('callback:func_uppercase(word)'); $this->tpl->setCallbackFunction('uppercase', '_uppercaseCallback'); $res = $this->tpl->performCallback(); if (PEAR::isError($res)) { $this->assertTrue(false, 'Error performing callback: '. $res->getMessage()); } $this->assertEquals('callback:WORD', $this->tpl->get()); } } ?> Index: pear/HTML_Template_Sigma/tests/Sigma_cache_testcase.php +++ pear/HTML_Template_Sigma/tests/Sigma_cache_testcase.php <?php require_once 'Sigma_api_testcase.php'; class Sigma_cache_TestCase extends Sigma_api_TestCase { function Sigma_cache_TestCase($name) { $this->Sigma_api_TestCase($name); } function setUp() { global $Sigma_cache_dir; $className = 'HTML_Template_' . $GLOBALS['IT_class']; $this->tpl =& new $className('./templates', $Sigma_cache_dir); } function _removeCachedFiles($filename) { if (!is_array($filename)) { $filename = array($filename); } foreach ($filename as $file) { $cachedName = $this->tpl->_cachedName($file); if (@file_exists($cachedName)) { @unlink($cachedName); } } } function assertCacheExists($filename) { if (!is_array($filename)) { $filename = array($filename); } foreach ($filename as $file) { $cachedName = $this->tpl->_cachedName($file); if (!@file_exists($cachedName)) { $this->assertTrue(false, "File '$file' is not cached"); } } } function testLoadTemplatefile() { if (!$this->_methodExists('_isCached')) { return; } $this->_removeCachedFiles('loadtemplatefile.html'); parent::testLoadTemplateFile(); $this->assertCacheExists('loadtemplatefile.html'); parent::testLoadTemplateFile(); } function testAddBlockfile() { if (!$this->_methodExists('_isCached')) { return; } $this->_removeCachedFiles(array('blocks.html', 'addblock.html')); parent::testAddBlockfile(); $this->assertCacheExists(array('blocks.html', 'addblock.html')); parent::testAddBlockfile(); } function testReplaceBlockFile() { if (!$this->_methodExists('_isCached')) { return; } $this->_removeCachedFiles(array('blocks.html', 'replaceblock.html')); parent::testReplaceBlockfile(); $this->assertCacheExists(array('blocks.html', 'replaceblock.html')); parent::testReplaceBlockfile(); } function testInclude() { if (!$this->_methodExists('_isCached')) { return; } $this->_removeCachedFiles(array('include.html', '__include.html')); parent::testInclude(); $this->assertCacheExists(array('include.html', '__include.html')); parent::testInclude(); } } ?> Index: pear/HTML_Template_Sigma/tests/Sigma_usage_testcase.php +++ pear/HTML_Template_Sigma/tests/Sigma_usage_testcase.php <?php class Sigma_Usage_TestCase extends PHPUnit_TestCase { /** * An HTML_Template_IT object * @var object */ var $tpl; function Sigma_Usage_TestCase($name) { $this->PHPUnit_TestCase($name); } function setUp() { $className = 'HTML_Template_' . $GLOBALS['IT_class']; $this->tpl =& new $className('./templates'); } function tearDown() { unset($this->tpl); } function _stripWhitespace($str) { return preg_replace('/\\s+/', '', $str); } function _methodExists($name) { if (in_array(strtolower($name), get_class_methods($this->tpl))) { return true; } $this->assertTrue(false, 'method '. $name . ' not implemented in ' . get_class($this->tpl)); return false; } /** * Tests iterations over two blocks * */ function testBlockIteration() { $data = array( 'a', array('b', array('1', '2', '3', '4')), 'c', array('d', array('5', '6', '7')) ); $result = $this->tpl->loadTemplateFile('blockiteration.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } foreach ($data as $value) { if (is_array($value)) { $this->tpl->setVariable('outer', $value[0]); foreach ($value[1] as $v) { $this->tpl->setVariable('inner', $v); $this->tpl->parse('inner_block'); } } else { $this->tpl->setVariable('outer', $value); } $this->tpl->parse('outer_block'); } $this->assertEquals('a#b|1|2|3|4#c#d|5|6|7#', $this->_stripWhitespace($this->tpl->get())); } /** * * */ function testTouchBlockIteration() { $data = array('a','b','c','d','e'); $result = $this->tpl->loadTemplateFile('blockiteration.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } for ($i = 0; $i < count($data); $i++) { $this->tpl->setVariable('outer', $data[$i]); // the inner_block is empty and should be removed if (0 == $i % 2) { $this->tpl->touchBlock('inner_block'); } $this->tpl->parse('outer_block'); } $this->assertEquals('a|#b#c|#d#e|#', $this->_stripWhitespace($this->tpl->get())); } // Not available in stock version /** * */ function testHideBlockIteration() { if (!$this->_methodExists('hideBlock')) { return; } $data = array('a','b','c','d','e'); $result = $this->tpl->loadTemplateFile('blockiteration.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } for ($i = 0; $i < count($data); $i++) { $this->tpl->setVariable(array( 'inner' => $i + 1, 'outer' => $data[$i] )); // the inner_block is not empty, but should be removed if (0 == $i % 2) { $this->tpl->hideBlock('inner_block'); } $this->tpl->parse('outer_block'); } $this->assertEquals('a#b|2#c#d|4#e#', $this->_stripWhitespace($this->tpl->get())); } } ?> Index: pear/HTML_Template_Sigma/tests/test.php +++ pear/HTML_Template_Sigma/tests/test.php <?php // This is where "prepared" templates will be stored, if you decide to run // Sigma_cache_testcase. Make sure that this directory is writeable for PHP $Sigma_cache_dir = './templates/prepared'; // What class are we going to test? // It is possible to also use the unit tests to test HTML_Template_ITX, which // also implements Integrated Templates API $IT_class = 'Sigma'; // $IT_class = 'ITX'; // Sigma_cache_testcase is useless if testing HTML_Template_ITX $testcases = array( 'Sigma_api_testcase', 'Sigma_cache_testcase', 'Sigma_usage_testcase' ); // BC hack to define PATH_SEPARATOR for version of PHP prior 4.3 if(!defined('PATH_SEPARATOR')) { if(defined('DIRECTORY_SEPARATOR') && DIRECTORY_SEPARATOR == "\\") { define('PATH_SEPARATOR', ';'); } else { define('PATH_SEPARATOR', ':'); } } ini_set('include_path', '..'.PATH_SEPARATOR.ini_get('include_path')); require_once 'PHPUnit.php'; require_once $IT_class . '.php'; $suite =& new PHPUnit_TestSuite(); foreach ($testcases as $testcase) { include_once $testcase . '.php'; $methods = preg_grep('/^test/', get_class_methods($testcase)); foreach ($methods as $method) { $suite->addTest(new $testcase($method)); } } require_once 'Console_TestListener.php'; $result =& new PHPUnit_TestResult(); $result->addListener(new Console_TestListener); $suite->run($result); ?> Index: pear/HTML_Template_Sigma/tests/templates/__include.html +++ pear/HTML_Template_Sigma/tests/templates/__include.html Included file Index: pear/HTML_Template_Sigma/tests/templates/addblock.html +++ pear/HTML_Template_Sigma/tests/templates/addblock.html added:{new_var} Index: pear/HTML_Template_Sigma/tests/templates/blockiteration.html +++ pear/HTML_Template_Sigma/tests/templates/blockiteration.html <!-- BEGIN outer_block --> {outer} <!-- BEGIN inner_block --> |{inner} <!-- END inner_block --> # <!-- END outer_block --> Index: pear/HTML_Template_Sigma/tests/templates/blocks.html +++ pear/HTML_Template_Sigma/tests/templates/blocks.html {var} <!-- BEGIN old_block --> old:{old_var} <!-- BEGIN old_inner_block --> |{old_inner_var} <!-- END old_inner_block --> # <!-- END old_block --> Index: pear/HTML_Template_Sigma/tests/templates/globals.html +++ pear/HTML_Template_Sigma/tests/templates/globals.html <!-- BEGIN block_one --> {glob}:{var1}# <!-- END block_one --> <!-- BEGIN block_two --> {glob}:{var2}# <!-- END block_two --> <!-- BEGIN block_three --> {glob}:{var3} <!-- BEGIN block_four --> |{glob}:{var4} <!-- END block_four --> # <!-- END block_three --> Index: pear/HTML_Template_Sigma/tests/templates/include.html +++ pear/HTML_Template_Sigma/tests/templates/include.html Master file; <!-- INCLUDE __include.html --> Index: pear/HTML_Template_Sigma/tests/templates/loadtemplatefile.html +++ pear/HTML_Template_Sigma/tests/templates/loadtemplatefile.html A template Index: pear/HTML_Template_Sigma/tests/templates/replaceblock.html +++ pear/HTML_Template_Sigma/tests/templates/replaceblock.html replaced:{replaced_var} <!-- BEGIN new_inner_block --> |{replaced_inner_var} <!-- END new_inner_block --> #
Index: pear/HTML_Template_Sigma/package.xml +++ pear/HTML_Template_Sigma/package.xml <?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE package SYSTEM "../package.dtd"> <package version="1.0"> <name>HTML_Template_Sigma</name> <summary>An implementation of Integrated Templates API with template 'compilation' added</summary> <description> HTML_Template_Sigma implements Integrated Templates API designed by Ulf Wendel. Features: * Nested blocks. Nesting is controlled by the engine. * Ability to include files from within template: &lt;!-- INCLUDE --&gt; * Automatic removal of empty blocks and unknown variables (methods to manually tweak/override this are also available) * Methods for runtime addition and replacement of blocks in templates * Callback functions (NOT RECOMMENDED, API may change in future release) * 'Compiled' templates: the engine has to parse a template file using regular expressions to find all the blocks and variable placeholders. This is a very "expensive" operation and is an overkill to do on every page request: templates seldom change on production websites. Thus this feature: an internal representation of the template structure is saved into a file and this file gets loaded instead of the source one on subsequent requests (unless the source changes) </description> <maintainers> <maintainer> <user>avb</user> <role>lead</role> <name>Alexey Borzov</name> <email>avb@php.net</email> </maintainer> </maintainers> <license>PHP License</license> <release> <version>0.9</version> <state>stable</state> <date>2003-04-04</date> <notes>Initial PEAR release</notes> <filelist> <dir name="/" baseinstalldir="HTML/Template"> <file role="php" name="Sigma.php"/> <dir name="tests"> <dir name="templates"> <file role="data" name="addblock.html"/> <file role="data" name="blockiteration.html"/> <file role="data" name="blocks.html"/> <file role="data" name="globals.html"/> <file role="data" name="__include.html"/> <file role="data" name="include.html"/> <file role="data" name="loadtemplatefile.html"/> <file role="data" name="replaceblock.html"/> </dir> <file role="test" name="Console_TestListener.php"/> <file role="test" name="Sigma_api_testcase.php"/> <file role="test" name="Sigma_cache_testcase.php"/> <file role="test" name="Sigma_usage_testcase.php"/> <file role="test" name="test.php"/> </dir> <dir name="docs"> <file role="doc" name="readme.txt"></file> </dir> </dir> </filelist> </release> </package> Index: pear/HTML_Template_Sigma/Sigma.php +++ pear/HTML_Template_Sigma/Sigma.php <?php // // +----------------------------------------------------------------------+ // | PHP Version 4 | // +----------------------------------------------------------------------+ // | Copyright (c) 1997-2003 The PHP Group | // +----------------------------------------------------------------------+ // | This source file is subject to version 2.02 of the PHP license, | // | that is bundled with this package in the file LICENSE, and is | // | available at through the world-wide-web at | // | http://www.php.net/license/2_02.txt. | // | If you did not receive a copy of the PHP license and are unable to | // | obtain it through the world-wide-web, please send a note to | // | license@php.net so we can mail you a copy immediately. | // +----------------------------------------------------------------------+ // | Authors: Ulf Wendel <ulf.wendel@phpdoc.de> | // | Alexey Borzov <avb@php.net> | // +----------------------------------------------------------------------+ // // $Id: Sigma.php,v 1.1 2003/04/04 14:02:39 avb Exp $ // require_once 'PEAR.php'; define('SIGMA_OK', 1); define('SIGMA_ERROR', -1); define('SIGMA_TPL_NOT_FOUND', -2); define('SIGMA_BLOCK_NOT_FOUND', -3); define('SIGMA_BLOCK_DUPLICATE', -4); define('SIGMA_CACHE_ERROR', -5); define('SIGMA_UNKNOWN_OPTION', -6); define('SIGMA_PLACEHOLDER_NOT_FOUND', -10); define('SIGMA_PLACEHOLDER_DUPLICATE', -11); define('SIGMA_BLOCK_EXISTS', -12); define('SIGMA_CONFLICT', -100); /** * HTML_Template_Sigma: implementation of Integrated Templates API with * template 'compilation' added. * * The main new feature in Sigma is the template 'compilation'. Consider the * following: when loading a template file the engine has to parse it using * regular expressions to find all the blocks and variable placeholders. This * is a very "expensive" operation and is definitely an overkill to do on * every page request: templates seldom change on production websites. This is * where the cache kicks in: it saves an internal representation of the * template structure into a file and this file gets loaded instead of the * source one on subsequent requests (unless the source changes, of course). * * While HTML_Template_Sigma inherits PHPLib Template's template syntax, it has * an API which is easier to understand. When using HTML_Template_PHPLIB, you * have to explicitly name a source and a target the block gets parsed into. * This gives maximum flexibility but requires full knowledge of template * structure from the programmer. * * Integrated Template on the other hands manages block nesting and parsing * itself. The engine knows that inner1 is a child of block2, there's * no need to tell it about this: * * + __global__ (hidden and automatically added) * + block1 * + block2 * + inner1 * + inner2 * * To add content to block1 you simply type: * <code>$tpl->setCurrentBlock("block1");</code> * and repeat this as often as needed: * <code> * $tpl->setVariable(...); * $tpl->parseCurrentBlock(); * </code> * * To add content to block2 you would type something like: * <code> * $tpl->setCurrentBlock("inner1"); * $tpl->setVariable(...); * $tpl->parseCurrentBlock(); * * $tpl->setVariable(...); * $tpl->parseCurrentBlock(); * * $tpl->parse("block2"); * </code> * * This will result in one repetition of block2 which contains two repetitions * of inner1. inner2 will be removed if $removeEmptyBlock is set to true (which * is the default). * * Usage: * <code> * $tpl = new HTML_Template_Sigma( [string filerootdir], [string cacherootdir] ); * * // load a template or set it with setTemplate() * $tpl->loadTemplatefile( string filename [, boolean removeUnknownVariables, boolean removeEmptyBlocks] ) * * // set "global" Variables meaning variables not beeing within a (inner) block * $tpl->setVariable( string variablename, mixed value ); * * // like with the HTML_Template_PHPLIB there's a second way to use setVariable() * $tpl->setVariable( array ( string varname => mixed value ) ); * * // Let's use any block, even a deeply nested one * $tpl->setCurrentBlock( string blockname ); * * // repeat this as often as you need it. * $tpl->setVariable( array ( string varname => mixed value ) ); * $tpl->parseCurrentBlock(); * * // get the parsed template or print it: $tpl->show() * $html = $tpl->get(); * </code> * * @author Ulf Wendel <uw@netuse.de> * @author Alexey Borzov <avb@php.net> * @version $Id: Sigma.php,v 1.1 2003/04/04 14:02:39 avb Exp $ * @access public * @package HTML_Template_Sigma */ class HTML_Template_Sigma extends PEAR { /** * First character of a variable placeholder ( _{_VARIABLE} ). * @var string * @access public * @see $closingDelimiter, $blocknameRegExp, $variablenameRegExp */ var $openingDelimiter = '{'; /** * Last character of a variable placeholder ( {VARIABLE_}_ ) * @var string * @access public * @see $openingDelimiter, $blocknameRegExp, $variablenameRegExp */ var $closingDelimiter = '}'; /** * RegExp for matching the block names in the template * Per default "sm" is used as the regexp modifier, "i" is missing. * That means a case sensitive search is done. * @var string * @access public * @see $variablenameRegExp, $openingDelimiter, $closingDelimiter */ var $blocknameRegExp = '[0-9A-Za-z_-]+'; /** * RegExp matching a variable placeholder in the template * Per default "sm" is used as the regexp modifier, "i" is missing. * That means a case sensitive search is done. * @var string * @access public * @see $blocknameRegExp, $openingDelimiter, $closingDelimiter */ var $variablenameRegExp = '[0-9A-Za-z_-]+'; /** * RegExp used to find variable placeholder, filled by the constructor * @var string Looks somewhat like @(delimiter varname delimiter)@ * @see HTML_Template_Sigma() */ var $variablesRegExp = ''; /** * RegExp used to strip unused variable placeholders * @brother $variablesRegExp * @see HTML_Template_Sigma() */ var $removeVariablesRegExp = ''; /** * RegExp used to find blocks and their content, filled by the constructor * @var string * @see HTML_Template_Sigma() */ var $blockRegExp = ''; /** * Controls the handling of unknown variables, default is remove * @var boolean * @access public */ var $removeUnknownVariables = true; /** * Controls the handling of empty blocks, default is remove * @var boolean * @access public */ var $removeEmptyBlocks = true; /** * Name of the current block * @var string */ var $currentBlock = '__global__'; /** * Template blocks and their content * @var array * @see _buildBlocks() */ var $_blocks = array(); /** * Content of parsed blocks * @var array */ var $_parsedBlocks = array(); /** * Variable names that appear in the block * @var array */ var $_blockVariables = array(); /** * Inner blocks inside the block * @var array */ var $_children = array(); /** * List of blocks to preserve even if they are "empty" * @var array $_touchedBlocks * @see touchBlock(), $removeEmptyBlocks */ var $_touchedBlocks = array(); /** * List of blocks which should not be shown even if not "empty" * @var array $_hiddenBlocks * @see hideBlock(), $removeEmptyBlocks */ var $_hiddenBlocks = array(); /** * Variables for substitution * * Variables are kept in this array before the replacements are done. * This allows automatic removal of empty blocks. * * @var array * @see setVariable() */ var $_variables = array(); /** * Global variables for substitution * * These are substituted into all blocks, are not cleared on * block parsing and do not trigger "non-empty" logic. I.e. if * only global variables are substituted into the block, it is * still considered "empty". * * @var array * @see setVariable(), setGlobalVariable() */ var $_globalVariables = array(); /** * Root directory for "source" templates * * @var string * @see HTML_Template_Sigma(), setRoot() */ var $fileRoot = ''; /** * Directory for "prepared" templates cache * * @var string * @see HTML_Template_Sigma(), setCacheRoot() */ var $_cacheRoot = null; /** * Flag indicating that the global block was parsed * @var boolean */ var $flagGlobalParsed = false; /** * $_options['preserve_data'] If false, then substitute variables and remove empty * placeholders also in data passed through setVariable (see also bugs #20199, #21951) * $_options['use_preg'] Whether to use preg_replace instead of str_replace in parse() * (this is a HTML_Template_IT backwards compatibility feature, see also bugs #21951, #20392) * $_options['trim_on_save'] Whether to trim extra whitespace from template on cache save. * Generally safe to have this on, unless you have <pre></pre> in templates or want to * preserve HTML indentantion */ var $_options = array( 'preserve_data' => false, 'use_preg' => false, 'trim_on_save' => true ); /** * Function name prefix used when searching for function calls in the template * @var string */ var $functionPrefix = 'func_'; /** * Function name RegExp * @var string */ var $functionnameRegExp = '[_a-zA-Z]+[A-Za-z_0-9]*'; /** * RegExp used to grep function calls in the template (set by the constructor) * * @var string * @see HTML_Template_Sigma() */ var $functionRegExp = ''; /** * List of functions found in the template. * @var array */ var $_functions = array(); /** * List of callback functions specified by the user * @var array */ var $_callback = array(); /** * RegExp used to find file inclusion calls in the template (should have 'e' modifier) * @var string */ var $includeRegExp = '#<!--\s+INCLUDE\s+(\S+)\s+-->#ime'; /** * Files queued for inclusion * @var array */ var $_triggers = array(); /** * Constructor: builds some complex regular expressions and optionally * sets the root directories. * * Make sure that you call this constructor if you derive your template * class from this one. * * @param $root string root directory for templates * @param $cacheRoot string directory to cache "prepared" templates in * @see setRoot() */ function HTML_Template_Sigma($root = '', $cacheRoot = '') { // the class is inherited from PEAR to be able to use $this->setErrorHandling() $this->PEAR(); $this->variablesRegExp = '@'.$this->openingDelimiter.'('.$this->variablenameRegExp.')'.$this->closingDelimiter.'@sm'; $this->removeVariablesRegExp = '@'.$this->openingDelimiter.'\s*('.$this->variablenameRegExp.')\s*'.$this->closingDelimiter.'@sm'; $this->blockRegExp = '@<!--\s+BEGIN\s+('.$this->blocknameRegExp.')\s+-->(.*)<!--\s+END\s+\1\s+-->@sm'; $this->functionRegExp = '@' . $this->functionPrefix . '(' . $this->functionnameRegExp . ')\s*\(@sm'; $this->setRoot($root); $this->setCacheRoot($cacheRoot); } /** * Sets the file root for templates. The file root gets prefixed to all * filenames passed to the object. * * @param string directory name * @see HTML_Template_Sigma() * @access public */ function setRoot($root) { if (('' != $root) && ('/' != substr($root, -1))) { $root .= '/'; } $this->fileRoot = $root; } /** * Sets the directory to cache "prepared" templates in. * * The directory should be writable to PHP, of course. The "prepared" * template is essentially a serialized array of $_blocks, $_blockVariables * and $_children. This allows to bypass expensive calls to * _buildBlockVariables() and especially _buildBlocks() when reading the * "prepared" template instead of the "source" one. * * @param string directory name * @see HTML_Template_Sigma(), _getCached(), _writeCache() * @access public */ function setCacheRoot($root) { if (empty($root)) { return true; } elseif (('' != $root) && ('/' != substr($root, -1))) { $root .= '/'; } $this->_cacheRoot = $root; } /** * Sets the option for the template class * * @access public * @param string option name * @param mixed option value * @return mixed SIGMA_OK on success, error object on failure */ function setOption($option, $value) { if (isset($this->_options[$option])) { $this->_options[$option] = $value; return SIGMA_OK; } return $this->raiseError($this->errorMessage(SIGMA_UNKNOWN_OPTION, $option), SIGMA_UNKNOWN_OPTION); } /** * Returns a textual error message for an error code * * @access public * @param integer error code * @param string additional data to insert into message * @return string error message */ function errorMessage($code, $data = null) { static $errorMessages; if (!isset($errorMessages)) { $errorMessages = array( SIGMA_ERROR => 'unknown error', SIGMA_OK => '', SIGMA_TPL_NOT_FOUND => 'Cannot read the template file \'%s\'', SIGMA_BLOCK_NOT_FOUND => 'Cannot find block \'%s\'', SIGMA_BLOCK_DUPLICATE => 'The name of a block must be unique within a template. Block \'%s\' found twice.', SIGMA_CACHE_ERROR => 'Cannot save template file \'%s\'', SIGMA_UNKNOWN_OPTION => 'Unknown option \'%s\'', SIGMA_PLACEHOLDER_NOT_FOUND => 'Variable placeholder \'%s\' not found', SIGMA_PLACEHOLDER_DUPLICATE => 'Placeholder \'%s\' should be unique, found in multiple blocks', SIGMA_BLOCK_EXISTS => 'Block \'%s\' already exists', SIGMA_CONFLICT => 'Sorry, you cannot use both callbacks and template caching' ); } if (PEAR::isError($code)) { $code = $code->getCode(); } if (!isset($errorMessages[$code])) { return $errorMessages[SIGMA_ERROR]; } else { return (null === $data)? $errorMessages[$code]: sprintf($errorMessages[$code], $data); } } /** * Prints a block with all replacements done. * * @access public * @param string block name * @brother get() */ function show($block = '__global__') { print $this->get($block); } /** * Returns a block with all replacements done. * * @param string block name * @param bool whether to clear parsed block contents * @return string block with all replacements done * @throws PEAR_Error * @access public * @see show() */ function get($block = '__global__', $clear = true) { if (!isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } if ('__global__' == $block && !$this->flagGlobalParsed) { $this->parse('__global__'); } // return the parsed block, removing the unknown placeholders if needed if (!isset($this->_parsedBlocks[$block])) { return ''; } else { $ret = $this->_parsedBlocks[$block]; if ($clear) { unset($this->_parsedBlocks[$block]); } if ($this->removeUnknownVariables) { $ret = preg_replace($this->removeVariablesRegExp, '', $ret); } if ($this->_options['preserve_data']) { $ret = str_replace($this->openingDelimiter . '%preserved%' . $this->closingDelimiter, $this->openingDelimiter, $ret); } return $ret; } } /** * Parses the given block. * * @param string block name * @param boolean true if the function is called recursively * @param boolean true if we are inside a "hidden" block * @access public * @see parseCurrentBlock() * @throws PEAR_Error */ function parse($block = '__global__', $flagRecursion = false, $fakeParse = false) { static $vars; if (!isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } if ('__global__' == $block) { $this->flagGlobalParsed = true; } if (!isset($this->_parsedBlocks[$block])) { $this->_parsedBlocks[$block] = ''; } $outer = $this->_blocks[$block]; if (!$flagRecursion) { $vars = array(); } // block is not empty if its local var is substituted $empty = true; foreach ($this->_blockVariables[$block] as $allowedvar => $v) { if (isset($this->_variables[$allowedvar])) { $vars[$this->openingDelimiter . $allowedvar . $this->closingDelimiter] = $this->_variables[$allowedvar]; $empty = false; // vital for checking "empty/nonempty" status unset($this->_variables[$allowedvar]); } } // processing of the inner blocks if (isset($this->_children[$block])) { foreach ($this->_children[$block] as $innerblock => $v) { $placeholder = $this->openingDelimiter.'__'.$innerblock.'__'.$this->closingDelimiter; if (isset($this->_hiddenBlocks[$innerblock])) { // don't bother actually parsing this inner block; but we _have_ // to go through its local vars to prevent problems on next iteration $this->parse($innerblock, true, true); unset($this->_hiddenBlocks[$innerblock]); $outer = str_replace($placeholder, '', $outer); } else { $this->parse($innerblock, true, $fakeParse); // block is not empty if its inner block is not empty if ('' != $this->_parsedBlocks[$innerblock]) { $empty = false; } $outer = str_replace($placeholder, $this->_parsedBlocks[$innerblock], $outer); $this->_parsedBlocks[$innerblock] = ''; } } } // add "global" variables to the static array foreach ($this->_globalVariables as $allowedvar => $value) { if (isset($this->_blockVariables[$block][$allowedvar])) { $vars[$this->openingDelimiter . $allowedvar . $this->closingDelimiter] = $value; } } // if we are inside a hidden block, don't bother if (!$fakeParse) { // substitute variables only on non-recursive call, thus all // variables from all inner blocks get substituted if (!$flagRecursion && 0 != count($vars)) { $varKeys = $this->_options['use_preg']? array_map(array(&$this, '_addPregDelimiters'), array_keys($vars)): array_keys($vars); $varValues = $this->_options['preserve_data']? array_map(array(&$this, '_preserveOpeningDelimiter'), array_values($vars)): array_values($vars); $funcReplace = $this->_options['use_preg']? 'preg_replace': 'str_replace'; $outer = $funcReplace($varKeys, $varValues, $outer); } // check whether the block is considered "empty" and append parsed content if not if (!$empty || ('__global__' == $block) || !$this->removeEmptyBlocks || isset($this->_touchedBlocks[$block])) { $this->_parsedBlocks[$block] .= $outer; if (isset($this->_touchedBlocks[$block])) { unset($this->_touchedBlocks[$block]); } } } return $empty; } /** * Sets a variable value. * * The function can be used either like setVariable("varname", "value") * or with one array $variables["varname"] = "value" given setVariable($variables) * * @access public * @param mixed $variable variable name or array ('varname'=>'value') * @param string $value variable value if $variable is not an array */ function setVariable($variable, $value = '') { if (is_array($variable)) { $this->_variables = array_merge($this->_variables, $variable); } else { $this->_variables[$variable] = $value; } } /** * Sets a global variable value. * * @access public * @param mixed $variable variable name or array ('varname'=>'value') * @param string $value variable value if $variable is not an array * @see setVariable() */ function setGlobalVariable($variable, $value = '') { if (is_array($variable)) { $this->_globalVariables = array_merge($this->_globalVariables, $variable); } else { $this->_globalVariables[$variable] = $value; } } /** * Sets the name of the current block: the block where variables are added * * @param string block name * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error * @access public */ function setCurrentBlock($block = '__global__') { if (!isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } $this->currentBlock = $block; return SIGMA_OK; } /** * Parses the current block * * @see parse(), setCurrentBlock(), $currentBlock * @access public */ function parseCurrentBlock() { return $this->parse($this->currentBlock); } /** * Returns the current block name * * @return string block name * @access public */ function getCurrentBlock() { return $this->currentBlock; } /** * Preserves the block even if empty blocks should be removed * * This is something special. Sometimes you have blocks that * should be preserved although they are empty (no placeholder replaced). * Think of a shopping basket. If it's empty you have to drop a message to * the user. If it's filled you have to show the contents of the shopping baseket. * Now where do you place the message that the basket is empty? It's no good * idea to place it in you applications as customers tend to like unecessary minor * text changes. Having another template file for an empty basket means that it's * very likely that one fine day the filled and empty basket templates have different * layout. I decided to introduce blocks that to not contain any placeholder but only * text such as the message "Your shopping basked is empty". * * Now if there is no replacement done in such a block the block will be recognized * as "empty" and by default ($removeEmptyBlocks = true) be stripped off. To avoid this * you can call touchBlock() * * @param string block name * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error * @access public * @see $removeEmptyBlocks, $_touchedBlocks */ function touchBlock($block) { if (!isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } if (isset($this->_hiddenBlocks[$block])) { unset($this->_hiddenBlocks[$block]); } $this->_touchedBlocks[$block] = true; return SIGMA_OK; } /** * Hides the block even if it is not "empty" * * Is somewhat an opposite to touchBlock(). * * Consider a block that should be visible to registered/"special" users only, but * its visibility is triggered by some little field passed in a large array * into setVariable(). You can either carefully juggle your variables to prevent the * block from appearing (a fragile solution) or simply call hideBlock() * * @param string block name * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error * @access public */ function hideBlock($block) { if (!isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } if (isset($this->_touchedBlocks[$block])) { unset($this->_touchedBlocks[$block]); } $this->_hiddenBlocks[$block] = true; return SIGMA_OK; } /** * Sets the template. * * You can either load a template file from disk with LoadTemplatefile() or set the * template manually using this function. * * @param string template content * @param boolean remove unknown/unused variables? * @param boolean remove empty blocks? * @see loadTemplatefile() * @access public * @return mixed SIGMA_OK on success, error object on failure */ function setTemplate($template, $removeUnknownVariables = true, $removeEmptyBlocks = true) { $this->_resetTemplate($removeUnknownVariables, $removeEmptyBlocks); // parse the template for function calls only if not using the cache feature if (null === $this->_cacheRoot) { $template = $this->_buildFunctionlist($template); } $list = $this->_buildBlocks('<!-- BEGIN __global__ -->'.$template.'<!-- END __global__ -->'); if (PEAR::isError($list)) { return $list; } $this->_buildBlockVariables(); return SIGMA_OK; } /** * Loads a template file * * If caching is on, then it checks whether a "prepared" template exists * If it does, it gets loaded instead of the original, if it does not, then * the original gets loaded and prepared and then the prepared version is saved. * addBlockfile() and replaceBlockfile() implement quite the same logic. * * @param string filename * @param boolean remove unknown/unused variables? * @param boolean remove empty blocks? * @access public * @return mixed SIGMA_OK on success, error object on failure * @see setTemplate(), $removeUnknownVariables, $removeEmptyBlocks */ function loadTemplateFile($filename, $removeUnknownVariables = true, $removeEmptyBlocks = true) { if ($this->_isCached($filename)) { $this->_resetTemplate($removeUnknownVariables, $removeEmptyBlocks); return $this->_getCached($filename); } $template = $this->_getFile($this->_sourceName($filename)); if (PEAR::isError($template)) { return $template; } $this->_triggers = array(); $template = preg_replace($this->includeRegExp, "\$this->_makeTrigger('\\1', '__global__')", $template); if (PEAR::isError($res = $this->setTemplate($template, $removeUnknownVariables, $removeEmptyBlocks))) { return $res; } else { return $this->_writeCache($filename, '__global__'); } } /** * Adds a block to the template changing a variable placeholder to a block placeholder. * * Add means "replace a variable placeholder by a new block". * This is different to PHPLib templates. The function loads a * block, creates a handle for it and assigns it to a certain * variable placeholder. To to the same with PHPLib templates you would * call set_file() to create the handle and parse() to assign the * parsed block to a variable. By this PHPLibstemplates assume that you tend * to assign a block to more than one one placeholder. To assign a parsed block * to more than only the placeholder you specify in this function you have * to use a combination of get() and setVariable(). * * The block content must not start with <!-- BEGIN blockname --> and end with * <!-- END blockname --> this would cause overhead and produce an error. * * @param string name of the variable placeholder, the name must be unique within the template. * @param string name of the block to be added * @param string content of the block * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error * @see addBlockfile() * @access public */ function addBlock($placeholder, $block, $template) { if (isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_EXISTS, $block), SIGMA_BLOCK_EXISTS); } $parents = $this->_findParentBlocks($placeholder); if (0 == count($parents)) { return $this->raiseError($this->errorMessage(SIGMA_PLACEHOLDER_NOT_FOUND, $placeholder), SIGMA_PLACEHOLDER_NOT_FOUND); } elseif (count($parents) > 1) { return $this->raiseError($this->errorMessage(SIGMA_PLACEHOLDER_DUPLICATE, $placeholder), SIGMA_PLACEHOLDER_DUPLICATE); } $template = "<!-- BEGIN $block -->" . $template . "<!-- END $block -->"; $list = $this->_buildBlocks($template); if (PEAR::isError($list)) { return $list; } $this->_replacePlaceholder($parents[0], $placeholder, $block); $this->_buildBlockVariables($block); return SIGMA_OK; } /** * Adds a block taken from a file to the template, changing a variable placeholder * to a block placeholder. * * @param string name of the variable placeholder * @param string name of the block to be added * @param string template file that contains the block * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error * @brother addBlock() * @access public */ function addBlockfile($placeholder, $block, $filename) { if ($this->_isCached($filename)) { return $this->_getCached($filename, $block, $placeholder); } $template = $this->_getFile($this->_sourceName($filename)); if (PEAR::isError($template)) { return $template; } $template = preg_replace($this->includeRegExp, "\$this->_makeTrigger('\\1', '{$block}')", $template); if (PEAR::isError($res = $this->addBlock($placeholder, $block, $template))) { return $res; } else { return $this->_writeCache($filename, $block); } } /** * Replaces an existing block with new content. * * This function will replace a block of the template and all blocks contained in * the replaced block and add a new block insted, means you can dynamically change your * template. * * In contrast to other systems Sigma analyses the way you've nested blocks and knows * which block belongs into another block. The nesting information helps to make the API * short and simple. Replacing blocks does not only mean that Sigma has to update * the nesting information (relatively time consumpting task) but you have to * make sure that you do not get confused due to the template change itself. * * @param string name of a block to replace * @param string new content * @param boolean true if the parsed contents of the block should be kept * @access public * @see replaceBlockfile(), addBlock() * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error */ function replaceBlock($block, $template, $keepContent = false) { if (!isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } // should not throw a error as we already checked for block existance $this->_removeBlockData($block, $keepContent); $template = "<!-- BEGIN $block -->" . $template . "<!-- END $block -->"; $list = $this->_buildBlocks($template); if (PEAR::isError($list)) { return $list; } // renew the variables list $this->_buildBlockVariables($block); return SIGMA_OK; } /** * Replaces an existing block with new content from a file. * * @param string name of a block to replace * @param string template file that contains the block * @param boolean true if the parsed contents of the block should be kept * @access public * @brother replaceBlock() * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error */ function replaceBlockfile($block, $filename, $keepContent = false) { if ($this->_isCached($filename)) { if (PEAR::isError($res = $this->_removeBlockData($block, $keepContent))) { return $res; } else { return $this->_getCached($filename, $block); } } $template = $this->_getFile($this->_sourceName($filename)); if (PEAR::isError($template)) { return $template; } $template = preg_replace($this->includeRegExp, "\$this->_makeTrigger('\\1', '{$block}')", $template); if (PEAR::isError($res = $this->replaceBlock($block, $template, $keepContent))) { return $res; } else { return $this->_writeCache($filename, $block); } } /** * Checks if the block exists in the template * * @param string block name * @return bool yes/no * @access public */ function blockExists($block) { return isset($this->_blocks[$block]); } /** * Returns the name of the (first) block that contains the specified placeholder. * * @param string Name of the placeholder you're searching * @param string Name of the block to scan. If left out (default) all blocks are scanned. * @return string Name of the (first) block that contains the specified placeholder. * If the placeholder was not found an empty string is returned. * @access public * @throws PEAR_Error */ function placeholderExists($placeholder, $block = '') { if ('' != $block && !isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } if ('' != $block) { // if we search in the specific block, we should just check the array return isset($this->_blockVariables[$block][$placeholder])? $block: ''; } else { // _findParentBlocks returns an array, we need only the first element $parents = $this->_findParentBlocks($placeholder); return empty($parents)? '': $parents[0]; } } // end func placeholderExists //------------------------------------------------------------ // FIXME FIXME FIXME FIXME FIXME FIXME FIXME FIXME FIXME FIXME // // Okay, what follows is essentialy a copy/paste from the "stock" // HTML_Template_ITX. These are the functions that deal with // callbacks inside a template. They somewhat work, just like in // the "stock" version, but they were not cleaned/optimised and // they cannot be mixed with caching: if you do pass a // $cacheRoot to a constructor, then callbacks are disabled. // // Why do I say that callbacks "somewhat" work? Well, they get // parsed only on main template load, not on addBlockfile()/ // replaceBlockfile(). You cannot use placeholders/blocks as // function arguments. You have to explicitly call performCallback(). // To sum it up: DON'T USE 'EM. // // IMNSHO, function calls should be redone as special case of // blocks, _buildFunctionList() should be invoked from // _buildBlocks() and callbacks should be called automatically // on parse. But this is for the next major release... // // FIXME FIXME FIXME FIXME FIXME FIXME FIXME FIXME FIXME FIXME //------------------------------------------------------------ /** * Sets a callback function. * * Sigma templates can contain simple function calls. * "function call" means that the editor of the template can add * special placeholder to the template like 'func_h1("embedded in h1")'. * Sigma will grab this function calls and allow you to define a callback * function for them. * * This is an absolutely evil feature. If your application makes heavy * use of such callbacks and you're even implementing if-then etc. on * the level of a template engine you're reiventing the wheel... - that's * actually how PHP came into life. Anyway, sometimes it's handy. * * Consider also using XML/XSLT or native PHP. * * <?php * ... * function h_one($args) { * return sprintf('<h1>%s</h1>', $args[0]); * } * * ... * $tpl = new HTML_Template_Sigma( ... ); * ... * $tpl->setCallbackFunction('h1', 'h_one'); * $tpl->performCallback(); * ?> * * template: * func_h1('H1 Headline'); * * @param string Function name in the template * @param string Name of the callback function * @param string Name of the callback object * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error * @access public */ function setCallbackFunction($tplFunction, $callbackFunction, $callbackObject = '') { if (null !== $this->_cacheRoot) { return $this->raiseError($this->errorMessage(SIGMA_CONFLICT), SIGMA_CONFLICT); } $this->_callback[$tplFunction] = array( 'function' => $callbackFunction, 'object' => $callbackObject ); return SIGMA_OK; } // end func setCallbackFunction /** * Checks the list of function calls in the template and calls their callback function. * * @access public * @return mixed SIGMA_OK on success, error object on failure * @throws PEAR_Error */ function performCallback() { if (null !== $this->_cacheRoot) { return $this->raiseError($this->errorMessage(SIGMA_CONFLICT), SIGMA_CONFLICT); } foreach ($this->_functions as $funcId => $funcData) { if (isset($this->_callback[$funcData['name']])) { if ('' != $this->_callback[$funcData['name']]['object']) { $this->_variables['__function' . $funcId . '__'] = call_user_func( array( &$GLOBALS[$this->_callback[$funcData['name']]['object']], $this->_callback[$funcData['name']]['function'] ), $funcData['args'] ); } else { $this->_variables['__function' . $funcId . '__'] = call_user_func( $this->_callback[$funcData['name']]['function'], $funcData['args'] ); } } } return SIGMA_OK; } // end func performCallback //------------------------------------------------------------ // // Private methods follow // //------------------------------------------------------------ /** * Reads the file and returns its content * * @param string filename * @return string file content (or error object) */ function _getFile($filename) { if (!($fh = @fopen($filename, 'r'))) { return $this->raiseError($this->errorMessage(SIGMA_TPL_NOT_FOUND, $filename), SIGMA_TPL_NOT_FOUND); } $content = fread($fh, filesize($filename)); fclose($fh); return $content; } /** * Recursively builds a list of all variables within a block * * @param string block name */ function _buildBlockVariables($block = '__global__') { $this->_blockVariables[$block] = array(); preg_match_all($this->variablesRegExp, $this->_blocks[$block], $regs); if (0 != count($regs[1])) { foreach ($regs[1] as $k => $var) { $this->_blockVariables[$block][$var] = true; } } if (isset($this->_children[$block]) && is_array($this->_children[$block])) { foreach ($this->_children[$block] as $child => $v) { $this->_buildBlockVariables($child); } } } /** * Recusively builds a list of all blocks within the template. * * @param string template to be scanned * @see $_blocks * @throws PEAR_Error * @return mixed array of block names on success or error object on failure */ function _buildBlocks($string) { $blocks = array(); if (preg_match_all($this->blockRegExp, $string, $regs, PREG_SET_ORDER)) { foreach ($regs as $k => $match) { $blockname = $match[1]; $blockcontent = $match[2]; if (isset($this->_blocks[$blockname]) || isset($blocks[$blockname])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_DUPLICATE, $blockname), SIGMA_BLOCK_DUPLICATE); } $this->_blocks[$blockname] = $blockcontent; $blocks[$blockname] = true; $inner = $this->_buildBlocks($blockcontent); if (PEAR::isError($inner)) { return $inner; } foreach ($inner as $name => $v) { $pattern = sprintf('@<!--\s+BEGIN\s+%s\s+-->(.*)<!--\s+END\s+%s\s+-->@sm', $name, $name); $replacement = $this->openingDelimiter.'__'.$name.'__'.$this->closingDelimiter; $this->_blocks[$blockname] = preg_replace($pattern, $replacement, $this->_blocks[$blockname]); $this->_children[$blockname][$name] = true; } } } return $blocks; } /** * Resets the object's properties, used before processing a new template * * @access private * @param boolean remove unknown/unused variables? * @param boolean remove empty blocks? * @see setTemplate(), loadTemplateFile() */ function _resetTemplate($removeUnknownVariables = true, $removeEmptyBlocks = true) { $this->removeUnknownVariables = $removeUnknownVariables; $this->removeEmptyBlocks = $removeEmptyBlocks; $this->currentBlock = '__global__'; $this->_variables = array(); $this->_blocks = array(); $this->_children = array(); $this->_parsedBlocks = array(); $this->_touchedBlocks = array(); $this->flagGlobalParsed = false; } // _resetTemplate /** * Checks whether we have a "prepared" template cached * * If we do not do caching, always returns false * * @access private * @param $filename source filename * @return bool yes/no */ function _isCached($filename) { if (null === $this->_cacheRoot) { return false; } $cachedName = $this->_cachedName($filename); $sourceName = $this->_sourceName($filename); if (@file_exists($cachedName) && (filemtime($cachedName) > @filemtime($sourceName))) { return true; } else { return false; } } // _isCached /** * Loads a "prepared" template file * * @access private * @param string filename * @param string block name * @param string variable placeholder to replace by a block * @return mixed SIGMA_OK on success, error object on failure */ function _getCached($filename, $block = '__global__', $placeholder = '') { $content = $this->_getFile($this->_cachedName($filename)); if (PEAR::isError($content)) { return $content; } $cache = unserialize($content); if ('__global__' != $block) { $this->_blocks[$block] = $cache['blocks']['__global__']; $this->_blockVariables[$block] = $cache['variables']['__global__']; $this->_children[$block] = $cache['children']['__global__']; unset($cache['blocks']['__global__'], $cache['variables']['__global__'], $cache['children']['__global__']); } $this->_blocks = array_merge($this->_blocks, $cache['blocks']); $this->_blockVariables = array_merge($this->_blockVariables, $cache['variables']); $this->_children = array_merge($this->_children, $cache['children']); // the same thing gets done in addBlockfile() if (!empty($placeholder)) { $parents = $this->_findParentBlocks($placeholder); $this->_replacePlaceholder($parents[0], $placeholder, $block); } // pull the triggers, if any if (isset($cache['triggers'])) { return $this->_pullTriggers($cache['triggers']); } return SIGMA_OK; } // _getCached /** * Returns a full name of a "prepared" template file * * @access private * @param string source filename, relative to root directory * @return string filename */ function _cachedName($filename) { if ('/' == $filename{0} && '/' == substr($this->_cacheRoot, -1)) { $filename = substr($filename, 1); } $filename = str_replace('/', '__', $filename); return $this->_cacheRoot. $filename. '.it'; } // _cachedName /** * Returns a full name of a "source" template file * * @param string source filename, relative to root directory * @access private * @return string */ function _sourceName($filename) { if ('/' == $filename{0} && '/' == substr($this->fileRoot, -1)) { $filename = substr($filename, 1); } return $this->fileRoot . $filename; } // _sourceName /** * Writes a prepared template to disk * * @access private * @param string source filename, relative to root directory * @param string name of the block to save * @return mixed SIGMA_OK on success, error object on failure */ function _writeCache($filename, $block) { // do not save anything if no cache dir, but do pull triggers if (null !== $this->_cacheRoot) { $cache = array( 'blocks' => array(), 'variables' => array(), 'children' => array() ); $cachedName = $this->_cachedName($filename); $this->_buildCache($cache, $block); if ('__global__' != $block) { foreach (array_keys($cache) as $k) { $cache[$k]['__global__'] = $cache[$k][$block]; unset($cache[$k][$block]); } } if (isset($this->_triggers[$block])) { $cache['triggers'] = $this->_triggers[$block]; } if (!($fh = @fopen($cachedName, 'w'))) { return $this->raiseError($this->errorMessage(SIGMA_CACHE_ERROR, $cachedName), SIGMA_CACHE_ERROR); } fwrite($fh, serialize($cache)); fclose($fh); } // now pull triggers if (isset($this->_triggers[$block])) { if (PEAR::isError($res = $this->_pullTriggers($this->_triggers[$block]))) { return $res; } unset($this->_triggers[$block]); } return SIGMA_OK; } // _writeCache /** * Readies the prepared template for saving * * @access private * @param array prepared template * @param string block to add */ function _buildCache(&$cache, $block) { if (!$this->_options['trim_on_save']) { $cache['blocks'][$block] = $this->_blocks[$block]; } else { $cache['blocks'][$block] = preg_replace( array('/^\\s+/m', '/\\s+$/m', '/(\\r?\\n)+/'), array('', '', "\n"), $this->_blocks[$block] ); } $cache['variables'][$block] = $this->_blockVariables[$block]; if (!isset($this->_children[$block])) { $cache['children'][$block] = array(); } else { $cache['children'][$block] = $this->_children[$block]; foreach (array_keys($this->_children[$block]) as $child) { $this->_buildCache($cache, $child); } } } /** * Recursively removes all data belonging to a block * * @param string block name * @param boolean true if the parsed contents of the block should be kept * @return mixed SIGMA_OK on success, error object on failure * @see replaceBlock(), replaceBlockfile() */ function _removeBlockData($block, $keepContent = false) { if (!isset($this->_blocks[$block])) { return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND); } if (!empty($this->_children[$block])) { foreach (array_keys($this->_children[$block]) as $child) { $this->_removeBlockData($child, false); } unset($this->_children[$block]); } unset($this->_blocks[$block]); unset($this->_blockVariables[$block]); unset($this->_hiddenBlocks[$block]); unset($this->_touchedBlocks[$block]); if (!$keepContent) { unset($this->_parsedBlocks[$block]); } return SIGMA_OK; } /** * Returns the names of the blocks where the variable placeholder appears * * @param string variable name * @return array block names */ function _findParentBlocks($variable) { $parents = array(); foreach ($this->_blockVariables as $blockname => $varnames) { if (!empty($varnames[$variable])) { $parents[] = $blockname; } } return $parents; } /** * Replaces a variable placeholder by a block placeholder * * Of course, it also updates the necessary arrays * * @param string name of the block containing the placeholder * @param string variable name * @param string block name */ function _replacePlaceholder($parent, $placeholder, $block) { $this->_children[$parent][$block] = true; $this->_blockVariables[$parent]['__'.$block.'__'] = true; $this->_blocks[$parent] = str_replace($this->openingDelimiter.$placeholder.$this->closingDelimiter, $this->openingDelimiter.'__'.$block.'__'.$this->closingDelimiter, $this->_blocks[$parent] ); unset($this->_blockVariables[$parent][$placeholder]); } /** * Generates a placeholder to replace an <!-- INCLUDE --> statement * * @access private * @param string filename * @param string current block name * @return string a placeholder */ function _makeTrigger($filename, $block) { $name = 'trigger_' . substr(md5($filename . ' ' . uniqid($block)), 0, 10); $this->_triggers[$block][$name] = $filename; return $this->openingDelimiter . $name . $this->closingDelimiter; } /** * Replaces the "trigger" placeholders by the matching file contents. * * @see addBlockfile() * @param array array ('trigger placeholder' => 'filename') * @return mixed SIGMA_OK on success, error object on failure */ function _pullTriggers($triggers) { foreach ($triggers as $placeholder => $filename) { if (PEAR::isError($res = $this->addBlockfile($placeholder, $placeholder, $filename))) { return $res; } // we actually do not need the resultant block... $parents = $this->_findParentBlocks('__' . $placeholder . '__'); // merge current block's children and variables with the parent's ones if (isset($this->_children[$placeholder])) { $this->_children[$parents[0]] = array_merge($this->_children[$parents[0]], $this->_children[$placeholder]); } $this->_blockVariables[$parents[0]] = array_merge($this->_blockVariables[$parents[0]], $this->_blockVariables[$placeholder]); // substitute the block's contents into parent's $this->_blocks[$parents[0]] = str_replace( $this->openingDelimiter . '__' . $placeholder . '__' . $this->closingDelimiter, $this->_blocks[$placeholder], $this->_blocks[$parents[0]] ); // remove the stuff that is no more needed unset($this->_blocks[$placeholder], $this->_blockVariables[$placeholder], $this->_children[$placeholder]); unset($this->_children[$parents[0]][$placeholder], $this->_blockVariables[$parents[0]]['__' . $placeholder . '__']); } return SIGMA_OK; } /** * Builds a list of functions from the template * * @param string template * @return string template with function calls replaced by placeholders */ function _buildFunctionlist($template) { $this->_functions = array(); $ret = $template; $funcIdx = 0; while (preg_match($this->functionRegExp, $template, $regs)) { $pos = strpos($template, $regs[0]); $template = substr($template, $pos + strlen($regs[0])); $head = $this->_getToken($template, ')'); $args = array(); $ret = str_replace($regs[0] . $head . ')', '{__function' . $funcIdx . '__}', $ret); $template = str_replace($regs[0] . $head . ')', '{__function' . $funcIdx . '__}', $template); while ('' != $head && $args2 = $this->_getToken($head, ',')) { $arg2 = trim($args2); $args[] = ('"' == $arg2{0} || "'" == $arg2{0}) ? substr($arg2, 1, -1) : $arg2; if ($arg2 == $head) { break; } $head = substr($head, strlen($arg2) + 1); } $this->_functions[$funcIdx++] = array( 'name' => $regs[1], 'args' => $args ); } return $ret; } // end func _buildFunctionlist /** * Returns a part of string up to a delimiter * * It should handle strings enclosed in single and double quotes, * thus the need for a non-trivial function * * @param string a string from which to extract * @param mixed a delimiter or an array of ('delimiter' => true) * @return string an extracted string */ function _getToken($code, $delimiter) { if ('' == $code) { return ''; } if (!is_array($delimiter)) { $delimiter = array( $delimiter => true ); } $len = strlen($code); $enclosed = false; $enclosed_by = ''; if (isset($delimiter[$code{0}])) { $i = 1; } else { for ($i = 0; $i < $len; $i++) { $char = $code{$i}; if (('"' == $char || "'" == $char) && ($char == $enclosed_by || '' == $enclosed_by) && (0 == $i || ($i > 0 && '\\' != $code{$i - 1}))) { $enclosed_by = $enclosed? '': $char; $enclosed = !$enclosed; } if (!$enclosed && isset($delimiter[$char])) { break; } } } return substr($code, 0, $i); } // end func _getToken /** * Adds delimiters to a string, so it can be used as a pattern in preg_* functions * * @param string * @return string */ function _addPregDelimiters($str) { return '@' . $str . '@'; } /** * Replaces an opening delimiter by a special string * * @param string * @return string */ function _preserveOpeningDelimiter($str) { return (false === strpos($str, $this->openingDelimiter))? $str: str_replace($this->openingDelimiter, $this->openingDelimiter . '%preserved%' . $this->closingDelimiter, $str); } } ?> Index: pear/HTML_Template_Sigma/docs/readme.txt +++ pear/HTML_Template_Sigma/docs/readme.txt HTML_Template_Sigma: implementation of Integrated Templates API with caching added. This template engine implements Integrated Templates API designed by Ulf Wendel. This API is also implemented by the original Integrated Template (now HTML_Template_IT in PEAR) and thus this package can (probably, YMMV) be used instead of it if you do not call any private methods in your programs and do not directly access objects' private properties. 1) New features and additions to API: a) Transparent caching of prepared templates. You just have to pass a second parameter to the constructor: a directory name (the directory should be writable for PHP). After the template gets parsed for blocks and variables, the resultant arrays are serialized and saved into files in this directory. The next time the template file needs to be loaded, the prepared one is loaded instead, with a cheap unserialize() instead of very expensive RegExp matching logic. b) Global variables. Variables set by a function setGlobalVariable() do not get cleared after first substitution, unlike ordinary ones, and do not trigger "block not empty" logic (block with only global variables is still considered "empty"). Can be used for directory prefixes, session identifiers and the stuff like that. c) hideBlock(): an opposite of touchBlock(). It prevents block from appearing in the result even if it is "not empty". 2) Removed features and incompatibilities: a) $flagCacheTemplatefile and related logic was dropped in favor of a more generic approach, see (1.a) b) $clearCache and $clearCacheOnParse were removed. $clearCache functionality is now controlled by passing a second parameter to get(), $clearCacheOnParse is assumed to always be false. c) Public functions (from IT) init() and free() were removed. Public functions (from ITX) getFunctioncalls(), setFunctioncontent(), getBlocklist(), setCallbackFuntiontable() (sic!), getBlockvariables(), BlockvariableExists() were removed. e) Callback functions and caching do not mix! If you pass a second parameter to the constructor, the template WILL NOT be parsed for function calls. If you attempt to call setCallbackFunction() of performCallback() afterwards, an error will be thrown. Alexey Borzov <avb@php.net> Index: pear/HTML_Template_Sigma/tests/Console_TestListener.php +++ pear/HTML_Template_Sigma/tests/Console_TestListener.php <?php class Console_TestListener extends PHPUnit_TestListener { function addError(&$test, &$t) { $this->_errors += 1; echo " Error $this->_errors in " . $test->getName() . " : $t\n"; } function addFailure(&$test, &$t) { $this->_fails += 1; if ($this->_fails == 1) { echo "\n"; } echo "Failure $this->_fails : $t\n"; } function endTest(&$test) { if ($this->_fails == 0 && $this->_errors == 0) { echo ' Test passed'; } else { echo "There were $this->_fails failures for " . $test->getName() . "\n"; echo "There were $this->_errors errors for " . $test->getName() . "\n"; } echo "\n"; } function startTest(&$test) { $this->_fails = 0; $this->_errors = 0; echo get_class($test) . " : Starting " . $test->getName() . " ..."; } } ?> Index: pear/HTML_Template_Sigma/tests/Sigma_api_testcase.php +++ pear/HTML_Template_Sigma/tests/Sigma_api_testcase.php <?php function _uppercaseCallback($ary) { return strtoupper($ary[0]); } class Sigma_api_TestCase extends PHPUnit_TestCase { /** * A template object * @var object */ var $tpl; function Sigma_api_TestCase($name) { $this->PHPUnit_TestCase($name); } function setUp() { $className = 'HTML_Template_' . $GLOBALS['IT_class']; $this->tpl =& new $className('./templates'); } function tearDown() { unset($this->tpl); } function _stripWhitespace($str) { return preg_replace('/\\s+/', '', $str); } function _methodExists($name) { if (in_array(strtolower($name), get_class_methods($this->tpl))) { return true; } $this->assertTrue(false, 'method '. $name . ' not implemented in ' . get_class($this->tpl)); return false; } /** * Tests a setTemplate method * */ function testSetTemplate() { $result = $this->tpl->setTemplate('A template', false, false); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error setting template: '. $result->getMessage()); } $this->assertEquals('A template', $this->tpl->get()); } /** * Tests a loadTemplatefile method * */ function testLoadTemplatefile() { $result = $this->tpl->loadTemplatefile('loadtemplatefile.html', false, false); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->assertEquals('A template', trim($this->tpl->get())); } /** * Tests a setVariable method * */ function testSetVariable() { $result = $this->tpl->setTemplate('{placeholder1} {placeholder2} {placeholder3}', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error setting template: '. $result->getMessage()); } // "scalar" call $this->tpl->setVariable('placeholder1', 'var1'); // array call $this->tpl->setVariable(array( 'placeholder2' => 'var2', 'placeholder3' => 'var3' )); $this->assertEquals('var1 var2 var3', $this->tpl->get()); } /** * Tests the <!-- INCLUDE --> functionality * */ function testInclude() { $result = $this->tpl->loadTemplateFile('include.html', false, false); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->assertEquals('Master file; Included file', trim($this->tpl->get())); } /** * */ function testCurrentBlock() { $result = $this->tpl->loadTemplateFile('blockiteration.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->tpl->setVariable('outer', 'a'); $this->tpl->setCurrentBlock('inner_block'); for ($i = 0; $i < 5; $i++) { $this->tpl->setVariable('inner', $i + 1); $this->tpl->parseCurrentBlock(); } // for $this->assertEquals('a|1|2|3|4|5#', $this->_stripWhitespace($this->tpl->get())); } /** * */ function testRemovePlaceholders() { $result = $this->tpl->setTemplate('{placeholder1},{placeholder2},{placeholder3}', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error setting template: '. $result->getMessage()); } // we do not set {placeholder3} $this->tpl->setVariable(array( 'placeholder1' => 'var1', 'placeholder2' => 'var2' )); $this->assertEquals('var1,var2,', $this->tpl->get()); // Now, we should really add a switch for keeping {stuff} in // data supplied to setVariable() safe. Until then, removing it should // be expected behaviour $result = $this->tpl->setTemplate('{placeholder1},{placeholder2},{placeholder3}', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error setting template: '. $result->getMessage()); } $this->tpl->setVariable(array( 'placeholder1' => 'var1', 'placeholder2' => 'var2', 'placeholder3' => 'var3{stuff}' )); $this->assertEquals('var1,var2,var3', $this->tpl->get()); } /** * */ function testTouchBlock() { $result = $this->tpl->loadTemplateFile('blockiteration.html', false, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->tpl->setVariable('outer', 'data'); // inner_block should be preserved in output, even if empty $this->tpl->touchBlock('inner_block'); $this->assertEquals('data|{inner}#', $this->_stripWhitespace($this->tpl->get())); } // Not available in stock class /** * */ function testHideBlock() { if (!$this->_methodExists('hideBlock')) { return; } $result = $this->tpl->loadTemplateFile('blockiteration.html', false, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->tpl->setVariable(array( 'outer' => 'data', 'inner' => 'stuff' )); // inner_block is not empty, but should be removed nonetheless $this->tpl->hideBlock('inner_block'); $this->assertEquals('data#', $this->_stripWhitespace($this->tpl->get())); } /** * */ function testSetGlobalVariable() { if (!$this->_methodExists('setGlobalVariable')) { return; } $result = $this->tpl->loadTemplateFile('globals.html', false, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->tpl->setGlobalVariable('glob', 'glob'); // {var2} is not, block_two should be removed $this->tpl->setVariable(array( 'var1' => 'one', 'var3' => 'three' )); for ($i = 0; $i < 3; $i++) { $this->tpl->setVariable('var4', $i + 1); $this->tpl->parse('block_four'); } // for $this->assertEquals('glob:one#glob:three|glob:1|glob:2|glob:3#', $this->_stripWhitespace($this->tpl->get())); } function testOptionUsePreg() { if (!$this->_methodExists('setOption')) { return; } $this->tpl->setTemplate('{var}', true, true); $this->tpl->setOption('use_preg', true); $this->tpl->setVariable('var', 'All templates for $1 or \\$2!'); $this->assertEquals('All templates for or $2!', $this->tpl->get()); } function testOptionPreserveData() { if (!$this->_methodExists('setOption')) { return; } $this->tpl->setTemplate('{placeholder1},{placeholder2},{placeholder3}', true, true); $this->tpl->setOption('preserve_data', true); $this->tpl->setVariable(array( 'placeholder1' => 'var1', 'placeholder2' => 'var2', 'placeholder3' => 'var3{stuff}' )); $this->assertEquals('var1,var2,var3{stuff}', $this->tpl->get()); } function testPlaceholderExists() { $this->tpl->setTemplate('{var}'); $this->assertTrue($this->tpl->placeholderExists('var'), 'Existing placeholder \'var\' reported as nonexistant'); $this->assertTrue(!$this->tpl->placeholderExists('foobar'), 'Nonexistant placeholder \'foobar\' reported as existing'); $this->assertTrue($this->tpl->placeholderExists('var', '__global__'), 'Existing in block \'__global__\' placeholder \'var\' reported as nonexistant'); $this->assertTrue(!$this->tpl->placeholderExists('foobar', '__global__'), 'Nonexistant in block \'__global__\' placeholder \'foobar\' reported as existing'); } function testBlockExists() { $this->tpl->setTemplate('{var}'); $this->assertTrue($this->tpl->blockExists('__global__'), 'Existing block \'__global__\' reported as nonexistant'); $this->assertTrue(!$this->tpl->blockExists('foobar'), 'Nonexistant block \'foobar\' reported as existing'); } function testAddBlock() { $result = $this->tpl->loadTemplatefile('blocks.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->tpl->addBlock('var', 'added', 'added:{new_var}'); $this->assertTrue($this->tpl->blockExists('added'), 'The new block seems to be missing'); $this->assertTrue(!$this->tpl->placeholderExists('var'), 'The old variable seems to be still present in the template'); $this->tpl->setVariable('new_var', 'new_value'); $this->assertEquals('added:new_value', $this->_stripWhitespace($this->tpl->get())); } function testAddBlockfile() { $result = $this->tpl->loadTemplatefile('blocks.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $result = $this->tpl->addBlockfile('var', 'added', 'addblock.html'); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error adding block from file: '. $result->getMessage()); } $this->assertTrue($this->tpl->blockExists('added'), 'The new block seems to be missing'); $this->assertTrue(!$this->tpl->placeholderExists('var'), 'The old variable seems to be still present in the template'); $this->tpl->setVariable('new_var', 'new_value'); $this->assertEquals('added:new_value', $this->_stripWhitespace($this->tpl->get())); } function testReplaceBlock() { $result = $this->tpl->loadTemplatefile('blocks.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->tpl->setVariable('old_var', 'old_value'); $this->tpl->parse('old_block'); // old_block's contents should be discarded $this->tpl->replaceBlock('old_block', 'replaced:{replaced_var}#', false); $this->assertTrue(!$this->tpl->blockExists('old_inner_block') && !$this->tpl->placeholderExists('old_var'), 'The replaced block\'s contents seem to be still present'); $this->tpl->setVariable('replaced_var', 'replaced_value'); $this->tpl->parse('old_block'); // this time old_block's contents should be preserved $this->tpl->replaceBlock('old_block', 'replaced_again:{brand_new_var}', true); $this->tpl->setVariable('brand_new_var', 'brand_new_value'); $this->assertEquals('replaced:replaced_value#replaced_again:brand_new_value', $this->_stripWhitespace($this->tpl->get())); } function testReplaceBlockfile() { $result = $this->tpl->loadTemplatefile('blocks.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } $this->tpl->setVariable('old_var', 'old_value'); $this->tpl->parse('old_block'); // old_block's contents should be discarded $result = $this->tpl->replaceBlockfile('old_block', 'replaceblock.html', false); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error replacing block from file: '. $result->getMessage()); } $this->assertTrue(!$this->tpl->blockExists('old_inner_block') && !$this->tpl->placeholderExists('old_var'), 'The replaced block\'s contents seem to be still present'); $this->tpl->setVariable(array( 'replaced_var' => 'replaced_value', 'replaced_inner_var' => 'inner_value' )); $this->tpl->parse('old_block'); // this time old_block's contents should be preserved $result = $this->tpl->replaceBlockfile('old_block', 'addblock.html', true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error replacing block from file: '. $result->getMessage()); } $this->tpl->setVariable('new_var', 'again'); $this->assertEquals('replaced:replaced_value|inner_value#added:again', $this->_stripWhitespace($this->tpl->get())); } function testCallback() { $this->tpl->setTemplate('callback:func_uppercase(word)'); $this->tpl->setCallbackFunction('uppercase', '_uppercaseCallback'); $res = $this->tpl->performCallback(); if (PEAR::isError($res)) { $this->assertTrue(false, 'Error performing callback: '. $res->getMessage()); } $this->assertEquals('callback:WORD', $this->tpl->get()); } } ?> Index: pear/HTML_Template_Sigma/tests/Sigma_cache_testcase.php +++ pear/HTML_Template_Sigma/tests/Sigma_cache_testcase.php <?php require_once 'Sigma_api_testcase.php'; class Sigma_cache_TestCase extends Sigma_api_TestCase { function Sigma_cache_TestCase($name) { $this->Sigma_api_TestCase($name); } function setUp() { global $Sigma_cache_dir; $className = 'HTML_Template_' . $GLOBALS['IT_class']; $this->tpl =& new $className('./templates', $Sigma_cache_dir); } function _removeCachedFiles($filename) { if (!is_array($filename)) { $filename = array($filename); } foreach ($filename as $file) { $cachedName = $this->tpl->_cachedName($file); if (@file_exists($cachedName)) { @unlink($cachedName); } } } function assertCacheExists($filename) { if (!is_array($filename)) { $filename = array($filename); } foreach ($filename as $file) { $cachedName = $this->tpl->_cachedName($file); if (!@file_exists($cachedName)) { $this->assertTrue(false, "File '$file' is not cached"); } } } function testLoadTemplatefile() { if (!$this->_methodExists('_isCached')) { return; } $this->_removeCachedFiles('loadtemplatefile.html'); parent::testLoadTemplateFile(); $this->assertCacheExists('loadtemplatefile.html'); parent::testLoadTemplateFile(); } function testAddBlockfile() { if (!$this->_methodExists('_isCached')) { return; } $this->_removeCachedFiles(array('blocks.html', 'addblock.html')); parent::testAddBlockfile(); $this->assertCacheExists(array('blocks.html', 'addblock.html')); parent::testAddBlockfile(); } function testReplaceBlockFile() { if (!$this->_methodExists('_isCached')) { return; } $this->_removeCachedFiles(array('blocks.html', 'replaceblock.html')); parent::testReplaceBlockfile(); $this->assertCacheExists(array('blocks.html', 'replaceblock.html')); parent::testReplaceBlockfile(); } function testInclude() { if (!$this->_methodExists('_isCached')) { return; } $this->_removeCachedFiles(array('include.html', '__include.html')); parent::testInclude(); $this->assertCacheExists(array('include.html', '__include.html')); parent::testInclude(); } } ?> Index: pear/HTML_Template_Sigma/tests/Sigma_usage_testcase.php +++ pear/HTML_Template_Sigma/tests/Sigma_usage_testcase.php <?php class Sigma_Usage_TestCase extends PHPUnit_TestCase { /** * An HTML_Template_IT object * @var object */ var $tpl; function Sigma_Usage_TestCase($name) { $this->PHPUnit_TestCase($name); } function setUp() { $className = 'HTML_Template_' . $GLOBALS['IT_class']; $this->tpl =& new $className('./templates'); } function tearDown() { unset($this->tpl); } function _stripWhitespace($str) { return preg_replace('/\\s+/', '', $str); } function _methodExists($name) { if (in_array(strtolower($name), get_class_methods($this->tpl))) { return true; } $this->assertTrue(false, 'method '. $name . ' not implemented in ' . get_class($this->tpl)); return false; } /** * Tests iterations over two blocks * */ function testBlockIteration() { $data = array( 'a', array('b', array('1', '2', '3', '4')), 'c', array('d', array('5', '6', '7')) ); $result = $this->tpl->loadTemplateFile('blockiteration.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } foreach ($data as $value) { if (is_array($value)) { $this->tpl->setVariable('outer', $value[0]); foreach ($value[1] as $v) { $this->tpl->setVariable('inner', $v); $this->tpl->parse('inner_block'); } } else { $this->tpl->setVariable('outer', $value); } $this->tpl->parse('outer_block'); } $this->assertEquals('a#b|1|2|3|4#c#d|5|6|7#', $this->_stripWhitespace($this->tpl->get())); } /** * * */ function testTouchBlockIteration() { $data = array('a','b','c','d','e'); $result = $this->tpl->loadTemplateFile('blockiteration.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } for ($i = 0; $i < count($data); $i++) { $this->tpl->setVariable('outer', $data[$i]); // the inner_block is empty and should be removed if (0 == $i % 2) { $this->tpl->touchBlock('inner_block'); } $this->tpl->parse('outer_block'); } $this->assertEquals('a|#b#c|#d#e|#', $this->_stripWhitespace($this->tpl->get())); } // Not available in stock version /** * */ function testHideBlockIteration() { if (!$this->_methodExists('hideBlock')) { return; } $data = array('a','b','c','d','e'); $result = $this->tpl->loadTemplateFile('blockiteration.html', true, true); if (PEAR::isError($result)) { $this->assertTrue(false, 'Error loading template file: '. $result->getMessage()); } for ($i = 0; $i < count($data); $i++) { $this->tpl->setVariable(array( 'inner' => $i + 1, 'outer' => $data[$i] )); // the inner_block is not empty, but should be removed if (0 == $i % 2) { $this->tpl->hideBlock('inner_block'); } $this->tpl->parse('outer_block'); } $this->assertEquals('a#b|2#c#d|4#e#', $this->_stripWhitespace($this->tpl->get())); } } ?> Index: pear/HTML_Template_Sigma/tests/test.php +++ pear/HTML_Template_Sigma/tests/test.php <?php // This is where "prepared" templates will be stored, if you decide to run // Sigma_cache_testcase. Make sure that this directory is writeable for PHP $Sigma_cache_dir = './templates/prepared'; // What class are we going to test? // It is possible to also use the unit tests to test HTML_Template_ITX, which // also implements Integrated Templates API $IT_class = 'Sigma'; // $IT_class = 'ITX'; // Sigma_cache_testcase is useless if testing HTML_Template_ITX $testcases = array( 'Sigma_api_testcase', 'Sigma_cache_testcase', 'Sigma_usage_testcase' ); // BC hack to define PATH_SEPARATOR for version of PHP prior 4.3 if(!defined('PATH_SEPARATOR')) { if(defined('DIRECTORY_SEPARATOR') && DIRECTORY_SEPARATOR == "\\") { define('PATH_SEPARATOR', ';'); } else { define('PATH_SEPARATOR', ':'); } } ini_set('include_path', '..'.PATH_SEPARATOR.ini_get('include_path')); require_once 'PHPUnit.php'; require_once $IT_class . '.php'; $suite =& new PHPUnit_TestSuite(); foreach ($testcases as $testcase) { include_once $testcase . '.php'; $methods = preg_grep('/^test/', get_class_methods($testcase)); foreach ($methods as $method) { $suite->addTest(new $testcase($method)); } } require_once 'Console_TestListener.php'; $result =& new PHPUnit_TestResult(); $result->addListener(new Console_TestListener); $suite->run($result); ?> Index: pear/HTML_Template_Sigma/tests/templates/__include.html +++ pear/HTML_Template_Sigma/tests/templates/__include.html Included file Index: pear/HTML_Template_Sigma/tests/templates/addblock.html +++ pear/HTML_Template_Sigma/tests/templates/addblock.html added:{new_var} Index: pear/HTML_Template_Sigma/tests/templates/blockiteration.html +++ pear/HTML_Template_Sigma/tests/templates/blockiteration.html <!-- BEGIN outer_block --> {outer} <!-- BEGIN inner_block --> |{inner} <!-- END inner_block --> # <!-- END outer_block --> Index: pear/HTML_Template_Sigma/tests/templates/blocks.html +++ pear/HTML_Template_Sigma/tests/templates/blocks.html {var} <!-- BEGIN old_block --> old:{old_var} <!-- BEGIN old_inner_block --> |{old_inner_var} <!-- END old_inner_block --> # <!-- END old_block --> Index: pear/HTML_Template_Sigma/tests/templates/globals.html +++ pear/HTML_Template_Sigma/tests/templates/globals.html <!-- BEGIN block_one --> {glob}:{var1}# <!-- END block_one --> <!-- BEGIN block_two --> {glob}:{var2}# <!-- END block_two --> <!-- BEGIN block_three --> {glob}:{var3} <!-- BEGIN block_four --> |{glob}:{var4} <!-- END block_four --> # <!-- END block_three --> Index: pear/HTML_Template_Sigma/tests/templates/include.html +++ pear/HTML_Template_Sigma/tests/templates/include.html Master file; <!-- INCLUDE __include.html --> Index: pear/HTML_Template_Sigma/tests/templates/loadtemplatefile.html +++ pear/HTML_Template_Sigma/tests/templates/loadtemplatefile.html A template Index: pear/HTML_Template_Sigma/tests/templates/replaceblock.html +++ pear/HTML_Template_Sigma/tests/templates/replaceblock.html replaced:{replaced_var} <!-- BEGIN new_inner_block --> |{replaced_inner_var} <!-- END new_inner_block --> #