GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Validator   F
last analyzed

Complexity

Total Complexity 195

Size/Duplication

Total Lines 924
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 99.75%

Importance

Changes 0
Metric Value
wmc 195
lcom 1
cbo 5
dl 0
loc 924
ccs 405
cts 406
cp 0.9975
rs 1.676
c 0
b 0
f 0

32 Methods

Rating   Name   Duplication   Size   Complexity  
B verify() 0 38 11
A pushLeft() 0 6 1
A pushPartial() 0 8 1
B pushToken() 0 24 6
A inlinePartial() 0 19 4
B partialBlock() 0 26 7
B blockBegin() 0 15 7
A section() 0 12 4
A with() 0 5 1
A unless() 0 5 1
A doIf() 0 5 1
A invertedSection() 0 4 1
C blockEnd() 0 46 14
D token() 0 77 25
A doElse() 0 16 3
B helper() 0 20 9
A resolveHelper() 0 19 5
A isBlockHelper() 0 12 3
A inline() 0 13 5
B partial() 0 23 7
A pushStack() 0 8 1
A delimiter() 0 13 5
C operator() 0 60 16
A doElseChain() 0 8 2
A builtin() 0 13 4
A blockCustomHelper() 0 8 3
A isDelimiter() 0 8 2
B rawblock() 0 31 9
A comment() 0 7 2
A log() 0 12 5
A lookup() 0 14 6
F spacing() 0 53 24

How to fix   Complexity   

Complex Class

Complex classes like Validator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Validator, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
4
MIT License
5
Copyright 2013-2020 Zordius Chen. All Rights Reserved.
6
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:
7
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
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.
9
10
Origin: https://github.com/zordius/lightncandy
11
*/
12
13
/**
14
 * file to keep LightnCandy Validator
15
 *
16
 * @package    LightnCandy
17
 * @author     Zordius <[email protected]>
18
 */
19
20
namespace LightnCandy;
21
22
/**
23
 * LightnCandy Validator
24
 */
25
class 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 795
    public static function verify(&$context, $template)
34
    {
35 795
        $template = SafeString::stripExtendedComments($template);
36 795
        $context['level'] = 0;
37 795
        Parser::setDelimiter($context);
38
39 795
        while (preg_match($context['tokens']['search'], $template, $matches)) {
40
            // Skip a token when it is slash escaped
41 784
            if ($context['flags']['slash'] && ($matches[Token::POS_LSPACE] === '') && preg_match('/^(.*?)(\\\\+)$/s', $matches[Token::POS_LOTHER], $escmatch)) {
42 4
                if (strlen($escmatch[2]) % 4) {
43 2
                    static::pushToken($context, substr($matches[Token::POS_LOTHER], 0, -2) . $context['tokens']['startchar']);
44 2
                    $matches[Token::POS_BEGINTAG] = substr($matches[Token::POS_BEGINTAG], 1);
45 2
                    $template = implode('', array_slice($matches, Token::POS_BEGINTAG));
46 2
                    continue;
47
                } else {
48 2
                    $matches[Token::POS_LOTHER] = $escmatch[1] . str_repeat('\\', strlen($escmatch[2]) / 2);
49
                }
50
            }
51 782
            $context['tokens']['count']++;
52 782
            $V = static::token($matches, $context);
53 782
            static::pushLeft($context);
54 782
            if ($V) {
55 743
                if (is_array($V)) {
56 736
                    array_push($V, $matches, $context['tokens']['partialind']);
57
                }
58 743
                static::pushToken($context, $V);
59
            }
60 782
            $template = "{$matches[Token::POS_RSPACE]}{$matches[Token::POS_ROTHER]}";
61
        }
62 795
        static::pushToken($context, $template);
63
64 795
        if ($context['level'] > 0) {
65 8
            array_pop($context['stack']);
66 8
            array_pop($context['stack']);
67 8
            $token = array_pop($context['stack']);
68 8
            $context['error'][] = 'Unclosed token ' . ($context['rawblock'] ? "{{{{{$token}}}}}" : ($context['partialblock'] ? "{{#>{$token}}}" : "{{#{$token}}}")) . ' !!';
69
        }
70 795
    }
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 782
    protected static function pushLeft(&$context)
78
    {
79 782
        $L = $context['currentToken'][Token::POS_LOTHER] . $context['currentToken'][Token::POS_LSPACE];
80 782
        static::pushToken($context, $L);
81 782
        $context['currentToken'][Token::POS_LOTHER] = $context['currentToken'][Token::POS_LSPACE] = '';
82 782
    }
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 784
    protected static function pushPartial(&$context, $append)
91
    {
92
        $appender = function (&$p) use ($append) {
93 21
            $p .= $append;
94 784
        };
95 784
        array_walk($context['inlinepartial'], $appender);
96 784
        array_walk($context['partialblock'], $appender);
97 784
    }
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 795
    protected static function pushToken(&$context, $token)
106
    {
107 795
        if ($token === '') {
108 655
            return;
109
        }
110 784
        if (is_string($token)) {
111 530
            static::pushPartial($context, $token);
112 530
            if (is_string(end($context['parsed'][0]))) {
113 33
                $context['parsed'][0][key($context['parsed'][0])] .= $token;
114 530
                return;
115
            }
116
        } else {
117 736
            static::pushPartial($context, Token::toString($context['currentToken']));
118 736
            switch ($context['currentToken'][Token::POS_OP]) {
119 736
            case '#*':
120 16
                array_unshift($context['inlinepartial'], '');
121 16
                break;
122 733
            case '#>':
123 28
                array_unshift($context['partialblock'], '');
124 28
                break;
125
            }
126
        }
127 784
        $context['parsed'][0][] = $token;
128 784
    }
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 395
    protected static function pushStack(&$context, $operation, $vars)
138
    {
139 395
        list($levels, $spvar, $var) = Expression::analyze($context, $vars[0]);
140 395
        $context['stack'][] = $context['currentToken'][Token::POS_INNERTAG];
141 395
        $context['stack'][] = Expression::toString($levels, $spvar, $var);
142 395
        $context['stack'][] = $operation;
143 395
        $context['level']++;
144 395
    }
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 783
    protected static function delimiter($token, &$context)
159
    {
160
        // {{ }}} or {{{ }} are invalid
161 783
        if (strlen($token[Token::POS_BEGINRAW]) !== strlen($token[Token::POS_ENDRAW])) {
162 6
            $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 6
            return true;
164
        }
165
        // {{{# }}} or {{{! }}} or {{{/ }}} or {{{^ }}} are invalid.
166 777
        if ((strlen($token[Token::POS_BEGINRAW]) == 1) && $token[Token::POS_OP] && ($token[Token::POS_OP] !== '&')) {
167 5
            $context['error'][] = 'Bad token ' . Token::toString($token) . ' ! Do you mean ' . Token::toString($token, array(Token::POS_BEGINRAW => '', Token::POS_ENDRAW => '')) . ' ?';
168 5
            return true;
169
        }
170 773
    }
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 744
    protected static function operator($operator, &$context, &$vars)
194
    {
195 744
        switch ($operator) {
196 744
            case '#*':
197 16
                if (!$context['compile']) {
198 16
                    $context['stack'][] = count($context['parsed'][0]) + ($context['currentToken'][Token::POS_LOTHER] . $context['currentToken'][Token::POS_LSPACE] === '' ? 0 : 1);
199 16
                    static::pushStack($context, '#*', $vars);
200
                }
201 16
                return static::inline($context, $vars);
202
203 741
            case '#>':
204 28
                if (!$context['compile']) {
205 28
                    $context['stack'][] = count($context['parsed'][0]) + ($context['currentToken'][Token::POS_LOTHER] . $context['currentToken'][Token::POS_LSPACE] === '' ? 0 : 1);
206 28
                    $vars[Parser::PARTIALBLOCK] = ++$context['usedFeature']['pblock'];
207 28
                    static::pushStack($context, '#>', $vars);
208
                }
209
                // no break
210 733
            case '>':
211 119
                return static::partial($context, $vars);
212
213 685
            case '^':
214 64
                if (!isset($vars[0][0])) {
215 24
                    if (!$context['flags']['else']) {
216 1
                        $context['error'][] = 'Do not support {{^}}, you should do compile with LightnCandy::FLAG_ELSE flag';
217 1
                        return;
218
                    } else {
219 23
                        return static::doElse($context, $vars);
220
                    }
221
                }
222
223 40
                static::doElseChain($context);
224
225 40
                if (static::isBlockHelper($context, $vars)) {
226 1
                    static::pushStack($context, '#', $vars);
227 1
                    return static::blockCustomHelper($context, $vars, true);
228
                }
229
230 39
                static::pushStack($context, '^', $vars);
231 39
                return static::invertedSection($context, $vars);
232
233 685
            case '/':
234 362
                $r = static::blockEnd($context, $vars);
235 362
                if ($r !== Token::POS_BACKFILL) {
236 362
                    array_pop($context['stack']);
237 362
                    array_pop($context['stack']);
238 362
                    array_pop($context['stack']);
239
                }
240 362
                return $r;
241
242 660
            case '#':
243 338
                static::doElseChain($context);
244 338
                static::pushStack($context, '#', $vars);
245
246 338
                if (static::isBlockHelper($context, $vars)) {
247 64
                    return static::blockCustomHelper($context, $vars);
248
                }
249
250 283
                return static::blockBegin($context, $vars);
251
        }
252 556
    }
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 743
    protected static function inlinePartial(&$context, $vars)
263
    {
264 743
        $ended = false;
265 743
        if ($context['currentToken'][Token::POS_OP] === '/') {
266 388
            if (static::blockEnd($context, $vars, '#*') !== null) {
267 16
                $context['usedFeature']['inlpartial']++;
268 16
                $tmpl = array_shift($context['inlinepartial']) . $context['currentToken'][Token::POS_LOTHER] . $context['currentToken'][Token::POS_LSPACE];
269 16
                $c = $context['stack'][count($context['stack']) - 4];
270 16
                $context['parsed'][0] = array_slice($context['parsed'][0], 0, $c + 1);
271 16
                $P = &$context['parsed'][0][$c];
272 16
                if (isset($P[1][1][0])) {
273 15
                    $context['usedPartial'][$P[1][1][0]] = $tmpl;
274 15
                    $P[1][0][0] = Partial::compileDynamic($context, $P[1][1][0]);
275
                }
276 16
                $ended = true;
277
            }
278
        }
279 743
        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 743
    protected static function partialBlock(&$context, $vars)
291
    {
292 743
        $ended = false;
293 743
        if ($context['currentToken'][Token::POS_OP] === '/') {
294 388
            if (static::blockEnd($context, $vars, '#>') !== null) {
295 27
                $c = $context['stack'][count($context['stack']) - 4];
296 27
                $context['parsed'][0] = array_slice($context['parsed'][0], 0, $c + 1);
297 27
                $found = Partial::resolve($context, $vars[0][0]) !== null;
298 27
                $v = $found ? "@partial-block{$context['parsed'][0][$c][1][Parser::PARTIALBLOCK]}" : "{$vars[0][0]}";
299 27
                if (count($context['partialblock']) == 1) {
300 27
                    $tmpl = $context['partialblock'][0] . $context['currentToken'][Token::POS_LOTHER] . $context['currentToken'][Token::POS_LSPACE];
301 27
                    if ($found) {
302 19
                        $context['partials'][$v] = $tmpl;
303
                    }
304 27
                    $context['usedPartial'][$v] = $tmpl;
305 27
                    Partial::compileDynamic($context, $v);
306 27
                    if ($found) {
307 19
                        Partial::read($context, $vars[0][0]);
308
                    }
309
                }
310 27
                array_shift($context['partialblock']);
311 27
                $ended = true;
312
            }
313
        }
314 743
        return $ended;
315
    }
316
317
    /**
318
     * handle else chain
319
     *
320
     * @param array<string,array|string|integer> $context current compile context
321
     */
322 367
    protected static function doElseChain(&$context)
323
    {
324 367
        if ($context['elsechain']) {
325 14
            $context['elsechain'] = false;
326
        } else {
327 367
            array_unshift($context['elselvl'], array());
328
        }
329 367
    }
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 282
    protected static function blockBegin(&$context, $vars)
340
    {
341 282
        switch ((isset($vars[0][0]) && is_string($vars[0][0])) ? $vars[0][0] : null) {
342 282
            case 'with':
343 36
                return static::with($context, $vars);
344 253
            case 'each':
345 60
                return static::section($context, $vars, true);
346 204
            case 'unless':
347 7
                return static::unless($context, $vars);
348 198
            case 'if':
349 80
                return static::doIf($context, $vars);
350
            default:
351 125
                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 166
    protected static function builtin(&$context, $vars)
362
    {
363 166
        if ($context['flags']['nohbh']) {
364 8
            if (isset($vars[1][0])) {
365 8
                $context['error'][] = "Do not support {{#{$vars[0][0]} var}} because you compile with LightnCandy::FLAG_NOHBHELPERS flag";
366
            }
367
        } else {
368 158
            if (count($vars) < 2) {
369 5
                $context['error'][] = "No argument after {{#{$vars[0][0]}}} !";
370
            }
371
        }
372 166
        $context['usedFeature'][$vars[0][0]]++;
373 166
    }
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 185
    protected static function section(&$context, $vars, $isEach = false)
385
    {
386 185
        if ($isEach) {
387 60
            static::builtin($context, $vars);
388
        } else {
389 125
            if ((count($vars) > 1) && !$context['flags']['lambda']) {
390 1
                $context['error'][] = "Custom helper not found: {$vars[0][0]} in " . Token::toString($context['currentToken']) . ' !';
391
            }
392 125
            $context['usedFeature']['sec']++;
393
        }
394 185
        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 36
    protected static function with(&$context, $vars)
406
    {
407 36
        static::builtin($context, $vars);
408 36
        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 7
    protected static function unless(&$context, $vars)
420
    {
421 7
        static::builtin($context, $vars);
422 7
        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 80
    protected static function doIf(&$context, $vars)
434
    {
435 80
        static::builtin($context, $vars);
436 80
        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 64
    protected static function blockCustomHelper(&$context, $vars, $inverted = false)
449
    {
450 64
        if (is_string($vars[0][0])) {
451 64
            if (static::resolveHelper($context, $vars)) {
452 64
                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 38
    protected static function invertedSection(&$context, $vars)
466
    {
467 38
        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 388
    protected static function blockEnd(&$context, &$vars, $match = null)
480
    {
481 388
        $c = count($context['stack']) - 2;
482 388
        $pop = ($c >= 0) ? $context['stack'][$c + 1] : '';
483 388
        if (($match !== null) && ($match !== $pop)) {
484 388
            return;
485
        }
486
        // if we didn't match our $pop, we didn't actually do a level, so only subtract a level here
487 388
        $context['level']--;
488 388
        $pop2 = ($c >= 0) ? $context['stack'][$c]: '';
489 388
        switch ($context['currentToken'][Token::POS_INNERTAG]) {
490 388
            case 'with':
491 40
                if (!$context['flags']['nohbh']) {
492 38
                    if ($pop2 !== '[with]') {
493 1
                        $context['error'][] = 'Unexpect token: {{/with}} !';
494 1
                        return;
495
                    }
496
                }
497 39
                return true;
498
        }
499
500 366
        switch ($pop) {
501 366
            case '#':
502 78
            case '^':
503 331
                $elsechain = array_shift($context['elselvl']);
504 331
                if (isset($elsechain[0])) {
505
                    // we need to repeat a level due to else chains: {{else if}}
506 14
                    $context['level']++;
507 14
                    $context['currentToken'][Token::POS_RSPACE] = $context['currentToken'][Token::POS_BACKFILL] = '{{/' . implode('}}{{/', $elsechain) . '}}' . Token::toString($context['currentToken']) . $context['currentToken'][Token::POS_RSPACE];
508 14
                    return Token::POS_BACKFILL;
509
                }
510
                // no break
511 40
            case '#>':
512 17
            case '#*':
513 365
                list($levels, $spvar, $var) = Expression::analyze($context, $vars[0]);
514 365
                $v = Expression::toString($levels, $spvar, $var);
515 365
                if ($pop2 !== $v) {
516 2
                    $context['error'][] = 'Unexpect token ' . Token::toString($context['currentToken']) . " ! Previous token {{{$pop}$pop2}} is not closed";
517 2
                    return;
518
                }
519 364
                return true;
520
            default:
521 1
                $context['error'][] = 'Unexpect token: ' . Token::toString($context['currentToken']) . ' !';
522 1
                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 772
    protected static function isDelimiter(&$context)
534
    {
535 772
        if (preg_match('/^=\s*([^ ]+)\s+([^ ]+)\s*=$/', $context['currentToken'][Token::POS_INNERTAG], $matched)) {
536 15
            $context['usedFeature']['delimiter']++;
537 15
            Parser::setDelimiter($context, $matched[1], $matched[2]);
538 15
            return true;
539
        }
540 763
    }
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 782
    protected static function rawblock(&$token, &$context)
551
    {
552 782
        $inner = $token[Token::POS_INNERTAG];
553 782
        trim($inner);
554
555
        // skip parse when inside raw block
556 782
        if ($context['rawblock'] && !(($token[Token::POS_BEGINRAW] === '{{') && ($token[Token::POS_OP] === '/') && ($context['rawblock'] === $inner))) {
557 6
            return true;
558
        }
559
560 782
        $token[Token::POS_INNERTAG] = $inner;
561
562
        // Handle raw block
563 782
        if ($token[Token::POS_BEGINRAW] === '{{') {
564 9
            if ($token[Token::POS_ENDRAW] !== '}}') {
565 1
                $context['error'][] = 'Bad token ' . Token::toString($token) . ' ! Do you mean ' . Token::toString($token, array(Token::POS_ENDRAW => '}}')) . ' ?';
566
            }
567 9
            if ($context['rawblock']) {
568 6
                Parser::setDelimiter($context);
569 6
                $context['rawblock'] = false;
570
            } else {
571 9
                if ($token[Token::POS_OP]) {
572 1
                    $context['error'][] = "Wrong raw block begin with " . Token::toString($token) . ' ! Remove "' . $token[Token::POS_OP] . '" to fix this issue.';
573
                }
574 9
                $context['rawblock'] = $token[Token::POS_INNERTAG];
575 9
                Parser::setDelimiter($context);
576 9
                $token[Token::POS_OP] = '#';
577
            }
578 9
            $token[Token::POS_ENDRAW] = '}}';
579
        }
580 782
    }
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 763
    protected static function comment(&$token, &$context)
591
    {
592 763
        if ($token[Token::POS_OP] === '!') {
593 26
            $context['usedFeature']['comment']++;
594 26
            return true;
595
        }
596 743
    }
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 782
    protected static function token(&$token, &$context)
607
    {
608 782
        $context['currentToken'] = &$token;
609
610 782
        if (static::rawblock($token, $context)) {
611 6
            return Token::toString($token);
612
        }
613
614 782
        if (static::delimiter($token, $context)) {
615 10
            return;
616
        }
617
618 772
        if (static::isDelimiter($context)) {
619 15
            static::spacing($token, $context);
620 15
            return;
621
        }
622
623 763
        if (static::comment($token, $context)) {
624 26
            static::spacing($token, $context);
625 26
            return;
626
        }
627
628 743
        list($raw, $vars) = Parser::parse($token, $context);
629
630
        // Handle spacing (standalone tags, partial indent)
631 743
        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 743
        $inlinepartial = static::inlinePartial($context, $vars);
634 743
        $partialblock = static::partialBlock($context, $vars);
635
636 743
        if ($partialblock || $inlinepartial) {
637 39
            $context['stack'] = array_slice($context['stack'], 0, -4);
638 39
            static::pushPartial($context, $context['currentToken'][Token::POS_LOTHER] . $context['currentToken'][Token::POS_LSPACE] . Token::toString($context['currentToken']));
639 39
            $context['currentToken'][Token::POS_LOTHER] = '';
640 39
            $context['currentToken'][Token::POS_LSPACE] = '';
641 39
            return;
642
        }
643
644 743
        if (static::operator($token[Token::POS_OP], $context, $vars)) {
645 453
            return isset($token[Token::POS_BACKFILL]) ? null : array($raw, $vars);
646
        }
647
648 560
        if (count($vars) == 0) {
649 6
            return $context['error'][] = 'Wrong variable naming in ' . Token::toString($token);
650
        }
651
652 554
        if (!isset($vars[0])) {
653 1
            return $context['error'][] = 'Do not support name=value in ' . Token::toString($token) . ', you should use it after a custom helper.';
654
        }
655
656 553
        $context['usedFeature'][$raw ? 'raw' : 'enc']++;
657
658 553
        foreach ($vars as $var) {
659 553
            if (!isset($var[0]) || ($var[0] === 0)) {
660 78
                if ($context['level'] == 0) {
661 26
                    $context['usedFeature']['rootthis']++;
662
                }
663 78
                $context['usedFeature']['this']++;
664
            }
665
        }
666
667 553
        if (!isset($vars[0][0])) {
668 58
            return array($raw, $vars);
669
        }
670
671 519
        if (($vars[0][0] === 'else') && $context['flags']['else']) {
672 39
            static::doElse($context, $vars);
673 39
            return array($raw, $vars);
674
        }
675
676 492
        if (!static::helper($context, $vars)) {
677 374
            static::lookup($context, $vars);
678 374
            static::log($context, $vars);
679
        }
680
681 492
        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 61
    protected static function doElse(&$context, $vars)
693
    {
694 61
        if ($context['level'] == 0) {
695 1
            $context['error'][] = '{{else}} only valid in if, unless, each, and #section context';
696
        }
697
698 61
        if (isset($vars[1][0])) {
699 14
            $token = $context['currentToken'];
700 14
            $context['currentToken'][Token::POS_INNERTAG] = 'else';
701 14
            $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 14
            array_unshift($context['elselvl'][0], $vars[1][0]);
703 14
            $context['elsechain'] = true;
704
        }
705
706 61
        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 374
    public static function log(&$context, $vars)
718
    {
719 374
        if (isset($vars[0][0]) && ($vars[0][0] === 'log')) {
720 3
            if (!$context['flags']['nohbh']) {
721 3
                if (count($vars) < 2) {
722 1
                    $context['error'][] = "No argument after {{log}} !";
723
                }
724 3
                $context['usedFeature']['log']++;
725 3
                return true;
726
            }
727
        }
728 371
    }
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 374
    public static function lookup(&$context, $vars)
739
    {
740 374
        if (isset($vars[0][0]) && ($vars[0][0] === 'lookup')) {
741 7
            if (!$context['flags']['nohbh']) {
742 7
                if (count($vars) < 2) {
743 1
                    $context['error'][] = "No argument after {{lookup}} !";
744 6
                } elseif (count($vars) < 3) {
745 1
                    $context['error'][] = "{{lookup}} requires 2 arguments !";
746
                }
747 7
                $context['usedFeature']['lookup']++;
748 7
                return true;
749
            }
750
        }
751 367
    }
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 500
    public static function helper(&$context, $vars, $checkSubexp = false)
763
    {
764 500
        if (static::resolveHelper($context, $vars)) {
765 134
            $context['usedFeature']['helper']++;
766 134
            return true;
767
        }
768
769 377
        if ($checkSubexp) {
770 5
            switch ($vars[0][0]) {
771 5
                case 'if':
772 5
                case 'unless':
773 5
                case 'with':
774 5
                case 'each':
775 5
                case 'lookup':
776 3
                    return $context['flags']['nohbh'] ? false : true;
777
            }
778
        }
779
780 376
        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 669
    public static function resolveHelper(&$context, &$vars)
792
    {
793 669
        if (count($vars[0]) !== 1) {
794 100
            return false;
795
        }
796 611
        if (isset($context['helpers'][$vars[0][0]])) {
797 186
            return true;
798
        }
799
800 476
        if ($context['helperresolver']) {
801 3
            $helper = $context['helperresolver']($context, $vars[0][0]);
802 3
            if ($helper) {
803 3
                $context['helpers'][$vars[0][0]] = $helper;
804 3
                return true;
805
            }
806
        }
807
808 473
        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 367
    protected static function isBlockHelper($context, $vars)
820
    {
821 367
        if (!isset($vars[0][0])) {
822 4
            return;
823
        }
824
825 364
        if (!static::resolveHelper($context, $vars)) {
826 308
            return;
827
        }
828
829 64
        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 16
    protected static function inline(&$context, $vars)
841
    {
842 16
        if (!$context['flags']['runpart']) {
843 1
            $context['error'][] = "Do not support {{#*{$context['currentToken'][Token::POS_INNERTAG]}}}, you should do compile with LightnCandy::FLAG_RUNTIMEPARTIAL flag";
844
        }
845 16
        if (!isset($vars[0][0]) || ($vars[0][0] !== 'inline')) {
846 1
            $context['error'][] = "Do not support {{#*{$context['currentToken'][Token::POS_INNERTAG]}}}, now we only support {{#*inline \"partialName\"}}template...{{/inline}}";
847
        }
848 16
        if (!isset($vars[1][0])) {
849 1
            $context['error'][] = "Error in {{#*{$context['currentToken'][Token::POS_INNERTAG]}}}: inline require 1 argument for partial name!";
850
        }
851 16
        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 118
    protected static function partial(&$context, $vars)
863
    {
864 118
        if (Parser::isSubExp($vars[0])) {
865 7
            if ($context['flags']['runpart']) {
866 6
                return $context['usedFeature']['dynpartial']++;
867
            } else {
868 1
                $context['error'][] = "You use dynamic partial name as '{$vars[0][2]}', this only works with option FLAG_RUNTIMEPARTIAL enabled";
869 1
                return true;
870
            }
871
        } else {
872 111
            if ($context['currentToken'][Token::POS_OP] !== '#>') {
873 100
                Partial::read($context, $vars[0][0]);
874
            }
875
        }
876 111
        if (!$context['flags']['runpart']) {
877 12
            $named = count(array_diff_key($vars, array_keys(array_keys($vars)))) > 0;
878 12
            if ($named || (count($vars) > 1)) {
879 1
                $context['error'][] = "Do not support {{>{$context['currentToken'][Token::POS_INNERTAG]}}}, you should do compile with LightnCandy::FLAG_RUNTIMEPARTIAL flag";
880
            }
881
        }
882
883 111
        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 772
    protected static function spacing(&$token, &$context, $nost = false)
896
    {
897
        // left line change detection
898 772
        $lsp = preg_match('/^(.*)(\\r?\\n)([ \\t]*?)$/s', $token[Token::POS_LSPACE], $lmatch);
899 772
        $ind = $lsp ? $lmatch[3] : $token[Token::POS_LSPACE];
900
        // right line change detection
901 772
        $rsp = preg_match('/^([ \\t]*?)(\\r?\\n)(.*)$/s', $token[Token::POS_RSPACE], $rmatch);
902 772
        $st = true;
903
        // setup ahead flag
904 772
        $ahead = $context['tokens']['ahead'];
905 772
        $context['tokens']['ahead'] = preg_match('/^[^\n]*{{/s', $token[Token::POS_RSPACE] . $token[Token::POS_ROTHER]);
906
        // reset partial indent
907 772
        $context['tokens']['partialind'] = '';
908
        // same tags in the same line , not standalone
909 772
        if (!$lsp && $ahead) {
910 383
            $st = false;
911
        }
912 772
        if ($nost) {
913 534
            $st = false;
914
        }
915
        // not standalone because other things in the same line ahead
916 772
        if ($token[Token::POS_LOTHER] && !$token[Token::POS_LSPACE]) {
917 239
            $st = false;
918
        }
919
        // not standalone because other things in the same line behind
920 772
        if ($token[Token::POS_ROTHER] && !$token[Token::POS_RSPACE]) {
921 382
            $st = false;
922
        }
923 772
        if ($st && (
924 174
            ($lsp && $rsp) // both side cr
925 146
                || ($rsp && !$token[Token::POS_LOTHER]) // first line without left
926 772
                || ($lsp && !$token[Token::POS_ROTHER]) // final line
927
            )) {
928
            // handle partial
929 71
            if ($token[Token::POS_OP] === '>') {
930 17
                if (!$context['flags']['noind']) {
931 12
                    $context['tokens']['partialind'] = $token[Token::POS_LSPACECTL] ? '' : $ind;
932 17
                    $token[Token::POS_LSPACE] = (isset($lmatch[2]) ? ($lmatch[1] . $lmatch[2]) : '');
933
                }
934
            } else {
935 60
                $token[Token::POS_LSPACE] = (isset($lmatch[2]) ? ($lmatch[1] . $lmatch[2]) : '');
936
            }
937 71
            $token[Token::POS_RSPACE] = isset($rmatch[3]) ? $rmatch[3] : '';
938
        }
939
940
        // Handle space control.
941 772
        if ($token[Token::POS_LSPACECTL]) {
942 32
            $token[Token::POS_LSPACE] = '';
943
        }
944 772
        if ($token[Token::POS_RSPACECTL]) {
945 35
            $token[Token::POS_RSPACE] = '';
946
        }
947 772
    }
948
}
949