1<?php 2/** 3 * Helpers 4 * 5 * a collection of helper function. normally a function like 6 * function ($sender, $name, $arguments) $arguments is unscaped arguments and 7 * is a string, not array 8 * 9 * @category Xamin 10 * @package Handlebars 11 * @author fzerorubigd <fzerorubigd@gmail.com> 12 * @author Behrooz Shabani <everplays@gmail.com> 13 * @author Mardix <https://github.com/mardix> 14 * @copyright 2012 (c) ParsPooyesh Co 15 * @copyright 2013 (c) Behrooz Shabani 16 * @copyright 2014 (c) Mardix 17 * @license MIT 18 * @link http://voodoophp.org/docs/handlebars 19 */ 20 21namespace Handlebars; 22 23use DateTime; 24use InvalidArgumentException; 25use Traversable; 26use LogicException; 27 28class Helpers 29{ 30 /** 31 * @var array array of helpers 32 */ 33 protected $helpers = []; 34 private $tpl = []; 35 protected $builtinHelpers = [ 36 "if", 37 "each", 38 "with", 39 "unless", 40 "bindAttr", 41 "upper", // Put all chars in uppercase 42 "lower", // Put all chars in lowercase 43 "capitalize", // Capitalize just the first word 44 "capitalize_words", // Capitalize each words 45 "reverse", // Reverse a string 46 "format_date", // Format a date 47 "inflect", // Inflect the wording based on count ie. 1 album, 10 albums 48 "default", // If a variable is null, it will use the default instead 49 "truncate", // Truncate section 50 "raw", // Return the source as is without converting 51 "repeat", // Repeat a section 52 "define", // Define a block to be used using "invoke" 53 "invoke", // Invoke a block that was defined with "define" 54 ]; 55 56 /** 57 * Create new helper container class 58 * 59 * @param array $helpers array of name=>$value helpers 60 * @throws \InvalidArgumentException when $helpers is not an array 61 * (or traversable) or helper is not a callable 62 */ 63 public function __construct($helpers = null) 64 { 65 foreach($this->builtinHelpers as $helper) { 66 $helperName = $this->underscoreToCamelCase($helper); 67 $this->add($helper, [$this, "helper{$helperName}"]); 68 } 69 70 if ($helpers != null) { 71 if (!is_array($helpers) && !$helpers instanceof Traversable) { 72 throw new InvalidArgumentException( 73 'HelperCollection constructor expects an array of helpers' 74 ); 75 } 76 foreach ($helpers as $name => $helper) { 77 $this->add($name, $helper); 78 } 79 } 80 } 81 82 /** 83 * Add a new helper to helpers 84 * 85 * @param string $name helper name 86 * @param callable $helper a function as a helper 87 * 88 * @throws \InvalidArgumentException if $helper is not a callable 89 * @return void 90 */ 91 public function add($name, $helper) 92 { 93 if (!is_callable($helper)) { 94 throw new InvalidArgumentException("$name Helper is not a callable."); 95 } 96 $this->helpers[$name] = $helper; 97 } 98 99 /** 100 * Check if $name helper is available 101 * 102 * @param string $name helper name 103 * 104 * @return boolean 105 */ 106 public function has($name) 107 { 108 return array_key_exists($name, $this->helpers); 109 } 110 111 /** 112 * Get a helper. __magic__ method :) 113 * 114 * @param string $name helper name 115 * 116 * @throws \InvalidArgumentException if $name is not available 117 * @return callable helper function 118 */ 119 public function __get($name) 120 { 121 if (!$this->has($name)) { 122 throw new InvalidArgumentException('Unknown helper :' . $name); 123 } 124 return $this->helpers[$name]; 125 } 126 127 /** 128 * Check if $name helper is available __magic__ method :) 129 * 130 * @param string $name helper name 131 * 132 * @return boolean 133 * @see Handlebras_Helpers::has 134 */ 135 public function __isset($name) 136 { 137 return $this->has($name); 138 } 139 140 /** 141 * Add a new helper to helpers __magic__ method :) 142 * 143 * @param string $name helper name 144 * @param callable $helper a function as a helper 145 * 146 * @return void 147 */ 148 public function __set($name, $helper) 149 { 150 $this->add($name, $helper); 151 } 152 153 154 /** 155 * Unset a helper 156 * 157 * @param string $name helper name to remove 158 * @return void 159 */ 160 public function __unset($name) 161 { 162 unset($this->helpers[$name]); 163 } 164 165 /** 166 * Check whether a given helper is present in the collection. 167 * 168 * @param string $name helper name 169 * @throws \InvalidArgumentException if the requested helper is not present. 170 * @return void 171 */ 172 public function remove($name) 173 { 174 if (!$this->has($name)) { 175 throw new InvalidArgumentException('Unknown helper: ' . $name); 176 } 177 unset($this->helpers[$name]); 178 } 179 180 /** 181 * Clear the helper collection. 182 * 183 * Removes all helpers from this collection 184 * 185 * @return void 186 */ 187 public function clear() 188 { 189 $this->helpers = []; 190 } 191 192 /** 193 * Check whether the helper collection is empty. 194 * 195 * @return boolean True if the collection is empty 196 */ 197 public function isEmpty() 198 { 199 return empty($this->helpers); 200 } 201 202 /** 203 * Create handler for the 'if' helper. 204 * 205 * {{#if condition}} 206 * Something here 207 * {{else if condition}} 208 * something else if here 209 * {{else if condition}} 210 * something else if here 211 * {{else}} 212 * something else here 213 * {{/if}} 214 * 215 * @param \Handlebars\Template $template template that is being rendered 216 * @param \Handlebars\Context $context context object 217 * @param array $args passed arguments to helper 218 * @param string $source part of template that is wrapped 219 * within helper 220 * 221 * @return mixed 222 */ 223 public function helperIf($template, $context, $args, $source) 224 { 225 $tpl = $template->getEngine()->loadString('{{#if ' . $args . '}}' . $source . '{{/if}}'); 226 $tree = $tpl->getTree(); 227 $tmp = $context->get($args); 228 if ($tmp) { 229 $token = 'else'; 230 foreach ($tree[0]['nodes'] as $node) { 231 $name = trim($node['name'] ?? ''); 232 if ($name && substr($name, 0, 7) == 'else if') { 233 $token = $node['name']; 234 break; 235 } 236 } 237 $template->setStopToken($token); 238 $buffer = $template->render($context); 239 $template->setStopToken(false); 240 $template->discard(); 241 return $buffer; 242 } else { 243 foreach ($tree[0]['nodes'] as $key => $node) { 244 $name = trim(isset($node['name']) ? $node['name'] : ''); 245 if ($name && substr($name, 0, 7) == 'else if') { 246 $template->setStopToken($node['name']); 247 $template->discard(); 248 $template->setStopToken(false); 249 $args = $this->parseArgs($context, substr($name, 7)); 250 $token = 'else'; 251 $remains = array_slice($tree[0]['nodes'], $key + 1); 252 foreach ($remains as $remain) { 253 $name = trim($remain['name'] ?? ''); 254 if ($name && substr($name, 0, 7) == 'else if') { 255 $token = $remain['name']; 256 break; 257 } 258 } 259 if (isset($args[0]) && $args[0]) { 260 $template->setStopToken($token); 261 $buffer = $template->render($context); 262 $template->setStopToken(false); 263 $template->discard(); 264 return $buffer; 265 } else if ($token != 'else') { 266 continue; 267 } else { 268 return $this->renderElse($template, $context); 269 } 270 } 271 } 272 return $this->renderElse($template, $context); 273 } 274 } 275 276 277 /** 278 * Create handler for the 'each' helper. 279 * example {{#each people}} {{name}} {{/each}} 280 * example with slice: {{#each people[0:10]}} {{name}} {{/each}} 281 * example with else 282 * {{#each Array}} 283 * {{.}} 284 * {{else}} 285 * Nothing found 286 * {{/each}} 287 * 288 * @param \Handlebars\Template $template template that is being rendered 289 * @param \Handlebars\Context $context context object 290 * @param array $args passed arguments to helper 291 * @param string $source part of template that is wrapped 292 * within helper 293 * 294 * @return mixed 295 */ 296 public function helperEach($template, $context, $args, $source) 297 { 298 list($keyname, $slice_start, $slice_end) = $this->extractSlice($args); 299 $tmp = $context->get($keyname); 300 301 if (is_array($tmp) || $tmp instanceof Traversable) { 302 $tmp = array_slice($tmp, $slice_start ?? 0, $slice_end, true); 303 $buffer = ''; 304 $islist = array_values($tmp) === $tmp; 305 306 if (is_array($tmp) && ! count($tmp)) { 307 return $this->renderElse($template, $context); 308 } else { 309 310 $itemCount = -1; 311 if ($islist) { 312 $itemCount = count($tmp); 313 } 314 315 foreach ($tmp as $key => $var) { 316 $tpl = clone $template; 317 if ($islist) { 318 $context->pushIndex($key); 319 320 // If data variables are enabled, push the data related to this #each context 321 if ($template->getEngine()->isDataVariablesEnabled()) { 322 $context->pushData([ 323 Context::DATA_KEY => $key, 324 Context::DATA_INDEX => $key, 325 Context::DATA_LAST => $key == ($itemCount - 1), 326 Context::DATA_FIRST => $key == 0, 327 ]); 328 } 329 } else { 330 $context->pushKey($key); 331 332 // If data variables are enabled, push the data related to this #each context 333 if ($template->getEngine()->isDataVariablesEnabled()) { 334 $context->pushData([ 335 Context::DATA_KEY => $key, 336 ]); 337 } 338 } 339 $context->push($var); 340 $tpl->setStopToken('else'); 341 $buffer .= $tpl->render($context); 342 $context->pop(); 343 if ($islist) { 344 $context->popIndex(); 345 } else { 346 $context->popKey(); 347 } 348 349 if ($template->getEngine()->isDataVariablesEnabled()) { 350 $context->popData(); 351 } 352 } 353 return $buffer; 354 } 355 } else { 356 return $this->renderElse($template, $context); 357 } 358 } 359 360 /** 361 * Applying the DRY principle here. 362 * This method help us render {{else}} portion of a block 363 * @param \Handlebars\Template $template 364 * @param \Handlebars\Context $context 365 * @return string 366 */ 367 private function renderElse($template, $context) 368 { 369 $template->setStopToken('else'); 370 $template->discard(); 371 $template->setStopToken(false); 372 return $template->render($context); 373 } 374 375 376 /** 377 * Create handler for the 'unless' helper. 378 * {{#unless condition}} 379 * Something here 380 * {{else}} 381 * something else here 382 * {{/unless}} 383 * @param \Handlebars\Template $template template that is being rendered 384 * @param \Handlebars\Context $context context object 385 * @param array $args passed arguments to helper 386 * @param string $source part of template that is wrapped 387 * within helper 388 * 389 * @return mixed 390 */ 391 public function helperUnless($template, $context, $args, $source) 392 { 393 $tmp = $context->get($args); 394 if (!$tmp) { 395 $template->setStopToken('else'); 396 $buffer = $template->render($context); 397 $template->setStopToken(false); 398 $template->discard(); 399 return $buffer; 400 } else { 401 return $this->renderElse($template, $context); 402 } 403 } 404 405 /** 406 * Create handler for the 'with' helper. 407 * Needed for compatibility with PHP 5.2 since it doesn't support anonymous 408 * functions. 409 * 410 * @param \Handlebars\Template $template template that is being rendered 411 * @param \Handlebars\Context $context context object 412 * @param array $args passed arguments to helper 413 * @param string $source part of template that is wrapped 414 * within helper 415 * 416 * @return mixed 417 */ 418 public function helperWith($template, $context, $args, $source) 419 { 420 $tmp = $context->get($args); 421 $context->push($tmp); 422 $buffer = $template->render($context); 423 $context->pop(); 424 425 return $buffer; 426 } 427 428 /** 429 * Create handler for the 'bindAttr' helper. 430 * Needed for compatibility with PHP 5.2 since it doesn't support anonymous 431 * functions. 432 * 433 * @param \Handlebars\Template $template template that is being rendered 434 * @param \Handlebars\Context $context context object 435 * @param array $args passed arguments to helper 436 * @param string $source part of template that is wrapped 437 * within helper 438 * 439 * @return mixed 440 */ 441 public function helperBindAttr($template, $context, $args, $source) 442 { 443 return $args; 444 } 445 446 /** 447 * To uppercase string 448 * 449 * {{#upper data}} 450 * 451 * @param \Handlebars\Template $template template that is being rendered 452 * @param \Handlebars\Context $context context object 453 * @param array $args passed arguments to helper 454 * @param string $source part of template that is wrapped 455 * within helper 456 * 457 * @return string 458 */ 459 public function helperUpper($template, $context, $args, $source) 460 { 461 return strtoupper($context->get($args)); 462 } 463 464 /** 465 * To lowercase string 466 * 467 * {{#lower data}} 468 * 469 * @param \Handlebars\Template $template template that is being rendered 470 * @param \Handlebars\Context $context context object 471 * @param array $args passed arguments to helper 472 * @param string $source part of template that is wrapped 473 * within helper 474 * 475 * @return string 476 */ 477 public function helperLower($template, $context, $args, $source) 478 { 479 return strtolower($context->get($args)); 480 } 481 482 /** 483 * to capitalize first letter 484 * 485 * {{#capitalize}} 486 * 487 * @param \Handlebars\Template $template template that is being rendered 488 * @param \Handlebars\Context $context context object 489 * @param array $args passed arguments to helper 490 * @param string $source part of template that is wrapped 491 * within helper 492 * 493 * @return string 494 */ 495 public function helperCapitalize($template, $context, $args, $source) 496 { 497 return ucfirst($context->get($args)); 498 } 499 500 /** 501 * To capitalize first letter in each word 502 * 503 * {{#capitalize_words data}} 504 * 505 * @param \Handlebars\Template $template template that is being rendered 506 * @param \Handlebars\Context $context context object 507 * @param array $args passed arguments to helper 508 * @param string $source part of template that is wrapped 509 * within helper 510 * 511 * @return string 512 */ 513 public function helperCapitalizeWords($template, $context, $args, $source) 514 { 515 return ucwords($context->get($args)); 516 } 517 518 /** 519 * To reverse a string 520 * 521 * {{#reverse data}} 522 * 523 * @param \Handlebars\Template $template template that is being rendered 524 * @param \Handlebars\Context $context context object 525 * @param array $args passed arguments to helper 526 * @param string $source part of template that is wrapped 527 * within helper 528 * 529 * @return string 530 */ 531 public function helperReverse($template, $context, $args, $source) 532 { 533 return strrev($context->get($args)); 534 } 535 536 /** 537 * Format a date 538 * 539 * {{#format_date date 'Y-m-d @h:i:s'}} 540 * 541 * @param \Handlebars\Template $template template that is being rendered 542 * @param \Handlebars\Context $context context object 543 * @param array $args passed arguments to helper 544 * @param string $source part of template that is wrapped 545 * within helper 546 * 547 * @return mixed 548 */ 549 public function helperFormatDate($template, $context, $args, $source) 550 { 551 preg_match("/(.*?)\s+(?:(?:\"|\')(.*?)(?:\"|\'))/", $args, $m); 552 $keyname = $m[1]; 553 $format = $m[2]; 554 555 $date = $context->get($keyname); 556 if ($format) { 557 $dt = new DateTime; 558 if (is_numeric($date)) { 559 $dt = (new DateTime)->setTimestamp($date); 560 } else { 561 $dt = new DateTime($date); 562 } 563 return $dt->format($format); 564 } else { 565 return $date; 566 } 567 } 568 569 /** 570 * {{inflect count 'album' 'albums'}} 571 * {{inflect count '%d album' '%d albums'}} 572 * 573 * @param \Handlebars\Template $template template that is being rendered 574 * @param \Handlebars\Context $context context object 575 * @param array $args passed arguments to helper 576 * @param string $source part of template that is wrapped 577 * within helper 578 * 579 * @return mixed 580 */ 581 public function helperInflect($template, $context, $args, $source) 582 { 583 preg_match("/(.*?)\s+(?:(?:\"|\')(.*?)(?:\"|\'))\s+(?:(?:\"|\')(.*?)(?:\"|\'))/", $args, $m); 584 $keyname = $m[1]; 585 $singular = $m[2]; 586 $plurial = $m[3]; 587 $value = $context->get($keyname); 588 $inflect = ($value <= 1) ? $singular : $plurial; 589 return sprintf($inflect, $value); 590 } 591 592 /** 593 * Provide a default fallback 594 * 595 * {{default title "No title available"}} 596 * 597 * @param \Handlebars\Template $template template that is being rendered 598 * @param \Handlebars\Context $context context object 599 * @param array $args passed arguments to helper 600 * @param string $source part of template that is wrapped 601 * within helper 602 * 603 * @return string 604 */ 605 public function helperDefault($template, $context, $args, $source) 606 { 607 preg_match("/(.*?)\s+(?:(?:\"|\')(.*?)(?:\"|\'))/", trim($args), $m); 608 $keyname = $m[1]; 609 $default = $m[2]; 610 $value = $context->get($keyname); 611 return ($value) ?: $default; 612 } 613 614 /** 615 * Truncate a string to a length, and append and ellipsis if provided 616 * {{#truncate content 5 "..."}} 617 * 618 * 619 * @param \Handlebars\Template $template template that is being rendered 620 * @param \Handlebars\Context $context context object 621 * @param array $args passed arguments to helper 622 * @param string $source part of template that is wrapped 623 * within helper 624 * 625 * @return string 626 */ 627 public function helperTruncate($template, $context, $args, $source) 628 { 629 preg_match("/(.*?)\s+(.*?)\s+(?:(?:\"|\')(.*?)(?:\"|\'))/", trim($args), $m); 630 $keyname = $m[1]; 631 $limit = $m[2]; 632 $ellipsis = $m[3]; 633 $value = substr($context->get($keyname), 0, $limit); 634 if ($ellipsis && strlen($context->get($keyname)) > $limit) { 635 $value .= $ellipsis; 636 } 637 return $value; 638 } 639 640 /** 641 * Return the data source as is 642 * 643 * {{#raw}} {{/raw}} 644 * 645 * @param \Handlebars\Template $template template that is being rendered 646 * @param \Handlebars\Context $context context object 647 * @param array $args passed arguments to helper 648 * @param string $source part of template that is wrapped 649 * within helper 650 * 651 * @return mixed 652 */ 653 public function helperRaw($template, $context, $args, $source) 654 { 655 return $source; 656 } 657 658 /** 659 * Repeat section $x times. 660 * 661 * {{#repeat 10}} 662 * This section will be repeated 10 times 663 * {{/repeat}} 664 * 665 * 666 * @param \Handlebars\Template $template template that is being rendered 667 * @param \Handlebars\Context $context context object 668 * @param array $args passed arguments to helper 669 * @param string $source part of template that is wrapped 670 * within helper 671 * 672 * @return string 673 */ 674 public function helperRepeat($template, $context, $args, $source) 675 { 676 $buffer = $template->render($context); 677 return str_repeat($buffer, intval($args)); 678 } 679 680 681 /** 682 * Define a section to be used later by using 'invoke' 683 * 684 * --> Define a section: hello 685 * {{#define hello}} 686 * Hello World! 687 * 688 * How is everything? 689 * {{/define}} 690 * 691 * --> This is how it is called 692 * {{#invoke hello}} 693 * 694 * 695 * @param \Handlebars\Template $template template that is being rendered 696 * @param \Handlebars\Context $context context object 697 * @param array $args passed arguments to helper 698 * @param string $source part of template that is wrapped 699 * within helper 700 * 701 * @return null 702 */ 703 public function helperDefine($template, $context, $args, $source) 704 { 705 $this->tpl["DEFINE"][$args] = clone($template); 706 } 707 708 /** 709 * Invoke a section that was created using 'define' 710 * 711 * --> Define a section: hello 712 * {{#define hello}} 713 * Hello World! 714 * 715 * How is everything? 716 * {{/define}} 717 * 718 * --> This is how it is called 719 * {{#invoke hello}} 720 * 721 * 722 * @param \Handlebars\Template $template template that is being rendered 723 * @param \Handlebars\Context $context context object 724 * @param array $args passed arguments to helper 725 * @param string $source part of template that is wrapped 726 * within helper 727 * 728 * @return null 729 */ 730 public function helperInvoke($template, $context, $args, $source) 731 { 732 if (! isset($this->tpl["DEFINE"][$args])) { 733 throw new LogicException("Can't INVOKE '{$args}'. '{$args}' was not DEFINE "); 734 } 735 return $this->tpl["DEFINE"][$args]->render($context); 736 } 737 738 739 /** 740 * Change underscore helper name to CamelCase 741 * 742 * @param string $string 743 * @return string 744 */ 745 private function underscoreToCamelCase($string) 746 { 747 return str_replace(' ', '', ucwords(str_replace('_', ' ', $string))); 748 } 749 750 /** 751 * slice 752 * Allow to split the data that will be returned 753 * #loop[start:end] => starts at start trhough end -1 754 * #loop[start:] = Starts at start though the rest of the array 755 * #loop[:end] = Starts at the beginning through end -1 756 * #loop[:] = A copy of the whole array 757 * 758 * #loop[-1] 759 * #loop[-2:] = Last two items 760 * #loop[:-2] = Everything except last two items 761 * 762 * @param string $string 763 * @return Array [tag_name, slice_start, slice_end] 764 */ 765 private function extractSlice($string) 766 { 767 preg_match("/^([\w\._\-]+)(?:\[([\-0-9]*?:[\-0-9]*?)\])?/i", $string, $m); 768 $slice_start = $slice_end = null; 769 if (isset($m[2])) { 770 list($slice_start, $slice_end) = explode(":", $m[2]); 771 $slice_start = (int) $slice_start; 772 $slice_end = $slice_end ? (int) $slice_end : null; 773 } 774 return [$m[1], $slice_start, $slice_end]; 775 } 776 777 /** 778 * Parse avariable from current args 779 * 780 * @param \Handlebars\Context $context context object 781 * @param array $args passed arguments to helper 782 * @return array 783 */ 784 private function parseArgs($context, $args) 785 { 786 $args = preg_replace('/\s+/', ' ', trim($args)); 787 $eles = explode(' ', $args); 788 foreach ($eles as $key => $ele) { 789 if (in_array(substr($ele, 0, 1), ['\'', '"'])) { 790 $val = trim($ele, '\'"'); 791 } else if (is_numeric($ele)) { 792 $val = $ele; 793 } else { 794 $val = $context->get($ele); 795 } 796 $eles[$key] = $val; 797 } 798 return $eles; 799 } 800} 801