Skip to content

About

Perl HTML::Template's template syntax, with a TypeScript API

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@libraz/html-template

CI npm codecov License TypeScript

Perl HTML::Template's template syntax, with a TypeScript API.

Existing .tmpl files render unchanged. The API around them is not a port of the Perl one: templates are compiled once and rendered many times, everything a render touches is its own, and a template's parameters can be turned into a TypeScript interface.

Installation

npm install @libraz/html-template

Node 22 or newer. No runtime dependencies.

Quick start

import { compile } from '@libraz/html-template';

const template = compile(`
  <h1><TMPL_VAR NAME="title"></h1>
  <TMPL_LOOP NAME="items">
    <p><TMPL_VAR NAME="name">: <TMPL_VAR NAME="value"></p>
  </TMPL_LOOP>
`);

const html = template.render({
  title: 'Hello World',
  items: [
    { name: 'Item 1', value: 'Value 1' },
    { name: 'Item 2', value: 'Value 2' }
  ]
});

Compiling is the expensive half, so keep the compiled template and render it as often as you like — it is immutable and safe to share across concurrent work.

For a one-off, render(source, data) does both in a single call.

Reading templates from disk

The core reaches templates only through a loader, which is what keeps it free of any filesystem dependency. Node's loader lives in its own subpath:

import { Environment } from '@libraz/html-template';
import { nodeFileLoader } from '@libraz/html-template/loaders';

const env = new Environment({
  loader: nodeFileLoader({ paths: ['./views'] }),
  cache: true
});

const html = env.renderFile('page.tmpl', { title: 'Hello' });

An environment holds the loader, the compile settings and the cache together, so nothing has to be configured twice and nothing lives in a process-wide global.

Template syntax

<TMPL_VAR NAME="title">
<TMPL_VAR NAME="raw" ESCAPE="none">
<TMPL_VAR NAME="missing" DEFAULT="fallback">

<TMPL_IF NAME="logged_in">welcome<TMPL_ELSE>please sign in</TMPL_IF>
<TMPL_UNLESS NAME="empty">there is something here</TMPL_UNLESS>

<TMPL_LOOP NAME="rows"><TMPL_VAR NAME="cell"></TMPL_LOOP>

<TMPL_INCLUDE NAME="header.tmpl">
<TMPL_COMMENT>never rendered</TMPL_COMMENT>

Every tag also has an HTML comment form — <!-- TMPL_VAR NAME="x" --> — for templates that have to stay valid HTML on their own.

See the syntax reference for the full set.

Escaping

Variables are HTML-escaped by default. ESCAPE="none" on a tag opts that one tag out, and defaultEscape at compile time changes the default:

compile('<TMPL_VAR NAME="x">').render({ x: '<b>' }); // '&lt;b&gt;'
compile('<TMPL_VAR NAME="x">', { defaultEscape: 'none' }).render({ x: '<b>' }); // '<b>'

Two defaults deliberately differ from Perl and change what renders: escaping, which Perl does not do unless told to, and case-sensitive name matching, which caseSensitive: false turns back into Perl's folding. Templates that already write ESCAPE="html" keep working; templates that relied on raw output need defaultEscape: 'none'. Coming from Perl has the full list.

Generated types

A template declares which names it uses and in what role, which is enough to describe the data it expects:

npx html-template-codegen views/ -o src/templates.d.ts
export interface PageData {
  title?: ScalarSource;
  items?: RowSource<{
    name?: ScalarSource;
  }>;
}

Pass the interface to compile and a typo in the data becomes a type error:

const template = compile<PageData>(source);

Add --check to a CI step to catch a generated file that has fallen behind.

Streaming

renderTo(sink, data) writes each piece as it is produced, and renderChunks(data) hands them back one at a time:

template.renderTo(response, data);

for (const chunk of template.renderChunks(data)) {
  // ...
}

Documentation

License

MIT

About

Perl HTML::Template's template syntax, with a TypeScript API

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages