├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── README.markdown ├── composer.json ├── phpunit.xml.dist ├── src └── Handlebars │ ├── Arguments.php │ ├── Autoloader.php │ ├── BaseString.php │ ├── Cache.php │ ├── Cache │ ├── APC.php │ ├── Disk.php │ └── Dummy.php │ ├── ChildContext.php │ ├── Context.php │ ├── Handlebars.php │ ├── Helper.php │ ├── Helper │ ├── BindAttrHelper.php │ ├── EachHelper.php │ ├── IfHelper.php │ ├── UnlessHelper.php │ └── WithHelper.php │ ├── Helpers.php │ ├── Loader.php │ ├── Loader │ ├── ArrayLoader.php │ ├── FilesystemLoader.php │ ├── InlineLoader.php │ └── StringLoader.php │ ├── Parser.php │ ├── SafeString.php │ ├── String.php │ ├── StringWrapper.php │ ├── Template.php │ └── Tokenizer.php └── tests ├── Xamin ├── Cache │ ├── APCTest.php │ └── DiskTest.php └── HandlebarsTest.php ├── bootstrap.php └── fixture ├── Handlebars ├── CustomTemplate.php ├── Example │ └── Test.php └── Test.php ├── another ├── __another.hb └── another.handlebars └── data ├── __loader.hb └── loader.handlebars /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | *.swp 3 | *.swo 4 | composer.phar 5 | composer.lock 6 | *.iml 7 | .idea 8 | GPATH 9 | GRTAGS 10 | GTAGS 11 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | inherit: true 2 | filter: 3 | paths: [src/*] 4 | excluded_paths: [tests/*] 5 | tools: 6 | php_code_coverage: true 7 | build: 8 | tests: 9 | override: 10 | - 11 | command: 'phpunit --coverage-clover=coverage.xml' 12 | coverage: 13 | file: 'coverage.xml' 14 | format: 'php-clover' -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.3 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - 7.0 8 | - hhvm 9 | branches: 10 | except: 11 | - php-52 12 | before_script: composer self-update && composer install 13 | script: "./vendor/bin/phpunit && ./vendor/bin/phpcs -n src/" 14 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Handlebars.php 2 | ============== 3 | [![Build Status](https://travis-ci.org/XaminProject/handlebars.php.png?branch=master)](https://travis-ci.org/XaminProject/handlebars.php) 4 | [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/XaminProject/handlebars.php/badges/quality-score.png?s=23a379f19b523498926eb3f2b60195815632e8ef)](https://scrutinizer-ci.com/g/XaminProject/handlebars.php/) 5 | [![Code Coverage](https://scrutinizer-ci.com/g/XaminProject/handlebars.php/badges/coverage.png?s=9d6acd80ef2bda03cbd00a0cff35535614ce79ed)](https://scrutinizer-ci.com/g/XaminProject/handlebars.php/) 6 | installation 7 | ------------ 8 | 9 | add the following to require property of your composer.json file: 10 | 11 | `"xamin/handlebars.php": "dev-master"` for php 5.3+ 12 | `"xamin/handlebars.php": "dev-php-52"` for php 5.2 13 | 14 | then run: 15 | 16 | `$ composer install` 17 | 18 | usage 19 | ----- 20 | 21 | ```php 22 | render( 33 | 'Planets:
{{#each planets}}
{{this}}
{{/each}}', 34 | array( 35 | 'planets' => array( 36 | "Mercury", 37 | "Venus", 38 | "Earth", 39 | "Mars" 40 | ) 41 | ) 42 | ); 43 | ``` 44 | 45 | ```php 46 | new \Handlebars\Loader\FilesystemLoader(__DIR__.'/templates/'), 52 | 'partials_loader' => new \Handlebars\Loader\FilesystemLoader( 53 | __DIR__ . '/templates/', 54 | array( 55 | 'prefix' => '_' 56 | ) 57 | ) 58 | )); 59 | 60 | /* templates/main.handlebars: 61 | 62 | {{> partial planets}} 63 | 64 | */ 65 | 66 | /* templates/_partial.handlebars: 67 | 68 | {{#each this}} 69 | {{this}} 70 | {{/each}} 71 | 72 | */ 73 | 74 | echo $engine->render( 75 | 'main', 76 | array( 77 | 'planets' => array( 78 | "Mercury", 79 | "Venus", 80 | "Earth", 81 | "Mars" 82 | ) 83 | ) 84 | ); 85 | ``` 86 | 87 | contribution 88 | ------------ 89 | 90 | contributions are more than welcome, just don't forget to: 91 | 92 | * add your name to each file that you edit as author 93 | * use PHP CodeSniffer to check coding style. 94 | 95 | license 96 | ------- 97 | 98 | Copyright (c) 2010 Justin Hileman 99 | Copyright (C) 2012-2013 Xamin Project and contributors 100 | 101 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 102 | 103 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 104 | 105 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 106 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xamin/handlebars.php", 3 | "description": "Handlebars processor for php", 4 | "homepage": "https://github.com/XaminProject/handlebars.php", 5 | "type": "library", 6 | "minimum-stability": "dev", 7 | "prefer-stable": true, 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "fzerorubigd", 12 | "email": "fzerorubigd@gmail.com" 13 | }, 14 | { 15 | "name": "Behrooz Shabani (everplays)", 16 | "email": "everplays@gmail.com" 17 | } 18 | ], 19 | "require": { 20 | 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "~4.4", 24 | "squizlabs/php_codesniffer": "~1.5" 25 | }, 26 | "autoload": { 27 | "psr-0": { 28 | "Handlebars": "src/" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tests/Xamin/ 7 | 8 | 9 | 10 | 11 | 12 | src/ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Handlebars/Arguments.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2014 Authors 11 | * @license MIT 12 | * @version GIT: $Id$ 13 | * @link http://xamin.ir 14 | */ 15 | 16 | namespace Handlebars; 17 | 18 | /** 19 | * Encapsulates helpers arguments. 20 | * 21 | * @category Xamin 22 | * @package Handlebars 23 | * @author Dmitriy Simushev 24 | * @copyright 2014 Authors 25 | * @license MIT 26 | * @version Release: @package_version@ 27 | * @link http://xamin.ir 28 | */ 29 | class Arguments 30 | { 31 | /** 32 | * List of named arguments. 33 | * 34 | * @var array 35 | */ 36 | protected $namedArgs = array(); 37 | 38 | /** 39 | * List of positional arguments. 40 | * 41 | * @var array 42 | */ 43 | protected $positionalArgs = array(); 44 | 45 | /** 46 | * The original arguments string that was used to fill in arguments. 47 | * 48 | * @var string 49 | */ 50 | protected $originalString = ''; 51 | 52 | /** 53 | * Class constructor. 54 | * 55 | * @param string $args_string Arguments string as passed to a helper. 56 | */ 57 | public function __construct($args_string = false) 58 | { 59 | $this->originalString = (string)$args_string; 60 | 61 | if ($this->originalString !== '') { 62 | $this->parse($args_string); 63 | } 64 | } 65 | 66 | /** 67 | * Returns string representation of the arguments list. 68 | * 69 | * This method is here mostly for backward compatibility reasons. 70 | * 71 | * @return string 72 | */ 73 | public function __toString() 74 | { 75 | return $this->originalString; 76 | } 77 | 78 | /** 79 | * Returns a list of named arguments. 80 | * 81 | * @return array 82 | */ 83 | public function getNamedArguments() 84 | { 85 | return $this->namedArgs; 86 | } 87 | 88 | /** 89 | * Returns a list of positional arguments. 90 | * 91 | * @return array 92 | */ 93 | public function getPositionalArguments() 94 | { 95 | return $this->positionalArgs; 96 | } 97 | 98 | /** 99 | * Breaks an argument string into arguments and stores them inside the 100 | * object. 101 | * 102 | * @param string $args_string Arguments string as passed to a helper. 103 | * 104 | * @return void 105 | * @throws \InvalidArgumentException 106 | */ 107 | protected function parse($args_string) 108 | { 109 | $bad_chars = preg_quote(Context::NOT_VALID_NAME_CHARS, '#'); 110 | $bad_seg_chars = preg_quote(Context::NOT_VALID_SEGMENT_NAME_CHARS, '#'); 111 | 112 | $name_chunk = '(?:[^' . $bad_chars . '\s]+)|(?:\[[^' . $bad_seg_chars . ']+\])'; 113 | $variable_name = '(?:\.\.\/)*(?:(?:' . $name_chunk . ')[\.\/])*(?:' . $name_chunk . ')\.?'; 114 | $special_variable_name = '@[a-z]+'; 115 | $escaped_value = '(?:(?prepareArgumentName($matches[1]); 128 | $value = $this->prepareArgumentValue($matches[2]); 129 | 130 | $this->namedArgs[$name] = $value; 131 | 132 | // Remove found argument from arguments string. 133 | $current_str = ltrim(substr($current_str, strlen($matches[0]))); 134 | } elseif (preg_match($positional_argument, $current_str, $matches)) { 135 | // A positional argument found. It cannot follow named arguments 136 | if (count($this->namedArgs) !== 0) { 137 | throw new \InvalidArgumentException('Positional arguments cannot follow named arguments'); 138 | } 139 | 140 | $this->positionalArgs[] = $this->prepareArgumentValue($matches[1]); 141 | 142 | // Remove found argument from arguments string. 143 | $current_str = ltrim(substr($current_str, strlen($matches[0]))); 144 | } else { 145 | throw new \InvalidArgumentException( 146 | sprintf( 147 | 'Malformed arguments string: "%s"', 148 | $args_string 149 | ) 150 | ); 151 | } 152 | } 153 | } 154 | 155 | /** 156 | * Prepares argument's value to add to arguments list. 157 | * 158 | * The method unescapes value and wrap it into \Handlebars\StringWrapper 159 | * class if needed. 160 | * 161 | * @param string $value Argument's value 162 | * 163 | * @return string|\Handlebars\StringWrapper 164 | */ 165 | protected function prepareArgumentValue($value) 166 | { 167 | // Check if argument's value is a quoted string literal 168 | if ($value[0] == "'" || $value[0] == '"') { 169 | // Remove enclosing quotes and unescape 170 | return new StringWrapper(stripcslashes(substr($value, 1, -1))); 171 | } 172 | 173 | // Check if the value is an integer literal 174 | if (preg_match("/^-?\d+$/", $value)) { 175 | // Wrap the value into the String class to tell the Context that 176 | // it's a value and not a variable name. 177 | return new StringWrapper($value); 178 | } 179 | 180 | return $value; 181 | } 182 | 183 | /** 184 | * Prepares argument's name. 185 | * 186 | * Remove sections braces if needed. 187 | * 188 | * @param strign $name Argument's name 189 | * 190 | * @return string 191 | */ 192 | protected function prepareArgumentName($name) 193 | { 194 | // Check if argument's name is a segment 195 | if ($name[0] == '[') { 196 | $name = substr($name, 1, -1); 197 | } 198 | 199 | return $name; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Handlebars/Autoloader.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Behrooz Shabani 14 | * @copyright 2010-2012 (c) Justin Hileman 15 | * @copyright 2012 (c) ParsPooyesh Co 16 | * @copyright 2013 (c) Behrooz Shabani 17 | * @license MIT 18 | * @version GIT: $Id$ 19 | * @link http://xamin.ir 20 | */ 21 | 22 | namespace Handlebars; 23 | 24 | /** 25 | * Autloader for handlebars.php 26 | * 27 | * @category Xamin 28 | * @package Handlebars 29 | * @author fzerorubigd 30 | * @copyright 2010-2012 (c) Justin Hileman 31 | * @copyright 2012 (c) ParsPooyesh Co 32 | * @license MIT 33 | * @version Release: @package_version@ 34 | * @link http://xamin.ir 35 | */ 36 | 37 | class Autoloader 38 | { 39 | 40 | private $_baseDir; 41 | 42 | /** 43 | * Autoloader constructor. 44 | * 45 | * @param string $baseDir Handlebars library base directory default is 46 | * __DIR__.'/..' 47 | */ 48 | protected function __construct($baseDir = null) 49 | { 50 | if ($baseDir === null) { 51 | $this->_baseDir = realpath(__DIR__ . '/..'); 52 | } else { 53 | $this->_baseDir = rtrim($baseDir, '/'); 54 | } 55 | } 56 | 57 | /** 58 | * Register a new instance as an SPL autoloader. 59 | * 60 | * @param string $baseDir Handlebars library base directory, default is 61 | * __DIR__.'/..' 62 | * 63 | * @return \Handlebars\Autoloader Registered Autoloader instance 64 | */ 65 | public static function register($baseDir = null) 66 | { 67 | $loader = new self($baseDir); 68 | spl_autoload_register(array($loader, 'autoload')); 69 | 70 | return $loader; 71 | } 72 | 73 | /** 74 | * Autoload Handlebars classes. 75 | * 76 | * @param string $class class to load 77 | * 78 | * @return void 79 | */ 80 | public function autoload($class) 81 | { 82 | if ($class[0] !== '\\') { 83 | $class = '\\' . $class; 84 | } 85 | 86 | if (strpos($class, 'Handlebars') !== 1) { 87 | return; 88 | } 89 | 90 | $file = sprintf( 91 | '%s%s.php', 92 | $this->_baseDir, 93 | str_replace('\\', '/', $class) 94 | ); 95 | 96 | if (is_file($file)) { 97 | include $file; 98 | } 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/Handlebars/BaseString.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Behrooz Shabani 11 | * @author Dmitriy Simushev 12 | * @copyright 2013 Authors 13 | * @license MIT 14 | * @version GIT: $Id$ 15 | * @link http://xamin.ir 16 | */ 17 | 18 | namespace Handlebars; 19 | 20 | /** 21 | * Handlebars base string 22 | * 23 | * @category Xamin 24 | * @package Handlebars 25 | * @author fzerorubigd 26 | * @copyright 2013 Authors 27 | * @license MIT 28 | * @version Release: @package_version@ 29 | * @link http://xamin.ir 30 | */ 31 | 32 | class BaseString 33 | { 34 | private $_string; 35 | 36 | /** 37 | * Create new string 38 | * 39 | * @param string $string input source 40 | */ 41 | public function __construct($string) 42 | { 43 | $this->_string = $string; 44 | } 45 | 46 | /** 47 | * To String 48 | * 49 | * @return string 50 | */ 51 | public function __toString() 52 | { 53 | return $this->getString(); 54 | } 55 | 56 | /** 57 | * Get string 58 | * 59 | * @return string 60 | */ 61 | public function getString() 62 | { 63 | return $this->_string; 64 | } 65 | 66 | /** 67 | * Create new string 68 | * 69 | * @param string $string input source 70 | * 71 | * @return void 72 | */ 73 | public function setString($string) 74 | { 75 | $this->_string = $string; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/Handlebars/Cache.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Behrooz Shabani 12 | * @author Mária Šormanová 13 | * @copyright 2012 (c) ParsPooyesh Co 14 | * @copyright 2013 (c) Behrooz Shabani 15 | * @license MIT 16 | * @version GIT: $Id$ 17 | * @link http://xamin.ir 18 | */ 19 | 20 | namespace Handlebars; 21 | 22 | /** 23 | * Cache interface 24 | * Base cache interface, Note that Handlebars.php never call for remove. 25 | * Driver should take care of expiered cache. 26 | * 27 | * @category Xamin 28 | * @package Handlebars 29 | * @author fzerorubigd 30 | * @copyright 2010-2012 (c) Justin Hileman 31 | * @copyright 2012 (c) ParsPooyesh Co 32 | * @license MIT 33 | * @version Release: @package_version@ 34 | * @link http://xamin.ir 35 | */ 36 | 37 | interface Cache 38 | { 39 | 40 | /** 41 | * Get cache for $name if exist. 42 | * 43 | * @param string $name Cache id 44 | * 45 | * @return mixed data on hit, boolean false on cache not found 46 | */ 47 | public function get($name); 48 | 49 | /** 50 | * Set a cache with $ttl, if present 51 | * If $ttl set to -1, the cache expires immediately 52 | * If $ttl set to 0 (default), cache is never purged 53 | * 54 | * @param string $name cache id 55 | * @param mixed $value data to store 56 | * @param int $ttl time to live in seconds 57 | * 58 | * @return void 59 | */ 60 | public function set($name, $value, $ttl = 0); 61 | 62 | /** 63 | * Remove cache 64 | * 65 | * @param string $name Cache id 66 | * 67 | * @return void 68 | */ 69 | public function remove($name); 70 | 71 | } -------------------------------------------------------------------------------- /src/Handlebars/Cache/APC.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Behrooz Shabani 12 | * @author Mária Šormanová 13 | * @copyright 2013 (c) Meraki, LLP 14 | * @copyright 2013 (c) Behrooz Shabani 15 | * @license MIT 16 | * @version GIT: $Id$ 17 | * @link http://xamin.ir 18 | */ 19 | 20 | namespace Handlebars\Cache; 21 | use Handlebars\Cache; 22 | 23 | /** 24 | * A APC cache 25 | * 26 | * @category Xamin 27 | * @package Handlebars 28 | * @author Joey Baker 29 | * @copyright 2012 (c) Meraki, LLP 30 | * @license MIT 31 | * @version Release: @package_version@ 32 | * @link http://xamin.ir 33 | */ 34 | 35 | class APC implements Cache 36 | { 37 | 38 | /** 39 | * @var string 40 | */ 41 | private $_prefix; 42 | 43 | /** 44 | * Construct the APC cache. 45 | * 46 | * @param string|null $prefix optional key prefix, defaults to null 47 | */ 48 | public function __construct( $prefix = null ) 49 | { 50 | $this->_prefix = (string)$prefix; 51 | } 52 | 53 | /** 54 | * Get cache for $name if exist 55 | * and if the cache is not older than defined TTL. 56 | * 57 | * @param string $name Cache id 58 | * 59 | * @return mixed data on hit, boolean false on cache not found/expired 60 | */ 61 | public function get($name) 62 | { 63 | $success = null; 64 | $result = apc_fetch($this->_getKey($name), $success); 65 | 66 | return $success ? $result : false; 67 | 68 | } 69 | 70 | /** 71 | * Set a cache with $ttl, if present 72 | * If $ttl set to -1, the cache expires immediately 73 | * If $ttl set to 0 (default), cache is never purged 74 | * 75 | * @param string $name cache id 76 | * @param mixed $value data to store 77 | * @param int $ttl time to live in seconds 78 | * 79 | * @return void 80 | */ 81 | public function set($name, $value, $ttl = 0) 82 | { 83 | apc_store($this->_getKey($name), $value, $ttl); 84 | } 85 | 86 | /** 87 | * Remove cache 88 | * 89 | * @param string $name Cache id 90 | * 91 | * @return void 92 | */ 93 | public function remove($name) 94 | { 95 | apc_delete($this->_getKey($name)); 96 | } 97 | 98 | /** 99 | * Gets the full cache key for a given cache item's 100 | * 101 | * @param string $name Name of the cache item 102 | * 103 | * @return string full cache key of cached item 104 | */ 105 | private function _getKey($name) 106 | { 107 | return $this->_prefix . ':' . $name; 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/Handlebars/Cache/Disk.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Behrooz Shabani 12 | * @author Mária Šormanová 13 | * @copyright 2013 (c) Brokerloop, Inc. 14 | * @copyright 2013 (c) Behrooz Shabani 15 | * @license MIT 16 | * @version GIT: $Id$ 17 | * @link http://xamin.ir 18 | */ 19 | 20 | namespace Handlebars\Cache; 21 | use Handlebars\Cache; 22 | 23 | /** 24 | * A flat-file filesystem cache. 25 | * 26 | * @category Xamin 27 | * @package Handlebars 28 | * @author Alex Soncodi 29 | * @author Mária Šormanová 30 | * @copyright 2013 (c) Brokerloop, Inc. 31 | * @license MIT 32 | * @version Release: @package_version@ 33 | * @link http://xamin.ir 34 | */ 35 | 36 | class Disk implements Cache 37 | { 38 | 39 | private $_path = ''; 40 | private $_prefix = ''; 41 | private $_suffix = ''; 42 | 43 | /** 44 | * Construct the disk cache. 45 | * 46 | * @param string $path Filesystem path to the disk cache location 47 | * @param string $prefix optional file prefix, defaults to empty string 48 | * @param string $suffix optional file extension, defaults to empty string 49 | * 50 | * @throws \RuntimeException 51 | * @throws \InvalidArgumentException 52 | */ 53 | public function __construct($path, $prefix = '', $suffix = '') 54 | { 55 | if (empty($path)) { 56 | throw new \InvalidArgumentException('Must specify disk cache path'); 57 | } elseif (!is_dir($path)) { 58 | @mkdir($path, 0777, true); 59 | 60 | if (!is_dir($path)) { 61 | throw new \RuntimeException('Could not create cache file path'); 62 | } 63 | } 64 | 65 | $this->_path = $path; 66 | $this->_prefix = $prefix; 67 | $this->_suffix = $suffix; 68 | } 69 | 70 | /** 71 | * Gets the full disk path for a given cache item's file, 72 | * taking into account the cache path, optional prefix, 73 | * and optional extension. 74 | * 75 | * @param string $name Name of the cache item 76 | * 77 | * @return string full disk path of cached item 78 | */ 79 | private function _getPath($name) 80 | { 81 | return $this->_path . DIRECTORY_SEPARATOR . 82 | $this->_prefix . $name . $this->_suffix; 83 | } 84 | 85 | /** 86 | * Get cache for $name if it exists 87 | * and if the cache is not older than defined TTL. 88 | * 89 | * @param string $name Cache id 90 | * 91 | * @return mixed data on hit, boolean false on cache not found/expired 92 | */ 93 | public function get($name) 94 | { 95 | $path = $this->_getPath($name); 96 | $output = false; 97 | if (file_exists($path)) { 98 | $file = fopen($path, "r"); 99 | $ttl = fgets($file); 100 | $ctime = filectime($path); 101 | $time = time(); 102 | if ($ttl == -1 || ($ttl > 0 && $time - $ctime > $ttl)) { 103 | unlink($path); 104 | } else { 105 | $serialized_data = fread($file, filesize($path)); 106 | $output = unserialize($serialized_data); 107 | } 108 | fclose($file); 109 | } 110 | return $output; 111 | } 112 | 113 | /** 114 | * Set a cache with $ttl, if present 115 | * If $ttl set to -1, the cache expires immediately 116 | * If $ttl set to 0 (default), cache is never purged 117 | * 118 | * @param string $name cache id 119 | * @param mixed $value data to store 120 | * @param int $ttl time to live in seconds 121 | * 122 | * @return void 123 | */ 124 | public function set($name, $value, $ttl = 0) 125 | { 126 | $path = $this->_getPath($name); 127 | 128 | file_put_contents($path, $ttl.PHP_EOL.serialize($value)); 129 | } 130 | 131 | /** 132 | * Remove cache 133 | * 134 | * @param string $name Cache id 135 | * 136 | * @return void 137 | */ 138 | public function remove($name) 139 | { 140 | $path = $this->_getPath($name); 141 | 142 | unlink($path); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/Handlebars/Cache/Dummy.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Behrooz Shabani 12 | * @author Mária Šormanová 13 | * @copyright 2012 (c) ParsPooyesh Co 14 | * @copyright 2013 (c) Behrooz Shabani 15 | * @license MIT 16 | * @version GIT: $Id$ 17 | * @link http://xamin.ir 18 | */ 19 | 20 | namespace Handlebars\Cache; 21 | use Handlebars\Cache; 22 | 23 | /** 24 | * A dummy array cache 25 | * 26 | * @category Xamin 27 | * @package Handlebars 28 | * @author fzerorubigd 29 | * @copyright 2012 (c) ParsPooyesh Co 30 | * @license MIT 31 | * @version Release: @package_version@ 32 | * @link http://xamin.ir 33 | */ 34 | 35 | class Dummy implements Cache 36 | { 37 | private $_cache = array(); 38 | 39 | /** 40 | * Get cache for $name if exist. 41 | * 42 | * @param string $name Cache id 43 | * 44 | * @return mixed data on hit, boolean false on cache not found 45 | */ 46 | public function get($name) 47 | { 48 | if (array_key_exists($name, $this->_cache)) { 49 | return $this->_cache[$name]; 50 | } 51 | return false; 52 | } 53 | 54 | /** 55 | * Set a cache 56 | * 57 | * @param string $name cache id 58 | * @param mixed $value data to store 59 | * @param int $ttl time to live in seconds 60 | * 61 | * $ttl is ignored since the cache is implemented 62 | * by an array and lives only inside one request 63 | * 64 | * @return void 65 | */ 66 | public function set($name, $value, $ttl = 0) 67 | { 68 | $this->_cache[$name] = $value; 69 | } 70 | 71 | /** 72 | * Remove cache 73 | * 74 | * @param string $name Cache id 75 | * 76 | * @return void 77 | */ 78 | public function remove($name) 79 | { 80 | unset($this->_cache[$name]); 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /src/Handlebars/ChildContext.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Behrooz Shabani 12 | * @author Chris Gray 13 | * @author Ulrik Lystbaek 14 | * @author Dmitriy Simushev 15 | * @author Christian Blanquera 16 | * @copyright 2010-2012 (c) Justin Hileman 17 | * @copyright 2012 (c) ParsPooyesh Co 18 | * @copyright 2013 (c) Behrooz Shabani 19 | * @copyright 2013 (c) f0ruD A 20 | * @license MIT 21 | * @version GIT: $Id$ 22 | * @link http://xamin.ir 23 | */ 24 | 25 | namespace Handlebars; 26 | 27 | /** 28 | * Handlebars context 29 | * Context for a template 30 | * 31 | * @category Xamin 32 | * @package Handlebars 33 | * @author fzerorubigd 34 | * @author Behrooz Shabani 35 | * @copyright 2010-2012 (c) Justin Hileman 36 | * @copyright 2012 (c) ParsPooyesh Co 37 | * @license MIT 38 | * @version Release: @package_version@ 39 | * @link http://xamin.ir 40 | */ 41 | 42 | class ChildContext extends Context 43 | { 44 | protected $parentContext = null; 45 | 46 | /** 47 | * Sets a parent context in which 48 | * we will case for the ../ in get() 49 | * 50 | * @param Context $parent parent context 51 | * 52 | * @return void 53 | */ 54 | public function setParent(Context $parent) 55 | { 56 | $this->parentContext = $parent; 57 | } 58 | 59 | /** 60 | * Get a available from current context 61 | * Supported types : 62 | * variable , ../variable , variable.variable , variable.[variable] , . 63 | * 64 | * @param string $variableName variable name to get from current context 65 | * @param boolean $strict strict search? if not found then throw exception 66 | * 67 | * @throws \InvalidArgumentException in strict mode and variable not found 68 | * @throws \RuntimeException if supplied argument is a malformed quoted string 69 | * @throws \InvalidArgumentException if variable name is invalid 70 | * @return mixed 71 | */ 72 | public function get($variableName, $strict = false) 73 | { 74 | //if the variable name starts with a ../ 75 | //and we have a parent 76 | if (strpos($variableName, '../') === 0 77 | && $this->parentContext instanceof Context 78 | ) { 79 | //just remove the first ../ 80 | $variableName = substr($variableName, 3); 81 | 82 | //and let the parent context handle the rest 83 | return $this->parentContext->get($variableName, $strict); 84 | } 85 | 86 | //otherwise, it's business as usual 87 | return parent::get($variableName, $strict); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Handlebars/Context.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Behrooz Shabani 12 | * @author Chris Gray 13 | * @author Ulrik Lystbaek 14 | * @author Dmitriy Simushev 15 | * @copyright 2010-2012 (c) Justin Hileman 16 | * @copyright 2012 (c) ParsPooyesh Co 17 | * @copyright 2013 (c) Behrooz Shabani 18 | * @copyright 2013 (c) f0ruD A 19 | * @license MIT 20 | * @version GIT: $Id$ 21 | * @link http://xamin.ir 22 | */ 23 | 24 | namespace Handlebars; 25 | 26 | /** 27 | * Handlebars context 28 | * Context for a template 29 | * 30 | * @category Xamin 31 | * @package Handlebars 32 | * @author fzerorubigd 33 | * @author Behrooz Shabani 34 | * @copyright 2010-2012 (c) Justin Hileman 35 | * @copyright 2012 (c) ParsPooyesh Co 36 | * @license MIT 37 | * @version Release: @package_version@ 38 | * @link http://xamin.ir 39 | */ 40 | 41 | class Context 42 | { 43 | /** 44 | * List of charcters that cannot be used in identifiers. 45 | */ 46 | const NOT_VALID_NAME_CHARS = '!"#%&\'()*+,./;<=>@[\\]^`{|}~'; 47 | 48 | /** 49 | * List of characters that cannot be used in identifiers in segment-literal 50 | * notation. 51 | */ 52 | const NOT_VALID_SEGMENT_NAME_CHARS = "]"; 53 | 54 | /** 55 | * Context stack 56 | * 57 | * @var array stack for context only top stack is available 58 | */ 59 | protected $stack = array(); 60 | 61 | /** 62 | * Section stack index 63 | * 64 | * @var array index stack for sections 65 | */ 66 | protected $index = array(); 67 | 68 | /** 69 | * Object stack keys 70 | * 71 | * @var array key stack for objects 72 | */ 73 | protected $key = array(); 74 | 75 | /** 76 | * Special variables stack for sections. 77 | * 78 | * @var array Each stack element can 79 | * contain elements with "@index", "@key", "@first" and "@last" keys. 80 | */ 81 | protected $specialVariables = array(); 82 | 83 | /** 84 | * Mustache rendering Context constructor. 85 | * 86 | * @param mixed $context Default rendering context (default: null) 87 | */ 88 | public function __construct($context = null) 89 | { 90 | if ($context !== null) { 91 | $this->stack = array($context); 92 | } 93 | } 94 | 95 | /** 96 | * Push a new Context frame onto the stack. 97 | * 98 | * @param mixed $value Object or array to use for context 99 | * 100 | * @return void 101 | */ 102 | public function push($value) 103 | { 104 | array_push($this->stack, $value); 105 | } 106 | 107 | /** 108 | * Push an array of special variables to stack. 109 | * 110 | * @param array $variables An associative array of special variables. 111 | * 112 | * @return void 113 | * 114 | * @see \Handlebars\Context::$specialVariables 115 | */ 116 | public function pushSpecialVariables($variables) 117 | { 118 | array_push($this->specialVariables, $variables); 119 | } 120 | 121 | /** 122 | * Pop the last Context frame from the stack. 123 | * 124 | * @return mixed Last Context frame (object or array) 125 | */ 126 | public function pop() 127 | { 128 | return array_pop($this->stack); 129 | } 130 | 131 | /** 132 | * Pop the last special variables set from the stack. 133 | * 134 | * @return array Associative array of special variables. 135 | * 136 | * @see \Handlebars\Context::$specialVariables 137 | */ 138 | public function popSpecialVariables() 139 | { 140 | return array_pop($this->specialVariables); 141 | } 142 | 143 | /** 144 | * Get the last Context frame. 145 | * 146 | * @return mixed Last Context frame (object or array) 147 | */ 148 | public function last() 149 | { 150 | return end($this->stack); 151 | } 152 | 153 | /** 154 | * Get the last special variables set from the stack. 155 | * 156 | * @return array Associative array of special variables. 157 | * 158 | * @see \Handlebars\Context::$specialVariables 159 | */ 160 | public function lastSpecialVariables() 161 | { 162 | return end($this->specialVariables); 163 | } 164 | 165 | /** 166 | * Change the current context to one of current context members 167 | * 168 | * @param string $variableName name of variable or a callable on current context 169 | * 170 | * @return mixed actual value 171 | */ 172 | public function with($variableName) 173 | { 174 | $value = $this->get($variableName); 175 | $this->push($value); 176 | 177 | return $value; 178 | } 179 | 180 | /** 181 | * Get a available from current context 182 | * Supported types : 183 | * variable , ../variable , variable.variable , variable.[variable] , . 184 | * 185 | * @param string $variableName variable name to get from current context 186 | * @param boolean $strict strict search? if not found then throw exception 187 | * 188 | * @throws \InvalidArgumentException in strict mode and variable not found 189 | * @throws \RuntimeException if supplied argument is a malformed quoted string 190 | * @throws \InvalidArgumentException if variable name is invalid 191 | * @return mixed 192 | */ 193 | public function get($variableName, $strict = false) 194 | { 195 | if ($variableName instanceof \Handlebars\StringWrapper) { 196 | return (string)$variableName; 197 | } 198 | $variableName = trim($variableName); 199 | $level = 0; 200 | while (substr($variableName, 0, 3) == '../') { 201 | $variableName = trim(substr($variableName, 3)); 202 | $level++; 203 | } 204 | if (count($this->stack) < $level) { 205 | if ($strict) { 206 | throw new \InvalidArgumentException( 207 | sprintf( 208 | 'Can not find variable in context: "%s"', 209 | $variableName 210 | ) 211 | ); 212 | } 213 | 214 | return ''; 215 | } 216 | if (substr($variableName, 0, 6) == '@root.') { 217 | $variableName = trim(substr($variableName, 6)); 218 | $level = count($this->stack)-1; 219 | } 220 | end($this->stack); 221 | while ($level) { 222 | prev($this->stack); 223 | $level--; 224 | } 225 | $current = current($this->stack); 226 | if (!$variableName) { 227 | if ($strict) { 228 | throw new \InvalidArgumentException( 229 | sprintf( 230 | 'Can not find variable in context: "%s"', 231 | $variableName 232 | ) 233 | ); 234 | } 235 | 236 | return ''; 237 | } elseif ($variableName == '.' || $variableName == 'this') { 238 | return $current; 239 | } elseif ($variableName[0] == '@') { 240 | $specialVariables = $this->lastSpecialVariables(); 241 | if (isset($specialVariables[$variableName])) { 242 | return $specialVariables[$variableName]; 243 | } elseif ($strict) { 244 | throw new \InvalidArgumentException( 245 | sprintf( 246 | 'Can not find variable in context: "%s"', 247 | $variableName 248 | ) 249 | ); 250 | } else { 251 | return ''; 252 | } 253 | } else { 254 | $chunks = $this->_splitVariableName($variableName); 255 | do { 256 | $current = current($this->stack); 257 | foreach ($chunks as $chunk) { 258 | if (is_string($current) and $current == '') { 259 | return $current; 260 | } 261 | $current = $this->_findVariableInContext($current, $chunk, $strict); 262 | } 263 | prev($this->stack); 264 | 265 | } while ($current === null && current($this->stack) !== false); 266 | } 267 | return $current; 268 | } 269 | 270 | /** 271 | * Check if $variable->$inside is available 272 | * 273 | * @param mixed $variable variable to check 274 | * @param string $inside property/method to check 275 | * @param boolean $strict strict search? if not found then throw exception 276 | * 277 | * @throws \InvalidArgumentException in strict mode and variable not found 278 | * @return boolean true if exist 279 | */ 280 | private function _findVariableInContext($variable, $inside, $strict = false) 281 | { 282 | $value = null; 283 | if (($inside !== '0' && empty($inside)) || ($inside == 'this')) { 284 | return $variable; 285 | } elseif (is_array($variable)) { 286 | if (isset($variable[$inside]) || array_key_exists($inside, $variable)) { 287 | return $variable[$inside]; 288 | } elseif ($inside == "length") { 289 | return count($variable); 290 | } 291 | } elseif (is_object($variable)) { 292 | if (isset($variable->$inside)) { 293 | return $variable->$inside; 294 | } elseif (is_callable(array($variable, $inside))) { 295 | return call_user_func(array($variable, $inside)); 296 | } 297 | } 298 | 299 | if ($strict) { 300 | throw new \InvalidArgumentException( 301 | sprintf( 302 | 'Can not find variable in context: "%s"', 303 | $inside 304 | ) 305 | ); 306 | } 307 | 308 | return $value; 309 | } 310 | 311 | /** 312 | * Splits variable name to chunks. 313 | * 314 | * @param string $variableName Fully qualified name of a variable. 315 | * 316 | * @throws \InvalidArgumentException if variable name is invalid. 317 | * @return array 318 | */ 319 | private function _splitVariableName($variableName) 320 | { 321 | $bad_chars = preg_quote(self::NOT_VALID_NAME_CHARS, '/'); 322 | $bad_seg_chars = preg_quote(self::NOT_VALID_SEGMENT_NAME_CHARS, '/'); 323 | 324 | $name_pattern = "(?:[^" 325 | . $bad_chars 326 | . "\s]+)|(?:\[[^" 327 | . $bad_seg_chars 328 | . "]+\])"; 329 | 330 | $check_pattern = "/^((" 331 | . $name_pattern 332 | . ")\.)*(" 333 | . $name_pattern 334 | . ")\.?$/"; 335 | 336 | $get_pattern = "/(?:" . $name_pattern . ")/"; 337 | 338 | if (!preg_match($check_pattern, $variableName)) { 339 | throw new \InvalidArgumentException( 340 | sprintf( 341 | 'Variable name is invalid: "%s"', 342 | $variableName 343 | ) 344 | ); 345 | } 346 | 347 | preg_match_all($get_pattern, $variableName, $matches); 348 | 349 | $chunks = array(); 350 | foreach ($matches[0] as $chunk) { 351 | // Remove wrapper braces if needed 352 | if ($chunk[0] == '[') { 353 | $chunk = substr($chunk, 1, -1); 354 | } 355 | $chunks[] = $chunk; 356 | } 357 | 358 | return $chunks; 359 | } 360 | 361 | } 362 | -------------------------------------------------------------------------------- /src/Handlebars/Handlebars.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Behrooz Shabani 12 | * @author Jeff Turcotte 13 | * @author Mária Šormanová 14 | * @copyright 2010-2012 (c) Justin Hileman 15 | * @copyright 2012 (c) ParsPooyesh Co 16 | * @copyright 2013 (c) Behrooz Shabani 17 | * @license MIT 18 | * @version GIT: $Id$ 19 | * @link http://xamin.ir 20 | */ 21 | 22 | namespace Handlebars; 23 | use Handlebars\Loader\StringLoader; 24 | use Handlebars\Cache\Dummy; 25 | 26 | /** 27 | * Handlebars template engine, based on mustache. 28 | * 29 | * @category Xamin 30 | * @package Handlebars 31 | * @author fzerorubigd 32 | * @copyright 2012 (c) ParsPooyesh Co 33 | * @license MIT 34 | * @version Release: @package_version@ 35 | * @link http://xamin.ir 36 | */ 37 | 38 | class Handlebars 39 | { 40 | private static $_instance = false; 41 | const VERSION = '1.1.0'; 42 | 43 | /** 44 | * Factory method 45 | * 46 | * @param array $options see __construct's options parameter 47 | * 48 | * @return Handlebars 49 | */ 50 | public static function factory($options = array()) 51 | { 52 | if (self::$_instance === false) { 53 | self::$_instance = new Handlebars($options); 54 | } 55 | 56 | return self::$_instance; 57 | } 58 | 59 | /** 60 | * Current tokenizer instance 61 | * 62 | * @var Tokenizer 63 | */ 64 | private $_tokenizer; 65 | 66 | /** 67 | * Current parser instance 68 | * 69 | * @var Parser 70 | */ 71 | private $_parser; 72 | 73 | /** 74 | * Current helper list 75 | * 76 | * @var Helpers 77 | */ 78 | private $_helpers; 79 | 80 | /** 81 | * Current loader instance 82 | * 83 | * @var Loader 84 | */ 85 | private $_loader; 86 | 87 | /** 88 | * Current partial loader instance 89 | * 90 | * @var Loader 91 | */ 92 | private $_partialsLoader; 93 | 94 | /** 95 | * Current cache instance 96 | * 97 | * @var Cache 98 | */ 99 | private $_cache; 100 | 101 | /** 102 | * @var int time to live parameter in seconds for the cache usage 103 | * default set to 0 which means that entries stay in cache 104 | * forever and are never purged 105 | */ 106 | private $_ttl = 0; 107 | 108 | /** 109 | * @var string the class to use for the template 110 | */ 111 | private $_templateClass = 'Handlebars\\Template'; 112 | 113 | /** 114 | * @var callable escape function to use 115 | */ 116 | private $_escape = 'htmlspecialchars'; 117 | 118 | /** 119 | * Parameters for the escpae method above 120 | * 121 | * @var array parametes to pass to escape function 122 | */ 123 | private $_escapeArgs = array( 124 | ENT_COMPAT, 125 | 'UTF-8' 126 | ); 127 | 128 | private $_aliases = array(); 129 | 130 | /** 131 | * Handlebars engine constructor 132 | * $options array can contain : 133 | * helpers => Helpers object 134 | * escape => a callable function to escape values 135 | * escapeArgs => array to pass as extra parameter to escape function 136 | * loader => Loader object 137 | * partials_loader => Loader object 138 | * cache => Cache object 139 | * template_class => the class to use for the template object 140 | * 141 | * @param array $options array of options to set 142 | * 143 | * @throws \InvalidArgumentException 144 | */ 145 | public function __construct(array $options = array()) 146 | { 147 | if (isset($options['helpers'])) { 148 | $this->setHelpers($options['helpers']); 149 | } 150 | 151 | if (isset($options['loader'])) { 152 | $this->setLoader($options['loader']); 153 | } 154 | 155 | if (isset($options['partials_loader'])) { 156 | $this->setPartialsLoader($options['partials_loader']); 157 | } 158 | 159 | if (isset($options['cache'])) { 160 | $this->setCache($options['cache']); 161 | } 162 | 163 | if (isset($options['ttl'])) { 164 | $this->setTtl($options['ttl']); 165 | } 166 | 167 | if (isset($options['template_class'])) { 168 | $this->setTemplateClass($options['template_class']); 169 | } 170 | 171 | if (isset($options['escape'])) { 172 | if (!is_callable($options['escape'])) { 173 | throw new \InvalidArgumentException( 174 | 'Handlebars Constructor "escape" option must be callable' 175 | ); 176 | } 177 | 178 | $this->_escape = $options['escape']; 179 | } 180 | 181 | if (isset($options['escapeArgs'])) { 182 | if (!is_array($options['escapeArgs'])) { 183 | $options['escapeArgs'] = array($options['escapeArgs']); 184 | } 185 | $this->_escapeArgs = $options['escapeArgs']; 186 | } 187 | 188 | if (isset($options['partials_alias']) 189 | && is_array($options['partials_alias']) 190 | ) { 191 | $this->_aliases = $options['partials_alias']; 192 | } 193 | } 194 | 195 | 196 | /** 197 | * Shortcut 'render' invocation. 198 | * 199 | * Equivalent to calling `$handlebars->loadTemplate($template)->render($data);` 200 | * 201 | * @param string $template template name 202 | * @param mixed $data data to use as context 203 | * 204 | * @return string Rendered template 205 | * @see Handlebars::loadTemplate 206 | * @see Template::render 207 | */ 208 | public function render($template, $data) 209 | { 210 | return $this->loadTemplate($template)->render($data); 211 | } 212 | 213 | /** 214 | * Set helpers for current enfine 215 | * 216 | * @param Helpers $helpers handlebars helper 217 | * 218 | * @return void 219 | */ 220 | public function setHelpers(Helpers $helpers) 221 | { 222 | $this->_helpers = $helpers; 223 | } 224 | 225 | /** 226 | * Get helpers, or create new one if ther is no helper 227 | * 228 | * @return Helpers 229 | */ 230 | public function getHelpers() 231 | { 232 | if (!isset($this->_helpers)) { 233 | $this->_helpers = new Helpers(); 234 | } 235 | 236 | return $this->_helpers; 237 | } 238 | 239 | /** 240 | * Add a new helper. 241 | * 242 | * @param string $name helper name 243 | * @param mixed $helper helper callable 244 | * 245 | * @return void 246 | */ 247 | public function addHelper($name, $helper) 248 | { 249 | $this->getHelpers()->add($name, $helper); 250 | } 251 | 252 | /** 253 | * Get a helper by name. 254 | * 255 | * @param string $name helper name 256 | * 257 | * @return callable Helper 258 | */ 259 | public function getHelper($name) 260 | { 261 | return $this->getHelpers()->__get($name); 262 | } 263 | 264 | /** 265 | * Check whether this instance has a helper. 266 | * 267 | * @param string $name helper name 268 | * 269 | * @return boolean True if the helper is present 270 | */ 271 | public function hasHelper($name) 272 | { 273 | return $this->getHelpers()->has($name); 274 | } 275 | 276 | /** 277 | * Add a new helper. 278 | * 279 | * @param string $name helper name 280 | * @param mixed $helper helper callable 281 | * 282 | * @return void 283 | */ 284 | public function registerHelper($name, $helper) 285 | { 286 | $callback = function ($template, $context, $arg) use ($helper) { 287 | $args = $template->parseArguments($arg); 288 | $named = $template->parseNamedArguments($arg); 289 | 290 | foreach ($args as $i => $arg) { 291 | //if it's literally string 292 | if ($arg instanceof BaseString) { 293 | //we have no problems here 294 | $args[$i] = (string) $arg; 295 | continue; 296 | } 297 | 298 | //not sure what to do if it's not a string or StringWrapper 299 | if (!is_string($arg)) { 300 | continue; 301 | } 302 | 303 | //it's a variable and we need to figure out the value of it 304 | $args[$i] = $context->get($arg); 305 | } 306 | 307 | //push the options 308 | $args[] = array( 309 | //special fields 310 | 'data' => array( 311 | 'index' => $context->get('@index'), 312 | 'key' => $context->get('@key'), 313 | 'first' => $context->get('@first'), 314 | 'last' => $context->get('@last')), 315 | // Named arguments 316 | 'hash' => $named, 317 | // A renderer for block helper 318 | 'fn' => function ($inContext = null) use ($context, $template) { 319 | $defined = !!$inContext; 320 | 321 | if (!$defined) { 322 | $inContext = $context; 323 | $inContext->push($inContext->last()); 324 | } else if (!$inContext instanceof Context) { 325 | $inContext = new ChildContext($inContext); 326 | $inContext->setParent($context); 327 | } 328 | 329 | $template->setStopToken('else'); 330 | $buffer = $template->render($inContext); 331 | $template->setStopToken(false); 332 | //what if it's a loop ? 333 | $template->rewind(); 334 | //What's the point of this again? 335 | //I mean in this context (literally) 336 | //$template->discard($inContext); 337 | 338 | if (!$defined) { 339 | $inContext->pop(); 340 | } 341 | 342 | return $buffer; 343 | }, 344 | 345 | // A render for the else block 346 | 'inverse' => function ($inContext = null) use ($context, $template) { 347 | $defined = !!$inContext; 348 | 349 | if (!$defined) { 350 | $inContext = $context; 351 | $inContext->push($inContext->last()); 352 | } else if (!$inContext instanceof Context) { 353 | $inContext = new ChildContext($inContext); 354 | $inContext->setParent($context); 355 | } 356 | 357 | $template->setStopToken('else'); 358 | $template->discard($inContext); 359 | $template->setStopToken(false); 360 | $buffer = $template->render($inContext); 361 | 362 | if (!$defined) { 363 | $inContext->pop(); 364 | } 365 | 366 | return $buffer; 367 | }, 368 | 369 | // The current context. 370 | 'context' => $context, 371 | // The current template 372 | 'template' => $template); 373 | 374 | return call_user_func_array($helper, $args); 375 | }; 376 | 377 | $this->addHelper($name, $callback); 378 | } 379 | 380 | /** 381 | * Remove a helper by name. 382 | * 383 | * @param string $name helper name 384 | * 385 | * @return void 386 | */ 387 | public function removeHelper($name) 388 | { 389 | $this->getHelpers()->remove($name); 390 | } 391 | 392 | /** 393 | * Set current loader 394 | * 395 | * @param Loader $loader handlebars loader 396 | * 397 | * @return void 398 | */ 399 | public function setLoader(Loader $loader) 400 | { 401 | $this->_loader = $loader; 402 | } 403 | 404 | /** 405 | * Get current loader 406 | * 407 | * @return Loader 408 | */ 409 | public function getLoader() 410 | { 411 | if (!isset($this->_loader)) { 412 | $this->_loader = new StringLoader(); 413 | } 414 | 415 | return $this->_loader; 416 | } 417 | 418 | /** 419 | * Set current partials loader 420 | * 421 | * @param Loader $loader handlebars loader 422 | * 423 | * @return void 424 | */ 425 | public function setPartialsLoader(Loader $loader) 426 | { 427 | $this->_partialsLoader = $loader; 428 | } 429 | 430 | /** 431 | * Get current partials loader 432 | * 433 | * @return Loader 434 | */ 435 | public function getPartialsLoader() 436 | { 437 | if (!isset($this->_partialsLoader)) { 438 | $this->_partialsLoader = new StringLoader(); 439 | } 440 | 441 | return $this->_partialsLoader; 442 | } 443 | 444 | /** 445 | * Set cache for current engine 446 | * 447 | * @param Cache $cache handlebars cache 448 | * 449 | * @return void 450 | */ 451 | public function setCache(Cache $cache) 452 | { 453 | $this->_cache = $cache; 454 | } 455 | 456 | /** 457 | * Get cache 458 | * 459 | * @return Cache 460 | */ 461 | public function getCache() 462 | { 463 | if (!isset($this->_cache)) { 464 | $this->_cache = new Dummy(); 465 | } 466 | 467 | return $this->_cache; 468 | } 469 | 470 | /** 471 | * Set time to live for the used cache 472 | * 473 | * @param int $ttl time to live in seconds 474 | * 475 | * @return void 476 | */ 477 | public function setTtl($ttl) 478 | { 479 | $this->_ttl = $ttl; 480 | } 481 | 482 | /** 483 | * Get ttl 484 | * 485 | * @return int 486 | */ 487 | public function getTtl() 488 | { 489 | return $this->_ttl; 490 | } 491 | 492 | /** 493 | * Get current escape function 494 | * 495 | * @return callable 496 | */ 497 | public function getEscape() 498 | { 499 | return $this->_escape; 500 | } 501 | 502 | /** 503 | * Set current escape function 504 | * 505 | * @param callable $escape function 506 | * 507 | * @throws \InvalidArgumentException 508 | * @return void 509 | */ 510 | public function setEscape($escape) 511 | { 512 | if (!is_callable($escape)) { 513 | throw new \InvalidArgumentException( 514 | 'Escape function must be a callable' 515 | ); 516 | } 517 | $this->_escape = $escape; 518 | } 519 | 520 | /** 521 | * Get current escape function 522 | * 523 | * @return array 524 | */ 525 | public function getEscapeArgs() 526 | { 527 | return $this->_escapeArgs; 528 | } 529 | 530 | /** 531 | * Set current escape function 532 | * 533 | * @param array $escapeArgs arguments to pass as extra arg to function 534 | * 535 | * @return void 536 | */ 537 | public function setEscapeArgs($escapeArgs) 538 | { 539 | if (!is_array($escapeArgs)) { 540 | $escapeArgs = array($escapeArgs); 541 | } 542 | $this->_escapeArgs = $escapeArgs; 543 | } 544 | 545 | 546 | /** 547 | * Set the Handlebars Tokenizer instance. 548 | * 549 | * @param Tokenizer $tokenizer tokenizer 550 | * 551 | * @return void 552 | */ 553 | public function setTokenizer(Tokenizer $tokenizer) 554 | { 555 | $this->_tokenizer = $tokenizer; 556 | } 557 | 558 | /** 559 | * Get the current Handlebars Tokenizer instance. 560 | * 561 | * If no Tokenizer instance has been explicitly specified, this method will 562 | * instantiate and return a new one. 563 | * 564 | * @return Tokenizer 565 | */ 566 | public function getTokenizer() 567 | { 568 | if (!isset($this->_tokenizer)) { 569 | $this->_tokenizer = new Tokenizer(); 570 | } 571 | 572 | return $this->_tokenizer; 573 | } 574 | 575 | /** 576 | * Set the Handlebars Parser instance. 577 | * 578 | * @param Parser $parser parser object 579 | * 580 | * @return void 581 | */ 582 | public function setParser(Parser $parser) 583 | { 584 | $this->_parser = $parser; 585 | } 586 | 587 | /** 588 | * Get the current Handlebars Parser instance. 589 | * 590 | * If no Parser instance has been explicitly specified, this method will 591 | * instantiate and return a new one. 592 | * 593 | * @return Parser 594 | */ 595 | public function getParser() 596 | { 597 | if (!isset($this->_parser)) { 598 | $this->_parser = new Parser(); 599 | } 600 | 601 | return $this->_parser; 602 | } 603 | 604 | /** 605 | * Sets the class to use for the template object 606 | * 607 | * @param string $class the class name 608 | * 609 | * @return void 610 | */ 611 | public function setTemplateClass($class) 612 | { 613 | if (!is_a($class, 'Handlebars\\Template', true)) { 614 | throw new \InvalidArgumentException( 615 | sprintf( 616 | 'Custom template class "%s" must extend Template', 617 | $class 618 | ) 619 | ); 620 | } 621 | 622 | $this->_templateClass = $class; 623 | } 624 | 625 | /** 626 | * Load a template by name with current template loader 627 | * 628 | * @param string $name template name 629 | * 630 | * @return Template 631 | */ 632 | public function loadTemplate($name) 633 | { 634 | $source = $this->getLoader()->load($name); 635 | $tree = $this->_tokenize($source); 636 | 637 | return new $this->_templateClass($this, $tree, $source); 638 | } 639 | 640 | /** 641 | * Load a partial by name with current partial loader 642 | * 643 | * @param string $name partial name 644 | * 645 | * @return Template 646 | */ 647 | public function loadPartial($name) 648 | { 649 | if (isset($this->_aliases[$name])) { 650 | $name = $this->_aliases[$name]; 651 | } 652 | $source = $this->getPartialsLoader()->load($name); 653 | $tree = $this->_tokenize($source); 654 | 655 | return new $this->_templateClass($this, $tree, $source); 656 | } 657 | 658 | /** 659 | * Register partial alias 660 | * 661 | * @param string $alias Partial alias 662 | * @param string $content The real value 663 | * 664 | * @return void 665 | */ 666 | public function registerPartial($alias, $content) 667 | { 668 | $this->_aliases[$alias] = $content; 669 | } 670 | 671 | /** 672 | * Un-register partial alias 673 | * 674 | * @param string $alias Partial alias 675 | * 676 | * @return void 677 | */ 678 | public function unRegisterPartial($alias) 679 | { 680 | if (isset($this->_aliases[$alias])) { 681 | unset($this->_aliases[$alias]); 682 | } 683 | } 684 | 685 | /** 686 | * Load string into a template object 687 | * 688 | * @param string $source string to load 689 | * 690 | * @return Template 691 | */ 692 | public function loadString($source) 693 | { 694 | $tree = $this->_tokenize($source); 695 | 696 | return new $this->_templateClass($this, $tree, $source); 697 | } 698 | 699 | /** 700 | * Try to tokenize source, or get them from cache if available 701 | * 702 | * @param string $source handlebars source code 703 | * 704 | * @return array handlebars parsed data into array 705 | */ 706 | private function _tokenize($source) 707 | { 708 | $hash = md5(sprintf('version: %s, data : %s', self::VERSION, $source)); 709 | $tree = $this->getCache()->get($hash); 710 | if ($tree === false) { 711 | $tokens = $this->getTokenizer()->scan($source); 712 | $tree = $this->getParser()->parse($tokens); 713 | $this->getCache()->set($hash, $tree, $this->_ttl); 714 | } 715 | 716 | return $tree; 717 | } 718 | 719 | } 720 | -------------------------------------------------------------------------------- /src/Handlebars/Helper.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2014 Authors 11 | * @license MIT 12 | * @version GIT: $Id$ 13 | * @link http://xamin.ir 14 | */ 15 | 16 | namespace Handlebars; 17 | 18 | /** 19 | * Handlebars helper interface 20 | * 21 | * @category Xamin 22 | * @package Handlebars 23 | * @author Jeff Turcotte 24 | * @copyright 2014 Authors 25 | * @license MIT 26 | * @version Release: @package_version@ 27 | * @link http://xamin.ir 28 | */ 29 | interface Helper 30 | { 31 | /** 32 | * Execute the helper 33 | * 34 | * @param \Handlebars\Template $template The template instance 35 | * @param \Handlebars\Context $context The current context 36 | * @param \Handlebars\Arguments $args The arguments passed the the helper 37 | * @param string $source The source 38 | * 39 | * @return mixed 40 | */ 41 | public function execute(Template $template, Context $context, $args, $source); 42 | } 43 | -------------------------------------------------------------------------------- /src/Handlebars/Helper/BindAttrHelper.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Behrooz Shabani 11 | * @author Dmitriy Simushev 12 | * @author Jeff Turcotte 13 | * @copyright 2014 Authors 14 | * @license MIT 15 | * @version GIT: $Id$ 16 | * @link http://xamin.ir 17 | */ 18 | 19 | namespace Handlebars\Helper; 20 | 21 | use Handlebars\Context; 22 | use Handlebars\Helper; 23 | use Handlebars\Template; 24 | 25 | /** 26 | * The bindAttr Helper 27 | * 28 | * @category Xamin 29 | * @package Handlebars 30 | * @author fzerorubigd 31 | * @author Behrooz Shabani 32 | * @author Dmitriy Simushev 33 | * @author Jeff Turcotte 34 | * @copyright 2014 Authors 35 | * @license MIT 36 | * @version Release: @package_version@ 37 | * @link http://xamin.ir 38 | */ 39 | class BindAttrHelper implements Helper 40 | { 41 | /** 42 | * Execute the helper 43 | * 44 | * @param \Handlebars\Template $template The template instance 45 | * @param \Handlebars\Context $context The current context 46 | * @param \Handlebars\Arguments $args The arguments passed the the helper 47 | * @param string $source The source 48 | * 49 | * @return mixed 50 | */ 51 | public function execute(Template $template, Context $context, $args, $source) 52 | { 53 | return $args; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Handlebars/Helper/EachHelper.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Behrooz Shabani 11 | * @author Dmitriy Simushev 12 | * @author Jeff Turcotte 13 | * @author John Slegers 14 | * @copyright 2014 Authors 15 | * @license MIT 16 | * @version GIT: $Id$ 17 | * @link http://xamin.ir 18 | */ 19 | 20 | namespace Handlebars\Helper; 21 | 22 | use Handlebars\Context; 23 | use Handlebars\Helper; 24 | use Handlebars\Template; 25 | 26 | /** 27 | * The Each Helper 28 | * 29 | * @category Xamin 30 | * @package Handlebars 31 | * @author fzerorubigd 32 | * @author Behrooz Shabani 33 | * @author Dmitriy Simushev 34 | * @author Jeff Turcotte 35 | * @author John Slegers 36 | * @copyright 2014 Authors 37 | * @license MIT 38 | * @version Release: @package_version@ 39 | * @link http://xamin.ir 40 | */ 41 | class EachHelper implements Helper 42 | { 43 | /** 44 | * Execute the helper 45 | * 46 | * @param \Handlebars\Template $template The template instance 47 | * @param \Handlebars\Context $context The current context 48 | * @param \Handlebars\Arguments $args The arguments passed the the helper 49 | * @param string $source The source 50 | * 51 | * @return mixed 52 | */ 53 | public function execute(Template $template, Context $context, $args, $source) 54 | { 55 | $positionalArgs = $args->getPositionalArguments(); 56 | $tmp = $context->get($positionalArgs[0]); 57 | $buffer = ''; 58 | 59 | if (!$tmp) { 60 | $template->setStopToken('else'); 61 | $template->discard(); 62 | $template->setStopToken(false); 63 | $buffer = $template->render($context); 64 | } elseif (is_array($tmp) || $tmp instanceof \Traversable) { 65 | $isList = is_array($tmp) && (array_keys($tmp) === range(0, count($tmp) - 1)); 66 | $index = 0; 67 | $lastIndex = $isList ? (count($tmp) - 1) : false; 68 | 69 | foreach ($tmp as $key => $var) { 70 | $specialVariables = array( 71 | '@index' => $index, 72 | '@first' => ($index === 0), 73 | '@last' => ($index === $lastIndex), 74 | ); 75 | if (!$isList) { 76 | $specialVariables['@key'] = $key; 77 | } 78 | $context->pushSpecialVariables($specialVariables); 79 | $context->push($var); 80 | $template->setStopToken('else'); 81 | $template->rewind(); 82 | $buffer .= $template->render($context); 83 | $context->pop(); 84 | $context->popSpecialVariables(); 85 | $index++; 86 | } 87 | 88 | $template->setStopToken(false); 89 | } 90 | 91 | return $buffer; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Handlebars/Helper/IfHelper.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Behrooz Shabani 11 | * @author Dmitriy Simushev 12 | * @author Jeff Turcotte 13 | * @copyright 2014 Authors 14 | * @license MIT 15 | * @version GIT: $Id$ 16 | * @link http://xamin.ir 17 | */ 18 | 19 | namespace Handlebars\Helper; 20 | 21 | use Handlebars\Context; 22 | use Handlebars\Helper; 23 | use Handlebars\Template; 24 | 25 | /** 26 | * Handlebars halper interface 27 | * 28 | * @category Xamin 29 | * @package Handlebars 30 | * @author fzerorubigd 31 | * @author Behrooz Shabani 32 | * @author Dmitriy Simushev 33 | * @author Jeff Turcotte 34 | * @copyright 2014 Authors 35 | * @license MIT 36 | * @version Release: @package_version@ 37 | * @link http://xamin.ir 38 | */ 39 | class IfHelper implements Helper 40 | { 41 | /** 42 | * Execute the helper 43 | * 44 | * @param \Handlebars\Template $template The template instance 45 | * @param \Handlebars\Context $context The current context 46 | * @param \Handlebars\Arguments $args The arguments passed the the helper 47 | * @param string $source The source 48 | * 49 | * @return mixed 50 | */ 51 | public function execute(Template $template, Context $context, $args, $source) 52 | { 53 | $parsedArgs = $template->parseArguments($args); 54 | $tmp = $context->get($parsedArgs[0]); 55 | 56 | if ($tmp) { 57 | $template->setStopToken('else'); 58 | $buffer = $template->render($context); 59 | $template->setStopToken(false); 60 | $template->discard($context); 61 | } else { 62 | $template->setStopToken('else'); 63 | $template->discard($context); 64 | $template->setStopToken(false); 65 | $buffer = $template->render($context); 66 | } 67 | 68 | return $buffer; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Handlebars/Helper/UnlessHelper.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Behrooz Shabani 11 | * @author Dmitriy Simushev 12 | * @author Jeff Turcotte 13 | * @copyright 2014 Authors 14 | * @license MIT 15 | * @version GIT: $Id$ 16 | * @link http://xamin.ir 17 | */ 18 | 19 | namespace Handlebars\Helper; 20 | 21 | use Handlebars\Context; 22 | use Handlebars\Helper; 23 | use Handlebars\Template; 24 | 25 | /** 26 | * The Unless Helper 27 | * 28 | * @category Xamin 29 | * @package Handlebars 30 | * @author fzerorubigd 31 | * @author Behrooz Shabani 32 | * @author Dmitriy Simushev 33 | * @author Jeff Turcotte 34 | * @copyright 2014 Authors 35 | * @license MIT 36 | * @version Release: @package_version@ 37 | * @link http://xamin.ir 38 | */ 39 | class UnlessHelper implements Helper 40 | { 41 | /** 42 | * Execute the helper 43 | * 44 | * @param \Handlebars\Template $template The template instance 45 | * @param \Handlebars\Context $context The current context 46 | * @param \Handlebars\Arguments $args The arguments passed the the helper 47 | * @param string $source The source 48 | * 49 | * @return mixed 50 | */ 51 | public function execute(Template $template, Context $context, $args, $source) 52 | { 53 | $parsedArgs = $template->parseArguments($args); 54 | $tmp = $context->get($parsedArgs[0]); 55 | 56 | if (!$tmp) { 57 | $template->setStopToken('else'); 58 | $buffer = $template->render($context); 59 | $template->setStopToken(false); 60 | } else { 61 | $template->setStopToken('else'); 62 | $template->discard(); 63 | $template->setStopToken(false); 64 | $buffer = $template->render($context); 65 | } 66 | 67 | return $buffer; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Handlebars/Helper/WithHelper.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Behrooz Shabani 11 | * @author Dmitriy Simushev 12 | * @author Jeff Turcotte 13 | * @copyright 2014 Authors 14 | * @license MIT 15 | * @version GIT: $Id$ 16 | * @link http://xamin.ir 17 | */ 18 | 19 | namespace Handlebars\Helper; 20 | 21 | use Handlebars\Context; 22 | use Handlebars\Helper; 23 | use Handlebars\Template; 24 | 25 | /** 26 | * The With Helper 27 | * 28 | * @category Xamin 29 | * @package Handlebars 30 | * @author fzerorubigd 31 | * @author Behrooz Shabani 32 | * @author Dmitriy Simushev 33 | * @author Jeff Turcotte 34 | * @copyright 2014 Authors 35 | * @license MIT 36 | * @version Release: @package_version@ 37 | * @link http://xamin.ir 38 | */ 39 | class WithHelper implements Helper 40 | { 41 | /** 42 | * Execute the helper 43 | * 44 | * @param \Handlebars\Template $template The template instance 45 | * @param \Handlebars\Context $context The current context 46 | * @param \Handlebars\Arguments $args The arguments passed the the helper 47 | * @param string $source The source 48 | * 49 | * @return mixed 50 | */ 51 | public function execute(Template $template, Context $context, $args, $source) 52 | { 53 | $positionalArgs = $args->getPositionalArguments(); 54 | $context->with($positionalArgs[0]); 55 | $buffer = $template->render($context); 56 | $context->pop(); 57 | 58 | return $buffer; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Handlebars/Helpers.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Behrooz Shabani 12 | * @author Dmitriy Simushev 13 | * @author Jeff Turcotte 14 | * @copyright 2012 (c) ParsPooyesh Co 15 | * @copyright 2013 (c) Behrooz Shabani 16 | * @license MIT 17 | * @version GIT: $Id$ 18 | * @link http://xamin.ir 19 | */ 20 | 21 | namespace Handlebars; 22 | 23 | /** 24 | * Handlebars helpers 25 | * 26 | * A collection of helper function. normally a function like 27 | * function ($sender, $name, $arguments) $arguments is unscaped arguments and 28 | * is a string, not array 29 | * 30 | * @category Xamin 31 | * @package Handlebars 32 | * @author fzerorubigd 33 | * @copyright 2012 (c) ParsPooyesh Co 34 | * @license MIT 35 | * @version Release: @package_version@ 36 | * @link http://xamin.ir 37 | */ 38 | class Helpers 39 | { 40 | /** 41 | * Raw helper array 42 | * 43 | * @var array array of helpers 44 | */ 45 | protected $helpers = array(); 46 | 47 | /** 48 | * Create new helper container class 49 | * 50 | * @param array $helpers array of name=>$value helpers 51 | * @param array|bool $defaults add defaults helper 52 | * (if, unless, each,with, bindAttr) 53 | * 54 | * @throws \InvalidArgumentException when $helpers is not an array 55 | * (or traversable) or helper is not a callable 56 | */ 57 | public function __construct($helpers = null, $defaults = true) 58 | { 59 | if ($defaults) { 60 | $this->addDefaultHelpers(); 61 | } 62 | if ($helpers != null) { 63 | if (!is_array($helpers) && !$helpers instanceof \Traversable) { 64 | throw new \InvalidArgumentException( 65 | 'HelperCollection constructor expects an array of helpers' 66 | ); 67 | } 68 | foreach ($helpers as $name => $helper) { 69 | $this->add($name, $helper); 70 | } 71 | } 72 | } 73 | 74 | 75 | /** 76 | * Add default helpers (if unless each with bindAttr) 77 | * 78 | * @return void 79 | */ 80 | protected function addDefaultHelpers() 81 | { 82 | $this->add('if', new Helper\IfHelper()); 83 | $this->add('each', new Helper\EachHelper()); 84 | $this->add('unless', new Helper\UnlessHelper()); 85 | $this->add('with', new Helper\WithHelper()); 86 | 87 | //Just for compatibility with ember 88 | $this->add('bindAttr', new Helper\BindAttrHelper()); 89 | } 90 | 91 | /** 92 | * Add a new helper to helpers 93 | * 94 | * @param string $name helper name 95 | * @param mixed $helper a callable or Helper implementation as a helper 96 | * 97 | * @throws \InvalidArgumentException if $helper is not a callable 98 | * @return void 99 | */ 100 | public function add($name, $helper) 101 | { 102 | if (!is_callable($helper) && ! $helper instanceof Helper) { 103 | throw new \InvalidArgumentException( 104 | sprintf( 105 | "%s Helper is not a callable or doesn't implement the Helper interface.", 106 | $name 107 | ) 108 | ); 109 | } 110 | $this->helpers[$name] = $helper; 111 | } 112 | 113 | /** 114 | * Add all helpers from the specified collection to the current one. 115 | * 116 | * The method will override helpers from the current collections with same 117 | * named helpers from the specified collection. 118 | * 119 | * @param Helpers $helpers A collection which helpers should be added. 120 | * 121 | * @return void 122 | */ 123 | public function addHelpers(Helpers $helpers) 124 | { 125 | $this->helpers = $helpers->getAll() + $this->helpers; 126 | } 127 | 128 | /** 129 | * Calls a helper, whether it be a Closure or Helper instance 130 | * 131 | * @param string $name The name of the helper 132 | * @param \Handlebars\Template $template The template instance 133 | * @param \Handlebars\Context $context The current context 134 | * @param array $args The arguments passed the the helper 135 | * @param string $source The source 136 | * 137 | * @throws \InvalidArgumentException 138 | * @return mixed The helper return value 139 | */ 140 | public function call($name, Template $template, Context $context, $args, $source) 141 | { 142 | if (!$this->has($name)) { 143 | throw new \InvalidArgumentException( 144 | sprintf( 145 | 'Unknown helper: "%s"', 146 | $name 147 | ) 148 | ); 149 | } 150 | 151 | $parsedArgs = new Arguments($args); 152 | if ($this->helpers[$name] instanceof Helper) { 153 | return $this->helpers[$name]->execute( 154 | $template, $context, $parsedArgs, $source 155 | ); 156 | } 157 | 158 | return call_user_func( 159 | $this->helpers[$name], 160 | $template, 161 | $context, 162 | $parsedArgs, 163 | $source 164 | ); 165 | } 166 | 167 | /** 168 | * Check if $name helper is available 169 | * 170 | * @param string $name helper name 171 | * 172 | * @return boolean 173 | */ 174 | public function has($name) 175 | { 176 | return array_key_exists($name, $this->helpers); 177 | } 178 | 179 | /** 180 | * Get a helper. __magic__ method :) 181 | * 182 | * @param string $name helper name 183 | * 184 | * @throws \InvalidArgumentException if $name is not available 185 | * @return callable helper function 186 | */ 187 | public function __get($name) 188 | { 189 | if (!$this->has($name)) { 190 | throw new \InvalidArgumentException( 191 | sprintf( 192 | 'Unknown helper: "%s"', 193 | $name 194 | ) 195 | ); 196 | } 197 | 198 | return $this->helpers[$name]; 199 | } 200 | 201 | /** 202 | * Check if $name helper is available __magic__ method :) 203 | * 204 | * @param string $name helper name 205 | * 206 | * @return boolean 207 | * @see Handlebras_Helpers::has 208 | */ 209 | public function __isset($name) 210 | { 211 | return $this->has($name); 212 | } 213 | 214 | /** 215 | * Add a new helper to helpers __magic__ method :) 216 | * 217 | * @param string $name helper name 218 | * @param callable $helper a function as a helper 219 | * 220 | * @return void 221 | */ 222 | public function __set($name, $helper) 223 | { 224 | $this->add($name, $helper); 225 | } 226 | 227 | /** 228 | * Unset a helper 229 | * 230 | * @param string $name helper name to remove 231 | * 232 | * @return void 233 | */ 234 | public function __unset($name) 235 | { 236 | $this->remove($name); 237 | } 238 | 239 | /** 240 | * Check whether a given helper is present in the collection. 241 | * 242 | * @param string $name helper name 243 | * 244 | * @throws \InvalidArgumentException if the requested helper is not present. 245 | * @return void 246 | */ 247 | public function remove($name) 248 | { 249 | if (!$this->has($name)) { 250 | throw new \InvalidArgumentException( 251 | sprintf( 252 | 'Unknown helper: "%s"', 253 | $name 254 | ) 255 | ); 256 | } 257 | 258 | unset($this->helpers[$name]); 259 | } 260 | 261 | /** 262 | * Clear the helper collection. 263 | * 264 | * Removes all helpers from this collection 265 | * 266 | * @return void 267 | */ 268 | public function clear() 269 | { 270 | $this->helpers = array(); 271 | } 272 | 273 | /** 274 | * Check whether the helper collection is empty. 275 | * 276 | * @return boolean True if the collection is empty 277 | */ 278 | public function isEmpty() 279 | { 280 | return empty($this->helpers); 281 | } 282 | 283 | /** 284 | * Returns all helpers from the collection. 285 | * 286 | * @return array Associative array of helpers which keys are helpers names 287 | * and the values are the helpers. 288 | */ 289 | public function getAll() 290 | { 291 | return $this->helpers; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/Handlebars/Loader.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Behrooz Shabani 12 | * @copyright 2010-2012 (c) Justin Hileman 13 | * @copyright 2012 (c) ParsPooyesh Co 14 | * @copyright 2013 (c) Behrooz Shabani 15 | * @license MIT 16 | * @version GIT: $Id$ 17 | * @link http://xamin.ir 18 | */ 19 | 20 | namespace Handlebars; 21 | 22 | /** 23 | * Handlebars loader interface 24 | * 25 | * @category Xamin 26 | * @package Handlebars 27 | * @author fzerorubigd 28 | * @copyright 2010-2012 (c) Justin Hileman 29 | * @copyright 2012 (c) ParsPooyesh Co 30 | * @license MIT 31 | * @version Release: @package_version@ 32 | * @link http://xamin.ir 33 | */ 34 | 35 | interface Loader 36 | { 37 | 38 | /** 39 | * Load a Template by name. 40 | * 41 | * @param string $name template name to load 42 | * 43 | * @return String 44 | */ 45 | public function load($name); 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Handlebars/Loader/ArrayLoader.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright 2014 (c) f0ruD 12 | * @license MIT 13 | * @version GIT: $Id$ 14 | * @link http://xamin.ir 15 | */ 16 | 17 | namespace Handlebars\Loader; 18 | 19 | use Handlebars\Loader; 20 | 21 | /** 22 | * Handlebars Template array Loader implementation. 23 | * 24 | * @category Xamin 25 | * @package Handlebars 26 | * @author fzerorubigd 27 | * @copyright 2014 (c) f0ruD 28 | * @license MIT 29 | * @version Release: @package_version@ 30 | * @link http://xamin.ir * 31 | */ 32 | class ArrayLoader implements Loader 33 | { 34 | private $_templates; 35 | 36 | /** 37 | * Create a new loader with associative array style 38 | * 39 | * @param array $templates the templates to load 40 | */ 41 | public function __construct(array $templates = array()) 42 | { 43 | $this->_templates = $templates; 44 | } 45 | 46 | /** 47 | * Add a template to list 48 | * 49 | * @param string $name template name 50 | * @param string $template the template 51 | * 52 | * @return void 53 | */ 54 | public function addTemplate($name, $template) 55 | { 56 | $this->_templates[$name] = $template; 57 | } 58 | 59 | /** 60 | * Load a Template by name. 61 | * 62 | * @param string $name template name to load 63 | * 64 | * @throws \RuntimeException 65 | * @return String 66 | */ 67 | public function load($name) 68 | { 69 | if (isset($this->_templates[$name])) { 70 | return $this->_templates[$name]; 71 | } 72 | throw new \RuntimeException( 73 | "Can not find the $name template" 74 | ); 75 | } 76 | } -------------------------------------------------------------------------------- /src/Handlebars/Loader/FilesystemLoader.php: -------------------------------------------------------------------------------- 1 | 12 | * @author Behrooz Shabani 13 | * @author Craig Bass 14 | * @author ^^ 15 | * @author Dave Stein 16 | * @copyright 2010-2012 (c) Justin Hileman 17 | * @copyright 2012 (c) ParsPooyesh Co 18 | * @copyright 2013 (c) Behrooz Shabani 19 | * @license MIT 20 | * @version GIT: $Id$ 21 | * @link http://xamin.ir 22 | */ 23 | 24 | namespace Handlebars\Loader; 25 | 26 | use Handlebars\Loader; 27 | use Handlebars\StringWrapper; 28 | 29 | /** 30 | * Handlebars Template filesystem Loader implementation. 31 | * 32 | * @category Xamin 33 | * @package Handlebars 34 | * @author fzerorubigd 35 | * @copyright 2010-2012 (c) Justin Hileman 36 | * @copyright 2012 (c) ParsPooyesh Co 37 | * @license MIT 38 | * @version Release: @package_version@ 39 | * @link http://xamin.ir * 40 | */ 41 | 42 | class FilesystemLoader implements Loader 43 | { 44 | protected $baseDir; 45 | private $_extension = '.handlebars'; 46 | private $_prefix = ''; 47 | private $_templates = array(); 48 | 49 | /** 50 | * Handlebars filesystem Loader constructor. 51 | * 52 | * $options array allows overriding certain Loader options during instantiation: 53 | * 54 | * $options = array( 55 | * // extension used for Handlebars templates. Defaults to '.handlebars' 56 | * 'extension' => '.other', 57 | * ); 58 | * 59 | * @param string|array $baseDirs A path contain template files or array of paths 60 | * @param array $options Array of Loader options (default: array()) 61 | * 62 | * @throws \RuntimeException if $baseDir does not exist. 63 | */ 64 | public function __construct($baseDirs, array $options = array()) 65 | { 66 | $this->setBaseDir($baseDirs); 67 | $this->handleOptions($options); 68 | } 69 | 70 | /** 71 | * Load a Template by name. 72 | * 73 | * $loader = new FilesystemLoader(dirname(__FILE__).'/views'); 74 | * // loads "./views/admin/dashboard.handlebars"; 75 | * $loader->load('admin/dashboard'); 76 | * 77 | * @param string $name template name 78 | * 79 | * @return StringWrapper Handlebars Template source 80 | */ 81 | public function load($name) 82 | { 83 | if (!isset($this->_templates[$name])) { 84 | $this->_templates[$name] = $this->loadFile($name); 85 | } 86 | 87 | return new StringWrapper($this->_templates[$name]); 88 | } 89 | 90 | /** 91 | * Sets directories to load templates from 92 | * 93 | * @param string|array $baseDirs A path contain template files or array of paths 94 | * 95 | * @return void 96 | */ 97 | protected function setBaseDir($baseDirs) 98 | { 99 | if (is_string($baseDirs)) { 100 | $baseDirs = array($this->sanitizeDirectory($baseDirs)); 101 | } else { 102 | foreach ($baseDirs as &$dir) { 103 | $dir = $this->sanitizeDirectory($dir); 104 | } 105 | unset($dir); 106 | } 107 | 108 | foreach ($baseDirs as $dir) { 109 | if (!is_dir($dir)) { 110 | throw new \RuntimeException( 111 | 'FilesystemLoader baseDir must be a directory: ' . $dir 112 | ); 113 | } 114 | } 115 | 116 | $this->baseDir = $baseDirs; 117 | } 118 | 119 | /** 120 | * Puts directory into standardized format 121 | * 122 | * @param String $dir The directory to sanitize 123 | * 124 | * @return String 125 | */ 126 | protected function sanitizeDirectory($dir) 127 | { 128 | return rtrim(realpath($dir), '/'); 129 | } 130 | 131 | /** 132 | * Sets properties based on options 133 | * 134 | * @param array $options Array of Loader options (default: array()) 135 | * 136 | * @return void 137 | */ 138 | protected function handleOptions(array $options = array()) 139 | { 140 | if (isset($options['extension'])) { 141 | $this->_extension = '.' . ltrim($options['extension'], '.'); 142 | } 143 | 144 | if (isset($options['prefix'])) { 145 | $this->_prefix = $options['prefix']; 146 | } 147 | } 148 | 149 | /** 150 | * Helper function for loading a Handlebars file by name. 151 | * 152 | * @param string $name template name 153 | * 154 | * @throws \InvalidArgumentException if a template file is not found. 155 | * @return string Handlebars Template source 156 | */ 157 | protected function loadFile($name) 158 | { 159 | $fileName = $this->getFileName($name); 160 | 161 | if ($fileName === false) { 162 | throw new \InvalidArgumentException('Template ' . $name . ' not found.'); 163 | } 164 | 165 | return file_get_contents($fileName); 166 | } 167 | 168 | /** 169 | * Helper function for getting a Handlebars template file name. 170 | * 171 | * @param string $name template name 172 | * 173 | * @return string Template file name 174 | */ 175 | protected function getFileName($name) 176 | { 177 | foreach ($this->baseDir as $baseDir) { 178 | $fileName = $baseDir . '/'; 179 | $fileParts = explode('/', $name); 180 | $file = array_pop($fileParts); 181 | 182 | if (substr($file, strlen($this->_prefix)) !== $this->_prefix) { 183 | $file = $this->_prefix . $file; 184 | } 185 | 186 | $fileParts[] = $file; 187 | $fileName .= implode('/', $fileParts); 188 | $lastCharacters = substr($fileName, 0 - strlen($this->_extension)); 189 | 190 | if ($lastCharacters !== $this->_extension) { 191 | $fileName .= $this->_extension; 192 | } 193 | if (file_exists($fileName)) { 194 | return $fileName; 195 | } 196 | } 197 | 198 | return false; 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /src/Handlebars/Loader/InlineLoader.php: -------------------------------------------------------------------------------- 1 | load('hello'); 13 | * $goodbye = $loader->load('goodbye'); 14 | * 15 | * __halt_compiler(); 16 | * 17 | * @@ hello 18 | * Hello, {{ planet }}! 19 | * 20 | * @@ goodbye 21 | * Goodbye, cruel {{ planet }} 22 | * 23 | * Templates are deliniated by lines containing only `@@ name`. 24 | * 25 | * @category Xamin 26 | * @package Handlebars 27 | * @author fzerorubigd 28 | * @author Hiroyuki Toda 29 | * @copyright 2010-2015 (c) Justin Hileman 30 | * @copyright 2015 (c) fzerorubigd 31 | * @license MIT 32 | * @version Release: @package_version@ 33 | * @link http://xamin.ir 34 | */ 35 | 36 | namespace Handlebars\Loader; 37 | 38 | use Handlebars\Loader; 39 | 40 | /** 41 | * The inline loader 42 | * 43 | * @category Xamin 44 | * @package Handlebars 45 | * @author fzerorubigd 46 | * @author Hiroyuki Toda 47 | * @copyright 2010-2015 (c) Justin Hileman 48 | * @copyright 2015 (c) fzerorubigd 49 | * @license MIT 50 | * @version Release: @package_version@ 51 | * @link http://xamin.ir * 52 | */ 53 | class InlineLoader implements Loader 54 | { 55 | protected $fileName; 56 | protected $offset; 57 | protected $templates; 58 | 59 | /** 60 | * The InlineLoader requires a filename and offset to process templates. 61 | * The magic constants `__FILE__` and `__COMPILER_HALT_OFFSET__` are usually 62 | * perfectly suited to the job: 63 | * 64 | * $loader = new \Handlebars\Loader\InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__); 65 | * 66 | * Note that this only works if the loader is instantiated inside the same 67 | * file as the inline templates. If the templates are located in another 68 | * file, it would be necessary to manually specify the filename and offset. 69 | * 70 | * @param string $fileName The file to parse for inline templates 71 | * @param int $offset A string offset for the start of the templates. 72 | * This usually coincides with the `__halt_compiler` 73 | * call, and the `__COMPILER_HALT_OFFSET__`. 74 | */ 75 | public function __construct($fileName, $offset) 76 | { 77 | if (!is_file($fileName)) { 78 | throw new \InvalidArgumentException( 79 | sprintf( 80 | 'InlineLoader expects a valid filename, "%s" given.', 81 | $fileName 82 | ) 83 | ); 84 | } 85 | 86 | if (!is_int($offset) || $offset < 0) { 87 | throw new \InvalidArgumentException( 88 | sprintf( 89 | 'InlineLoader expects a valid file offset, "%s" given.', 90 | $offset 91 | ) 92 | ); 93 | } 94 | 95 | $this->fileName = $fileName; 96 | $this->offset = $offset; 97 | } 98 | 99 | /** 100 | * Load a Template by name. 101 | * 102 | * @param string $name template name 103 | * 104 | * @return string Handlebars Template source 105 | */ 106 | public function load($name) 107 | { 108 | $this->loadTemplates(); 109 | 110 | if (!array_key_exists($name, $this->templates)) { 111 | throw new \InvalidArgumentException("Template $name not found."); 112 | } 113 | 114 | return $this->templates[$name]; 115 | } 116 | 117 | /** 118 | * Parse and load templates from the end of a source file. 119 | * 120 | * @return void 121 | */ 122 | protected function loadTemplates() 123 | { 124 | if (!is_null($this->templates)) { 125 | return; 126 | } 127 | 128 | $this->templates = array(); 129 | $data = file_get_contents($this->fileName, false, null, $this->offset); 130 | foreach (preg_split('/^@@(?= [\w\d\.]+$)/m', $data, -1) as $chunk) { 131 | if (trim($chunk)) { 132 | list($name, $content) = explode("\n", $chunk, 2); 133 | $this->templates[trim($name)] = trim($content); 134 | } 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /src/Handlebars/Loader/StringLoader.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Behrooz Shabani 12 | * @copyright 2010-2012 (c) Justin Hileman 13 | * @copyright 2012 (c) ParsPooyesh Co 14 | * @copyright 2013 (c) Behrooz Shabani 15 | * @license MIT 16 | * @version GIT: $Id$ 17 | * @link http://xamin.ir 18 | */ 19 | 20 | namespace Handlebars\Loader; 21 | use Handlebars\Loader; 22 | use Handlebars\StringWrapper; 23 | 24 | /** 25 | * Handlebars Template string Loader implementation. 26 | * 27 | * @category Xamin 28 | * @package Handlebars 29 | * @author fzerorubigd 30 | * @copyright 2010-2012 (c) Justin Hileman 31 | * @copyright 2012 (c) ParsPooyesh Co 32 | * @license MIT 33 | * @version Release: @package_version@ 34 | * @link http://xamin.ir * 35 | */ 36 | 37 | class StringLoader implements Loader 38 | { 39 | 40 | /** 41 | * Load a Template by source. 42 | * 43 | * @param string $name Handlebars Template source 44 | * 45 | * @return StringWrapper Handlebars Template source 46 | */ 47 | public function load($name) 48 | { 49 | return new StringWrapper($name); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/Handlebars/Parser.php: -------------------------------------------------------------------------------- 1 | 12 | * @author Behrooz Shabani 13 | * @copyright 2010-2012 (c) Justin Hileman 14 | * @copyright 2012 (c) ParsPooyesh Co 15 | * @copyright 2013 (c) Behrooz Shabani 16 | * @license MIT 17 | * @version GIT: $Id$ 18 | * @link http://xamin.ir 19 | */ 20 | 21 | namespace Handlebars; 22 | 23 | /** 24 | * Handlebars parser (based on mustache) 25 | * 26 | * This class is responsible for turning raw template source into a set of 27 | * Handlebars tokens. 28 | * 29 | * @category Xamin 30 | * @package Handlebars 31 | * @author fzerorubigd 32 | * @copyright 2010-2012 (c) Justin Hileman 33 | * @copyright 2012 (c) ParsPooyesh Co 34 | * @license MIT 35 | * @version Release: @package_version@ 36 | * @link http://xamin.ir 37 | */ 38 | 39 | class Parser 40 | { 41 | /** 42 | * Process array of tokens and convert them into parse tree 43 | * 44 | * @param array $tokens Set of 45 | * 46 | * @return array Token parse tree 47 | */ 48 | public function parse(array $tokens = array()) 49 | { 50 | return $this->_buildTree(new \ArrayIterator($tokens)); 51 | } 52 | 53 | /** 54 | * Helper method for recursively building a parse tree. 55 | * Trim right and trim left is a bit tricky here. 56 | * {{#begin~}}{{TOKEN}}, TOKEN.. {{LAST}}{{~/begin}} is translated to: 57 | * {{#begin}}{{~TOKEN}}, TOKEN.. {{LAST~}}{{/begin}} 58 | * 59 | * @param \ArrayIterator $tokens Stream of tokens 60 | * 61 | * @throws \LogicException when nesting errors or mismatched section tags 62 | * are encountered. 63 | * @return array Token parse tree 64 | */ 65 | private function _buildTree(\ArrayIterator $tokens) 66 | { 67 | $stack = array(); 68 | 69 | do { 70 | $token = $tokens->current(); 71 | $tokens->next(); 72 | 73 | if ($token !== null) { 74 | switch ($token[Tokenizer::TYPE]) { 75 | case Tokenizer::T_END_SECTION: 76 | $newNodes = array($token); 77 | do { 78 | $result = array_pop($stack); 79 | if ($result === null) { 80 | throw new \LogicException( 81 | sprintf( 82 | 'Unexpected closing tag: /%s', 83 | $token[Tokenizer::NAME] 84 | ) 85 | ); 86 | } 87 | 88 | if (!array_key_exists(Tokenizer::NODES, $result) 89 | && isset($result[Tokenizer::NAME]) 90 | && ($result[Tokenizer::TYPE] == Tokenizer::T_SECTION 91 | || $result[Tokenizer::TYPE] == Tokenizer::T_INVERTED) 92 | && $result[Tokenizer::NAME] == $token[Tokenizer::NAME] 93 | ) { 94 | if (isset($result[Tokenizer::TRIM_RIGHT]) 95 | && $result[Tokenizer::TRIM_RIGHT] 96 | ) { 97 | // If the start node has trim right, then its equal 98 | //with the first item in the loop with 99 | // Trim left 100 | $newNodes[0][Tokenizer::TRIM_LEFT] = true; 101 | } 102 | 103 | if (isset($token[Tokenizer::TRIM_RIGHT]) 104 | && $token[Tokenizer::TRIM_RIGHT] 105 | ) { 106 | //OK, if we have trim right here, we should 107 | //pass it to the upper level. 108 | $result[Tokenizer::TRIM_RIGHT] = true; 109 | } 110 | 111 | $result[Tokenizer::NODES] = $newNodes; 112 | $result[Tokenizer::END] = $token[Tokenizer::INDEX]; 113 | array_push($stack, $result); 114 | break; 115 | } else { 116 | array_unshift($newNodes, $result); 117 | } 118 | } while (true); 119 | break; 120 | default: 121 | array_push($stack, $token); 122 | } 123 | } 124 | 125 | } while ($tokens->valid()); 126 | 127 | return $stack; 128 | } 129 | 130 | } -------------------------------------------------------------------------------- /src/Handlebars/SafeString.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2014 Authors 11 | * @license MIT 12 | * @version GIT: $Id$ 13 | * @link http://xamin.ir 14 | */ 15 | 16 | namespace Handlebars; 17 | 18 | /** 19 | * Handlebars safe string. Can be used in line helpers as wrapper for result to 20 | * indicate that there is no need to escape the result. 21 | * 22 | * @category Xamin 23 | * @package Handlebars 24 | * @author Dmitriy Simushev 25 | * @copyright 2014 Authors 26 | * @license MIT 27 | * @version Release: @package_version@ 28 | * @link http://xamin.ir 29 | */ 30 | 31 | class SafeString extends BaseString 32 | { 33 | } 34 | -------------------------------------------------------------------------------- /src/Handlebars/String.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Behrooz Shabani 11 | * @author Dmitriy Simushev 12 | * @copyright 2013 Authors 13 | * @license MIT 14 | * @version GIT: $Id$ 15 | * @link http://xamin.ir 16 | */ 17 | 18 | namespace Handlebars; 19 | 20 | /** 21 | * Handlebars string 22 | * 23 | * @category Xamin 24 | * @package Handlebars 25 | * @author fzerorubigd 26 | * @copyright 2013 Authors 27 | * @license MIT 28 | * @version Release: @package_version@ 29 | * @link http://xamin.ir 30 | * @deprecated Since v0.10.3. Use \Handlebars\StringWrapper instead. 31 | */ 32 | 33 | class String extends StringWrapper 34 | { 35 | } 36 | -------------------------------------------------------------------------------- /src/Handlebars/StringWrapper.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Behrooz Shabani 11 | * @author Dmitriy Simushev 12 | * @copyright 2013 Authors 13 | * @license MIT 14 | * @version GIT: $Id$ 15 | * @link http://xamin.ir 16 | */ 17 | 18 | namespace Handlebars; 19 | 20 | /** 21 | * Handlebars string 22 | * 23 | * @category Xamin 24 | * @package Handlebars 25 | * @author fzerorubigd 26 | * @copyright 2013 Authors 27 | * @license MIT 28 | * @version Release: @package_version@ 29 | * @link http://xamin.ir 30 | */ 31 | 32 | class StringWrapper extends BaseString 33 | { 34 | } 35 | -------------------------------------------------------------------------------- /src/Handlebars/Template.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Behrooz Shabani 12 | * @author Chris Gray 13 | * @author Dmitriy Simushev 14 | * @author majortom731 15 | * @author Jeff Turcotte 16 | * @author John Slegers 17 | * @copyright 2010-2012 (c) Justin Hileman 18 | * @copyright 2012 (c) ParsPooyesh Co 19 | * @copyright 2013 (c) Behrooz Shabani 20 | * @license MIT 21 | * @version GIT: $Id$ 22 | * @link http://xamin.ir 23 | */ 24 | 25 | namespace Handlebars; 26 | use Handlebars\Arguments; 27 | use Traversable; 28 | 29 | /** 30 | * Handlebars base template 31 | * contain some utility method to get context and helpers 32 | * 33 | * @category Xamin 34 | * @package Handlebars 35 | * @author fzerorubigd 36 | * @author Pascal Thormeier 37 | * @copyright 2010-2012 (c) Justin Hileman 38 | * @copyright 2012 (c) ParsPooyesh Co 39 | * @license MIT 40 | * @version Release: @package_version@ 41 | * @link http://xamin.ir 42 | */ 43 | 44 | class Template 45 | { 46 | /** 47 | * Handlebars instance 48 | * 49 | * @var Handlebars 50 | */ 51 | protected $handlebars; 52 | 53 | /** 54 | * @var array The tokenized tree 55 | */ 56 | protected $tree = array(); 57 | 58 | /** 59 | * @var string The template source 60 | */ 61 | protected $source = ''; 62 | 63 | /** 64 | * Run stack 65 | * 66 | * @var array Run stack 67 | */ 68 | protected $stack = array(); 69 | 70 | /** 71 | * Handlebars template constructor 72 | * 73 | * @param Handlebars $engine handlebar engine 74 | * @param array $tree Parsed tree 75 | * @param string $source Handlebars source 76 | */ 77 | public function __construct(Handlebars $engine, $tree, $source) 78 | { 79 | $this->handlebars = $engine; 80 | $this->tree = $tree; 81 | $this->source = $source; 82 | array_push($this->stack, array(0, $this->getTree(), false)); 83 | } 84 | 85 | /** 86 | * Get current tree 87 | * 88 | * @return array 89 | */ 90 | public function getTree() 91 | { 92 | return $this->tree; 93 | } 94 | 95 | /** 96 | * Get current source 97 | * 98 | * @return string 99 | */ 100 | public function getSource() 101 | { 102 | return $this->source; 103 | } 104 | 105 | /** 106 | * Get current engine associated with this object 107 | * 108 | * @return Handlebars 109 | */ 110 | public function getEngine() 111 | { 112 | return $this->handlebars; 113 | } 114 | 115 | /** 116 | * Set stop token for render and discard method 117 | * 118 | * @param string $token token to set as stop token or false to remove 119 | * 120 | * @return void 121 | */ 122 | public function setStopToken($token) 123 | { 124 | $topStack = array_pop($this->stack); 125 | $topStack[2] = $token; 126 | array_push($this->stack, $topStack); 127 | } 128 | 129 | /** 130 | * Get current stop token 131 | * 132 | * @return string|bool 133 | */ 134 | public function getStopToken() 135 | { 136 | $topStack = end($this->stack); 137 | 138 | return $topStack[2]; 139 | } 140 | 141 | /** 142 | * Get the current token's tree 143 | * 144 | * @return array 145 | */ 146 | public function getCurrentTokenTree() 147 | { 148 | $topStack = end($this->stack); 149 | 150 | return $topStack[1]; 151 | } 152 | 153 | /** 154 | * Render top tree 155 | * 156 | * @param mixed $context current context 157 | * 158 | * @throws \RuntimeException 159 | * @return string 160 | */ 161 | public function render($context) 162 | { 163 | if (!$context instanceof Context) { 164 | $context = new Context($context); 165 | } 166 | $topTree = end($this->stack); // never pop a value from stack 167 | list($index, $tree, $stop) = $topTree; 168 | 169 | $buffer = ''; 170 | $rTrim = false; 171 | while (array_key_exists($index, $tree)) { 172 | $current = $tree[$index]; 173 | $index++; 174 | //if the section is exactly like waitFor 175 | if (is_string($stop) 176 | && $current[Tokenizer::TYPE] == Tokenizer::T_ESCAPED 177 | && $current[Tokenizer::NAME] === $stop 178 | ) { 179 | break; 180 | } 181 | if (isset($current[Tokenizer::TRIM_LEFT]) 182 | && $current[Tokenizer::TRIM_LEFT] 183 | ) { 184 | $buffer = rtrim($buffer); 185 | } 186 | 187 | $tmp = $this->renderInternal($current, $context); 188 | 189 | if (isset($current[Tokenizer::TRIM_LEFT]) 190 | && $current[Tokenizer::TRIM_LEFT] 191 | ) { 192 | $tmp = rtrim($tmp); 193 | } 194 | 195 | if ($rTrim 196 | || (isset($current[Tokenizer::TRIM_RIGHT]) 197 | && $current[Tokenizer::TRIM_RIGHT]) 198 | ) { 199 | $tmp = ltrim($tmp); 200 | } 201 | 202 | $buffer .= $tmp; 203 | // Some time, there is more than 204 | //one string token (first is empty), 205 | //so we need to trim all of them in one shot 206 | 207 | $rTrim = (empty($tmp) && $rTrim) || 208 | isset($current[Tokenizer::TRIM_RIGHT]) 209 | && $current[Tokenizer::TRIM_RIGHT]; 210 | } 211 | if ($stop) { 212 | //Ok break here, the helper should be aware of this. 213 | $newStack = array_pop($this->stack); 214 | $newStack[0] = $index; 215 | $newStack[2] = false; //No stop token from now on 216 | array_push($this->stack, $newStack); 217 | } 218 | 219 | return $buffer; 220 | } 221 | 222 | /** 223 | * Render tokens base on type of tokens 224 | * 225 | * @param array $current current token 226 | * @param mixed $context current context 227 | * 228 | * @return string 229 | */ 230 | protected function renderInternal($current, $context) 231 | { 232 | $result = ''; 233 | switch ($current[Tokenizer::TYPE]) { 234 | case Tokenizer::T_END_SECTION: 235 | break; // Its here just for handling whitespace trim. 236 | case Tokenizer::T_SECTION : 237 | $newStack = isset($current[Tokenizer::NODES]) 238 | ? $current[Tokenizer::NODES] : array(); 239 | array_push($this->stack, array(0, $newStack, false)); 240 | $result = $this->_section($context, $current); 241 | array_pop($this->stack); 242 | break; 243 | case Tokenizer::T_INVERTED : 244 | $newStack = isset($current[Tokenizer::NODES]) ? 245 | $current[Tokenizer::NODES] : array(); 246 | array_push($this->stack, array(0, $newStack, false)); 247 | $result = $this->_inverted($context, $current); 248 | array_pop($this->stack); 249 | break; 250 | case Tokenizer::T_COMMENT : 251 | $result = ''; 252 | break; 253 | case Tokenizer::T_PARTIAL: 254 | case Tokenizer::T_PARTIAL_2: 255 | $result = $this->_partial($context, $current); 256 | break; 257 | case Tokenizer::T_UNESCAPED: 258 | case Tokenizer::T_UNESCAPED_2: 259 | $result = $this->_get($context, $current, false); 260 | break; 261 | case Tokenizer::T_ESCAPED: 262 | $result = $this->_get($context, $current, true); 263 | break; 264 | case Tokenizer::T_TEXT: 265 | $result = $current[Tokenizer::VALUE]; 266 | break; 267 | /* How we could have another type of token? this part of code 268 | is not used at all. 269 | default: 270 | throw new \RuntimeException( 271 | 'Invalid node type : ' . json_encode($current) 272 | ); 273 | */ 274 | } 275 | 276 | return $result; 277 | } 278 | 279 | /** 280 | * Discard top tree 281 | * 282 | * @return string 283 | */ 284 | public function discard() 285 | { 286 | $topTree = end($this->stack); //This method never pop a value from stack 287 | list($index, $tree, $stop) = $topTree; 288 | while (array_key_exists($index, $tree)) { 289 | $current = $tree[$index]; 290 | $index++; 291 | //if the section is exactly like waitFor 292 | if (is_string($stop) 293 | && $current[Tokenizer::TYPE] == Tokenizer::T_ESCAPED 294 | && $current[Tokenizer::NAME] === $stop 295 | ) { 296 | break; 297 | } 298 | } 299 | if ($stop) { 300 | //Ok break here, the helper should be aware of this. 301 | $newStack = array_pop($this->stack); 302 | $newStack[0] = $index; 303 | $newStack[2] = false; 304 | array_push($this->stack, $newStack); 305 | } 306 | 307 | return ''; 308 | } 309 | 310 | /** 311 | * Rewind top tree index to the first element 312 | * 313 | * @return void 314 | */ 315 | public function rewind() 316 | { 317 | $topStack = array_pop($this->stack); 318 | $topStack[0] = 0; 319 | array_push($this->stack, $topStack); 320 | } 321 | 322 | /** 323 | * Process handlebars section style 324 | * 325 | * @param Context $context current context 326 | * @param array $current section node data 327 | * 328 | * @return mixed|string 329 | */ 330 | private function _handlebarsStyleSection(Context $context, $current) 331 | { 332 | $helpers = $this->handlebars->getHelpers(); 333 | $sectionName = $current[Tokenizer::NAME]; 334 | 335 | if (isset($current[Tokenizer::END])) { 336 | $source = substr( 337 | $this->getSource(), 338 | $current[Tokenizer::INDEX], 339 | $current[Tokenizer::END] - $current[Tokenizer::INDEX] 340 | ); 341 | } else { 342 | $source = ''; 343 | } 344 | 345 | // subexpression parsing loop 346 | // will contain all subexpressions 347 | // inside outermost brackets 348 | $subexprs = array(); 349 | $insideOf = array( 'single' => false, 'double' => false ); 350 | $lvl = 0; 351 | $cur_start = 0; 352 | for ($i=0; $i < strlen($current[Tokenizer::ARGS]); $i++) { 353 | $cur = substr($current[Tokenizer::ARGS], $i, 1); 354 | if ($cur == "'" ) { 355 | $insideOf['single'] = ! $insideOf['single']; 356 | } 357 | if ($cur == '"' ) { 358 | $insideOf['double'] = ! $insideOf['double']; 359 | } 360 | if ($cur == '(' && ! $insideOf['single'] && ! $insideOf['double']) { 361 | if ($lvl == 0) { 362 | $cur_start = $i+1; 363 | } 364 | $lvl++; 365 | continue; 366 | } 367 | if ($cur == ')' && ! $insideOf['single'] && ! $insideOf['double']) { 368 | $lvl--; 369 | if ($lvl == 0) { 370 | $subexprs[] = substr( 371 | $current[Tokenizer::ARGS], 372 | $cur_start, 373 | $i - $cur_start 374 | ); 375 | } 376 | 377 | } 378 | } 379 | 380 | if (! empty($subexprs)) { 381 | foreach ($subexprs as $expr) { 382 | $cmd = explode(" ", $expr); 383 | $name = trim($cmd[0]); 384 | // construct artificial section node 385 | $section_node = array( 386 | Tokenizer::TYPE => Tokenizer::T_ESCAPED, 387 | Tokenizer::NAME => $name, 388 | Tokenizer::OTAG => $current[Tokenizer::OTAG], 389 | Tokenizer::CTAG => $current[Tokenizer::CTAG], 390 | Tokenizer::INDEX => $current[Tokenizer::INDEX], 391 | Tokenizer::ARGS => implode(" ", array_slice($cmd, 1)) 392 | ); 393 | 394 | // resolve the node recursively 395 | $resolved = $this->_handlebarsStyleSection( 396 | $context, 397 | $section_node 398 | ); 399 | 400 | $resolved = addcslashes($resolved, '"'); 401 | // replace original subexpression with result 402 | $current[Tokenizer::ARGS] = str_replace( 403 | '('.$expr.')', 404 | '"' . $resolved . '"', 405 | $current[Tokenizer::ARGS] 406 | ); 407 | } 408 | } 409 | 410 | $return = $helpers->call( 411 | $sectionName, 412 | $this, 413 | $context, 414 | $current[Tokenizer::ARGS], 415 | $source 416 | ); 417 | 418 | if ($return instanceof StringWrapper) { 419 | return $this->handlebars->loadString($return)->render($context); 420 | } else { 421 | return $return; 422 | } 423 | } 424 | 425 | /** 426 | * Process Mustache section style 427 | * 428 | * @param Context $context current context 429 | * @param array $current section node data 430 | * 431 | * @throws \RuntimeException 432 | * @return mixed|string 433 | */ 434 | private function _mustacheStyleSection(Context $context, $current) 435 | { 436 | $sectionName = $current[Tokenizer::NAME]; 437 | 438 | // fallback to mustache style each/with/for just if there is 439 | // no argument at all. 440 | try { 441 | $sectionVar = $context->get($sectionName, false); 442 | } catch (\InvalidArgumentException $e) { 443 | throw new \RuntimeException( 444 | sprintf( 445 | '"%s" is not registered as a helper', 446 | $sectionName 447 | ) 448 | ); 449 | } 450 | $buffer = ''; 451 | if ($this->_checkIterable($sectionVar)) { 452 | $index = 0; 453 | $lastIndex = (count($sectionVar) - 1); 454 | foreach ($sectionVar as $key => $d) { 455 | $context->pushSpecialVariables( 456 | array( 457 | '@index' => $index, 458 | '@first' => ($index === 0), 459 | '@last' => ($index === $lastIndex), 460 | '@key' => $key 461 | ) 462 | ); 463 | $context->push($d); 464 | $buffer .= $this->render($context); 465 | $context->pop(); 466 | $context->popSpecialVariables(); 467 | $index++; 468 | } 469 | } elseif ($sectionVar) { 470 | //Act like with 471 | $context->push($sectionVar); 472 | $buffer = $this->render($context); 473 | $context->pop(); 474 | } 475 | 476 | return $buffer; 477 | } 478 | 479 | /** 480 | * Process section nodes 481 | * 482 | * @param Context $context current context 483 | * @param array $current section node data 484 | * 485 | * @throws \RuntimeException 486 | * @return string the result 487 | */ 488 | private function _section(Context $context, $current) 489 | { 490 | $helpers = $this->handlebars->getHelpers(); 491 | $sectionName = $current[Tokenizer::NAME]; 492 | if ($helpers->has($sectionName)) { 493 | return $this->_handlebarsStyleSection($context, $current); 494 | } elseif (trim($current[Tokenizer::ARGS]) == '') { 495 | return $this->_mustacheStyleSection($context, $current); 496 | } else { 497 | throw new \RuntimeException( 498 | sprintf( 499 | '"%s"" is not registered as a helper', 500 | $sectionName 501 | ) 502 | ); 503 | } 504 | } 505 | 506 | /** 507 | * Process inverted section 508 | * 509 | * @param Context $context current context 510 | * @param array $current section node data 511 | * 512 | * @return string the result 513 | */ 514 | private function _inverted(Context $context, $current) 515 | { 516 | $sectionName = $current[Tokenizer::NAME]; 517 | $data = $context->get($sectionName); 518 | if (!$data) { 519 | return $this->render($context); 520 | } else { 521 | //No need to discard here, since it has no else 522 | return ''; 523 | } 524 | } 525 | 526 | /** 527 | * Process partial section 528 | * 529 | * @param Context $context current context 530 | * @param array $current section node data 531 | * 532 | * @return string the result 533 | */ 534 | private function _partial(Context $context, $current) 535 | { 536 | $partial = $this->handlebars->loadPartial($current[Tokenizer::NAME]); 537 | 538 | if ($current[Tokenizer::ARGS]) { 539 | $arguments = new Arguments($current[Tokenizer::ARGS]); 540 | 541 | $context = new Context($this->_preparePartialArguments($context, $arguments)); 542 | } 543 | 544 | return $partial->render($context); 545 | } 546 | 547 | /** 548 | * Prepare the arguments of a partial to actual array values to be used in a new context 549 | * 550 | * @param Context $context Current context 551 | * @param Arguments $arguments Arguments for partial 552 | * 553 | * @return array 554 | */ 555 | private function _preparePartialArguments(Context $context, Arguments $arguments) 556 | { 557 | $positionalArgs = array(); 558 | foreach ($arguments->getPositionalArguments() as $positionalArg) { 559 | $contextArg = $context->get($positionalArg); 560 | if (is_array($contextArg)) { 561 | foreach ($contextArg as $key => $value) { 562 | $positionalArgs[$key] = $value; 563 | } 564 | } else { 565 | $positionalArgs[$positionalArg] = $contextArg; 566 | } 567 | } 568 | 569 | $namedArguments = array(); 570 | foreach ($arguments->getNamedArguments() as $key => $value) { 571 | $namedArguments[$key] = $context->get($value); 572 | } 573 | 574 | return array_merge($positionalArgs, $namedArguments); 575 | } 576 | 577 | 578 | /** 579 | * Check if there is a helper with this variable name available or not. 580 | * 581 | * @param array $current current token 582 | * 583 | * @return boolean 584 | */ 585 | private function _isSection($current) 586 | { 587 | $helpers = $this->getEngine()->getHelpers(); 588 | // Tokenizer doesn't process the args -if any- so be aware of that 589 | $name = explode(' ', $current[Tokenizer::NAME], 2); 590 | 591 | return $helpers->has(reset($name)); 592 | } 593 | 594 | /** 595 | * Get replacing value of a tag 596 | * 597 | * Will process the tag as section, if a helper with the same name could be 598 | * found, so {{helper arg}} can be used instead of {{#helper arg}}. 599 | * 600 | * @param Context $context current context 601 | * @param array $current section node data 602 | * @param boolean $escaped escape result or not 603 | * 604 | * @return string the string to be replaced with the tag 605 | */ 606 | private function _get(Context $context, $current, $escaped) 607 | { 608 | if ($this->_isSection($current)) { 609 | return $this->_getSection($context, $current, $escaped); 610 | } else { 611 | return $this->_getVariable($context, $current, $escaped); 612 | } 613 | } 614 | 615 | /** 616 | * Process section 617 | * 618 | * @param Context $context current context 619 | * @param array $current section node data 620 | * @param boolean $escaped escape result or not 621 | * 622 | * @return string the result 623 | */ 624 | private function _getSection(Context $context, $current, $escaped) 625 | { 626 | $args = explode(' ', $current[Tokenizer::NAME], 2); 627 | $name = array_shift($args); 628 | $current[Tokenizer::NAME] = $name; 629 | $current[Tokenizer::ARGS] = implode(' ', $args); 630 | $result = $this->_section($context, $current); 631 | 632 | if ($escaped && !($result instanceof SafeString)) { 633 | $escape_args = $this->handlebars->getEscapeArgs(); 634 | array_unshift($escape_args, $result); 635 | $result = call_user_func_array( 636 | $this->handlebars->getEscape(), 637 | array_values($escape_args) 638 | ); 639 | } 640 | 641 | return $result; 642 | } 643 | 644 | /** 645 | * Process variable 646 | * 647 | * @param Context $context current context 648 | * @param array $current section node data 649 | * @param boolean $escaped escape result or not 650 | * 651 | * @return string the result 652 | */ 653 | private function _getVariable(Context $context, $current, $escaped) 654 | { 655 | $name = $current[Tokenizer::NAME]; 656 | $value = $context->get($name); 657 | if (is_array($value)) { 658 | return 'Array'; 659 | } 660 | if ($escaped && !($value instanceof SafeString)) { 661 | $args = $this->handlebars->getEscapeArgs(); 662 | array_unshift($args, (string)$value); 663 | $value = call_user_func_array( 664 | $this->handlebars->getEscape(), 665 | array_values($args) 666 | ); 667 | } 668 | 669 | return (string)$value; 670 | } 671 | 672 | /** 673 | * Break an argument string into an array of named arguments 674 | * 675 | * @param string $string Argument String as passed to a helper 676 | * 677 | * @return array the argument list as an array 678 | */ 679 | public function parseNamedArguments($string) 680 | { 681 | if ($string instanceof Arguments) { 682 | // This code is needed only for backward compatibility 683 | $args = $string; 684 | } else { 685 | $args = new Arguments($string); 686 | } 687 | 688 | return $args->getNamedArguments(); 689 | } 690 | 691 | /** 692 | * Break an argument string into an array of strings 693 | * 694 | * @param string $string Argument String as passed to a helper 695 | * 696 | * @throws \RuntimeException 697 | * @return array the argument list as an array 698 | */ 699 | public function parseArguments($string) 700 | { 701 | if ($string instanceof Arguments) { 702 | // This code is needed only for backward compatibility 703 | $args = $string; 704 | } else { 705 | $args = new Arguments($string); 706 | } 707 | 708 | return $args->getPositionalArguments(); 709 | } 710 | 711 | /** 712 | * Tests whether a value should be iterated over (e.g. in a section context). 713 | * 714 | * @param mixed $value Value to check if iterable. 715 | * 716 | * @return bool True if the value is 'iterable' 717 | * 718 | * @see https://github.com/bobthecow/mustache.php/blob/18a2adc/src/Mustache/Template.php#L85-L113 719 | */ 720 | private function _checkIterable($value) 721 | { 722 | switch (gettype($value)) { 723 | case 'object': 724 | return $value instanceof Traversable; 725 | case 'array': 726 | $i = 0; 727 | foreach ($value as $k => $v) { 728 | if ($k !== $i++) { 729 | return false; 730 | } 731 | } 732 | return true; 733 | default: 734 | return false; 735 | } 736 | } 737 | } 738 | -------------------------------------------------------------------------------- /src/Handlebars/Tokenizer.php: -------------------------------------------------------------------------------- 1 | 13 | * @author fzerorubigd 14 | * @author Behrooz Shabani 15 | * @author Dmitriy Simushev 16 | * @copyright 2010-2012 (c) Justin Hileman 17 | * @copyright 2012 (c) ParsPooyesh Co 18 | * @copyright 2013 (c) Behrooz Shabani 19 | * @license MIT 20 | * @version GIT: $Id$ 21 | * @link http://xamin.ir 22 | */ 23 | 24 | namespace Handlebars; 25 | 26 | /** 27 | * Handlebars tokenizer (based on mustache) 28 | * 29 | * @category Xamin 30 | * @package Handlebars 31 | * @author Justin Hileman 32 | * @author fzerorubigd 33 | * @copyright 2012 Justin Hileman 34 | * @license MIT 35 | * @version Release: @package_version@ 36 | * @link http://xamin.ir 37 | */ 38 | 39 | class Tokenizer 40 | { 41 | 42 | // Finite state machine states 43 | const IN_TEXT = 0; 44 | const IN_TAG_TYPE = 1; 45 | const IN_TAG = 2; 46 | 47 | // Token types 48 | const T_SECTION = '#'; 49 | const T_INVERTED = '^'; 50 | const T_END_SECTION = '/'; 51 | const T_COMMENT = '!'; 52 | // XXX: remove partials support from tokenizer and make it a helper? 53 | const T_PARTIAL = '>'; 54 | const T_PARTIAL_2 = '<'; 55 | const T_DELIM_CHANGE = '='; 56 | const T_ESCAPED = '_v'; 57 | const T_UNESCAPED = '{'; 58 | const T_UNESCAPED_2 = '&'; 59 | const T_TEXT = '_t'; 60 | const T_ESCAPE = "\\"; 61 | const T_SINGLE_Q = "'"; 62 | const T_DOUBLE_Q = "\""; 63 | const T_TRIM = "~"; 64 | 65 | // Valid token types 66 | private static $_tagTypes = array( 67 | self::T_SECTION => true, 68 | self::T_INVERTED => true, 69 | self::T_END_SECTION => true, 70 | self::T_COMMENT => true, 71 | self::T_PARTIAL => true, 72 | self::T_PARTIAL_2 => true, 73 | self::T_DELIM_CHANGE => true, 74 | self::T_ESCAPED => true, 75 | self::T_UNESCAPED => true, 76 | self::T_UNESCAPED_2 => true, 77 | ); 78 | 79 | // Interpolated tags 80 | private static $_interpolatedTags = array( 81 | self::T_ESCAPED => true, 82 | self::T_UNESCAPED => true, 83 | self::T_UNESCAPED_2 => true, 84 | ); 85 | 86 | // Token properties 87 | const TYPE = 'type'; 88 | const NAME = 'name'; 89 | const OTAG = 'otag'; 90 | const CTAG = 'ctag'; 91 | const INDEX = 'index'; 92 | const END = 'end'; 93 | const INDENT = 'indent'; 94 | const NODES = 'nodes'; 95 | const VALUE = 'value'; 96 | const ARGS = 'args'; 97 | const TRIM_LEFT = 'tleft'; 98 | const TRIM_RIGHT = 'tright'; 99 | 100 | protected $state; 101 | protected $tagType; 102 | protected $tag; 103 | protected $buffer = ''; 104 | protected $tokens; 105 | protected $seenTag; 106 | protected $lineStart; 107 | protected $otag; 108 | protected $ctag; 109 | protected $escaped; 110 | protected $escaping; 111 | protected $trimLeft; 112 | protected $trimRight; 113 | 114 | /** 115 | * Scan and tokenize template source. 116 | * 117 | * @param string $text Mustache template source to tokenize 118 | * 119 | * @internal string $delimiters Optional, pass opening and closing delimiters 120 | * 121 | * @return array Set of Mustache tokens 122 | */ 123 | public function scan($text/*, $delimiters = null*/) 124 | { 125 | if ($text instanceof StringWrapper) { 126 | $text = $text->getString(); 127 | } 128 | $this->reset(); 129 | 130 | /* Actually we not support this. so this code is not used at all, yet. 131 | if ($delimiters = trim($delimiters)) { 132 | list($otag, $ctag) = explode(' ', $delimiters); 133 | $this->otag = $otag; 134 | $this->ctag = $ctag; 135 | } 136 | */ 137 | $len = strlen($text); 138 | for ($i = 0; $i < $len; $i++) { 139 | $this->escaping = $this->tagChange(self::T_ESCAPE, $text, $i); 140 | 141 | // To play nice with helpers' arguments quote and apostrophe marks 142 | // should be additionally escaped only when they are not in a tag. 143 | $quoteInTag = $this->state != self::IN_TEXT 144 | && ($text[$i] == self::T_SINGLE_Q || $text[$i] == self::T_DOUBLE_Q); 145 | 146 | if ($this->escaped && !$this->tagChange($this->otag, $text, $i) && !$quoteInTag) { 147 | $this->buffer .= "\\"; 148 | } 149 | 150 | switch ($this->state) { 151 | case self::IN_TEXT: 152 | // Handlebars.js does not think that openning curly brace in 153 | // "\\\{{data}}" template is escaped. Instead it removes one 154 | // slash and leaves others "as is". To emulate similar behavior 155 | // we have to check the last character in the buffer. If it's a 156 | // slash we actually does not need to escape openning curly 157 | // brace. 158 | $prev_slash = substr($this->buffer, -1) == '\\'; 159 | 160 | if ($this->tagChange($this->otag. self::T_TRIM, $text, $i) and (!$this->escaped || $prev_slash)) { 161 | $this->flushBuffer(); 162 | $this->state = self::IN_TAG_TYPE; 163 | $this->trimLeft = true; 164 | } elseif ($this->tagChange(self::T_UNESCAPED.$this->otag, $text, $i) and $this->escaped) { 165 | $this->buffer .= "{{{"; 166 | $i += 2; 167 | continue; 168 | } elseif ($this->tagChange($this->otag, $text, $i) and (!$this->escaped || $prev_slash)) { 169 | $i--; 170 | $this->flushBuffer(); 171 | $this->state = self::IN_TAG_TYPE; 172 | } elseif ($this->escaped and $this->escaping) { 173 | // We should not add extra slash before opening tag because 174 | // doubled slash where should be transformed to single one 175 | if (($i + 1) < $len && !$this->tagChange($this->otag, $text, $i + 1)) { 176 | $this->buffer .= "\\"; 177 | } 178 | } elseif (!$this->escaping) { 179 | if ($text[$i] == "\n") { 180 | $this->filterLine(); 181 | } else { 182 | $this->buffer .= $text[$i]; 183 | } 184 | } 185 | break; 186 | 187 | case self::IN_TAG_TYPE: 188 | 189 | $i += strlen($this->otag) - 1; 190 | if (isset(self::$_tagTypes[$text[$i + 1]])) { 191 | $tag = $text[$i + 1]; 192 | $this->tagType = $tag; 193 | } else { 194 | $tag = null; 195 | $this->tagType = self::T_ESCAPED; 196 | } 197 | 198 | if ($this->tagType === self::T_DELIM_CHANGE) { 199 | $i = $this->changeDelimiters($text, $i); 200 | $this->state = self::IN_TEXT; 201 | } else { 202 | if ($tag !== null) { 203 | $i++; 204 | } 205 | $this->state = self::IN_TAG; 206 | } 207 | $this->seenTag = $i; 208 | break; 209 | 210 | default: 211 | if ($this->tagChange(self::T_TRIM . $this->ctag, $text, $i)) { 212 | $this->trimRight = true; 213 | continue; 214 | } 215 | if ($this->tagChange($this->ctag, $text, $i)) { 216 | // Sections (Helpers) can accept parameters 217 | // Same thing for Partials (little known fact) 218 | if (($this->tagType == self::T_SECTION) 219 | || ($this->tagType == self::T_PARTIAL) 220 | || ($this->tagType == self::T_PARTIAL_2) 221 | ) { 222 | $newBuffer = explode(' ', trim($this->buffer), 2); 223 | $args = ''; 224 | if (count($newBuffer) == 2) { 225 | $args = $newBuffer[1]; 226 | } 227 | $this->buffer = $newBuffer[0]; 228 | } 229 | $t = array( 230 | self::TYPE => $this->tagType, 231 | self::NAME => trim($this->buffer), 232 | self::OTAG => $this->otag, 233 | self::CTAG => $this->ctag, 234 | self::INDEX => ($this->tagType == self::T_END_SECTION) ? 235 | $this->seenTag - strlen($this->otag) : 236 | $i + strlen($this->ctag), 237 | self::TRIM_LEFT => $this->trimLeft, 238 | self::TRIM_RIGHT => $this->trimRight 239 | ); 240 | if (isset($args)) { 241 | $t[self::ARGS] = $args; 242 | } 243 | $this->tokens[] = $t; 244 | unset($t); 245 | unset($args); 246 | $this->buffer = ''; 247 | $this->trimLeft = false; 248 | $this->trimRight = false; 249 | $i += strlen($this->ctag) - 1; 250 | $this->state = self::IN_TEXT; 251 | if ($this->tagType == self::T_UNESCAPED) { 252 | if ($this->ctag == '}}') { 253 | $i++; 254 | } /* else { // I can't remember why this part is here! the ctag is always }} and 255 | // Clean up `{{{ tripleStache }}}` style tokens. 256 | $lastIndex = count($this->tokens) - 1; 257 | $lastName = $this->tokens[$lastIndex][self::NAME]; 258 | if (substr($lastName, -1) === '}') { 259 | $this->tokens[$lastIndex][self::NAME] = trim( 260 | substr($lastName, 0, -1) 261 | ); 262 | } 263 | } */ 264 | } 265 | } else { 266 | $this->buffer .= $text[$i]; 267 | } 268 | break; 269 | } 270 | 271 | $this->escaped = ($this->escaping and !$this->escaped); 272 | } 273 | 274 | $this->filterLine(true); 275 | 276 | return $this->tokens; 277 | } 278 | 279 | /** 280 | * Helper function to reset tokenizer internal state. 281 | * 282 | * @return void 283 | */ 284 | protected function reset() 285 | { 286 | $this->state = self::IN_TEXT; 287 | $this->escaped = false; 288 | $this->escaping = false; 289 | $this->tagType = null; 290 | $this->tag = null; 291 | $this->buffer = ''; 292 | $this->tokens = array(); 293 | $this->seenTag = false; 294 | $this->lineStart = 0; 295 | $this->otag = '{{'; 296 | $this->ctag = '}}'; 297 | $this->trimLeft = false; 298 | $this->trimRight = false; 299 | } 300 | 301 | /** 302 | * Flush the current buffer to a token. 303 | * 304 | * @return void 305 | */ 306 | protected function flushBuffer() 307 | { 308 | if ($this->buffer !== '') { 309 | $this->tokens[] = array( 310 | self::TYPE => self::T_TEXT, 311 | self::VALUE => $this->buffer 312 | ); 313 | $this->buffer = ''; 314 | } 315 | } 316 | 317 | /** 318 | * Test whether the current line is entirely made up of whitespace. 319 | * 320 | * @return boolean True if the current line is all whitespace 321 | */ 322 | protected function lineIsWhitespace() 323 | { 324 | $tokensCount = count($this->tokens); 325 | for ($j = $this->lineStart; $j < $tokensCount; $j++) { 326 | $token = $this->tokens[$j]; 327 | if (isset(self::$_tagTypes[$token[self::TYPE]])) { 328 | if (isset(self::$_interpolatedTags[$token[self::TYPE]])) { 329 | return false; 330 | } 331 | } elseif ($token[self::TYPE] == self::T_TEXT) { 332 | if (preg_match('/\S/', $token[self::VALUE])) { 333 | return false; 334 | } 335 | } 336 | } 337 | 338 | return true; 339 | } 340 | 341 | /** 342 | * Filter out whitespace-only lines and store indent levels for partials. 343 | * 344 | * @param bool $noNewLine Suppress the newline? (default: false) 345 | * 346 | * @return void 347 | */ 348 | protected function filterLine($noNewLine = false) 349 | { 350 | $this->flushBuffer(); 351 | if ($this->seenTag && $this->lineIsWhitespace()) { 352 | $tokensCount = count($this->tokens); 353 | for ($j = $this->lineStart; $j < $tokensCount; $j++) { 354 | if ($this->tokens[$j][self::TYPE] == self::T_TEXT) { 355 | if (isset($this->tokens[$j + 1]) 356 | && $this->tokens[$j + 1][self::TYPE] == self::T_PARTIAL 357 | ) { 358 | $this->tokens[$j + 1][self::INDENT] 359 | = $this->tokens[$j][self::VALUE]; 360 | } 361 | 362 | $this->tokens[$j] = null; 363 | } 364 | } 365 | } elseif (!$noNewLine) { 366 | $this->tokens[] = array(self::TYPE => self::T_TEXT, self::VALUE => "\n"); 367 | } 368 | 369 | $this->seenTag = false; 370 | $this->lineStart = count($this->tokens); 371 | } 372 | 373 | /** 374 | * Change the current Handlebars delimiters. Set new `otag` and `ctag` values. 375 | * 376 | * @param string $text Mustache template source 377 | * @param int $index Current tokenizer index 378 | * 379 | * @return int New index value 380 | */ 381 | protected function changeDelimiters($text, $index) 382 | { 383 | $startIndex = strpos($text, '=', $index) + 1; 384 | $close = '=' . $this->ctag; 385 | $closeIndex = strpos($text, $close, $index); 386 | 387 | list($otag, $ctag) = explode( 388 | ' ', 389 | trim(substr($text, $startIndex, $closeIndex - $startIndex)) 390 | ); 391 | $this->otag = $otag; 392 | $this->ctag = $ctag; 393 | 394 | return $closeIndex + strlen($close) - 1; 395 | } 396 | 397 | /** 398 | * Test whether it's time to change tags. 399 | * 400 | * @param string $tag Current tag name 401 | * @param string $text Handlebars template source 402 | * @param int $index Current tokenizer index 403 | * 404 | * @return boolean True if this is a closing section tag 405 | */ 406 | protected function tagChange($tag, $text, $index) 407 | { 408 | return substr($text, $index, strlen($tag)) === $tag; 409 | } 410 | 411 | } 412 | -------------------------------------------------------------------------------- /tests/Xamin/Cache/APCTest.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Dmitriy Simushev 12 | * @author Mária Šormanová 13 | * @copyright 2013 (c) f0ruD A 14 | * @license MIT 15 | * @version GIT: $Id$ 16 | * @link http://xamin.ir 17 | */ 18 | 19 | /** 20 | * Test of APC cache driver 21 | * 22 | * Run without sikp: 23 | * php -d apc.enable_cli=1 ./vendor/bin/phpunit 24 | * 25 | * @category Xamin 26 | * @package Handlebars 27 | * @subpackage Test 28 | * @author Tamás Szijártó 29 | * @license MIT 30 | * @version Release: @package_version@ 31 | * @link http://xamin.ir 32 | */ 33 | class APCTest extends \PHPUnit_Framework_TestCase 34 | { 35 | /** 36 | * {@inheritdoc} 37 | * 38 | * @return void 39 | */ 40 | public function setUp() 41 | { 42 | if ( ! extension_loaded('apc') || false === @apc_cache_info()) { 43 | $this->markTestSkipped('The ' . __CLASS__ .' requires the use of APC'); 44 | } 45 | } 46 | 47 | /** 48 | * Return the new driver 49 | * 50 | * @param null|string $prefix optional key prefix, defaults to null 51 | * 52 | * @return \Handlebars\Cache\APC 53 | */ 54 | private function _getCacheDriver( $prefix = null ) 55 | { 56 | return new \Handlebars\Cache\APC($prefix); 57 | } 58 | 59 | /** 60 | * Test with cache prefix 61 | * 62 | * @return void 63 | */ 64 | public function testWithPrefix() 65 | { 66 | $prefix = __CLASS__; 67 | $driver = $this->_getCacheDriver($prefix); 68 | 69 | $this->assertEquals(false, $driver->get('foo')); 70 | 71 | $driver->set('foo', 10); 72 | $this->assertEquals(10, $driver->get('foo')); 73 | 74 | $driver->set('foo', array(12)); 75 | $this->assertEquals(array(12), $driver->get('foo')); 76 | 77 | $driver->remove('foo'); 78 | $this->assertEquals(false, $driver->get('foo')); 79 | } 80 | 81 | /** 82 | * Test without cache prefix 83 | * 84 | * @return void 85 | */ 86 | public function testWithoutPrefix() 87 | { 88 | $driver = $this->_getCacheDriver(); 89 | 90 | $this->assertEquals(false, $driver->get('foo')); 91 | 92 | $driver->set('foo', 20); 93 | $this->assertEquals(20, $driver->get('foo')); 94 | 95 | $driver->set('foo', array(22)); 96 | 97 | $this->assertEquals(array(22), $driver->get('foo')); 98 | 99 | $driver->remove('foo'); 100 | $this->assertEquals(false, $driver->get('foo')); 101 | } 102 | 103 | /** 104 | * Test ttl 105 | * 106 | * @return void 107 | */ 108 | public function testTtl() 109 | { 110 | $driver = $this->_getCacheDriver(); 111 | 112 | $driver->set('foo', 10, -1); 113 | $this->assertEquals(false, $driver->get('foo')); 114 | 115 | $driver->set('foo', 20, 3600); 116 | $this->assertEquals(20, $driver->get('foo')); 117 | } 118 | } -------------------------------------------------------------------------------- /tests/Xamin/Cache/DiskTest.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright 2016 (c) Mária Šormanová 12 | * @license MIT 13 | * @version GIT: $Id$ 14 | * @link http://xamin.ir 15 | */ 16 | 17 | /** 18 | * Test of Disk cache driver 19 | * 20 | * @category Xamin 21 | * @package Handlebars 22 | * @subpackage Test 23 | * @author Mária Šormanová 24 | * @license MIT 25 | * @version Release: @package_version@ 26 | * @link http://xamin.ir 27 | */ 28 | 29 | class DiskTest extends \PHPUnit_Framework_TestCase 30 | { 31 | /** 32 | * {@inheritdoc} 33 | * 34 | * @return void 35 | */ 36 | public function setUp() 37 | { 38 | \Handlebars\Autoloader::register(); 39 | } 40 | 41 | /** 42 | * Return the new driver 43 | * 44 | * @param string $path folder where the cache is located 45 | * 46 | * @return \Handlebars\Cache\Disk 47 | */ 48 | private function _getCacheDriver( $path = '') 49 | { 50 | return new \Handlebars\Cache\Disk($path); 51 | } 52 | 53 | /** 54 | * Test the Disk cache 55 | * 56 | * @return void 57 | */ 58 | public function testDiskCache() 59 | { 60 | $cache_dir = getcwd().'/tests/cache'; 61 | $driver = $this->_getCacheDriver($cache_dir); 62 | 63 | $this->assertEquals(false, $driver->get('foo')); 64 | 65 | $driver->set('foo', "hello world"); 66 | $this->assertEquals("hello world", $driver->get('foo')); 67 | 68 | $driver->set('foo', "hello world", -1); 69 | $this->assertEquals(false, $driver->get('foo')); 70 | 71 | $driver->set('foo', "hello world", 3600); 72 | $this->assertEquals("hello world", $driver->get('foo')); 73 | 74 | $driver->set('foo', array(12)); 75 | $this->assertEquals(array(12), $driver->get('foo')); 76 | 77 | $driver->remove('foo'); 78 | $this->assertEquals(false, $driver->get('foo')); 79 | 80 | rmdir($cache_dir); 81 | } 82 | } 83 | 84 | ?> -------------------------------------------------------------------------------- /tests/Xamin/HandlebarsTest.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Dmitriy Simushev 12 | * @copyright 2013 (c) f0ruD A 13 | * @license MIT 14 | * @version GIT: $Id$ 15 | * @link http://xamin.ir 16 | */ 17 | 18 | /** 19 | * Class HandlebarsTest 20 | */ 21 | class HandlebarsTest extends \PHPUnit_Framework_TestCase 22 | { 23 | public function setUp() 24 | { 25 | \Handlebars\Autoloader::register(); 26 | } 27 | 28 | /** 29 | * Test handlebars autoloader 30 | * 31 | * @return void 32 | */ 33 | public function testAutoLoad() 34 | { 35 | Handlebars\Autoloader::register(realpath(__DIR__ . '/../fixture/')); 36 | 37 | $this->assertTrue(class_exists('Handlebars\\Test')); 38 | $this->assertTrue(class_exists('\\Handlebars\\Test')); 39 | $this->assertTrue(class_exists('Handlebars\\Example\\Test')); 40 | $this->assertTrue(class_exists('\\Handlebars\\Example\\Test')); 41 | $this->assertFalse(class_exists('\\Another\\Example\\Test')); 42 | } 43 | 44 | /** 45 | * Test basic tags 46 | * 47 | * @param string $src handlebars source 48 | * @param array $data data 49 | * @param string $result expected data 50 | * 51 | * @dataProvider simpleTagdataProvider 52 | * 53 | * @return void 54 | */ 55 | public function testBasicTags($src, $data, $result) 56 | { 57 | $loader = new \Handlebars\Loader\StringLoader(); 58 | $engine = new \Handlebars\Handlebars(array('loader' => $loader)); 59 | $this->assertEquals($result, $engine->render($src, $data)); 60 | } 61 | 62 | /** 63 | * Simple tag provider 64 | * 65 | * @return array 66 | */ 67 | public function simpleTagdataProvider() 68 | { 69 | return array( 70 | array( 71 | '{{! This is comment}}', 72 | array(), 73 | '' 74 | ), 75 | array( 76 | '{{data}}', 77 | array('data' => 'result'), 78 | 'result' 79 | ), 80 | array( 81 | '{{data.key}}', 82 | array('data' => array('key' => 'result')), 83 | 'result' 84 | ), 85 | array( 86 | '{{data.length}}', 87 | array("data" => array(1, 2, 3, 4)), 88 | '4' 89 | ), 90 | array( 91 | '{{data.length}}', 92 | array("data" => (object)array(1, 2, 3, 4)), 93 | '' 94 | ), 95 | array( 96 | '{{data.length}}', 97 | array("data" => array("length" => "15 inches", "test", "test", "test")), 98 | "15 inches" 99 | ), 100 | array( 101 | '{{data.0}}', 102 | array("data" => array(1, 2, 3, 4)), 103 | '1' 104 | ), 105 | array( 106 | '{{data.property.3}}', 107 | array("data" => array("property" => array(1, 2, 3, 4))), 108 | '4' 109 | ), 110 | array( 111 | '{{data.unsafe}}', 112 | array('data' => array('unsafe' => 'Test')), 113 | '<strong>Test</strong>' 114 | ), 115 | array( 116 | '{{{data.safe}}}', 117 | array('data' => array('safe' => 'Test')), 118 | 'Test' 119 | ), 120 | array( 121 | "\\{{data}}", // is equal to \{{data}} in template file 122 | array('data' => 'foo'), 123 | '{{data}}', 124 | ), 125 | array( 126 | '\\\\{{data}}', // is equal to \\{{data}} in template file 127 | array('data' => 'foo'), 128 | '\\foo' // is equals to \foo in output 129 | ), 130 | array( 131 | '\\\\\\{{data}}', // is equal to \\\{{data}} in template file 132 | array('data' => 'foo'), 133 | '\\\\foo' // is equals to \\foo in output 134 | ), 135 | array( 136 | '\\\\\\\\{{data}}', // is equal to \\\\{{data}} in template file 137 | array('data' => 'foo'), 138 | '\\\\\\foo' // is equals to \\\foo in output 139 | ), 140 | array( 141 | '\{{{data}}}', // is equal to \{{{data}}} in template file 142 | array('data' => 'foo'), 143 | '{{{data}}}' 144 | ), 145 | array( 146 | '\pi', // is equal to \pi in template 147 | array(), 148 | '\pi' 149 | ), 150 | array( 151 | '\\\\foo', // is equal to \\foo in template 152 | array(), 153 | '\\\\foo' 154 | ), 155 | array( 156 | '\\\\\\bar', // is equal to \\\bar in template 157 | array(), 158 | '\\\\\\bar' 159 | ), 160 | array( 161 | '\\\\\\\\qux', // is equal to \\\\qux in template file 162 | array(), 163 | '\\\\\\\\qux' 164 | ), 165 | array( 166 | "var jsVar = 'It\'s a phrase in apos';", 167 | array(), 168 | "var jsVar = 'It\'s a phrase in apos';" 169 | ), 170 | array( 171 | 'var jsVar = "A \"quoted\" text";', 172 | array(), 173 | 'var jsVar = "A \"quoted\" text";', 174 | ), 175 | array( 176 | '{{#if first}}The first{{else}}{{#if second}}The second{{/if}}{{/if}}', 177 | array('first' => false, 'second' => true), 178 | 'The second' 179 | ), 180 | array( 181 | '{{#value}}Hello {{value}}, from {{parent_context}}{{/value}}', 182 | array('value' => 'string', 'parent_context' => 'parent string'), 183 | 'Hello string, from parent string' 184 | ), 185 | ); 186 | } 187 | 188 | /** 189 | * Test helpers (internal helpers) 190 | * 191 | * @param string $src handlebars source 192 | * @param array $data data 193 | * @param string $result expected data 194 | * 195 | * @dataProvider internalHelpersdataProvider 196 | * 197 | * @return void 198 | */ 199 | public function testSimpleHelpers($src, $data, $result) 200 | { 201 | $loader = new \Handlebars\Loader\StringLoader(); 202 | $helpers = new \Handlebars\Helpers(); 203 | $engine = new \Handlebars\Handlebars(array('loader' => $loader, 'helpers' => $helpers)); 204 | 205 | $this->assertEquals($result, $engine->render($src, $data)); 206 | } 207 | 208 | /** 209 | * Simple helpers provider 210 | * 211 | * @return array 212 | */ 213 | public function internalHelpersdataProvider() 214 | { 215 | return array( 216 | array( 217 | '{{#if data}}Yes{{/if}}', 218 | array('data' => true), 219 | 'Yes' 220 | ), 221 | # see the issue #76 222 | array( 223 | '{{#if data}}0{{/if}}', 224 | array('data' => true), 225 | '0' 226 | ), 227 | array( 228 | '{{#if data}}Yes{{/if}}', 229 | array('data' => false), 230 | '' 231 | ), 232 | array( 233 | '{{#with data}}{{key}}{{/with}}', 234 | array('data' => array('key' => 'result')), 235 | 'result' 236 | ), 237 | array( 238 | '{{#each data}}{{this}}{{/each}}', 239 | array('data' => array(1, 2, 3, 4)), 240 | '1234' 241 | ), 242 | array( 243 | '{{#each data}}{{@key}}{{/each}}', 244 | array('data' => array('the_only_key' => 1)), 245 | 'the_only_key' 246 | ), 247 | array( 248 | '{{#each data}}{{@key}}=>{{this}}{{/each}}', 249 | array('data' => array('key1' => 1, 'key2' => 2,)), 250 | 'key1=>1key2=>2' 251 | ), 252 | array( 253 | '{{#each data}}{{@key}}=>{{this}}{{/each}}', 254 | array('data' => new \ArrayIterator(array('key1' => 1, 'key2' => 2))), 255 | 'key1=>1key2=>2' 256 | ), 257 | array( 258 | '{{#each data}}{{@index}}=>{{this}},{{/each}}', 259 | array('data' => array('key1' => 1, 'key2' => 2,)), 260 | '0=>1,1=>2,' 261 | ), 262 | array( 263 | '{{#each data}}{{#if @first}}the first is {{this}}{{/if}}{{/each}}', 264 | array('data' => array('one', 'two', 'three')), 265 | 'the first is one' 266 | ), 267 | array( 268 | '{{#each data}}{{#if @last}}the last is {{this}}{{/if}}{{/each}}', 269 | array('data' => array('one', 'two', 'three')), 270 | 'the last is three' 271 | ), 272 | array( 273 | '{{#each data}}{{this}}{{else}}fail{{/each}}', 274 | array('data' => array(1, 2, 3, 4)), 275 | '1234' 276 | ), 277 | array( 278 | '{{#each data}}fail{{else}}ok{{/each}}', 279 | array('data' => false), 280 | 'ok' 281 | ), 282 | array( 283 | '{{#unless data}}ok{{/unless}}', 284 | array('data' => true), 285 | '' 286 | ), 287 | array( 288 | '{{#unless data}}ok{{/unless}}', 289 | array('data' => false), 290 | 'ok' 291 | ), 292 | array( 293 | '{{#unless data}}ok{{else}}fail{{/unless}}', 294 | array('data' => false), 295 | 'ok' 296 | ), 297 | array( 298 | '{{#unless data}}fail{{else}}ok{{/unless}}', 299 | array('data' => true), 300 | 'ok' 301 | ), 302 | array( 303 | '{{#bindAttr data}}', 304 | array(), 305 | 'data' 306 | ), 307 | array( 308 | '{{#if 1}}ok{{else}}fail{{/if}}', 309 | array(), 310 | 'ok' 311 | ), 312 | array( 313 | '{{#if 0}}ok{{else}}fail{{/if}}', 314 | array(), 315 | 'fail' 316 | ), 317 | array( 318 | ' {{~#if 1}}OK {{~else~}} NO {{~/if~}} END', 319 | array(), 320 | 'OKEND' 321 | ), 322 | array( 323 | 'XX {{~#bindAttr data}} XX', 324 | array(), 325 | 'XXdata XX' 326 | ), 327 | array( 328 | '{{#each data}}{{#if @last}}the last is 329 | {{~this}}{{/if}}{{/each}}', 330 | array('data' => array('one', 'two', 'three')), 331 | 'the last isthree' 332 | ), 333 | array( 334 | '{{#with data}} 335 | 336 | {{~key~}} 337 | 338 | {{/with}}', 339 | array('data' => array('key' => 'result')), 340 | 'result' 341 | ), 342 | array( 343 | '{{= (( )) =}}((#if 1))OK((else))NO((/if))', 344 | array(), 345 | 'OK' 346 | ), 347 | array( 348 | '{{#each data~}} {{this}} {{~/each}}', 349 | array('data' => array(1, 2, 3, 4)), 350 | '1234' 351 | ), 352 | array( 353 | '{{#each data}}{{this}} {{~/each}}', 354 | array('data' => array(1, 2, 3, 4)), 355 | '1234' 356 | ), 357 | array( 358 | '{{#each data~}} {{this}}{{/each}}', 359 | array('data' => array(1, 2, 3, 4)), 360 | '1234' 361 | ), 362 | array('{{#if first}}The first{{else}}{{#if second}}The second{{/if}}{{/if}}', 363 | array('first' => false, 'second' => true), 364 | 'The second' 365 | ) 366 | ); 367 | } 368 | 369 | /** 370 | * Management helpers 371 | */ 372 | public function testHelpersManagement() 373 | { 374 | $helpers = new \Handlebars\Helpers(array('test' => function () { 375 | }), false); 376 | $engine = new \Handlebars\Handlebars(array('helpers' => $helpers)); 377 | $this->assertTrue(is_callable($engine->getHelper('test'))); 378 | $this->assertTrue($engine->hasHelper('test')); 379 | $engine->removeHelper('test'); 380 | $this->assertFalse($engine->hasHelper('test')); 381 | } 382 | 383 | /** 384 | * Custom helper test 385 | */ 386 | public function testCustomHelper() 387 | { 388 | $loader = new \Handlebars\Loader\StringLoader(); 389 | $engine = new \Handlebars\Handlebars(array('loader' => $loader)); 390 | $engine->addHelper('test', function () { 391 | return 'Test helper is called'; 392 | }); 393 | $this->assertEquals('Test helper is called', $engine->render('{{#test}}', array())); 394 | $this->assertEquals('Test helper is called', $engine->render('{{test}}', array())); 395 | 396 | $engine->addHelper('test2', function ($template, $context, $arg) { 397 | return 'Test helper is called with ' . $arg; 398 | }); 399 | $this->assertEquals('Test helper is called with a b c', $engine->render('{{#test2 a b c}}', array())); 400 | $this->assertEquals('Test helper is called with a b c', $engine->render('{{test2 a b c}}', array())); 401 | 402 | $engine->addHelper('renderme', function () { 403 | return new \Handlebars\StringWrapper("{{test}}"); 404 | }); 405 | $this->assertEquals('Test helper is called', $engine->render('{{#renderme}}', array())); 406 | 407 | $engine->addHelper('dontrenderme', function () { 408 | return "{{test}}"; 409 | }); 410 | $this->assertEquals('{{test}}', $engine->render('{{#dontrenderme}}', array())); 411 | 412 | $engine->addHelper('markupHelper', function () { 413 | return 'Test'; 414 | }); 415 | $this->assertEquals('Test', $engine->render('{{{markupHelper}}}', array())); 416 | $this->assertEquals('<strong>Test</strong>', $engine->render('{{markupHelper}}', array())); 417 | 418 | $engine->addHelper('safeStringTest', function () { 419 | return new \Handlebars\SafeString('Test'); 420 | }); 421 | $this->assertEquals('Test', $engine->render('{{safeStringTest}}', array())); 422 | 423 | $engine->addHelper('argsTest', function ($template, $context, $arg) { 424 | $parsedArgs = $template->parseArguments($arg); 425 | 426 | return implode(' ', $parsedArgs); 427 | }); 428 | $this->assertEquals("a \"b\" c", $engine->render('{{{argsTest "a" "\"b\"" \'c\'}}}', array())); 429 | 430 | // This is just a fun thing to do :) 431 | $that = $this; 432 | $engine->addHelper('stopToken', 433 | function ($template, $context, $arg) use ($that) { 434 | /** @var $template \Handlebars\Template */ 435 | $parsedArgs = $template->parseArguments($arg); 436 | $first = array_shift($parsedArgs); 437 | $last = array_shift($parsedArgs); 438 | if ($last == 'yes') { 439 | $template->setStopToken($first); 440 | $that->assertEquals($first, $template->getStopToken()); 441 | $buffer = $template->render($context); 442 | $template->setStopToken(false); 443 | $template->discard($context); 444 | } else { 445 | $template->setStopToken($first); 446 | $that->assertEquals($first, $template->getStopToken()); 447 | $template->discard($context); 448 | $template->setStopToken(false); 449 | $buffer = $template->render($context); 450 | } 451 | 452 | return $buffer; 453 | }); 454 | 455 | $this->assertEquals("Used", $engine->render('{{# stopToken fun no}}Not used{{ fun }}Used{{/stopToken }}', array())); 456 | $this->assertEquals("Not used", $engine->render('{{# stopToken any yes}}Not used{{ any }}Used{{/stopToken }}', array())); 457 | 458 | $this->setExpectedException('InvalidArgumentException'); 459 | $engine->getHelpers()->call('invalid', $engine->loadTemplate(''), new \Handlebars\Context(), '', ''); 460 | } 461 | 462 | public function testRegisterHelper() 463 | { 464 | $loader = new \Handlebars\Loader\StringLoader(); 465 | $engine = new \Handlebars\Handlebars(array('loader' => $loader)); 466 | //date_default_timezone_set('GMT'); 467 | 468 | //FIRST UP: some awesome helpers!! 469 | 470 | //translations 471 | $translations = array( 472 | 'hello' => 'bonjour', 473 | 'my name is %s' => 'mon nom est %s', 474 | 'how are your %s kids and %s' => 'comment sont les enfants de votre %s et %s' 475 | ); 476 | 477 | //i18n 478 | $engine->registerHelper('_', function($key) use ($translations) { 479 | $args = func_get_args(); 480 | $key = array_shift($args); 481 | $options = array_pop($args); 482 | 483 | //make sure it's a string 484 | $key = (string) $key; 485 | 486 | //by default the translation is the key 487 | $translation = $key; 488 | 489 | //if there is a translation 490 | if(isset($translations[$key])) { 491 | //translate it 492 | $translation = $translations[$key]; 493 | } 494 | 495 | //if there are more arguments 496 | if(!empty($args)) { 497 | //it means the translations was 498 | //something like 'Hello %s' 499 | return vsprintf($translation, $args); 500 | } 501 | 502 | //just return what we got 503 | return $translation; 504 | }); 505 | 506 | //create a better if helper 507 | $engine->registerHelper('when', function($value1, $operator, $value2, $options) { 508 | $valid = false; 509 | //the amazing reverse switch! 510 | switch (true) { 511 | case $operator == 'eq' && $value1 == $value2: 512 | case $operator == '==' && $value1 == $value2: 513 | case $operator == 'req' && $value1 === $value2: 514 | case $operator == '===' && $value1 === $value2: 515 | case $operator == 'neq' && $value1 != $value2: 516 | case $operator == '!=' && $value1 != $value2: 517 | case $operator == 'rneq' && $value1 !== $value2: 518 | case $operator == '!==' && $value1 !== $value2: 519 | case $operator == 'lt' && $value1 < $value2: 520 | case $operator == '<' && $value1 < $value2: 521 | case $operator == 'lte' && $value1 <= $value2: 522 | case $operator == '<=' && $value1 <= $value2: 523 | case $operator == 'gt' && $value1 > $value2: 524 | case $operator == '>' && $value1 > $value2: 525 | case $operator == 'gte' && $value1 >= $value2: 526 | case $operator == '>=' && $value1 >= $value2: 527 | case $operator == 'and' && $value1 && $value2: 528 | case $operator == '&&' && ($value1 && $value2): 529 | case $operator == 'or' && ($value1 || $value2): 530 | case $operator == '||' && ($value1 || $value2): 531 | $valid = true; 532 | break; 533 | } 534 | 535 | if($valid) { 536 | return $options['fn'](); 537 | } 538 | 539 | return $options['inverse'](); 540 | }); 541 | 542 | //a loop helper 543 | $engine->registerHelper('loop', function($object, $options) { 544 | //expected for subtemplates of this block to use 545 | // {{value.profile_name}} vs {{profile_name}} 546 | // {{key}} vs {{@index}} 547 | 548 | $i = 0; 549 | $buffer = array(); 550 | $total = count($object); 551 | 552 | //loop through the object 553 | foreach($object as $key => $value) { 554 | //call the sub template and 555 | //add it to the buffer 556 | $buffer[] = $options['fn'](array( 557 | 'key' => $key, 558 | 'value' => $value, 559 | 'last' => ++$i === $total 560 | )); 561 | } 562 | 563 | return implode('', $buffer); 564 | }); 565 | 566 | //array in 567 | $engine->registerHelper('in', function(array $array, $key, $options) { 568 | if(in_array($key, $array)) { 569 | return $options['fn'](); 570 | } 571 | 572 | return $options['inverse'](); 573 | }); 574 | 575 | //converts date formats to other formats 576 | $engine->registerHelper('date', function($time, $format, $options) { 577 | return date($format, strtotime($time)); 578 | }); 579 | 580 | //nesting helpers, these don't really help anyone :) 581 | $engine->registerHelper('nested1', function($test1, $test2, $options) { 582 | return $options['fn'](array( 583 | 'test4' => $test1, 584 | 'test5' => 'This is Test 5' 585 | )); 586 | }); 587 | 588 | $engine->registerHelper('nested2', function($options) { 589 | return $options['fn'](array('test6' => 'This is Test 6')); 590 | }); 591 | 592 | //NEXT UP: some practical case studies 593 | 594 | //case 1 - i18n 595 | $variable1 = array(); 596 | $template1 = "{{_ 'hello'}}, {{_ 'my name is %s' 'Foo'}}! {{_ 'how are your %s kids and %s' 6 'dog'}}?"; 597 | $expected1 = 'bonjour, mon nom est Foo! comment sont les enfants de votre 6 et dog?'; 598 | 599 | //case 2 - when 600 | $variable2 = array('gender' => 'female', 'foo' => 'bar'); 601 | $template2 = "Hello {{#when gender '===' 'male'}}sir{{else}}maam{{/when}} {{foo}}"; 602 | $expected2 = 'Hello maam bar'; 603 | 604 | //case 3 - when else 605 | $variable3 = array('gender' => 'male'); 606 | $template3 = "Hello {{#when gender '===' 'male'}}sir{{else}}maam{{/when}}"; 607 | $expected3 = 'Hello sir'; 608 | 609 | //case 4 - loop 610 | $variable4 = array( 611 | 'rows' => array( 612 | array( 613 | 'profile_name' => 'Jane Doe', 614 | 'profile_created' => '2014-04-04 00:00:00' 615 | ), 616 | array( 617 | 'profile_name' => 'John Doe', 618 | 'profile_created' => '2015-01-21 00:00:00' 619 | ) 620 | ) 621 | ); 622 | $template4 = "{{#loop rows}}
  • {{value.profile_name}} - {{date value.profile_created 'M d'}}
  • {{/loop}}"; 623 | $expected4 = '
  • Jane Doe - Apr 04
  • John Doe - Jan 21
  • '; 624 | 625 | //case 5 - array in 626 | $variable5 = $variable4; 627 | $variable5['me'] = 'Jack Doe'; 628 | $variable5['admins'] = array('Jane Doe', 'John Doe'); 629 | $template5 = "{{#in admins me}}
      ".$template4."
    {{else}}No Access{{/in}}{{suffix}}"; 630 | $expected5 = 'No Access'; 631 | 632 | //case 6 - array in else 633 | $variable6 = $variable5; 634 | $variable6['me'] = 'Jane Doe'; 635 | $variable6['suffix'] = 'qux'; 636 | $template6 = $template5; 637 | $expected6 = '
    • Jane Doe - Apr 04
    • John Doe - Jan 21
    qux'; 638 | 639 | //case 7 - nested templates and parent-grand variables 640 | $variable7 = array('test' => 'Hello World'); 641 | $template7 = '{{#nested1 test "test2"}} ' 642 | .'In 1: {{test4}} {{#nested1 ../test \'test3\'}} ' 643 | .'In 2: {{test5}}{{#nested2}} ' 644 | .'In 3: {{test6}} {{../../../test}}{{/nested2}}{{/nested1}}{{/nested1}}'; 645 | $expected7 = ' In 1: Hello World In 2: This is Test 5 In 3: This is Test 6 Hello World'; 646 | 647 | //case 8 - when inside an each 648 | $variable8 = array('data' => array(0, 1, 2, 3),'finish' => 'ok'); 649 | $template8 = '{{#each data}}{{#when this ">" "0"}}{{this}}{{/when}}{{/each}} {{finish}}'; 650 | $expected8 = '123 ok'; 651 | 652 | //case 9 - when inside an each 653 | $variable9 = array('data' => array(),'finish' => 'ok'); 654 | $template9 = '{{#each data}}{{#when this ">" "0"}}{{this}}{{/when}}{{else}}foo{{/each}} {{finish}}'; 655 | $expected9 = 'foo ok'; 656 | 657 | //LAST UP: the actual testing 658 | 659 | $this->assertEquals($expected1, $engine->render($template1, $variable1)); 660 | $this->assertEquals($expected2, $engine->render($template2, $variable2)); 661 | $this->assertEquals($expected3, $engine->render($template3, $variable3)); 662 | $this->assertEquals($expected4, $engine->render($template4, $variable4)); 663 | $this->assertEquals($expected5, $engine->render($template5, $variable5)); 664 | $this->assertEquals($expected6, $engine->render($template6, $variable6)); 665 | $this->assertEquals($expected7, $engine->render($template7, $variable7)); 666 | $this->assertEquals($expected8, $engine->render($template8, $variable8)); 667 | $this->assertEquals($expected9, $engine->render($template9, $variable9)); 668 | } 669 | 670 | public function testInvalidHelper() 671 | { 672 | $this->setExpectedException('RuntimeException'); 673 | $loader = new \Handlebars\Loader\StringLoader(); 674 | $engine = new \Handlebars\Handlebars(array('loader' => $loader)); 675 | $engine->render('{{#NOTVALID argument}}XXX{{/NOTVALID}}', array()); 676 | } 677 | 678 | /** 679 | * Test mustache style loop and if 680 | */ 681 | public function testMustacheStyle() 682 | { 683 | $loader = new \Handlebars\Loader\StringLoader(); 684 | $engine = new \Handlebars\Handlebars(array('loader' => $loader)); 685 | $this->assertEquals('yes', $engine->render('{{#x}}yes{{/x}}', array('x' => true))); 686 | $this->assertEquals('', $engine->render('{{#x}}yes{{/x}}', array('x' => false))); 687 | $this->assertEquals('', $engine->render('{{#NOTVALID}}XXX{{/NOTVALID}}', array())); 688 | $this->assertEquals('yes', $engine->render('{{^x}}yes{{/x}}', array('x' => false))); 689 | $this->assertEquals('', $engine->render('{{^x}}yes{{/x}}', array('x' => true))); 690 | $this->assertEquals('1234', $engine->render('{{#x}}{{this}}{{/x}}', array('x' => array(1, 2, 3, 4)))); 691 | $this->assertEquals('012', $engine->render('{{#x}}{{@index}}{{/x}}', array('x' => array('a', 'b', 'c')))); 692 | $this->assertEquals('123', $engine->render('{{#x}}{{a}}{{b}}{{c}}{{/x}}', array('x' => array('a' => 1, 'b' => 2, 'c' => 3)))); 693 | $this->assertEquals('1', $engine->render('{{#x}}{{the_only_key}}{{/x}}', array('x' => array('the_only_key' => 1)))); 694 | $std = new stdClass(); 695 | $std->value = 1; 696 | $std->other = 4; 697 | $this->assertEquals('1', $engine->render('{{#x}}{{value}}{{/x}}', array('x' => $std))); 698 | $this->assertEquals('1', $engine->render('{{{x}}}', array('x' => 1))); 699 | $this->assertEquals('1 2', $engine->render('{{#x}}{{value}} {{parent}}{{/x}}', array('x' => $std, 'parent' => 2))); 700 | 701 | $y = new stdClass(); 702 | $y->value = 2; 703 | $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))); 704 | } 705 | 706 | /** 707 | * @expectedException \LogicException 708 | */ 709 | public function testParserException() 710 | { 711 | $loader = new \Handlebars\Loader\StringLoader(); 712 | $engine = new \Handlebars\Handlebars(array('loader' => $loader)); 713 | $engine->render('{{#test}}{{#test2}}{{/test}}{{/test2}}', array()); 714 | } 715 | 716 | /** 717 | * Test add/addHelpers/get/getAll/has/clear functions on helper class 718 | */ 719 | public function testHelpersClass() 720 | { 721 | $helpers = new \Handlebars\Helpers(); 722 | $helpers->add('test', function () { 723 | }); 724 | $this->assertTrue($helpers->has('test')); 725 | $this->assertTrue(isset($helpers->test)); 726 | $this->assertFalse($helpers->isEmpty()); 727 | $helpers->test2 = function () { 728 | }; 729 | $this->assertTrue($helpers->has('test2')); 730 | $this->assertTrue(isset($helpers->test2)); 731 | $this->assertFalse($helpers->isEmpty()); 732 | unset($helpers->test2); 733 | $this->assertFalse($helpers->has('test2')); 734 | $this->assertFalse(isset($helpers->test2)); 735 | $helpers->clear(); 736 | $this->assertFalse($helpers->has('test')); 737 | $this->assertFalse(isset($helpers->test)); 738 | $this->assertTrue($helpers->isEmpty()); 739 | $helpers->add('test', function () { 740 | }); 741 | $this->assertCount(0, array_diff(array_keys($helpers->getAll()), array('test'))); 742 | $extraHelpers = new \Handlebars\Helpers(); 743 | $extraHelpers->add('test', function () { 744 | }); 745 | $extraHelpers->add('test2', function () { 746 | }); 747 | $helpers->addHelpers($extraHelpers); 748 | $this->assertTrue($helpers->has('test2')); 749 | $this->assertEquals($helpers->test, $extraHelpers->test); 750 | } 751 | 752 | /** 753 | * @expectedException \InvalidArgumentException 754 | */ 755 | public function testHelperWrongConstructor() 756 | { 757 | $helper = new \Handlebars\Helpers("helper"); 758 | } 759 | 760 | /** 761 | * @expectedException \InvalidArgumentException 762 | */ 763 | public function testHelperWrongCallable() 764 | { 765 | $helper = new \Handlebars\Helpers(); 766 | $helper->add('test', 1); 767 | } 768 | 769 | /** 770 | * @expectedException \InvalidArgumentException 771 | */ 772 | public function testHelperWrongGet() 773 | { 774 | $helper = new \Handlebars\Helpers(); 775 | $x = $helper->test; 776 | } 777 | 778 | /** 779 | * @expectedException \InvalidArgumentException 780 | */ 781 | public function testHelperWrongUnset() 782 | { 783 | $helper = new \Handlebars\Helpers(); 784 | unset($helper->test); 785 | } 786 | 787 | /** 788 | * test String class 789 | */ 790 | public function testStringWrapperClass() 791 | { 792 | $string = new \Handlebars\StringWrapper('test'); 793 | $this->assertEquals('test', $string->getString()); 794 | $string->setString('new'); 795 | $this->assertEquals('new', $string->getString()); 796 | } 797 | 798 | /** 799 | * test SafeString class 800 | */ 801 | public function testSafeStringClass() 802 | { 803 | $loader = new \Handlebars\Loader\StringLoader(); 804 | $helpers = new \Handlebars\Helpers(); 805 | $engine = new \Handlebars\Handlebars(array('loader' => $loader, 'helpers' => $helpers)); 806 | 807 | $this->assertEquals('Test', $engine->render('{{string}}', array( 808 | 'string' => new \Handlebars\SafeString('Test') 809 | ))); 810 | } 811 | 812 | /** 813 | * @param $dir 814 | * 815 | * @return bool 816 | */ 817 | private function delTree($dir) 818 | { 819 | $files = array_diff(scandir($dir), array('.', '..')); 820 | foreach ($files as $file) { 821 | (is_dir("$dir/$file")) ? $this->delTree("$dir/$file") : unlink("$dir/$file"); 822 | } 823 | 824 | return rmdir($dir); 825 | } 826 | 827 | /** 828 | * Its not a good test :) but ok 829 | */ 830 | public function testCacheSystem() 831 | { 832 | $path = sys_get_temp_dir() . '/__cache__handlebars'; 833 | 834 | @$this->delTree($path); 835 | 836 | $dummy = new \Handlebars\Cache\Disk($path); 837 | $engine = new \Handlebars\Handlebars(array('cache' => $dummy)); 838 | $this->assertEquals(0, count(glob($path . '/*'))); 839 | $engine->render('test', array()); 840 | $this->assertEquals(1, count(glob($path . '/*'))); 841 | } 842 | 843 | public function testArrayLoader() 844 | { 845 | $loader = new \Handlebars\Loader\ArrayLoader(array('test' => 'HELLO')); 846 | $loader->addTemplate('another', 'GOODBYE'); 847 | $engine = new \Handlebars\Handlebars(array('loader' => $loader)); 848 | $this->assertEquals($engine->render('test', array()), 'HELLO'); 849 | $this->assertEquals($engine->render('another', array()), 'GOODBYE'); 850 | 851 | $this->setExpectedException('RuntimeException'); 852 | $engine->render('invalid-template', array()); 853 | } 854 | 855 | /** 856 | * Test file system loader 857 | */ 858 | public function testFileSystemLoader() 859 | { 860 | $loader = new \Handlebars\Loader\FilesystemLoader(realpath(__DIR__ . '/../fixture/data')); 861 | $engine = new \Handlebars\Handlebars(); 862 | $engine->setLoader($loader); 863 | $this->assertEquals('test', $engine->render('loader', array())); 864 | } 865 | 866 | /** 867 | * Test inline loader 868 | */ 869 | public function testInlineLoader() 870 | { 871 | $loader = new \Handlebars\Loader\InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__); 872 | $this->assertEquals('This is a inline template.', $loader->load('template1')); 873 | 874 | $expected = <<assertEquals($expected, $loader->load('template2')); 881 | } 882 | 883 | /** 884 | * Test file system loader 885 | */ 886 | public function testFileSystemLoaderMultipleFolder() 887 | { 888 | $paths = array( 889 | realpath(__DIR__ . '/../fixture/data'), 890 | realpath(__DIR__ . '/../fixture/another') 891 | ); 892 | 893 | $options = array( 894 | 'prefix' => '__', 895 | 'extension' => 'hb' 896 | ); 897 | $loader = new \Handlebars\Loader\FilesystemLoader($paths, $options); 898 | $engine = new \Handlebars\Handlebars(); 899 | $engine->setLoader($loader); 900 | $this->assertEquals('test_extra', $engine->render('loader', array())); 901 | $this->assertEquals('another_extra', $engine->render('another', array())); 902 | } 903 | 904 | /** 905 | * Test file system loader 906 | * 907 | * @expectedException \InvalidArgumentException 908 | */ 909 | public function testFileSystemLoaderNotFound() 910 | { 911 | $loader = new \Handlebars\Loader\FilesystemLoader(realpath(__DIR__ . '/../fixture/data')); 912 | $engine = new \Handlebars\Handlebars(); 913 | $engine->setLoader($loader); 914 | $engine->render('invalid_file', array()); 915 | } 916 | 917 | /** 918 | * Test file system loader 919 | * 920 | * @expectedException \RuntimeException 921 | */ 922 | public function testFileSystemLoaderInvalidFolder() 923 | { 924 | new \Handlebars\Loader\FilesystemLoader(realpath(__DIR__ . '/../fixture/') . 'invalid/path'); 925 | } 926 | 927 | /** 928 | * Test partial loader 929 | */ 930 | public function testPartialLoader() 931 | { 932 | $loader = new \Handlebars\Loader\StringLoader(); 933 | $partialLoader = new \Handlebars\Loader\FilesystemLoader(realpath(__DIR__ . '/../fixture/data')); 934 | $engine = new \Handlebars\Handlebars(); 935 | $engine->setLoader($loader); 936 | $engine->setPartialsLoader($partialLoader); 937 | 938 | $this->assertEquals('test', $engine->render('{{>loader}}', array())); 939 | } 940 | 941 | public function testPartial() 942 | { 943 | $loader = new \Handlebars\Loader\StringLoader(); 944 | $partialLoader = new \Handlebars\Loader\ArrayLoader(array( 945 | 'test' => '{{key}}', 946 | 'bar' => 'its foo', 947 | 'presetVariables' => '{{myVar}}', 948 | )); 949 | $partialAliasses = array('foo' => 'bar'); 950 | $engine = new \Handlebars\Handlebars( 951 | array( 952 | 'loader' => $loader, 953 | 'partials_loader' => $partialLoader, 954 | 'partials_alias' => $partialAliasses 955 | ) 956 | ); 957 | 958 | $this->assertEquals('foobar', $engine->render("{{>presetVariables myVar='foobar'}}", array())); 959 | $this->assertEquals('foobar=barbaz', $engine->render("{{>presetVariables myVar='foobar=barbaz'}}", array())); 960 | $this->assertEquals('qux', $engine->render("{{>presetVariables myVar=foo}}", array('foo' => 'qux'))); 961 | $this->assertEquals('qux', $engine->render("{{>presetVariables myVar=foo.bar}}", array('foo' => array('bar' => 'qux')))); 962 | 963 | $this->assertEquals('HELLO', $engine->render('{{>test parameter}}', array('parameter' => array('key' => 'HELLO')))); 964 | $this->assertEquals('its foo', $engine->render('{{>foo}}', array())); 965 | $engine->registerPartial('foo-again', 'bar'); 966 | $this->assertEquals('its foo', $engine->render('{{>foo-again}}', array())); 967 | $engine->unRegisterPartial('foo-again'); 968 | 969 | $this->setExpectedException('RuntimeException'); 970 | $engine->render('{{>foo-again}}', array()); 971 | 972 | } 973 | 974 | /** 975 | * test variable access 976 | */ 977 | public function testVariableAccess() 978 | { 979 | $loader = new \Handlebars\Loader\StringLoader(); 980 | $engine = \Handlebars\Handlebars::factory(); 981 | $engine->setLoader($loader); 982 | 983 | $var = new \StdClass(); 984 | $var->x = 'var-x'; 985 | $var->y = array( 986 | 'z' => 'var-y-z' 987 | ); 988 | $this->assertEquals('test', $engine->render('{{var}}', array('var' => 'test'))); 989 | $this->assertEquals('var-x', $engine->render('{{var.x}}', array('var' => $var))); 990 | $this->assertEquals('var-y-z', $engine->render('{{var.y.z}}', array('var' => $var))); 991 | // Access parent context in with helper 992 | $this->assertEquals('var-x', $engine->render('{{#with var.y}}{{../var.x}}{{/with}}', array('var' => $var))); 993 | // Reference array as string 994 | $this->assertEquals('Array', $engine->render('{{var}}', array('var' => array('test')))); 995 | 996 | // Test class with __toString method 997 | $this->assertEquals('test', $engine->render('{{var}}', array('var' => new TestClassWithToStringMethod()))); 998 | 999 | $obj = new DateTime(); 1000 | $time = $obj->getTimestamp(); 1001 | $this->assertEquals($time, $engine->render('{{time.getTimestamp}}', array('time' => $obj))); 1002 | } 1003 | 1004 | public function testContext() 1005 | { 1006 | $test = new stdClass(); 1007 | $test->value = 'value'; 1008 | $test->array = array( 1009 | 'a' => '1', 1010 | 'b' => '2', 1011 | '!"#%&\'()*+,./;<=>@[\\^`{|}~ ' => '3', 1012 | ); 1013 | $context = new \Handlebars\Context($test); 1014 | $this->assertEquals('value', $context->get('value')); 1015 | $this->assertEquals('value', $context->get('value', true)); 1016 | $this->assertEquals('value', $context->get('[value]', true)); 1017 | $this->assertEquals('1', $context->get('array.a', true)); 1018 | $this->assertEquals('2', $context->get('array.b', true)); 1019 | $this->assertEquals('3', $context->get('array.[!"#%&\'()*+,./;<=>@[\\^`{|}~ ]', true)); 1020 | $new = array('value' => 'new value'); 1021 | $context->push($new); 1022 | $this->assertEquals('new value', $context->get('value')); 1023 | $this->assertEquals('new value', $context->get('value', true)); 1024 | $this->assertEquals('value', $context->get('../value')); 1025 | $this->assertEquals('value', $context->get('../value', true)); 1026 | $this->assertEquals($new, $context->last()); 1027 | $this->assertEquals($new, $context->get('.')); 1028 | $this->assertEquals($new, $context->get('this')); 1029 | $this->assertEquals($new, $context->get('this.')); 1030 | $this->assertEquals($test, $context->get('../.')); 1031 | $context->pop(); 1032 | $this->assertEquals('value', $context->get('value')); 1033 | $this->assertEquals('value', $context->get('value', true)); 1034 | } 1035 | 1036 | /** 1037 | * @expectedException \InvalidArgumentException 1038 | * @dataProvider getInvalidData 1039 | */ 1040 | public function testInvalidAccessContext($invalid) 1041 | { 1042 | $context = new \Handlebars\Context(array()); 1043 | $this->assertEmpty($context->get($invalid)); 1044 | $context->get($invalid, true); 1045 | } 1046 | 1047 | public function getInvalidData() 1048 | { 1049 | return array( 1050 | array('../../data'), 1051 | array('data'), 1052 | array(''), 1053 | array('data.key.key'), 1054 | ); 1055 | } 1056 | 1057 | 1058 | /** 1059 | * Test invalid custom template class 1060 | * 1061 | * @expectedException \InvalidArgumentException 1062 | */ 1063 | public function testInvalidCustomTemplateClass() 1064 | { 1065 | $loader = new \Handlebars\Loader\StringLoader(); 1066 | $engine = new \Handlebars\Handlebars(array( 1067 | 'loader' => $loader, 1068 | 'template_class' => 'stdclass' 1069 | )); 1070 | } 1071 | 1072 | /** 1073 | * Test custom template class 1074 | */ 1075 | public function testValidCustomTemplateClass() 1076 | { 1077 | Handlebars\Autoloader::register(realpath(__DIR__ . '/../fixture/')); 1078 | 1079 | $loader = new \Handlebars\Loader\StringLoader(); 1080 | $engine = new \Handlebars\Handlebars(array( 1081 | 'loader' => $loader, 1082 | 'template_class' => 'Handlebars\CustomTemplate' 1083 | )); 1084 | 1085 | $render = $engine->render('Original Template', array()); 1086 | 1087 | $this->assertEquals($render, 'Altered Template'); 1088 | } 1089 | 1090 | /** 1091 | * Test for proper handling of the length property 1092 | **/ 1093 | public function testArrayLengthEmulation() 1094 | { 1095 | 1096 | $data = array("numbers" => array(1, 2, 3, 4), 1097 | "object" => (object)array("prop1" => "val1", "prop2" => "val2"), 1098 | "object_with_length_property" => (object)array("length" => "15cm") 1099 | ); 1100 | $context = new \Handlebars\Context($data); 1101 | // make sure we are getting the array length when given an array 1102 | $this->assertEquals($context->get("numbers.length"), 4); 1103 | // make sure we are not getting a length when given an object 1104 | $this->assertEmpty($context->get("object.length")); 1105 | // make sure we can still get the length property when given an object 1106 | $this->assertEquals($context->get("object_with_length_property.length"), "15cm"); 1107 | } 1108 | 1109 | public function argumentParserProvider() 1110 | { 1111 | return array( 1112 | array('arg1 arg2', array("arg1", "arg2")), 1113 | array('"arg1 arg2"', array("arg1 arg2")), 1114 | array('arg1 arg2 "arg number 3"', array("arg1", "arg2", "arg number 3")), 1115 | array('arg1 "arg\"2" "\"arg3\""', array("arg1", 'arg"2', '"arg3"')), 1116 | array("'arg1 arg2'", array("arg1 arg2")), 1117 | array("arg1 arg2 'arg number 3'", array("arg1", "arg2", "arg number 3")), 1118 | array('arg1 "arg\"2" "\\\'arg3\\\'"', array("arg1", 'arg"2', "'arg3'")), 1119 | array('arg1 arg2.[value\'s "segment"].val', array("arg1", 'arg2.[value\'s "segment"].val')), 1120 | array('"arg1.[value 1]" arg2', array("arg1.[value 1]", 'arg2')), 1121 | array('0', array('0')), 1122 | ); 1123 | } 1124 | 1125 | /** 1126 | * Test Argument Parser 1127 | * 1128 | * @param string $arg_string argument text 1129 | * @param $expected_array 1130 | * 1131 | * @dataProvider argumentParserProvider 1132 | * 1133 | * @return void 1134 | */ 1135 | public function testArgumentParser($arg_string, $expected_array) 1136 | { 1137 | $engine = new \Handlebars\Handlebars(); 1138 | $template = new \Handlebars\Template($engine, null, null); 1139 | // get the string version of the arguments array 1140 | $args = $template->parseArguments($arg_string); 1141 | $args = array_map(function ($a) { 1142 | return (string)$a; 1143 | }, $args); 1144 | $this->assertEquals($args, $expected_array); 1145 | } 1146 | 1147 | public function namedArgumentParserProvider() 1148 | { 1149 | return array( 1150 | array('arg1="value" arg2="value 2"', array('arg1' => 'value', 'arg2' => 'value 2')), 1151 | array('arg1=var1', array('arg1' => 'var1')), 1152 | array('[arg "1"]="arg number 1"', array('arg "1"' => "arg number 1")), 1153 | array('arg1 = "value"', array('arg1' => "value")), 1154 | ); 1155 | } 1156 | 1157 | /** 1158 | * Test Named Argument Parser 1159 | * 1160 | * @param string $arg_string argument text 1161 | * @param $expected_array 1162 | * 1163 | * @dataProvider namedArgumentParserProvider 1164 | * 1165 | * @return void 1166 | */ 1167 | public function testNamedArgumentsParser($arg_string, $expected_array) 1168 | { 1169 | $engine = new \Handlebars\Handlebars(); 1170 | $template = new \Handlebars\Template($engine, null, null); 1171 | // get the string version of the arguments array 1172 | $args = $template->parseNamedArguments($arg_string); 1173 | $args = array_map(function ($a) { 1174 | return (string)$a; 1175 | }, $args); 1176 | $this->assertEquals($args, $expected_array); 1177 | } 1178 | 1179 | /** 1180 | * Test Combined Arguments Parser 1181 | * 1182 | * @param string $arg_string argument text 1183 | * @param $positional_args 1184 | * @param $named_args 1185 | * 1186 | * @dataProvider combinedArgumentsParserProvider 1187 | * 1188 | * @return void 1189 | */ 1190 | public function testCombinedArgumentsParser($arg_string, $positional_args, $named_args) 1191 | { 1192 | $args = new \Handlebars\Arguments($arg_string); 1193 | 1194 | // Get the string version of the arguments array 1195 | $stringify = function ($a) { 1196 | return (string)$a; 1197 | }; 1198 | 1199 | if ($positional_args !== false) { 1200 | $this->assertEquals( 1201 | array_map($stringify, $args->getPositionalArguments()), 1202 | $positional_args 1203 | ); 1204 | } 1205 | if ($named_args !== false) { 1206 | $this->assertEquals( 1207 | array_map($stringify, $args->getNamedArguments()), 1208 | $named_args 1209 | ); 1210 | } 1211 | } 1212 | 1213 | public function combinedArgumentsParserProvider() 1214 | { 1215 | $result = array(); 1216 | 1217 | // Use data provider for positional arguments parser 1218 | foreach ($this->argumentParserProvider() as $dataSet) { 1219 | $result[] = array( 1220 | $dataSet[0], 1221 | $dataSet[1], 1222 | false, 1223 | ); 1224 | } 1225 | 1226 | // Use data provider for named arguments parser 1227 | foreach ($this->namedArgumentParserProvider() as $dataSet) { 1228 | $result[] = array( 1229 | $dataSet[0], 1230 | false, 1231 | $dataSet[1], 1232 | ); 1233 | } 1234 | 1235 | // Add test cases with combined arguments 1236 | return array_merge( 1237 | $result, 1238 | array( 1239 | array( 1240 | 'arg1 arg2 arg3=value1 arg4="value2"', 1241 | array('arg1', 'arg2'), 1242 | array('arg3' => 'value1', 'arg4' => 'value2') 1243 | ), 1244 | array( 1245 | '@first arg=@last', 1246 | array('@first'), 1247 | array('arg' => '@last') 1248 | ), 1249 | array( 1250 | '[seg arg1] [seg arg2] = [seg value "1"]', 1251 | array('[seg arg1]'), 1252 | array('seg arg2' => '[seg value "1"]') 1253 | ) 1254 | ) 1255 | ); 1256 | } 1257 | 1258 | public function stringLiteralInCustomHelperProvider() 1259 | { 1260 | return array( 1261 | array('{{#test2 arg1 "Argument 2"}}', array("arg1" => "Argument 1"), "Argument 1:Argument 2"), 1262 | array('{{#test2 "Argument 1" "Argument 2"}}', array("arg1" => "Argument 1"), "Argument 1:Argument 2"), 1263 | array('{{#test2 "Argument 1" arg2}}', array("arg2" => "Argument 2"), "Argument 1:Argument 2") 1264 | ); 1265 | } 1266 | 1267 | /** 1268 | * Test String literals in the context of a helper 1269 | * 1270 | * @param string $template template text 1271 | * @param array $data context data 1272 | * @param string $results The Expected Results 1273 | * 1274 | * @dataProvider stringLiteralInCustomHelperProvider 1275 | * 1276 | * @return void 1277 | */ 1278 | public function testStringLiteralInCustomHelper($template, $data, $results) 1279 | { 1280 | $engine = new \Handlebars\Handlebars(); 1281 | $engine->addHelper('test2', function ($template, $context, $args) { 1282 | $args = $template->parseArguments($args); 1283 | 1284 | $args = array_map(function ($a) use ($context) { 1285 | return $context->get($a); 1286 | }, $args); 1287 | 1288 | return implode(':', $args); 1289 | }); 1290 | $res = $engine->render($template, $data); 1291 | $this->assertEquals($res, $results); 1292 | } 1293 | 1294 | public function integerLiteralInCustomHelperProvider() 1295 | { 1296 | return array( 1297 | array('{{test -5}}', array(), '-5'), 1298 | array('{{test 0}}', array(), '0'), 1299 | array('{{test 12}}', array(), '12'), 1300 | array('{{test 12 [12]}}', array('12' => 'foo'), '12:foo'), 1301 | ); 1302 | } 1303 | 1304 | /** 1305 | * Test integer literals in the context of a helper 1306 | * 1307 | * @param string $template template text 1308 | * @param array $data context data 1309 | * @param string $results The Expected Results 1310 | * 1311 | * @dataProvider integerLiteralInCustomHelperProvider 1312 | * 1313 | * @return void 1314 | */ 1315 | public function testIntegerLiteralInCustomHelper($template, $data, $results) 1316 | { 1317 | $engine = new \Handlebars\Handlebars(); 1318 | $engine->addHelper('test', function ($template, $context, $args) { 1319 | $args = $template->parseArguments($args); 1320 | 1321 | $args = array_map(function ($a) use ($context) { 1322 | return $context->get($a); 1323 | }, $args); 1324 | 1325 | return implode(':', $args); 1326 | }); 1327 | $res = $engine->render($template, $data); 1328 | $this->assertEquals($res, $results); 1329 | } 1330 | 1331 | public function testString() 1332 | { 1333 | $string = new \Handlebars\StringWrapper("Hello World"); 1334 | $this->assertEquals((string)$string, "Hello World"); 1335 | } 1336 | 1337 | public function testInvalidNames() 1338 | { 1339 | $loader = new \Handlebars\Loader\StringLoader(); 1340 | $engine = new \Handlebars\Handlebars( 1341 | array( 1342 | 'loader' => $loader, 1343 | ) 1344 | ); 1345 | $all = \Handlebars\Context::NOT_VALID_NAME_CHARS; 1346 | 1347 | for ($i = 0; $i < strlen($all); $i++) { 1348 | // Dot in string is valid, its an exception here 1349 | if ($all{$i} === '.') { 1350 | continue; 1351 | } 1352 | try { 1353 | $name = 'var' . $all{$i} . 'var'; 1354 | $engine->render('{{' . $name . '}}', array($name => 'VALUE')); 1355 | throw new Exception("Accept the $name :/"); 1356 | } catch (Exception $e) { 1357 | $this->assertInstanceOf("InvalidArgumentException", $e); 1358 | } 1359 | } 1360 | } 1361 | 1362 | /** 1363 | * Helper subexpressions test 1364 | */ 1365 | public function testHelperSubexpressions() 1366 | { 1367 | $loader = new \Handlebars\Loader\StringLoader(); 1368 | $engine = new \Handlebars\Handlebars(array('loader' => $loader)); 1369 | $engine->addHelper('test', function ($template, $context, $args) { 1370 | $parsedArgs = $template->parseArguments($args); 1371 | 1372 | return (count($parsedArgs) ? $context->get($parsedArgs[0]) : '') . 'Test.'; 1373 | }); 1374 | 1375 | // assert that nested syntax is accepted and sub-helper is run 1376 | $this->assertEquals('Test.Test.', $engine->render('{{test (test)}}', array())); 1377 | 1378 | $engine->addHelper('add', function ($template, $context, $args) { 1379 | $sum = 0; 1380 | 1381 | foreach ($template->parseArguments($args) as $value) { 1382 | $sum += intval($context->get($value)); 1383 | } 1384 | 1385 | return $sum; 1386 | }); 1387 | 1388 | // assert that subexpression result is inserted correctly as argument to top level helper 1389 | $this->assertEquals('42', $engine->render('{{add 21 (add 10 (add 5 6))}}', array())); 1390 | 1391 | // assert that bracketed expressions within string literals are treated correctly 1392 | $this->assertEquals("(test)Test.", $engine->render("{{test '(test)'}}", array())); 1393 | $this->assertEquals(")Test.Test.", $engine->render("{{test (test ')')}}", array())); 1394 | 1395 | $engine->addHelper('concat', function (\Handlebars\Template $template, \Handlebars\Context $context, $args) { 1396 | $result = ''; 1397 | 1398 | foreach ($template->parseArguments($args) as $arg) { 1399 | $result .= $context->get($arg); 1400 | } 1401 | 1402 | return $result; 1403 | }); 1404 | 1405 | $this->assertEquals('ACB', $engine->render('{{concat a (concat c b)}}', array('a' => 'A', 'b' => 'B', 'c' => 'C'))); 1406 | $this->assertEquals('ACB', $engine->render('{{concat (concat a c) b}}', array('a' => 'A', 'b' => 'B', 'c' => 'C'))); 1407 | $this->assertEquals('A-B', $engine->render('{{concat (concat a "-") b}}', array('a' => 'A', 'b' => 'B'))); 1408 | $this->assertEquals('A-B', $engine->render('{{concat (concat a "-") b}}', array('a' => 'A', 'b' => 'B', 'A-' => '!'))); 1409 | } 1410 | 1411 | public function ifUnlessDepthDoesntChangeProvider() 1412 | { 1413 | return array(array( 1414 | '{{#with b}}{{#if this}}{{../a}}{{/if}}{{/with}}', 1415 | array('a' => 'good', 'b' => 'stump'), 1416 | 'good', 1417 | ), array( 1418 | '{{#with b}}{{#unless false}}{{../a}}{{/unless}}{{/with}}', 1419 | array('a' => 'good', 'b' => 'stump'), 1420 | 'good', 1421 | ), array( 1422 | '{{#with foo}}{{#if goodbye}}GOODBYE cruel {{../world}}!{{/if}}{{/with}}', 1423 | array('foo' => array('goodbye' => true), 'world' => 'world'), 1424 | 'GOODBYE cruel world!', 1425 | )); 1426 | } 1427 | 1428 | /** 1429 | * Test if and unless do not add an extra layer when accessing parent 1430 | * 1431 | * @dataProvider ifUnlessDepthDoesntChangeProvider 1432 | */ 1433 | public function testIfUnlessDepthDoesntChange($template, $data, $expected) 1434 | { 1435 | $loader = new \Handlebars\Loader\StringLoader(); 1436 | $engine = new \Handlebars\Handlebars(array('loader' => $loader)); 1437 | 1438 | $this->assertEquals($expected, $engine->render($template, $data)); 1439 | } 1440 | 1441 | /** 1442 | * Test of Arguments to string conversion. 1443 | */ 1444 | public function testArgumentsString() 1445 | { 1446 | $argsString = 'foo bar [foo bar] baz="value"'; 1447 | $args = new \Handlebars\Arguments($argsString); 1448 | $this->assertEquals($argsString, (string)$args); 1449 | } 1450 | 1451 | 1452 | public function stringLiteralsInIfAndUnlessHelpersProvider() 1453 | { 1454 | return array( 1455 | // IfHelper 1456 | array('{{#if "truthyString"}}true{{else}}false{{/if}}', array(), "true"), 1457 | array("{{#if 'truthyStringSingleQuotes'}}true{{else}}false{{/if}}", array(), "true"), 1458 | array("{{#if ''}}true{{else}}false{{/if}}", array(), "false"), 1459 | array("{{#if '0'}}true{{else}}false{{/if}}", array(), "false"), 1460 | array("{{#if (add 0 1)}}true{{else}}false{{/if}}", array(), "true"), 1461 | array("{{#if (add 1 -1)}}true{{else}}false{{/if}}", array(), "false"), 1462 | // UnlessHelper 1463 | array('{{#unless "truthyString"}}true{{else}}false{{/unless}}', array(), "false"), 1464 | array("{{#unless 'truthyStringSingleQuotes'}}true{{else}}false{{/unless}}", array(), "false"), 1465 | array("{{#unless ''}}true{{else}}false{{/unless}}", array(), "true"), 1466 | array("{{#unless '0'}}true{{else}}false{{/unless}}", array(), "true"), 1467 | array("{{#unless (add 0 1)}}true{{else}}false{{/unless}}", array(), "false"), 1468 | array("{{#unless (add 1 -1)}}true{{else}}false{{/unless}}", array(), "true"), 1469 | ); 1470 | } 1471 | 1472 | /** 1473 | * Test string literals in the context of if and unless helpers 1474 | * 1475 | * @param string $template template text 1476 | * @param array $data context data 1477 | * @param string $results The Expected Results 1478 | * 1479 | * @dataProvider stringLiteralsInIfAndUnlessHelpersProvider 1480 | * 1481 | * @return void 1482 | */ 1483 | public function testStringLiteralsInIfAndUnlessHelpers($template, $data, $results) 1484 | { 1485 | $engine = new \Handlebars\Handlebars(); 1486 | 1487 | $engine->addHelper('add', function ($template, $context, $args) { 1488 | $sum = 0; 1489 | 1490 | foreach ($template->parseArguments($args) as $value) { 1491 | $sum += intval($context->get($value)); 1492 | } 1493 | 1494 | return $sum; 1495 | }); 1496 | 1497 | $res = $engine->render($template, $data); 1498 | $this->assertEquals($res, $results); 1499 | } 1500 | 1501 | public function rootSpecialVariableProvider() 1502 | { 1503 | return array( 1504 | array('{{foo}} {{ @root.foo }}', array( 'foo' => 'bar' ), "bar bar"), 1505 | array('{{@root.foo}} {{#each arr}}{{ @root.foo }}{{/each}}', array( 'foo' => 'bar', 'arr' => array( '1' ) ), "bar bar"), 1506 | ); 1507 | } 1508 | 1509 | /** 1510 | * Test 'root' special variable 1511 | * 1512 | * @param string $template template text 1513 | * @param array $data context data 1514 | * @param string $results The Expected Results 1515 | * 1516 | * @dataProvider rootSpecialVariableProvider 1517 | * 1518 | * @return void 1519 | */ 1520 | public function testRootSpecialVariableHelpers($template, $data, $results) 1521 | { 1522 | $engine = new \Handlebars\Handlebars(); 1523 | 1524 | $res = $engine->render($template, $data); 1525 | $this->assertEquals($res, $results); 1526 | } 1527 | 1528 | } 1529 | 1530 | class TestClassWithToStringMethod { 1531 | public function __toString() { 1532 | return 'test'; 1533 | } 1534 | } 1535 | 1536 | /** 1537 | * Testcase for testInlineLoader 1538 | * 1539 | */ 1540 | __halt_compiler(); 1541 | @@ template1 1542 | This is a inline template. 1543 | 1544 | @@ template2 1545 | a 1546 | b 1547 | c 1548 | d 1549 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |