. namespace hoplite\views; use \hoplite\base\Profiling; require_once HOPLITE_ROOT . '/base/filter.php'; require_once HOPLITE_ROOT . '/base/profiling.php'; /*! Template parses a a text file (typically HTML) by expanding a small macro language into "compiled" PHP. The opening and close tags are user-customizable but the default is {% %}. The open and close tags are translated to '', respectively. Modifiers may be placed after the open and close tags as shorthand for further shorthand. {% expression %} Evaluates a non-printing expression, treating it as pure PHP. This can be used to evaluate conditions: {% if (!$user->user_id): %}

Hello, Guest!

{% endif %} {%= $value %} Prints the value and automatically HTML-escapes it. {%= $value | int } Prints $value by coerceing it to an integer. Other coercion types are str (the default if no pipe symbol, above), int, float, and raw (no escaping). {%# macro_function arg1, arg2, ... argN %} Runs the built-in macro_function with the specified arguments. The built-ins are: * import(template_name:string, new_variables:array) Imports a subtemplate into the current scope, giving it access to all the current variables, while overriding and defining new ones via new_variables. * url(path:string) Constructs an absolute URL to the given path from the script root. */ class Template { /*! @var string The macro opening delimiter. */ static protected $open_tag = '{%'; /*! @var string The macro closing delimiter. */ static protected $close_tag = '%}'; /*! @var string The name of the template. */ protected $template_name = ''; /*! @var string The compiled template. */ protected $data = NULL; /*! @var array Variables to provide to the template. */ protected $vars = array(); /*! Creates a new Template with a given name. @param string */ private function __construct($name) { $this->template_name = $name; } static public function NewWithData($name, $data) { $template = new Template($name); $template->data = $template->_ProcessTemplate($data); return $template; } static public function NewWithCompiledData($name, $data) { $template = new Template($name); $template->data = $data; return $template; } /*! Sets the open and closing delimiters. The caller is responsible for choosing values that will not cause the parser to barf. */ static public function set_open_close_tags($open_tag, $close_tag) { self::$open_tag = $open_tag; self::$close_tag = $close_tag; } /*! Gets the name of the template. */ public function template_name() { return $this->template_name; } /*! Gets the parsed data of the template. */ public function template() { return $this->data; } /*! Overload property accessors to set view variables. */ public function __get($key) { return $this->vars[$key]; } public function __set($key, $value) { $this->vars[$key] = $value; } /*! This includes the template and renders it out. */ public function Render($vars = array()) { $__template_data = $this->data; $__template_vars = array_merge($this->vars, $vars); $render = function () use ($__template_data, $__template_vars) { extract($__template_vars); eval('?>' . $__template_data . '<' . '?'); }; ob_start(); $error = error_reporting(); error_reporting($error & ~E_NOTICE); $render(); error_reporting($error); if (Profiling::IsProfilingEnabled()) TemplateLoader::GetInstance()->MarkTemplateRendered($this->template_name); $data = ob_get_contents(); ob_end_clean(); return $data; } /*! @brief Does any pre-processing on the template. This performs the macro expansion. The language is very simple and is merely shorthand for the PHP tags. @param string Raw template data @return string Executable PHP */ protected function _ProcessTemplate($data) { // The parsed output as compiled PHP. $processed = ''; // If processing a macro, this contains the contents of the macro while // it is being extracted from the template. $macro = ''; $length = strlen($data); $i = 0; // The current position of the iterator. $looking_for_end = FALSE; // Whehter or not an end tag is expected. $line_number = 1; // The current line number. $i_last_line = 0; // The value of |i| at the previous new line, used for column numbering. $open_tag_len = strlen(self::$open_tag); $close_tag_len = strlen(self::$close_tag); while ($i < $length) { // See how far the current position is from the end of the string. $delta = $length - $i; // When a new line is reached, update the counters. if ($data[$i] == "\n") { ++$line_number; $i_last_line = $i; } // Check for the open tag. if ($delta >= $open_tag_len && substr($data, $i, $open_tag_len) == self::$open_tag) { // If an expansion has already been opened, then it's an error to nest. if ($looking_for_end) { $column = $i - $i_last_line; throw new TemplateException("Unexpected start of expansion at line $line_number:$column"); } $looking_for_end = TRUE; $macro = ''; $i += $open_tag_len; continue; } // Check for the close tag. else if ($delta >= $close_tag_len && substr($data, $i, $close_tag_len) == self::$close_tag) { // If an end tag was encountered without an open tag, that's an error. if (!$looking_for_end) { $column = $i - $i_last_line; throw new TemplateException("Unexpected end of expansion at line $line_number:$column"); } $expanded_macro = $this->_ProcessMacro($macro); $processed .= ""; $looking_for_end = FALSE; $i += $close_tag_len; continue; } // All other characters go into a storage bin. If currently in a macro, // save off the data separately for parsing. if ($looking_for_end) $macro .= $data[$i]; else $processed .= $data[$i]; ++$i; } return $processed; } /*! Takes the contents of a macro, i.e. the string between the open and close tags, and transforms it into a PHP statement. */ protected function _ProcessMacro($macro) { if (strlen($macro) < 1) return $macro; // If the macro has a modifier character, as described in the class comment, // futher thransform the statement. switch ($macro[0]) { case '=': return $this->_ProcessInterpolation(substr($macro, 1)); case '#': return $this->_ProcessBuiltin(substr($macro, 1)); default: return $macro; } } /*! Creates a printing expression for a value, optionally coercing and escaping it to a specific type. */ protected function _ProcessInterpolation($macro) { // The pipe operator specifies how to sanitize the output. $formatter_pos = strrpos($macro, '|'); // No specifier defaults to escaped string. if ($formatter_pos === FALSE) return 'echo hoplite\\base\\filter\\String(' . $macro . ')'; $expression = trim(substr($macro, 0, $formatter_pos)); // Otherwise, apply the right filter. $formatter = trim(substr($macro, $formatter_pos + 1)); $function = ''; switch (strtolower($formatter)) { case 'int': return "echo intval($expression)"; case 'float': return "echo floatval($expression)"; case 'str': return "echo hoplite\\base\\filter\\String($expression)"; case 'raw': return "echo $expression"; case 'json': return "echo json_encode($expression)"; default: throw new TemplateException('Invalid macro formatter "' . $formatter . '"'); } } protected function _ProcessBuiltin($macro) { $macro = trim($macro); $function_pos = strpos($macro, ' '); if ($function_pos === FALSE) throw new TemplateException('No macro function specified'); $function = substr($macro, 0, $function_pos); $args = substr($macro, $function_pos + 1); switch ($function) { case 'url': return $this->_Builtin_url($args); case 'import': return $this->_Builtin_import($args); default: throw new TemplateException("Invalid macro function '$function'"); } } protected function _Builtin_url($args) { return "hoplite\\views\\TemplateBuiltins::MakeURL($args)"; } protected function _Builtin_import($args) { $template = $args; $vars = ''; $template_pos = strpos($args, ','); if ($template_pos !== FALSE) { $template = substr($args, 0, $template_pos); $vars = substr($args, $template_pos + 1); } if ($vars) $vars = "array_merge(\$__template_vars, $vars)"; else $vars = '$__template_vars'; return "hoplite\\views\\TemplateBuiltins::Import($template, $vars)"; } } /*! Namespace for builtin template macros. */ class TemplateBuiltins { /*! Renders a template with additional vars. @param string The template name to render @param array Variables with which to render. */ static public function Import($template, $vars) { echo TemplateLoader::Fetch($template)->Render($vars); } /*! @brief Creates a URL via RootController::MakeURL(). This requires the root controller be set in the $GLOBALS as hoplite\http\RootController. @param string Path. */ static public function MakeURL($path) { echo $GLOBALS['hoplite\http\RootController']->MakeURL($path, FALSE); } } class TemplateException extends \Exception {}