diff --git a/.gitignore b/.gitignore index fbc0d5d..4b32bc9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ vendor composer.phar composer.lock *.iml -.idea \ No newline at end of file +.idea +GPATH +GRTAGS +GTAGS diff --git a/.travis.yml b/.travis.yml index 784aee5..b2a6e8b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ php: - 5.4 - 5.5 - 5.6 + - 7.0 - hhvm branches: except: diff --git a/src/Handlebars/Arguments.php b/src/Handlebars/Arguments.php index b8ff6b5..69df4cd 100644 --- a/src/Handlebars/Arguments.php +++ b/src/Handlebars/Arguments.php @@ -142,7 +142,12 @@ class Arguments // Remove found argument from arguments string. $current_str = ltrim(substr($current_str, strlen($matches[0]))); } else { - throw new \InvalidArgumentException('Malformed arguments string'); + throw new \InvalidArgumentException( + sprintf( + 'Malformed arguments string: "%s"', + $args_string + ) + ); } } } @@ -150,26 +155,26 @@ class Arguments /** * Prepares argument's value to add to arguments list. * - * The method unescapes value and wrap it into \Handlebars\String class if - * needed. + * The method unescapes value and wrap it into \Handlebars\StringWrapper + * class if needed. * * @param string $value Argument's value * - * @return string|\Handlebars\String + * @return string|\Handlebars\StringWrapper */ protected function prepareArgumentValue($value) { // Check if argument's value is a quoted string literal if ($value[0] == "'" || $value[0] == '"') { // Remove enclosing quotes and unescape - return new String(stripcslashes(substr($value, 1, -1))); + return new StringWrapper(stripcslashes(substr($value, 1, -1))); } // Check if the value is an integer literal if (preg_match("/^-?\d+$/", $value)) { // Wrap the value into the String class to tell the Context that // it's a value and not a variable name. - return new String($value); + return new StringWrapper($value); } return $value; diff --git a/src/Handlebars/Cache.php b/src/Handlebars/Cache.php index 579f9d9..0911479 100755 --- a/src/Handlebars/Cache.php +++ b/src/Handlebars/Cache.php @@ -9,6 +9,7 @@ * @package Handlebars * @author fzerorubigd * @author Behrooz Shabani + * @author Mária Šormanová * @copyright 2012 (c) ParsPooyesh Co * @copyright 2013 (c) Behrooz Shabani * @license MIT @@ -46,14 +47,17 @@ interface Cache public function get($name); /** - * Set a cache + * Set a cache with $ttl, if present + * If $ttl set to -1, the cache expires immediately + * If $ttl set to 0 (default), cache is never purged * * @param string $name cache id * @param mixed $value data to store + * @param int $ttl time to live in seconds * * @return void */ - public function set($name, $value); + public function set($name, $value, $ttl = 0); /** * Remove cache diff --git a/src/Handlebars/Cache/APC.php b/src/Handlebars/Cache/APC.php index 8671791..69ce1b4 100755 --- a/src/Handlebars/Cache/APC.php +++ b/src/Handlebars/Cache/APC.php @@ -9,6 +9,7 @@ * @package Handlebars * @author Joey Baker * @author Behrooz Shabani + * @author Mária Šormanová * @copyright 2013 (c) Meraki, LLP * @copyright 2013 (c) Behrooz Shabani * @license MIT @@ -20,7 +21,7 @@ namespace Handlebars\Cache; use Handlebars\Cache; /** - * A dummy array cache + * A APC cache * * @category Xamin * @package Handlebars @@ -35,31 +36,51 @@ class APC implements Cache { /** - * Get cache for $name if exist. - * - * @param string $name Cache id - * - * @return mixed data on hit, boolean false on cache not found + * @var string */ - public function get($name) + private $_prefix; + + /** + * Construct the APC cache. + * + * @param string|null $prefix optional key prefix, defaults to null + */ + public function __construct( $prefix = null ) { - if (apc_exists($name)) { - return apc_fetch($name); - } - return false; + $this->_prefix = (string)$prefix; } /** - * Set a cache + * Get cache for $name if exist + * and if the cache is not older than defined TTL. + * + * @param string $name Cache id + * + * @return mixed data on hit, boolean false on cache not found/expired + */ + public function get($name) + { + $success = null; + $result = apc_fetch($this->_getKey($name), $success); + + return $success ? $result : false; + + } + + /** + * Set a cache with $ttl, if present + * If $ttl set to -1, the cache expires immediately + * If $ttl set to 0 (default), cache is never purged * * @param string $name cache id * @param mixed $value data to store + * @param int $ttl time to live in seconds * * @return void */ - public function set($name, $value) + public function set($name, $value, $ttl = 0) { - apc_store($name, $value); + apc_store($this->_getKey($name), $value, $ttl); } /** @@ -71,7 +92,19 @@ class APC implements Cache */ public function remove($name) { - apc_delete($name); + apc_delete($this->_getKey($name)); + } + + /** + * Gets the full cache key for a given cache item's + * + * @param string $name Name of the cache item + * + * @return string full cache key of cached item + */ + private function _getKey($name) + { + return $this->_prefix . ':' . $name; } } diff --git a/src/Handlebars/Cache/Disk.php b/src/Handlebars/Cache/Disk.php index 8d6625b..9f7e7a7 100644 --- a/src/Handlebars/Cache/Disk.php +++ b/src/Handlebars/Cache/Disk.php @@ -9,6 +9,7 @@ * @package Handlebars * @author Alex Soncodi * @author Behrooz Shabani + * @author Mária Šormanová * @copyright 2013 (c) Brokerloop, Inc. * @copyright 2013 (c) Behrooz Shabani * @license MIT @@ -25,6 +26,7 @@ use Handlebars\Cache; * @category Xamin * @package Handlebars * @author Alex Soncodi + * @author Mária Šormanová * @copyright 2013 (c) Brokerloop, Inc. * @license MIT * @version Release: @package_version@ @@ -81,33 +83,49 @@ class Disk implements Cache } /** - * Get cache for $name if it exists. + * Get cache for $name if it exists + * and if the cache is not older than defined TTL. * * @param string $name Cache id * - * @return mixed data on hit, boolean false on cache not found + * @return mixed data on hit, boolean false on cache not found/expired */ public function get($name) { $path = $this->_getPath($name); - - return (file_exists($path)) ? - unserialize(file_get_contents($path)) : false; + $output = false; + if (file_exists($path)) { + $file = fopen($path, "r"); + $ttl = fgets($file); + $ctime = filectime($path); + $time = time(); + if ($ttl == -1 || ($ttl > 0 && $time - $ctime > $ttl)) { + unlink($path); + } else { + $serialized_data = fread($file, filesize($path)); + $output = unserialize($serialized_data); + } + fclose($file); + } + return $output; } /** - * Set a cache + * Set a cache with $ttl, if present + * If $ttl set to -1, the cache expires immediately + * If $ttl set to 0 (default), cache is never purged * * @param string $name cache id * @param mixed $value data to store + * @param int $ttl time to live in seconds * * @return void */ - public function set($name, $value) + public function set($name, $value, $ttl = 0) { $path = $this->_getPath($name); - file_put_contents($path, serialize($value)); + file_put_contents($path, $ttl.PHP_EOL.serialize($value)); } /** diff --git a/src/Handlebars/Cache/Dummy.php b/src/Handlebars/Cache/Dummy.php index b18266c..470e911 100755 --- a/src/Handlebars/Cache/Dummy.php +++ b/src/Handlebars/Cache/Dummy.php @@ -9,6 +9,7 @@ * @package Handlebars * @author fzerorubigd * @author Behrooz Shabani + * @author Mária Šormanová * @copyright 2012 (c) ParsPooyesh Co * @copyright 2013 (c) Behrooz Shabani * @license MIT @@ -55,10 +56,14 @@ class Dummy implements Cache * * @param string $name cache id * @param mixed $value data to store + * @param int $ttl time to live in seconds + * + * $ttl is ignored since the cache is implemented + * by an array and lives only inside one request * * @return void */ - public function set($name, $value) + public function set($name, $value, $ttl = 0) { $this->_cache[$name] = $value; } diff --git a/src/Handlebars/ChildContext.php b/src/Handlebars/ChildContext.php new file mode 100644 index 0000000..cbbc67f --- /dev/null +++ b/src/Handlebars/ChildContext.php @@ -0,0 +1,89 @@ + + * @author Behrooz Shabani + * @author Chris Gray + * @author Ulrik Lystbaek + * @author Dmitriy Simushev + * @author Christian Blanquera + * @copyright 2010-2012 (c) Justin Hileman + * @copyright 2012 (c) ParsPooyesh Co + * @copyright 2013 (c) Behrooz Shabani + * @copyright 2013 (c) f0ruD A + * @license MIT + * @version GIT: $Id$ + * @link http://xamin.ir + */ + +namespace Handlebars; + +/** + * Handlebars context + * Context for a template + * + * @category Xamin + * @package Handlebars + * @author fzerorubigd + * @author Behrooz Shabani + * @copyright 2010-2012 (c) Justin Hileman + * @copyright 2012 (c) ParsPooyesh Co + * @license MIT + * @version Release: @package_version@ + * @link http://xamin.ir + */ + +class ChildContext extends Context +{ + protected $parentContext = null; + + /** + * Sets a parent context in which + * we will case for the ../ in get() + * + * @param Context $parent parent context + * + * @return void + */ + public function setParent(Context $parent) + { + $this->parentContext = $parent; + } + + /** + * Get a available from current context + * Supported types : + * variable , ../variable , variable.variable , variable.[variable] , . + * + * @param string $variableName variable name to get from current context + * @param boolean $strict strict search? if not found then throw exception + * + * @throws \InvalidArgumentException in strict mode and variable not found + * @throws \RuntimeException if supplied argument is a malformed quoted string + * @throws \InvalidArgumentException if variable name is invalid + * @return mixed + */ + public function get($variableName, $strict = false) + { + //if the variable name starts with a ../ + //and we have a parent + if (strpos($variableName, '../') === 0 + && $this->parentContext instanceof Context + ) { + //just remove the first ../ + $variableName = substr($variableName, 3); + + //and let the parent context handle the rest + return $this->parentContext->get($variableName, $strict); + } + + //otherwise, it's business as usual + return parent::get($variableName, $strict); + } +} diff --git a/src/Handlebars/Context.php b/src/Handlebars/Context.php index 79a7f70..49c390f 100644 --- a/src/Handlebars/Context.php +++ b/src/Handlebars/Context.php @@ -40,7 +40,6 @@ namespace Handlebars; class Context { - /** * List of charcters that cannot be used in identifiers. */ @@ -53,22 +52,30 @@ class Context const NOT_VALID_SEGMENT_NAME_CHARS = "]"; /** + * Context stack + * * @var array stack for context only top stack is available */ protected $stack = array(); /** + * Section stack index + * * @var array index stack for sections */ protected $index = array(); /** + * Object stack keys + * * @var array key stack for objects */ protected $key = array(); /** - * @var array Special variables stack for sections. Each stack element can + * Special variables stack for sections. + * + * @var array Each stack element can * contain elements with "@index", "@key", "@first" and "@last" keys. */ protected $specialVariables = array(); @@ -179,13 +186,13 @@ class Context * @param boolean $strict strict search? if not found then throw exception * * @throws \InvalidArgumentException in strict mode and variable not found - * @throws \RuntimeException if supplied argument is a malformed quoted string + * @throws \RuntimeException if supplied argument is a malformed quoted string * @throws \InvalidArgumentException if variable name is invalid * @return mixed */ public function get($variableName, $strict = false) { - if ($variableName instanceof \Handlebars\String) { + if ($variableName instanceof \Handlebars\StringWrapper) { return (string)$variableName; } $variableName = trim($variableName); @@ -197,7 +204,10 @@ class Context if (count($this->stack) < $level) { if ($strict) { throw new \InvalidArgumentException( - 'can not find variable in context' + sprintf( + 'Can not find variable in context: "%s"', + $variableName + ) ); } @@ -216,9 +226,13 @@ class Context if (!$variableName) { if ($strict) { throw new \InvalidArgumentException( - 'can not find variable in context' + sprintf( + 'Can not find variable in context: "%s"', + $variableName + ) ); } + return ''; } elseif ($variableName == '.' || $variableName == 'this') { return $current; @@ -228,19 +242,27 @@ class Context return $specialVariables[$variableName]; } elseif ($strict) { throw new \InvalidArgumentException( - 'can not find variable in context' + sprintf( + 'Can not find variable in context: "%s"', + $variableName + ) ); } else { return ''; } } else { $chunks = $this->_splitVariableName($variableName); - foreach ($chunks as $chunk) { - if (is_string($current) and $current == '') { - return $current; + do { + $current = current($this->stack); + foreach ($chunks as $chunk) { + if (is_string($current) and $current == '') { + return $current; + } + $current = $this->_findVariableInContext($current, $chunk, $strict); } - $current = $this->_findVariableInContext($current, $chunk, $strict); - } + prev($this->stack); + + } while ($current === null && current($this->stack) !== false); } return $current; } @@ -275,7 +297,12 @@ class Context } if ($strict) { - throw new \InvalidArgumentException('can not find variable in context'); + throw new \InvalidArgumentException( + sprintf( + 'Can not find variable in context: "%s"', + $inside + ) + ); } return $value; @@ -294,12 +321,27 @@ class Context $bad_chars = preg_quote(self::NOT_VALID_NAME_CHARS, '/'); $bad_seg_chars = preg_quote(self::NOT_VALID_SEGMENT_NAME_CHARS, '/'); - $name_pattern = "(?:[^" . $bad_chars . "\s]+)|(?:\[[^" . $bad_seg_chars . "]+\])"; - $check_pattern = "/^((" . $name_pattern . ")\.)*(" . $name_pattern . ")\.?$/"; + $name_pattern = "(?:[^" + . $bad_chars + . "\s]+)|(?:\[[^" + . $bad_seg_chars + . "]+\])"; + + $check_pattern = "/^((" + . $name_pattern + . ")\.)*(" + . $name_pattern + . ")\.?$/"; + $get_pattern = "/(?:" . $name_pattern . ")/"; if (!preg_match($check_pattern, $variableName)) { - throw new \InvalidArgumentException('variable name is invalid'); + throw new \InvalidArgumentException( + sprintf( + 'Variable name is invalid: "%s"', + $variableName + ) + ); } preg_match_all($get_pattern, $variableName, $matches); diff --git a/src/Handlebars/Handlebars.php b/src/Handlebars/Handlebars.php index 5f4462c..09a2e1b 100755 --- a/src/Handlebars/Handlebars.php +++ b/src/Handlebars/Handlebars.php @@ -10,6 +10,7 @@ * @author fzerorubigd * @author Behrooz Shabani * @author Jeff Turcotte + * @author Mária Šormanová * @copyright 2010-2012 (c) Justin Hileman * @copyright 2012 (c) ParsPooyesh Co * @copyright 2013 (c) Behrooz Shabani @@ -37,10 +38,10 @@ use Handlebars\Cache\Dummy; class Handlebars { private static $_instance = false; - const VERSION = '1.0.0'; + const VERSION = '1.1.0'; /** - * factory method + * Factory method * * @param array $options see __construct's options parameter * @@ -56,35 +57,54 @@ class Handlebars } /** + * Current tokenizer instance + * * @var Tokenizer */ private $_tokenizer; /** + * Current parser instance + * * @var Parser */ private $_parser; /** + * Current helper list + * * @var Helpers */ private $_helpers; /** + * Current loader instance + * * @var Loader */ private $_loader; /** + * Current partial loader instance + * * @var Loader */ private $_partialsLoader; /** + * Current cache instance + * * @var Cache */ private $_cache; + /** + * @var int time to live parameter in seconds for the cache usage + * default set to 0 which means that entries stay in cache + * forever and are never purged + */ + private $_ttl = 0; + /** * @var string the class to use for the template */ @@ -96,6 +116,8 @@ class Handlebars private $_escape = 'htmlspecialchars'; /** + * Parameters for the escpae method above + * * @var array parametes to pass to escape function */ private $_escapeArgs = array( @@ -138,6 +160,10 @@ class Handlebars $this->setCache($options['cache']); } + if (isset($options['ttl'])) { + $this->setTtl($options['ttl']); + } + if (isset($options['template_class'])) { $this->setTemplateClass($options['template_class']); } @@ -176,8 +202,8 @@ class Handlebars * @param mixed $data data to use as context * * @return string Rendered template - * @see Handlebars::loadTemplate - * @see Template::render + * @see Handlebars::loadTemplate + * @see Template::render */ public function render($template, $data) { @@ -246,6 +272,110 @@ class Handlebars { return $this->getHelpers()->has($name); } + + /** + * Add a new helper. + * + * @param string $name helper name + * @param mixed $helper helper callable + * + * @return void + */ + public function registerHelper($name, $helper) + { + $callback = function ($template, $context, $arg) use ($helper) { + $args = $template->parseArguments($arg); + $named = $template->parseNamedArguments($arg); + + foreach ($args as $i => $arg) { + //if it's literally string + if ($arg instanceof BaseString) { + //we have no problems here + $args[$i] = (string) $arg; + continue; + } + + //not sure what to do if it's not a string or StringWrapper + if (!is_string($arg)) { + continue; + } + + //it's a variable and we need to figure out the value of it + $args[$i] = $context->get($arg); + } + + //push the options + $args[] = array( + //special fields + 'data' => array( + 'index' => $context->get('@index'), + 'key' => $context->get('@key'), + 'first' => $context->get('@first'), + 'last' => $context->get('@last')), + // Named arguments + 'hash' => $named, + // A renderer for block helper + 'fn' => function ($inContext = null) use ($context, $template) { + $defined = !!$inContext; + + if (!$defined) { + $inContext = $context; + $inContext->push($inContext->last()); + } else if (!$inContext instanceof Context) { + $inContext = new ChildContext($inContext); + $inContext->setParent($context); + } + + $template->setStopToken('else'); + $buffer = $template->render($inContext); + $template->setStopToken(false); + //what if it's a loop ? + $template->rewind(); + //What's the point of this again? + //I mean in this context (literally) + //$template->discard($inContext); + + if (!$defined) { + $inContext->pop(); + } + + return $buffer; + }, + + // A render for the else block + 'inverse' => function ($inContext = null) use ($context, $template) { + $defined = !!$inContext; + + if (!$defined) { + $inContext = $context; + $inContext->push($inContext->last()); + } else if (!$inContext instanceof Context) { + $inContext = new ChildContext($inContext); + $inContext->setParent($context); + } + + $template->setStopToken('else'); + $template->discard($inContext); + $template->setStopToken(false); + $buffer = $template->render($inContext); + + if (!$defined) { + $inContext->pop(); + } + + return $buffer; + }, + + // The current context. + 'context' => $context, + // The current template + 'template' => $template); + + return call_user_func_array($helper, $args); + }; + + $this->addHelper($name, $callback); + } /** * Remove a helper by name. @@ -272,7 +402,7 @@ class Handlebars } /** - * get current loader + * Get current loader * * @return Loader */ @@ -298,7 +428,7 @@ class Handlebars } /** - * get current partials loader + * Get current partials loader * * @return Loader */ @@ -337,6 +467,28 @@ class Handlebars return $this->_cache; } + /** + * Set time to live for the used cache + * + * @param int $ttl time to live in seconds + * + * @return void + */ + public function setTtl($ttl) + { + $this->_ttl = $ttl; + } + + /** + * Get ttl + * + * @return int + */ + public function getTtl() + { + return $this->_ttl; + } + /** * Get current escape function * @@ -460,7 +612,10 @@ class Handlebars { if (!is_a($class, 'Handlebars\\Template', true)) { throw new \InvalidArgumentException( - 'Custom template class must extend Template' + sprintf( + 'Custom template class "%s" must extend Template', + $class + ) ); } @@ -542,7 +697,7 @@ class Handlebars } /** - * try to tokenize source, or get them from cache if available + * Try to tokenize source, or get them from cache if available * * @param string $source handlebars source code * @@ -555,7 +710,7 @@ class Handlebars if ($tree === false) { $tokens = $this->getTokenizer()->scan($source); $tree = $this->getParser()->parse($tokens); - $this->getCache()->set($hash, $tree); + $this->getCache()->set($hash, $tree, $this->_ttl); } return $tree; diff --git a/src/Handlebars/Helper.php b/src/Handlebars/Helper.php index 6c5c121..725099d 100644 --- a/src/Handlebars/Helper.php +++ b/src/Handlebars/Helper.php @@ -31,10 +31,10 @@ interface Helper /** * Execute the helper * - * @param \Handlebars\Template $template The template instance - * @param \Handlebars\Context $context The current context - * @param array $args The arguments passed the the helper - * @param string $source The source + * @param \Handlebars\Template $template The template instance + * @param \Handlebars\Context $context The current context + * @param \Handlebars\Arguments $args The arguments passed the the helper + * @param string $source The source * * @return mixed */ diff --git a/src/Handlebars/Helper/BindAttrHelper.php b/src/Handlebars/Helper/BindAttrHelper.php index f3c4f0e..a301c4a 100644 --- a/src/Handlebars/Helper/BindAttrHelper.php +++ b/src/Handlebars/Helper/BindAttrHelper.php @@ -41,10 +41,10 @@ class BindAttrHelper implements Helper /** * Execute the helper * - * @param \Handlebars\Template $template The template instance - * @param \Handlebars\Context $context The current context - * @param array $args The arguments passed the the helper - * @param string $source The source + * @param \Handlebars\Template $template The template instance + * @param \Handlebars\Context $context The current context + * @param \Handlebars\Arguments $args The arguments passed the the helper + * @param string $source The source * * @return mixed */ diff --git a/src/Handlebars/Helper/EachHelper.php b/src/Handlebars/Helper/EachHelper.php index d2dc49c..fb871c9 100644 --- a/src/Handlebars/Helper/EachHelper.php +++ b/src/Handlebars/Helper/EachHelper.php @@ -43,16 +43,17 @@ class EachHelper implements Helper /** * Execute the helper * - * @param \Handlebars\Template $template The template instance - * @param \Handlebars\Context $context The current context - * @param array $args The arguments passed the the helper - * @param string $source The source + * @param \Handlebars\Template $template The template instance + * @param \Handlebars\Context $context The current context + * @param \Handlebars\Arguments $args The arguments passed the the helper + * @param string $source The source * * @return mixed */ public function execute(Template $template, Context $context, $args, $source) { - $tmp = $context->get($args); + $positionalArgs = $args->getPositionalArguments(); + $tmp = $context->get($positionalArgs[0]); $buffer = ''; if (!$tmp) { diff --git a/src/Handlebars/Helper/IfHelper.php b/src/Handlebars/Helper/IfHelper.php index 9d742db..16398bb 100644 --- a/src/Handlebars/Helper/IfHelper.php +++ b/src/Handlebars/Helper/IfHelper.php @@ -41,10 +41,10 @@ class IfHelper implements Helper /** * Execute the helper * - * @param \Handlebars\Template $template The template instance - * @param \Handlebars\Context $context The current context - * @param array $args The arguments passed the the helper - * @param string $source The source + * @param \Handlebars\Template $template The template instance + * @param \Handlebars\Context $context The current context + * @param \Handlebars\Arguments $args The arguments passed the the helper + * @param string $source The source * * @return mixed */ @@ -53,7 +53,6 @@ class IfHelper implements Helper $parsedArgs = $template->parseArguments($args); $tmp = $context->get($parsedArgs[0]); - $context->push($context->last()); if ($tmp) { $template->setStopToken('else'); $buffer = $template->render($context); @@ -65,7 +64,6 @@ class IfHelper implements Helper $template->setStopToken(false); $buffer = $template->render($context); } - $context->pop(); return $buffer; } diff --git a/src/Handlebars/Helper/UnlessHelper.php b/src/Handlebars/Helper/UnlessHelper.php index bab6ca4..be141cb 100644 --- a/src/Handlebars/Helper/UnlessHelper.php +++ b/src/Handlebars/Helper/UnlessHelper.php @@ -41,10 +41,10 @@ class UnlessHelper implements Helper /** * Execute the helper * - * @param \Handlebars\Template $template The template instance - * @param \Handlebars\Context $context The current context - * @param array $args The arguments passed the the helper - * @param string $source The source + * @param \Handlebars\Template $template The template instance + * @param \Handlebars\Context $context The current context + * @param \Handlebars\Arguments $args The arguments passed the the helper + * @param string $source The source * * @return mixed */ @@ -53,8 +53,6 @@ class UnlessHelper implements Helper $parsedArgs = $template->parseArguments($args); $tmp = $context->get($parsedArgs[0]); - $context->push($context->last()); - if (!$tmp) { $template->setStopToken('else'); $buffer = $template->render($context); @@ -66,8 +64,6 @@ class UnlessHelper implements Helper $buffer = $template->render($context); } - $context->pop(); - return $buffer; } } diff --git a/src/Handlebars/Helper/WithHelper.php b/src/Handlebars/Helper/WithHelper.php index 262c6dd..647c9dd 100644 --- a/src/Handlebars/Helper/WithHelper.php +++ b/src/Handlebars/Helper/WithHelper.php @@ -41,16 +41,17 @@ class WithHelper implements Helper /** * Execute the helper * - * @param \Handlebars\Template $template The template instance - * @param \Handlebars\Context $context The current context - * @param array $args The arguments passed the the helper - * @param string $source The source + * @param \Handlebars\Template $template The template instance + * @param \Handlebars\Context $context The current context + * @param \Handlebars\Arguments $args The arguments passed the the helper + * @param string $source The source * * @return mixed */ public function execute(Template $template, Context $context, $args, $source) { - $context->with($args); + $positionalArgs = $args->getPositionalArguments(); + $context->with($positionalArgs[0]); $buffer = $template->render($context); $context->pop(); diff --git a/src/Handlebars/Helpers.php b/src/Handlebars/Helpers.php index 8dfb516..10d6b42 100644 --- a/src/Handlebars/Helpers.php +++ b/src/Handlebars/Helpers.php @@ -23,7 +23,7 @@ namespace Handlebars; /** * Handlebars helpers * - * a collection of helper function. normally a function like + * A collection of helper function. normally a function like * function ($sender, $name, $arguments) $arguments is unscaped arguments and * is a string, not array * @@ -35,10 +35,11 @@ namespace Handlebars; * @version Release: @package_version@ * @link http://xamin.ir */ - class Helpers { /** + * Raw helper array + * * @var array array of helpers */ protected $helpers = array(); @@ -100,7 +101,10 @@ class Helpers { if (!is_callable($helper) && ! $helper instanceof Helper) { throw new \InvalidArgumentException( - "$name Helper is not a callable or doesn't implement the Helper interface." + sprintf( + "%s Helper is not a callable or doesn't implement the Helper interface.", + $name + ) ); } $this->helpers[$name] = $helper; @@ -136,16 +140,28 @@ class Helpers public function call($name, Template $template, Context $context, $args, $source) { if (!$this->has($name)) { - throw new \InvalidArgumentException('Unknown helper: ' . $name); - } - - if ($this->helpers[$name] instanceof Helper) { - return $this->helpers[$name]->execute( - $template, $context, $args, $source + throw new \InvalidArgumentException( + sprintf( + 'Unknown helper: "%s"', + $name + ) ); } - return call_user_func($this->helpers[$name], $template, $context, $args, $source); + $parsedArgs = new Arguments($args); + if ($this->helpers[$name] instanceof Helper) { + return $this->helpers[$name]->execute( + $template, $context, $parsedArgs, $source + ); + } + + return call_user_func( + $this->helpers[$name], + $template, + $context, + $parsedArgs, + $source + ); } /** @@ -171,7 +187,12 @@ class Helpers public function __get($name) { if (!$this->has($name)) { - throw new \InvalidArgumentException('Unknown helper :' . $name); + throw new \InvalidArgumentException( + sprintf( + 'Unknown helper: "%s"', + $name + ) + ); } return $this->helpers[$name]; @@ -183,7 +204,7 @@ class Helpers * @param string $name helper name * * @return boolean - * @see Handlebras_Helpers::has + * @see Handlebras_Helpers::has */ public function __isset($name) { @@ -226,7 +247,12 @@ class Helpers public function remove($name) { if (!$this->has($name)) { - throw new \InvalidArgumentException('Unknown helper: ' . $name); + throw new \InvalidArgumentException( + sprintf( + 'Unknown helper: "%s"', + $name + ) + ); } unset($this->helpers[$name]); diff --git a/src/Handlebars/Loader/FilesystemLoader.php b/src/Handlebars/Loader/FilesystemLoader.php old mode 100755 new mode 100644 index 1010fbf..4ce6c34 --- a/src/Handlebars/Loader/FilesystemLoader.php +++ b/src/Handlebars/Loader/FilesystemLoader.php @@ -12,6 +12,7 @@ * @author Behrooz Shabani * @author Craig Bass * @author ^^ + * @author Dave Stein * @copyright 2010-2012 (c) Justin Hileman * @copyright 2012 (c) ParsPooyesh Co * @copyright 2013 (c) Behrooz Shabani @@ -23,7 +24,7 @@ namespace Handlebars\Loader; use Handlebars\Loader; -use Handlebars\String; +use Handlebars\StringWrapper; /** * Handlebars Template filesystem Loader implementation. @@ -40,7 +41,7 @@ use Handlebars\String; class FilesystemLoader implements Loader { - private $_baseDir; + protected $baseDir; private $_extension = '.handlebars'; private $_prefix = ''; private $_templates = array(); @@ -62,32 +63,8 @@ class FilesystemLoader implements Loader */ public function __construct($baseDirs, array $options = array()) { - if (is_string($baseDirs)) { - $baseDirs = array(rtrim(realpath($baseDirs), '/')); - } else { - foreach ($baseDirs as &$dir) { - $dir = rtrim(realpath($dir), '/'); - } - unset($dir); - } - - $this->_baseDir = $baseDirs; - - foreach ($this->_baseDir as $dir) { - if (!is_dir($dir)) { - throw new \RuntimeException( - 'FilesystemLoader baseDir must be a directory: ' . $dir - ); - } - } - - if (isset($options['extension'])) { - $this->_extension = '.' . ltrim($options['extension'], '.'); - } - - if (isset($options['prefix'])) { - $this->_prefix = $options['prefix']; - } + $this->setBaseDir($baseDirs); + $this->handleOptions($options); } /** @@ -99,7 +76,7 @@ class FilesystemLoader implements Loader * * @param string $name template name * - * @return String Handlebars Template source + * @return StringWrapper Handlebars Template source */ public function load($name) { @@ -107,7 +84,66 @@ class FilesystemLoader implements Loader $this->_templates[$name] = $this->loadFile($name); } - return new String($this->_templates[$name]); + return new StringWrapper($this->_templates[$name]); + } + + /** + * Sets directories to load templates from + * + * @param string|array $baseDirs A path contain template files or array of paths + * + * @return void + */ + protected function setBaseDir($baseDirs) + { + if (is_string($baseDirs)) { + $baseDirs = array($this->sanitizeDirectory($baseDirs)); + } else { + foreach ($baseDirs as &$dir) { + $dir = $this->sanitizeDirectory($dir); + } + unset($dir); + } + + foreach ($baseDirs as $dir) { + if (!is_dir($dir)) { + throw new \RuntimeException( + 'FilesystemLoader baseDir must be a directory: ' . $dir + ); + } + } + + $this->baseDir = $baseDirs; + } + + /** + * Puts directory into standardized format + * + * @param String $dir The directory to sanitize + * + * @return String + */ + protected function sanitizeDirectory($dir) + { + return rtrim(realpath($dir), '/'); + } + + /** + * Sets properties based on options + * + * @param array $options Array of Loader options (default: array()) + * + * @return void + */ + protected function handleOptions(array $options = array()) + { + if (isset($options['extension'])) { + $this->_extension = '.' . ltrim($options['extension'], '.'); + } + + if (isset($options['prefix'])) { + $this->_prefix = $options['prefix']; + } } /** @@ -138,7 +174,7 @@ class FilesystemLoader implements Loader */ protected function getFileName($name) { - foreach ($this->_baseDir as $baseDir) { + foreach ($this->baseDir as $baseDir) { $fileName = $baseDir . '/'; $fileParts = explode('/', $name); $file = array_pop($fileParts); diff --git a/src/Handlebars/Loader/InlineLoader.php b/src/Handlebars/Loader/InlineLoader.php index 5cc2908..ede329f 100644 --- a/src/Handlebars/Loader/InlineLoader.php +++ b/src/Handlebars/Loader/InlineLoader.php @@ -75,11 +75,21 @@ class InlineLoader implements Loader public function __construct($fileName, $offset) { if (!is_file($fileName)) { - throw new \InvalidArgumentException('InlineLoader expects a valid filename.'); + throw new \InvalidArgumentException( + sprintf( + 'InlineLoader expects a valid filename, "%s" given.', + $fileName + ) + ); } if (!is_int($offset) || $offset < 0) { - throw new \InvalidArgumentException('InlineLoader expects a valid file offset.'); + throw new \InvalidArgumentException( + sprintf( + 'InlineLoader expects a valid file offset, "%s" given.', + $offset + ) + ); } $this->fileName = $fileName; @@ -98,7 +108,7 @@ class InlineLoader implements Loader $this->loadTemplates(); if (!array_key_exists($name, $this->templates)) { - throw new \InvalidArgumentException("Template {$name} not found."); + throw new \InvalidArgumentException("Template $name not found."); } return $this->templates[$name]; diff --git a/src/Handlebars/Loader/StringLoader.php b/src/Handlebars/Loader/StringLoader.php old mode 100755 new mode 100644 index d3b4ec3..f4d9a6c --- a/src/Handlebars/Loader/StringLoader.php +++ b/src/Handlebars/Loader/StringLoader.php @@ -19,7 +19,7 @@ namespace Handlebars\Loader; use Handlebars\Loader; -use Handlebars\String; +use Handlebars\StringWrapper; /** * Handlebars Template string Loader implementation. @@ -42,11 +42,11 @@ class StringLoader implements Loader * * @param string $name Handlebars Template source * - * @return String Handlebars Template source + * @return StringWrapper Handlebars Template source */ public function load($name) { - return new String($name); + return new StringWrapper($name); } } diff --git a/src/Handlebars/Parser.php b/src/Handlebars/Parser.php index 3417bfb..dd7f724 100755 --- a/src/Handlebars/Parser.php +++ b/src/Handlebars/Parser.php @@ -61,7 +61,6 @@ class Parser * @throws \LogicException when nesting errors or mismatched section tags * are encountered. * @return array Token parse tree - * */ private function _buildTree(\ArrayIterator $tokens) { @@ -79,22 +78,33 @@ class Parser $result = array_pop($stack); if ($result === null) { throw new \LogicException( - 'Unexpected closing tag: /' . $token[Tokenizer::NAME] + sprintf( + 'Unexpected closing tag: /%s', + $token[Tokenizer::NAME] + ) ); } if (!array_key_exists(Tokenizer::NODES, $result) && isset($result[Tokenizer::NAME]) + && ($result[Tokenizer::TYPE] == Tokenizer::T_SECTION + || $result[Tokenizer::TYPE] == Tokenizer::T_INVERTED) && $result[Tokenizer::NAME] == $token[Tokenizer::NAME] ) { - if (isset($result[Tokenizer::TRIM_RIGHT]) && $result[Tokenizer::TRIM_RIGHT]) { - // If the start node has trim right, then its equal with the first item in the loop with + if (isset($result[Tokenizer::TRIM_RIGHT]) + && $result[Tokenizer::TRIM_RIGHT] + ) { + // If the start node has trim right, then its equal + //with the first item in the loop with // Trim left $newNodes[0][Tokenizer::TRIM_LEFT] = true; } - if (isset($token[Tokenizer::TRIM_RIGHT]) && $token[Tokenizer::TRIM_RIGHT]) { - //OK, if we have trim right here, we should pass it to the upper level. + if (isset($token[Tokenizer::TRIM_RIGHT]) + && $token[Tokenizer::TRIM_RIGHT] + ) { + //OK, if we have trim right here, we should + //pass it to the upper level. $result[Tokenizer::TRIM_RIGHT] = true; } @@ -117,4 +127,4 @@ class Parser return $stack; } -} +} \ No newline at end of file diff --git a/src/Handlebars/String.php b/src/Handlebars/String.php index b1b0d37..2c3d690 100644 --- a/src/Handlebars/String.php +++ b/src/Handlebars/String.php @@ -20,15 +20,16 @@ namespace Handlebars; /** * Handlebars string * - * @category Xamin - * @package Handlebars - * @author fzerorubigd - * @copyright 2013 Authors - * @license MIT - * @version Release: @package_version@ - * @link http://xamin.ir + * @category Xamin + * @package Handlebars + * @author fzerorubigd + * @copyright 2013 Authors + * @license MIT + * @version Release: @package_version@ + * @link http://xamin.ir + * @deprecated Since v0.10.3. Use \Handlebars\StringWrapper instead. */ -class String extends BaseString +class String extends StringWrapper { } diff --git a/src/Handlebars/StringWrapper.php b/src/Handlebars/StringWrapper.php new file mode 100644 index 0000000..7e1162e --- /dev/null +++ b/src/Handlebars/StringWrapper.php @@ -0,0 +1,34 @@ + + * @author Behrooz Shabani + * @author Dmitriy Simushev + * @copyright 2013 Authors + * @license MIT + * @version GIT: $Id$ + * @link http://xamin.ir + */ + +namespace Handlebars; + +/** + * Handlebars string + * + * @category Xamin + * @package Handlebars + * @author fzerorubigd + * @copyright 2013 Authors + * @license MIT + * @version Release: @package_version@ + * @link http://xamin.ir + */ + +class StringWrapper extends BaseString +{ +} diff --git a/src/Handlebars/Template.php b/src/Handlebars/Template.php index 45bdd37..28860fe 100644 --- a/src/Handlebars/Template.php +++ b/src/Handlebars/Template.php @@ -23,6 +23,8 @@ */ namespace Handlebars; +use Handlebars\Arguments; +use Traversable; /** * Handlebars base template @@ -31,6 +33,7 @@ namespace Handlebars; * @category Xamin * @package Handlebars * @author fzerorubigd + * @author Pascal Thormeier * @copyright 2010-2012 (c) Justin Hileman * @copyright 2012 (c) ParsPooyesh Co * @license MIT @@ -41,6 +44,8 @@ namespace Handlebars; class Template { /** + * Handlebars instance + * * @var Handlebars */ protected $handlebars; @@ -56,9 +61,11 @@ class Template protected $source = ''; /** + * Run stack + * * @var array Run stack */ - private $_stack = array(); + protected $stack = array(); /** * Handlebars template constructor @@ -72,7 +79,7 @@ class Template $this->handlebars = $engine; $this->tree = $tree; $this->source = $source; - array_push($this->_stack, array(0, $this->getTree(), false)); + array_push($this->stack, array(0, $this->getTree(), false)); } /** @@ -106,29 +113,27 @@ class Template } /** - * set stop token for render and discard method + * Set stop token for render and discard method * * @param string $token token to set as stop token or false to remove * * @return void */ - public function setStopToken($token) { - $topStack = array_pop($this->_stack); + $topStack = array_pop($this->stack); $topStack[2] = $token; - array_push($this->_stack, $topStack); + array_push($this->stack, $topStack); } /** - * get current stop token + * Get current stop token * * @return string|bool */ - public function getStopToken() { - $topStack = end($this->_stack); + $topStack = end($this->stack); return $topStack[2]; } @@ -140,7 +145,7 @@ class Template */ public function getCurrentTokenTree() { - $topStack = end($this->_stack); + $topStack = end($this->stack); return $topStack[1]; } @@ -158,7 +163,7 @@ class Template if (!$context instanceof Context) { $context = new Context($context); } - $topTree = end($this->_stack); // never pop a value from stack + $topTree = end($this->stack); // never pop a value from stack list($index, $tree, $stop) = $topTree; $buffer = ''; @@ -173,33 +178,42 @@ class Template ) { break; } - if (isset($current[Tokenizer::TRIM_LEFT]) && $current[Tokenizer::TRIM_LEFT]) { + if (isset($current[Tokenizer::TRIM_LEFT]) + && $current[Tokenizer::TRIM_LEFT] + ) { $buffer = rtrim($buffer); } - $tmp = $this->_renderInternal($current, $context); + $tmp = $this->renderInternal($current, $context); - if (isset($current[Tokenizer::TRIM_LEFT]) && $current[Tokenizer::TRIM_LEFT]) { + if (isset($current[Tokenizer::TRIM_LEFT]) + && $current[Tokenizer::TRIM_LEFT] + ) { $tmp = rtrim($tmp); } - if ($rTrim || (isset($current[Tokenizer::TRIM_RIGHT]) && $current[Tokenizer::TRIM_RIGHT])) { + if ($rTrim + || (isset($current[Tokenizer::TRIM_RIGHT]) + && $current[Tokenizer::TRIM_RIGHT]) + ) { $tmp = ltrim($tmp); } $buffer .= $tmp; - // Some time, there is more than one string token (first is empty), + // Some time, there is more than + //one string token (first is empty), //so we need to trim all of them in one shot $rTrim = (empty($tmp) && $rTrim) || - isset($current[Tokenizer::TRIM_RIGHT]) && $current[Tokenizer::TRIM_RIGHT]; + isset($current[Tokenizer::TRIM_RIGHT]) + && $current[Tokenizer::TRIM_RIGHT]; } if ($stop) { //Ok break here, the helper should be aware of this. - $newStack = array_pop($this->_stack); + $newStack = array_pop($this->stack); $newStack[0] = $index; $newStack[2] = false; //No stop token from now on - array_push($this->_stack, $newStack); + array_push($this->stack, $newStack); } return $buffer; @@ -213,7 +227,7 @@ class Template * * @return string */ - private function _renderInternal($current, $context) + protected function renderInternal($current, $context) { $result = ''; switch ($current[Tokenizer::TYPE]) { @@ -222,16 +236,16 @@ class Template case Tokenizer::T_SECTION : $newStack = isset($current[Tokenizer::NODES]) ? $current[Tokenizer::NODES] : array(); - array_push($this->_stack, array(0, $newStack, false)); + array_push($this->stack, array(0, $newStack, false)); $result = $this->_section($context, $current); - array_pop($this->_stack); + array_pop($this->stack); break; case Tokenizer::T_INVERTED : $newStack = isset($current[Tokenizer::NODES]) ? $current[Tokenizer::NODES] : array(); - array_push($this->_stack, array(0, $newStack, false)); + array_push($this->stack, array(0, $newStack, false)); $result = $this->_inverted($context, $current); - array_pop($this->_stack); + array_pop($this->stack); break; case Tokenizer::T_COMMENT : $result = ''; @@ -269,7 +283,7 @@ class Template */ public function discard() { - $topTree = end($this->_stack); //This method never pop a value from stack + $topTree = end($this->stack); //This method never pop a value from stack list($index, $tree, $stop) = $topTree; while (array_key_exists($index, $tree)) { $current = $tree[$index]; @@ -284,10 +298,10 @@ class Template } if ($stop) { //Ok break here, the helper should be aware of this. - $newStack = array_pop($this->_stack); + $newStack = array_pop($this->stack); $newStack[0] = $index; $newStack[2] = false; - array_push($this->_stack, $newStack); + array_push($this->stack, $newStack); } return ''; @@ -300,9 +314,9 @@ class Template */ public function rewind() { - $topStack = array_pop($this->_stack); + $topStack = array_pop($this->stack); $topStack[0] = 0; - array_push($this->_stack, $topStack); + array_push($this->stack, $topStack); } /** @@ -329,7 +343,9 @@ class Template } // subexpression parsing loop - $subexprs = array(); // will contain all subexpressions inside outermost brackets + // will contain all subexpressions + // inside outermost brackets + $subexprs = array(); $insideOf = array( 'single' => false, 'double' => false ); $lvl = 0; $cur_start = 0; @@ -351,7 +367,11 @@ class Template if ($cur == ')' && ! $insideOf['single'] && ! $insideOf['double']) { $lvl--; if ($lvl == 0) { - $subexprs[] = substr($current[Tokenizer::ARGS], $cur_start, $i - $cur_start); + $subexprs[] = substr( + $current[Tokenizer::ARGS], + $cur_start, + $i - $cur_start + ); } } @@ -370,16 +390,32 @@ class Template Tokenizer::INDEX => $current[Tokenizer::INDEX], Tokenizer::ARGS => implode(" ", array_slice($cmd, 1)) ); + // resolve the node recursively - $resolved = addcslashes($this->_handlebarsStyleSection($context, $section_node), '"'); + $resolved = $this->_handlebarsStyleSection( + $context, + $section_node + ); + + $resolved = addcslashes($resolved, '"'); // replace original subexpression with result - $current[Tokenizer::ARGS] = str_replace('('.$expr.')', '"' . $resolved . '"', $current[Tokenizer::ARGS]); + $current[Tokenizer::ARGS] = str_replace( + '('.$expr.')', + '"' . $resolved . '"', + $current[Tokenizer::ARGS] + ); } } - $return = $helpers->call($sectionName, $this, $context, $current[Tokenizer::ARGS], $source); + $return = $helpers->call( + $sectionName, + $this, + $context, + $current[Tokenizer::ARGS], + $source + ); - if ($return instanceof String) { + if ($return instanceof StringWrapper) { return $this->handlebars->loadString($return)->render($context); } else { return $return; @@ -402,42 +438,39 @@ class Template // fallback to mustache style each/with/for just if there is // no argument at all. try { - $sectionVar = $context->get($sectionName, true); + $sectionVar = $context->get($sectionName, false); } catch (\InvalidArgumentException $e) { throw new \RuntimeException( - $sectionName . ' is not registered as a helper' + sprintf( + '"%s" is not registered as a helper', + $sectionName + ) ); } $buffer = ''; - if (is_array($sectionVar) || $sectionVar instanceof \Traversable) { - $isList = is_array($sectionVar) && - (array_keys($sectionVar) === range(0, count($sectionVar) - 1)); + if ($this->_checkIterable($sectionVar)) { $index = 0; - $lastIndex = $isList ? (count($sectionVar) - 1) : false; - + $lastIndex = (count($sectionVar) - 1); foreach ($sectionVar as $key => $d) { - $specialVariables = array( - '@index' => $index, - '@first' => ($index === 0), - '@last' => ($index === $lastIndex), + $context->pushSpecialVariables( + array( + '@index' => $index, + '@first' => ($index === 0), + '@last' => ($index === $lastIndex), + '@key' => $key + ) ); - if (!$isList) { - $specialVariables['@key'] = $key; - } - $context->pushSpecialVariables($specialVariables); $context->push($d); $buffer .= $this->render($context); $context->pop(); $context->popSpecialVariables(); $index++; } - } elseif (is_object($sectionVar)) { + } elseif ($sectionVar) { //Act like with $context->push($sectionVar); $buffer = $this->render($context); $context->pop(); - } elseif ($sectionVar) { - $buffer = $this->render($context); } return $buffer; @@ -462,7 +495,10 @@ class Template return $this->_mustacheStyleSection($context, $current); } else { throw new \RuntimeException( - $sectionName . ' is not registered as a helper' + sprintf( + '"%s"" is not registered as a helper', + $sectionName + ) ); } } @@ -500,12 +536,44 @@ class Template $partial = $this->handlebars->loadPartial($current[Tokenizer::NAME]); if ($current[Tokenizer::ARGS]) { - $context = $context->get($current[Tokenizer::ARGS]); + $arguments = new Arguments($current[Tokenizer::ARGS]); + + $context = new Context($this->_preparePartialArguments($context, $arguments)); } return $partial->render($context); } + /** + * Prepare the arguments of a partial to actual array values to be used in a new context + * + * @param Context $context Current context + * @param Arguments $arguments Arguments for partial + * + * @return array + */ + private function _preparePartialArguments(Context $context, Arguments $arguments) + { + $positionalArgs = array(); + foreach ($arguments->getPositionalArguments() as $positionalArg) { + $contextArg = $context->get($positionalArg); + if (is_array($contextArg)) { + foreach ($contextArg as $key => $value) { + $positionalArgs[$key] = $value; + } + } else { + $positionalArgs[$positionalArg] = $contextArg; + } + } + + $namedArguments = array(); + foreach ($arguments->getNamedArguments() as $key => $value) { + $namedArguments[$key] = $context->get($value); + } + + return array_merge($positionalArgs, $namedArguments); + } + /** * Check if there is a helper with this variable name available or not. @@ -524,9 +592,9 @@ class Template } /** - * get replacing value of a tag + * Get replacing value of a tag * - * will process the tag as section, if a helper with the same name could be + * Will process the tag as section, if a helper with the same name could be * found, so {{helper arg}} can be used instead of {{#helper arg}}. * * @param Context $context current context @@ -589,16 +657,16 @@ class Template if (is_array($value)) { return 'Array'; } - if ($escaped) { + if ($escaped && !($value instanceof SafeString)) { $args = $this->handlebars->getEscapeArgs(); - array_unshift($args, $value); + array_unshift($args, (string)$value); $value = call_user_func_array( $this->handlebars->getEscape(), array_values($args) ); } - return $value; + return (string)$value; } /** @@ -639,4 +707,31 @@ class Template return $args->getPositionalArguments(); } + + /** + * Tests whether a value should be iterated over (e.g. in a section context). + * + * @param mixed $value Value to check if iterable. + * + * @return bool True if the value is 'iterable' + * + * @see https://github.com/bobthecow/mustache.php/blob/18a2adc/src/Mustache/Template.php#L85-L113 + */ + private function _checkIterable($value) + { + switch (gettype($value)) { + case 'object': + return $value instanceof Traversable; + case 'array': + $i = 0; + foreach ($value as $k => $v) { + if ($k !== $i++) { + return false; + } + } + return true; + default: + return false; + } + } } diff --git a/src/Handlebars/Tokenizer.php b/src/Handlebars/Tokenizer.php index 1c18bde..d0e0ea0 100644 --- a/src/Handlebars/Tokenizer.php +++ b/src/Handlebars/Tokenizer.php @@ -122,7 +122,7 @@ class Tokenizer */ public function scan($text/*, $delimiters = null*/) { - if ($text instanceof String) { + if ($text instanceof StringWrapper) { $text = $text->getString(); } $this->reset(); @@ -136,7 +136,6 @@ class Tokenizer */ $len = strlen($text); for ($i = 0; $i < $len; $i++) { - $this->escaping = $this->tagChange(self::T_ESCAPE, $text, $i); // To play nice with helpers' arguments quote and apostrophe marks @@ -144,13 +143,21 @@ class Tokenizer $quoteInTag = $this->state != self::IN_TEXT && ($text[$i] == self::T_SINGLE_Q || $text[$i] == self::T_DOUBLE_Q); - if ($this->escaped && $text[$i] != self::T_UNESCAPED && !$quoteInTag) { + if ($this->escaped && !$this->tagChange($this->otag, $text, $i) && !$quoteInTag) { $this->buffer .= "\\"; } switch ($this->state) { case self::IN_TEXT: - if ($this->tagChange($this->otag. self::T_TRIM, $text, $i) and !$this->escaped) { + // Handlebars.js does not think that openning curly brace in + // "\\\{{data}}" template is escaped. Instead it removes one + // slash and leaves others "as is". To emulate similar behavior + // we have to check the last character in the buffer. If it's a + // slash we actually does not need to escape openning curly + // brace. + $prev_slash = substr($this->buffer, -1) == '\\'; + + if ($this->tagChange($this->otag. self::T_TRIM, $text, $i) and (!$this->escaped || $prev_slash)) { $this->flushBuffer(); $this->state = self::IN_TAG_TYPE; $this->trimLeft = true; @@ -158,12 +165,16 @@ class Tokenizer $this->buffer .= "{{{"; $i += 2; continue; - } elseif ($this->tagChange($this->otag, $text, $i) and !$this->escaped) { + } elseif ($this->tagChange($this->otag, $text, $i) and (!$this->escaped || $prev_slash)) { $i--; $this->flushBuffer(); $this->state = self::IN_TAG_TYPE; } elseif ($this->escaped and $this->escaping) { - $this->buffer .= "\\"; + // We should not add extra slash before opening tag because + // doubled slash where should be transformed to single one + if (($i + 1) < $len && !$this->tagChange($this->otag, $text, $i + 1)) { + $this->buffer .= "\\"; + } } elseif (!$this->escaping) { if ($text[$i] == "\n") { $this->filterLine(); diff --git a/tests/Xamin/Cache/APCTest.php b/tests/Xamin/Cache/APCTest.php new file mode 100644 index 0000000..8947a7f --- /dev/null +++ b/tests/Xamin/Cache/APCTest.php @@ -0,0 +1,118 @@ + + * @author Dmitriy Simushev + * @author Mária Šormanová + * @copyright 2013 (c) f0ruD A + * @license MIT + * @version GIT: $Id$ + * @link http://xamin.ir + */ + +/** + * Test of APC cache driver + * + * Run without sikp: + * php -d apc.enable_cli=1 ./vendor/bin/phpunit + * + * @category Xamin + * @package Handlebars + * @subpackage Test + * @author Tamás Szijártó + * @license MIT + * @version Release: @package_version@ + * @link http://xamin.ir + */ +class APCTest extends \PHPUnit_Framework_TestCase +{ + /** + * {@inheritdoc} + * + * @return void + */ + public function setUp() + { + if ( ! extension_loaded('apc') || false === @apc_cache_info()) { + $this->markTestSkipped('The ' . __CLASS__ .' requires the use of APC'); + } + } + + /** + * Return the new driver + * + * @param null|string $prefix optional key prefix, defaults to null + * + * @return \Handlebars\Cache\APC + */ + private function _getCacheDriver( $prefix = null ) + { + return new \Handlebars\Cache\APC($prefix); + } + + /** + * Test with cache prefix + * + * @return void + */ + public function testWithPrefix() + { + $prefix = __CLASS__; + $driver = $this->_getCacheDriver($prefix); + + $this->assertEquals(false, $driver->get('foo')); + + $driver->set('foo', 10); + $this->assertEquals(10, $driver->get('foo')); + + $driver->set('foo', array(12)); + $this->assertEquals(array(12), $driver->get('foo')); + + $driver->remove('foo'); + $this->assertEquals(false, $driver->get('foo')); + } + + /** + * Test without cache prefix + * + * @return void + */ + public function testWithoutPrefix() + { + $driver = $this->_getCacheDriver(); + + $this->assertEquals(false, $driver->get('foo')); + + $driver->set('foo', 20); + $this->assertEquals(20, $driver->get('foo')); + + $driver->set('foo', array(22)); + + $this->assertEquals(array(22), $driver->get('foo')); + + $driver->remove('foo'); + $this->assertEquals(false, $driver->get('foo')); + } + + /** + * Test ttl + * + * @return void + */ + public function testTtl() + { + $driver = $this->_getCacheDriver(); + + $driver->set('foo', 10, -1); + $this->assertEquals(false, $driver->get('foo')); + + $driver->set('foo', 20, 3600); + $this->assertEquals(20, $driver->get('foo')); + } +} \ No newline at end of file diff --git a/tests/Xamin/Cache/DiskTest.php b/tests/Xamin/Cache/DiskTest.php new file mode 100644 index 0000000..abd9661 --- /dev/null +++ b/tests/Xamin/Cache/DiskTest.php @@ -0,0 +1,84 @@ + + * @copyright 2016 (c) Mária Šormanová + * @license MIT + * @version GIT: $Id$ + * @link http://xamin.ir + */ + +/** + * Test of Disk cache driver + * + * @category Xamin + * @package Handlebars + * @subpackage Test + * @author Mária Šormanová + * @license MIT + * @version Release: @package_version@ + * @link http://xamin.ir + */ + +class DiskTest extends \PHPUnit_Framework_TestCase +{ + /** + * {@inheritdoc} + * + * @return void + */ + public function setUp() + { + \Handlebars\Autoloader::register(); + } + + /** + * Return the new driver + * + * @param string $path folder where the cache is located + * + * @return \Handlebars\Cache\Disk + */ + private function _getCacheDriver( $path = '') + { + return new \Handlebars\Cache\Disk($path); + } + + /** + * Test the Disk cache + * + * @return void + */ + public function testDiskCache() + { + $cache_dir = getcwd().'/tests/cache'; + $driver = $this->_getCacheDriver($cache_dir); + + $this->assertEquals(false, $driver->get('foo')); + + $driver->set('foo', "hello world"); + $this->assertEquals("hello world", $driver->get('foo')); + + $driver->set('foo', "hello world", -1); + $this->assertEquals(false, $driver->get('foo')); + + $driver->set('foo', "hello world", 3600); + $this->assertEquals("hello world", $driver->get('foo')); + + $driver->set('foo', array(12)); + $this->assertEquals(array(12), $driver->get('foo')); + + $driver->remove('foo'); + $this->assertEquals(false, $driver->get('foo')); + + rmdir($cache_dir); + } +} + +?> \ No newline at end of file diff --git a/tests/Xamin/HandlebarsTest.php b/tests/Xamin/HandlebarsTest.php index 4b87caa..e892163 100644 --- a/tests/Xamin/HandlebarsTest.php +++ b/tests/Xamin/HandlebarsTest.php @@ -118,32 +118,47 @@ class HandlebarsTest extends \PHPUnit_Framework_TestCase 'Test' ), array( - "\{{data}}", // is equal to \\{{data}} + "\\{{data}}", // is equal to \{{data}} in template file array('data' => 'foo'), '{{data}}', ), array( - '\\\\{{data}}', + '\\\\{{data}}', // is equal to \\{{data}} in template file array('data' => 'foo'), - '\\\\foo' + '\\foo' // is equals to \foo in output ), array( - '\\\{{data}}', // is equal to \\\\{{data}} in php + '\\\\\\{{data}}', // is equal to \\\{{data}} in template file array('data' => 'foo'), - '\\\\foo' + '\\\\foo' // is equals to \\foo in output ), array( - '\{{{data}}}', + '\\\\\\\\{{data}}', // is equal to \\\\{{data}} in template file + array('data' => 'foo'), + '\\\\\\foo' // is equals to \\\foo in output + ), + array( + '\{{{data}}}', // is equal to \{{{data}}} in template file array('data' => 'foo'), '{{{data}}}' ), array( - '\pi', + '\pi', // is equal to \pi in template array(), '\pi' ), array( - '\\\\\\\\qux', + '\\\\foo', // is equal to \\foo in template + array(), + '\\\\foo' + ), + array( + '\\\\\\bar', // is equal to \\\bar in template + array(), + '\\\\\\bar' + ), + array( + '\\\\\\\\qux', // is equal to \\\\qux in template file array(), '\\\\\\\\qux' ), @@ -161,7 +176,12 @@ class HandlebarsTest extends \PHPUnit_Framework_TestCase '{{#if first}}The first{{else}}{{#if second}}The second{{/if}}{{/if}}', array('first' => false, 'second' => true), 'The second' - ) + ), + array( + '{{#value}}Hello {{value}}, from {{parent_context}}{{/value}}', + array('value' => 'string', 'parent_context' => 'parent string'), + 'Hello string, from parent string' + ), ); } @@ -380,7 +400,7 @@ class HandlebarsTest extends \PHPUnit_Framework_TestCase $this->assertEquals('Test helper is called with a b c', $engine->render('{{test2 a b c}}', array())); $engine->addHelper('renderme', function () { - return new \Handlebars\String("{{test}}"); + return new \Handlebars\StringWrapper("{{test}}"); }); $this->assertEquals('Test helper is called', $engine->render('{{#renderme}}', array())); @@ -438,13 +458,213 @@ class HandlebarsTest extends \PHPUnit_Framework_TestCase $this->setExpectedException('InvalidArgumentException'); $engine->getHelpers()->call('invalid', $engine->loadTemplate(''), new \Handlebars\Context(), '', ''); } - - public function testInvalidHelperMustacheStyle() + + public function testRegisterHelper() { - $this->setExpectedException('RuntimeException'); $loader = new \Handlebars\Loader\StringLoader(); $engine = new \Handlebars\Handlebars(array('loader' => $loader)); - $engine->render('{{#NOTVALID}}XXX{{/NOTVALID}}', array()); + //date_default_timezone_set('GMT'); + + //FIRST UP: some awesome helpers!! + + //translations + $translations = array( + 'hello' => 'bonjour', + 'my name is %s' => 'mon nom est %s', + 'how are your %s kids and %s' => 'comment sont les enfants de votre %s et %s' + ); + + //i18n + $engine->registerHelper('_', function($key) use ($translations) { + $args = func_get_args(); + $key = array_shift($args); + $options = array_pop($args); + + //make sure it's a string + $key = (string) $key; + + //by default the translation is the key + $translation = $key; + + //if there is a translation + if(isset($translations[$key])) { + //translate it + $translation = $translations[$key]; + } + + //if there are more arguments + if(!empty($args)) { + //it means the translations was + //something like 'Hello %s' + return vsprintf($translation, $args); + } + + //just return what we got + return $translation; + }); + + //create a better if helper + $engine->registerHelper('when', function($value1, $operator, $value2, $options) { + $valid = false; + //the amazing reverse switch! + switch (true) { + case $operator == 'eq' && $value1 == $value2: + case $operator == '==' && $value1 == $value2: + case $operator == 'req' && $value1 === $value2: + case $operator == '===' && $value1 === $value2: + case $operator == 'neq' && $value1 != $value2: + case $operator == '!=' && $value1 != $value2: + case $operator == 'rneq' && $value1 !== $value2: + case $operator == '!==' && $value1 !== $value2: + case $operator == 'lt' && $value1 < $value2: + case $operator == '<' && $value1 < $value2: + case $operator == 'lte' && $value1 <= $value2: + case $operator == '<=' && $value1 <= $value2: + case $operator == 'gt' && $value1 > $value2: + case $operator == '>' && $value1 > $value2: + case $operator == 'gte' && $value1 >= $value2: + case $operator == '>=' && $value1 >= $value2: + case $operator == 'and' && $value1 && $value2: + case $operator == '&&' && ($value1 && $value2): + case $operator == 'or' && ($value1 || $value2): + case $operator == '||' && ($value1 || $value2): + $valid = true; + break; + } + + if($valid) { + return $options['fn'](); + } + + return $options['inverse'](); + }); + + //a loop helper + $engine->registerHelper('loop', function($object, $options) { + //expected for subtemplates of this block to use + // {{value.profile_name}} vs {{profile_name}} + // {{key}} vs {{@index}} + + $i = 0; + $buffer = array(); + $total = count($object); + + //loop through the object + foreach($object as $key => $value) { + //call the sub template and + //add it to the buffer + $buffer[] = $options['fn'](array( + 'key' => $key, + 'value' => $value, + 'last' => ++$i === $total + )); + } + + return implode('', $buffer); + }); + + //array in + $engine->registerHelper('in', function(array $array, $key, $options) { + if(in_array($key, $array)) { + return $options['fn'](); + } + + return $options['inverse'](); + }); + + //converts date formats to other formats + $engine->registerHelper('date', function($time, $format, $options) { + return date($format, strtotime($time)); + }); + + //nesting helpers, these don't really help anyone :) + $engine->registerHelper('nested1', function($test1, $test2, $options) { + return $options['fn'](array( + 'test4' => $test1, + 'test5' => 'This is Test 5' + )); + }); + + $engine->registerHelper('nested2', function($options) { + return $options['fn'](array('test6' => 'This is Test 6')); + }); + + //NEXT UP: some practical case studies + + //case 1 - i18n + $variable1 = array(); + $template1 = "{{_ 'hello'}}, {{_ 'my name is %s' 'Foo'}}! {{_ 'how are your %s kids and %s' 6 'dog'}}?"; + $expected1 = 'bonjour, mon nom est Foo! comment sont les enfants de votre 6 et dog?'; + + //case 2 - when + $variable2 = array('gender' => 'female', 'foo' => 'bar'); + $template2 = "Hello {{#when gender '===' 'male'}}sir{{else}}maam{{/when}} {{foo}}"; + $expected2 = 'Hello maam bar'; + + //case 3 - when else + $variable3 = array('gender' => 'male'); + $template3 = "Hello {{#when gender '===' 'male'}}sir{{else}}maam{{/when}}"; + $expected3 = 'Hello sir'; + + //case 4 - loop + $variable4 = array( + 'rows' => array( + array( + 'profile_name' => 'Jane Doe', + 'profile_created' => '2014-04-04 00:00:00' + ), + array( + 'profile_name' => 'John Doe', + 'profile_created' => '2015-01-21 00:00:00' + ) + ) + ); + $template4 = "{{#loop rows}}
  • {{value.profile_name}} - {{date value.profile_created 'M d'}}
  • {{/loop}}"; + $expected4 = '
  • Jane Doe - Apr 04
  • John Doe - Jan 21
  • '; + + //case 5 - array in + $variable5 = $variable4; + $variable5['me'] = 'Jack Doe'; + $variable5['admins'] = array('Jane Doe', 'John Doe'); + $template5 = "{{#in admins me}}
      ".$template4."
    {{else}}No Access{{/in}}{{suffix}}"; + $expected5 = 'No Access'; + + //case 6 - array in else + $variable6 = $variable5; + $variable6['me'] = 'Jane Doe'; + $variable6['suffix'] = 'qux'; + $template6 = $template5; + $expected6 = '
    • Jane Doe - Apr 04
    • John Doe - Jan 21
    qux'; + + //case 7 - nested templates and parent-grand variables + $variable7 = array('test' => 'Hello World'); + $template7 = '{{#nested1 test "test2"}} ' + .'In 1: {{test4}} {{#nested1 ../test \'test3\'}} ' + .'In 2: {{test5}}{{#nested2}} ' + .'In 3: {{test6}} {{../../../test}}{{/nested2}}{{/nested1}}{{/nested1}}'; + $expected7 = ' In 1: Hello World In 2: This is Test 5 In 3: This is Test 6 Hello World'; + + //case 8 - when inside an each + $variable8 = array('data' => array(0, 1, 2, 3),'finish' => 'ok'); + $template8 = '{{#each data}}{{#when this ">" "0"}}{{this}}{{/when}}{{/each}} {{finish}}'; + $expected8 = '123 ok'; + + //case 9 - when inside an each + $variable9 = array('data' => array(),'finish' => 'ok'); + $template9 = '{{#each data}}{{#when this ">" "0"}}{{this}}{{/when}}{{else}}foo{{/each}} {{finish}}'; + $expected9 = 'foo ok'; + + //LAST UP: the actual testing + + $this->assertEquals($expected1, $engine->render($template1, $variable1)); + $this->assertEquals($expected2, $engine->render($template2, $variable2)); + $this->assertEquals($expected3, $engine->render($template3, $variable3)); + $this->assertEquals($expected4, $engine->render($template4, $variable4)); + $this->assertEquals($expected5, $engine->render($template5, $variable5)); + $this->assertEquals($expected6, $engine->render($template6, $variable6)); + $this->assertEquals($expected7, $engine->render($template7, $variable7)); + $this->assertEquals($expected8, $engine->render($template8, $variable8)); + $this->assertEquals($expected9, $engine->render($template9, $variable9)); } public function testInvalidHelper() @@ -464,16 +684,23 @@ class HandlebarsTest extends \PHPUnit_Framework_TestCase $engine = new \Handlebars\Handlebars(array('loader' => $loader)); $this->assertEquals('yes', $engine->render('{{#x}}yes{{/x}}', array('x' => true))); $this->assertEquals('', $engine->render('{{#x}}yes{{/x}}', array('x' => false))); + $this->assertEquals('', $engine->render('{{#NOTVALID}}XXX{{/NOTVALID}}', array())); $this->assertEquals('yes', $engine->render('{{^x}}yes{{/x}}', array('x' => false))); $this->assertEquals('', $engine->render('{{^x}}yes{{/x}}', array('x' => true))); $this->assertEquals('1234', $engine->render('{{#x}}{{this}}{{/x}}', array('x' => array(1, 2, 3, 4)))); $this->assertEquals('012', $engine->render('{{#x}}{{@index}}{{/x}}', array('x' => array('a', 'b', 'c')))); - $this->assertEquals('abc', $engine->render('{{#x}}{{@key}}{{/x}}', array('x' => array('a' => 1, 'b' => 2, 'c' => 3)))); - $this->assertEquals('the_only_key', $engine->render('{{#x}}{{@key}}{{/x}}', array('x' => array('the_only_key' => 1)))); + $this->assertEquals('123', $engine->render('{{#x}}{{a}}{{b}}{{c}}{{/x}}', array('x' => array('a' => 1, 'b' => 2, 'c' => 3)))); + $this->assertEquals('1', $engine->render('{{#x}}{{the_only_key}}{{/x}}', array('x' => array('the_only_key' => 1)))); $std = new stdClass(); $std->value = 1; + $std->other = 4; $this->assertEquals('1', $engine->render('{{#x}}{{value}}{{/x}}', array('x' => $std))); $this->assertEquals('1', $engine->render('{{{x}}}', array('x' => 1))); + $this->assertEquals('1 2', $engine->render('{{#x}}{{value}} {{parent}}{{/x}}', array('x' => $std, 'parent' => 2))); + + $y = new stdClass(); + $y->value = 2; + $this->assertEquals('2 1 3 4', $engine->render('{{#x}}{{#y}}{{value}} {{x.value}} {{from_root}} {{other}}{{/y}}{{/x}}', array('x' => $std, 'y' => $y, 'from_root' => 3))); } /** @@ -560,14 +787,28 @@ class HandlebarsTest extends \PHPUnit_Framework_TestCase /** * test String class */ - public function testStringClass() + public function testStringWrapperClass() { - $string = new \Handlebars\String('test'); + $string = new \Handlebars\StringWrapper('test'); $this->assertEquals('test', $string->getString()); $string->setString('new'); $this->assertEquals('new', $string->getString()); } + /** + * test SafeString class + */ + public function testSafeStringClass() + { + $loader = new \Handlebars\Loader\StringLoader(); + $helpers = new \Handlebars\Helpers(); + $engine = new \Handlebars\Handlebars(array('loader' => $loader, 'helpers' => $helpers)); + + $this->assertEquals('Test', $engine->render('{{string}}', array( + 'string' => new \Handlebars\SafeString('Test') + ))); + } + /** * @param $dir * @@ -700,7 +941,11 @@ EOM; public function testPartial() { $loader = new \Handlebars\Loader\StringLoader(); - $partialLoader = new \Handlebars\Loader\ArrayLoader(array('test' => '{{key}}', 'bar' => 'its foo')); + $partialLoader = new \Handlebars\Loader\ArrayLoader(array( + 'test' => '{{key}}', + 'bar' => 'its foo', + 'presetVariables' => '{{myVar}}', + )); $partialAliasses = array('foo' => 'bar'); $engine = new \Handlebars\Handlebars( array( @@ -710,6 +955,11 @@ EOM; ) ); + $this->assertEquals('foobar', $engine->render("{{>presetVariables myVar='foobar'}}", array())); + $this->assertEquals('foobar=barbaz', $engine->render("{{>presetVariables myVar='foobar=barbaz'}}", array())); + $this->assertEquals('qux', $engine->render("{{>presetVariables myVar=foo}}", array('foo' => 'qux'))); + $this->assertEquals('qux', $engine->render("{{>presetVariables myVar=foo.bar}}", array('foo' => array('bar' => 'qux')))); + $this->assertEquals('HELLO', $engine->render('{{>test parameter}}', array('parameter' => array('key' => 'HELLO')))); $this->assertEquals('its foo', $engine->render('{{>foo}}', array())); $engine->registerPartial('foo-again', 'bar'); @@ -718,6 +968,7 @@ EOM; $this->setExpectedException('RuntimeException'); $engine->render('{{>foo-again}}', array()); + } /** @@ -742,6 +993,9 @@ EOM; // Reference array as string $this->assertEquals('Array', $engine->render('{{var}}', array('var' => array('test')))); + // Test class with __toString method + $this->assertEquals('test', $engine->render('{{var}}', array('var' => new TestClassWithToStringMethod()))); + $obj = new DateTime(); $time = $obj->getTimestamp(); $this->assertEquals($time, $engine->render('{{time.getTimestamp}}', array('time' => $obj))); @@ -1076,7 +1330,7 @@ EOM; public function testString() { - $string = new \Handlebars\String("Hello World"); + $string = new \Handlebars\StringWrapper("Hello World"); $this->assertEquals((string)$string, "Hello World"); } @@ -1154,16 +1408,34 @@ EOM; $this->assertEquals('A-B', $engine->render('{{concat (concat a "-") b}}', array('a' => 'A', 'b' => 'B', 'A-' => '!'))); } + public function ifUnlessDepthDoesntChangeProvider() + { + return array(array( + '{{#with b}}{{#if this}}{{../a}}{{/if}}{{/with}}', + array('a' => 'good', 'b' => 'stump'), + 'good', + ), array( + '{{#with b}}{{#unless false}}{{../a}}{{/unless}}{{/with}}', + array('a' => 'good', 'b' => 'stump'), + 'good', + ), array( + '{{#with foo}}{{#if goodbye}}GOODBYE cruel {{../world}}!{{/if}}{{/with}}', + array('foo' => array('goodbye' => true), 'world' => 'world'), + 'GOODBYE cruel world!', + )); + } + /** - * Test if and unless adding an extra layer when accessing parent + * Test if and unless do not add an extra layer when accessing parent + * + * @dataProvider ifUnlessDepthDoesntChangeProvider */ - public function testIfUnlessExtraLayer() + public function testIfUnlessDepthDoesntChange($template, $data, $expected) { $loader = new \Handlebars\Loader\StringLoader(); $engine = new \Handlebars\Handlebars(array('loader' => $loader)); - $this->assertEquals('good', $engine->render('{{#with b}}{{#if this}}{{../../a}}{{/if}}{{/with}}', array('a' => 'good', 'b' => 'stump'))); - $this->assertEquals('good', $engine->render('{{#with b}}{{#unless false}}{{../../a}}{{/unless}}{{/with}}', array('a' => 'good', 'b' => 'stump'))); + $this->assertEquals($expected, $engine->render($template, $data)); } /** @@ -1255,6 +1527,12 @@ EOM; } +class TestClassWithToStringMethod { + public function __toString() { + return 'test'; + } +} + /** * Testcase for testInlineLoader * @@ -1267,4 +1545,4 @@ This is a inline template. a b c -d \ No newline at end of file +d