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 🙏

© 2026 – Pkg Stats / Ryan Hefner

jtlt

v0.25.0

Published

Uses an approach similar to [XSLT](https://www.w3.org/Style/XSL/) for declarative, linear declaration of templates, but with JSON or JavaScript object data sources. As with XSLT, can be transformed into different formats (e.g., HTML strings, JSON, DOM obj

Downloads

2,566

Readme

jtlt

JavaScript Template Language Transformations (JTLT, pronounced as "Jetlet")—a JavaScript equivalent of XSLT, for JSON/JavaScript object or XML data sources.

As with XSLT, allows for declarative, linear declaration of (recursive) templates and can be transformed into different formats (e.g., strings, JSON, or DOM objects).

See the Demo.

Credits

Packaged with JSONPath Plus.

The sample file is from https://goessner.net/articles/JsonPath/

Installation

npm install jtlt

In the browser, you will also need to include the dependencies. See the test file.

Basic usage

The quickest way to run a transform is the jtlt() function. Give it a config object; it runs the transform and returns a Promise that resolves to the result:

import {jtlt} from 'jtlt';

const data = {title: 'Hello', items: ['a', 'b']};

const templates = [
  {path: '$', template () {
    this.applyTemplates('$.title');
    this.applyTemplates('$.items[*]');
  }},
  {path: '$.title', template (v) {
    this.element('h1', {}, [], () => this.text(v));
  }},
  {path: '$.items[*]', template (v) {
    this.element('li', {}, [], () => this.text(v));
  }}
];

const out = await jtlt({data, templates, outputType: 'string'});
// -> <h1>Hello</h1><li>a</li><li>b</li>

Templates may be async (for example to await this.indexedDB(...)); jtlt() awaits them automatically. Pass sync: true to forbid asynchronous templates (a template that then returns a Promise throws).

The same call works in Node and the browser (in the browser you must also load the dependencies — see the test file). For XML/HTML sources, add engineType: 'xpath' — see Quick start (XML source with XPath).

jtlt() vs JTLT.create()

jtlt() is a thin, Promise-returning wrapper around the lower-level JTLT class. Prefer jtlt(). Reach for JTLT.create() / new JTLT() only when you need the instance itself, autostart: false, or to drive .transform(mode) yourself.

| | jtlt(config) | JTLT.create(config) | | --- | --- | --- | | Returns | a Promise of the result | a JTLT instance | | Result delivery | the resolved value | a required success callback (also returned by .transform()) | | Async templates | awaited automatically | awaited automatically; .transform() returns a Promise |

Anywhere below that shows JTLT.create({…}).transform(mode) can instead be written await jtlt({…, mode}).

API

See the docs. A high‑level overview is below.

API overview

Run a transform with the jtlt(config) function (Promise-returning, recommended) or the lower-level JTLT class (JTLT.create(config) / new JTLT(config), which delivers the result through a required success callback). Under the hood JTLT has two layers:

  • Engine (template application):

    • JSONPathTransformer: Applies templates to JSON by matching JSONPath selectors (and optional modes), resolving priority, and invoking the winning template. Falls back to built‑in default rules when no user template matches.
    • JSONPathTransformerContext: The execution context passed to templates. It mirrors the joiner API (e.g., string(), object(), array()) so templates can emit results. It also provides helpers like applyTemplates(), callTemplate(), valueOf(), variable(), param(), withParam(), and forEach().
    • XPathTransformer (experimental): Applies templates to XML/HTML DOM by matching XPath selectors (and optional modes). Supports three evaluation modes: version 1 (native XPathEvaluator), version 2 (via xpath2.js), and version 3.1 (via fontoxpath). Falls back to built‑in default rules when no template matches.
    • XPathTransformerContext (experimental): Execution context for XPath. Offers get(), forEach(), valueOf(), variable(), param(), withParam(), key() and the same joiner helpers as the JSONPath context.
  • Joiners (output builders):

    • StringJoiningTransformer: Builds a string. Context‑aware append() routes into objects/arrays when inside object()/array() scopes, otherwise concatenates to a buffer. Includes element(), attribute(), and text() helpers for HTML/XML emission.
    • DOMJoiningTransformer: Builds a DocumentFragment/Element tree. element()/attribute()/text() add real nodes; primitives append as text nodes.
    • JSONJoiningTransformer: Builds real JS values (objects/arrays/primitives) without serialization.

Output formats and multi-document

  • Output formats supported via output({method}): xml, html, text, xhtml, and json.
    • xml/xhtml behave like XML: XML declaration (unless omitted) and optional DOCTYPE.
    • html is HTML‑centric; text is raw text; json is JSON‑centric (no XML declaration/DOCTYPE).
  • Multi-document APIs:
    • document(cb, cfg?): Create additional documents; when a joiner is configured with {exposeDocuments: true}, get() returns an array of documents.
    • resultDocument(href, cb, cfg?): Create additional documents with metadata (href, format, and document) accessible on joiner._resultDocuments.

Common joiner methods

  • append(value): Central sink. Based on context, concatenates to string, pushes to array, or assigns to an object property.
  • get(): Return the accumulated result.
  • object(obj?, cb?, usePropertySets?, propSets?): Enter object context; optionally seed from an object or build via cb.
  • array(arr?, cb?): Enter array context; optionally seed from an array or build via cb.
  • string(str, cb?): Emit a string value (no HTML escaping). In String joiner, optional cb lets you compose nested fragments before emitting.
  • number(num), boolean(bool), null(), undefined() (JS mode only), nonfiniteNumber(NaN|Infinity), function(fn) (JS mode only): Emit primitives/functions.
  • element(name, attrs?, children?, cb?): Build elements (String and DOM joiners). In String joiner, uses Jamilih under the hood to serialize; in DOM joiner, creates Elements.
  • attribute(name, value, avoidEscape?): Add attributes to the most recently opened element (String joiner) or to the current Element (DOM joiner).
  • text(txt): Emit text content. In String joiner, escapes & and <, and closes an open start tag if needed.
  • plainText(str): Raw, no‑escape append that bypasses context routing in the String joiner (always writes to top‑level buffer). In DOM/JSON joiners, it maps to text()/string() respectively.

string() vs text() vs plainText() (String joiner)

  • text(): Escapes &, < and closes an open start tag. Use for safe text nodes in markup.
  • string(): No HTML escaping or JSON stringify; routes via append() so it participates in object()/array()/propOnly() states. Optional cb to build a composite string before emitting.
  • plainText(): Always writes directly to the top‑level string buffer with no escaping, ignoring object/array state. Useful for deliberate raw insertion.

Configuration quick reference

Provide joiningConfig when constructing JTLT:

  • joiningConfig.mode: 'JavaScript' or 'JSON' controls allowance of undefined/functions/non‑finite numbers in the String joiner.
  • joiningConfig.JHTMLForJSON: If true, object()/array() serialize via JHTML instead of JSON.
  • joiningConfig.xmlElements: Switch element() to XML serialization mode in the String joiner.
  • joiningConfig.preEscapedAttributes: Skip escaping attribute values in the String joiner.

Notes on the basic example

  • Modes let you organize multiple passes or output targets.
  • You can also call templates by name via this.callTemplate('name').
  • For DOM output, use outputType: 'dom'. For JSON output, use 'json' (the default is 'string').

Quick start (XML source with XPath)

You can run templates against XML/HTML using XPath instead of JSONPath.

  • data should be a Document or Element (e.g., from DOMParser with text/xml).
  • xpathVersion: 1 uses native XPath (browser‑like). 2 uses xpath2.js for XPath 2.0‑style evaluation. 3.1 uses fontoxpath for XPath 3.1. Default is 1.
  • In version 2, some functions may be missing; prefer simple path expressions. Use version 1 for standard XPath 1.0 function support.

Example (string output) with jtlt() and the XPath engine:

import {JSDOM} from 'jsdom';
import {jtlt} from 'jtlt';

const {window} = new JSDOM('<!doctype html><html><body></body></html>');
const parser = new window.DOMParser();
const doc = parser.parseFromString(
  '<root><item>a</item><item>b</item></root>', 'text/xml'
);

const templates = [
  {
    path: '/',
    template () {
      this.applyTemplates('//item');
    }
  },
  {
    path: '//item',
    template (n) {
      this.element('li', {}, [], () => this.text(n.textContent));
    }
  }
];

const out = await jtlt({
  data: doc,
  templates,
  outputType: 'string',
  engineType: 'xpath',
  xpathVersion: 1 // or 2, or 3.1
});
// -> <li>a</li><li>b</li>

One-off queries with forQuery (XQuery-like)

If you just want to run a single, non-recursive query (similar to an XQuery "for … where … return …"), you can skip defining templates and use forQuery to seed a root function that iterates a JSONPath and emits results.

  • forQuery takes the same arguments you’d pass to this.forEach(select, cb): an absolute JSONPath selector and a callback invoked for each match.
  • The callback runs once per match with this bound to that match, so use plain JavaScript if for conditions.

Example: collect item names whose price is at least 10.

import {jtlt} from 'jtlt';

const data = {
  items: [
    {name: 'A', price: 8},
    {name: 'B', price: 12},
    {name: 'C', price: 10}
  ]
};

const result = await jtlt({
  data,
  outputType: 'json', // Top-level result will be a JSON array
  // forQuery mirrors: this.forEach(select, cb)
  forQuery: [
    '$.items[*]',
    function (item) {
      // Use normal JS conditionals (no this.if helper)
      if (item.price >= 10) {
        // In JSON output mode, appending a string pushes into
        //   the top-level array
        this.string(item.name);
      }
    }
  ]
});
// result => ['B', 'C']

Tips:

  • For string output, set outputType: 'string' and emit with this.text()/this.string() in the callback.
  • forQuery's callback context is the matched item, not the root — to use a value from the root (e.g. a threshold), use a root template instead: this.variable('threshold', '$.threshold') then this.forEach('$.items[*]', cb) (see the FLWOR example below).
  • If you need multiple passes or richer logic, switch to named templates and modes.

FLWOR-style (XQuery) example

You can express the essentials of a FLWOR expression (For, Let, Where, Order by, Return) using a template with forEach() and the new sort support:

Scenario: list book titles whose price is at/above a threshold, ordered by price descending and then title ascending.

import {jtlt} from 'jtlt';

const data = {
  threshold: 10,
  store: {
    book: [
      {title: 'A Tale', price: 8},
      {title: 'Brave New', price: 12},
      {title: 'Cobalt', price: 12},
      {title: 'Delta', price: 10}
    ]
  }
};

const templates = [
  // Root template builds an HTML list
  {path: '$', mode: 'html', template () {
    // Let: bind a reusable variable from root
    this.variable('threshold', '$.threshold');

    this.element('ul', {}, [], () => {
      // For + Order by: iterate books with multi-key sort
      this.forEach('$.store.book[*]', function (b) {
        // Where: filter in JS
        if (b.price >= this.vars.threshold) {
          // Return: emit a list item for each match
          this.element('li', {}, [], () => this.text(b.title));
        }
      }, [
        {select: '$.price', type: 'number', order: 'descending'},
        {select: '$.title', type: 'text', order: 'ascending'}
      ]);
    });
  }}
];

const out = await jtlt({data, templates, outputType: 'string', mode: 'html'});

// -> <ul><li>Brave New</li><li>Cobalt</li><li>Delta</li></ul>
console.log(out);

Notes:

  • You can also drive a FLWOR-like flow with applyTemplates({select, mode}, sort) and a dedicated template mode instead of using an inline forEach() callback.
  • The sort parameter accepts:
    • a JSONPath string relative to each item (e.g., $.name or .)
    • a comparator function (aValue, bValue, ctx) => number
    • an object {select, order, type, locale, localeOptions}
    • an array of such strings/objects for multi-key sorting

FLWOR-style join (two forEach loops)

You can model a join across two arrays (e.g., orders ↔ customers) using two forEach() passes: the first builds a lookup (an index), the second consumes it to emit joined rows. This mirrors a FLWOR-style join while keeping intent explicit and fast.

Example: render an HTML list of orders annotated with customer names.

import {jtlt} from 'jtlt';

const data = {
  customers: [
    {id: 1, name: 'Alice'},
    {id: 2, name: 'Bob'}
  ],
  orders: [
    {id: 'o-10', customerId: 2, item: 'Keyboard', date: '2024-10-01'},
    {id: 'o-11', customerId: 1, item: 'Mouse', date: '2024-09-20'}
  ]
};

const templates = [
  {path: '$', mode: 'html', template () {
    // 1) Build an index by id (first forEach)
    const byId = {};
    this.forEach('$.customers[*]', function (c) {
      byId[c.id] = c;
    });

    // 2) Emit joined rows (second forEach)
    this.element('ul', {}, [], () => {
      this.forEach('$.orders[*]', function (o) {
        const c = byId[o.customerId];
        if (!c) {
          return; // skip if no matching customer
        }
        this.element('li', {}, [], () => {
          this.text(`${c.name} — ${o.item}`);
        });
      }, {select: '$.date', type: 'text', order: 'ascending'}); // optional sort
    });
  }}
];

const out = await jtlt({
  data, templates, outputType: 'string', mode: 'html'
});
// sorted by date ascending:
// -> <ul><li>Alice — Mouse</li><li>Bob — Keyboard</li></ul>
console.log(out);

Notes:

  • This pattern uses two forEach() calls rather than nesting them, which avoids repeatedly scanning the second array for each outer item.
  • If you already maintain keys in your data, you can skip the first pass and derive byId with Object.fromEntries or similar.
  • For locale-aware or numeric ordering of the second pass, use the sort parameter (string/comparator/object/array as shown above).

Joins with key()/getKey() (xsl:key-like)

Define an index once, then perform O(1) lookups from another sequence when rendering. If no match is found, getKey() returns the current context (this) as a sentinel; check for that to skip safely.

import {jtlt} from 'jtlt';

const data = {
  customers: [
    {id: 1, name: 'Alice'},
    {id: 2, name: 'Bob'}
  ],
  orders: [
    {id: 'o-10', customerId: 2, item: 'Keyboard'},
    {id: 'o-11', customerId: 3, item: 'Cable'} // no matching customer
  ]
};

const templates = [
  {path: '$', mode: 'html', template () {
    // Define an index by id: key(name, match, use)
    this.key('customerById', '$.customers[*]', 'id');

    this.element('ul', {}, [], () => {
      this.forEach('$.orders[*]', function (o) {
        const c = this.getKey('customerById', o.customerId);
        // getKey returns `this` if no match; skip such rows
        if (c === this) {
          return;
        }
        this.element('li', {}, [], () => this.text(`${c.name}: ${o.item}`));
      }, {select: '$.id', order: 'ascending'});
    });
  }}
];

const out = await jtlt({
  data, templates, outputType: 'string', mode: 'html'
});
// -> <ul><li>Bob: Keyboard</li></ul>
console.log(out);

Tips:

  • You can define multiple keys with different use properties (e.g., lookup by email, id, etc.).
  • The match expression can target nested arrays (e.g., $.stores[*].customers[*]).
  • For JSON output joins, switch outputType: 'json' and use object()/array() to build structured results.

How this compares to XSLT: pros and cons

Advantages (strong parallels with XSLT):

  • Template matching by path and mode: templates use JSONPath selectors and optional mode, with priority resolution and an option to error on equal priority.
  • Built‑in default rules: when no template matches, defaults traverse and render objects, arrays, scalars, property names, and functions, similar to XSLT’s built‑in templates.
  • applyTemplates/forEach and sorting: applyTemplates(select, mode, sort) and forEach(select, cb, sort) mirror xsl:apply-templates/xsl:for-each and xsl:sort.
  • Named templates and parameters: callTemplate(name, withParam) reflects xsl:call-template + xsl:with-param. this.param(name, default) mirrors xsl:param — the declared default is used unless the caller supplied a value (via this.withParam(name, value) before the call, or callTemplate's withParam array) or a runtime value was passed as config.params (like an XSLT processor's stylesheet parameters). this.withParam(name, value) stages a parameter for the next callTemplate()/applyTemplates(), which consumes and clears the staged set. config.params values are also readable as $name from any template. In each of param()/withParam(), the value argument is a selector expression string (or {select}), or a literal {value}.
  • Conditionals: this.if(test, cb), this.choose(test, whenCb, otherwiseCb), and this.assert(test, message) mirror xsl:if, xsl:choose, and xsl:assert. test is a JSONPath/XPath expression (a non-empty result set is truthy), a bare $name parameter reference, or a simple non-eval comparison such as '$name === "x"' or '$count < 50' — a $name parameter (or, for the JSONPath engine, a plain dotted/indexed $... path) on the left, one of === / !== / == / != / < / <= / > / >=, and a string, number, boolean, null, or undefined literal on the right.
  • Keys and lookups: key(name, match, use) + getKey(name, value) provide xsl:key-style indexing for joins and fast lookups.
  • Multiple output forms: string, DOM, and JSON builders ("joiners") allow emitting different result trees like XSLT’s result tree model.

Differences / current limitations:

  • Expression language: XPath 2.0 implementation is not fully feature complete and the XPath 1.0 implementation from jsdom may not be either.
  • As the syntax is JavaScript, it is not feasible to recursively transform JTLT syntax with itself (at least not easily) as one can do with XSLT. However, one can use arbitrary JavaScript, or using such as jsep, allow a particuluar subset of JavaScript.
  • Stylesheet composition/precedence: no xsl:import/xsl:include equivalents; only basic priority and modes.
  • Schema awareness: no type-aware processing (a major XSLT/XQuery feature).
  • One output type per transform, though document() / resultDocument() can emit several documents of that type within a run.

Differences between an exact equivalence with XSLT

JTLT deviates somewhat from making an exact equivalence with XSLT (to the extent JTLT and JSONPath implement what could possibly be transferred to JSON-based transformations from XSLT):

  1. The method this.stylesheet() (or this.transform()) is used similarly to XSLT for configuration, but it does not call for including the templates within it as nested content.

  2. JTLT adds path as an alias for match on templates.

  3. No single-root-node restriction. XSLT's content model has, at root, a single node per template (not counting processing instructions) — xsl:output is a separate top-level stylesheet declaration, and xsl:template's own content is one nested tree. JTLT has no equivalent of that single xsl:stylesheet/xsl:transform wrapper to enforce it:

    • A function template is a plain JavaScript function body, so it can make any number of top-level calls in sequence — there is nothing that limits it to producing (or delegating to) one node:
    const templateObj = {path: '$', template () {
      // Two independent things at the root, not one nested tree:
      this.output({method: 'html'}); // configure output
      this.applyTemplates('$.items[*]'); // then produce content
    }};
    • A declarative (jamilih-shaped) template — the Array-of-nodes form compiled by compileJSONTemplate() — is likewise an array of sibling top-level nodes, not one node:
    const templateObj = {
      template: [
        ['h1', ['Custom heading']], // a literal element, then
        [{$applyTemplates: '$.items[*]'}] // an operation node
      ]
      // -> <h1>Custom heading</h1><li>...</li>...
    };

    jamilih's own # fragment node offers a similar grouping when only one node is structurally expected, but the top level here never requires it — an array of nodes is already accepted directly.

    This is also why a template holding just one operation node, with no other siblings, still needs two levels of array, not one:

    // Rejected: a bare `{$applyTemplates}` object is not itself a node —
    // every node (elements included) is array-shaped, `[head, ...args]`.
    const rejected = {template: [{$applyTemplates: '$.items[*]'}]};
    
    // Accepted: the outer array is the sibling-node list (point 3, above);
    // the inner array is this one operation node's own `[head, ...args]`
    // shape — `$applyTemplates` just happens to need no further `args`.
    const accepted = {template: [[{$applyTemplates: '$.items[*]'}]]};

    The inner array isn't there for $applyTemplates specifically — it's the same shape every operation node uses, whether or not that operation happens to need trailing arguments: $if and $forEach both use later array items for their bodies ([{$if}, thenNodes, elseNodes?], [{$forEach}, childNodes]), so the node model treats [head, ...args] as the one uniform shape rather than special-casing the argument-less operations to a bare head object.

    $if's then/else and $forEach's children are, in turn, each a thenNodes/elseNodes/childNodes array of sibling nodes — the exact same shape as template itself (point 3, above) — not a single unwrapped node, even when the body happens to hold just one:

    // Not this — a single node, unwrapped:
    const unwrapped = [[{$if: '$flag'}, ['p', ['yes']]]];
    
    // This — an array of (one) sibling node(s):
    const wrapped = [[{$if: '$flag'}, [['p', ['yes']]]]];

    Requiring the wrapper isn't just consistency for its own sake — an unwrapped single node would be genuinely ambiguous, silently so: an element node is itself an array, [name, attrs?, children?], so ['p', ['yes']] is indistinguishable, at the array level, from a two-node list: a bare string 'p' (a text node) followed by an element node ['yes'] (an empty <yes> tag). That is exactly what unwrapped above produces — p<yes></yes>, not <p>yes</p> — with no error, since both readings are structurally valid nodes on their own; only the wrapped array-of-nodes form is unambiguous.

To-dos

See TO-DO.