Skip to content
This repository has been archived by the owner on Aug 23, 2022. It is now read-only.

How It Works

Hristiyan Dodov edited this page Jan 7, 2019 · 3 revisions

Compilation

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});
}

}

Caveats

Template Literals

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:

  1. 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.

  2. 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>
  3. 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.

Await

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.

Including templates

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>

Rendering

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.

Clone this wiki locally