├── .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 | [](https://travis-ci.org/XaminProject/handlebars.php)
4 | [](https://scrutinizer-ci.com/g/XaminProject/handlebars.php/)
5 | [](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 04John 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}}{{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 |