mozilla / nunjucks

A powerful templating engine with inheritance, asynchronous control, and more (jinja2 inspired)
https://mozilla.github.io/nunjucks/
BSD 2-Clause "Simplified" License
8.57k stars 639 forks source link

How to compile expression directly, and keep data type? #1117

Open TitaniteSlab opened 6 years ago

TitaniteSlab commented 6 years ago

I would like to use nunjucks to evaluate a single expression enclosed by {{ }}. nunjucks always returns a String, but I would like to keep the expression result data type.

So, one solution is to dump the expression and then use JSON.parse() on the result:

JSON.parse(nunjucks.renderString(`{{ ${expr} | dump | safe }}`, ctx));

Is there another/better way to accomplish this?

devoidfury commented 6 years ago

Not really, nunjucks is a template library made to output a string. What's the use-case for this, why use nunjucks instead of eval here?

TitaniteSlab commented 6 years ago

For use case, consider Redhat Ansible which utilizes the expression engine from Jinja2 but not the templating features.

https://docs.ansible.com/ansible/2.5/user_guide/playbooks_templating.html

To be clear I'm not necessarily looking for an existing API - I'm willing to fork and tear up the source, just hoping for guidance where to start.

ArmorDarks commented 6 years ago

What I see in Ansible docs, it doesn't care that Nunjucks returns a string, since it is later used in YAML configs, which are a string, which later serialized.

For instance, even if you will do this:

---
{% set aString = '123' %}

aNumber: {{ aString }}
aString: '{{ aString }}

You will get following results, because it depends not on Nunjucks output, but YAML syntax parsing.

typeof yaml.aNumber === 'number' // true, because numbers without strings will be read by YAML parser as numbers

typeof yaml.aString === 'string' // true, because quoted numbers will be read by YAML parser as strings
TitaniteSlab commented 6 years ago

Ansible parses pure YAML first, and evaluates Jinja2 expressions property-by-property:

https://github.com/ansible/ansible/blob/devel/lib/ansible/template/__init__.py#L682

It does this by calling Jinja2's root_render_func() directly. This is a supported feature in Jinja2 (though perhaps uncommonly used in this manner), as they have a generate() function that can evaluate one statement at a time:

https://github.com/pallets/jinja/blob/master/jinja2/environment.py#L1031

I am essentially asking if nunjucks can be hacked to support the same thing. Evaluate 1 expression by itself.

ArmorDarks commented 6 years ago

Well, that's strange, because if they parse YAML before rendering, any YAML file with such content will throw an error:

aNumber: {{ aString }}

because it will treat { and } as an inline object, and because it isn't valid, it should throw.

Meanwhile, using such construction is the only way to declare numbers (because addition of any other character will make YAML treat it as a string).

Anyway, answering your question — no, right now Nunjucks can output only strings.

TitaniteSlab commented 6 years ago

You're correct, which is why Ansible requires you wrap bare expressions in quotes ;)

aNumber: "{{ foo }}"    # don't remove the quotes or Ansible will get angry!

Any concerns with the first snippet I suggested?

JSON.parse(nunjucks.renderString(`{{ ${expr} | dump | safe }}`, ctx));

It seems to work. I'll report back if I figure out another way.

TitaniteSlab commented 6 years ago

I implemented an Environment.evaluateExpression() that does this:

https://github.com/TitaniteSlab/nunjucks/commit/865cb61a9d8ee6f1a8cb0316c718e83d35755117

This is a quick hack that creates a separate path through the compiler, as the string coercion was happening in the generated code. I'd like some feedback and hopefully we can turn this into a proper pull request.

Note that Jinja2 actually does support this feature directly:

https://github.com/pallets/jinja/blob/master/jinja2/environment.py#L595

I notice that the design principles include Jinja2 parity. This would be a great feature to add to nunjucks. Jinja2's compile_expression() works slightly differently (it returns the compiled function to be called from a little helper class called ExpressionTemplate). I'd be happy to continue working on this to make it more similar to Jinja2.

Usage example:

let nunjucks = require('nunjucks');
let Environment = nunjucks.Environment;

let expr = `{{ foo + 1 + 2 | string | int }}`;
let ctx = {foo: 3};

let environment = new Environment();
let result = environment.evaluateExpression(expr, ctx);

console.info(`${expr}  ->  ${result}  [${typeof result}]`);
TitaniteSlab commented 6 years ago

Updated version with Environment.compileExpression() and without requiring {{ }} on the input to be more like Jinja2:

https://github.com/TitaniteSlab/nunjucks/commit/7e4c9252bcc70fefc332ba76f940aaf86cd3353d

Example: new Environment().compileExpression('foo')({foo: 3}); => 3

I want to make a pull request but I'm having trouble building the docs. I posted the problem over on the google group:

https://groups.google.com/forum/#!topic/nunjucks/JWCGUOUEK3k

fdintino commented 6 years ago

I can't seem to replicate your doc build error. Even so, I don't think it should be necessary to build the docs as part of the pull request. I'll build and push the docs to the gh-pages branch when I create a release, but generally not before. If you open the PR as-is I can check whether I encounter a similar doc-build issue.