Overview

Namespaces

  • LightnCandy

Classes

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