* template engines (PHP code, XSLT, etc.) without requiring usage
* changes.
*
+ * Compilation code adapted from code written by Bruno Pedro <bpedro@ptm.pt>.
+ *
* Copyright 2002-2009 The Horde Project (http://www.horde.org/)
*
* See the enclosed file COPYING for license information (LGPL). If you
*/
class Horde_Template
{
+ /** The identifier to use for memory-only templates. */
+ const TEMPLATE_STRING = '**string';
+
+ /**
+ * The Horde_Cache object to use.
+ *
+ * @var Horde_Cache
+ */
+ protected $_cache;
+
/**
* Option values.
*
protected $_arrays = array();
/**
- * Cloop tag values.
+ * Path to template source.
*
- * @var array
+ * @var string
*/
- protected $_carrays = array();
+ protected $_templateFile = null;
+
+ /**
+ * Template source.
+ *
+ * @var string
+ */
+ protected $_template = null;
/**
- * If tag values.
+ * Foreach variable mappings.
*
* @var array
*/
- protected $_ifs = array();
+ protected $_foreachMap = array();
/**
- * Name of cached template file.
+ * Foreach variable incrementor.
*
- * @var string
+ * @var integer
*/
- protected $_templateFile = null;
+ protected $_foreachVar = 0;
/**
- * Cached source of template file.
+ * preg_match() cache.
*
- * @var string
+ * @var array
*/
- protected $_template = null;
+ protected $_pregcache = array();
/**
- * Constructor. Can set the template base path and whether or not
- * to drop template variables after a parsing a template.
+ * Constructor.
*
- * @param string $basepath The directory where templates are read from.
+ * @param string $basepath The directory where templates are read from.
*/
public function __construct($basepath = null)
{
if (!is_null($basepath)) {
$this->_basepath = $basepath;
}
+
+ try {
+ $this->_cache = Horde_Cache::singleton($GLOBALS['conf']['cache']['driver'], Horde::getDriverConfig('cache', $GLOBALS['conf']['cache']['driver']));
+ } catch (Horde_Exception $e) {}
}
/**
* Sets an option.
+ * Currently available options are:
+ * <pre>
+ * 'debug' - Output debugging information to screen
+ * 'forcecompile' - Force a compilation on every page load
+ * 'gettext' - Activate gettext detection
+ * <pre>
*
* @param string $option The option name.
- * @param mixed $val The option's value.
+ * @param mixed $val The option's value.
*/
public function setOption($option, $val)
{
public function setTemplate($template)
{
$this->_template = $template;
- $this->_templateFile = 'string';
+ $this->_parse();
+ $this->_templateFile = self::TEMPLATE_STRING;
}
/**
*
* @param string|array $tag Either the tag name or a hash with tag names
* as keys and tag values as values.
- * @param mixed $var The value to replace the tag with.
- * @param boolean $isIf Is this for an <if:> tag? (Default: no).
+ * @param mixed $var The value to replace the tag with.
*/
- public function set($tag, $var, $isIf = false)
+ public function set($tag, $var)
{
if (is_array($tag)) {
foreach ($tag as $tTag => $tVar) {
- $this->set($tTag, $tVar, $isIf);
+ $this->set($tTag, $tVar);
}
} elseif (is_array($var) || is_object($var)) {
$this->_arrays[$tag] = $var;
- if ($isIf) {
- // Just store the same variable that we stored in
- // $this->_arrays - if we don't modify it, PHP's
- // reference counting ensures we're not using any
- // additional memory here.
- $this->_ifs[$tag] = $var;
- }
} else {
$this->_scalars[$tag] = $var;
- if ($isIf) {
- // Just store the same variable that we stored in
- // $this->_scalars - if we don't modify it, PHP's
- // reference counting ensures we're not using any
- // additional memory here.
- $this->_ifs[$tag] = $var;
- }
}
}
/**
- * Sets values for a cloop.
- *
- * @param string $tag The name of the cloop.
- * @param array $array The values for the cloop.
- * @param array $cases The cases (test values) for the cloops.
- */
- public function setCloop($tag, $array, $cases)
- {
- $this->_carrays[$tag] = array(
- 'array' => $array,
- 'cases' => $cases,
- );
- }
-
- /**
* Returns the value of a tag or loop.
*
* @param string $tag The tag name.
{
if (isset($this->_arrays[$tag])) {
return $this->_arrays[$tag];
- } elseif (isset($this->_scalars[$tag])) {
+ }
+ if (isset($this->_scalars[$tag])) {
return $this->_scalars[$tag];
}
return null;
* @param string $filename The file to fetch the template from.
*
* @return string The parsed template.
- * @throws Horde_Exception
*/
- public function fetch($filename)
+ public function fetch($filename = null)
{
- $contents = $this->_getTemplate($filename);
+ $file = $this->_basepath . $filename;
+ $force = $this->getOption('forcecompile');
+
+ if (!is_null($filename) && ($file != $this->_templateFile)) {
+ $this->_template = $this->_templateFile = null;
+ }
+
+ /* First, check for a cached compiled version. */
+ $cacheid = 'horde_template|' . filemtime($file) . '|' . $file . '|' . $this->getOption('gettext');
+ if (!$force && is_null($this->_template) && $this->_cache) {
+ $this->_template = $this->_cache->get($cacheid, 0);
+ if ($this->_template === false) {
+ $this->_template = null;
+ }
+ }
- // Parse and return the contents.
- return $this->parse($contents);
+ /* Parse and compile the template. */
+ if ($force || is_null($this->_template)) {
+ $this->_template = str_replace("\n", " \n", file_get_contents($file));
+ $this->_parse();
+ if ($this->_cache &&
+ isset($cacheid) &&
+ !$this->_cache->set($cacheid, $this->_template)) {
+ Horde::logMessage(sprintf(_("Could not save the compiled template file '%s'."), $file), __FILE__, __LINE__, PEAR_LOG_ERR);
+ }
+ }
+
+ $this->_templateFile = $file;
+
+ /* Template debugging. */
+ if ($this->getOption('debug')) {
+ echo '<pre>' . htmlspecialchars($this->_template) . '</pre>';
+ }
+
+ return $this->parse();
}
/**
*/
public function parse($contents = null)
{
- if (is_null($contents)) {
- $contents = $this->_template;
- }
-
- // Process ifs.
- if (!empty($this->_ifs)) {
- foreach (array_keys($this->_ifs) as $tag) {
- $contents = $this->_parseIf($tag, $contents);
- }
+ if (!is_null($contents)) {
+ $this->setTemplate(str_replace("\n", " \n", $contents));
}
- // Process tags.
- $replace = $search = array();
- reset($this->_scalars);
- while (list($key, $value) = each($this->_scalars)) {
- $search[] = $this->_getTag($key);
- $replace[] = $value;
- }
- if (count($search)) {
- $contents = str_replace($search, $replace, $contents);
- }
+ /* Evaluate the compiled template and return the output. */
+ ob_start();
+ eval('?>' . $this->_template);
+ return is_null($contents)
+ ? ob_get_clean()
+ : str_replace(" \n", "\n", ob_get_clean());
+ }
- // Process cloops.
- reset($this->_carrays);
- while (list($key, $array) = each($this->_carrays)) {
- $contents = $this->_parseCloop($key, $array, $contents);
- }
+ /**
+ * Parses all variables/tags in the template.
+ */
+ protected function _parse()
+ {
+ // Escape XML instructions.
+ $this->_template = preg_replace('/\?>|<\?/', '<?php echo \'$0\' ?>', $this->_template);
// Parse gettext tags, if the option is enabled.
if ($this->getOption('gettext')) {
- $contents = $this->_parseGettext($contents);
+ $this->_parseGettext();
}
+ // Process ifs.
+ $this->_parseIf();
+
// Process loops and arrays.
- reset($this->_arrays);
- while (list($key, $array) = each($this->_arrays)) {
- $contents = $this->_parseLoop($key, $array, $contents);
- }
+ $this->_parseLoop();
- // Return parsed template.
- return $contents;
+ // Process base scalar tags. Needs to be after _parseLoop() as we
+ // rely on _foreachMap().
+ $this->_parseTags();
+
+ // Finally, process any associative array scalar tags.
+ $this->_parseAssociativeTags();
}
/**
- * Returns full start and end tags for a named tag.
- *
- * @param string $tag The name of the tag.
- * @param string $directive The kind of tag [tag, if, loop, cloop].
- *
- * @return array 'b' => Start tag, 'e' => End tag.
+ * Parses gettext tags.
*/
- protected function _getTags($tag, $directive)
+ protected function _parseGettext()
{
- return array(
- 'b' => '<' . $directive . ':' . $tag . '>',
- 'e' => '</' . $directive . ':' . $tag . '>'
- );
+ if (preg_match_all("/<gettext>(.+?)<\/gettext>/s", $this->_template, $matches, PREG_SET_ORDER)) {
+ $replace = array();
+ foreach ($matches as $val) {
+ $replace[$val[0]] = '<?php echo _(\'' . str_replace("'", "\\'", $val[1]) . '\'); ?>';
+ }
+ $this->_doReplace($replace);
+ }
}
/**
- * Formats a scalar tag (default format is <tag:name>).
+ * Parses 'if' statements.
*
- * @param string $tag The name of the tag.
- *
- * @return string The full tag with the current start/end delimiters.
+ * @param string $key The key prefix to parse.
*/
- protected function _getTag($tag)
+ protected function _parseIf($key = null)
{
- return '<tag:' . $tag . ' />';
+ $replace = array();
+
+ foreach ($this->_doSearch('if', $key) as $val) {
+ $replace[$val[0]] = '<?php if (!empty(' . $this->_generatePHPVar('scalars', $val[1]) . ') || !empty(' . $this->_generatePHPVar('arrays', $val[1]) . ')): ?>';
+ $replace[$val[2]] = '<?php endif; ?>';
+
+ // Check for else statement.
+ foreach ($this->_doSearch('else', $key) as $val2) {
+ $replace[$val2[0]] = '<?php else: ?>';
+ $replace[$val2[2]] = '';
+ }
+ }
+
+ $this->_doReplace($replace);
}
/**
- * Extracts a portion of a template.
+ * Parses the given array for any loops or other uses of the array.
*
- * @param array $t The tag to extract. Hash format is:
- * $t['b'] - The start tag
- * $t['e'] - The end tag
- * @param string &$contents The template to extract from.
+ * @param string $key The key prefix to parse.
*/
- protected function _getStatement($t, &$contents)
+ protected function _parseLoop($key = null)
{
- // Locate the statement.
- $pos = strpos($contents, $t['b']);
- if ($pos === false) {
- return false;
- }
+ $replace = array();
- $tag_length = strlen($t['b']);
- $fpos = $pos + $tag_length;
- $lpos = strpos($contents, $t['e']);
- $length = $lpos - $fpos;
+ foreach ($this->_doSearch('loop', $key) as $val) {
+ $divider = null;
- // Extract & return the statement.
- return substr($contents, $fpos, $length);
+ // See if we have a divider.
+ if (preg_match("/<divider:" . $val[1] . ">(.*)<\/divider:" . $val[1] . ">/sU", $this->_template, $m)) {
+ $divider = $m[1];
+ $replace[$m[0]] = '';
+ }
+
+ if (!isset($this->_foreachMap[$val[1]])) {
+ $this->_foreachMap[$val[1]] = ++$this->_foreachVar;
+ }
+ $varId = $this->_foreachMap[$val[1]];
+ $var = $this->_generatePHPVar('arrays', $val[1]);
+
+ $replace[$val[0]] = '<?php ' .
+ (($divider) ? '$i' . $varId . ' = count(' . $var . '); ' : '') .
+ 'foreach (' . $this->_generatePHPVar('arrays', $val[1]) . ' as $k' . $varId . ' => $v' . $varId . '): ?>';
+ $replace[$val[2]] = '<?php ' .
+ (($divider) ? 'if (--$i' . $varId . ' != 0) { echo \'' . $divider . '\'; }; ' : '') .
+ 'endforeach; ?>';
+
+ // Parse ifs.
+ $this->_parseIf($val[1]);
+
+ // Parse interior loops.
+ $this->_parseLoop($val[1]);
+
+ // Replace scalars.
+ $this->_parseTags($val[1]);
+ }
+
+ $this->_doReplace($replace);
}
/**
- * Parses gettext tags.
- *
- * @param string $contents The unparsed content of the file.
+ * Replaces 'tag' tags with their PHP equivalents.
*
- * @return string The parsed contents of the gettext blocks.
+ * @param string $key The key prefix to parse.
*/
- protected function _parseGettext($contents)
+ protected function _parseTags($key = null)
{
- // Get the tags & loop.
- $t = array(
- 'b' => '<gettext>',
- 'e' => '</gettext>'
- );
-
- while ($text = $this->_getStatement($t, $contents)) {
- $contents = str_replace($t['b'] . $text . $t['e'], _($text), $contents);
+ $replace = array();
+
+ foreach ($this->_doSearch('tag', $key, true) as $val) {
+ $replace_text = '<?php ';
+ if (isset($this->_foreachMap[$val[1]])) {
+ $var = $this->_foreachMap[$val[1]];
+ $replace_text .= 'if (isset($v' . $var . ')) { echo is_array($v' . $var . ') ? $k' . $var . ' : $v' . $var . '; } else';
+ }
+ $var = $this->_generatePHPVar('scalars', $val[1]);
+ $replace[$val[0]] = $replace_text . 'if (isset(' . $var . ')) { echo ' . $var . '; } ?>';
}
- return $contents;
+ $this->_doReplace($replace);
}
/**
- * Parses a given if statement.
- *
- * @param string $tag The name of the if block to parse.
- * @param string $contents The unparsed contents of the if block.
- *
- * @return string The parsed contents of the if block.
+ * Parse associative tags (i.e. <tag:foo.bar />).
*/
- protected function _parseIf($tag, $contents, $key = null)
+ protected function _parseAssociativeTags()
{
- // Get the tags & if statement.
- $t = $this->_getTags($tag, 'if');
- $et = $this->_getTags($tag, 'else');
+ $replace = array();
- // explode the tag, so we have the correct keys for the array
- if (isset($key)) {
- list($tg, $k) = explode('.', $tag);
+ foreach ($this->_pregcache['tag'] as $key => $val) {
+ $parts = explode('.', $val[1]);
+ $var = '$this->_arrays[\'' . $parts[0] . '\'][\'' . $parts[1] . '\']';
+ $replace[$val[0]] = '<?php if (isset(' . $var . ')) { echo ' . $var . '; } ?>';
+ unset($this->_pregcache['tag'][$key]);
}
- while (($if = $this->_getStatement($t, $contents)) !== false) {
- // Check for else statement.
- if ($else = $this->_getStatement($et, $if)) {
- // Process the if statement.
- $replace = ((isset($key) && $this->_ifs[$tg][$key][$k]) || (isset($this->_ifs[$tag]) && $this->_ifs[$tag]))
- ? str_replace($et['b'] . $else . $et['e'], '', $if)
- : $else;
- } else {
- // Process the if statement.
- $replace = isset($key)
- ? ($this->_ifs[$tg][$key][$k] ? $if : null)
- : ($this->_ifs[$tag] ? $if : null);
- }
- // Parse the template.
- $contents = str_replace($t['b'] . $if . $t['e'], $replace, $contents);
- }
-
- // Return parsed template.
- return $contents;
+ $this->_doReplace($replace);
}
/**
- * Parses the given array for any loops or other uses of the array.
- *
- * @param string $tag The name of the loop to parse.
- * @param array $array The values for the loop.
- * @param string $contents The unparsed contents of the loop.
- *
- * @return string The parsed contents of the loop.
+ * Output the correct PHP variable string for use in template space.
*/
- protected function _parseLoop($tag, $array, $contents)
+ protected function _generatePHPVar($tag, $key)
{
- // Get the tags & loop.
- $t = $this->_getTags($tag, 'loop');
- $loop = $this->_getStatement($t, $contents);
-
- // See if we have a divider.
- $l = $this->_getTags($tag, 'divider');
- $divider = $this->_getStatement($l, $loop);
- $contents = str_replace($l['b'] . $divider . $l['e'], '', $contents);
-
- // Process the array.
- do {
- $parsed = '';
- $first = true;
- reset($array);
- while (list($key, $value) = each($array)) {
- if (is_array($value) || is_object($value)) {
- $i = $loop;
- reset($value);
- while (list($key2, $value2) = each($value)) {
- if (!is_array($value2) && !is_object($value2)) {
- // Replace associative array tags.
- $aa_tag = $tag . '.' . $key2;
- $i = str_replace($this->_getTag($aa_tag), $value2, $i);
- $pos = strpos($tag, '.');
- if (($pos !== false) &&
- !empty($this->_ifs[substr($tag, 0, $pos)])) {
- $this->_ifs[$aa_tag] = $value2;
- $i = $this->_parseIf($aa_tag, $i);
- unset($this->_ifs[$aa_tag]);
- }
- } else {
- // Check to see if it's a nested loop.
- $i = $this->_parseLoop($tag . '.' . $key2, $value2, $i);
- }
- }
- $i = str_replace($this->_getTag($tag), $key, $i);
- } elseif (is_string($key) && !is_array($value) && !is_object($value)) {
- $contents = str_replace($this->_getTag($tag . '.' . $key), $value, $contents);
- } elseif (!is_array($value) && !is_object($value)) {
- $i = str_replace($this->_getTag($tag . ''), $value, $loop);
- } else {
- $i = null;
- }
+ $out = '';
- // Parse conditions in the array.
- if (!empty($this->_ifs[$tag][$key]) &&
- is_array($this->_ifs[$tag][$key]) &&
- $this->_ifs[$tag][$key]) {
- reset($this->_ifs[$tag][$key]);
- foreach (array_keys($this->_ifs[$tag][$key]) as $cTag) {
- $i = $this->_parseIf($tag . '.' . $cTag, $i, $key);
- }
- }
+ $a = explode('.', $key);
+ $a_count = count($a);
- // Add the parsed iteration.
- if (isset($i)) {
- // If it's not the first time through, prefix the
- // loop divider, if there is one.
- if (!$first) {
- $i = $divider . $i;
- }
- $parsed .= rtrim($i);
- }
+ if ($a_count == 1) {
+ switch ($tag) {
+ case 'arrays':
+ $out = '$this->_arrays';
+ break;
- // No longer the first time through.
- $first = false;
+ case 'scalars':
+ $out = '$this->_scalars';
+ break;
}
+ } else {
+ $out = '$v' . $this->_foreachMap[implode('.', array_slice($a, 0, -1))];
+ }
- // Replace the parsed pieces of the template.
- $contents = str_replace($t['b'] . $loop . $t['e'], $parsed, $contents);
- } while ($loop = $this->_getStatement($t, $contents));
-
- return $contents;
+ return $out . '[\'' . end($a) . '\']';
}
/**
- * Parses the given case loop (cloop).
- *
- * @param string $tag The name of the cloop to parse.
- * @param array $array The values for the cloop.
- * @param string $contents The unparsed contents of the cloop.
- *
- * @return string The parsed contents of the cloop.
+ * TODO
*/
- protected function _parseCloop($tag, $array, $contents)
+ protected function _doSearch($tag, $key, $noclose = false)
{
- // Get the tags & cloop.
- $t = $this->_getTags($tag, 'cloop');
-
- while ($loop = $this->_getStatement($t, $contents)) {
- // Set up the cases.
- $array['cases'][] = 'default';
- $case_content = array();
-
- // Get the case strings.
- foreach ($array['cases'] as $case) {
- $ctags[$case] = $this->_getTags($case, 'case');
- $case_content[$case] = $this->_getStatement($ctags[$case], $loop);
- }
+ $out = array();
+ $level = (is_null($key)) ? 0 : substr_count($key, '.') + 1;
+
+ if (!isset($this->_pregcache[$key])) {
+ $regex = ($noclose) ?
+ "/<" . $tag . ":(.+?)\s\/>/" :
+ "/<" . $tag . ":([^>]+)>/";
+ preg_match_all($regex, $this->_template, $this->_pregcache[$tag], PREG_SET_ORDER);
+ }
- // Process the cloop.
- $parsed = '';
- reset($array['array']);
- while (list($key, $value) = each($array['array'])) {
- if (is_numeric($key) &&
- (is_array($value) || is_object($value))) {
- // Set up the cases.
- $current_case = isset($value['case'])
- ? $value['case']
- : 'default';
- unset($value['case']);
- $i = $case_content[$current_case];
-
- // Loop through each value.
- reset($value);
- while (list($key2, $value2) = each($value)) {
- $i = (is_array($value2) || is_object($value2))
- ? $this->_parseLoop($tag . '.' . $key2, $value2, $i)
- : str_replace($this->_getTag($tag . '.' . $key2), $value2, $i);
- }
+ foreach ($this->_pregcache[$tag] as $pkey => $val) {
+ $val_level = substr_count($val[1], '.');
+ $add = false;
+ if (is_null($key)) {
+ $add = !$val_level;
+ } else {
+ $add = (($val_level == $level) &&
+ (strpos($val[1], $key . '.') === 0));
+ }
+ if ($add) {
+ if (!$noclose) {
+ $val[2] = '</' . $tag . ':' . $val[1] . '>';
}
-
- // Add the parsed iteration.
- $parsed .= rtrim($i);
+ $out[] = $val;
+ unset($this->_pregcache[$tag][$pkey]);
}
-
- // Parse the cloop.
- $contents = str_replace($t['b'] . $loop . $t['e'], $parsed, $contents);
}
- return $contents;
+ return $out;
}
/**
- * Fetch the contents of a template into $this->_template; cache
- * the filename in $this->_templateFile.
- *
- * @param string $filename Location of template file on disk.
- *
- * @return string The loaded template content.
- * @throws Horde_Exception
+ * TODO
*/
- protected function _getTemplate($filename = null)
+ protected function _doReplace($replace)
{
- if (!is_null($filename) && ($filename != $this->_templateFile)) {
- $this->_template = null;
+ if (empty($replace)) {
+ return;
}
- if (!is_null($this->_template)) {
- return $this->_template;
- }
+ $search = array();
- // Get the contents of the file.
- $file = $this->_basepath . $filename;
- $contents = file_get_contents($file);
- if ($contents === false) {
- throw new Horde_Exception(sprintf(_("Template \"%s\" not found."), $file));
+ foreach (array_keys($replace) as $val) {
+ $search[] = '/' . preg_quote($val, '/') . '/';
}
- $this->_template = $contents;
- $this->_templateFile = $filename;
-
- return $this->_template;
+ $this->_template = preg_replace($search, array_values($replace), $this->_template);
}
}