Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

JavaScript

The @onlabsorg/texl package compiles the Texl engine to WebAssembly, with a JavaScript API for web pages and web workers. It renders templates from strings, loads documents over HTTP, and lets templates call your JavaScript functions.

A package for Node.js is coming soon.

Installing the package

Install the package from npm, and import it in code that a bundler, such as Vite or webpack, builds:

npm install @onlabsorg/texl
import init, { TexlEngine } from "@onlabsorg/texl";

The package contains the module texl_wasm.js, the WebAssembly binary texl_wasm_bg.wasm, and the TypeScript declarations texl_wasm.d.ts.

Without a bundler, import the module from a CDN that serves npm packages, with the version you want:

import init, { TexlEngine } from "https://cdn.jsdelivr.net/npm/@onlabsorg/texl@0.3.0/texl_wasm.js";

You can also build the package from a clone of the repository with wasm-pack, and copy the texl-wasm/pkg folder that it creates to your web site:

wasm-pack build texl-wasm --target web

Quick start

<script type="module">
import init, { TexlEngine } from "https://cdn.jsdelivr.net/npm/@onlabsorg/texl@0.3.0/texl_wasm.js";

await init();                                  // load the WebAssembly module

const engine = new TexlEngine({ user: { name: "Ada" } });
const text = await engine.render("Hello <% user.name %>!");
console.log(text);                             // Hello Ada!
</script>

The page must be served over HTTP: browsers do not load WebAssembly modules, or let them fetch documents, from file:// pages.

Loading documents

engine.load fetches a document by URL, renders it, and returns its rendering, source and data:

const doc = await engine.load("templates/report.texl");
if (doc) {
    document.querySelector("#report").textContent = doc.rendering;
}

A relative URL is resolved against the engine's base URL, which is the URL of the page by default. Inside a document, imports are resolved against the URL of the document:

In https://example.com/docs/report.texl, texl.import(...) offetches
"header.texl"https://example.com/docs/header.texl
"../lib/format.texl"https://example.com/lib/format.texl
"/lib/format.texl"https://example.com/lib/format.texl
"https://cdn.example.org/a.texl"https://cdn.example.org/a.texl

Templates rendered with engine.render, or with a context created without a URL, have no URL of their own: their imports are resolved against the root of the base URL, so "lib/format.texl" and "/lib/format.texl" both fetch https://example.com/lib/format.texl. A context created with a URL resolves them against that URL, like a document (see Contexts).

Documents are fetched with fetch, so the usual browser rules apply: a document on another origin must allow it with CORS headers. A response with an error status counts as a failed load: load gives null, and texl.import gives Nothing.

Each engine loads and renders each document once, and reuses the result for later loads and imports. Create a new engine to see changes to the documents.

Globals and context values

The objects you pass to the engine and to contexts become Swan values:

JavaScriptSwan
null, undefinedNothing
booleanBool
numberNumb
stringText
arrayList
Promise, or any object with a then methodPromise; a rejection resolves to Nothing
functionFunc (see below)
other objectNamespace of its own enumerable properties

Values that come back to JavaScript, from context.get, from document data and as function arguments, are converted the other way:

SwanJavaScript
Nothingnull
Bool, Numb, Textboolean, number, string
List, Tuplearray
Namespaceplain object with its members
PromisePromise
Func, Undefinedundefined

Functions

A JavaScript function becomes a Swan function of one argument. A Swan call with several arguments passes them as one array, so destructure it:

const engine = new TexlEngine({
    shout: (s) => s.toUpperCase() + "!",
    add: ([a, b]) => a + b,
});
await engine.render('<% shout "hi" %> <% add (2, 3) %>');   // "HI! 5"

The return value is converted to a Swan value. If the function throws, the result is Nothing. An async function, or a function that returns a promise, gives a Swan promise, and the tag waits for it:

const engine = new TexlEngine({
    weather: async (city) => (await fetch(`/api/weather/${city}`)).text(),
});
await engine.render('Today in Rome: <% weather "rome" %>');

Contexts

engine.render uses a fresh scope each time, so the names a template binds are lost when it ends. A context keeps them: it is a scope that you render several templates in, and that you can read and write from JavaScript.

const context = engine.createContext({ price: 12.5 });
await context.render("<% total = price * 4 %>");
context.get("total");                          // 50
await context.render("Total: <% total %>");    // "Total: 50"

A context can also stand for a document: pass its URL as the second argument, and the imports of the templates rendered in the context resolve against it. Use it to render a document whose source you already have, such as the text of an editor, without fetching it:

const context = engine.createContext(null, "docs/report.texl");
const rendering = await context.render(source);  // imports resolve in docs/

API reference

init

default function init(
    module_or_path?: { module_or_path: InitInput | Promise<InitInput> }
): Promise<InitOutput>

Loads and instantiates the WebAssembly module. Call it once, and wait for it, before using any other export.

By default, it fetches texl_wasm_bg.wasm from the folder of texl_wasm.js. Pass { module_or_path: url } to load it from somewhere else. The module also exports initSync({ module }), which instantiates an already compiled module synchronously.

import init from "@onlabsorg/texl";
await init();

TexlEngine

An engine renders templates and loads documents. Its globals, documents and engine-wide names (bound with bind) are shared by everything it renders.

new TexlEngine(globals, baseUrl)

constructor(globals?: object | null, baseUrl?: string | null)

Creates an engine.

ArgumentDescription
globalsAn object whose properties become globals of every template and document. Optional.
baseUrlThe URL that relative URLs resolve against. Optional; defaults to location.href, the URL of the page or worker.

Throws a string with an error message if baseUrl is not an absolute URL.

const engine = new TexlEngine(
    { site: { name: "My site" } },
    "https://example.com/templates/",
);

engine.baseUrl

readonly baseUrl: string | undefined

Returns the base URL of the engine, normalized. It is undefined only when no base URL was given and the global scope has no location.

new TexlEngine().baseUrl;                      // the URL of the page

engine.set(name, value)

set(name: string, value: any): void

Binds a global, visible to every template and document of the engine, including contexts created earlier. A global with the same name is replaced.

ArgumentDescription
nameThe name of the global.
valueIts value, converted to a Swan value.
engine.set("today", new Date().toISOString().slice(0, 10));

engine.load(uri)

load(uri: string): Promise<TexlDocument | null>

Loads and renders the document at uri.

ArgumentDescription
uriThe URL of the document, resolved against baseUrl.

Returns a promise of the document, or of null if it cannot be loaded. The promise rejects, with a string message, only if uri cannot be resolved to a URL. Each document is loaded once per engine; later calls with the same URL return the same result.

const doc = await engine.load("templates/report.texl");
console.log(doc?.rendering ?? "not found");

engine.render(template)

render(template: string): Promise<string>

Renders a template in a fresh scope, and returns a promise of the rendered text. The names the template binds with = or : are discarded afterwards.

ArgumentDescription
templateThe template source.
await engine.render("2 + 2 = <% 2 + 2 %>");    // "2 + 2 = 4"

engine.createContext(vars, uri)

createContext(vars?: object | null, uri?: string | null): TexlContext

Creates a context: a scope that keeps its names across renders.

ArgumentDescription
varsAn object whose properties are bound in the context. Optional.
uriThe URL of the document that the context stands for, resolved against baseUrl. Optional.

With a uri, imports in the context resolve against it, as in a document loaded with load; nothing is fetched from it. Without one, they resolve against the root of baseUrl, as in render. Throws, with a string message, if uri cannot be resolved to a URL.

const context = engine.createContext({ user: "Ada" });
const report = engine.createContext(null, "templates/report.texl");

engine.free()

free(): void

Releases the memory of the engine. The engine cannot be used afterwards. You only need it for engines that you create and discard many times; the engine also supports using declarations through Symbol.dispose.

TexlContext

A scope of an engine, created with engine.createContext. Templates rendered in a context share its names, and so do get and set. The context also sees the globals of its engine.

context.get(name)

get(name: string): any

Returns the value of a name, converted to a JavaScript value. The name is looked up in the context, then in the engine globals and the built-ins. A name that is not bound gives null.

ArgumentDescription
nameThe name to look up.
await context.render("<% x = (1, 2) %>");
context.get("x");                              // [1, 2]

context.set(name, value)

set(name: string, value: any): void

Binds a name in the context, replacing any previous value.

ArgumentDescription
nameThe name to bind.
valueIts value, converted to a Swan value.
context.set("items", ["a", "b"]);

context.names()

names(): string[]

Returns the names bound in the context, sorted. Engine globals and built-ins are not included. The list always includes texl, the namespace that holds texl.import.

engine.createContext({ b: 1, a: 2 }).names();  // ["a", "b", "texl"]

context.render(template)

render(template: string): Promise<string>

Renders a template in the context, and returns a promise of the rendered text. The names the template binds stay in the context.

ArgumentDescription
templateThe template source.
await context.render("<% count = 1 %>");
await context.render("Count: <% count %>");    // "Count: 1"

context.free()

free(): void

Releases the memory of the context, like engine.free.

TexlDocument

interface TexlDocument {
    uri: string;
    source: string;
    data: Record<string, any>;
    rendering: string;
}

A loaded document, as returned by engine.load.

PropertyDescription
uriThe URL of the document.
sourceThe template source.
dataThe names the document's tags bound, converted to JavaScript values. Functions are undefined.
renderingThe rendered text.
const doc = await engine.load("settings.texl");
console.log(doc.data.title, doc.rendering);