Skip to content

Commit

Permalink
Merge pull request #16 from xp-forge/feature/each-with-params
Browse files Browse the repository at this point in the history
Add support for `each` with block params
  • Loading branch information
thekid authored May 2, 2021
2 parents 6b9e228 + 872c9e9 commit 558a255
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 136 deletions.
55 changes: 0 additions & 55 deletions src/main/php/com/handlebarsjs/Alias.class.php

This file was deleted.

46 changes: 46 additions & 0 deletions src/main/php/com/handlebarsjs/BlockParams.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php namespace com\handlebarsjs;

use lang\Value;
use util\Objects;

class BlockParams implements Value {
public $names;

/**
* Creates a new constant
*
* @param string[] $names
*/
public function __construct($names) {
$this->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).')';
}
}
24 changes: 6 additions & 18 deletions src/main/php/com/handlebarsjs/EachBlockHelper.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* @test xp://com.handlebarsjs.unittest.EachHelperTest
*/
class EachBlockHelper extends BlockNode {
private $params;

/**
* Creates a new with block helper
Expand All @@ -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 : [];
}

/**
Expand All @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/php/com/handlebarsjs/HandlebarsParser.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
69 changes: 40 additions & 29 deletions src/main/php/com/handlebarsjs/HashContext.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
68 changes: 37 additions & 31 deletions src/main/php/com/handlebarsjs/ListContext.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
4 changes: 3 additions & 1 deletion src/main/php/com/handlebarsjs/WithBlockHelper.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* @test xp://com.handlebarsjs.unittest.WithHelperTest
*/
class WithBlockHelper extends BlockNode {
private $alias;

/**
* Creates a new with block helper
Expand All @@ -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;
}

/**
Expand All @@ -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);
Expand Down
Loading

0 comments on commit 558a255

Please sign in to comment.