Skip to content

crper/placefill

Repository files navigation

placefill

English | 简体中文

Lightweight placeholder filling for strings, objects, and arrays.

placefill is for "fill data into templates" workflows. It is not a full template engine and it does not parse HTML into an AST.

Why placefill

  • Deeply fills strings inside plain objects and arrays
  • Supports path access like user.name, items.0, and matrix.1.2
  • Preserves original value types for pure-placeholder strings like {{count}}
  • Supports controlled synchronous function execution
  • Supports lightweight tag templates for rich text assembly
  • Keeps unknown tags untouched while still processing known child tags and placeholders
  • Zero runtime dependencies

Good Fit

Use placefill when you need predictable, low-complexity template filling in production code:

  • Transactional messages: order notifications, support ticket summaries, campaign copy
  • Object payload generation: email payloads, push payloads, internal config objects
  • CMS or content assembly: article snippets, CTA blocks, localized rich text fragments
  • Compile-once render-many flows: i18n fragments, server-side repeated rendering, template registries

Not A Good Fit

Skip placefill if you need:

  • Conditionals, loops, async expressions, or arbitrary expression evaluation
  • Full HTML parsing or AST transforms
  • A full component or template language

Install

# Before the package is published to npm
npm pack
npm install ./placefill-0.1.0.tgz
# or
pnpm add ./placefill-0.1.0.tgz
# or
bun add ./placefill-0.1.0.tgz

# After the package is published to npm
npm install placefill
# or
pnpm add placefill
# or
bun add placefill

Runtime Targets

placefill is designed for modern browser, Node.js, and edge-runtime environments that can run ES2020 output.

  • No runtime dependencies
  • Node.js >=16.20
  • Works well in frontend apps, server handlers, workers, and CLI scripts
  • Keeps the API synchronous and predictable

Quick Start

import { fill } from "placefill";

const result = fill(
  {
    title: "Hello {{user.name}}",
    firstItem: "{{items.0}}",
  },
  {
    user: { name: "Lin" },
    items: ["A", "B"],
  },
);

console.log(result.value);
// {
//   title: "Hello Lin",
//   firstItem: "A"
// }

Compile Once, Render Many

When the same template is rendered repeatedly, use compile.

import { compile } from "placefill";

const template = compile('<Link href="/orders/{{order.id}}">{{user.name}}</Link>', {
  tags: {
    Link: '<a href="{{attrs.href}}">{{children}}</a>',
  },
});

console.log(template.render({ order: { id: "A-100" }, user: { name: "Lin" } }).value);
console.log(template.render({ order: { id: "A-101" }, user: { name: "Qin" } }).value);
// <a href="/orders/A-100">Lin</a>
// <a href="/orders/A-101">Qin</a>

This is especially useful for:

  • Repeated server-side rendering
  • i18n fragment reuse
  • Template registries loaded at startup
  • Rich text snippets rendered with different value sets

Choosing fill vs compile

Pick the simplest entry point that matches the call pattern:

  • fill(objectOrArray, values): best for one-off payload assembly and deep template traversal
  • fill(string, values): best for ergonomic one-shot string rendering; repeated string calls can reuse the shared compile cache via cacheSize
  • compile(template).render(values): best for hot paths, repeated rendering, tag-heavy rich text, and template registries loaded once then reused many times

If you use function-based tags and still want cache hits for fill(string, ...), provide a stable cacheKey.

Rich Text With Tag Templates

import { fill } from "placefill";

const result = fill(
  'Welcome <highlight>{{user.name}}</highlight>, open <link href="/orders/{{order.id}}">order</link>',
  { user: { name: "Lin" }, order: { id: "A-100" } },
  {
    tags: {
      highlight: "<strong>{{children}}</strong>",
      link: '<a href="{{attrs.href}}">{{children}}</a>',
    },
  },
);

console.log(result.value);
// Welcome <strong>Lin</strong>, open <a href="/orders/A-100">order</a>

Tag behavior:

  • Tag names are case-sensitive
  • Supported names include MyComp, ui.slot, x:label, and my-comp
  • Unknown tags are preserved as-is
  • {{children}} and {{attrs.xxx}} always use double braces inside tag templates
  • If a tag fragment is malformed, the malformed fragment is preserved and other valid placeholders in the same string still continue to render

Return Shape

fill and compile().render() both return structured results:

Field Description
value Rendered output
ok true when there are no errors
issues Collected warnings and errors
stats Runtime stats such as placeholder count, replacements, warnings, errors, and duration

Supported Placeholder Formats

Double braces are the default:

fill("Hello {{name}}", { name: "Lin" });

double_braces keeps backward-compatible matching for direct keys such as {{full name}}, {{user:name}}, and {{用户}}.

Other formats must be enabled explicitly:

fill("Hello {name}", { name: "Lin" }, { format: "single_braces" });
fill("Hello ${name}", { name: "Lin" }, { format: "dollar_braces" });
fill("Hello %name%", { name: "Lin" }, { format: "percent" });

For single_braces, dollar_braces, and percent, placeholders are restricted to path-like keys such as {user.name}, ${order.id}, or %campaign.slug%.

Real-World Examples

The repository includes realistic examples under examples:

Run them locally:

pnpm run examples
pnpm dlx tsx examples/campaign-banner-i18n.ts

Options Overview

Option Type Default Description
format PlaceholderFormat double_braces Placeholder syntax
defaultValue string | number | boolean | null unset Fallback value for missing placeholders
onError collect | throw collect Error handling strategy
executeFunctions boolean false Execute synchronous placeholder functions
tags Record<string, TagTemplate> {} Tag template registry
maxDepth number 100 Maximum object/array traversal depth
maxTagDepth number 100 Maximum tag nesting depth
treatUndefinedAsMissing boolean false Treat undefined as missing
cacheSize number 200 Shared string-template compile cache for fill(string, ...)
cacheKey string unset Explicit cache key when using function-based tags

Notes:

  • values must be a plain object, otherwise a FillError is thrown
  • A pure placeholder string like {{count}} can return a number, boolean, array, object, Date, or function result directly
  • Placeholder functions are synchronous only

Compatibility

  • Ships both ESM and CJS builds
  • Bundled output targets ES2020
  • Includes bundled type declarations
  • Intended for current Node.js and evergreen browsers through modern bundlers
  • No UMD or global browser build is shipped

For Node.js consumers:

  • Package export maps support both import and require
  • prepublishOnly runs check, test, and pack

For browser consumers:

  • Use the ESM build through your bundler or native module pipeline
  • The package is dependency-free, and the published output targets modern runtimes
  • If you need older browser support, handle transpilation and polyfills in your own build pipeline

Benchmarking

Benchmarks live in tests/bench/fill.bench.ts and are organized by user call pattern instead of internal function names. They use vitest bench with warmup, multi-sample runs, rotating datasets, and scenario inputs derived from the repository examples.

Included benchmark layers:

  • layer: micro: low-noise regression checks for short strings, fallback strings, tag-rich text, and object payloads
  • layer: realistic one-shot: low-reuse scenarios where each iteration includes compile cost, such as single-brace notifications, order-confirmation payloads, and invoice reminders
  • layer: realistic repeat: fixed-template repeated rendering scenarios such as support-ticket payloads, CMS article fragments, and campaign banners

Run benchmarks locally:

pnpm run bench

How to read the results:

  • One-shot groups compare fill(...) with compile(...).render(...) while counting compile cost on every iteration
  • Repeated string groups compare fill(cache off), fill(shared cache), and compile.render(precompiled)
  • Repeated object groups compare fill(...) with compile.render(...) because the shared string cache does not apply to object templates

Development

pnpm run check
pnpm test
pnpm build
pnpm run bench
pnpm run examples

License

MIT

About

Lightweight placeholder filling for strings, objects, and arrays.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors