Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce concept of scopes, with the engine simply being one. #12

Open
wants to merge 26 commits into
base: master
Choose a base branch
from

Conversation

thekid
Copy link
Member

@thekid thekid commented Feb 27, 2021

This opens possibilities to render contexts in different scopes, e.g. a HTTP request scope, which then pass language and internationalization preferences

Refactoring

  • Create com.github.mustache.Scope class
  • Sublass MustacheEngine from this new class
  • Create Context::inScope() to replace ::withEngine()
  • Remove deprecated template loading mechanism and compatibility layer for it
  • Move parser instances to templating mechanism (see below)

Templating

The templating mechanism consist of consists of template sources, which are responsible for loading the templates' code from a given source (e.g. the filesystem) and the template parser, which parses these tokens into a parse tree.

public class com.github.mustache.Templating {
  public function __construct(...Sources $sources, ...TemplateParser $parser)

  public function sources(): com.github.mustache.templates.Sources
  public function parser(): com.github.mustache.TemplateParser
  public function from(com.github.mustache.templates.Sources $sources): self
  public function use(com.github.mustache.TemplateParser $parser): self
  public function load(string $name): com.github.mustache.templates.Source
  public function compile(...Source $source, string $start, string $end, string $indent): ...Template
}

Template sources are represented by the Sources class. The following templates sources exist:

// Interface
public abstract class com.github.mustache.templates.Sources {
  public abstract function source(string $name): com.github.mustache.templates.Source
  public abstract function listing(): com.github.mustache.templates.Listing
}

// Loads templates from filesystem
com.github.mustache.FilesIn(string|io.Path|io.Folder $folder)
`- com.github.mustache.templates.FilesBased
    `- com.github.mustache.templates.Sources

// Loads templates from class loader resources
com.github.mustache.ResourcesIn(string|lang.IClassLoader $arg)
`- com.github.mustache.templates.FilesBased
    `- com.github.mustache.templates.Sources

// Loads files from associative array passed to constructor
com.github.mustache.InMemory([:string] $templates)
`- com.github.mustache.templates.Sources

Their source() methods return Source instances:

// Interface
public abstract class com.github.mustache.templates.Source {
  public function exists(): bool
  public abstract function code(): string
  public abstract function compile(...MustacheParser $parser, string $start, string $end, string $indent): ...Template
}

// When Sources::source() cannot find a template, it yields instances of NotFound
com.github.mustache.templates.NotFound
`- com.github.mustache.templates.Source

// Templates read from io.stream.InputStream, e.g. a file
com.github.mustache.templates.FromStream
`- com.github.mustache.templates.Source

// Templates read from strings
com.github.mustache.templates.InString
`- com.github.mustache.templates.Source

// Pre-compiled templates
com.github.mustache.templates.Compiled
`- com.github.mustache.templates.Source

...which are then used to return the tokenized templates.

See xp-forge/handlebars-templates#1

@thekid
Copy link
Member Author

thekid commented Feb 27, 2021

Range of BC breaks

Diff against handlebars library to restore compatibility:

  • Member $context->engine ->$context->scope
  • Signature asContext($result) -> asContext($result, $parent= null)
  • Type com\github\mustache\templates\Templates -> com\github\mustache\templates\Sources
  • Type com\github\mustache\TemplateListing -> com\github\mustache\templates\Listing
  • Refactor Tokens(<T>Tokenizer($s)) -> InString($s) (could've been done even before this change!)

Details

diff --git a/src/main/php/com/handlebarsjs/Decoration.class.php b/src/main/php/com/handlebarsjs/Decoration.class.php
index 2582c01..4cfce99 100755
--- a/src/main/php/com/handlebarsjs/Decoration.class.php
+++ b/src/main/php/com/handlebarsjs/Decoration.class.php
@@ -37,11 +37,11 @@ class Decoration {
    * @return void
    */
   public function enter($context) {
-    if (isset($context->engine->helpers[$this->kind])) {
-      $f= $context->engine->helpers[$this->kind];
+    if (isset($context->scope->helpers[$this->kind])) {
+      $f= $context->scope->helpers[$this->kind];
       $f($this->fn, $context, $this->options);
     } else {
-      throw new MethodNotImplementedException('No such decorator '.$this->kind);
+      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 aa3922c..d762fbc 100755
--- a/src/main/php/com/handlebarsjs/HandlebarsEngine.class.php
+++ b/src/main/php/com/handlebarsjs/HandlebarsEngine.class.php
@@ -41,7 +41,7 @@ class HandlebarsEngine {
         return $options[0][$options[1]] ?? null;
       },
       '*inline' => function($node, $context, $options) {
-        $context->engine->templates->declare($options[0]($node, $context, []), $node);
+        $context->scope->templates->declare($options[0]($node, $context, []), $node);
       }
     ];
   }
@@ -172,7 +172,7 @@ class HandlebarsEngine {
    */
   public function evaluate(Template $template, $arg) {
     $c= $arg instanceof Context ? $arg : new DataContext($arg);
-    return $template->evaluate($c->withEngine($this));
+    return $template->evaluate($c->inScope($this));
   }
 
   /**
@@ -185,7 +185,7 @@ class HandlebarsEngine {
    */
   public function write(Template $template, $arg, $out) {
     $c= $arg instanceof Context ? $arg : new DataContext($arg);
-    $template->write($c->withEngine($this), $out);
+    $template->write($c->inScope($this), $out);
   }
 
   /**
diff --git a/src/main/php/com/handlebarsjs/HashContext.class.php b/src/main/php/com/handlebarsjs/HashContext.class.php
index b75b6d3..82d1061 100755
--- a/src/main/php/com/handlebarsjs/HashContext.class.php
+++ b/src/main/php/com/handlebarsjs/HashContext.class.php
@@ -30,10 +30,11 @@ class HashContext extends Context {
    * Returns a context inherited from this context
    *
    * @param  var $result
+   * @param  parent $parent
    * @return self
    */
-  public function asContext($result) {
-    return new DataContext($result, $this);
+  public function asContext($result, $parent= null) {
+    return new DataContext($result, $parent ?? $this);
   }
 
   /**
diff --git a/src/main/php/com/handlebarsjs/ListContext.class.php b/src/main/php/com/handlebarsjs/ListContext.class.php
index 5de5269..c9e9bf6 100755
--- a/src/main/php/com/handlebarsjs/ListContext.class.php
+++ b/src/main/php/com/handlebarsjs/ListContext.class.php
@@ -30,10 +30,11 @@ class ListContext extends Context {
    * Returns a context inherited from this context
    *
    * @param  var $result
+   * @param  parent $parent
    * @return self
    */
-  public function asContext($result) {
-    return new DataContext($result, $this);
+  public function asContext($result, $parent= null) {
+    return new DataContext($result, $parent ?? $this);
   }
 
   /**
diff --git a/src/main/php/com/handlebarsjs/PartialBlockHelper.class.php b/src/main/php/com/handlebarsjs/PartialBlockHelper.class.php
index 2061f0d..0924bc3 100755
--- a/src/main/php/com/handlebarsjs/PartialBlockHelper.class.php
+++ b/src/main/php/com/handlebarsjs/PartialBlockHelper.class.php
@@ -31,27 +31,27 @@ class PartialBlockHelper extends BlockNode {
    * @param  io.streams.OutputStream $out
    */
   public function write($context, $out) {
-    $templates= $context->engine->templates();
+    $templates= $context->scope->templates();
 
     // {{#> partial context}} vs {{> partial key="Value"}}
     if (isset($this->options[0])) {
-      $context= $context->newInstance($this->options[0]($this, $context, []));
+      $context= $context->asContext($this->options[0]($this, $context, []));
     } else if ($this->options) {
       $pass= [];
       foreach ($context->asTraversable($this->options) as $key => $value) {
         $pass[$key]= $value($this, $context, []);
       }
-      $context= $context->newInstance($pass);
+      $context= $context->asContext($pass);
     }
 
     $source= $templates->source($this->name);
     if ($source->exists()) {
       $this->fn->enter($context);
 
-      $template= $context->engine->load($this->name, $this->start, $this->end, '');
+      $template= $context->scope->load($this->name, $this->start, $this->end, '');
       $previous= $templates->register('@partial-block', $this->fn->block());
       try {
-        $context->engine->write($template, $context, $out);
+        $context->scope->write($template, $context, $out);
       } finally {
         $templates->register('@partial-block', $previous);
       }
diff --git a/src/main/php/com/handlebarsjs/PartialNode.class.php b/src/main/php/com/handlebarsjs/PartialNode.class.php
index 7762aef..3582b07 100755
--- a/src/main/php/com/handlebarsjs/PartialNode.class.php
+++ b/src/main/php/com/handlebarsjs/PartialNode.class.php
@@ -92,16 +92,16 @@ class PartialNode extends Node {
 
     // {{> partial context}} vs {{> partial key="Value"}}
     if (isset($this->options[0])) {
-      $context= $context->newInstance($this->options[0]($this, $context, []));
+      $context= $context->asContext($this->options[0]($this, $context, []));
     } else if ($this->options) {
       $pass= [];
       foreach ($context->asTraversable($this->options) as $key => $value) {
         $pass[$key]= $value($this, $context, []);
       }
-      $context= $context->newInstance($pass);
+      $context= $context->asContext($pass);
     }
 
-    $engine= $context->engine;
+    $engine= $context->scope;
     $template= $engine->load($this->template->__invoke($this, $context, []), '{{', '}}', $this->indent);
     $engine->write($template, $context, $out);
   }
diff --git a/src/main/php/com/handlebarsjs/Templates.class.php b/src/main/php/com/handlebarsjs/Templates.class.php
index ecea071..b171738 100755
--- a/src/main/php/com/handlebarsjs/Templates.class.php
+++ b/src/main/php/com/handlebarsjs/Templates.class.php
@@ -1,9 +1,8 @@
 <?php namespace com\handlebarsjs;
 
-use com\github\mustache\templates\{Compiled, NotFound, Source, Tokens};
-use com\github\mustache\{Node, Template, TemplateListing};
+use com\github\mustache\templates\{Compiled, NotFound, Source, Listing, InString};
+use com\github\mustache\{Node, Template};
 use lang\{ClassLoader, IllegalArgumentException};
-use text\StringTokenizer;
 
 /**
  * Template loading implementation
@@ -18,7 +17,7 @@ class Templates {
   /** @return lang.XPClass */
   private function composite() {
     if (null === self::$composite) {
-      self::$composite= ClassLoader::defineClass('CompositeListing', TemplateListing::class, [], [
+      self::$composite= ClassLoader::defineClass('CompositeListing', Listing::class, [], [
         'templates' => null,
         'delegate' => null,
         '__construct' => function($templates, $delegate) {
@@ -78,7 +77,7 @@ class Templates {
     } else if ($content instanceof Node) {
       $this->templates[$name]= new Compiled(new Template($name, $content));
     } else {
-      $this->templates[$name]= new Tokens($name, new StringTokenizer($content));
+      $this->templates[$name]= new InString($name, $content);
     }
 
     return $previous;
@@ -92,7 +91,7 @@ class Templates {
    * @return com.github.mustache.templates.Source
    */
   public function tokens($content, $name= '(string)') {
-    return new Tokens($name, new StringTokenizer((string)$content));
+    return new InString($name, (string)$content);
   }
 
   /**
@@ -111,12 +110,12 @@ class Templates {
     }
   }
 
-  /** @return com.github.mustache.TemplateListing */
+  /** @return com.github.mustache.templates.Listing */
   public function listing() {
     if ($this->delegate) {
       return $this->composite()->newInstance($this->templates, $this->delegate->listing());
     } else {
-      return new TemplateListing('', function($package) { return array_keys($this->templates); });
+      return new Listing('', function($package) { return array_keys($this->templates); });
     }
   }
 }
\ No newline at end of file
diff --git a/src/test/php/com/handlebarsjs/unittest/HelperTest.class.php b/src/test/php/com/handlebarsjs/unittest/HelperTest.class.php
index 5cab52a..94da851 100755
--- a/src/test/php/com/handlebarsjs/unittest/HelperTest.class.php
+++ b/src/test/php/com/handlebarsjs/unittest/HelperTest.class.php
@@ -1,19 +1,19 @@
 <?php namespace com\handlebarsjs\unittest;
 
 use com\github\mustache\InMemory;
-use com\github\mustache\templates\Templates;
+use com\github\mustache\templates\Sources;
 use com\handlebarsjs\HandlebarsEngine;
 
 /** Base class for all helper tests */
 abstract class HelperTest {
 
   /** Returns in-memory templates initialized from a given map */
-  protected function templates(array $templates= []): Templates {
+  protected function templates(array $templates= []): Sources {
     return new InMemory($templates);
   }
 
   /** Returns handlebars engine with specified templates */
-  protected function engine(Templates $templates= null): HandlebarsEngine {
+  protected function engine(Sources $templates= null): HandlebarsEngine {
     return (new HandlebarsEngine())->withTemplates($templates ?? $this->templates());
   }
 

thekid added a commit to xp-forge/handlebars that referenced this pull request Feb 27, 2021
@thekid
Copy link
Member Author

thekid commented Feb 28, 2021

With all this, this is how Mustache and Handlebars engines compare:

// MustacheEngine class extends Scope
public function evaluate(Template $template, $context) {
  $c= $context instanceof Context ? $context : new DataContext($context);
  return $template->evaluate($c->inScope($this));
}

// HandlebarsEngine class comes without parent, Transformation extends Scope
public function evaluate(Template $template, $context) {
  $c= $context instanceof Context ? $context : new DataContext($context);
  return $template->evaluate($c->inScope(new Transformation($this->templates, $this->helpers)));
}

While in Mustache, the engine itself is the scope, in handlebars we create a new Transformation scope for each template evaluation. This is so we can support *inline templates, which are only available during the transformation. This solves the problem with inline templates "surviving" after transformation and potentially causing side-effects.

However, we now have to know these internal implementation details - that templates need to be passed a Transformation instance when using the Handlebars engine and on the other hand when using Mustache, we can simply pass the engine - if we use its load() or compile() methods to retrieve a template and use its evaluate() method directly.

$context= ['user' => '@test'];
$template= $engine->compile('Hello {{user}}');

// This is where it gets messy:
$c= $context instanceof Context ? $context : new DataContext($context);
$template->evaluate($c->inScope($engine));
$template->evaluate($c->inScope(new Transformation($engine->templates(), $engine->helpers())));

If we add a method to create a context, we could encapsulate these details:

// On the engine:
public function context($context) {
  $c= $context instanceof Context ? $context : new DataContext($context);
  return $c->inScope($this); // or inScope(new Transformation(...))
}

// The above code then would read:
$template= $engine->compile('Hello {{user}}');
$template->evaluate($engine->context(['user' => '@test']));

Still, it's not easily possible to integrate the concept of something like a RequestScope into this model.

@thekid
Copy link
Member Author

thekid commented Feb 28, 2021

Still, it's not easily possible to integrate the concept of something like a RequestScope into this model.

If we wrap scopes around their parents, it might work:

// On the scope:
public function wrap(self $parent) {
  $this->templates= $parent->templates;
  $this->helpers= $parent->helpers;
  return $this;
}

// On the engine:
public function context($context, Scope $scope= null) {
  $c= $context instanceof Context ? $context : new DataContext($context);
  return $c->inScope($scope ? $scope->wrap($this) : $this); // same with s/$this/new Transformation(...)/g
}

// The above code then would read:
$template= $engine->compile('Hello {{user}}');
$template->evaluate($engine->context(['user' => '@test'], new RequestScope($request)));

...but only for the top-most scope, as $context->scope->request would break as soon as RequestScope is wrapped in another scope.

@thekid
Copy link
Member Author

thekid commented Feb 28, 2021

Still, it's not easily possible to integrate the concept of something like a RequestScope into this model.

Another idea would be to rename scope helpers to variables (or globals) and then have the following possibility:

yield 't' => function($in, $context, $options) {
  $lang= $context->scope->globals['request']->values('user')['language'];
  // ...
}

...or even registering the language beforehand:

// In scope:
public function including($globals) {
  $this->globals+= $globals;
  return $this;
}

// Call
$template= $engine->compile('Hello {{user}}');
$template->evaluate($engine->context(['user' => '@test'])->including(['lang' => $request->values('user')['language']));

// Helper
yield 't' => function($in, $context, $options) {
  $lang= $context->scope->globals['lang'];
  // ...
}

Alternatively, we could pass an object to globals:

class LocalizationOf {
  public $language;

  public function __construct($request) {
    $this->language= $this->negotiate($request->header('Accept-Language'));
  }
}

// In scope:
public function including($globals) {
  $this->globals= $this->helpers['globals']= $globals;
  return $this;
}

// Call
$template= $engine->compile('Hello {{user}}');
$template->evaluate($engine->context(['user' => '@test'])->including(new LocalizationOf($request));

// Helper
yield 't' => function($in, $context, $options) {
  $lang= $context->scope->globals->language;
  // ...
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant