commit 2af461c7f67e8fd170b146c02eb33511e0d61a9b Author: fzerorubigd Date: Thu Oct 25 19:00:10 2012 +0330 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22d0d82 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..baf4718 --- /dev/null +++ b/composer.json @@ -0,0 +1,13 @@ +{ + "name": "xamin/handlebars.php", + "description": "Handlebars processor for php", + "authors": [ + { + "name": "fzerorubigd", + "email": "fzerorubigd@gmail.com" + } + ], + "require": { + + } +} diff --git a/src/Handlebars/Cache.php b/src/Handlebars/Cache.php new file mode 100644 index 0000000..d420b99 --- /dev/null +++ b/src/Handlebars/Cache.php @@ -0,0 +1,60 @@ + + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version GIT: $Id$ + * @link http://xamin.ir + */ + + +/** + * Cache interface + * Base cache interface + * + * @category Xamin + * @package Handlebars + * @author fzerorubigd + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version Release: @package_version@ + * @link http://xamin.ir + */ + +interface Handlebars_Cache +{ + /** + * Get cache for $name if exist. + * + * @param string $name Cache id + * + * @return data on hit, boolean false on cache not found + */ + public function get($name); + + /** + * Set a cache + * + * @param string $name cache id + * @param mixed $value data to store + * + * @return void + */ + public function set($name, $value); + + /** + * Remove cache + * + * @param string $name Cache id + * + * @return void + */ + public function remove($name); +} \ No newline at end of file diff --git a/src/Handlebars/Cache/Dummy.php b/src/Handlebars/Cache/Dummy.php new file mode 100644 index 0000000..6095b33 --- /dev/null +++ b/src/Handlebars/Cache/Dummy.php @@ -0,0 +1,73 @@ + + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version GIT: $Id$ + * @link http://xamin.ir + */ + + +/** + * A dummy array cache + * + * @category Xamin + * @package Handlebars + * @author fzerorubigd + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version Release: @package_version@ + * @link http://xamin.ir + */ + +class Handlebars_Cache_Dummy implements Handlebars_Cache +{ + private $_cache = array(); + + /** + * Get cache for $name if exist. + * + * @param string $name Cache id + * + * @return data on hit, boolean false on cache not found + */ + public function get($name) + { + if (array_key_exists($name, $this->_cache)) { + return $this->_cache[$name]; + } + return false; + } + + /** + * Set a cache + * + * @param string $name cache id + * @param mixed $value data to store + * + * @return void + */ + public function set($name, $value) + { + $this->_cache[$name] = $value; + } + + /** + * Remove cache + * + * @param string $name Cache id + * + * @return void + */ + public function remove($name) + { + unset($this->_cache[$name]); + } +} \ No newline at end of file diff --git a/src/Handlebars/Context.php b/src/Handlebars/Context.php new file mode 100644 index 0000000..173a62c --- /dev/null +++ b/src/Handlebars/Context.php @@ -0,0 +1,163 @@ + + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version GIT: $Id$ + * @link http://xamin.ir + */ + +/** + * Handlebars context + * Context for a template + * + * @category Xamin + * @package Handlebars + * @author fzerorubigd + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version Release: @package_version@ + * @link http://xamin.ir + */ +class Handlebars_Context +{ + /** + * @var array stack for context only top stack is available + */ + protected $stack = array(); + + /** + * Mustache rendering Context constructor. + * + * @param mixed $context Default rendering context (default: null) + */ + public function __construct($context = null) + { + if ($context !== null) { + $this->stack = array($context); + } + } + + /** + * Push a new Context frame onto the stack. + * + * @param mixed $value Object or array to use for context + * + * @return void + */ + public function push($value) + { + array_push($this->stack, $value); + } + + /** + * Pop the last Context frame from the stack. + * + * @return mixed Last Context frame (object or array) + */ + public function pop() + { + return array_pop($this->stack); + } + + /** + * Get the last Context frame. + * + * @return mixed Last Context frame (object or array) + */ + public function last() + { + return end($this->stack); + } + + /** + * Change the current context to one of current context members + * + * @param string $variableName name of variable or a callable on current context + * + * @return mixed actual value + */ + public function with($variableName) + { + $value = $this->get($variableName); + $this->push($value); + return $value; + } + + /** + * Get a avariable from current context + * Supported types : + * variable , ../variable , variable.variable , . + * + * @param string $variableName variavle name to get from current context + * + * @return mixed + */ + public function get($variableName) + { + //Need to clean up + $variableName = trim($variableName); + $level = 0; + while (substr($variableName, 0, 3) == '../') { + $variableName = trim(substr($variableName, 3)); + $level++; + } + if (count($this->stack) < $level) { + return ''; + } + end($this->stack); + while ($level) { + prev($this->stack); + $level--; + } + $current = current($this->stack); + if (!$variableName) { + return ''; + } elseif ($variableName == '.') { + return $current; + } else { + $chunks = explode('.', $variableName); + foreach ($chunks as $chunk) { + if ($current == '') { + return $current; + } + $current = $this->_findVariableInContext($current, $chunk); + } + } + return $current; + } + + /** + * Check if $variable->$inside is available + * + * @param mixed $variable variable to check + * @param string $inside property/method to check + * + * @return boolean true if exist + */ + private function _findVariableInContext($variable, $inside) + { + $value = ''; + if (is_array($variable)) { + if (isset($variable[$inside])) { + $value = $variable[$inside]; + } + } elseif (is_object($variable)) { + if (isset($variable->$inside)) { + $value = $variable->$inside; + } elseif (is_callable(array($variable, $inside))) { + $value = call_user_func(array($variable, $inside)); + } + } elseif ($inside === '.') { + $value = $variable; + } + return $value; + } +} \ No newline at end of file diff --git a/src/Handlebars/Engine.php b/src/Handlebars/Engine.php new file mode 100644 index 0000000..ad0bd51 --- /dev/null +++ b/src/Handlebars/Engine.php @@ -0,0 +1,389 @@ + + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version GIT: $Id$ + * @link http://xamin.ir + */ + + +/** + * Handlebars parser (infact its a mustache parser) + * This class is responsible for turning raw template source into a set of Mustache tokens. + * + * @category Xamin + * @package Handlebars + * @author fzerorubigd + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version Release: @package_version@ + * @link http://xamin.ir + */ + +class Handlebars_Engine +{ + const VERSION = '1.0.0'; + + /** + * @var Handlebars_Tokenizer + */ + private $_tokenizer; + + /** + * @var Handlebars_Parser + */ + private $_parser; + /** + * @var Handlebars_Helpers + */ + private $_helpers; + + /** + * @var Handlebars_Loader + */ + private $_loader; + + /** + * @var Handlebars_Loader + */ + private $_partialLoader; + + /** + * @var Handlebars_Cache + */ + private $_cache; + /** + * @var callable escape function to use + */ + private $_escape = 'htmlspecialchars'; + + /** + * @var array parametes to pass to escape function, script prepend string to this array + */ + private $_escapeArgs = array ( + ENT_COMPAT, + 'UTF-8' + ); + + /** + * Handlebars engine constructor + * $options array can contain : + * helpers => Handlebars_Helpers object + * escape => a callable function to escape values + * escapeArgs => array to pass as extra parameter to escape function + * loader => Handlebars_Loader object + * partial_loader => Handlebars_Loader object + * cache => Handlebars_Cache object + * + * @param array $options array of options to set + */ + public function __construct(array $options = array()) + { + if (isset($options['helpers'])) { + $this->setHelpers($options['helpers']); + } + + if (isset($options['loader'])) { + $this->setLoader($options['loader']); + } + + if (isset($options['partial_loader'])) { + $this->setPartialLoader($options['partial_loader']); + } + + if (isset($options['cache'])) { + $this->setCache($options['cache']); + } + + if (isset($options['escape'])) { + if (!is_callable($options['escape'])) { + throw new InvalidArgumentException('Handlebars Constructor "escape" option must be callable'); + } + + $this->_escape = $options['escape']; + } + + if (isset($options['escapeArgs'])) { + if (!is_array($options['escapeArgs'])) { + $options['escapeArgs'] = array($options['escapeArgs']); + } + $this->_escapeArgs = $options['escapeArgs']; + } + } + + + /** + * Shortcut 'render' invocation. + * + * Equivalent to calling `$handlebars->loadTemplate($template)->render($data);` + * + * @param string $template template name + * @param mixed $data data to use as context + * + * @return string Rendered template + * @see Handlebars_Engine::loadTemplate + * @see Handlebars_Template::render + */ + public function render($template, $data) + { + return $this->loadTemplate($template)->render($data); + } + + /** + * Set helpers for current enfine + * + * @param Handlebars_Helpers $helpers handlebars helper + * + * @return void + */ + public function setHelpers(Handlebars_Helpers $helpers) + { + $this->_helpers = $helpers; + } + + /** + * Get helpers, or create new one if ther is no helper + * + * @return Handlebars_Helpers + */ + public function getHelpers() + { + if (!isset($this->_helpers)) { + $this->_helpers = new Handlebars_Helpers(); + } + return $this->_helpers; + } + + /** + * Set current loader + * + * @param Handlebars_Loader $loader handlebars loader + * + * @return void + */ + public function setLoader(Handlebars_Loader $loader) + { + $this->_loader = $loader; + } + + /** + * get current loader + * + * @return Handlebars_Loader + */ + public function getLoader() + { + if (!isset($this->_loader)) { + $this->_loader = new Handlebars_Loader_StringLoader(); + } + return $this->_loader; + } + + /** + * Set current partial loader + * + * @param Handlebars_Loader $loader handlebars loader + * + * @return void + */ + public function setPartialLoader(Handlebars_Loader $loader) + { + $this->_partialLoader = $loader; + } + + /** + * get current partial loader + * + * @return Handlebars_Loader + */ + public function getPartialLoader() + { + if (!isset($this->_partialLoader)) { + $this->_partialLoader = new Handlebars_Loader_StringLoader(); + } + return $this->_partialLoader; + } + + /** + * Set cache for current engine + * + * @param Handlebars_cache $cache handlebars cache + * + * @return void + */ + public function setCache(Handlebars_Cache $cache) + { + $this->_cache = $cache; + } + + /** + * Get cache + * + * @return Handlebars_Cache + */ + public function getCache() + { + if (!isset($this->_cache)) { + $this->_cache = new Handlebars_Cache_Dummy(); + } + return $this->_cache; + } + /** + * Get current escape function + * + * @return callable + */ + public function getEscape() + { + return $this->_escape; + } + + /** + * Set current escpae function + * + * @param callable $escape function + * + * @return void + */ + public function setEscape($escape) + { + if (!is_callable($escape)) { + throw new InvalidArgumentException('Escape function must be a callable'); + } + $this->_escape = $escape; + } + + /** + * Get current escape function + * + * @return callable + */ + public function getEscapeArgs() + { + return $this->_escapeArgs; + } + + /** + * Set current escpae function + * + * @param array $escapeArgs arguments to pass as extra arg to function + * + * @return void + */ + public function setEscapeArgs($escapeArgs) + { + if (!is_array($escapeArgs)) { + $escapeArgs = array($escapeArgs); + } + $this->_escapeArgs = $escapeArgs; + } + + + /** + * Set the Handlebars Tokenizer instance. + * + * @param Handlebars_Tokenizer $tokenizer tokenizer + * + * @return void + */ + public function setTokenizer(Handlebars_Tokenizer $tokenizer) + { + $this->_tokenizer = $tokenizer; + } + + /** + * Get the current Handlebars Tokenizer instance. + * + * If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one. + * + * @return Handlebars_Tokenizer + */ + public function getTokenizer() + { + if (!isset($this->_tokenizer)) { + $this->_tokenizer = new Handlebars_Tokenizer(); + } + + return $this->_tokenizer; + } + /** + * Set the Handlebars Parser instance. + * + * @param Handlebars_Parser $parser parser object + * + * @return void + */ + public function setParser(Handlebars_Parser $parser) + { + $this->_parser = $parser; + } + + /** + * Get the current Handlebars Parser instance. + * + * If no Parser instance has been explicitly specified, this method will instantiate and return a new one. + * + * @return Handlebars_Parser + */ + public function getParser() + { + if (!isset($this->_parser)) { + $this->_parser = new Handlebars_Parser(); + } + + return $this->_parser; + } + /** + * Load a template by name with current template loader + * + * @param string $name template name + * + * @return Handlebars_Template + */ + public function loadTemplate($name) + { + $source = $this->getLoader()->load($name); + $tree = $this->_tokenize($source); + return new Handlebars_Template($this, $tree, $source); + } + + /** + * Load a partial by name with current partial loader + * + * @param string $name partial name + * + * @return Handlebars_Template + */ + public function loadPartial($name) + { + $source = $this->getPartialLoader()->load($name); + $tree = $this->_tokenize($source); + return new Handlebars_Template($this, $tree, $source); + } + + /** + * try to tokenize source, or get them from cache if available + * + * @param string $source handlebars source code + * + * @return array handlebars parsed data into array + */ + private function _tokenize($source) + { + $hash = md5(sprintf('version: %s, data : %s', self::VERSION, $source)); + $tree = $this->getCache()->get($hash); + if ($tree === false) { + $tokens = $this->getTokenizer()->scan($source); + $tree = $this->getParser()->parse($tokens); + } + return $tree; + } +} \ No newline at end of file diff --git a/src/Handlebars/Helpers.php b/src/Handlebars/Helpers.php new file mode 100644 index 0000000..a1186dd --- /dev/null +++ b/src/Handlebars/Helpers.php @@ -0,0 +1,249 @@ + + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version GIT: $Id$ + * @link http://xamin.ir + */ + +/** + * Handlebars helpers + * + * a collection of helper function. normally a function like + * function ($sender, $name, $arguments) $arguments is unscaped arguments and is a string, not array + * TODO: Add support for an interface with an execute method + * + * @category Xamin + * @package Handlebars + * @author fzerorubigd + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version Release: @package_version@ + * @link http://xamin.ir + */ + +class Handlebars_Helpers +{ + /** + * @var array array of helpers + */ + protected $helpers = array(); + + /** + * Create new helper container class + * + * @param array $helpers array of name=>$value helpers + * @param array $defaults add defaults helper (if, unless, each,with) + * + * @throw InvalidArgumentException when $helpers is not an array (or traversable) or helper is not a caallable + */ + public function __construct($helpers = null, $defaults = true) + { + if ($defaults) { + $this->addDefaultHelpers(); + } + if ($helpers != null) { + if (!is_array($helpers) && !$helpers instanceof Traversable) { + throw new InvalidArgumentException('HelperCollection constructor expects an array of helpers'); + } + foreach ($helpers as $name => $helper) { + $this->add($name, $helpers); + } + } + } + /** + * Add default helpers (if unless each with) + * + * @return void + */ + protected function addDefaultHelpers() + { + $this->add( + 'if', + function ($template, $context, $args, $source) + { + $tmp = $context->get($args); + $buffer = ''; + if ($tmp) { + $buffer = $template->render($context); + } + return $buffer; + } + ); + + $this->add( + 'each', + function($template, $context, $args, $source) + { + $tmp = $context->get($args); + $buffer = ''; + if (is_array($tmp) || $tmp instanceof Traversable) { + foreach ($tmp as $var) { + $context->push($var); + $buffer .= $template->render($context); + $context->pop(); + } + } + return $buffer; + } + ); + + $this->add( + 'unless', + function ($template, $context, $args, $source) + { + $tmp = $context->get($args); + $buffer = ''; + if (!$tmp) { + $buffer = $template->render($context); + } + return $buffer; + } + ); + + $this->add( + 'with', + function ($template, $context, $args, $source) + { + $tmp = $context->get($args); + $context->push($tmp); + $buffer = $template->render($context); + $context->pop(); + return $buffer; + } + ); + } + + /** + * Add a new helper to helpers + * + * @param string $name helper name + * @param callable $helper a function as a helper + * + * @return void + * @throw InvalidArgumentException if $helper is not a callable + */ + public function add($name ,$helper) + { + if (!is_callable($helper)) { + throw new InvalidArgumentException("$name Helper is not a callable."); + } + $this->helpers[$name] = $helper; + } + + /** + * Check if $name helper is available + * + * @param string $name helper name + * + * @return boolean + */ + public function has($name) + { + return array_key_exists($name, $this->helpers); + } + + /** + * Get a helper. __magic__ method :) + * + * @param string $name helper name + * + * @return callable helper function + * @throw InvalidArgumentException if $name is not available + */ + public function __get($name) + { + if (!$this->has($name)) { + throw new InvalidArgumentException('Unknow helper :' . $name); + } + return $this->helpers[$name]; + } + + /** + * Check if $name helper is available __magic__ method :) + * + * @param string $name helper name + * + * @return boolean + * @see Handlebras_Helpers::has + */ + public function __isset($name) + { + return $this->has($name); + } + + /** + * Add a new helper to helpers __magic__ method :) + * + * @param string $name helper name + * @param callable $helper a function as a helper + * + * @return void + * @throw InvalidArgumentException if $helper is not a callable + */ + public function __set($name ,$helper) + { + $this->add($name, $helpers); + } + + + /** + * Unset a helper + * + * @param string $name helpername to remove + * + * @return void + */ + public function __unset($name) + { + unset($this->helpers[$name]); + } + + /** + * Check whether a given helper is present in the collection. + * + * @param string $name helper name + * + * @return void + * @throws InvalidArgumentException if the requested helper is not present. + */ + public function remove($name) + { + if (!$this->has($name)) { + throw new InvalidArgumentException('Unknown helper: ' . $name); + } + + unset($this->helpers[$name]); + } + + /** + * Clear the helper collection. + * + * Removes all helpers from this collection + * + * @return void + */ + public function clear() + { + $this->helpers = array(); + } + + /** + * Check whether the helper collection is empty. + * + * @return boolean True if the collection is empty + */ + public function isEmpty() + { + return empty($this->helpers); + } + +} \ No newline at end of file diff --git a/src/Handlebars/Loader.php b/src/Handlebars/Loader.php new file mode 100644 index 0000000..b13e9f4 --- /dev/null +++ b/src/Handlebars/Loader.php @@ -0,0 +1,40 @@ + + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version GIT: $Id$ + * @link http://xamin.ir + */ + + +/** + * Handlebars loader interface + * + * @category Xamin + * @package Handlebars + * @author fzerorubigd + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version Release: @package_version@ + * @link http://xamin.ir + */ +interface Handlebars_Loader +{ + + /** + * Load a Template by name. + * + * @param string $name template name to load + * + * @return string Mustache Template source + */ + public function load($name); +} diff --git a/src/Handlebars/Loader/FilesystemLoader.php b/src/Handlebars/Loader/FilesystemLoader.php new file mode 100644 index 0000000..9e7d79b --- /dev/null +++ b/src/Handlebars/Loader/FilesystemLoader.php @@ -0,0 +1,118 @@ + + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version GIT: $Id$ + * @link http://xamin.ir + */ + +/** + * Handlebars Template filesystem Loader implementation. + * + * @category Xamin + * @package Handlebars + * @author fzerorubigd + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version Release: @package_version@ + * @link http://xamin.ir * + * @implements Loader + */ +class Handlebars_Loader_FilesystemLoader implements Handlebars_Loader +{ + private $_baseDir; + private $_extension = '.handlebars'; + private $_templates = array(); + + /** + * Handlebars filesystem Loader constructor. + * + * Passing an $options array allows overriding certain Loader options during instantiation: + * + * $options = array( + * // The filename extension used for Mustache templates. Defaults to '.mustache' + * 'extension' => '.ms', + * ); + * + * @param string $baseDir Base directory containing Mustache template files. + * @param array $options Array of Loader options (default: array()) + * + * @throws RuntimeException if $baseDir does not exist. + */ + public function __construct($baseDir, array $options = array()) + { + $this->_baseDir = rtrim(realpath($baseDir), '/'); + + if (!is_dir($this->_baseDir)) { + throw new RuntimeException('FilesystemLoader baseDir must be a directory: '.$baseDir); + } + + if (isset($options['extension'])) { + $this->_extension = '.' . ltrim($options['extension'], '.'); + } + } + + /** + * Load a Template by name. + * + * $loader = new FilesystemLoader(dirname(__FILE__).'/views'); + * $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache"; + * + * @param string $name template name + * + * @return string Handkebars Template source + */ + public function load($name) + { + if (!isset($this->_templates[$name])) { + $this->_templates[$name] = $this->loadFile($name); + } + + return $this->_templates[$name]; + } + + /** + * Helper function for loading a Mustache file by name. + * + * @param string $name template name + * + * @return string Mustache Template source + * @throws InvalidArgumentException if a template file is not found. + */ + protected function loadFile($name) + { + $fileName = $this->getFileName($name); + + if (!file_exists($fileName)) { + throw new InvalidArgumentException('Template '.$name.' not found.'); + } + + return file_get_contents($fileName); + } + + /** + * Helper function for getting a Mustache template file name. + * + * @param string $name template name + * + * @return string Template file name + */ + protected function getFileName($name) + { + $fileName = $this->_baseDir . '/' . $name; + if (substr($fileName, 0 - strlen($this->_extension)) !== $this->_extension) { + $fileName .= $this->_extension; + } + + return $fileName; + } +} diff --git a/src/Handlebars/Loader/StringLoader.php b/src/Handlebars/Loader/StringLoader.php new file mode 100644 index 0000000..98cd2ae --- /dev/null +++ b/src/Handlebars/Loader/StringLoader.php @@ -0,0 +1,43 @@ + + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version GIT: $Id$ + * @link http://xamin.ir + */ + +/** + * Handlebars Template string Loader implementation. + * + * @category Xamin + * @package Handlebars + * @author fzerorubigd + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version Release: @package_version@ + * @link http://xamin.ir * + * @implements Loader + */ +class Handlebars_Loader_StringLoader implements Handlebars_Loader +{ + + /** + * Load a Template by source. + * + * @param string $name Handlebars Template source + * + * @return string Handlebars Template source + */ + public function load($name) + { + return $name; + } +} diff --git a/src/Handlebars/Parser.php b/src/Handlebars/Parser.php new file mode 100644 index 0000000..01b7aa2 --- /dev/null +++ b/src/Handlebars/Parser.php @@ -0,0 +1,98 @@ + + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version GIT: $Id$ + * @link http://xamin.ir + */ + +/** + * Handlebars parser (infact its a mustache parser) + * This class is responsible for turning raw template source into a set of Mustache tokens. + * + * @category Xamin + * @package Handlebars + * @author fzerorubigd + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version Release: @package_version@ + * @link http://xamin.ir + */ + +class Handlebars_Parser +{ + /** + * Process array of tokens and convert them into parse tree + * + * @param array $tokens Set of + * + * @return array Token parse tree + */ + public function parse(array $tokens = array()) + { + return $this->_buildTree(new ArrayIterator($tokens)); + } + + /** + * Helper method for recursively building a parse tree. + * + * @param ArrayIterator $tokens Stream of tokens + * + * @return array Token parse tree + * + * @throws LogicException when nesting errors or mismatched section tags are encountered. + */ + private function _buildTree(ArrayIterator $tokens) + { + $stack = array(); + + do { + $token = $tokens->current(); + $tokens->next(); + + if ($token === null) { + continue; + } else { + switch ($token[Handlebars_Tokenizer::TYPE]) { + case Handlebars_Tokenizer::T_END_SECTION: + $newNodes = array (); + $continue = true; + do { + $result = array_pop($stack); + if ($result === null) { + throw new LogicException('Unexpected closing tag: /'. $token[Handlebars_Tokenizer::NAME]); + } + + if (!array_key_exists(Handlebars_Tokenizer::NODES, $result) + && isset($result[Handlebars_Tokenizer::NAME]) + && $result[Handlebars_Tokenizer::NAME] == $token[Handlebars_Tokenizer::NAME] + ) { + $result[Handlebars_Tokenizer::NODES] = $newNodes; + $result[Handlebars_Tokenizer::END] = $token[Handlebars_Tokenizer::INDEX]; + array_push($stack, $result); + break 2; + } else { + array_unshift($newNodes, $result); + } + } while (true); + break; + default: + array_push($stack, $token); + } + } + + } while ($tokens->valid()); + + return $stack; + + } +} \ No newline at end of file diff --git a/src/Handlebars/Template.php b/src/Handlebars/Template.php new file mode 100644 index 0000000..39f19a6 --- /dev/null +++ b/src/Handlebars/Template.php @@ -0,0 +1,207 @@ + + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version GIT: $Id$ + * @link http://xamin.ir + */ + + +/** + * Handlebars base template + * contain some utility method to get context and helpers + * + * @category Xamin + * @package Handlebars + * @author fzerorubigd + * @copyright 2012 (c) ParsPooyesh Co + * @license GPLv3 + * @version Release: @package_version@ + * @link http://xamin.ir + */ + +class Handlebars_Template +{ + /** + * @var Handlebars_Engine + */ + protected $handlebars; + + + protected $tree = array(); + + protected $source = ''; + + /** + * @var array Run stack + */ + private $_stack = array(); + /** + * Handlebars template constructor + * + * @param Handlebars_Engine $engine handlebar engine + * @param array $tree Parsed tree + * @param string $source Handlebars source + */ + public function __construct(Handlebars_Engine $engine, $tree, $source) + { + $this->handlebars = $engine; + $this->tree = $tree; + $this->source = $source; + array_push($this->_stack, array (0, $this->getTree())); + } + + /** + * Get current tree + * + * @return array + */ + public function getTree() + { + return $this->tree; + } + + /** + * Get current source + * + * @return string + */ + public function getSource() + { + return $this->source; + } + + /** + * Get current engine associated with this object + * + * @return Handlebars_Engine + */ + public function getEngine() + { + return $this->handlebars; + } + + /** + * Render top tree + * + * @param mixed $context current context + * + * @return string + */ + public function render($context) + { + if (!$context instanceof Handlebars_Context) { + $context = new Handlebars_Context($context); + } + $topTree = end($this->_stack); //This method never pop a value from stack + list($index ,$tree) = $topTree; + + $buffer = ''; + while (array_key_exists($index, $tree)) { + $current = $tree[$index]; + $index++; + switch ($current[Handlebars_Tokenizer::TYPE]) { + case Handlebars_Tokenizer::T_SECTION : + array_push($this->_stack, array(0, $current[Handlebars_Tokenizer::NODES])); + $buffer .= $this->_section($context, $current); + array_pop($this->_stack); + break; + case Handlebars_Tokenizer::T_INVERTED : //TODO: This has no effect, remove the whole ^ thing! + case Handlebars_Tokenizer::T_COMMENT : + $buffer .= ''; + break; + case Handlebars_Tokenizer::T_PARTIAL: + case Handlebars_Tokenizer::T_PARTIAL_2: + $buffer .= $this->_partial($context, $current); + break; + case Handlebars_Tokenizer::T_UNESCAPED: + case Handlebars_Tokenizer::T_UNESCAPED_2: + $buffer .= $this->_variables($context, $current, false); + break; + case Handlebars_Tokenizer::T_ESCAPED: + $buffer .= $this->_variables($context, $current, true); + break; + case Handlebars_Tokenizer::T_TEXT: + $buffer .= $current[Handlebars_Tokenizer::VALUE]; + break; + default: + throw new RuntimeException('Invalid node type : ' . json_encode($current)); + } + } + return $buffer; + } + + /** + * Process section nodes + * + * @param Handlebars_Context $context current context + * @param array $current section node data + * + * @return string the result + */ + private function _section(Handlebars_Context $context, $current) + { + $helpers = $this->handlebars->getHelpers(); + $sectionName = $current[Handlebars_Tokenizer::NAME]; + if ($helpers->has($sectionName)) { + $source = substr( + $this->getSource(), + $current[Handlebars_Tokenizer::INDEX], + $current[Handlebars_Tokenizer::END] - $current[Handlebars_Tokenizer::INDEX] + ); + $params = array( + $this, //First argument is this template + $context, //Secound is current context + $current[Handlebars_Tokenizer::ARGS], //Arguments + $source + ); + return call_user_func_array($helpers->$sectionName, $params); + } else { + throw new RuntimeException($sectionName . ' is not registered as a helper'); + } + } + + /** + * Process partial section + * + * @param Handlebars_Context $context current context + * @param array $current section node data + * + * @return string the result + */ + private function _partial($context, $current) + { + $partial = $this->handlebars->loadPartial($current[Handlebars_Tokenizer::NAME]); + return $partial->render($context); + } + + /** + * Process partial section + * + * @param Handlebars_Context $context current context + * @param array $current section node data + * @param boolean $escaped escape result or not + * + * @return string the result + */ + private function _variables($context, $current, $escaped) + { + $value = $context->get($current[Handlebars_Tokenizer::NAME]); + if ($escaped) { + $args = $this->handlebars->getEscapeArgs(); + array_unshift($args, $value); + $value = call_user_func_array($this->handlebars->getEscape(), array_values($args)); + } + return $value; + } + + +} \ No newline at end of file diff --git a/src/Handlebars/Tokenizer.php b/src/Handlebars/Tokenizer.php new file mode 100644 index 0000000..3fb65f0 --- /dev/null +++ b/src/Handlebars/Tokenizer.php @@ -0,0 +1,322 @@ + + * @author fzerorubigd + * @copyright 2012 Justin Hileman + * @license MIT + * @version GIT: $Id$ + * @link http://xamin.ir + */ + + +/** + * Handlebars parser (infact its a mustache parser) + * This class is responsible for turning raw template source into a set of Mustache tokens. + * + * @category Xamin + * @package Handlebars + * @author Justin Hileman + * @copyright 2012 Justin Hileman + * @license MIT + * @version Release: @package_version@ + * @link http://xamin.ir + */ +class Handlebars_Tokenizer +{ + + // Finite state machine states + const IN_TEXT = 0; + const IN_TAG_TYPE = 1; + const IN_TAG = 2; + + // Token types + const T_SECTION = '#'; + const T_INVERTED = '^'; //Must remove this + const T_END_SECTION = '/'; + const T_COMMENT = '!'; + const T_PARTIAL = '>'; //Maybe remove this partials and replace them with helpers + const T_PARTIAL_2 = '<'; + const T_DELIM_CHANGE = '='; + const T_ESCAPED = '_v'; + const T_UNESCAPED = '{'; + const T_UNESCAPED_2 = '&'; + const T_TEXT = '_t'; + + // Valid token types + private static $_tagTypes = array( + self::T_SECTION => true, + self::T_INVERTED => true, + self::T_END_SECTION => true, + self::T_COMMENT => true, + self::T_PARTIAL => true, + self::T_PARTIAL_2 => true, + self::T_DELIM_CHANGE => true, + self::T_ESCAPED => true, + self::T_UNESCAPED => true, + self::T_UNESCAPED_2 => true, + ); + + // Interpolated tags + private static $_interpolatedTags = array( + self::T_ESCAPED => true, + self::T_UNESCAPED => true, + self::T_UNESCAPED_2 => true, + ); + + // Token properties + const TYPE = 'type'; + const NAME = 'name'; + const OTAG = 'otag'; + const CTAG = 'ctag'; + const INDEX = 'index'; + const END = 'end'; + const INDENT = 'indent'; + const NODES = 'nodes'; + const VALUE = 'value'; + const ARGS = 'args'; + + protected $state; + protected $tagType; + protected $tag; + protected $buffer; + protected $tokens; + protected $seenTag; + protected $lineStart; + protected $otag; + protected $ctag; + + /** + * Scan and tokenize template source. + * + * @param string $text Mustache template source to tokenize + * @param string $delimiters Optionally, pass initial opening and closing delimiters (default: null) + * + * @return array Set of Mustache tokens + */ + public function scan($text, $delimiters = null) + { + $this->reset(); + + if ($delimiters = trim($delimiters)) { + list($otag, $ctag) = explode(' ', $delimiters); + $this->otag = $otag; + $this->ctag = $ctag; + } + + $len = strlen($text); + for ($i = 0; $i < $len; $i++) { + switch ($this->state) { + case self::IN_TEXT: + if ($this->tagChange($this->otag, $text, $i)) { + $i--; + $this->flushBuffer(); + $this->state = self::IN_TAG_TYPE; + } else { + if ($text[$i] == "\n") { + $this->filterLine(); + } else { + $this->buffer .= $text[$i]; + } + } + break; + + case self::IN_TAG_TYPE: + + $i += strlen($this->otag) - 1; + if (isset(self::$_tagTypes[$text[$i + 1]])) { + $tag = $text[$i + 1]; + $this->tagType = $tag; + } else { + $tag = null; + $this->tagType = self::T_ESCAPED; + } + + if ($this->tagType === self::T_DELIM_CHANGE) { + $i = $this->changeDelimiters($text, $i); + $this->state = self::IN_TEXT; + } else { + if ($tag !== null) { + $i++; + } + $this->state = self::IN_TAG; + } + $this->seenTag = $i; + break; + + default: + if ($this->tagChange($this->ctag, $text, $i)) { + if ($this->tagType == self::T_SECTION || $this->tagType == self::T_INVERTED) { + $newBuffer = explode(' ', trim($this->buffer), 2); + $args = ''; + if (count($newBuffer) == 2) { + $args = $newBuffer[1]; + } + $this->buffer = $newBuffer[0]; + } + $t = array( + self::TYPE => $this->tagType, + self::NAME => trim($this->buffer), + self::OTAG => $this->otag, + self::CTAG => $this->ctag, + self::INDEX => ($this->tagType == self::T_END_SECTION) ? $this->seenTag - strlen($this->otag) : $i + strlen($this->ctag), + ); + if (isset($args)) { + $t[self::ARGS] = $args; + } + $this->tokens[] = $t; + unset($t); + unset($args); + $this->buffer = ''; + $i += strlen($this->ctag) - 1; + $this->state = self::IN_TEXT; + if ($this->tagType == self::T_UNESCAPED) { + if ($this->ctag == '}}') { + $i++; + } else { + // Clean up `{{{ tripleStache }}}` style tokens. + $lastName = $this->tokens[count($this->tokens) - 1][self::NAME]; + if (substr($lastName, -1) === '}') { + $this->tokens[count($this->tokens) - 1][self::NAME] = trim(substr($lastName, 0, -1)); + } + } + } + } else { + $this->buffer .= $text[$i]; + } + break; + } + } + + $this->filterLine(true); + + return $this->tokens; + } + + /** + * Helper function to reset tokenizer internal state. + * + * @return void + */ + protected function reset() + { + $this->state = self::IN_TEXT; + $this->tagType = null; + $this->tag = null; + $this->buffer = ''; + $this->tokens = array(); + $this->seenTag = false; + $this->lineStart = 0; + $this->otag = '{{'; + $this->ctag = '}}'; + } + + /** + * Flush the current buffer to a token. + * + * @return void + */ + protected function flushBuffer() + { + if (!empty($this->buffer)) { + $this->tokens[] = array(self::TYPE => self::T_TEXT, self::VALUE => $this->buffer); + $this->buffer = ''; + } + } + + /** + * Test whether the current line is entirely made up of whitespace. + * + * @return boolean True if the current line is all whitespace + */ + protected function lineIsWhitespace() + { + $tokensCount = count($this->tokens); + for ($j = $this->lineStart; $j < $tokensCount; $j++) { + $token = $this->tokens[$j]; + if (isset(self::$_tagTypes[$token[self::TYPE]])) { + if (isset(self::$_interpolatedTags[$token[self::TYPE]])) { + return false; + } + } elseif ($token[self::TYPE] == self::T_TEXT) { + if (preg_match('/\S/', $token[self::VALUE])) { + return false; + } + } + } + + return true; + } + + /** + * Filter out whitespace-only lines and store indent levels for partials. + * + * @param bool $noNewLine Suppress the newline? (default: false) + * + * @return void + */ + protected function filterLine($noNewLine = false) + { + $this->flushBuffer(); + if ($this->seenTag && $this->lineIsWhitespace()) { + $tokensCount = count($this->tokens); + for ($j = $this->lineStart; $j < $tokensCount; $j++) { + if ($this->tokens[$j][self::TYPE] == self::T_TEXT) { + if (isset($this->tokens[$j + 1]) && $this->tokens[$j + 1][self::TYPE] == self::T_PARTIAL) { + $this->tokens[$j + 1][self::INDENT] = $this->tokens[$j][self::VALUE]; + } + + $this->tokens[$j] = null; + } + } + } elseif (!$noNewLine) { + $this->tokens[] = array(self::TYPE => self::T_TEXT, self::VALUE => "\n"); + } + + $this->seenTag = false; + $this->lineStart = count($this->tokens); + } + + /** + * Change the current Mustache delimiters. Set new `otag` and `ctag` values. + * + * @param string $text Mustache template source + * @param int $index Current tokenizer index + * + * @return int New index value + */ + protected function changeDelimiters($text, $index) + { + $startIndex = strpos($text, '=', $index) + 1; + $close = '='.$this->ctag; + $closeIndex = strpos($text, $close, $index); + + list($otag, $ctag) = explode(' ', trim(substr($text, $startIndex, $closeIndex - $startIndex))); + $this->otag = $otag; + $this->ctag = $ctag; + + return $closeIndex + strlen($close) - 1; + } + + /** + * Test whether it's time to change tags. + * + * @param string $tag Current tag name + * @param string $text Mustache template source + * @param int $index Current tokenizer index + * + * @return boolean True if this is a closing section tag + */ + protected function tagChange($tag, $text, $index) + { + return substr($text, $index, strlen($tag)) === $tag; + } +} diff --git a/src/Handlebars/simpletest.php b/src/Handlebars/simpletest.php new file mode 100644 index 0000000..55b5ef0 --- /dev/null +++ b/src/Handlebars/simpletest.php @@ -0,0 +1,61 @@ + {{!Place holder for message, leave it be in any case}} + {{#if error}} +
    + {{#each errors}} +
  • {{.}}
  • + {{/each}} +
+ {{/if}} + + {{#with t}} + {{{form}}} + {{/with}} +{{>Test}} {{! since there is no Test partial and loader is string loder, just Test is printed out}} +{{{slots.search}}} + +{{slots.tags}} + +Test + + {{#each t.errors}} +
  • {{.}}
  • + {{/each}} +END_HERE; + + + +$contextArray = + [ + 'error' => true, + 'errors' => ['err1', 'err2', 'err3'], + 'slots' => [ 'search' => 'search' ,'tags' => 'tags'], + 't' => + [ + 'errors' => ['t.err1', 't.err2'], + 'form' => '
    ' + ] + ]; +$engine = new Handlebars_Engine(); + +$helper = new Handlebars_Helpers(); + + +$engine->setHelpers($helper); + +echo $engine->render($temp, $contextArray); \ No newline at end of file