Initial Commit
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
# InterpolationEngine
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`InterpolationEngine` replaces placeholder tokens (default: `{{ name }}`) in message template strings with caller-supplied values. It is the rendering workhorse behind `MessageCatalog.t()` and standalone template usage. The engine is designed to be instantiated once and reused across many render calls, with all configuration captured in the constructor.
|
||||
|
||||
The engine handles the common i18n case of named variable substitution, and additionally supports pluggable per-slot value formatters (e.g., number, date, currency) via a named formatter registry. Values can be HTML-escaped on output to prevent XSS when templates are rendered into HTML.
|
||||
|
||||
## Features
|
||||
|
||||
- Named placeholder substitution: `{{ name }}`, `{{ count, number }}`, etc.
|
||||
- Custom delimiter pairs (e.g., `%{` / `}` for Ruby-style templates)
|
||||
- Per-slot `ValueFormatter` registry — applied when a placeholder contains a format name
|
||||
- Optional `defaultFormatter` called for all slots without an explicit formatter
|
||||
- HTML escaping of rendered values (`escapeHtml: true`) via `escapeHtmlChars`
|
||||
- Recursive interpolation with configurable depth limit (`maxDepth`)
|
||||
- `missingVariables` tracking in the result — non-throwing for production resilience
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { InterpolationEngine } from "@suaveplan/i18n-icu-parser/interpolation";
|
||||
|
||||
const engine = new InterpolationEngine();
|
||||
const { text } = engine.render(
|
||||
"Hello, {{ name }}! You have {{ count }} messages.",
|
||||
{ name: "Alice", count: 3 }
|
||||
);
|
||||
// text === "Hello, Alice! You have 3 messages."
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { InterpolationEngine } from "@suaveplan/i18n-icu-parser/interpolation";
|
||||
|
||||
const engine = new InterpolationEngine({
|
||||
escapeHtml: true,
|
||||
formatters: {
|
||||
upper: (v) => String(v).toUpperCase(),
|
||||
number: (v, locale) =>
|
||||
new Intl.NumberFormat(locale).format(Number(v)),
|
||||
},
|
||||
});
|
||||
|
||||
const { text, missingVariables } = engine.render(
|
||||
"Welcome, {{ user, upper }}! Balance: {{ amount, number }}.",
|
||||
{ user: "alice", amount: 1234567 },
|
||||
"en-US"
|
||||
);
|
||||
// text === "Welcome, ALICE! Balance: 1,234,567."
|
||||
// missingVariables === []
|
||||
|
||||
// HTML escaping
|
||||
const { text: safe } = engine.render(
|
||||
"Comment: {{ comment }}",
|
||||
{ comment: "<script>alert('xss')</script>" }
|
||||
);
|
||||
// safe === "Comment: <script>alert('xss')</script>"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new InterpolationEngine(options?: InterpolationOptions)`
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `prefix` | `string` | `"{{"` | Placeholder open delimiter |
|
||||
| `suffix` | `string` | `"}}"` | Placeholder close delimiter |
|
||||
| `escapeHtml` | `boolean` | `false` | HTML-escape rendered values |
|
||||
| `maxDepth` | `number` | `10` | Maximum recursion depth |
|
||||
| `formatters` | `Record<string, ValueFormatter>?` | `{}` | Named formatter map |
|
||||
| `defaultFormatter` | `ValueFormatter?` | — | Applied when no named formatter matches |
|
||||
|
||||
### `.render(template, values?, locale?): InterpolationResult`
|
||||
|
||||
Renders `template` replacing all placeholders. Returns `{ text: string; missingVariables: string[] }`.
|
||||
|
||||
### `.addFormatter(name, formatter): void`
|
||||
|
||||
Registers or replaces a named formatter at runtime.
|
||||
|
||||
### `.removeFormatter(name): boolean`
|
||||
|
||||
Removes a named formatter; returns `true` if it existed.
|
||||
|
||||
### `.hasPlaceholders(template): boolean`
|
||||
|
||||
Returns `true` if the template contains at least one placeholder.
|
||||
|
||||
### `.extractVariables(template): string[]`
|
||||
|
||||
Returns all unique variable names referenced in the template.
|
||||
|
||||
### `escapeHtmlChars(str: string): string`
|
||||
|
||||
Standalone export. Escapes `&`, `<`, `>`, `"`, `'`.
|
||||
|
||||
### `escapeRegex(str: string): string`
|
||||
|
||||
Standalone export. Escapes all regex metacharacters for safe use in `RegExp`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The placeholder regex is rebuilt on each call to `replaceValues` rather than cached, because `options.prefix` / `suffix` could theoretically contain characters whose escaped forms would produce a different pattern — rebuilding is correct and the cost is negligible for the expected template sizes
|
||||
- Recursive interpolation is capped at `maxDepth` (default 10) to prevent infinite loops when a value itself contains placeholder syntax
|
||||
- HTML escaping is applied _after_ formatting so that formatter output (e.g., `"$1,234"`) is escaped as a unit, not before
|
||||
- `missingVariables` accumulates in-place via the closure; the engine never throws on a missing variable — it leaves the placeholder in place and records the name
|
||||
Reference in New Issue
Block a user