Kernel Template Language
Kernel is a super simple template language that has a couple really nice features that make it a nice fit for node servers.
Why call it "kernel"? I dunno, I often name my projects after simple food ingredients (wheat, corn, etc..) Also it's a data flow kernel for a web app.
The Language
The kernel template language is very simple. There are only three types of tags. Everything else is treated as raw text and output as-is.
Basic Variable
To do basic variable replacement in Kernel, simple wrap the variable (or other simple javascript expression) in curly braces.
Hello world, my name is {name}!
This will compile roughly to:
"Hello world, my name is " + name + "!"
Async function
The main feature of Kernel is the ability to load async. Also via helper functions, you can implement all kinds of useful constructs (loops, conditionals, lazy database queries, etc..)
To call an async function and insert it's callback's result inline, simply use parentheses.
Hello world, the database says {query("saying")}
In your data object that's passed into the template, this will be called as:
function query(arg, callback) {
callback(null, data);
}
Notice that query
is a normal node-style async function. Any arguments passed in the template will be passed before the callback in the async function.
Kernel will automatically call all the async functions in parallel and merge the results for you.
Block function
Block functions are just like normal async functions, except they can contain nested template. The syntax is the same, except you put a #
before the opening tag and a /
before the closing tag.
<ul>
{#loop(items)}
<li>{name} - {price}</li>
{/loop}
</ul>
The template code between the tags is compiled into a sub-template function. When this is executed, the loop
function will be called with the sub-template as the second-to-last argument.
function loop(arg, block, callback) {
block(newData, function (err, result) {
callback(null, result);
});
}
Just like plain async functions, block functions can have any number of extra arguments. With blocks, you can call them without any parentheses at all and it will assume zero arguments.
Basic NodeJS Usage
The basic usage is pretty simple. The module is a function that takes a file path and gives you a compiled template via the callback. You can then call the template with an object providing the local variables and context. It lets you know when it's done via a callback.
var Kernel = require('kernel');
var template = Kernel("mytemplate.html", function (err, template) {
if (err) throw err;
template({some: "vars", go: "here"}, function (err, html) {
if (err) throw err;
console.log(html);
});
});
Since Kernel
does file I/O, it automatically batches similar requests into a single FS.readFile
and returns the same template to all concurrent requests. Also, by default the compiled template is cached for 1000ms. Subsequent requests to compile the same template will just reuse the function. You can disable the cache by setting Kernel.cacheLifetime
to something falsy.
Basic Browser Usage
Usage in the browser is almost identical. Simply include the kernel.js script in your html page and there is now a global function called Kernel
. The loader looks for script tags by name. If you want some other behavior, simply override Kernel.resourceLoader()
.
<html>
<head>
<script src="kernel.js"></script>
<script name="mytemplate" type="text/x-kernel-template">
Template goes here
</script>
</head>
<body>
<script>
Kernel("mytemplate", function (err, template) {
if (err) throw err;
template({some: "vars", go: "here"}, function (err, html) {
if (err) throw err;
console.log(html);
});
});
</script>
</body>
</html>
Advanced Usage
If you don't want to use the built-in file loading and caching, then you can get at the compile steps directly. Then you can use any data source for your templates.
Kernel.resourceLoader(filename, callback)
This function is used by Kernel()
to load a template. By default it's FS.readFile with the encoding set to utf8 for node and reads the contents of a file in node, and loads the contents of a specially marked script tag in the browser.
If you want to load templates some other way, simply override this function.
Kernel.compile(source, filename)
Pass in pre-loaded template source and a filename (used for debugging) and it will tokenize, parse, generate, and eval all in one step. Returns the compiled template function.
Kernel.tokenizer(source)
The tokenizer takes in a string of kernel source and returns an array of tokens.
Kernel.parser(tokens, source, filename)
This step takes in as input, the tokens returned by Kernel.tokenizer
as well as the source and filename (for error reporting purposes).
This function returns a new token stream, but slightly modified. Instead of a flat string of tokens, this is a tree of nested blocks. Also basic tokens that touch each other are merged into arrays.
Kernel.generator(tokens)
The generator takes in the modified tokens that were output by Kernel.parser
and returns the javascript source of the template function. It's still up to you at this point to turn the string into an executable function. You can save the generated file to disk that you later require or eval it into a new live function.
Kernel.cacheLifetime
As mentioned earlier. This is used by Kernel
to know if and how long to cache loaded and compiled templates. Changes to this value take effect immedietly for all new template compiles.
Tips and Tricks
Now that the basic API of the language it out of the way, how about some examples of how to use this effectively.
Conditionals
While there are no conditionals built into the language, they are trivial to implement as block functions.
{#conditional(someValue)}
someValue was truthy!
{/conditional}
And to implement this, you would write the condition function as:
function conditional(condition, block, callback) {
if (condition) block({}, callback);
else callback(null, "");
}
See how easy that was. And since it's outside the language, you can get creative.
Loops
There are many kinds of loops and different degrees of asynchronousness. But here is a simple loop to get you started.
{#loop(items)}
{name} - {price}
{/loop}
And the function to implement this is:
function loop(array, block, callback) {
var index = array.length;
var parts = new Array(index--);
array.forEach(function (part, i) {
block(part, function (err, result) {
if (err) return error(err);
parts[i] = result;
check();
});
});
var done;
check();
function error(err) {
if (done) return;
done = true;
callback(err);
}
function check() {
if (done) return;
while (parts.hasOwnProperty(index)) { index--; }
if (index < 0) {
done = true;
callback(null, parts.join(""));
}
}
}
It's a little complicated, but very robust. The sub-template will be executed once for every value in array. If the sub-template is async, the waits will be in parallel. The done
variable is to ensure that callback is only ever called once. This is important behavior for async functions. The check
function simply runs through the array and stops on the first missing slot.