diff --git a/README.md b/README.md index 2069d00..817157c 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ Alternatively, BladeOne allows running arbitrary code from any class or method i ## Install (pick one of the next one) 1) Download the file manually then unzip (using WinRAR,7zip or any other program) https://github.com/EFTEC/BladeOne/archive/master.zip -2) git clone https://github.com/EFTEC/BladeOne +2) git clone https://github.com/EFTEC/BladeOne (it doesn't include the examples) 3) Composer. See [usage](#usage) 4) wget https://github.com/EFTEC/BladeOne/archive/master.zip unzip master.zip @@ -253,6 +253,14 @@ $blade = new BladeOne(); // MODE_DEBUG allows to pinpoint troubles. echo $blade->run("hello",array("variable1"=>"value1")); // it calls /views/hello.blade.php ``` +### Injection +You can inject the Bladeone using an existing instance of it. If there is no instance, then it will create a new one using +the default folders. +``` +$blade=BladeOne::$instance; +echo $blade->run("hello",array("variable1"=>"value1")); // it calls /views/hello.blade.php +``` + ### Fluent ```php @@ -341,6 +349,10 @@ or @endguest ``` +### Custom controls. +There are multiples ways to create a new control (tag) + + ## Extensions Libraries (optional) diff --git a/composer.json b/composer.json index df83489..2d317cd 100644 --- a/composer.json +++ b/composer.json @@ -1,49 +1,49 @@ -{ - "name": "eftec/bladeone", - "description": "The standalone version Blade Template Engine from Laravel in a single php file", - "type": "library", - "keywords": [ - "blade", - "template", - "view", - "php", - "templating" - ], - "homepage": "https://github.com/EFTEC/BladeOne", - "license": "MIT", - "authors": [ - { - "name": "Jorge Patricio Castro Castillo", - "email": "jcastro@eftec.cl" - } - ], - "require": { - "php": ">=7.2.5", - "ext-json": "*" - }, - "suggest": { - "ext-mbstring": "This extension is used if it's active", - "eftec/bladeonehtml": "Extension to create forms" - }, - "archive": { - "exclude": [ - "/examples" - ] - }, - "autoload": { - "psr-4": { - "eftec\\bladeone\\": "lib/" - } - }, - "autoload-dev": { - "psr-4": { - "eftec\\tests\\": "tests/" - } - }, - "bin": [ - "lib/bladeonecli" - ], - "require-dev": { - "phpunit/phpunit": "^8.5.23" - } -} +{ + "name": "eftec/bladeone", + "description": "The standalone version Blade Template Engine from Laravel in a single php file", + "type": "library", + "keywords": [ + "blade", + "template", + "view", + "php", + "templating" + ], + "homepage": "https://github.com/EFTEC/BladeOne", + "license": "MIT", + "authors": [ + { + "name": "Jorge Patricio Castro Castillo", + "email": "jcastro@eftec.cl" + } + ], + "require": { + "php": ">=7.2", + "ext-json": "*" + }, + "suggest": { + "ext-mbstring": "This extension is used if it's active", + "eftec/bladeonehtml": "Extension to create forms" + }, + "archive": { + "exclude": [ + "/examples" + ] + }, + "autoload": { + "psr-4": { + "eftec\\bladeone\\": "lib/" + } + }, + "autoload-dev": { + "psr-4": { + "eftec\\tests\\": "tests/" + } + }, + "bin": [ + "lib/bladeonecli" + ], + "require-dev": { + "phpunit/phpunit": "^8.5.23" + } +} diff --git a/examples/example_customcontrol.php b/examples/example_customcontrol.php new file mode 100644 index 0000000..bbaef85 --- /dev/null +++ b/examples/example_customcontrol.php @@ -0,0 +1,48 @@ +pipeEnable=true; + + +$blade->clearMethods(); + +$blade->addMethod('runtime','table',static function($args) { + // in this context, $this means the autoTest class and not the blade class. + $args=array_merge(['alias'=>'alias'],$args); // you could use array merge to set a default value. + BladeOne::$instance->addControlStackChild('runtimeTable',$args); // we store the current control in the stack. + return ''; +}); +$blade->addMethod('runtime','row',static function() { + $parent=BladeOne::$instance->lastControlStack()['args']; // getting the values of the parent control using the stack + $result=''; + foreach($parent['values'] as $v) { + $result.=BladeOne::$instance->runChild('auto.test2_control',[$parent['alias']=>$v]); + } + return $result; +}); +$blade->addMethod('runtime','row2',function() { + $parent=BladeOne::$instance->lastControlStack()['args']; // getting the values of the parent control using the stack + $result=''; + foreach($parent['values'] as $v) { + $result.="
  • $v
  • \n"; + } + return $result; +}); + + +try { + echo $blade->run("auto.test2",['countries' => ["chile","argentina","peru"]]); +} catch (Exception $e) { + echo "error found ".$e->getMessage()."
    ".$e->getTraceAsString(); +} diff --git a/examples/example_customcontrol2.php b/examples/example_customcontrol2.php new file mode 100644 index 0000000..cbee925 --- /dev/null +++ b/examples/example_customcontrol2.php @@ -0,0 +1,32 @@ +pipeEnable=true; + + +$blade->clearMethods(); + + +$blade->addMethod('runtime','card',static function($args) { + $result=''; + $result.=BladeOne::$instance->runChild('auto.card',['value'=>$args[0]]); + return $result; +}); + + +try { + echo $blade->run("auto.test3",['items' => [ + ['title'=>"chile",'content'=>'lorem ipsum'], + ['title'=>"argentina",'content'=>'lorem ipsum'], + ['title'=>"peru",'content'=>'lorem ipsum'], + ]]); +} catch (Exception $e) { + echo "error found ".$e->getMessage()."
    ".$e->getTraceAsString(); +} diff --git a/examples/views/auto/card.blade.php b/examples/views/auto/card.blade.php new file mode 100644 index 0000000..d64cb98 --- /dev/null +++ b/examples/views/auto/card.blade.php @@ -0,0 +1,7 @@ +
    +
    +
    {{$value['title']}}
    +

    {{$value['content']}}

    + Go somewhere +
    +
    diff --git a/examples/views/auto/test1.blade.php b/examples/views/auto/test1.blade.php new file mode 100644 index 0000000..b42b713 --- /dev/null +++ b/examples/views/auto/test1.blade.php @@ -0,0 +1,4 @@ +it is test 1 +@one(a1="hola" a2="mundo") + +two:@two(1,2,3) diff --git a/examples/views/auto/test2.blade.php b/examples/views/auto/test2.blade.php new file mode 100644 index 0000000..a49a31b --- /dev/null +++ b/examples/views/auto/test2.blade.php @@ -0,0 +1,6 @@ +@table(values=$countries alias="alias") +row: +@row() +row2: +@row2() +@endtable diff --git a/examples/views/auto/test2_control.blade.php b/examples/views/auto/test2_control.blade.php new file mode 100644 index 0000000..eb47f25 --- /dev/null +++ b/examples/views/auto/test2_control.blade.php @@ -0,0 +1 @@ +
  • {{$alias}}
  • diff --git a/examples/views/auto/test3.blade.php b/examples/views/auto/test3.blade.php new file mode 100644 index 0000000..61b8a4b --- /dev/null +++ b/examples/views/auto/test3.blade.php @@ -0,0 +1,29 @@ + + + + + + + + + + + Hello, world! + + +

    Hello, world!

    +
    +
    +@foreach($items as $item) + @card($item) +@endforeach +
    +
    + + + + + + + + diff --git a/lib/BladeOne.php b/lib/BladeOne.php index b96ec05..13bf55c 100644 --- a/lib/BladeOne.php +++ b/lib/BladeOne.php @@ -1,5 +1,4 @@ - public const VERSION = '4.10'; + public const VERSION = '4.11'; /** @var int BladeOne reads if the compiled file has changed. If it has changed,then the file is replaced. */ public const MODE_AUTO = 0; /** @var int Then compiled file is always replaced. It's slow and it's useful for development. */ @@ -75,10 +73,24 @@ class BladeOne public $csrf_token = ''; /** @var string The path to the missing translations log file. If empty then every missing key is not saved. */ public $missingLog = ''; - /** @var bool */ + /** @var bool if true then pipes commands are available, example {{$a1|strtolower}} */ public $pipeEnable = false; /** @var array Alias (with or without namespace) of the classes */ public $aliasClasses = []; + protected $hierarcy = []; + /** + * @var callable[] associative array with the callable methods. The key must be the name of the method
    + * example:
    + * ```php + * $this->methods['compileAlert']=static function(?string $expression=null) { return }; + * $this->methods['runtimeAlert']=function(?array $arguments=null) { return }; + * ``` + */ + protected $methods = []; + protected $controlStack=[['name'=>'','args'=>[],'parent'=>0]]; + protected $controlStackParent=0; + /** @var BladeOne it is used to get the last instance */ + public static $instance; /** * @var bool if true then the variables defined in the "include" as arguments are scoped to work only * inside the "include" statement.
    @@ -97,13 +109,13 @@ class BladeOne public $includeScope = false; /** * @var callable[] It allows to parse the compiled output using a function. - * This function doesn't require to return a value
    - * **Example:** this converts all compiled result in uppercase (note, content is a ref) - * ```php - * $this->compileCallbacks[]= static function (&$content, $templatename=null) { + * This function doesn't require to return a value
    + * **Example:** this converts all compiled result in uppercase (note, content is a ref) + * ```php + * $this->compileCallbacks[]= static function (&$content, $templatename=null) { * $content=strtoupper($content); - * }; - * ``` + * }; + * ``` */ public $compileCallbacks = []; /** @var array All the registered extensions. */ @@ -153,9 +165,7 @@ class BladeOne * **sha1** the filename is converted into a sha1 hash
    * **md5** the filename is converted into a md5 hash
    */ - protected $compileTypeFileName='auto'; - - + protected $compileTypeFileName = 'auto'; /** @var array Custom "directive" dictionary. Those directives run at compile time. */ protected $customDirectives = []; /** @var bool[] Custom directive dictionary. Those directives run at runtime. */ @@ -223,11 +233,8 @@ class BladeOne protected $switchCount = 0; /** @var bool Indicates if the switch is recently open */ protected $firstCaseInSwitch = true; - // - // - /** * Bob the constructor. * The folder at $compiledPath is created in case it doesn't exist. @@ -247,15 +254,15 @@ public function __construct($templatePath = null, $compiledPath = null, $mode = $this->templatePath = (is_array($templatePath)) ? $templatePath : [$templatePath]; $this->compiledPath = $compiledPath; $this->setMode($mode); - $this->authCallBack = function ( + self::$instance = $this; + $this->authCallBack = function( $action = null, /** @noinspection PhpUnusedParameterInspection */ $subject = null ) { return \in_array($action, $this->currentPermission, true); }; - - $this->authAnyCallBack = function ($array = []) { + $this->authAnyCallBack = function($array = []) { foreach ($array as $permission) { if (\in_array($permission, $this->currentPermission ?? [], true)) { return true; @@ -263,22 +270,19 @@ public function __construct($templatePath = null, $compiledPath = null, $mode = } return false; }; - - $this->errorCallBack = static function ( + $this->errorCallBack = static function( /** @noinspection PhpUnusedParameterInspection */ $key = null ) { return false; }; - - // If the "traits" has "Constructors", then we call them. // Requisites. // 1- the method must be public or protected // 2- it must don't have arguments // 3- It must have the name of the trait. i.e. trait=MyTrait, method=MyTrait() $traits = get_declared_traits(); - $currentTraits = (array) class_uses($this); + $currentTraits = (array)class_uses($this); foreach ($traits as $trait) { $r = explode('\\', $trait); $name = end($r); @@ -290,9 +294,121 @@ public function __construct($templatePath = null, $compiledPath = null, $mode = } } } + + /** + * It gets an instance of Bladeone. If none, then it will create a new one witht eh default data. + * @param string|array $templatePath If null then it uses (caller_folder)/views + * @param string $compiledPath If null then it uses (caller_folder)/compiles + * @param int $mode + * =[BladeOne::MODE_AUTO,BladeOne::MODE_DEBUG,BladeOne::MODE_FAST,BladeOne::MODE_SLOW][$i] + * @return BladeOne + */ + public static function getInstance($templatePath = null, $compiledPath = null, $mode = 0): BladeOne + { + if (self::$instance === null) { + new self($templatePath, $compiledPath, $mode); + } + return self::$instance; + } + + /** + * It adds a control to the stack
    + * **Example:**
    + * ```php + * $this->addControlStackChild('alert',['message'=>'hello']); + * ``` + * @param string $name the nametag of the stack + * @param array $args + * @return void + */ + public function addControlStackChild(string $name,array $args): void + { + $this->controlStack[]=['name'=>$name,'args'=>$args,'parent'=>$this->controlStackParent]; + $this->controlStackParent=array_key_last($this->controlStack); + } + public function addControlStackSibling(string $name,array $args): void + { + $grandparent=$this->controlStack[$this->controlStackParent]['parent']; + $this->controlStack[]=['name'=>$name,'args'=>$args,'parent'=>$grandparent]; + } + + /** + * It returns the lastest control from the stack and removes it. + * @return mixed|null + */ + public function closeControlStack() { + $this->controlStackParent=$this->controlStack[$this->controlStackParent]['parent']; + return array_pop($this->controlStack); + } + /** + * It removes the last parent and returns the new parent (the previous grandparent)
    + * Usually this method and closeControlStack must return the same if every child was closed correctly. + * @return mixed|null + */ + public function closeControlStackParent() { + $grandparent=$this->controlStack[$this->controlStackParent]['parent']; + unset($this->controlStack[$this->controlStackParent]); + $this->controlStackParent=$grandparent; + return $this->controlStack[$this->controlStackParent]; + } + /** + * It returns the last control from the stack without removing it.
    + * It is useful to get the previous control, it could be a parent or a sibling. + * @return array + */ + public function lastControlStack(): array + { + return @end($this->controlStack); + } + + /** + * It gets the parent control stack + * @return array + */ + public function parentControlStack(): array + { + return $this->controlStack[$this->controlStackParent]; + } + + /** + * It clears the whole control stack + * @return void + */ + public function clearControlStack(): void + { + $this->controlStack=[['name'=>'','args'=>[],'parent'=>0]]; + } + + /** + * It adds a new method
    + * **Example:**
    + * ```php + * $this->addMethod('compile','alert',static function(?string $expression=null) { return }); + * $this->addMethod('runtime','alert',function(?array $arguments=null) { return }); + * ``` + * @param string $type=['compile','runtime'][$i] if you want to add a compile method or a runtime method + * @param string $name the name of the method. Commonly it is in lowercase. + * @param callable $callable the callable method + * @return BladeOne + */ + public function addMethod(string $type,string $name,callable $callable): BladeOne + { + $fullName=$type.ucfirst($name); + $this->methods[$fullName]=$callable; + return $this; + } + + /** + * It clears all the methods defined. + * @return $this + */ + public function clearMethods(): self + { + $this->methods=[]; + return $this; + } //
    // - /** * Show an error in the web. * @@ -309,7 +425,6 @@ public function showError($id, $text, $critic = false, $alwaysThrow = false): st if ($this->throwOnError || $alwaysThrow || $critic === true) { throw new \RuntimeException("BladeOne Error [$id] $text"); } - $msg = "
    "; $msg .= "BladeOne Error [$id]:
    "; $msg .= "$text
    \n"; @@ -336,7 +451,7 @@ public static function e($value): string return \htmlentities(\print_r($value, true), ENT_QUOTES, 'UTF-8', false); } if (\is_numeric($value)) { - $value=(string)$value; + $value = (string)$value; } return \htmlentities($value, ENT_QUOTES, 'UTF-8', false); } @@ -373,27 +488,25 @@ public function format($variable, $format = null): string * ``` * * @param ?string $input The input value - * @param string $quote The quote used (to quote the result) - * @param bool $parse If the result will be parsed or not. If false then it's returned without $this->e + * @param string $quote The quote used (to quote the result) + * @param bool $parse If the result will be parsed or not. If false then it's returned without $this->e * @return string */ public function wrapPHP($input, $quote = '"', $parse = true): string { - if($input===null) { + if ($input === null) { return 'null'; } if (strpos($input, '(') !== false && !$this->isQuoted($input)) { if ($parse) { return $quote . $this->phpTagEcho . '$this->e(' . $input . ');?>' . $quote; } - return $quote . $this->phpTagEcho . $input . ';?>' . $quote; } if (strpos($input, '$') === false) { if ($parse) { return self::enq($input); } - return $input; } if ($parse) { @@ -430,7 +543,7 @@ public static function enq($value): string if (\is_array($value) || \is_object($value)) { return \htmlentities(\print_r($value, true), ENT_NOQUOTES, 'UTF-8', false); } - return \htmlentities($value??'', ENT_NOQUOTES, 'UTF-8', false); + return \htmlentities($value ?? '', ENT_NOQUOTES, 'UTF-8', false); } /** @@ -443,7 +556,7 @@ public function addInclude($view, $alias = null): void $alias = \explode('.', $view); $alias = \end($alias); } - $this->directive($alias, function ($expression) use ($view) { + $this->directive($alias, function($expression) use ($view) { $expression = $this->stripParentheses($expression) ?: '[]'; return "$this->phpTag echo \$this->runChild('$view', $expression); ?>"; }); @@ -473,11 +586,9 @@ public function stripParentheses($expression): string if (\is_null($expression)) { return ''; } - if (static::startsWith($expression, '(')) { $expression = \substr($expression, 1, -1); } - return $expression; } @@ -501,7 +612,6 @@ public static function startsWith($haystack, $needles): bool } } } - return false; } @@ -568,7 +678,6 @@ public function addAliasClasses($aliasName, $classWithNS): void } //
    // - /** * Authentication. Sets with a user,role and permission * @@ -587,20 +696,17 @@ public function setAuth($user = '', $role = null, $permission = []): void * run the blade engine. It returns the result of the code. * * @param string $string HTML to parse - * @param array $data It is an associative array with the datas to display. + * @param array $data It is an associative array with the datas to display. * @return string It returns a parsed string * @throws Exception */ public function runString($string, $data = []): string { $php = $this->compileString($string); - $obLevel = \ob_get_level(); \ob_start(); \extract($data, EXTR_SKIP); - $previousError = \error_get_last(); - try { @eval('?' . '>' . $php); } catch (Exception $e) { @@ -615,7 +721,6 @@ public function runString($string, $data = []): string $this->showError('runString', $e->getMessage() . ' ' . $e->getCode(), true); return ''; } - $lastError = \error_get_last(); // PHP 5.6 if ($previousError != $lastError && $lastError['type'] == E_PARSE) { while (\ob_get_level() > $obLevel) { @@ -624,7 +729,6 @@ public function runString($string, $data = []): string $this->showError('runString', $lastError['message'] . ' ' . $lastError['type'], true); return ''; } - return \ob_get_clean(); } @@ -668,7 +772,7 @@ public function compileString($value): string */ protected function storeVerbatimBlocks($value): string { - return \preg_replace_callback('/(?verbatimBlocks[] = $matches[1]; return $this->verbatimPlaceholder; }, $value); @@ -705,7 +809,7 @@ protected function parseToken($token): string */ protected function restoreVerbatimBlocks($result): string { - $result = \preg_replace_callback('/' . \preg_quote($this->verbatimPlaceholder) . '/', function () { + $result = \preg_replace_callback('/' . \preg_quote($this->verbatimPlaceholder) . '/', function() { return \array_shift($this->verbatimBlocks); }, $result); $this->verbatimBlocks = []; @@ -803,7 +907,6 @@ public function startPush($section, $content = ''): void /* * endswitch tag */ - /** * Append content to a given push section. * @@ -899,16 +1002,16 @@ protected function extendStartPush($section, $content): void */ public function yieldPushContent($section, $default = ''): string { - if($section===null || $section==='') { + if ($section === null || $section === '') { return $default; } - if($section[-1]==='*') { - $keys=array_keys($this->pushes); - $findme=rtrim($section,'*'); - $result=""; - foreach($keys as $key) { - if(strpos($key,$findme)===0) { - $result.=\implode(\array_reverse($this->pushes[$key])); + if ($section[-1] === '*') { + $keys = array_keys($this->pushes); + $findme = rtrim($section, '*'); + $result = ""; + foreach ($keys as $key) { + if (strpos($key, $findme) === 0) { + $result .= \implode(\array_reverse($this->pushes[$key])); } } return $result; @@ -944,7 +1047,6 @@ public function splitForeach($each = 1, $splitText = ',', $splitEnd = ''): strin } else { $eachN = PHP_INT_MAX; } - if (($loopStack['index'] + 1) % $eachN === 0) { return $splitText; } @@ -1024,22 +1126,19 @@ public function __call($name, $args) public function registerIfStatement($name, callable $callback): string { $this->conditions[$name] = $callback; - - $this->directive($name, function ($expression) use ($name) { + $this->directive($name, function($expression) use ($name) { $tmp = $this->stripParentheses($expression); return $expression !== '' ? $this->phpTag . " if (\$this->check('$name', $tmp)): ?>" : $this->phpTag . " if (\$this->check('$name')): ?>"; }); - - $this->directive('else' . $name, function ($expression) use ($name) { + $this->directive('else' . $name, function($expression) use ($name) { $tmp = $this->stripParentheses($expression); return $expression !== '' ? $this->phpTag . " elseif (\$this->check('$name', $tmp)): ?>" : $this->phpTag . " elseif (\$this->check('$name')): ?>"; }); - - $this->directive('end' . $name, function () { + $this->directive('end' . $name, function() { return $this->phpTag . ' endif; ?>'; }); return ''; @@ -1073,14 +1172,15 @@ public function includeWhen($bool = false, $view = '', $value = []): string } /** - * Macro of function run + * Macro of function run. Runchild backups the operations, so it is ideal to run as a child process without + * intervining with other processes. * * @param $view * @param array $variables * @return string * @throws Exception */ - public function runChild($view, $variables = []): string + public function runChild($view,$variables = []): string { if (\is_array($variables)) { if ($this->includeScope) { @@ -1089,13 +1189,10 @@ public function runChild($view, $variables = []): string $backup = null; } $newVariables = \array_merge($this->variables, $variables); + $backupControlStack=$this->controlStack; + $backupSectionStack=$this->sectionStack; + $backupLookStack=$this->loopsStack; } else { - if ($variables === null) { - $newVariables = $this->variables; - var_dump($newVariables); - die(1); - } - $this->showError('run/include', "RunChild: Include/run variables should be defined as array ['idx'=>'value']", true); return ''; } @@ -1103,6 +1200,9 @@ public function runChild($view, $variables = []): string if ($backup !== null) { $this->variables = $backup; } + $this->controlStack=$backupControlStack; + $this->sectionStack=$backupSectionStack; + $this->loopsStack=$backupLookStack; return $r; } @@ -1285,22 +1385,18 @@ public function compile($templateName = null, $forced = false) public function getCompiledFile($templateName = ''): string { $templateName = (empty($templateName)) ? $this->fileName : $templateName; - $fullPath = $this->getTemplateFile($templateName); - if($fullPath == '') { - throw new \RuntimeException('Template not found: ' . $templateName); + if ($fullPath == '') { + throw new \RuntimeException('Template not found: ' .($this->mode == self::MODE_DEBUG ? $this->templatePath[0].'/'.$templateName : $templateName)); } - - $style=$this->compileTypeFileName; - if ($style==='auto') { - $style='sha1'; + $style = $this->compileTypeFileName; + if ($style === 'auto') { + $style = 'sha1'; } $hash = $style === 'md5' ? \md5($fullPath) : \sha1($fullPath); return $this->compiledPath . '/' . basename($templateName) . '_' . $hash . $this->compileExtension; } - - /** * Get the mode of the engine.See BladeOne::MODE_* constants * @@ -1344,7 +1440,6 @@ public function getTemplateFile($templateName = ''): string // it's in the root of the template folder. return $this->locateTemplate($templateName . $this->fileExtension); } - $file = $arr[$c - 1]; \array_splice($arr, $c - 1, $c - 1); // delete the last element $path = \implode('/', $arr); @@ -1365,7 +1460,6 @@ protected function locateTemplate($name): string if (\is_file($path)) { return $path; } - $this->notFoundPath .= $path . ","; } return ''; @@ -1594,7 +1688,6 @@ public function csrfIsValid($alwaysRegenerate = false, $tokenId = '_token'): boo $this->csrf_token = $_POST[$tokenId] ?? null; // ping pong the token. return $this->csrf_token . '|' . $this->ipClient() === ($_SESSION[$tokenId] ?? null); } - if ($this->csrf_token == '' || $alwaysRegenerate) { // if not token then we generate a new one $this->regenerateToken($tokenId); @@ -1753,7 +1846,6 @@ public function yieldContent($section, $default = ''): string if (isset($this->sections[$section])) { return \str_replace($this->PARENTKEY, $default, $this->sections[$section]); } - return $default; } @@ -1890,6 +1982,7 @@ public function setCompiledExtension($fileExtension): void { $this->compileExtension = $fileExtension; } + /** * @return string * @see BladeOne::setCompileTypeFileName @@ -1904,7 +1997,7 @@ public function getCompileTypeFileName(): string * * **auto** (default mode) the mode is "sha1"
    * * **sha1** the filename is converted into a sha1 hash (it's the slow method, but it is safest)
    * * **md5** the filename is converted into a md5 hash (it's faster than sha1, and it uses less space)
    - * @param string $compileTypeFileName=['auto','sha1','md5'][$i] + * @param string $compileTypeFileName =['auto','sha1','md5'][$i] * @return BladeOne */ public function setCompileTypeFileName(string $compileTypeFileName): BladeOne @@ -1912,6 +2005,7 @@ public function setCompileTypeFileName(string $compileTypeFileName): BladeOne $this->compileTypeFileName = $compileTypeFileName; return $this; } + /** * Add new loop to the stack. * @@ -1945,7 +2039,6 @@ public function incrementLoopIndices(): object { $c = \count($this->loopsStack) - 1; $loop = &$this->loopsStack[$c]; - $loop['index']++; $loop['iteration']++; $loop['first'] = $loop['index'] == 0; @@ -1991,7 +2084,6 @@ public function getFirstLoop(): ?object public function renderEach($view, $data, $iterator, $empty = 'raw|'): string { $result = ''; - if (\count($data) > 0) { // If is actually data in the array, we will loop through the data and append // an instance of the partial view to the final result HTML passing in the @@ -2019,7 +2111,6 @@ public function renderEach($view, $data, $iterator, $empty = 'raw|'): string public function run($view = null, $variables = []): string { $mode = $this->getMode(); - if ($view === null) { $view = $this->viewStack; } @@ -2028,9 +2119,8 @@ public function run($view = null, $variables = []): string $this->showError('run', 'BladeOne: view not set', true); return ''; } - - $forced = ($mode & 1)!==0; // mode=1 forced:it recompiles no matter if the compiled file exists or not. - $runFast = ($mode & 2)!==0; // mode=2 runfast: the code is not compiled neither checked, and it runs directly the compiled + $forced = ($mode & 1) !== 0; // mode=1 forced:it recompiles no matter if the compiled file exists or not. + $runFast = ($mode & 2) !== 0; // mode=2 runfast: the code is not compiled neither checked, and it runs directly the compiled $this->sections = []; if ($mode == 3) { $this->showError('run', "we can't force and run fast at the same time", true); @@ -2067,7 +2157,8 @@ public function setView($view): BladeOne * $this->composer(); // clear all composer. * ``` * - * @param string|array|null $view It could contain wildcards (*). Example: 'aa.bb.cc','*.bb.cc','aa.bb.*','*.bb.*' + * @param string|array|null $view It could contain wildcards (*). Example: + * 'aa.bb.cc','*.bb.cc','aa.bb.*','*.bb.*' * * @param callable|string|null $functionOrClass * @return BladeOne @@ -2085,7 +2176,6 @@ public function composer($view = null, $functionOrClass = null): BladeOne } else { $this->composerStack[$view] = $functionOrClass; } - return $this; } @@ -2100,9 +2190,7 @@ public function startComponent($name, array $data = []): void { if (\ob_start()) { $this->componentStack[] = $name; - $this->componentData[$this->currentComponent()] = $data; - $this->slots[$this->currentComponent()] = []; } } @@ -2171,7 +2259,6 @@ public function slot($name, $content = null): void $this->slots[$this->currentComponent()][$name] = $content; } elseif (\ob_start()) { $this->slots[$this->currentComponent()][$name] = ''; - $this->slotStack[$this->currentComponent()][] = $name; } } @@ -2184,11 +2271,9 @@ public function slot($name, $content = null): void public function endSlot(): void { static::last($this->componentStack); - $currentSlot = \array_pop( $this->slotStack[$this->currentComponent()] ); - $this->slots[$this->currentComponent()][$currentSlot] = \trim(\ob_get_clean()); } @@ -2456,7 +2541,6 @@ public function setErrorFunction(callable $fn): void //
    // - /** * Get the entire loop stack. * @@ -2532,7 +2616,6 @@ public function _e($phrase): string $this->missingTranslation($phrase); return $phrase; } - return static::$dictionary[$phrase]; } @@ -2574,7 +2657,6 @@ public function _n($phrase, $phrases, $num = 0): string $this->missingTranslation($phrase); return ($num <= 1) ? $phrase : $phrases; } - return ($num <= 1) ? $this->_e($phrase) : $this->_e($phrases); } @@ -2613,7 +2695,6 @@ protected function compileSwitch($expression): string } // // - protected function compileDump($expression): string { return $this->phpTagEcho . "\$this->dump$expression;?>"; @@ -2627,7 +2708,6 @@ protected function compileRelative($expression): string protected function compileMethod($expression): string { $v = $this->stripParentheses($expression); - return ""; } @@ -2783,7 +2863,7 @@ protected function getEchoMethods(): array 'compileEscapedEchos' => \strlen(\stripcslashes($this->escapedTags[0])), 'compileRegularEchos' => \strlen(\stripcslashes($this->contentTags[0])), ]; - \uksort($methods, static function ($method1, $method2) use ($methods) { + \uksort($methods, static function($method1, $method2) use ($methods) { // Ensure the longest tags are processed first if ($methods[$method1] > $methods[$method2]) { return -1; @@ -2828,7 +2908,7 @@ protected function compileStatements($value) * * @return mixed|string */ - $callback = function ($match) { + $callback = function($match) { if (static::contains($match[1], '@')) { // @@escaped tag $match[0] = isset($match[3]) ? $match[1] . $match[3] : $match[1]; @@ -2846,10 +2926,23 @@ protected function compileStatements($value) $this->stripParentheses(static::get($match, 3)) ); } - } elseif (\method_exists($this, $method = 'compile' . \ucfirst($match[1]))) { - // it calls the function compile - $match[0] = $this->$method(static::get($match, 3)); } else { + $nameMethod = 'compile' . \ucfirst($match[1]); + if (isset($this->methods[$nameMethod])) { + return $this->methods[$nameMethod](static::get($match, 3)); + } + if (\method_exists($this, $nameMethod)) { + // it calls the function compile + return $this->$nameMethod(static::get($match, 3)); + } + $nameMethod = 'runtime' . \ucfirst($match[1]); + $m4 = $match[4]??''; + if (isset($this->methods[$nameMethod])) { + return $this->autoruntime($m4, $nameMethod); + } + if (\method_exists($this, $nameMethod)) { + return $this->autoruntime($m4, $nameMethod, true); + } return $match[0]; } } @@ -2859,6 +2952,31 @@ protected function compileStatements($value) return preg_replace_callback('/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x', $callback, $value); } + /** + * This function generates a php code to run a runtime method. + * @param string|null $expression the expression to add in the code.
    + * For compile, it is of the type "($a2,"222")" + * For runtime, it is of the time "arg1=$a2 arg2="222"" + * @param string $nameFunction The name of the function. + * @param bool $compileMethod If the method is a compiled method, or it is a runtime method. + * @return string + */ + protected function autoruntime(?string $expression, string $nameFunction, $compileMethod = false): string + { + + if ($compileMethod) { + return $this->wrapPHP("\$this->$nameFunction$expression", '', false); + } + $args = $this->parseArgs($expression, ' ','=',false); + + $argsV = '['; + foreach ($args as $k => $v) { + $argsV .= "'$k'=>$v,"; + } + $argsV .= ']'; + return $this->wrapPHP("\$this->methods['$nameFunction']($argsV)", '', false); + } + /** * Determine if a given string contains a given substring. * @@ -2879,7 +2997,6 @@ public static function contains($haystack, $needles): bool } } } - return false; } @@ -2888,7 +3005,6 @@ protected function compileStatementClass($match): string if (isset($match[3])) { return $this->phpTagEcho . $this->fixNamespaceClass($match[1]) . $match[3] . '; ?>'; } - return $this->phpTagEcho . $this->fixNamespaceClass($match[1]) . '(); ?>'; } @@ -3052,6 +3168,7 @@ public function parseArgs($text, $separator = ',', $assigment = '=', $emptyKey = } } */ + foreach ($parts as $part) { $part = trim($part); if ($part) { @@ -3123,7 +3240,7 @@ public function parseArgsOld($text, $separator = ','): array protected function compileRawEchos($value): string { $pattern = \sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $this->rawTags[0], $this->rawTags[1]); - $callback = function ($matches) { + $callback = function($matches) { $whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3]; return $matches[1] ? \substr( $matches[0], @@ -3145,7 +3262,6 @@ protected function compileEchoDefaults($value): string { // Source: https://www.php.net/manual/en/language.variables.basics.php $patternPHPVariableName = '\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*'; - $result = \preg_replace('/^(' . $patternPHPVariableName . ')\s+or\s+(.+?)$/s', 'isset($1) ? $1 : $2', $value); if (!$this->pipeEnable) { return $this->fixNamespaceClass($result); @@ -3178,7 +3294,7 @@ protected function pipeDream($result): string return $result; } $prev = ''; - for ($i = 1; $i <=$c; $i++) { + for ($i = 1; $i <= $c; $i++) { $r = @explode(':', $array[$i], 2); $fnName = trim($r[0]); $fnNameF = $fnName[0]; // first character @@ -3189,7 +3305,7 @@ protected function pipeDream($result): string } elseif (method_exists($this, $fnName)) { $fnName = '$this->' . $fnName; } - $hasArgument=count($r) === 2; + $hasArgument = count($r) === 2; if ($i === 1) { $prev = $fnName . '(' . $array[0]; if ($hasArgument) { @@ -3199,13 +3315,12 @@ protected function pipeDream($result): string } else { $prev = $fnName . '(' . $prev; if ($hasArgument) { - $prev .=','. $r[1] . ')'; + $prev .= ',' . $r[1] . ')'; } else { - $prev.=')'; + $prev .= ')'; } } } - return $prev; } @@ -3218,7 +3333,7 @@ protected function pipeDream($result): string protected function compileRegularEchos($value): string { $pattern = \sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $this->contentTags[0], $this->contentTags[1]); - $callback = function ($matches) { + $callback = function($matches) { $whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3]; $wrapped = \sprintf($this->echoFormat, $this->compileEchoDefaults($matches[2])); return $matches[1] ? \substr($matches[0], 1) : $this->phpTagEcho . $wrapped . '; ?>' . $whitespace; @@ -3235,9 +3350,8 @@ protected function compileRegularEchos($value): string protected function compileEscapedEchos($value): string { $pattern = \sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $this->escapedTags[0], $this->escapedTags[1]); - $callback = function ($matches) { + $callback = function($matches) { $whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3]; - return $matches[1] ? $matches[0] : $this->phpTag . \sprintf($this->echoFormat, $this->compileEchoDefaults($matches[2])) . '; ?>' . $whitespace; @@ -3320,7 +3434,6 @@ protected function compileAuth($expression = ''): string if ($role == '') { return $this->phpTag . 'if(isset($this->currentUser)): ?>'; } - return $this->phpTag . "if(isset(\$this->currentUser) && \$this->currentRole==$role): ?>"; } @@ -3336,7 +3449,6 @@ protected function compileElseAuth($expression = ''): string if ($role == '') { return $this->phpTag . 'else: ?>'; } - return $this->phpTag . "elseif(isset(\$this->currentUser) && \$this->currentRole==$role): ?>"; } @@ -3368,12 +3480,10 @@ protected function compileElseCan($expression = ''): string if ($v) { return $this->phpTag . 'elseif (call_user_func($this->authCallBack,' . $v . ')): ?>'; } - return $this->phpTag . 'else: ?>'; } //
    // - protected function compileCannot($expression): string { $v = $this->stripParentheses($expression); @@ -3392,7 +3502,6 @@ protected function compileElseCannot($expression = ''): string if ($v) { return $this->phpTag . 'elseif (!call_user_func($this->authCallBack,' . $v . ')): ?>'; } - return $this->phpTag . 'else: ?>'; } @@ -3435,12 +3544,10 @@ protected function compileGuest($expression = null): string if ($expression === null) { return $this->phpTag . 'if(!isset($this->currentUser)): ?>'; } - $role = $this->stripParentheses($expression); if ($role == '') { return $this->phpTag . 'if(!isset($this->currentUser)): ?>'; } - return $this->phpTag . "if(!isset(\$this->currentUser) || \$this->currentRole!=$role): ?>"; } @@ -3456,7 +3563,6 @@ protected function compileElseGuest($expression): string if ($role == '') { return $this->phpTag . 'else: ?>'; } - return $this->phpTag . "elseif(!isset(\$this->currentUser) || \$this->currentRole!=$role): ?>"; } @@ -3533,7 +3639,6 @@ protected function compileEndunless(): string } // // - /** * @error('key') * @@ -3578,7 +3683,6 @@ protected function compileFor($expression): string } // // - /** * Compile the foreach statements into valid PHP. * @@ -3588,7 +3692,7 @@ protected function compileFor($expression): string protected function compileForeach($expression): string { //\preg_match('/\( *(.*) * as *([^\)]*)/', $expression, $matches); - if($expression===null) { + if ($expression === null) { return '@foreach'; } \preg_match('/\( *(.*) * as *([^)]*)/', $expression, $matches); @@ -3656,7 +3760,6 @@ protected function compileIf($expression): string } // // - /** * Compile the else-if statements into valid PHP. * @@ -3826,7 +3929,6 @@ protected function compileExtends($expression): string return $this->phpTag . '$_shouldextend[' . $this->uidCounter . ']=1; ?>'; } - /** * Execute the @parent command. This operation works in tandem with extendSection * @@ -4041,9 +4143,7 @@ protected function compileJSon($expression): string return $this->phpTagEcho . "json_encode($parts[0], $options, $depth); ?>"; } // - // - protected function compileIsset($expression): string { return $this->phpTag . "if(isset$expression): ?>"; @@ -4073,7 +4173,6 @@ protected function injectClass($className, $variableName = null) if (isset($this->injectResolver)) { return call_user_func($this->injectResolver, $className, $variableName); } - $fullClassName = $className . "\\" . $variableName; return new $fullClassName(); } @@ -4117,8 +4216,6 @@ protected function compile_n($expression): string } // - - // public static function isCli(): bool { @@ -4176,14 +4273,14 @@ public static function colorLog($str, $type = 'i'): string public function checkHealthPath(): bool { echo self::colorLog("Checking Health\n"); - $status=true; + $status = true; if (is_dir($this->compiledPath)) { echo "Compile-path [$this->compiledPath] is a folder " . self::colorLog("OK") . "\n"; } else { - $status=false; + $status = false; echo "Compile-path [$this->compiledPath] is not a folder " . self::colorLog("ERROR", 'e') . "\n"; } - foreach($this->templatePath as $t) { + foreach ($this->templatePath as $t) { if (is_dir($t)) { echo "Template-path (view) [$t] is a folder " . self::colorLog("OK") . "\n"; } else { @@ -4197,12 +4294,12 @@ public function checkHealthPath(): bool $rnd = $this->compiledPath . '/dummy' . rand(10000, 900009); $f = @file_put_contents($rnd, 'dummy'); if ($f === false) { - $status=false; + $status = false; $error = self::colorLog("Unable to create file [" . $this->compiledPath . '/dummy]', 'e'); } @unlink($rnd); } catch (\Throwable $ex) { - $status=false; + $status = false; $error = self::colorLog($ex->getMessage(), 'e'); } echo "Testing write in the compile folder [$rnd] $error\n"; @@ -4211,10 +4308,11 @@ public function checkHealthPath(): bool echo "View(s) found :" . count($files) . "\n"; return $status; } + public function createFolders(): void { echo self::colorLog("Creating Folder\n"); - echo "Creating compile folder[".self::colorLog($this->compiledPath,'b')."] "; + echo "Creating compile folder[" . self::colorLog($this->compiledPath, 'b') . "] "; if (!\is_dir($this->compiledPath)) { $ok = @\mkdir($this->compiledPath, 0770, true); if ($ok === false) { @@ -4225,8 +4323,8 @@ public function createFolders(): void } else { echo self::colorLog("Note: folder already exist.\n", 'w'); } - foreach($this->templatePath as $t) { - echo "Creating template folder [".self::colorLog($t,'b')."] "; + foreach ($this->templatePath as $t) { + echo "Creating template folder [" . self::colorLog($t, 'b') . "] "; if (!\is_dir($t)) { $ok = @\mkdir($t, 0770, true); if ($ok === false) { @@ -4282,24 +4380,24 @@ public function cliEngine(): void $done = true; $this->clearcompile(); } - if($createfolder) { - $done=true; + if ($createfolder) { + $done = true; $this->createFolders(); } if (!$done) { echo " Syntax:\n"; - echo " ".self::colorLog("-templatepath","b")." (optional) the template-path (view path).\n"; + echo " " . self::colorLog("-templatepath", "b") . " (optional) the template-path (view path).\n"; echo " Default value: 'views'\n"; echo " Example: 'php /vendor/bin/bladeonecli /folder/views' (absolute)\n"; echo " Example: 'php /vendor/bin/bladeonecli folder/view1' (relative)\n"; - echo " ".self::colorLog("-compilepath","b")." (optional) the compile-path.\n"; + echo " " . self::colorLog("-compilepath", "b") . " (optional) the compile-path.\n"; echo " Default value: 'compiles'\n"; echo " Example: 'php /vendor/bin/bladeonecli /folder/compiles' (absolute)\n"; echo " Example: 'php /vendor/bin/bladeonecli compiles' (relative)\n"; - echo " ".self::colorLog("-createfolder","b")." it creates the folders if they don't exist.\n"; + echo " " . self::colorLog("-createfolder", "b") . " it creates the folders if they don't exist.\n"; echo " Example: php ./vendor/bin/bladeonecli -createfolder\n"; - echo " ".self::colorLog("-clearcompile","b")." It deletes the content of the compile path\n"; - echo " ".self::colorLog("-check","b")." It checks the folders and permissions\n"; + echo " " . self::colorLog("-clearcompile", "b") . " It deletes the content of the compile path\n"; + echo " " . self::colorLog("-check", "b") . " It checks the folders and permissions\n"; } } @@ -4314,6 +4412,14 @@ public static function isAbsolutePath($path): bool } return $path[1] === ':'; } - // } +if (! function_exists("array_key_last")) { + function array_key_last($array) { + if (!is_array($array) || empty($array)) { + return NULL; + } + + return array_keys($array)[count($array)-1]; + } +} diff --git a/tests/AbstractBladeTestCase.php b/tests/AbstractBladeTestCase.php index 83e2317..fb26331 100644 --- a/tests/AbstractBladeTestCase.php +++ b/tests/AbstractBladeTestCase.php @@ -1,37 +1,38 @@ - - * @since 16/09/2018 - */ -abstract class AbstractBladeTestCase extends TestCase { - const TEMPLATE_PATH = __DIR__ . '/resources/templates'; - const COMPILED_PATH = __DIR__ . '/resources/compiled'; - - protected $blade; - public function __construct($name = null, array $data = [], $dataName = '') { - parent::__construct($name, $data, $dataName); - $this->blade = new BladeOne(self::TEMPLATE_PATH, self::COMPILED_PATH, BladeOne::MODE_SLOW); - } - /* - // tearDown on php7.2 is implemented as tearDown():void. However PHP 5.6 doesn't allows it. - // So I comment this line because it breaks Travis. - protected function tearDown() { - // Remove files compiled in this test to prevent side effects. - array_map('unlink', glob(self::COMPILED_PATH . "/*")); - } - */ - - public function assertEqualsIgnoringWhitespace($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { - $this->assertEquals( - preg_replace('/\s/', '', $expected), - preg_replace('/\s/', '', $actual), - $message, $delta, $maxDepth, $canonicalize, $ignoreCase - ); - } -} \ No newline at end of file + + * @since 16/09/2018 + */ +abstract class AbstractBladeTestCase extends TestCase { + const TEMPLATE_PATH = __DIR__ . '/resources/templates'; + const COMPILED_PATH = __DIR__ . '/resources/compiled'; + + protected $blade; + public function __construct($name = null, array $data = [], $dataName = '') { + parent::__construct($name, $data, $dataName); + $this->blade = new BladeOne(self::TEMPLATE_PATH, self::COMPILED_PATH, BladeOne::MODE_DEBUG); + + } + /* + // tearDown on php7.2 is implemented as tearDown():void. However PHP 5.6 doesn't allows it. + // So I comment this line because it breaks Travis. + protected function tearDown() { + // Remove files compiled in this test to prevent side effects. + array_map('unlink', glob(self::COMPILED_PATH . "/*")); + } + */ + + public function assertEqualsIgnoringWhitespace($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { + $this->assertEquals( + preg_replace('/\s/', '', $expected), + preg_replace('/\s/', '', $actual), + $message, $delta, $maxDepth, $canonicalize, $ignoreCase + ); + } +} diff --git a/tests/autoTest.php b/tests/autoTest.php new file mode 100644 index 0000000..7eb3180 --- /dev/null +++ b/tests/autoTest.php @@ -0,0 +1,81 @@ +clearMethods(); + BladeOne::$instance->addMethod('runtime', 'one', static function($args) { + return "method one " . $args['a1'] . ',' . $args['a2']; + }); + BladeOne::$instance->addMethod('compile', 'two', function($args) { + return BladeOne::$instance->wrapPHP("eftec\\tests\\sum$args", false, false); + }); + $this->assertEquals("it is test 1\nmethod one hola,mundo\ntwo:6", + BladeOne::$instance->run("auto.test1", ['a1' => 10, 'a2' => 20])); + } + + public function test2() + { + // it clears the previous methods created in different tests + BladeOne::$instance->clearMethods(); + BladeOne::$instance->addMethod('runtime', 'table', function($args) { + // you could use array merge to set a default value, or use conditions, ternary operators, etc. + $args = array_merge(['alias' => 'alias'], $args); + // we store the current control in the stack, and we turn @table as the current parent + BladeOne::$instance->addControlStackChild('table', $args); + return ''; + }); + BladeOne::$instance->addMethod('runtime', 'row', function() { + // getting the values of the parent control (@table) using the stack + // note: we don't need to add a child everytime a new control is added, its optional + $parent = BladeOne::$instance->parentControlStack()['args']; + $result = ''; + foreach ($parent['values'] as $v) { + $result .= BladeOne::$instance->runChild('auto.test2_control', [$parent['alias'] => $v]); + } + return $result; + }); + BladeOne::$instance->addMethod('runtime', 'row2', function() { + // getting the values of the parent control (@table) using the stack + // note: we don't need to add a child everytime a new control is added, its optional + $parent = BladeOne::$instance->parentControlStack()['args']; + $result = ''; + foreach ($parent['values'] as $v) { + $result .= "
  • $v
  • \n"; + } + return $result; + }); + $this->assertEquals("", + BladeOne::$instance->run("auto.test2", ['countries' => ["chile", "argentina", "peru"]])); + } + /** + * @throws \Exception + */ +} diff --git a/tests/resources/templates/auto/test1.blade.php b/tests/resources/templates/auto/test1.blade.php new file mode 100644 index 0000000..b42b713 --- /dev/null +++ b/tests/resources/templates/auto/test1.blade.php @@ -0,0 +1,4 @@ +it is test 1 +@one(a1="hola" a2="mundo") + +two:@two(1,2,3) diff --git a/tests/resources/templates/auto/test2.blade.php b/tests/resources/templates/auto/test2.blade.php new file mode 100644 index 0000000..da290f6 --- /dev/null +++ b/tests/resources/templates/auto/test2.blade.php @@ -0,0 +1,4 @@ +@table(values=$countries alias="alias") +@row() +@row2() +@endtable diff --git a/tests/resources/templates/auto/test2_control.blade.php b/tests/resources/templates/auto/test2_control.blade.php new file mode 100644 index 0000000..eb47f25 --- /dev/null +++ b/tests/resources/templates/auto/test2_control.blade.php @@ -0,0 +1 @@ +
  • {{$alias}}