Jet Engine Documentation v1.3.9

Introduction

jet-engine is a powerful template engine with syntax very similar to dustjs-linked in. It was written because I wanted to produce the smallest bundle possible for a recent project. I liked dustjs syntax and thought this would be a fun side project.

While I used only a subset of it's features for our first project, I continued developing jet-engine into the robust tool it is today. I'm very pleased with the result. Each function is written in a separate source file, and it is easy to add additional features. In the few cases in which I wanted templates to behave differently than dustjs, I implemented my preferred approach. I have never had a need for asynchronous data fetching, so I left that out. I suppose it's possible this could be added in a future version.

Quick Start

Quick Start

Credit where it is due: the syntax of the jet-engine template language is very similar to that of dustjs. However, I will not assume that you have had any experience with dustjs. We'll start our first example from scratch:

import { render } from 'jet-engine';
const out = render('Hello, {name}!', { name: 'World' });

console.log(out); // Hello, World!

jet-engine templates are rendered with the render() function. This function takes 2 arguments: a template and a context. The context may be of any type, but Objects are most often used. To "render" in jet-engine terminology simply means to process the template and context to produce output.

jet-engine templates are made up of template commands. In this example, we see two types of template commands: a data substitution command, and two text commands. The first command is the text command 'Hello, '. Text commands are copied directly to the output. They are the only template command not enclosed in {curly braces}.

The next template command in this example is the data subsitution command {name}. The value of name in the context is substituted for the {name} command in the template.

Lastly, there is another text command '!'. This text is added to the output, and the result, 'Hello, World!' is returned by the function.

Templates can do much more than text output and data substitution, however. Continue to learn more about all of the jet-engine template commands.

Introduction Template commands

Template Commands

There are four types of jet-engine template commands. Any or all of these may be present within a template.

  • Data substitution

    The commands consist of Javascript data access notation inside of curly braces. Here are some examples:

    {name}
    {people.names}
    {models["mustang"].year}
    {names[2]}
    {table[4][6]}

    See the Context Stack example to learn how data substitution commands are resolved during rendering.

  • Tag

    Tags are also surrounded by curly braces, but with a unique character immediately following the '{' character. The following tags are currently supported:

    {#section}      // set context or iterate
    {?test}         // output if test is true
    {!test}         // output if test is false
    {:else}         // else clause of a test
    {* comment *}   // comments
    {>subroutine}   // output a pre-registered template
    {<definition}   // define a block value
    {+block}        // output block value

    Each of these tag commands is explained in it's own section.

  • Helper

    Helpers are a special type of tag, always beginning with the '@' character, and the helper name. These are the currently supported helper commands:

    {@eq key=aKey value=aValue}     // output if key === value
    {@ne key=aKey value=aValue}     // output if key !== value
    {@gt key=aKey value=aValue}     // output if key > value
    {@gte key=aKey value=aValue}    // output if key >= value
    {@lt key=aKey value=aValue}     // output if key < value
    {@lte key=aKey value=aValue}    // output if key <= value
    {@first}                        // output if first iteration
    {@last}                         // output if last iteration
    {@sep}                          // output if not the last iteration
    {@select key=aKey}              // set a key for select
    {@any}                          // output if any @select cases are true
    {@none}                         // output if no @select cases are true
  • Text

    Any text not contained within curly braces is output as-is. We saw examples of this in the Quick Start example.

Quick Start Data substitution

Data Substitution

We saw a simple example of a data substitution command in the Quick Start example. These commands consist of Javascript data access notation (a data reference) inside of curly braces:

{name}
{list[0]}
{cities["phoenix"].population}
{amount[4][6]}

Data substitution commands are resolved during rendering using the resolver, and their values placed in the output. How does the resolver work? To understand the answer to that question requires an understanding of the context stack.

The Context Stack

The context stack is, as the name implies, a stack of data contexts that operate as a last-in-first-out (LIFO) stack, with data being pushed onto and popped off of the stack as required by the template rendering engine. The most recent context pushed onto the stack is referred to as the "top" of the stack. The first context pushed onto the stack is referred to as the "bottom" of the stack.

When the render() method is called, the context argument is pushed onto the stack, thus becoming the "top" of the stack. Some template commands, such as {#section} and {>subroutine} may push and pop other contexts as required. When the rendering engine attempts to resolve a data substitution command, the resolver first tries to resolve the command using the context at the top of the stack.

Consider the following code:

const out = render('Our employee discount is {discounts.employee}.', {
  item: "Toolkit",
  price: 40.00,
  discounts: {
    "employee": 0.15,
    "AAA": 0.05,
    "veteran": 0.10
  }
});

console.log(out); // Our employee discount is 0.15.

Initially, the context argument will be on the top of the stack. When the {discounts.employee} data substitution command is encountered, the resolver looks there to try to resolve it. In this case, there is indeed a discounts attribute on the top of the stack, and the value of discounts.employee is 0.15.

What happens when the required data is not found on the top of the stack? In this case, the resolver looks at the next lowest context on the stack. If the data is still not found, the next lowest context is examined, and so on until the bottom of the stack has been reached. If the data cannot be resolved, an empty string "" is returned. Here is another example using a different template, but the same context.

const out = render('Employee discount for {item} is {#discounts}{employee} * {price} = {@math key=employee method="*" operand=price}{/discounts}.', {
  item: "Toolkit",
  price: 40.00,
  discounts: {
    "employee": 0.15,
    "AAA": 0.05,
    "veteran": 0.10
  }
});

console.log(out); // Employee discount for Toolkit is 0.15 * 40.00 = 6.00

As always the context passed to the render() function is pushed onto the context stack. You can visualize the stack at this point like this:

TOP
{
  item: "Toolkit",
  price: 40.00,
  discounts: {
    "employee": 0.15,
    "AAA": 0.05,
    "veteran": 0.10
  }
}

The {item} data substitution command is resolved from the context and replaced with the value "Toolkit". The output up to this point is:

Employee discount for Toolkit is 

The {#discounts} command is a section command. It consists of a # character followed by a data reference. The section command resolves the data reference from the current context, and pushes this value onto the stack. In this case, the value of discounts is pushed onto the stack. The stack now looks like this:

TOP
{
    "employee": 0.15,
    "AAA": 0.05,
    "veteran": 0.10
}
TOP - 1
{
  item: "Toolkit",
  price: 40.00,
  discounts: {
    "employee": 0.15,
    "AAA": 0.05,
    "veteran": 0.10
}

The next command is the data substitution {employee}. The resolver tries to fond this value on the top of the stack. Now that the top of the stack is the original discounts object, the value of employee is 0.15. The output so far is:

Employee discount for Toolkit is 0.15 *

Here is where things get interesting. The data substitution command is {price}. The resolver again tries to find this data reference using the context on top of the stack. However, there is no price attribute there, so the resolver looks at the previous item on the stack, at position TOP - 1. Here, the value of price is found to be 40.00. The output so far is:

Employee discount for Toolkit is 0.15 * 40.00 =

You can think of the resolver as always finding the data reference that is closest to the top of the context stack.

The next command is a math helper command, {@math key=employee method="*" operand=price}. It performs an arithmetic operation on two values. Helpers will be described in detail in another section, but as you might imagine, this particular instance of the math helper multiples the values of the employee discount and the price, and outputs the value 6.00,

Employee discount for Toolkit is 0.15 * 40.00 = 6.00

The final command is {/discounts}. This is the closing tag for the {#discounts} command encountered earlier. The text following the / must exactly match the data reference of the corresponding section tag. The section closing tag results in the top of the context stack being popped and discarded, leaving the stack as it was before the section tag:

TOP
{
  item: "Toolkit",
  price: 40.00,
  discounts: {
    "employee": 0.15,
    "AAA": 0.05,
    "veteran": 0.10
  }
}

More documentation to follow

Template Commands