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