From: Michael M Slusarz Date: Thu, 9 Jul 2009 20:48:32 +0000 (-0600) Subject: Merge IMP_Template X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=06f8f73c1882a5af2f9d62bacf84be017ec877c8;p=horde.git Merge IMP_Template --- diff --git a/framework/Template/lib/Horde/Template.php b/framework/Template/lib/Horde/Template.php index 20f491c93..775a758f4 100644 --- a/framework/Template/lib/Horde/Template.php +++ b/framework/Template/lib/Horde/Template.php @@ -11,6 +11,8 @@ * template engines (PHP code, XSLT, etc.) without requiring usage * changes. * + * Compilation code adapted from code written by Bruno Pedro . + * * Copyright 2002-2009 The Horde Project (http://www.horde.org/) * * See the enclosed file COPYING for license information (LGPL). If you @@ -22,6 +24,16 @@ */ 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. * @@ -51,51 +63,67 @@ class Horde_Template 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: + *
+     * 'debug' - Output debugging information to screen
+     * 'forcecompile' - Force a compilation on every page load
+     * 'gettext' - Activate gettext detection
+     * 
      *
      * @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)
     {
@@ -110,7 +138,8 @@ class Horde_Template
     public function setTemplate($template)
     {
         $this->_template = $template;
-        $this->_templateFile = 'string';
+        $this->_parse();
+        $this->_templateFile = self::TEMPLATE_STRING;
     }
 
     /**
@@ -132,52 +161,22 @@ class Horde_Template
      *
      * @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  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.
@@ -188,7 +187,8 @@ class Horde_Template
     {
         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;
@@ -201,14 +201,44 @@ class Horde_Template
      * @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 '
' . htmlspecialchars($this->_template) . '
'; + } + + return $this->parse(); } /** @@ -220,338 +250,244 @@ class Horde_Template */ 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('/\?>|<\?/', '', $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' => '' - ); + if (preg_match_all("/(.+?)<\/gettext>/s", $this->_template, $matches, PREG_SET_ORDER)) { + $replace = array(); + foreach ($matches as $val) { + $replace[$val[0]] = ''; + } + $this->_doReplace($replace); + } } /** - * Formats a scalar tag (default format is ). + * 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 ''; + $replace = array(); + + foreach ($this->_doSearch('if', $key) as $val) { + $replace[$val[0]] = '_generatePHPVar('scalars', $val[1]) . ') || !empty(' . $this->_generatePHPVar('arrays', $val[1]) . ')): ?>'; + $replace[$val[2]] = ''; + + // Check for else statement. + foreach ($this->_doSearch('else', $key) as $val2) { + $replace[$val2[0]] = ''; + $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] . ">/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]] = '_generatePHPVar('arrays', $val[1]) . ' as $k' . $varId . ' => $v' . $varId . '): ?>'; + $replace[$val[2]] = ''; + + // 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' => '', - 'e' => '' - ); - - 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 = '_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. ). */ - 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]] = ''; + 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] = ''; } - - // 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); } }