Usage

This chapter describes the API of Twing and not the Twig language.

Basics

The simplest and more straightforward way to use Twing is to instantiate an environment and use it to load and render templates.

import {createEnvironment, createFilesystemLoader, createFilesystemCache} from "twing";
import * as fs from "fs";

const loader = createFilesystemLoader(fs);
const environment = createEnvironment(loader);

This creates an environment with the default settings and a loader that looks up the templates in the filesystem. Different loaders are available, and you can also write your own.

Note that createEnvironment takes a hash of options as second argument.

Loading and rendering a template

To load a template, call the environment loadTemplate method, which returns a TwingTemplate instance.

environment.loadTemplate('index.html').then((template) => {
    // ...
});

Then, to render the template with some context, call the template’s render method:

template.render({'the': 'variables', 'go': 'here'}).then((output) => {
    // ...
});

You can also load and render a template in one single swoop by using the environment convenience method render:

environment.render('index.html', {'the': 'variables', 'go': 'here'}).then((output) => {
    // ...
});

Loaders

Loaders are responsible for locating and loading templates.

Here is a list of the loader implementations provided by the Twing package:

createFilesystemLoader

createFilesystemLoader creates an instance of TwingFilesystemLoader capable of loading templates from the filesystem.

import * as fr from "fs";

const loader = createFilesystemLoader(fs);

createFilesystemLoader takes an instance of TwingFilesystemLoaderFilesystem interface as second argument. It means that it can be used in any kind of context, browsers included.

The filesystem loader also supports namespaced templates. This allows to group your templates under different namespaces which have their own template paths.

When using the addPath, and prependPath methods, specify the namespace as the second argument.

loader.addPath('a/path', 'admin');
loader.addPath('another/path', 'admin');

Namespaced templates can be accessed via the namespace_name/template_path notation:

environment.render('admin/index.html', {});

createArrayLoader

createArrayLoader create an instance of TwingArrayLoader capable of loading templates from an arbitrary register.

const loader = createArrayLoader({
    'index.html': 'Everybody loves {{ name }}!',
});
const environment = createEnvironment(loader);

environment.render('index.html', {'name': 'Twing'}).then((output) => {
    // ...
});

createChainLoader

createChainLoader create an instance of TwingChainLoader that delegates the loading of templates to some other loaders:

const loader1 = createArrayLoader({
    'base.html': '{% block content %}{% endblock %}',
});

const loader2 = createArrayLoader({
    'index.html': '{% extends "base.html" %}{% block content %}Hello {{ name }}{% endblock %}',
    'base.html': 'Will never be loaded',
});

const loader = createChainLoader([loader1, loader2]);

When looking for a template, the loader will try each loader in turn and will return as soon as the template is found. When rendering the index.html template from the above example, the loader will load it with loader2 but the base.html template will be loaded from loader1.

Extending Twing

Twing can be extended in many ways; you can add extra tags, filters, tests, operators, global variables, and functions. You can even extend the parser itself with node visitors.

The first section of this chapter describes how to extend Twing easily. If you want to reuse your changes in different projects or if you want to share them with others, you should then create an extension as described in the following section.

Before extending Twing, you must understand the differences between all the different possible extension points and when to use them.

First, remember that Twig has two main language constructs:

  • {{ }}: used to print the result of an expression evaluations

  • {% %}: used to execute statements

To understand why Twing exposes so many extension points, let’s see how to implement a Lorem ipsum generator that needs to know the number of words to generate.

You can use a lipsum tag:

{% lipsum 40 %}

That works, but using a tag for lipsum is not a good idea for at least three main reasons:

  • lipsum is not a language construct;
  • The tag outputs something;
  • The tag is not flexible as you cannot use it in an expression:
{{ 'some text' ~ {{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }}

In fact, you rarely need to create tags; and that’s good news because tags are the most complex extension point of Twing.

Now, let’s use a lipsum filter:

{{ 40|lipsum }}

Again, it works, but it looks weird. A filter transforms the passed value to something else but here we use the value to indicate the number of words to generate (so, 40 is an argument of the filter, not the value we want to transform).

Next, let’s use a lipsum function:

{{ lipsum(40) }}

Here we go. For this specific example, the creation of a function is the extension point to use. And you can use it anywhere an expression is accepted:

{{ 'some text' ~ lipsum(40) ~ 'some more text' }}

{% set lipsum = lipsum(40) %}

Last but not the least, you can also use an object - here text - with a method able to generate lorem ipsum text, and pass this object to the rendering context.

const text = {
  lipsum: (numberOfWords) => {
    // ...
  }
};

template.render({
  text
});
{{ text.lipsum(40) }}

Filters

Filters can be created using the createFilter method which requires at least three parameters: the name of the filter, the function that is executed when the filter is applied - the executor, and an array of accepted arguments.

The executor is a function that takes an instance of TwingExecutionContext) as first parameter, the value to filter as second parameter and the specific parameters of the filter as rest parameters, and returns a Promise that resolve to the filtered result.

const rot13 = require('rot13');

const filter = createFilter('rot13', (_executionContext, value) => {
    return Promise.resolve(rot13(value));
}, []);

Then, add the filter to your environment.

const environment = createEnvironment(loader);

environment.addFilter(filter);

Last, use it in a template.

{{ 'Twing'|rot13 }}

{# will output Gjvat #}

When called by the template, the executor receives the left side of the filter (before the pipe |) as second argument and the arguments passed within parentheses () as rest arguments.

For instance, the following code…

{{ 'Twing'|lower }}
{{ now|date('d/m/Y') }}

…would make the date filter executor receives “Twing” as second argument and “d/m/Y” as third argument.

createFilter also accepts a fourth argument that consists of a hash of options.

  • isSafe

If automatic escaping is enabled, the output of the filter may be escaped before printing. If your filter acts as an escaper (or explicitly outputs a safe markup), you will want the raw output to be printed. In such a case, set the isSafe option to the list of escaping strategy names that should not be applied to the filter output.

const filter = createFilter('nl2br', nl2br, [{
    name: 'value'
}], {
    isSafe: ['html']
});
  • preEscape

Some filters may need to work on input that is already escaped or safe. In such a case, set the preEscape option to the escaping strategy name that should be applied to the filter input.

const filter = createFilter('somefilter', somefilter, [{
    name: 'value'
}], {
    preEscape: 'html',
    isSafe: ['html']
});
  • isVariadic

When a filter should accept a variable number of arguments, set the isVariadic option to true so that the extra arguments are passed to the executor as rest parameters.

const filter = createFilter('thumbnail', (fileName: string, ...options: Array<any>) => {
    // ...
}, [{
    name: 'fileName'
}], {
    isVariadic: true
});

Be warned that named arguments passed to a variadic filter cannot be checked for validity as they will automatically end up in the rest parameters.

Dynamic Filters

A filter name containing the special * character is a dynamic filter as the * token can be any string:

const filter = createFilter('*_path', (_executionContext, prefix, value) => {
    // ...
}, []);

The following filters will be matched by the above defined dynamic filter:

  • product_path
  • category_path

A dynamic filter can define more than one dynamic parts:

const filter = createFilter('*_path_*', (_executionContext, prefix, suffix, value) => {
    // ...
}, []);

The filter will receive all dynamic part values before the normal filter arguments, but after the execution context. For instance, a call to 'foo'|a_path_b() would result in the following arguments to be received by the filter: (executionContext, 'a', 'b', 'foo').

Deprecated Filters

You can mark a filter as being deprecated by setting the deprecated option to true. You can also give an alternative filter that replaces the deprecated one when that makes sense:

const filter = createFilter('obsolete', () => {
    // ...
}, [], {
    deprecated: true, 
    alternative: 'new_one'
});

When a filter is deprecated, Twing emits a deprecation warning when compiling a template using it.

Functions

Functions are defined in the exact same way as filters, but you need to create an instance of TwingFunction.

const environment = createEnvironment(loader);
const myFunction = createFunction('myFunction', () => {
    // ...
}, []);

environment.addFunction(myFunction);

Functions support the same features as filters, except for the pre_escape and preserves_safety options.

Tests

Tests are defined in the exact same way as filters and functions, but you need to create an instance of TwingTest. Not that a test’s executor must return a promise that resolves to a boolean.

const environment = createEnvironment(loader);
const test = createTest('test_name', () => {
    return Promise.resolve(true);
});

environment.addTest(test);

Tags

One of the most exciting features of a compiler like Twing is the possibility to define new language constructs. This is also the most complex feature as you need to understand how Twing’s internals work.

Most of the time though, a tag is not needed:

  • If your tag generates some output, use a function instead.

  • If your tag modifies some content and returns it, use a filter instead.

    For instance, if you want to create a tag that converts a Markdown formatted text to HTML, create a markdown filter instead:

    {{ '**markdown** text'|markdown }}
    

    If you want to use this filter on large amounts of text, wrap it with the apply tag:

    {% apply markdown %}
    Title
    =====
    
    Much better than creating a tag as you can **compose** filters.
    {% endapply %}
    
  • If your tag does not output anything, but only exists to create a side effect, create a function that returns nothing and call it via the do tag.

    For instance, if you want to create a tag that logs text, create a log function instead and call it via the do tag:

    {% do log('Log some things') %}
    

If you still want to create a tag for a new language construct, great!

Let’s create a tag handler that supports Drupal’s trans tag.

{% trans %}
{% endtrans %}

Note that the trans tag of Drupal should not exist, according to the rule “if it outputs something, it must be a function”. But even core Twig tags violate this rule so…

Adding a tag is as simple as calling the addTagHandler method of TwingEnvironment, with an instance of TwingTagHandler as sole argument.

const environment = createEnvironment(loader);

environment.addTagHandler({
  tag: 'trans',
  initialize: (parser) => {
    return (token, stream) => {
      const {line, column} = token;

      stream.expect(TokenType.TAG_END);

      const data = parser.subparse(stream, 'trans', (token) => {
        return token.test(TokenType.NAME, 'endtrans');
      });

      stream.next();
      stream.expect(TokenType.TAG_END);

      const translateNode = createBaseNode("trans", {}, {
        body: createPrintNode(data, line, column)
      });

      translateNode.execute = (...args) => {
        const {body} = translateNode.children;
        const outputBuffer = args[2];

        outputBuffer.start();

        return body.execute(...args)
          .then(() => {
            outputBuffer.echo(`This is the translated flavor of "${outputBuffer.getAndClean()}"`);
          })
      };

      return translateNode;
    }
  }
});

Creating an Extension

The main motivation for writing an extension is to move often used code into a reusable component. An extension can define tag handlers, filters, tests, operators, functions, and node visitors.

An extension is an object that implements the TwingExtension interface:

interface TwingExtension {
    readonly filters: Array<TwingFilter>;
    readonly functions: Array<TwingFunction>;
    readonly nodeVisitors: Array<TwingNodeVisitor>;
    readonly operators: Array<TwingOperator>;
    readonly tagHandlers: Array<TwingTagHandler>;
    readonly tests: Array<TwingTest>;
}

Hence, providing an extension is as simple as providing a factory that returns a TwingExtension instance, like the barren following one.

const createMyExtension = (): TwingExtension => {
  return {
    get filters() {
      return [];
    },
    get functions() {
      return [];
    },
    get nodeVisitors() {
      return [];
    },
    get operators() {
      return [];
    },
    get tagHandlers() {
      return [];
    },
    get tests() {
      return [];
    }
  };
};

All extensions must be registered to be available in your templates.

You can register an extension by using the addExtension() method of TwingEnvironment instances:

const environment = createEnvironment(loader);

environment.addExtension(createMyExtension());