1<?php
2/**
3 * Handlebars
4 *
5 * @category  Xamin
6 * @package   Handlebars
7 * @author    fzerorubigd <fzerorubigd@gmail.com>
8 * @author    Behrooz Shabani <everplays@gmail.com>
9 * @author    Mardix <https://github.com/mardix>
10 * @copyright 2012 (c) ParsPooyesh Co
11 * @copyright 2013 (c) Behrooz Shabani
12 * @copyright 2014 (c) Mardix
13 * @license   MIT
14 * @link      http://voodoophp.org/docs/handlebars
15 */
16
17namespace Handlebars;
18use Handlebars\Loader\StringLoader;
19use Handlebars\Cache\Dummy;
20use InvalidArgumentException;
21
22
23class Handlebars
24{
25    private static $instance = null;
26    const VERSION = '2.2';
27
28    const OPTION_ENABLE_DATA_VARIABLES = 'enableDataVariables';
29
30    /**
31     * factory method
32     *
33     * @param array $options see __construct's options parameter
34     *
35     * @return Handlebars
36     */
37    public static function factory($options = array())
38    {
39        if (! self::$instance) {
40            self::$instance = new self($options);
41        }
42
43        return self::$instance;
44    }
45
46    /**
47     * @var Tokenizer
48     */
49    private $tokenizer;
50
51    /**
52     * @var Parser
53     */
54    private $parser;
55
56    /**
57     * @var Helpers
58     */
59    private $helpers;
60
61    /**
62     * @var Loader
63     */
64    private $loader;
65
66    /**
67     * @var Loader
68     */
69    private $partialsLoader;
70
71    /**
72     * @var Cache
73     */
74    private $cache;
75
76    /**
77     * @var callable escape function to use
78     */
79    private $escape = 'htmlspecialchars';
80
81    /**
82     * @var array parametes to pass to escape function
83     */
84    private $escapeArgs = array(
85        ENT_COMPAT,
86        'UTF-8'
87    );
88
89    private $aliases = array();
90
91    /**
92     * @var bool Enable @data variables
93     */
94    private $enableDataVariables = false;
95
96    /**
97     * Handlebars engine constructor
98     * $options array can contain :
99     * helpers        => Helpers object
100     * escape         => a callable function to escape values
101     * escapeArgs     => array to pass as extra parameter to escape function
102     * loader         => Loader object
103     * partials_loader => Loader object
104     * cache          => Cache object
105     * enableDataVariables => boolean. Enables @data variables (default: false)
106     *
107     * @param array $options array of options to set
108     *
109     * @throws \InvalidArgumentException
110     */
111    public function __construct(Array $options = [])
112    {
113        if (isset($options['helpers'])) {
114            $this->setHelpers($options['helpers']);
115        }
116
117        if (isset($options['loader'])) {
118            $this->setLoader($options['loader']);
119        }
120
121        if (isset($options['partials_loader'])) {
122            $this->setPartialsLoader($options['partials_loader']);
123        }
124
125        if (isset($options['cache'])) {
126            $this->setCache($options['cache']);
127        }
128
129        if (isset($options['escape'])) {
130            if (!is_callable($options['escape'])) {
131                throw new InvalidArgumentException(
132                    'Handlebars Constructor "escape" option must be callable'
133                );
134            }
135            $this->escape = $options['escape'];
136        }
137
138        if (isset($options['escapeArgs'])) {
139            if (!is_array($options['escapeArgs'])) {
140                $options['escapeArgs'] = array($options['escapeArgs']);
141            }
142            $this->escapeArgs = $options['escapeArgs'];
143        }
144
145        if (isset($options['partials_alias'])
146            && is_array($options['partials_alias'])
147        ) {
148            $this->aliases = $options['partials_alias'];
149        }
150
151        if (isset($options[self::OPTION_ENABLE_DATA_VARIABLES])) {
152            if (!is_bool($options[self::OPTION_ENABLE_DATA_VARIABLES])) {
153                throw new InvalidArgumentException(
154                    'Handlebars Constructor "' . self::OPTION_ENABLE_DATA_VARIABLES . '" option must be a boolean'
155                );
156            }
157            $this->enableDataVariables = $options[self::OPTION_ENABLE_DATA_VARIABLES];
158        }
159    }
160
161
162
163    /**
164     * Shortcut 'render' invocation.
165     *
166     * Equivalent to calling `$handlebars->loadTemplate($template)->render($data);`
167     *
168     * @param string $template template name
169     * @param mixed  $data     data to use as context
170     * @return string Rendered template
171     */
172    public function render($template, $data)
173    {
174        return $this->loadTemplate($template)->render($data);
175    }
176    /**
177     * To invoke when this object is called as a function
178     *
179     * @param string $template template name
180     * @param mixed  $data     data to use as context
181     * @return string Rendered template
182     */
183    public function __invoke($template, $data)
184    {
185        return $this->render($template, $data);
186    }
187
188    /**
189     * Set helpers for current enfine
190     *
191     * @param Helpers $helpers handlebars helper
192     *
193     * @return void
194     */
195    public function setHelpers(Helpers $helpers)
196    {
197        $this->helpers = $helpers;
198    }
199
200    /**
201     * Get helpers, or create new one if ther is no helper
202     *
203     * @return Helpers
204     */
205    public function getHelpers()
206    {
207        if (!isset($this->helpers)) {
208            $this->helpers = new Helpers();
209        }
210        return $this->helpers;
211    }
212
213    /**
214     * Add a new helper.
215     *
216     * @param string $name   helper name
217     * @param mixed  $helper helper callable
218     *
219     * @return void
220     */
221    public function addHelper($name, $helper)
222    {
223        $this->getHelpers()->add($name, $helper);
224    }
225
226    /**
227     * Get a helper by name.
228     *
229     * @param string $name helper name
230     * @return callable Helper
231     */
232    public function getHelper($name)
233    {
234        return $this->getHelpers()->__get($name);
235    }
236
237    /**
238     * Check whether this instance has a helper.
239     *
240     * @param string $name helper name
241     * @return boolean True if the helper is present
242     */
243    public function hasHelper($name)
244    {
245        return $this->getHelpers()->has($name);
246    }
247
248    /**
249     * Remove a helper by name.
250     *
251     * @param string $name helper name
252     * @return void
253     */
254    public function removeHelper($name)
255    {
256        $this->getHelpers()->remove($name);
257    }
258
259    /**
260     * Set current loader
261     *
262     * @param Loader $loader handlebars loader
263     * @return void
264     */
265    public function setLoader(Loader $loader)
266    {
267        $this->loader = $loader;
268    }
269
270    /**
271     * get current loader
272     *
273     * @return Loader
274     */
275    public function getLoader()
276    {
277        if (! isset($this->loader)) {
278            $this->loader = new StringLoader();
279        }
280        return $this->loader;
281    }
282
283    /**
284     * Set current partials loader
285     *
286     * @param Loader $loader handlebars loader
287     * @return void
288     */
289    public function setPartialsLoader(Loader $loader)
290    {
291        $this->partialsLoader = $loader;
292    }
293
294    /**
295     * get current partials loader
296     *
297     * @return Loader
298     */
299    public function getPartialsLoader()
300    {
301        if (!isset($this->partialsLoader)) {
302            $this->partialsLoader = new StringLoader();
303        }
304        return $this->partialsLoader;
305    }
306
307    /**
308     * Set cache  for current engine
309     *
310     * @param Cache $cache handlebars cache
311     * @return void
312     */
313    public function setCache(Cache $cache)
314    {
315        $this->cache = $cache;
316    }
317
318    /**
319     * Get cache
320     *
321     * @return Cache
322     */
323    public function getCache()
324    {
325        if (!isset($this->cache)) {
326            $this->cache = new Dummy();
327        }
328        return $this->cache;
329    }
330
331    /**
332     * Get current escape function
333     *
334     * @return callable
335     */
336    public function getEscape()
337    {
338        return $this->escape;
339    }
340
341    /**
342     * Set current escape function
343     *
344     * @param callable $escape function
345     * @throws \InvalidArgumentException
346     * @return void
347     */
348    public function setEscape($escape)
349    {
350        if (!is_callable($escape)) {
351            throw new InvalidArgumentException(
352                'Escape function must be a callable'
353            );
354        }
355        $this->escape = $escape;
356    }
357
358    /**
359     * Get current escape function
360     *
361     * @return array
362     */
363    public function getEscapeArgs()
364    {
365        return $this->escapeArgs;
366    }
367
368    /**
369     * Set current escape function
370     *
371     * @param array $escapeArgs arguments to pass as extra arg to function
372     * @return void
373     */
374    public function setEscapeArgs($escapeArgs)
375    {
376        if (! is_array($escapeArgs)) {
377            $escapeArgs = array($escapeArgs);
378        }
379        $this->escapeArgs = $escapeArgs;
380    }
381
382
383    /**
384     * Set the Handlebars Tokenizer instance.
385     *
386     * @param Tokenizer $tokenizer tokenizer
387     * @return void
388     */
389    public function setTokenizer(Tokenizer $tokenizer)
390    {
391        $this->tokenizer = $tokenizer;
392    }
393
394    /**
395     * Get the current Handlebars Tokenizer instance.
396     *
397     * If no Tokenizer instance has been explicitly specified, this method will
398     * instantiate and return a new one.
399     *
400     * @return Tokenizer
401     */
402    public function getTokenizer()
403    {
404        if (! isset($this->tokenizer)) {
405            $this->tokenizer = new Tokenizer();
406        }
407
408        return $this->tokenizer;
409    }
410
411    /**
412     * Set the Handlebars Parser instance.
413     *
414     * @param Parser $parser parser object
415     * @return void
416     */
417    public function setParser(Parser $parser)
418    {
419        $this->parser = $parser;
420    }
421
422    /**
423     * Get the current Handlebars Parser instance.
424     *
425     * If no Parser instance has been explicitly specified, this method will
426     * instantiate and return a new one.
427     *
428     * @return Parser
429     */
430    public function getParser()
431    {
432        if (! isset($this->parser)) {
433            $this->parser = new Parser();
434        }
435        return $this->parser;
436    }
437
438    /**
439     * Determines if the @data variables are enabled.
440     * @return bool
441     */
442    public function isDataVariablesEnabled()
443    {
444        return $this->enableDataVariables;
445    }
446
447    /**
448     * Load a template by name with current template loader
449     *
450     * @param string $name template name
451     *
452     * @return Template
453     */
454    public function loadTemplate($name)
455    {
456        $source = $this->getLoader()->load($name);
457        $tree = $this->tokenize($source);
458        return new Template($this, $tree, $source);
459    }
460
461    /**
462     * Load a partial by name with current partial loader
463     *
464     * @param string $name partial name
465     *
466     * @return Template
467     */
468    public function loadPartial($name)
469    {
470        if (isset($this->aliases[$name])) {
471            $name = $this->aliases[$name];
472        }
473        $source = $this->getPartialsLoader()->load($name);
474        $tree = $this->tokenize($source);
475        return new Template($this, $tree, $source);
476    }
477
478    /**
479     * Register partial alias
480     *
481     * @param string $alias   Partial alias
482     * @param string $content The real value
483     * @return void
484     */
485    public function registerPartial($alias, $content)
486    {
487        $this->aliases[$alias] = $content;
488    }
489
490    /**
491     * Un-register partial alias
492     *
493     * @param string $alias Partial alias
494     * @return void
495     */
496    public function unRegisterPartial($alias)
497    {
498        if (isset($this->aliases[$alias])) {
499            unset($this->aliases[$alias]);
500        }
501    }
502
503    /**
504     * Load string into a template object
505     *
506     * @param string $source string to load
507     * @return Template
508     */
509    public function loadString($source)
510    {
511        $tree = $this->tokenize($source);
512        return new Template($this, $tree, $source);
513    }
514
515    /**
516     * try to tokenize source, or get them from cache if available
517     *
518     * @param string $source handlebars source code
519     * @return array handlebars parsed data into array
520     */
521    private function tokenize($source)
522    {
523        $hash = md5(sprintf('version: %s, data : %s', self::VERSION, $source));
524        $tree = $this->getCache()->get($hash);
525        if ($tree === false) {
526            $tokens = $this->getTokenizer()->scan($source);
527            $tree = $this->getParser()->parse($tokens);
528            $this->getCache()->set($hash, $tree);
529        }
530        return $tree;
531    }
532
533}