diff --git a/src/main/php/com/handlebarsjs/Alias.class.php b/src/main/php/com/handlebarsjs/Alias.class.php deleted file mode 100755 index e1d06c4..0000000 --- a/src/main/php/com/handlebarsjs/Alias.class.php +++ /dev/null @@ -1,55 +0,0 @@ -name= $name; - } - - /** - * (string) cast overloading - * - * @return string - */ - public function __toString() { - return 'as |'.$this->name.'|'; - } - - /** - * Invocation overloading - * - * @param com.github.mustache.Node $node - * @param com.github.mustache.Context $context - * @param var[] $options - * @return var - */ - public function __invoke($node, $context, $options) { - return [$this->name => $context]; - } - - /** - * Compares - * - * @param var $value - * @return int - */ - public function compareTo($value) { - return $value instanceof self ? $this->name <=> $value->name : 1; - } - - /** @return string */ - public function hashCode() { - return md5($this->name); - } - - /** @return string */ - public function toString() { - return nameof($this).'('.$this.')'; - } -} \ No newline at end of file diff --git a/src/main/php/com/handlebarsjs/BlockParams.class.php b/src/main/php/com/handlebarsjs/BlockParams.class.php new file mode 100755 index 0000000..2693431 --- /dev/null +++ b/src/main/php/com/handlebarsjs/BlockParams.class.php @@ -0,0 +1,46 @@ +names= $names; + } + + /** + * (string) cast overloading + * + * @return string + */ + public function __toString() { + return 'as |'.implode(' ', $this->names).'|'; + } + + /** + * Compares + * + * @param var $value + * @return int + */ + public function compareTo($value) { + return $value instanceof self ? Objects::compare($this->names, $value->names) : 1; + } + + /** @return string */ + public function hashCode() { + return Objects::hashOf($this->names); + } + + /** @return string */ + public function toString() { + return nameof($this).'('.implode(' ', $this->names).')'; + } +} \ No newline at end of file diff --git a/src/main/php/com/handlebarsjs/EachBlockHelper.class.php b/src/main/php/com/handlebarsjs/EachBlockHelper.class.php index 77c60da..795f7d7 100755 --- a/src/main/php/com/handlebarsjs/EachBlockHelper.class.php +++ b/src/main/php/com/handlebarsjs/EachBlockHelper.class.php @@ -6,6 +6,7 @@ * @test xp://com.handlebarsjs.unittest.EachHelperTest */ class EachBlockHelper extends BlockNode { + private $params; /** * Creates a new with block helper @@ -18,6 +19,7 @@ class EachBlockHelper extends BlockNode { */ public function __construct($options= [], NodeList $fn= null, NodeList $inverse= null, $start= '{{', $end= '}}') { parent::__construct('each', $options, $fn, $inverse, $start, $end); + $this->params= isset($options[1]) ? cast($options[1], BlockParams::class)->names : []; } /** @@ -27,28 +29,14 @@ public function __construct($options= [], NodeList $fn= null, NodeList $inverse= * @param io.streams.OutputStream $out */ public function write($context, $out) { - $f= $this->options[0]; - $target= $f($this, $context, []); + $target= $this->options[0]($this, $context, []); if ($target instanceof \Generator) { - $first= true; - foreach ($target as $key => $value) { - $this->fn->write(new HashContext($key, $first, $context->asContext($value)), $out); - $first= false; - } + (new HashContext($context, $target, ...$this->params))->write($this->fn, $out); } else if ($context->isList($target)) { - $list= $context->asTraversable($target); - $size= sizeof($list); - foreach ($list as $index => $element) { - $this->fn->write(new ListContext($index, $size, $context->asContext($element)), $out); - } + (new ListContext($context, $target, ...$this->params))->write($this->fn, $out); } else if ($context->isHash($target)) { - $hash= $context->asTraversable($target); - $first= true; - foreach ($hash as $key => $value) { - $this->fn->write(new HashContext($key, $first, $context->asContext($value)), $out); - $first= false; - } + (new HashContext($context, $target, ...$this->params))->write($this->fn, $out); } else { $this->inverse->write($context, $out); } diff --git a/src/main/php/com/handlebarsjs/HandlebarsParser.class.php b/src/main/php/com/handlebarsjs/HandlebarsParser.class.php index c5e01c0..97da34c 100755 --- a/src/main/php/com/handlebarsjs/HandlebarsParser.class.php +++ b/src/main/php/com/handlebarsjs/HandlebarsParser.class.php @@ -79,10 +79,10 @@ public function options($tag) { $value= new Constant(null); } else if ('.' === $token) { $value= new Lookup(null); - } else if ('as' === $token) { // Aliases (as |...|) + } else if ('as' === $token) { // Block parameters (as |...|) $o= strpos($tag, '|', $o); $p= strcspn($tag, '|', $o + 1); - $value= new Alias(trim(substr($tag, $o + 1, $p))); + $value= new BlockParams(explode(' ', trim(substr($tag, $o + 1, $p)))); $p++; } else if (strspn($token, '-.0123456789') === strlen($token)) { $value= new Constant(strstr($token, '.') ? (double)$token : (int)$token); diff --git a/src/main/php/com/handlebarsjs/HashContext.class.php b/src/main/php/com/handlebarsjs/HashContext.class.php index b75b6d3..fab472f 100755 --- a/src/main/php/com/handlebarsjs/HashContext.class.php +++ b/src/main/php/com/handlebarsjs/HashContext.class.php @@ -7,52 +7,63 @@ * * @test xp://com.handlebarsjs.unittest.EachHelperTest */ -class HashContext extends Context { - protected $key; - protected $first; - protected $backing; +class HashContext extends DataContext { + private $map, $element, $index; + private $first= true; + private $last= null; + private $key= null; /** * Constructor * - * @param string $key - * @param bool $first - * @param parent $backing + * @param com.github.mustache.Context $parent + * @param [:var]|Generator $iterable + * @param ?string $element + * @param ?string $index */ - public function __construct($key, $first, parent $backing) { - parent::__construct($backing->variables, $backing->parent); - $this->key= $key; - $this->first= $first; - $this->backing= $backing; + public function __construct(Context $parent, $iterable, $element= null, $index= null) { + parent::__construct(null, $parent); + $this->map= $iterable; + $this->last= is_array($iterable) ? (end($this->map) ? key($this->map) : null) : null; // array_key_last for PHP >= 7.3 + $this->element= $element; + $this->index= $index; } /** - * Returns a context inherited from this context + * Writes output * - * @param var $result - * @return self + * @param com.handlebarsjs.Nodes $fn + * @param io.streams.OutputStream $out */ - public function asContext($result) { - return new DataContext($result, $this); + public function write($fn, $out) { + + // We modify this context directly while we're going - this way, + // we save creating context instances for each element. + foreach ($this->map as $this->key => $this->variables) { + $fn->write($this, $out); + $this->first= false; + } } /** - * Helper method to retrieve a pointer inside a given data structure - * using a given segment. Returns null if there is no such segment. - * Called from within the `lookup()` method for every segment in the - * variable name. + * Looks up segments inside a given collection * - * @param var $ptr - * @param string $segment + * @param var $v + * @param string[] $segments * @return var */ - protected function pointer($ptr, $segment) { - if ('@first' === $segment) { - return $this->first ? 'true' : null; - } else if ('@key' === $segment) { + protected function lookup0($v, $segments) { + $s= $segments[0]; + if ('@key' === $s || $this->index === $s) { return $this->key; - } else { - return $this->backing->pointer($ptr, $segment); + } else if ('@first' === $s) { + return $this->first ? 'true' : null; + } else if ('@last' === $s && null !== $this->last) { + return $this->key === $this->last ? 'true' : null; + } else if ($this->element === $s) { + return $this->variables; } + + return parent::lookup0($v, $segments); } } \ No newline at end of file diff --git a/src/main/php/com/handlebarsjs/ListContext.class.php b/src/main/php/com/handlebarsjs/ListContext.class.php index 5de5269..0e1ff1f 100755 --- a/src/main/php/com/handlebarsjs/ListContext.class.php +++ b/src/main/php/com/handlebarsjs/ListContext.class.php @@ -7,54 +7,60 @@ * * @test xp://com.handlebarsjs.unittest.EachHelperTest */ -class ListContext extends Context { - protected $index; - protected $last; - protected $backing; +class ListContext extends DataContext { + private $list, $last, $element, $index; + private $offset= null; /** * Constructor * - * @param int $index - * @param int $size - * @param parent $backing + * @param com.github.mustache.Context $parent + * @param var[] $list + * @param ?string $element + * @param ?string $index */ - public function __construct($index, $size, parent $backing) { - parent::__construct($backing->variables, $backing->parent); + public function __construct(Context $parent, $list, $element= null, $index= null) { + parent::__construct(null, $parent); + $this->list= $list; + $this->last= sizeof($this->list) - 1; + $this->element= $element; $this->index= $index; - $this->last= $size - 1; - $this->backing= $backing; } /** - * Returns a context inherited from this context + * Writes output * - * @param var $result - * @return self + * @param com.handlebarsjs.Nodes $fn + * @param io.streams.OutputStream $out */ - public function asContext($result) { - return new DataContext($result, $this); + public function write($fn, $out) { + + // We modify this context directly while we're going - this way, + // we save creating context instances for each element. + foreach ($this->list as $this->offset => $this->variables) { + $fn->write($this, $out); + } } /** - * Helper method to retrieve a pointer inside a given data structure - * using a given segment. Returns null if there is no such segment. - * Called from within the `lookup()` method for every segment in the - * variable name. + * Looks up segments inside a given collection * - * @param var $ptr - * @param string $segment + * @param var $v + * @param string[] $segments * @return var */ - protected function pointer($ptr, $segment) { - if ('@first' === $segment) { - return 0 === $this->index ? 'true' : null; - } else if ('@last' === $segment) { - return $this->last === $this->index ? 'true' : null; - } else if ('@index' === $segment) { - return $this->index; - } else { - return $this->backing->pointer($ptr, $segment); + protected function lookup0($v, $segments) { + $s= $segments[0]; + if ('@index' === $s || $this->index === $s) { + return $this->offset; + } else if ('@first' === $s) { + return 0 === $this->offset ? 'true' : null; + } else if ('@last' === $s) { + return $this->last === $this->offset ? 'true' : null; + } else if ($this->element === $s) { + return $this->variables; } + + return parent::lookup0($v, $segments); } } \ No newline at end of file diff --git a/src/main/php/com/handlebarsjs/WithBlockHelper.class.php b/src/main/php/com/handlebarsjs/WithBlockHelper.class.php index a00fe34..bd2c173 100755 --- a/src/main/php/com/handlebarsjs/WithBlockHelper.class.php +++ b/src/main/php/com/handlebarsjs/WithBlockHelper.class.php @@ -6,6 +6,7 @@ * @test xp://com.handlebarsjs.unittest.WithHelperTest */ class WithBlockHelper extends BlockNode { + private $alias; /** * Creates a new with block helper @@ -18,6 +19,7 @@ class WithBlockHelper extends BlockNode { */ public function __construct($options= [], NodeList $fn= null, NodeList $inverse= null, $start= '{{', $end= '}}') { parent::__construct('with', $options, $fn, $inverse, $start, $end); + $this->alias= isset($options[1]) ? cast($options[1], BlockParams::class)->names[0] : null; } /** @@ -28,7 +30,7 @@ public function __construct($options= [], NodeList $fn= null, NodeList $inverse= */ public function write($context, $out) { $target= $this->options[0]($this, $context, []); - $self= $context->asContext(isset($this->options[1]) ? $this->options[1]($this, $target, []) : $target); + $self= $context->asContext($this->alias ? [$this->alias => $target] : $target); if ($context->isTruthy($target)) { $this->fn->write($self, $out); diff --git a/src/test/php/com/handlebarsjs/unittest/EachHelperTest.class.php b/src/test/php/com/handlebarsjs/unittest/EachHelperTest.class.php index 3c4147b..f7332fd 100755 --- a/src/test/php/com/handlebarsjs/unittest/EachHelperTest.class.php +++ b/src/test/php/com/handlebarsjs/unittest/EachHelperTest.class.php @@ -107,6 +107,14 @@ public function with_hash_properties_and_first() { )); } + #[Test] + public function with_hash_properties_and_last() { + Assert::equals(': green true: $12.40 ', $this->evaluate( + '{{#each item}}{{@last}}: {{.}} {{/each}}', + $this->item() + )); + } + #[Test, Values(['else', '^'])] public function else_invoked_for_non_truthy($else) { Assert::equals('Default', $this->evaluate('{{#each var}}-{{.}}-{{'.$else.'}}Default{{/each}}', [ @@ -180,4 +188,44 @@ public function from_iterator_with_keys() { ['people' => $f()] )); } + + #[Test] + public function hash_with_as_index_element() { + Assert::equals('key: value', $this->evaluate( + '{{#each items as |item index|}}{{index}}: {{item.name}}{{/each}}', + ['items' => ['key' => 'value']] + )); + } + + #[Test] + public function hash_with_as_element() { + Assert::equals('key: value', $this->evaluate( + '{{#each items as |item|}}{{@key}}: {{item.name}}{{/each}}', + ['items' => ['key' => 'value']] + )); + } + + #[Test] + public function generator_with_as() { + Assert::equals('key: value', $this->evaluate( + '{{#each items as |item index|}}{{index}}: {{item.name}}{{/each}}', + ['items' => (function() { yield 'key' => 'value'; })()] + )); + } + + #[Test] + public function array_with_as_index_element() { + Assert::equals('0: value', $this->evaluate( + '{{#each items as |item index|}}{{index}}: {{item.name}}{{/each}}', + ['items' => ['value']] + )); + } + + #[Test] + public function array_with_as_element() { + Assert::equals('0: value', $this->evaluate( + '{{#each items as |item|}}{{@index}}: {{item.name}}{{/each}}', + ['items' => ['value']] + )); + } } \ No newline at end of file