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

hogan.js style template inheritance #198

Merged
merged 14 commits into from
Aug 16, 2014
Merged

hogan.js style template inheritance #198

merged 14 commits into from
Aug 16, 2014

Conversation

jazzdan
Copy link

@jazzdan jazzdan commented Apr 4, 2014

Modeled after hogan.js, with functional tests for template inheritance copied from the hogan.js test suite.

An additional test I performed was modifying the mustache/spec to have these same template inheritance rules. I then successfully ran the test suites of mustache.php with this patch and the most recent version of hogan.js after pointing them both at my modified mustache spec. It appears that they are both 100% compatible with one another.

This wasn't a solo effort by any means. Much thanks to my coworkers @dtb, @mdg and @benburry at @etsy.

{
$block = '';

$real_children = array_filter($children, function($ch) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work on PHP 5.2.x, and Mustache supports 5.2.x until the next major release.

@bobthecow
Copy link
Owner

How feasible would it be to add a pragma for template inheritance, then treat T_PARENT as T_PARTIAL unless it's present? Using pragmas to extend Mustache into non-spec behavior has a long, rich history :)

I understand that this makes templates a bit ugly, but I also really value compatibility with the Mustache spec — and with existing templates of existing users. While template inheritance is awesome, it's not strictly compatible with Mustache. For example, {{$foo}} is a perfectly valid context variable name in vanilla Mustache.

To make it easier for people who just want to use all the awesome all the time, it might make sense to add Mustache_Engine constructor options for enabling pragmas without putting them in all your templates.

$node[Mustache_Tokenizer::NAME],
isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
$level,
$node['nodes']
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be $node[Mustache_Tokenizer::NODES]

public function loadInheritanceSpec()
{
return $this->loadSpec('inheritance');
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to include these tests even without the modified version of mustache/spec?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I accidentally committed this. It can be removed because I added all the tests to the functional test here.

@bobthecow
Copy link
Owner

Looking great so far. Thanks @jazzdan, @dtb, @mdg and @benburry!

@jazzdan
Copy link
Author

jazzdan commented Apr 4, 2014

@bobthecow, implementing template inheritance as a pragma doesn’t work for our use case. Our developers are writing front-end features in hogan.js and sharing templates between the client and the server. If I understand pragmas correct, then implementing template inheritance as a pragma would mean that some templates could no longer be shared between the client (hogan) and the server (mustache.php).

Is it possible that, instead, this change could go out with the next major version? (Being not backwards-compatible)

@bobthecow
Copy link
Owner

@jazzdan Yeah, I hear you about the annoyance of pragmas, but I definitely want to make non-spec features opt-in, at least for now.

First, you should be able to use pragmas and any library that doesn't understand a given pragma will just skip over it. For example, if Mustache.php used {{% INHERITANCE }} as the pragma name, your template would look like this:

{{% INHERITANCE }}
{{< foo }}
{{$ bar }}this is bar{{/ bar }}
{{/ foo }}

In Mustache.php, that would toggle template inheritance and everything would work as expected. But Hogan.js, which doesn't know about the {{% INHERITANCE }} pragma, would treat that tag as a "variable not in the current context stack" and would, effectively, see this:

{{< foo }}
{{$ bar }}this is bar{{/ bar }}
{{/ foo }}

Second, I would consider adding a constructor option to the Mustache_Engine for toggling features at a global level rather than in individual templates:

$m = new Mustache_Engine([
    'enabled_pragmas' => [Mustache_Engine::PRAGMA_INHERITANCE]
]);

Would that make sense?

break;

case Mustache_Tokenizer::T_BLOCK_VAR:
if ($parent[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_PARENT) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this would fail for blocks inside sections inside parent tags:

{{< foo }}
  {{# bar }}
    {{$ baz }}{{/ baz }}
  {{/ bar }}
{{/ foo }}

… which probably is a perfectly valid failure case, since the contents of parent tags should really just be the blocks to be overridden. But if that's the case, do we want to throw an exception when parsing if a block arg is ever nested inside something besides a parent tag? Or is it better to let things fail and not tell people why?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(i haven't tested in java or hogan, so i don't know whether this works, is ignored, or throws an exception for either of those)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does throw an exception in hogan.js:

Hogan = require("hogan.js");

var template = "{{< foo }}{{# bar }}{{$ baz }}{{/ baz }}{{/ bar }}{{/ foo }}";

var foo = "{{$baz}}default content{{/baz}}";

var data = {
    bar: "set by user"
}

var template = Hogan.compile(template);
var output = template.render(data, {'foo': header});

console.log(output);
// Returns
/*
 * /home/dmiller/development/test.js/node_modules/hogan.js/lib/compiler.js:203
 *       throw new Error('Illegal content in < super tag.');
 *                     ^
 *                     Error: Illegal content in < super tag.
 *                         at buildTree (/home/dmiller/development/test.js/node_modules/hogan.js/lib/compiler.js:203:15)
 *       at buildTree (/home/dmiller/development/test.js/node_modules/hogan.js/lib/compiler.js:208:23)
 *       at Object.Hogan.parse (/home/dmiller/development/test.js/node_modules/hogan.js/lib/compiler.js:394:12)
 *       at Object.Hogan.compile (/home/dmiller/development/test.js/node_modules/hogan.js/lib/compiler.js:416:35)
 *       at Object.<anonymous> (/home/dmiller/development/test.js/index.js:11:22)
 *       at Module._compile (module.js:456:26)
 *       at Object.Module._extensions..js (module.js:474:10)
 *       at Module.load (module.js:356:32)
 *       at Function.Module._load (module.js:312:12)
 *       at Function.Module.runMain (module.js:497:10)
 */

I'll add a throw here for that.

@@ -61,10 +61,13 @@ public function __invoke($context = array())
* @param mixed $context Array or object rendering context (default: array())
*
* @return string Rendered template
* TODO construct a parent context stack here and pass it in
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is this TODO a TODONE yet?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It surely is!

@bobthecow
Copy link
Owner

From the GRMustache test cases linked to comments on the spec issue, we should probably make sure we handle cases like this correctly:

  - name: Shenanigans
    desc: Templates can override parent blocks inside sections
    data: {items: [1, 2, 3]}
    template: '{{<parent}}{{$value}}<{{.}}>{{/value}}{{/parent}}'
    partials:
      parent: '{{#items}}{{$value}}ignored{{/value}}{{/items}}'
    expected: '<1><2><3>'

It currently renders as <1><1><1>.

@jazzdan
Copy link
Author

jazzdan commented Apr 22, 2014

Hey @bobthecow, I spent some time looking at this test case. I think that in order to support this the template inheritance feature I wrote would have to be rewritten to be lazily evaluated, instead of strictly evaluated. =\ Unfortunately I have run out of time to work on this at work, but I'd welcome any contributions to implement that behavior.

I've confirmed that this test does pass in hogan.

@bobthecow
Copy link
Owner

@jazzdan I've got an idea or two, I'll take a poke at it when I get a minute.

@jazzdan
Copy link
Author

jazzdan commented Jun 26, 2014

@bobthecow did you ever get a chance to look at the GRMustache test case? Is there anything we can do to help get this into a release? Thanks for all your help so far, happy to report this has been running in production at @etsy for a few months now with no issues.

@bobthecow
Copy link
Owner

@jazzdan Thanks for the poke. Unfortunately I haven't had the time to take a shot at that last issue.

I'm fine releasing this as-is, with the understanding that it'll most likely change to be lazily evaluated in the future. Since that's the case, I'd prefer to guard the feature with a pragma to make it extra-clear that this isn't part of the spec :)

I've merged your branch and added a pragma in the pragma-blocks feature branch.

I've also added the ability to globally enable pragmas in the Engine, since that seems to be a much more useful way to enable them most of the time... take a look at that and let me know what you think?

If you think this works, I'd like to add some documentation around this specific pragma / feature (including the bit about lazy evaluation), then it should be ready to drop into a release.

@jazzdan
Copy link
Author

jazzdan commented Jun 30, 2014

@bobthecow awesome, thanks! I'd be happy to write up a wiki page here on it some time this week.

@bobthecow
Copy link
Owner

That'd be great, thanks :)

@jazzdan
Copy link
Author

jazzdan commented Jul 3, 2014

First draft of documentation available here.

@bobthecow
Copy link
Owner

@jazzdan Thanks :)

@bobthecow bobthecow merged commit 9cb926a into bobthecow:dev Aug 16, 2014
@bobthecow
Copy link
Owner

Sorry about the delay. This has been merged into dev, and I'm planning to cut a release some time this weekend. Thanks again for everything :)

@bobthecow
Copy link
Owner

Thanks @jazzdan, @dtb, @mdg and @benburry at @etsy for this feature. It has shipped in v2.7.0 :)

@bobthecow bobthecow removed the pending label Dec 30, 2014
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants