      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: