1<?php
2/*
3
4MIT License
5Copyright 2013-2021 Zordius Chen. All Rights Reserved.
6Permission 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:
7The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8THE 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.
9
10Origin: https://github.com/zordius/lightncandy
11*/
12
13/**
14 * file to keep LightnCandy Validator
15 *
16 * @package    LightnCandy
17 * @author     Zordius <zordius@gmail.com>
18 */
19
20namespace LightnCandy;
21
22/**
23 * LightnCandy Validator
24 */
25class Validator
26{
27    /**
28     * Verify template
29     *
30     * @param array<string,array|string|integer> $context Current context
31     * @param string $template handlebars template
32     */
33    public static function verify(&$context, $template)
34    {
35        $template = SafeString::stripExtendedComments($template);
36        $context['level'] = 0;
37        Parser::setDelimiter($context);
38
39        while (preg_match($context['tokens']['search'], $template, $matches)) {
40            // Skip a token when it is slash escaped
41            if ($context['flags']['slash'] && ($matches[Token::POS_LSPACE] === '') && preg_match('/^(.*?)(\\\\+)$/s', $matches[Token::POS_LOTHER], $escmatch)) {
42                if (strlen($escmatch[2]) % 4) {
43                    static::pushToken($context, substr($matches[Token::POS_LOTHER], 0, -2) . $context['tokens']['startchar']);
44                    $matches[Token::POS_BEGINTAG] = substr($matches[Token::POS_BEGINTAG], 1);
45                    $template = implode('', array_slice($matches, Token::POS_BEGINTAG));
46                    continue;
47                } else {
48                    $matches[Token::POS_LOTHER] = $escmatch[1] . str_repeat('\\', strlen($escmatch[2]) / 2);
49                }
50            }
51            $context['tokens']['count']++;
52            $V = static::token($matches, $context);
53            static::pushLeft($context);
54            if ($V) {
55                if (is_array($V)) {
56                    array_push($V, $matches, $context['tokens']['partialind']);
57                }
58                static::pushToken($context, $V);
59            }
60            $template = "{$matches[Token::POS_RSPACE]}{$matches[Token::POS_ROTHER]}";
61        }
62        static::pushToken($context, $template);
63
64        if ($context['level'] > 0) {
65            array_pop($context['stack']);
66            array_pop($context['stack']);
67            $token = array_pop($context['stack']);
68            $context['error'][] = 'Unclosed token ' . ($context['rawblock'] ? "{{{{{$token}}}}}" : ($context['partialblock'] ? "{{#>{$token}}}" : "{{#{$token}}}")) . ' !!';
69        }
70    }
71
72    /**
73     * push left string of current token and clear it
74     *
75     * @param array<string,array|string|integer> $context Current context
76     */
77    protected static function pushLeft(&$context)
78    {
79        $L = $context['currentToken'][Token::POS_LOTHER] . $context['currentToken'][Token::POS_LSPACE];
80        static::pushToken($context, $L);
81        $context['currentToken'][Token::POS_LOTHER] = $context['currentToken'][Token::POS_LSPACE] = '';
82    }
83
84    /**
85     * push a string into the partial stacks
86     *
87     * @param array<string,array|string|integer> $context Current context
88     * @param string $append a string to be appended int partial stacks
89     */
90    protected static function pushPartial(&$context, $append)
91    {
92        $appender = function (&$p) use ($append) {
93            $p .= $append;
94        };
95        array_walk($context['inlinepartial'], $appender);
96        array_walk($context['partialblock'], $appender);
97    }
98
99    /**
100     * push a token into the stack when it is not empty string
101     *
102     * @param array<string,array|string|integer> $context Current context
103     * @param string|array $token a parsed token or a string
104     */
105    protected static function pushToken(&$context, $token)
106    {
107        if ($token === '') {
108            return;
109        }
110        if (is_string($token)) {
111            static::pushPartial($context, $token);
112            if (is_string(end($context['parsed'][0]))) {
113                $context['parsed'][0][key($context['parsed'][0])] .= $token;
114                return;
115            }
116        } else {
117            static::pushPartial($context, Token::toString($context['currentToken']));
118            switch ($context['currentToken'][Token::POS_OP]) {
119            case '#*':
120                array_unshift($context['inlinepartial'], '');
121                break;
122            case '#>':
123                array_unshift($context['partialblock'], '');
124                break;
125            }
126        }
127        $context['parsed'][0][] = $token;
128    }
129
130    /**
131     * push current token into the section stack
132     *
133     * @param array<string,array|string|integer> $context Current context
134     * @param string $operation operation string
135     * @param array<boolean|integer|string|array> $vars parsed arguments list
136     */
137    protected static function pushStack(&$context, $operation, $vars)
138    {
139        list($levels, $spvar, $var) = Expression::analyze($context, $vars[0]);
140        $context['stack'][] = $context['currentToken'][Token::POS_INNERTAG];
141        $context['stack'][] = Expression::toString($levels, $spvar, $var);
142        $context['stack'][] = $operation;
143        $context['level']++;
144    }
145
146    /**
147     * Verify delimiters and operators
148     *
149     * @param string[] $token detected handlebars {{ }} token
150     * @param array<string,array|string|integer> $context current compile context
151     *
152     * @return boolean|null Return true when invalid
153     *
154     * @expect null when input array_fill(0, 11, ''), array()
155     * @expect null when input array(0, 0, 0, 0, 0, '{{', '#', '...', '}}'), array()
156     * @expect true when input array(0, 0, 0, 0, 0, '{', '#', '...', '}'), array()
157     */
158    protected static function delimiter($token, &$context)
159    {
160        // {{ }}} or {{{ }} are invalid
161        if (strlen($token[Token::POS_BEGINRAW]) !== strlen($token[Token::POS_ENDRAW])) {
162            $context['error'][] = 'Bad token ' . Token::toString($token) . ' ! Do you mean ' . Token::toString($token, array(Token::POS_BEGINRAW => '', Token::POS_ENDRAW => '')) . ' or ' . Token::toString($token, array(Token::POS_BEGINRAW => '{', Token::POS_ENDRAW => '}')) . '?';
163            return true;
164        }
165        // {{{# }}} or {{{! }}} or {{{/ }}} or {{{^ }}} are invalid.
166        if ((strlen($token[Token::POS_BEGINRAW]) == 1) && $token[Token::POS_OP] && ($token[Token::POS_OP] !== '&')) {
167            $context['error'][] = 'Bad token ' . Token::toString($token) . ' ! Do you mean ' . Token::toString($token, array(Token::POS_BEGINRAW => '', Token::POS_ENDRAW => '')) . ' ?';
168            return true;
169        }
170    }
171
172    /**
173     * Verify operators
174     *
175     * @param string $operator the operator string
176     * @param array<string,array|string|integer> $context current compile context
177     * @param array<boolean|integer|string|array> $vars parsed arguments list
178     *
179     * @return boolean|integer|null Return true when invalid or detected
180     *
181     * @expect null when input '', array(), array()
182     * @expect 2 when input '^', array('usedFeature' => array('isec' => 1), 'level' => 0, 'currentToken' => array(0,0,0,0,0,0,0,0), 'elselvl' => array(), 'flags' => array('spvar' => 0), 'elsechain' => false, 'helperresolver' => 0), array(array('foo'))
183     * @expect true when input '/', array('stack' => array('[with]', '#'), 'level' => 1, 'currentToken' => array(0,0,0,0,0,0,0,'with'), 'flags' => array('nohbh' => 0)), array(array())
184     * @expect 4 when input '#', array('usedFeature' => array('sec' => 3), 'level' => 0, 'currentToken' => array(0,0,0,0,0,0,0,0), 'flags' => array('spvar' => 0), 'elsechain' => false, 'elselvl' => array(), 'helperresolver' => 0), array(array('x'))
185     * @expect 5 when input '#', array('usedFeature' => array('if' => 4), 'level' => 0, 'currentToken' => array(0,0,0,0,0,0,0,0), 'flags' => array('spvar' => 0, 'nohbh' => 0), 'elsechain' => false, 'elselvl' => array(), 'helperresolver' => 0), array(array('if'))
186     * @expect 6 when input '#', array('usedFeature' => array('with' => 5), 'level' => 0, 'flags' => array('nohbh' => 0, 'runpart' => 0, 'spvar' => 0), 'currentToken' => array(0,0,0,0,0,0,0,0), 'elsechain' => false, 'elselvl' => array(), 'helperresolver' => 0), array(array('with'))
187     * @expect 7 when input '#', array('usedFeature' => array('each' => 6), 'level' => 0, 'currentToken' => array(0,0,0,0,0,0,0,0), 'flags' => array('spvar' => 0, 'nohbh' => 0), 'elsechain' => false, 'elselvl' => array(), 'helperresolver' => 0), array(array('each'))
188     * @expect 8 when input '#', array('usedFeature' => array('unless' => 7), 'level' => 0, 'currentToken' => array(0,0,0,0,0,0,0,0), 'flags' => array('spvar' => 0, 'nohbh' => 0), 'elsechain' => false, 'elselvl' => array(), 'helperresolver' => 0), array(array('unless'))
189     * @expect 9 when input '#', array('helpers' => array('abc' => ''), 'usedFeature' => array('helper' => 8), 'level' => 0, 'currentToken' => array(0,0,0,0,0,0,0,0), 'flags' => array('spvar' => 0), 'elsechain' => false, 'elselvl' => array()), array(array('abc'))
190     * @expect 11 when input '#', array('helpers' => array('abc' => ''), 'usedFeature' => array('helper' => 10), 'level' => 0, 'currentToken' => array(0,0,0,0,0,0,0,0), 'flags' => array('spvar' => 0), 'elsechain' => false, 'elselvl' => array()), array(array('abc'))
191     * @expect true when input '>', array('partialresolver' => false, 'usedFeature' => array('partial' => 7), 'level' => 0, 'flags' => array('skippartial' => 0, 'runpart' => 0, 'spvar' => 0), 'currentToken' => array(0,0,0,0,0,0,0,0), 'elsechain' => false, 'elselvl' => array()), array('test')
192     */
193    protected static function operator($operator, &$context, &$vars)
194    {
195        switch ($operator) {
196            case '#*':
197                if (!$context['compile']) {
198                    $context['stack'][] = count($context['parsed'][0]) + ($context['currentToken'][Token::POS_LOTHER] . $context['currentToken'][Token::POS_LSPACE] === '' ? 0 : 1);
199                    static::pushStack($context, '#*', $vars);
200                }
201                return static::inline($context, $vars);
202
203            case '#>':
204                if (!$context['compile']) {
205                    $context['stack'][] = count($context['parsed'][0]) + ($context['currentToken'][Token::POS_LOTHER] . $context['currentToken'][Token::POS_LSPACE] === '' ? 0 : 1);
206                    $vars[Parser::PARTIALBLOCK] = ++$context['usedFeature']['pblock'];
207                    static::pushStack($context, '#>', $vars);
208                }
209                // no break
210            case '>':
211                return static::partial($context, $vars);
212
213            case '^':
214                if (!isset($vars[0][0])) {
215                    if (!$context['flags']['else']) {
216                        $context['error'][] = 'Do not support {{^}}, you should do compile with LightnCandy::FLAG_ELSE flag';
217                        return;
218                    } else {
219                        return static::doElse($context, $vars);
220                    }
221                }
222
223                static::doElseChain($context);
224
225                if (static::isBlockHelper($context, $vars)) {
226                    static::pushStack($context, '#', $vars);
227                    return static::blockCustomHelper($context, $vars, true);
228                }
229
230                static::pushStack($context, '^', $vars);
231                return static::invertedSection($context, $vars);
232
233            case '/':
234                $r = static::blockEnd($context, $vars);
235                if ($r !== Token::POS_BACKFILL) {
236                    array_pop($context['stack']);
237                    array_pop($context['stack']);
238                    array_pop($context['stack']);
239                }
240                return $r;
241
242            case '#':
243                static::doElseChain($context);
244                static::pushStack($context, '#', $vars);
245
246                if (static::isBlockHelper($context, $vars)) {
247                    return static::blockCustomHelper($context, $vars);
248                }
249
250                return static::blockBegin($context, $vars);
251        }
252    }
253
254    /**
255     * validate inline partial begin token
256     *
257     * @param array<string,array|string|integer> $context current compile context
258     * @param array<boolean|integer|string|array> $vars parsed arguments list
259     *
260     * @return boolean|null Return true when inline partial ends
261     */
262    protected static function inlinePartial(&$context, $vars)
263    {
264        $ended = false;
265        if ($context['currentToken'][Token::POS_OP] === '/') {
266            if (static::blockEnd($context, $vars, '#*') !== null) {
267                $context['usedFeature']['inlpartial']++;
268                $tmpl = array_shift($context['inlinepartial']) . $context['currentToken'][Token::POS_LOTHER] . $context['currentToken'][Token::POS_LSPACE];
269                $c = $context['stack'][count($context['stack']) - 4];
270                $context['parsed'][0] = array_slice($context['parsed'][0], 0, $c + 1);
271                $P = &$context['parsed'][0][$c];
272                if (isset($P[1][1][0])) {
273                    $context['usedPartial'][$P[1][1][0]] = $tmpl;
274                    $P[1][0][0] = Partial::compileDynamic($context, $P[1][1][0]);
275                }
276                $ended = true;
277            }
278        }
279        return $ended;
280    }
281
282    /**
283     * validate partial block token
284     *
285     * @param array<string,array|string|integer> $context current compile context
286     * @param array<boolean|integer|string|array> $vars parsed arguments list
287     *
288     * @return boolean|null Return true when partial block ends
289     */
290    protected static function partialBlock(&$context, $vars)
291    {
292        $ended = false;
293        if ($context['currentToken'][Token::POS_OP] === '/') {
294            if (static::blockEnd($context, $vars, '#>') !== null) {
295                $c = $context['stack'][count($context['stack']) - 4];
296                $context['parsed'][0] = array_slice($context['parsed'][0], 0, $c + 1);
297                $found = Partial::resolve($context, $vars[0][0]) !== null;
298                $v = $found ? "@partial-block{$context['parsed'][0][$c][1][Parser::PARTIALBLOCK]}" : "{$vars[0][0]}";
299                if (count($context['partialblock']) == 1) {
300                    $tmpl = $context['partialblock'][0] . $context['currentToken'][Token::POS_LOTHER] . $context['currentToken'][Token::POS_LSPACE];
301                    if ($found) {
302                        $context['partials'][$v] = $tmpl;
303                    }
304                    $context['usedPartial'][$v] = $tmpl;
305                    Partial::compileDynamic($context, $v);
306                    if ($found) {
307                        Partial::read($context, $vars[0][0]);
308                    }
309                }
310                array_shift($context['partialblock']);
311                $ended = true;
312            }
313        }
314        return $ended;
315    }
316
317    /**
318     * handle else chain
319     *
320     * @param array<string,array|string|integer> $context current compile context
321     */
322    protected static function doElseChain(&$context)
323    {
324        if ($context['elsechain']) {
325            $context['elsechain'] = false;
326        } else {
327            array_unshift($context['elselvl'], array());
328        }
329    }
330
331    /**
332     * validate block begin token
333     *
334     * @param array<string,array|string|integer> $context current compile context
335     * @param array<boolean|integer|string|array> $vars parsed arguments list
336     *
337     * @return boolean Return true always
338     */
339    protected static function blockBegin(&$context, $vars)
340    {
341        switch ((isset($vars[0][0]) && is_string($vars[0][0])) ? $vars[0][0] : null) {
342            case 'with':
343                return static::with($context, $vars);
344            case 'each':
345                return static::section($context, $vars, true);
346            case 'unless':
347                return static::unless($context, $vars);
348            case 'if':
349                return static::doIf($context, $vars);
350            default:
351                return static::section($context, $vars);
352        }
353    }
354
355    /**
356     * validate builtin helpers
357     *
358     * @param array<string,array|string|integer> $context current compile context
359     * @param array<boolean|integer|string|array> $vars parsed arguments list
360     */
361    protected static function builtin(&$context, $vars)
362    {
363        if ($context['flags']['nohbh']) {
364            if (isset($vars[1][0])) {
365                $context['error'][] = "Do not support {{#{$vars[0][0]} var}} because you compile with LightnCandy::FLAG_NOHBHELPERS flag";
366            }
367        } else {
368            if (count($vars) < 2) {
369                $context['error'][] = "No argument after {{#{$vars[0][0]}}} !";
370            }
371        }
372        $context['usedFeature'][$vars[0][0]]++;
373    }
374
375    /**
376     * validate section token
377     *
378     * @param array<string,array|string|integer> $context current compile context
379     * @param array<boolean|integer|string|array> $vars parsed arguments list
380     * @param boolean $isEach the section is #each
381     *
382     * @return boolean Return true always
383     */
384    protected static function section(&$context, $vars, $isEach = false)
385    {
386        if ($isEach) {
387            static::builtin($context, $vars);
388        } else {
389            if ((count($vars) > 1) && !$context['flags']['lambda']) {
390                $context['error'][] = "Custom helper not found: {$vars[0][0]} in " . Token::toString($context['currentToken']) . ' !';
391            }
392            $context['usedFeature']['sec']++;
393        }
394        return true;
395    }
396
397    /**
398     * validate with token
399     *
400     * @param array<string,array|string|integer> $context current compile context
401     * @param array<boolean|integer|string|array> $vars parsed arguments list
402     *
403     * @return boolean Return true always
404     */
405    protected static function with(&$context, $vars)
406    {
407        static::builtin($context, $vars);
408        return true;
409    }
410
411    /**
412     * validate unless token
413     *
414     * @param array<string,array|string|integer> $context current compile context
415     * @param array<boolean|integer|string|array> $vars parsed arguments list
416     *
417     * @return boolean Return true always
418     */
419    protected static function unless(&$context, $vars)
420    {
421        static::builtin($context, $vars);
422        return true;
423    }
424
425    /**
426     * validate if token
427     *
428     * @param array<string,array|string|integer> $context current compile context
429     * @param array<boolean|integer|string|array> $vars parsed arguments list
430     *
431     * @return boolean Return true always
432     */
433    protected static function doIf(&$context, $vars)
434    {
435        static::builtin($context, $vars);
436        return true;
437    }
438
439    /**
440     * validate block custom helper token
441     *
442     * @param array<string,array|string|integer> $context current compile context
443     * @param array<boolean|integer|string|array> $vars parsed arguments list
444     * @param boolean $inverted the logic will be inverted
445     *
446     * @return integer|null Return number of used custom helpers
447     */
448    protected static function blockCustomHelper(&$context, $vars, $inverted = false)
449    {
450        if (is_string($vars[0][0])) {
451            if (static::resolveHelper($context, $vars)) {
452                return ++$context['usedFeature']['helper'];
453            }
454        }
455    }
456
457    /**
458     * validate inverted section
459     *
460     * @param array<string,array|string|integer> $context current compile context
461     * @param array<boolean|integer|string|array> $vars parsed arguments list
462     *
463     * @return integer Return number of inverted sections
464     */
465    protected static function invertedSection(&$context, $vars)
466    {
467        return ++$context['usedFeature']['isec'];
468    }
469
470    /**
471     * Return compiled PHP code for a handlebars block end token
472     *
473     * @param array<string,array|string|integer> $context current compile context
474     * @param array<boolean|integer|string|array> $vars parsed arguments list
475     * @param string|null $match should also match to this operator
476     *
477     * @return boolean|integer Return true when required block ended, or Token::POS_BACKFILL when backfill happened.
478     */
479    protected static function blockEnd(&$context, &$vars, $match = null)
480    {
481        $c = count($context['stack']) - 2;
482        $pop = ($c >= 0) ? $context['stack'][$c + 1] : '';
483        if (($match !== null) && ($match !== $pop)) {
484            return;
485        }
486        // if we didn't match our $pop, we didn't actually do a level, so only subtract a level here
487        $context['level']--;
488        $pop2 = ($c >= 0) ? $context['stack'][$c]: '';
489        switch ($context['currentToken'][Token::POS_INNERTAG]) {
490            case 'with':
491                if (!$context['flags']['nohbh']) {
492                    if ($pop2 !== '[with]') {
493                        $context['error'][] = 'Unexpect token: {{/with}} !';
494                        return;
495                    }
496                }
497                return true;
498        }
499
500        switch ($pop) {
501            case '#':
502            case '^':
503                $elsechain = array_shift($context['elselvl']);
504                if (isset($elsechain[0])) {
505                    // we need to repeat a level due to else chains: {{else if}}
506                    $context['level']++;
507                    $context['currentToken'][Token::POS_RSPACE] = $context['currentToken'][Token::POS_BACKFILL] = '{{/' . implode('}}{{/', $elsechain) . '}}' . Token::toString($context['currentToken']) . $context['currentToken'][Token::POS_RSPACE];
508                    return Token::POS_BACKFILL;
509                }
510                // no break
511            case '#>':
512            case '#*':
513                list($levels, $spvar, $var) = Expression::analyze($context, $vars[0]);
514                $v = Expression::toString($levels, $spvar, $var);
515                if ($pop2 !== $v) {
516                    $context['error'][] = 'Unexpect token ' . Token::toString($context['currentToken']) . " ! Previous token {{{$pop}$pop2}} is not closed";
517                    return;
518                }
519                return true;
520            default:
521                $context['error'][] = 'Unexpect token: ' . Token::toString($context['currentToken']) . ' !';
522                return;
523        }
524    }
525
526    /**
527     * handle delimiter change
528     *
529     * @param array<string,array|string|integer> $context current compile context
530     *
531     * @return boolean|null Return true when delimiter changed
532     */
533    protected static function isDelimiter(&$context)
534    {
535        if (preg_match('/^=\s*([^ ]+)\s+([^ ]+)\s*=$/', $context['currentToken'][Token::POS_INNERTAG], $matched)) {
536            $context['usedFeature']['delimiter']++;
537            Parser::setDelimiter($context, $matched[1], $matched[2]);
538            return true;
539        }
540    }
541
542    /**
543     * handle raw block
544     *
545     * @param string[] $token detected handlebars {{ }} token
546     * @param array<string,array|string|integer> $context current compile context
547     *
548     * @return boolean|null Return true when in rawblock mode
549     */
550    protected static function rawblock(&$token, &$context)
551    {
552        $inner = $token[Token::POS_INNERTAG];
553        trim($inner);
554
555        // skip parse when inside raw block
556        if ($context['rawblock'] && !(($token[Token::POS_BEGINRAW] === '{{') && ($token[Token::POS_OP] === '/') && ($context['rawblock'] === $inner))) {
557            return true;
558        }
559
560        $token[Token::POS_INNERTAG] = $inner;
561
562        // Handle raw block
563        if ($token[Token::POS_BEGINRAW] === '{{') {
564            if ($token[Token::POS_ENDRAW] !== '}}') {
565                $context['error'][] = 'Bad token ' . Token::toString($token) . ' ! Do you mean ' . Token::toString($token, array(Token::POS_ENDRAW => '}}')) . ' ?';
566            }
567            if ($context['rawblock']) {
568                Parser::setDelimiter($context);
569                $context['rawblock'] = false;
570            } else {
571                if ($token[Token::POS_OP]) {
572                    $context['error'][] = "Wrong raw block begin with " . Token::toString($token) . ' ! Remove "' . $token[Token::POS_OP] . '" to fix this issue.';
573                }
574                $context['rawblock'] = $token[Token::POS_INNERTAG];
575                Parser::setDelimiter($context);
576                $token[Token::POS_OP] = '#';
577            }
578            $token[Token::POS_ENDRAW] = '}}';
579        }
580    }
581
582    /**
583     * handle comment
584     *
585     * @param string[] $token detected handlebars {{ }} token
586     * @param array<string,array|string|integer> $context current compile context
587     *
588     * @return boolean|null Return true when is comment
589     */
590    protected static function comment(&$token, &$context)
591    {
592        if ($token[Token::POS_OP] === '!') {
593            $context['usedFeature']['comment']++;
594            return true;
595        }
596    }
597
598    /**
599     * Collect handlebars usage information, detect template error.
600     *
601     * @param string[] $token detected handlebars {{ }} token
602     * @param array<string,array|string|integer> $context current compile context
603     *
604     * @return string|array<string,array|string|integer>|null $token string when rawblock; array when valid token require to be compiled, null when skip the token.
605     */
606    protected static function token(&$token, &$context)
607    {
608        $context['currentToken'] = &$token;
609
610        if (static::rawblock($token, $context)) {
611            return Token::toString($token);
612        }
613
614        if (static::delimiter($token, $context)) {
615            return;
616        }
617
618        if (static::isDelimiter($context)) {
619            static::spacing($token, $context);
620            return;
621        }
622
623        if (static::comment($token, $context)) {
624            static::spacing($token, $context);
625            return;
626        }
627
628        list($raw, $vars) = Parser::parse($token, $context);
629
630        // Handle spacing (standalone tags, partial indent)
631        static::spacing($token, $context, (($token[Token::POS_OP] === '') || ($token[Token::POS_OP] === '&')) && (!$context['flags']['else'] || !isset($vars[0][0]) || ($vars[0][0] !== 'else')) || ($context['flags']['nostd'] > 0));
632
633        $inlinepartial = static::inlinePartial($context, $vars);
634        $partialblock = static::partialBlock($context, $vars);
635
636        if ($partialblock || $inlinepartial) {
637            $context['stack'] = array_slice($context['stack'], 0, -4);
638            static::pushPartial($context, $context['currentToken'][Token::POS_LOTHER] . $context['currentToken'][Token::POS_LSPACE] . Token::toString($context['currentToken']));
639            $context['currentToken'][Token::POS_LOTHER] = '';
640            $context['currentToken'][Token::POS_LSPACE] = '';
641            return;
642        }
643
644        if (static::operator($token[Token::POS_OP], $context, $vars)) {
645            return isset($token[Token::POS_BACKFILL]) ? null : array($raw, $vars);
646        }
647
648        if (count($vars) == 0) {
649            return $context['error'][] = 'Wrong variable naming in ' . Token::toString($token);
650        }
651
652        if (!isset($vars[0])) {
653            return $context['error'][] = 'Do not support name=value in ' . Token::toString($token) . ', you should use it after a custom helper.';
654        }
655
656        $context['usedFeature'][$raw ? 'raw' : 'enc']++;
657
658        foreach ($vars as $var) {
659            if (!isset($var[0]) || ($var[0] === 0)) {
660                if ($context['level'] == 0) {
661                    $context['usedFeature']['rootthis']++;
662                }
663                $context['usedFeature']['this']++;
664            }
665        }
666
667        if (!isset($vars[0][0])) {
668            return array($raw, $vars);
669        }
670
671        if (($vars[0][0] === 'else') && $context['flags']['else']) {
672            static::doElse($context, $vars);
673            return array($raw, $vars);
674        }
675
676        if (!static::helper($context, $vars)) {
677            static::lookup($context, $vars);
678            static::log($context, $vars);
679        }
680
681        return array($raw, $vars);
682    }
683
684    /**
685     * Return 1 or larger number when else token detected
686     *
687     * @param array<string,array|string|integer> $context current compile context
688     * @param array<boolean|integer|string|array> $vars parsed arguments list
689     *
690     * @return integer Return 1 or larger number when else token detected
691     */
692    protected static function doElse(&$context, $vars)
693    {
694        if ($context['level'] == 0) {
695            $context['error'][] = '{{else}} only valid in if, unless, each, and #section context';
696        }
697
698        if (isset($vars[1][0])) {
699            $token = $context['currentToken'];
700            $context['currentToken'][Token::POS_INNERTAG] = 'else';
701            $context['currentToken'][Token::POS_RSPACE] = "{{#{$vars[1][0]} " . preg_replace('/^\\s*else\\s+' . $vars[1][0] . '\\s*/', '', $token[Token::POS_INNERTAG]) . '}}' . $context['currentToken'][Token::POS_RSPACE];
702            array_unshift($context['elselvl'][0], $vars[1][0]);
703            $context['elsechain'] = true;
704        }
705
706        return ++$context['usedFeature']['else'];
707    }
708
709    /**
710     * Return true when this is {{log ...}}
711     *
712     * @param array<string,array|string|integer> $context current compile context
713     * @param array<boolean|integer|string|array> $vars parsed arguments list
714     *
715     * @return boolean|null Return true when it is custom helper
716     */
717    public static function log(&$context, $vars)
718    {
719        if (isset($vars[0][0]) && ($vars[0][0] === 'log')) {
720            if (!$context['flags']['nohbh']) {
721                if (count($vars) < 2) {
722                    $context['error'][] = "No argument after {{log}} !";
723                }
724                $context['usedFeature']['log']++;
725                return true;
726            }
727        }
728    }
729
730    /**
731     * Return true when this is {{lookup ...}}
732     *
733     * @param array<string,array|string|integer> $context current compile context
734     * @param array<boolean|integer|string|array> $vars parsed arguments list
735     *
736     * @return boolean|null Return true when it is custom helper
737     */
738    public static function lookup(&$context, $vars)
739    {
740        if (isset($vars[0][0]) && ($vars[0][0] === 'lookup')) {
741            if (!$context['flags']['nohbh']) {
742                if (count($vars) < 2) {
743                    $context['error'][] = "No argument after {{lookup}} !";
744                } elseif (count($vars) < 3) {
745                    $context['error'][] = "{{lookup}} requires 2 arguments !";
746                }
747                $context['usedFeature']['lookup']++;
748                return true;
749            }
750        }
751    }
752
753    /**
754     * Return true when the name is listed in helper table
755     *
756     * @param array<string,array|string|integer> $context current compile context
757     * @param array<boolean|integer|string|array> $vars parsed arguments list
758     * @param boolean $checkSubexp true when check for subexpression
759     *
760     * @return boolean Return true when it is custom helper
761     */
762    public static function helper(&$context, $vars, $checkSubexp = false)
763    {
764        if (static::resolveHelper($context, $vars)) {
765            $context['usedFeature']['helper']++;
766            return true;
767        }
768
769        if ($checkSubexp) {
770            switch ($vars[0][0]) {
771                case 'if':
772                case 'unless':
773                case 'with':
774                case 'each':
775                case 'lookup':
776                    return $context['flags']['nohbh'] ? false : true;
777            }
778        }
779
780        return false;
781    }
782
783    /**
784     * use helperresolver to resolve helper, return true when helper founded
785     *
786     * @param array<string,array|string|integer> $context Current context of compiler progress.
787     * @param array<boolean|integer|string|array> $vars parsed arguments list
788     *
789     * @return boolean $found helper exists or not
790     */
791    public static function resolveHelper(&$context, &$vars)
792    {
793        if (count($vars[0]) !== 1) {
794            return false;
795        }
796        if (isset($context['helpers'][$vars[0][0]])) {
797            return true;
798        }
799
800        if ($context['helperresolver']) {
801            $helper = $context['helperresolver']($context, $vars[0][0]);
802            if ($helper) {
803                $context['helpers'][$vars[0][0]] = $helper;
804                return true;
805            }
806        }
807
808        return false;
809    }
810
811    /**
812     * detect for block custom helper
813     *
814     * @param array<string,array|string|integer> $context current compile context
815     * @param array<boolean|integer|string|array> $vars parsed arguments list
816     *
817     * @return boolean|null Return true when this token is block custom helper
818     */
819    protected static function isBlockHelper($context, $vars)
820    {
821        if (!isset($vars[0][0])) {
822            return;
823        }
824
825        if (!static::resolveHelper($context, $vars)) {
826            return;
827        }
828
829        return true;
830    }
831
832    /**
833     * validate inline partial
834     *
835     * @param array<string,array|string|integer> $context current compile context
836     * @param array<boolean|integer|string|array> $vars parsed arguments list
837     *
838     * @return boolean Return true always
839     */
840    protected static function inline(&$context, $vars)
841    {
842        if (!$context['flags']['runpart']) {
843            $context['error'][] = "Do not support {{#*{$context['currentToken'][Token::POS_INNERTAG]}}}, you should do compile with LightnCandy::FLAG_RUNTIMEPARTIAL flag";
844        }
845        if (!isset($vars[0][0]) || ($vars[0][0] !== 'inline')) {
846            $context['error'][] = "Do not support {{#*{$context['currentToken'][Token::POS_INNERTAG]}}}, now we only support {{#*inline \"partialName\"}}template...{{/inline}}";
847        }
848        if (!isset($vars[1][0])) {
849            $context['error'][] = "Error in {{#*{$context['currentToken'][Token::POS_INNERTAG]}}}: inline require 1 argument for partial name!";
850        }
851        return true;
852    }
853
854    /**
855     * validate partial
856     *
857     * @param array<string,array|string|integer> $context current compile context
858     * @param array<boolean|integer|string|array> $vars parsed arguments list
859     *
860     * @return integer|boolean Return 1 or larger number for runtime partial, return true for other case
861     */
862    protected static function partial(&$context, $vars)
863    {
864        if (Parser::isSubExp($vars[0])) {
865            if ($context['flags']['runpart']) {
866                return $context['usedFeature']['dynpartial']++;
867            } else {
868                $context['error'][] = "You use dynamic partial name as '{$vars[0][2]}', this only works with option FLAG_RUNTIMEPARTIAL enabled";
869                return true;
870            }
871        } else {
872            if ($context['currentToken'][Token::POS_OP] !== '#>') {
873                Partial::read($context, $vars[0][0]);
874            }
875        }
876        if (!$context['flags']['runpart']) {
877            $named = count(array_diff_key($vars, array_keys(array_keys($vars)))) > 0;
878            if ($named || (count($vars) > 1)) {
879                $context['error'][] = "Do not support {{>{$context['currentToken'][Token::POS_INNERTAG]}}}, you should do compile with LightnCandy::FLAG_RUNTIMEPARTIAL flag";
880            }
881        }
882
883        return true;
884    }
885
886    /**
887     * Modify $token when spacing rules matched.
888     *
889     * @param array<string> $token detected handlebars {{ }} token
890     * @param array<string,array|string|integer> $context current compile context
891     * @param boolean $nost do not do stand alone logic
892     *
893     * @return string|null Return compiled code segment for the token
894     */
895    protected static function spacing(&$token, &$context, $nost = false)
896    {
897        // left line change detection
898        $lsp = preg_match('/^(.*)(\\r?\\n)([ \\t]*?)$/s', $token[Token::POS_LSPACE], $lmatch);
899        $ind = $lsp ? $lmatch[3] : $token[Token::POS_LSPACE];
900        // right line change detection
901        $rsp = preg_match('/^([ \\t]*?)(\\r?\\n)(.*)$/s', $token[Token::POS_RSPACE], $rmatch);
902        $st = true;
903        // setup ahead flag
904        $ahead = $context['tokens']['ahead'];
905        $context['tokens']['ahead'] = preg_match('/^[^\n]*{{/s', $token[Token::POS_RSPACE] . $token[Token::POS_ROTHER]);
906        // reset partial indent
907        $context['tokens']['partialind'] = '';
908        // same tags in the same line , not standalone
909        if (!$lsp && $ahead) {
910            $st = false;
911        }
912        if ($nost) {
913            $st = false;
914        }
915        // not standalone because other things in the same line ahead
916        if ($token[Token::POS_LOTHER] && !$token[Token::POS_LSPACE]) {
917            $st = false;
918        }
919        // not standalone because other things in the same line behind
920        if ($token[Token::POS_ROTHER] && !$token[Token::POS_RSPACE]) {
921            $st = false;
922        }
923        if ($st && (
924            ($lsp && $rsp) // both side cr
925                || ($rsp && !$token[Token::POS_LOTHER]) // first line without left
926                || ($lsp && !$token[Token::POS_ROTHER]) // final line
927            )) {
928            // handle partial
929            if ($token[Token::POS_OP] === '>') {
930                if (!$context['flags']['noind']) {
931                    $context['tokens']['partialind'] = $token[Token::POS_LSPACECTL] ? '' : $ind;
932                    $token[Token::POS_LSPACE] = (isset($lmatch[2]) ? ($lmatch[1] . $lmatch[2]) : '');
933                }
934            } else {
935                $token[Token::POS_LSPACE] = (isset($lmatch[2]) ? ($lmatch[1] . $lmatch[2]) : '');
936            }
937            $token[Token::POS_RSPACE] = isset($rmatch[3]) ? $rmatch[3] : '';
938        }
939
940        // Handle space control.
941        if ($token[Token::POS_LSPACECTL]) {
942            $token[Token::POS_LSPACE] = '';
943        }
944        if ($token[Token::POS_RSPACECTL]) {
945            $token[Token::POS_RSPACE] = '';
946        }
947    }
948}
949