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

@bonniernews/json-templates

v0.2.0

Published

Completes incomplete JSON string with caching capabilites

Downloads

1,662

Readme

@bonniernews/json-templates

A template engine for strings, objects, and arrays. Placeholders use {{ }} and support path access, optional chaining, nullish defaults, ternary conditions, logical operators, arithmetic, array methods with arrow functions, template string interpolation, and strict mode.

Installation

npm install @bonniernews/json-templates

API

import { render, validate, MissingVariableError, ArithmeticError, ParseError, TypeError, JsonTemplateError } from "@bonniernews/json-templates";

render(template, data, options?)
validate(template, data?)

| Argument | Type | Description | | ---------- | --------------------------- | ----------------------------- | | template | string \| object \| array | The template to render | | data | Record<string, any> | The values to substitute | | options | RenderOptions | Optional settings (see below) |

Options

| Option | Type | Default | Description | | ----------------- | --------- | ------- | ------------------------------------------------------------------------------ | | strict | boolean | false | Throw on missing variables and invalid arithmetic | | preserveMissing | boolean | false | Keep {{expr}} verbatim in output when the expression resolves to undefined |

strict

render("Hello {{name}}", {}, { strict: true });
// → throws MissingVariableError: Missing variable: "name"

render("{{name ?? 'Guest'}}", {}, { strict: true });
// → "Guest"  — default covers the missing variable, no throw

render("{{'hello' * 2}}", {}, { strict: true });
// → throws ArithmeticError: Arithmetic operation produced NaN — operands must be numeric

render("{{1 / 0}}", {}, { strict: true });
// → "Infinity"  — division by zero is allowed

preserveMissing

When true, any placeholder that resolves to undefined is left unchanged in the output — the original {{expr}} string is preserved verbatim — instead of being replaced with "" or undefined.

Tokens that resolve successfully are rendered as normal. Tokens with a fallback (: default or ??) use their fallback as normal.

render("hello {{name}}", {}, { preserveMissing: true });
// → "hello {{name}}"

render({ city: "{{order.city}}" }, {}, { preserveMissing: true });
// → { city: "{{order.city}}" }

// Mixed: found tokens render, missing tokens are preserved
render("{{first}} {{last}}", { first: "John" }, { preserveMissing: true });
// → "John {{last}}"

// Fallbacks still apply — not treated as missing
render("{{name ?? 'Guest'}}", {}, { preserveMissing: true });
// → "Guest"

MissingVariableError extends JsonTemplateError and has a path property with the variable path (e.g. "user.name", "list[0]"). ArithmeticError extends JsonTemplateError and is thrown in strict mode when an arithmetic operation produces NaN (i.e. non-numeric operands). ParseError extends JsonTemplateError and is thrown when a placeholder has invalid syntax. TypeError extends JsonTemplateError and is thrown in strict mode when array methods are called on non-array values. All error classes are exported from the package.

Return type

Return type matches the template type:

  • string template → always returns string (objects are JSON-stringified)
  • object / array template → returns the same shape with values rendered; a value that is exactly one {{expr}} returns the raw evaluated value (preserving non-string types)

Example

A JSON template using all preferred syntaxes:

{
  "to": "{{user.email}}",
  "subject": "{{user.active ? `Welcome back, ${user.firstName}!` : 'Your account is inactive'}}",
  "role": "{{user.role || 'viewer'}}",
  "isAdmin": "{{user.role && user.active}}",
  "optedOut": "{{!user.notifications}}",
  "firstTag": "{{user.tags[0]}}",
  "contact": "{{user.contact}}",
  "contactJson": "Contact info: {{user.contact}}",
  "contactPhone": "{{user?.contact?.phone ?? 'not provided'}}",
  "Display Name": "{{user['Display Name'] ?? user.firstName ?? 'there'}}",
  "Preferred Publication": "{{['Preferred Publication'] ?? 'none'}}"
}

With data:

{
  "user": {
    "email": "[email protected]",
    "firstName": "Alice",
    "active": true,
    "role": "editor",
    "notifications": false,
    "tags": ["news", "sports"],
    "contact": { "phone": "+1 555 0100" },
    "Display Name": "Alice Wonderland"
  },
  "Preferred Publication": "Dagens Nyheter"
}

Result:

{
  "to": "[email protected]",
  "subject": "Welcome back, Alice!",
  "role": "editor",
  "isAdmin": true,
  "optedOut": true,
  "firstTag": "news",
  "contact": { "phone": "+1 555 0100" },
  "contactJson": "Contact info: {\"phone\":\"+1 555 0100\"}",
  "contactPhone": "+1 555 0100",
  "Display Name": "Alice Wonderland",
  "Preferred Publication": "Dagens Nyheter"
}

isAdmin, optedOut, and contact pass through as their raw types (boolean / boolean / object) because they are sole placeholders in an object template.


Placeholder syntax

Every placeholder is wrapped in {{ }}.

{{ expression }}

Expressions

1. Variable path

{{name}}
{{user.address.city}}

2. Array indexing — [n] (preferred)

{{list[0]}}
{{users[0].tags[1]}}
{{matrix[0][1].value}}

Legacy dot-index syntax is also supported and can be mixed:

{{users.0.name}}
{{users[0].tags.1}}

3. Bracket notation — ['key']

Required for keys containing spaces or other special characters.

{{['Sundsvalls Tidning']}}
{{['Sundsvalls Tidning'].v1}}
{{['Sundsvalls Tidning']['Nested Property']}}
{{items[0]['my key']}}

Simple identifiers still use dot notation: {{user.name}}.

4. Optional chaining — ?.

Returns undefined silently if any part of the chain is null or undefined — even in strict mode.

{{user?.name}}              ← undefined if user is missing or null
{{user?.address?.city}}     ← safe deep access
{{user?.['my key']}}        ← optional bracket notation
{{list?.[0]}}               ← optional index access

Combine with ?? to provide a fallback:

{{user?.name ?? 'Guest'}}

5. Nullish coalescing — expr ?? fallback

Returns the fallback when the value is null, undefined, or missing. false, 0, and "" are not replaced.

Works at any depth — no parentheses needed inside a ternary (unlike : default).

{{name ?? 'Guest'}}
{{user.name ?? 'Anonymous'}}
{{a ? b ?? 'fallback' : 'other'}}   ← works directly inside a ternary branch

| Value of name | Result | | --------------- | --------- | | "Alice" | "Alice" | | null | "Guest" | | undefined | "Guest" | | (missing) | "Guest" | | "" | "" | | false | "false" | | 0 | "0" |

6. Default value — expr:fallback (legacy nullish — top-level only)

Same nullish semantics as ?? but only works at the top level of an expression. To use inside a ternary, wrap in parentheses or use ?? instead.

{{name:'Guest'}}
{{url:'http://example.com'}}        ← colon inside quoted default is safe
{{a ? (b:'Guest') : 'other'}}       ← parens required inside ternary

7. Ternary — condition ? truthy : falsy

false, 0, "", null, and undefined are all falsy.

{{isActive ? 'Active' : 'Inactive'}}
{{user.name ? user.name : 'Guest'}}
{{flag ? a : b}}

Ternaries are right-associative — parentheses are optional but accepted for readability:

{{a ? b ? 'both' : 'a only' : 'neither'}}
{{a ? (b ? 'both' : 'a only') : 'neither'}}   ← identical result

8. Logical operators — &&, ||, !

JavaScript-style short-circuit semantics: && and || return the actual value, not a boolean. ! always returns a boolean.

| Operator | Behaviour | | ---------- | ----------------------------------------- | | a && b | Returns a if falsy, otherwise b | | a \|\| b | Returns a if truthy, otherwise b | | !a | true if a is falsy, false if truthy |

Precedence (highest → lowest): -x ! > * / % > + - > < > <= >= > == === != !== > && > || > ?? > ternary ? : > default :

{{a && b}}
{{a || b}}
{{!isActive}}
{{a || b ? 'yes' : 'no'}}
{{!isActive ? 'off' : 'on'}}
{{a || b && c}}                 ← parsed as a || (b && c)

9. Comparison — <, >, <=, >=

{{count > 0 ? 'items' : 'empty'}}
{{score >= 90 ? 'A' : 'B'}}
{{a + b > threshold}}

10. Equality — ==, ===, !=, !==

Standard JavaScript equality and inequality. ==/!= coerce types; ===/!== do not.

{{status == 'active' ? 'yes' : 'no'}}
{{role === 'admin' ? 'yes' : 'no'}}
{{role !== 'admin' ? 'restricted' : 'allowed'}}
{{a + b === total}}

| Operator | Behaviour | | --------- | ------------------------------------ | | a == b | Loose equality — coerces types | | a === b | Strict equality — no type coercion | | a != b | Loose inequality — coerces types | | a !== b | Strict inequality — no type coercion |

11. Arithmetic — +, -, *, /, %

Standard arithmetic with JavaScript semantics. + also concatenates strings. - can also be used as unary negation.

{{a + b}}
{{price * quantity}}
{{total - discount}}
{{score / count}}
{{index % 2 === 0 ? 'even' : 'odd'}}
{{-price}}
{{(a + b) * c}}

Precedence (highest → lowest within arithmetic): * / % > + -

Operators are left-associative: 8 / 2 / 2 == 2.

In strict mode, an operation that produces NaN (e.g. 'hello' * 2) throws ArithmeticError. Division by zero producing Infinity is allowed in both modes.

12. Array methods with arrow functions

Transform and query arrays using JavaScript-style array methods with arrow function syntax.

Arrow function syntax

x => expression                    ← single parameter
(a, b) => expression              ← multiple parameters
x => ({key: value})               ← object literal shorthand
x => ({[x.id]: x.name})           ← computed/dynamic keys

Arrow functions support expression bodies only (no {} block syntax). The expression result is the return value.

Supported array methods

| Method | Description | Returns | | ----------------------- | --------------------------------------- | ---------------------- | | .map(fn) | Transform each element | New array | | .filter(fn) | Keep elements matching predicate | New array | | .find(fn) | Find first element matching predicate | Element or undefined | | .some(fn) | Check if any element matches predicate | Boolean | | .reduce(fn, initial?) | Reduce array to single value | Any value | | .sort(fn?) | Sort array (with optional comparator) | New array | | .join(separator?) | Join elements into string | String | | .reverse() | Reverse array order | New array | | .length | Get array length (property, not method) | Number |

Important: All methods return new arrays and do not mutate the originals.

Examples

// Transform with .map()
render("{{items.map(x => x.name)}}", { items: [{ name: "A" }, { name: "B" }] });
// → '["A","B"]'

// Object literal shorthand
render("{{users.map(u => ({id: u.id, name: u.name}))}}", {
  users: [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
  ],
});
// → [{id: 1, name: "Alice"}, {id: 2, name: "Bob"}]

// Computed keys (dynamic property names)
render("{{users.map(u => ({[u.id]: u.name}))}}", {
  users: [
    { id: "user1", name: "Alice" },
    { id: "user2", name: "Bob" },
  ],
});
// → [{user1: "Alice"}, {user2: "Bob"}]

// Filter with predicate
render("{{items.filter(x => x.active)}}", {
  items: [
    { active: true, name: "A" },
    { active: false, name: "B" },
  ],
});
// → [{active: true, name: "A"}]

// Filter with comparison
render("{{scores.filter(s => s > 50)}}", { scores: [45, 78, 92, 33] });
// → [78, 92]

// Find first match
render("{{users.find(u => u.id === 2)}}", {
  users: [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
  ],
});
// → {id: 2, name: "Bob"}

// Check if any match
render("{{items.some(x => x.price > 100)}}", { items: [{ price: 50 }, { price: 150 }] });
// → "true"

// Reduce to sum
render("{{numbers.reduce((sum, x) => sum + x, 0)}}", { numbers: [1, 2, 3, 4] });
// → "10"

// Reduce with index parameter
render("{{items.reduce((acc, item, i) => acc + item * i, 0)}}", { items: [5, 10, 15] });
// → "40"

// Sort with comparator (numeric sort)
render("{{numbers.sort((a, b) => a - b)}}", { numbers: [10, 2, 33, 4] });
// → [2, 4, 10, 33]

// Sort objects by property
render("{{users.sort((a, b) => a.age - b.age)}}", { users: [{ age: 30 }, { age: 20 }, { age: 25 }] });
// → [{age: 20}, {age: 25}, {age: 30}]

// Join with separator
render("{{tags.join(', ')}}", { tags: ["js", "ts", "node"] });
// → "js, ts, node"

// Reverse array
render("{{numbers.reverse()}}", { numbers: [1, 2, 3] });
// → [3, 2, 1]

// Array length property
render("{{items.length}}", { items: [1, 2, 3, 4, 5] });
// → "5"

Method chaining

All array methods can be chained together:

render("{{users.filter(u => u.active).map(u => u.name).join(', ')}}", {
  users: [
    { active: true, name: "Alice" },
    { active: false, name: "Bob" },
    { active: true, name: "Charlie" },
  ],
});
// → "Alice, Charlie"

render("{{numbers.filter(n => n > 2).map(n => n * 2).reduce((a, b) => a + b, 0)}}", { numbers: [1, 2, 3, 4, 5, 6] });
// → "36"

Error handling

Non-strict mode (default): Array methods gracefully handle non-array values:

| Method | Returns for non-array | Example | | ------------------- | --------------------- | ------------------------------------------------ | | .map() | [] | "{{null.map(x => x)}}""[]" | | .filter() | [] | "{{42.filter(x => x)}}""[]" | | .find() | undefined | {v: "{{obj.find(x => x)}}"}{v: undefined} | | .some() | false | "{{'text'.some(x => x)}}""false" | | .reduce(fn, init) | init value | "{{null.reduce((a,b) => a+b, 10)}}""10" | | .join() | "" | "{{123.join(',')}}""" | | .reverse() | [] | "{{false.reverse()}}""[]" | | .sort() | [] | "{{'str'.sort()}}""[]" |

Strict mode: Throws TypeError with path information:

render("{{value.map(x => x)}}", { value: null }, { strict: true });
// → throws TypeError: Cannot call .map() on non-array value at path: value.map(...)

render("{{user.items.filter(x => x)}}", { user: { items: "not array" } }, { strict: true });
// → throws TypeError: Cannot call .filter() on non-array value at path: user.items.filter(...)

Property vs method disambiguation

Objects can have properties named like array methods. Properties are accessed without parentheses; methods require parentheses:

// Property access (no parentheses)
render("{{obj.map}}", { obj: { map: "property value" } });
// → "property value"

// Method call (with parentheses)
render("{{items.map(x => x * 2)}}", { items: [1, 2, 3] });
// → [2, 4, 6]

13. Template string interpolation — `text ${expr} text`

Use backtick strings with ${} to embed expressions inline.

{{`Hello ${name}!`}}
{{`${first} ${last}`}}
{{`City: ${user.address.city}`}}
{{`Status: ${isActive ? statusOn : statusOff}`}}

Works inside ternary branches:

{{isActive ? `Hello ${name}!` : `Goodbye ${name}`}}
{{a ? `Both: ${name}` : `Neither`}}

Object and array templates

The template itself can be an object or array. Each string value is rendered independently.

render({ message: "Hello {{name}}", id: "{{id}}" }, { name: "Alice", id: 42 });
// → { message: "Hello Alice", id: 42 }

When a string value in an object or array template is exactly one placeholder and the resolved value is not a string, the raw value is passed through:

render({ data: "{{payload}}" }, { payload: { id: 1 } });
// → { data: { id: 1 } }

In a top-level string template, objects are always JSON-stringified:

render("{{user}}", { user: { name: "Alice" } });
// → '{"name":"Alice"}'

Validation

validate(template, data?) checks a template against data using the same evaluation logic as render. It returns { isValid: boolean, missing: string[], errors: string[] } and never throws.

validate("Hello {{name}}", { name: "Alice" });
// → { isValid: true, missing: [], errors: [] }

validate("Hello {{name}}", {});
// → { isValid: false, missing: ["name"], errors: [] }

validate({ greeting: "Hi {{name}}", city: "{{location.city}}" }, {});
// → { isValid: false, missing: ["name", "location.city"], errors: [] }

// Syntax errors are reported in errors[] instead of throwing
validate("{{(name}}");
// → { isValid: false, missing: [], errors: ['Syntax error in "{{(name}}": Expected RPAREN but got EOF'] }

Because it uses the same logic as render, covered variables are not reported as missing:

validate("{{name ?? 'Guest'}}", {}); // → { isValid: true, missing: [], errors: [] }
validate("{{user?.name}}", {}); // → { isValid: true, missing: [], errors: [] }
validate("{{name : 'Guest'}}", {}); // → { isValid: true, missing: [], errors: [] }

Ternary branches are only checked for the branch actually taken with the given data — consistent with how render evaluates them:

validate("{{flag ? name : fallback}}", { flag: true });
// → { isValid: false, missing: ["name"], errors: [] }  — fallback is not checked

validate("{{flag ? name : fallback}}", { flag: false });
// → { isValid: false, missing: ["fallback"], errors: [] }  — name is not checked

Missing variables

| Context | Default result | With preserveMissing: true | | ----------------------------------------- | -------------- | ---------------------------- | | In a string template | "" | "{{expr}}" verbatim | | As sole placeholder in an object template | undefined | "{{expr}}" verbatim | | In a ternary / logical condition | falsy | falsy | | In string interpolation | "" | "{{expr}}" verbatim |


Escape sequences

In the template string

\{{expr}} prevents rendering and emits {{expr}} literally.

"Hello \{{name}}"  →  "Hello {{name}}"

Inside a string literal "..."

| Write | Produces | Notes | | ----- | -------- | -------------------- | | \" | " | Literal double-quote | | \\ | \ | Literal backslash |

Unclosed placeholder

A {{ with no matching }} is left as-is:

"Hello {{name"   →  "Hello {{name"

Complete syntax reference

template    = string | object | array  — any value containing {{ expr }} blocks

expr        = nullish-expr ( ? expr : expr )?  — ternary (right-associative)
            | nullish-expr : expr              — nullish default (top-level only)
nullish-expr= or-expr ( ?? or-expr )*
or-expr     = and-expr ( || and-expr )*
and-expr    = eq-expr ( && eq-expr )*
eq-expr     = cmp-expr ( (==|===|!=|!==) cmp-expr )*
cmp-expr    = add-expr ( (<|>|<=|>=) add-expr )*
add-expr    = mul-expr ( (+|-) mul-expr )*
mul-expr    = unary ( (*|/|%) unary )*
unary       = (-|!) unary | primary
primary     = path
            | arrow-function
            | object-literal
            | 'string literal'           — single quotes preferred (no JSON escaping)
            | `template literal`
            | number (integer or float) | true | false | null
            | ( expr )                         — grouping (resets default depth)

path        = segment ( accessor | opt-accessor )*
accessor    = .segment | [number] | ["string"] | .method-call
opt-accessor= ?.segment | ?.[number] | ?.["string"] | ?.method-call
segment     = identifier | number
method-call = identifier ( arg-list )
arg-list    = ( expr (, expr)* )?

arrow-fn    = identifier => expr                      — single parameter
            | ( identifier (, identifier)* ) => expr  — multiple parameters
obj-literal = ( { obj-props } )                       — parenthesized for arrow return
obj-props   = prop (, prop)*
prop        = identifier : expr                       — static key
            | [ expr ] : expr                         — computed key

template lit= ` chars `
chars       = literal text
            | ${ expr }                        — interpolation
            | \`                               — escaped backtick
            | \\                               — escaped backslash

Quick-reference examples

// Variable access
render("Hello {{name}}", { name: "Alice" }); // → "Hello Alice"
render("{{user.address.city}}", { user: { address: { city: "X" } } }); // → "X"
render("{{list[0]}}", { list: ["a", "b"] }); // → "a"

// Keys with spaces
render("{{['My Key']}}", { "My Key": "value" }); // → "value"

// Optional chaining
render({ v: "{{user?.name}}" }, {}); // → { v: undefined }
render("{{user?.name ?? 'Guest'}}", {}); // → "Guest"

// Nullish coalescing
render("{{name ?? 'Guest'}}", {}); // → "Guest"
render("{{name ?? 'Guest'}}", { name: "" }); // → ""

// Legacy default (top-level only)
render("{{name:'Guest'}}", {}); // → "Guest"

// Ternary
render("{{on ? 'yes' : 'no'}}", { on: true }); // → "yes"
render("{{a ? b ?? 'fallback' : 'other'}}", { a: true }); // → "fallback"

// Logical operators
render("{{a && b ? 'both' : 'no'}}", { a: true, b: true }); // → "both"
render("{{name || 'Anonymous'}}", {}); // → "Anonymous"
render("{{!isActive ? 'off' : 'on'}}", { isActive: false }); // → "off"

// Template string interpolation
render("{{`Hello ${name}!`}}", { name: "Alice" }); // → "Hello Alice!"
render("{{isActive ? `Hi ${name}` : `Bye`}}", { isActive: true, name: "Bob" }); // → "Hi Bob"

// Object template — raw value pass-through
render({ data: "{{payload}}" }, { payload: { id: 1 } }); // → { data: { id: 1 } }

// Arithmetic
render("{{a + b}}", { a: 3, b: 4 }); // → "7"
render("{{a + b * c}}", { a: 1, b: 2, c: 3 }); // → "7"  (* before +)
render("{{(a + b) * c}}", { a: 1, b: 2, c: 3 }); // → "9"
render("{{1 / 0}}", {}); // → "Infinity"

// Strict mode
render("{{name}}", {}, { strict: true }); // → throws MissingVariableError
render("{{name ?? 'Guest'}}", {}, { strict: true }); // → "Guest"
render("{{'hello' * 2}}", {}, { strict: true }); // → throws ArithmeticError

// Escape
render("Value: \\{{x}}", { x: "ignored" }); // → "Value: {{x}}"

// Validation
validate("{{name}}", { name: "Alice" }); // → { isValid: true, missing: [], errors: [] }
validate("{{name}}", {}); // → { isValid: false, missing: ["name"], errors: [] }
validate("{{name ?? 'Guest'}}", {}); // → { isValid: true, missing: [], errors: [] }

// preserveMissing — keep unresolved tokens verbatim
render("hello {{name}}", {}, { preserveMissing: true }); // → "hello {{name}}"
render({ city: "{{order.city}}" }, {}, { preserveMissing: true }); // → { city: "{{order.city}}" }
render("{{first}} {{last}}", { first: "John" }, { preserveMissing: true }); // → "John {{last}}"
render("{{name ?? 'Guest'}}", {}, { preserveMissing: true }); // → "Guest"  (fallback applies)

// Array methods with arrow functions
render("{{items.map(x => x.name)}}", { items: [{ name: "A" }, { name: "B" }] }); // → '["A","B"]'
render("{{numbers.filter(n => n > 5)}}", { numbers: [3, 7, 2, 9] }); // → [7, 9]
render("{{users.find(u => u.id === 2)}}", { users: [{ id: 1 }, { id: 2, name: "Bob" }] }); // → {id: 2, name: "Bob"}
render("{{items.some(x => x > 10)}}", { items: [5, 15, 3] }); // → "true"
render("{{nums.reduce((a, b) => a + b, 0)}}", { nums: [1, 2, 3] }); // → "6"
render("{{numbers.sort((a, b) => a - b)}}", { numbers: [3, 1, 2] }); // → [1, 2, 3]
render("{{tags.join(', ')}}", { tags: ["a", "b", "c"] }); // → "a, b, c"
render("{{items.length}}", { items: [1, 2, 3] }); // → "3"

// Method chaining
render("{{users.filter(u => u.active).map(u => u.name).join(', ')}}", {
  users: [
    { active: true, name: "A" },
    { active: false, name: "B" },
  ],
}); // → "A"

// Object literal with computed keys
render("{{users.map(u => ({[u.id]: u.name}))}}", {
  users: [
    { id: "a", name: "Alice" },
    { id: "b", name: "Bob" },
  ],
}); // → [{a: "Alice"}, {b: "Bob"}]

License

MIT