diff --git a/src/main/php/com/handlebarsjs/BlockHelpers.class.php b/src/main/php/com/handlebarsjs/BlockHelpers.class.php index 09cb2ef..15a2759 100755 --- a/src/main/php/com/handlebarsjs/BlockHelpers.class.php +++ b/src/main/php/com/handlebarsjs/BlockHelpers.class.php @@ -1,48 +1,49 @@ new XPClass(IfBlockHelper::class), - 'unless' => new XPClass(UnlessBlockHelper::class), - 'with' => new XPClass(WithBlockHelper::class), - 'each' => new XPClass(EachBlockHelper::class), - '>' => new XPClass(PartialBlockHelper::class) - ]; + /** @param [:string] $impl */ + public function __construct($impl) { + $this->impl= $impl; } /** - * Gets a block helper class by a given name + * Register a named implementation * * @param string $name - * @return ?lang.XPClass + * @param ?string $impl + * @return self */ - public static function named($name) { - return self::$byName[$name] ?? null; + public function register($name, $impl) { + if (null === $impl) { + unset($this->impl[$name]); + } else { + $this->impl[$name]= $impl; + } + return $this; } /** * Creates a new with block helper * - * @param string $name - * @param string[] $options - * @param com.github.mustache.NodeList $fn - * @param com.github.mustache.NodeList $inverse - * @param string $start - * @param string $end + * - Creates instances of named block implementations + * - Registers `*inline` partials in top-level nodes + * - Uses default block implementation otherwise + * + * @param var[] $options + * @param com.github.ParseState $state + * @return com.github.mustache.Node */ - public static function newInstance($name, $options, $fn, $inverse, $start, $end) { - if (isset(self::$byName[$name])) { - return self::$byName[$name]->newInstance($options, $fn, $inverse, $start, $end); + public function newInstance($options, $state) { + $name= array_shift($options); + if ($impl= $this->impl[$name] ?? null) { + return $state->target->add(new $impl($options, null, null, $state->start, $state->end)); + } else if ('*inline' === $name) { + return new BlockNode('inline', $options, $state->parents[0]->declare($options[0] ?? null)); } else { - return new BlockNode($name, $options, $fn, $inverse, $start, $end); + return $state->target->add(new BlockNode($name, $options, null, null, $state->start, $state->end)); } } } \ No newline at end of file diff --git a/src/main/php/com/handlebarsjs/BlockNode.class.php b/src/main/php/com/handlebarsjs/BlockNode.class.php index 2f3d782..2765c3c 100755 --- a/src/main/php/com/handlebarsjs/BlockNode.class.php +++ b/src/main/php/com/handlebarsjs/BlockNode.class.php @@ -1,6 +1,6 @@ name= $name; $this->options= $options; - $this->fn= $fn ?: new Nodes(); - $this->inverse= $inverse ?: new Nodes(); + $this->fn= $fn ?? new NodeList(); + $this->inverse= $inverse ?? new NodeList(); $this->start= $start; $this->end= $end; } @@ -47,7 +47,7 @@ public function name() { /** * Returns fn * - * @return com.handlebarsjs.Nodes + * @return com.github.mustache.NodeList */ public function fn() { return $this->fn; @@ -56,7 +56,7 @@ public function fn() { /** * Returns inverse * - * @return com.handlebarsjs.Nodes + * @return com.github.mustache.NodeList */ public function inverse() { return $this->inverse; @@ -65,7 +65,7 @@ public function inverse() { /** * Returns options passed to this section * - * @return string[] + * @return var[] */ public function options() { return $this->options; diff --git a/src/main/php/com/handlebarsjs/Decoration.class.php b/src/main/php/com/handlebarsjs/Decoration.class.php deleted file mode 100755 index 2582c01..0000000 --- a/src/main/php/com/handlebarsjs/Decoration.class.php +++ /dev/null @@ -1,47 +0,0 @@ -kind= $kind; - $this->options= $options; - $this->fn= $fn ?: new Nodes(); - } - - /** @return string */ - public function name() { return substr($this->kind, 1); } - - /** @return com.handlebarsjs.Nodes */ - public function fn() { return $this->fn; } - - /** - * Evaluates this decoration - * - * @param com.github.mustache.Context $context the rendering context - * @return void - */ - public function enter($context) { - if (isset($context->engine->helpers[$this->kind])) { - $f= $context->engine->helpers[$this->kind]; - $f($this->fn, $context, $this->options); - } else { - throw new MethodNotImplementedException('No such decorator '.$this->kind); - } - } -} \ No newline at end of file diff --git a/src/main/php/com/handlebarsjs/HandlebarsEngine.class.php b/src/main/php/com/handlebarsjs/HandlebarsEngine.class.php index 5fa1202..ef173d3 100755 --- a/src/main/php/com/handlebarsjs/HandlebarsEngine.class.php +++ b/src/main/php/com/handlebarsjs/HandlebarsEngine.class.php @@ -31,9 +31,6 @@ static function __static() { self::$builtin= [ 'lookup' => function($node, $context, $options) { return $options[0][$options[1]] ?? null; - }, - '*inline' => function($node, $context, $options) { - $context->engine->templates->register($options[0]($node, $context, []), $node); } ]; } diff --git a/src/main/php/com/handlebarsjs/HandlebarsParser.class.php b/src/main/php/com/handlebarsjs/HandlebarsParser.class.php index 1f30dc9..a41bcda 100755 --- a/src/main/php/com/handlebarsjs/HandlebarsParser.class.php +++ b/src/main/php/com/handlebarsjs/HandlebarsParser.class.php @@ -15,11 +15,12 @@ /** * Parses handlebars templates * - * @test xp://com.handlebarsjs.unittest.ParsingTest - * @test xp://com.handlebarsjs.unittest.SubexpressionsTest + * @test com.handlebarsjs.unittest.ParsingTest + * @test com.handlebarsjs.unittest.SubexpressionsTest * @see https://github.com/wycats/handlebars.js/blob/master/spec/parser.js */ class HandlebarsParser extends AbstractMustacheParser { + public $blocks; /** * Tokenize name and options from a given tag, e.g.: @@ -103,29 +104,24 @@ public function options($tag) { /** * Initialize this parser. + * + * @return void */ protected function initialize() { + $this->blocks= new BlockHelpers([ + 'if' => IfBlockHelper::class, + 'unless' => UnlessBlockHelper::class, + 'with' => WithBlockHelper::class, + 'each' => EachBlockHelper::class, + '>' => PartialBlockHelper::class, + ]); // Sections $this->withHandler('#', true, function($tag, $state, $parse) { - $parsed= $parse->options(trim(substr($tag, 1))); - $name= array_shift($parsed); $state->parents[]= $state->target; - - if ('*' === $name[0]) { - $block= $state->target->decorate(new Decoration($name, $parsed)); - } else { - $block= $state->target->add(BlockHelpers::newInstance( - $name, - $parsed, - null, - null, - $state->start, - $state->end - )); - } - $state->parents[]= $block; + $block= $this->blocks->newInstance($parse->options(trim(substr($tag, 1))), $state); $state->target= $block->fn(); + $state->parents[]= $block; }); $this->withHandler('/', true, function($tag, $state) { $name= trim(substr($tag, 1)); diff --git a/src/main/php/com/handlebarsjs/Nodes.class.php b/src/main/php/com/handlebarsjs/Nodes.class.php index 1e632de..d608c15 100755 --- a/src/main/php/com/handlebarsjs/Nodes.class.php +++ b/src/main/php/com/handlebarsjs/Nodes.class.php @@ -1,39 +1,44 @@ decorations[]= $decoration; - return $decoration; - } - - /** - * Evaluates decorators - * - * @param com.github.mustache.Context $context the rendering context - * @return void + * @param string|com.handlebarsjs.Quoted $partial + * @param ?com.github.mustache.NodeList $nodes + * @return com.github.mustache.NodeList + * @throws lang.IllegalArgumentException */ - public function enter($context) { - foreach ($this->decorations as $decoration) { - $decoration->enter($context); + public function declare($partial, $nodes= null) { + if ($partial instanceof Quoted) { + $name= $partial->chars; + } else if (is_string($partial)) { + $name= $partial; + } else { + throw new IllegalArgumentException('Partial names must be strings or Quoted instances, have '.typeof($partial)); } + + return $this->partials[$name]= $nodes ?? new NodeList(); } + /** @return [:com.github.mustache.NodeList] */ + public function partials() { return $this->partials; } + /** - * Returns block without decorators + * Returns a partial with a given name, or NULL if this partial does + * not exist. * - * @return com.github.mustache.NodeList + * @param string $partial + * @return ?com.github.mustache.NodeList */ - public function block() { return new NodeList($this->nodes); } + public function partial($partial) { + return $this->partials[$partial] ?? null; + } /** * Evaluates this node @@ -42,9 +47,19 @@ public function block() { return new NodeList($this->nodes); } * @param io.streams.OutputStream $out */ public function write($context, $out) { - foreach ($this->decorations as $decoration) { - $decoration->enter($context); + $templates= $context->engine->templates(); + $previous= []; + foreach ($this->partials as $name => $partial) { + $previous[$name]= $templates->register($name, $partial); + } + + // Restore partials to previous state after processing this template + try { + parent::write($context, $out); + } finally { + foreach ($previous as $name => $partial) { + $templates->register($name, $partial); + } } - parent::write($context, $out); } } \ No newline at end of file diff --git a/src/main/php/com/handlebarsjs/PartialBlockHelper.class.php b/src/main/php/com/handlebarsjs/PartialBlockHelper.class.php index 2061f0d..a1b09f5 100755 --- a/src/main/php/com/handlebarsjs/PartialBlockHelper.class.php +++ b/src/main/php/com/handlebarsjs/PartialBlockHelper.class.php @@ -46,10 +46,8 @@ public function write($context, $out) { $source= $templates->source($this->name); if ($source->exists()) { - $this->fn->enter($context); - $template= $context->engine->load($this->name, $this->start, $this->end, ''); - $previous= $templates->register('@partial-block', $this->fn->block()); + $previous= $templates->register('@partial-block', $this->fn); try { $context->engine->write($template, $context, $out); } finally { diff --git a/src/main/php/com/handlebarsjs/Quoted.class.php b/src/main/php/com/handlebarsjs/Quoted.class.php index 8b407dd..deb0c72 100755 --- a/src/main/php/com/handlebarsjs/Quoted.class.php +++ b/src/main/php/com/handlebarsjs/Quoted.class.php @@ -3,7 +3,7 @@ use lang\Value; class Quoted implements Value { - protected $chars; + public $chars; /** * Creates a new Quoted instance diff --git a/src/test/php/com/handlebarsjs/unittest/ParsingTest.class.php b/src/test/php/com/handlebarsjs/unittest/ParsingTest.class.php index a47c857..0a4aafd 100755 --- a/src/test/php/com/handlebarsjs/unittest/ParsingTest.class.php +++ b/src/test/php/com/handlebarsjs/unittest/ParsingTest.class.php @@ -1,10 +1,9 @@ decorate(new Decoration('inline', [new Quoted('myPartial')], new Nodes([new TextNode('Content')]))); + public function inline_partials() { + Assert::equals( + ['one' => new NodeList([new TextNode('One')]), 'two' => new NodeList([new TextNode('Two')])], + $this->parse('{{#*inline "one"}}One{{/inline}}{{#*inline "two"}}Two{{/inline}}')->partials() + ); + } + + #[Test, Expect(IllegalArgumentException::class)] + public function inline_name_cannot_be_missing() { + $this->parse('{{#*inline}}...{{/inline}}'); + } - Assert::equals($nodes, $this->parse('{{#*inline "myPartial"}}Content{{/inline}}')); + #[Test, Expect(IllegalArgumentException::class)] + public function inline_name_cannot_be_reference() { + $this->parse('{{#*inline one}}...{{/inline}}'); } #[Test, Expect(class: TemplateFormatException::class, message: '/Illegal nesting/')] diff --git a/src/test/php/com/handlebarsjs/unittest/PartialBlockHelperTest.class.php b/src/test/php/com/handlebarsjs/unittest/PartialBlockHelperTest.class.php index d083103..b4c883c 100755 --- a/src/test/php/com/handlebarsjs/unittest/PartialBlockHelperTest.class.php +++ b/src/test/php/com/handlebarsjs/unittest/PartialBlockHelperTest.class.php @@ -1,5 +1,6 @@ engine($this->templates([ + 'fixture' => '{{#*inline "test"}}Test{{/inline}}{{> test}}' + ])); + $engine->transform('fixture', []); + + Assert::instance(NotFound::class, $engine->templates()->source('test')); + } + #[Test] public function layout() { $templates= $this->templates([