1<?php
2/**
3 * Handlebars base template
4 * contain some utility method to get context and helpers
5 *
6 * @category  Xamin
7 * @package   Handlebars
8 * @author    fzerorubigd <fzerorubigd@gmail.com>
9 * @author    Behrooz Shabani <everplays@gmail.com>
10 * @author    Mardix <https://github.com/mardix>
11 * @copyright 2012 (c) ParsPooyesh Co
12 * @copyright 2013 (c) Behrooz Shabani
13 * @copyright 2013 (c) Mardix
14 * @license   MIT
15 * @link      http://voodoophp.org/docs/handlebars
16 */
17
18namespace Handlebars;
19
20use InvalidArgumentException;
21use RuntimeException;
22
23class Template
24{
25    /**
26     * @var Handlebars
27     */
28    protected $handlebars;
29
30    protected $tree = [];
31
32    protected $source = '';
33
34    /**
35     * @var array Run stack
36     */
37    private $stack = [];
38
39    /**
40     * Handlebars template constructor
41     *
42     * @param Handlebars $engine handlebar engine
43     * @param array      $tree   Parsed tree
44     * @param string     $source Handlebars source
45     */
46    public function __construct(Handlebars $engine, $tree, $source)
47    {
48        $this->handlebars = $engine;
49        $this->tree = $tree;
50        $this->source = $source;
51        array_push($this->stack, [0, $this->getTree(), false]);
52    }
53
54    /**
55     * Get current tree
56     *
57     * @return array
58     */
59    public function getTree()
60    {
61        return $this->tree;
62    }
63
64    /**
65     * Get current source
66     *
67     * @return string
68     */
69    public function getSource()
70    {
71        return $this->source;
72    }
73
74    /**
75     * Get current engine associated with this object
76     *
77     * @return Handlebars
78     */
79    public function getEngine()
80    {
81        return $this->handlebars;
82    }
83
84    /**
85     * set stop token for render and discard method
86     *
87     * @param string|false $token token to set as stop token or false to remove
88     *
89     * @return void
90     */
91    public function setStopToken($token)
92    {
93        $topStack = array_pop($this->stack);
94        $topStack[2] = $token;
95        array_push($this->stack, $topStack);
96    }
97
98    /**
99     * get current stop token
100     *
101     * @return string|false
102     */
103    public function getStopToken()
104    {
105        return end($this->stack)[2];
106    }
107
108    /**
109     * Render top tree
110     *
111     * @param mixed $context current context
112     *
113     * @throws \RuntimeException
114     * @return string
115     */
116    public function render($context)
117    {
118        if (!$context instanceof Context) {
119            $context = new Context($context, [
120                'enableDataVariables' => $this->handlebars->isDataVariablesEnabled(),
121            ]);
122        }
123        $topTree = end($this->stack); // never pop a value from stack
124        list($index, $tree, $stop) = $topTree;
125
126        $buffer = '';
127        while (array_key_exists($index, $tree)) {
128            $current = $tree[$index];
129            $index++;
130            //if the section is exactly like waitFor
131            if (is_string($stop)
132                && $current[Tokenizer::TYPE] == Tokenizer::T_ESCAPED
133                && $current[Tokenizer::NAME] === $stop
134            ) {
135                break;
136            }
137            switch ($current[Tokenizer::TYPE]) {
138            case Tokenizer::T_SECTION :
139                $newStack = isset($current[Tokenizer::NODES])
140                    ? $current[Tokenizer::NODES] : [];
141                array_push($this->stack, [0, $newStack, false]);
142                $buffer .= $this->section($context, $current);
143                array_pop($this->stack);
144                break;
145            case Tokenizer::T_INVERTED :
146                $newStack = isset($current[Tokenizer::NODES]) ?
147                    $current[Tokenizer::NODES] : [];
148                array_push($this->stack, [0, $newStack, false]);
149                $buffer .= $this->inverted($context, $current);
150                array_pop($this->stack);
151                break;
152            case Tokenizer::T_COMMENT :
153                $buffer .= '';
154                break;
155            case Tokenizer::T_PARTIAL:
156            case Tokenizer::T_PARTIAL_2:
157                $buffer .= $this->partial($context, $current);
158                break;
159            case Tokenizer::T_UNESCAPED:
160            case Tokenizer::T_UNESCAPED_2:
161                $buffer .= $this->variables($context, $current, false);
162                break;
163            case Tokenizer::T_ESCAPED:
164                $buffer .= $this->variables($context, $current, true);
165                break;
166            case Tokenizer::T_TEXT:
167                $buffer .= $current[Tokenizer::VALUE];
168                break;
169            default:
170                throw new RuntimeException(
171                    'Invalid node type : ' . json_encode($current)
172                );
173            }
174        }
175        if ($stop) {
176            //Ok break here, the helper should be aware of this.
177            $newStack = array_pop($this->stack);
178            $newStack[0] = $index;
179            $newStack[2] = false; //No stop token from now on
180            array_push($this->stack, $newStack);
181        }
182
183        return $buffer;
184    }
185
186    /**
187     * Discard top tree
188     *
189     * @return string
190     */
191    public function discard()
192    {
193        $topTree = end($this->stack); //This method never pop a value from stack
194        list($index, $tree, $stop) = $topTree;
195        while (array_key_exists($index, $tree)) {
196            $current = $tree[$index];
197            $index++;
198            //if the section is exactly like waitFor
199            if (is_string($stop)
200                && $current[Tokenizer::TYPE] == Tokenizer::T_ESCAPED
201                && $current[Tokenizer::NAME] === $stop
202            ) {
203                break;
204            }
205        }
206        if ($stop) {
207            //Ok break here, the helper should be aware of this.
208            $newStack = array_pop($this->stack);
209            $newStack[0] = $index;
210            $newStack[2] = false;
211            array_push($this->stack, $newStack);
212        }
213
214        return '';
215    }
216
217    /**
218     * Process section nodes
219     *
220     * @param Context $context current context
221     * @param array   $current section node data
222     *
223     * @throws \RuntimeException
224     * @return string the result
225     */
226    private function section(Context $context, $current)
227    {
228        $helpers = $this->handlebars->getHelpers();
229        $sectionName = $current[Tokenizer::NAME];
230        if ($helpers->has($sectionName)) {
231            if (isset($current[Tokenizer::END])) {
232                $source = substr(
233                    $this->getSource(),
234                    $current[Tokenizer::INDEX],
235                    $current[Tokenizer::END] - $current[Tokenizer::INDEX]
236                );
237            } else {
238                $source = '';
239            }
240            $params = [
241                $this, //First argument is this template
242                $context, //Second is current context
243                $current[Tokenizer::ARGS], //Arguments
244                $source
245            ];
246
247            $return = call_user_func_array($helpers->$sectionName, $params);
248            if ($return instanceof String) {
249                return $this->handlebars->loadString($return)->render($context);
250            } else {
251                return $return;
252            }
253        } elseif (trim($current[Tokenizer::ARGS]) == '') {
254            // fallback to mustache style each/with/for just if there is
255            // no argument at all.
256            try {
257                $sectionVar = $context->get($sectionName, true);
258            } catch (InvalidArgumentException $e) {
259                throw new RuntimeException(
260                    $sectionName . ' is not registered as a helper'
261                );
262            }
263            $buffer = '';
264            if (is_array($sectionVar) || $sectionVar instanceof \Traversable) {
265                foreach ($sectionVar as $index => $d) {
266                    $context->pushIndex($index);
267                    $context->push($d);
268                    $buffer .= $this->render($context);
269                    $context->pop();
270                    $context->popIndex();
271                }
272            } elseif (is_object($sectionVar)) {
273                //Act like with
274                $context->push($sectionVar);
275                $buffer = $this->render($context);
276                $context->pop();
277            } elseif ($sectionVar) {
278                $buffer = $this->render($context);
279            }
280
281            return $buffer;
282        } else {
283            throw new RuntimeException(
284                $sectionName . ' is not registered as a helper'
285            );
286        }
287    }
288
289    /**
290     * Process inverted section
291     *
292     * @param Context $context current context
293     * @param array   $current section node data
294     *
295     * @return string the result
296     */
297    private function inverted(Context $context, $current)
298    {
299        $sectionName = $current[Tokenizer::NAME];
300        $data = $context->get($sectionName);
301        if (!$data) {
302            return $this->render($context);
303        } else {
304            //No need to discard here, since it has no else
305            return '';
306        }
307    }
308
309    /**
310     * Process partial section
311     *
312     * @param Context $context current context
313     * @param array   $current section node data
314     *
315     * @return string the result
316     */
317    private function partial(Context $context, $current)
318    {
319        $partial = $this->handlebars->loadPartial($current[Tokenizer::NAME]);
320
321        if ($current[Tokenizer::ARGS]) {
322            $context = $context->get($current[Tokenizer::ARGS]);
323        }
324
325        return $partial->render($context);
326    }
327
328    /**
329     * Process partial section
330     *
331     * @param Context $context current context
332     * @param array   $current section node data
333     * @param boolean $escaped escape result or not
334     *
335     * @return string the result
336     */
337    private function variables(Context $context, $current, $escaped)
338    {
339        $name = $current[Tokenizer::NAME];
340        $value = $context->get($name);
341
342        // If @data variables are enabled, use the more complex algorithm for handling the the variables otherwise
343        // use the previous version.
344        if ($this->handlebars->isDataVariablesEnabled()) {
345            if (substr(trim($name), 0, 1) == '@') {
346                $variable = $context->getDataVariable($name);
347                if (is_bool($variable)) {
348                    return $variable ? 'true' : 'false';
349                }
350                return $variable;
351            }
352        } else {
353            // If @data variables are not enabled, then revert back to legacy behavior
354            if ($name == '@index') {
355                return $context->lastIndex();
356            }
357            if ($name == '@key') {
358                return $context->lastKey();
359            }
360        }
361
362        if ($escaped) {
363            $args = $this->handlebars->getEscapeArgs();
364            array_unshift($args, $value);
365            $value = call_user_func_array(
366                $this->handlebars->getEscape(),
367                array_values($args)
368            );
369        }
370
371        return $value;
372    }
373
374    public function __clone()
375    {
376        return $this;
377    }
378}
379