-
Notifications
You must be signed in to change notification settings - Fork 0
How It Works
NHP templates are compiled to JavaScript and more specifically—an async function. Why? Because async functions allow us to easily pause the execution of code until an asynchronous action happens. This allows templates to wait for a database query to arrive, an API to respond to a request, etc. before rendering the final markup.
Essentially, your whole template is wrapped in a function, similarly to how Node.js wraps modules in the module wrapper. This allows templates to have their own scope while also providing them with global-like variables, described here.
For example, the following template:
var path = require('path');
if (true) {
<h1>Hello! Look at this path: ${ path.join(__dirname, 'foobar.yaml') }</h1>
<echo escape>
Look at my markup:
<div>
<span>This is it!</span>
</div>
</echo>
} else {
<echo plain>
How is `true` not "truthy"?
This world has gone mad!
</echo>
}
is compiled to:
async function (__dirname, __filename, input, module, exports, require, include, echo, capture) {
var path = require('path');
if (true) {
echo(` <h1>Hello! Look at this path: ${ path.join(__dirname, 'foobar.yaml') }</h1>
`);
echo(`
Look at my markup:
<div>
<span>This is it!</span>
</div>
`, {'escape': true});
} else {
echo("\n How is `true` not \"truthy\"?\n This world has gone mad!\n ", {'plain': true});
}
}
As you can see above, markup segments and <echo>
tags are compiled with template literals. What would happen if you put a backtick `
in your markup? Yep, you would get an error because you terminate the literal and the rest is invalid JavaScript.
Why aren't backticks simply escaped by the compiler? Well:
-
It's tricky to determine when a template literal ends because of nested templates. So if you watch for the end of a template literal, you need to also monitor embedded expressions, string literals, regex literals, and don't forget that all literals and their tokens can be escaped, which makes things even harder.
-
If backticks are escaped, backslashes should be escaped too. If you have this input
\`
and backticks are escaped, the result is\\`
, which is an escaped backslash and an unescaped backtick, which leads us to our initial problem. To avoid it, we must also escape backslashes so that the result is\\\`
. However, this would remove a feature—having the ability to escape characters in your markup can be useful. For example:<h1>Backtick \` escaped, no worries. Also \${ template literal escaped }. And hey, here is a \n new line</h1>
-
If all characters are escaped due to #2, how would you escape an embedded expression? If you tried
\${ print this please }
, the compiled result would start with\\${...
which would still evaluate the expression. That's pretty much an edge case, but still...
If you want to output content as-is without the fear of breaking your template, you could use the <echo>
tag with the plain
option. This tells the compiler to use a normal string literal, instead of a template literal, and escape all fatal characters "
, \
, and \n
(because newlines are not allowed in a string literal). In the example script above, you can see that in action.
If we have something that is echoed asynchronously, we must make sure we await it. Otherwise, the template would be resolved before the actual result is received. For example:
<p>Before</p>
new Promise((resolve, reject) => {
setTimeout(() => {
<h1>The time has come!</h1>
resolve();
}, 200);
});
<p>After</p>
the output of this template would be:
<p>Before</p>
<p>After</p>
and we would get a warning in the console. To avoid that, we must use await
to continue the template's execution only after the Promise has resolved:
<p>Before</p>
await new Promise((resolve, reject) => {
setTimeout(() => {
<h1>The time has come!</h1>
resolve();
}, 200);
});
<p>After</p>
and the output is:
<p>Before</p>
<h1>The time has come!</h1>
<p>After</p>
Note: The await
keyword is only allowed inside an async function and we can use it because, as described above, our code is wrapped in an async function.
Since templates are wrapped in async functions, we must await
them as well when we use include()
:
<p>Before child</p>
include('./child');
<p>After child</p>
would render:
<p>Before child</p>
<p>After child</p>
However, this:
<p>Before child</p>
await include('./child');
<p>After child</p>
would correctly render:
<p>Before child</p>
<h1>Child content!</h1>
<p>After child</p>
When a template should be rendered, a new instance of the Renderer class is created. It contains all variables and functions that are passed as arguments in the async wrapper function described above. The template's compiled code is evaluated and its definition (its async function) is created and stored. Then, that async function is run and the template does its thing. After that, the final markup is resolved and ready to be consumed by whatever created the renderer.