outie
TypeScript icon, indicating that this package has built-in type declarations

1.1.5 • Public • Published

Build Status

outie

A customizable templating engine for node, written in TypeScript.

Basic usage

Render a simple string template

import { Outie } from 'outie';
const outie = new Outie();

const template = `Hello, {name}!`;
const data = { name: 'world' };
const rendered = await outie.render(template, data);

console.log(rendered); // "Hello, world!"

Render a template file

<!-- hello.html.outie -->
<h1>Hello, {name}!</h1>
import { Outie } from 'outie';
const outie = new Outie();

const absPath = path.join(__dirname, 'hello.html.outie');
const data = { name: 'world' };
const rendered = await outie.renderFile(absPath, data);

console.log(rendered); // "<h1>Hello, world!</h1>"

Configuration

No configuration is required to get started.

import { Outie } from 'outie';

// use the default config
const outie = new Outie();

However, you have the option to configure almost all of the syntax you can see in the usage examples below.

import { Outie, defaultConfig, MruCache, Template } from 'outie';

// customize everything
const customConfig = {
    // these are the defaults
    tokenStart: '{',
    tokenEnd: '}',
    closeTokenIdentifier: '/',
    
    // tokens lets you add, remove, or customize
    // the set of supported "tokens" (aka tags)
    tokens: {
        // you can easily rename the bundled tokens 
        // using the exported `defaultConfig`

        // rename "raw" token to "~"
        '~': defaultConfig.tokens.raw, 
        // rename "includeRaw" token to "incRaw"
        'incRaw': defaultConfig.tokens.includeRaw, 
        // rename "include" token to "inc"
        'inc': defaultConfig.tokens.include, 
        // rename "if" token to "?"
        '?': defaultConfig.tokens.if, 
        // rename "unless" token to "!"
        '!': defaultConfig.tokens.unless, 
        // rename "for" token to "each"
        'each': defaultConfig.tokens.for, 

        // you can also create your own token definitions
        'random': class RandomToken extends Token {
            async render() {
                return Math.random().toString();
            }
        }
    },

    // cache up to 100 template files
    fileCache: new MruCache<Template>(100),
};

const outie = new Outie(customConfig);

Precompiling templates from strings

import { Outie } from 'outie';
const outie = new Outie();

const templateStr = `Hello, {name}!`;
const data = { name: 'world' };
const template = await outie.template(templateStr); // compile template
const rendered = await template.render(data); // render pre-compiled template

console.log(rendered); // "Hello, world!"

Precompiling templates from files

import { Outie } from 'outie';
const outie = new Outie();

const absPath = path.join(__dirname, 'hello.html.outie');
const data = { name: 'world' };
const template = await outie.templateFromFile(absPath); // compile template
const rendered = await template.render(data); // render pre-compiled template

console.log(rendered); // "Hello, world!"

Logic and looping

If/Unless

import { Outie } from 'outie';
const outie = new Outie();

const template = `
    {if lastVisit}Welcome back!{/if}
    {unless lastVisit}Welcome!{/unless}
`;
const data = { lastVisit: null };
const rendered = await outie.render(template, data);

console.log(rendered.trim()); // "Welcome!"

For loops

You can loop through any collection that is iterable using Object.keys, including arrays and objects. You can access both the key and the value within the loop.

import { Outie } from 'outie';
const outie = new Outie();

const template = `
    {for key:value in birds}
        The common name of {key} is {value}.
    {/for}
`;
const data = { 
    birds: {
        'Turdus migratorius': 'American robin',
        'Cardinalis cardinalis': 'Northern cardinal'
    }
};
const rendered = await outie.render(template, data);

console.log(rendered.trim());
// The common name of Turdus migratorius is American robin.
// The common name of Cardinalis cardinalis is Northern cardinal.

You can omit the key if you're only interested in the values.

import { Outie } from 'outie';
const outie = new Outie();

const template = `
    <ul>
    {for city in cities}
        <li>{city}</li>
    {/for}
    </ul>
`;
const data = { 
    cities: ['London', 'Tokyo']
};
const rendered = await outie.render(template, data);

console.log(rendered.trim());
// <ul>
//     <li>London</li>
//     <li>Tokyo</li>
// </ul>

Includes/Partials

Includes

You can include templates from other templates using relative or absolute paths. Relative paths are based on the location of the template from which they are included.

<!-- main.html.outie -->
<h1>Hello, {name}!</h1>
{include account.html.outie}

Included templates inherit the data model that is present at the time they're included, so you can use any data that would have been available in the same spot in the including template.

<!-- account.html.outie -->
<h2>Your Account</h2>
Your balance is {balance}.
import { Outie } from 'outie';
const outie = new Outie();

const absPath = path.join(__dirname, 'main.html.outie');
const data = { name: 'world', balance: '$1' };
const rendered = await outie.renderFile(absPath, data);

console.log(rendered);
// <h1>Hello, world!</h1>
// <h2>Your Account</h2>
// Your balance is $1.

Raw includes

If you just want to dump the contents of another file into your template, you can use a raw include.

<!-- main.html.outie -->
<h1>Hello, {name}!</h1>
{includeRaw raw.html.outie}
<!-- raw.html.outie -->
The contents of this {file} are left unparsed.
import { Outie } from 'outie';
const outie = new Outie();

const absPath = path.join(__dirname, 'main.html.outie');
const data = { name: 'world' };
const rendered = await outie.renderFile(absPath, data);

console.log(rendered);
// <h1>Hello, world!</h1>
// The contents of this {file} are left unparsed.

HTML encoding and raw values

By default, all data is HTML encoded when rendered in templates. You can, however, also render data unencoded.

import { Outie } from 'outie';
const outie = new Outie();

const template = `Hello, {raw name}!`;
const data = { name: '<script>alert("xss");</script>' };
const rendered = await outie.render(template, data);

console.log(rendered); // "Hello, <script>alert("xss");</script>!"

Custom tokens

Basic example

Here's a complete example of creating a simple custom token that simply outputs a random number when it's used.

We start by extending the abstract class Token:

import { Token } from 'outie';

class RandomToken extends Token {
    async render() {
        return Math.random().toString();
    }
}

Then add the token to your config and use it in a template:

import { Outie, defaultConfig } from 'outie';

const outie = new Outie({
    ...defaultConfig
    tokens: {
        ...defaultConfig.tokens,
        'random': RandomToken
    }
});

await outie.render('Your number is: {random}', {}); // Your number is: 0.24507892345

Adding parameters

Math.random() is great, but it would be better if we could control the range of the number that's generated. Let's add some parameters to our custom token to do just that.

When we're done, we'll be able to use it like so to get a random number between 10 and 20:

{random 10 20}

We'll use our previous example as a starting point, but add a constructor and a couple of fields to keep track of the desired min and max.

import { Token, Template } from 'outie';

class RandomToken extends Token {
    private readonly min: number;
    private readonly max: number;

    constructor(content: string) {
        super(content);

        // `content` is the content of the token with the 
        // _identifier_ stripped away.
        // So, for "{random 10 20}", `content` is "10 20".
        const [min, max] = this.content.trim()
            .split(/\s+/)
            .map(s => parseInt(s));

        // a "real" implementation would include some
        // error handling
        this.min = min;
        this.max = max;
    }

    async render() {
        const n = (Math.random() * (this.max - this.min)) + this.min;
        return n.toString();
    }
}

Creating a block token

Block tokens are used when a token should have a start and an end. This is commonly used for looping and conditionals but can be used anywhere that you need to handle nested content.

To create a block token, extend the abstract class BlockStartToken.

As an example, we'll create a simple token that wraps anything inside in an <h1> element.

Note: Block tokens have full control over the rendering of any child (i.e. nested) tokens. If your block token doesn't render its child tokens, they will not be rendered.

export class HeadingToken extends BlockStartToken {

    async render(model: RenderModel): Promise<string> {
        // all child tokens are stored in `this.children`
        const nestedTokens = this.children;
        // you can use `Token.renderTokens` to easily render all child tokens
        const renderedChildren = await Token.renderTokens(nestedTokens, model);
        
        return `<h1>${renderedChildren}</h1>`;
    }
}

Versions

Current Tags

  • Version
    Downloads (Last 7 Days)
    • Tag
  • 1.1.5
    2
    • latest

Version History

Package Sidebar

Install

npm i outie

Weekly Downloads

2

Version

1.1.5

License

MIT

Unpacked Size

123 kB

Total Files

78

Last publish

Collaborators

  • dgrundel