npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

outie

v1.1.5

Published

Templating

Downloads

5

Readme

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