@orkestrel/template 0.0.10 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/core/index.cjs +41 -46
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +4 -0
- package/dist/src/core/index.d.ts +4 -0
- package/dist/src/core/index.js +42 -47
- package/dist/src/core/index.js.map +1 -1
- package/package.json +15 -15
package/dist/src/core/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let _orkestrel_contract = require("@orkestrel/contract");
|
|
3
3
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
4
|
-
|
|
4
|
+
// src/core/constants.ts
|
|
5
5
|
/**
|
|
6
6
|
* Holds the single-pass `{{name}}` substitution pattern shared by
|
|
7
7
|
* `Template#fill` and `Template#validate`.
|
|
@@ -43,8 +43,7 @@ var UNSAFE_FIELD_SEGMENTS = Object.freeze([
|
|
|
43
43
|
"constructor",
|
|
44
44
|
"prototype"
|
|
45
45
|
]);
|
|
46
|
-
|
|
47
|
-
//#region src/core/errors.ts
|
|
46
|
+
// src/core/errors.ts
|
|
48
47
|
/**
|
|
49
48
|
* Represents an error thrown by the template layer — a machine-readable
|
|
50
49
|
* {@link TemplateErrorCode} and an optional `context` record naming the
|
|
@@ -62,6 +61,7 @@ var TemplateError = class extends Error {
|
|
|
62
61
|
context;
|
|
63
62
|
constructor(code, message, context) {
|
|
64
63
|
super(message);
|
|
64
|
+
_orkestrel_contract.INTRINSICS.define(this, Symbol.for("@orkestrel/template.TemplateError"), { value: this });
|
|
65
65
|
this.name = "TemplateError";
|
|
66
66
|
this.code = code;
|
|
67
67
|
if (context !== void 0) this.context = context;
|
|
@@ -70,6 +70,10 @@ var TemplateError = class extends Error {
|
|
|
70
70
|
/**
|
|
71
71
|
* Narrows an unknown caught value to a {@link TemplateError}.
|
|
72
72
|
*
|
|
73
|
+
* @remarks
|
|
74
|
+
* Recognizes self-branded errors from other package copies and subclasses, and
|
|
75
|
+
* refuses transparent proxies and unbranded look-alikes.
|
|
76
|
+
*
|
|
73
77
|
* @param value - The value to test (typically a `catch` binding)
|
|
74
78
|
* @returns True if `value` is a {@link TemplateError}; false otherwise
|
|
75
79
|
*
|
|
@@ -85,10 +89,15 @@ var TemplateError = class extends Error {
|
|
|
85
89
|
* ```
|
|
86
90
|
*/
|
|
87
91
|
function isTemplateError(value) {
|
|
88
|
-
return (0, _orkestrel_contract.
|
|
92
|
+
return (0, _orkestrel_contract.holds)(() => {
|
|
93
|
+
if (!(value instanceof Error) || _orkestrel_contract.INTRINSICS.prototype(value) === Error.prototype) return false;
|
|
94
|
+
if (_orkestrel_contract.INTRINSICS.describe(value, Symbol.for("@orkestrel/template.TemplateError"))?.value !== value) return false;
|
|
95
|
+
const code = _orkestrel_contract.INTRINSICS.reflect.read(value, "code");
|
|
96
|
+
const context = _orkestrel_contract.INTRINSICS.reflect.read(value, "context");
|
|
97
|
+
return (code === "MISSING" || code === "NOTFOUND" || code === "INVALID" || code === "CONFLICT") && (context === void 0 || (0, _orkestrel_contract.isObject)(context) || (0, _orkestrel_contract.isFunction)(context));
|
|
98
|
+
});
|
|
89
99
|
}
|
|
90
|
-
|
|
91
|
-
//#region src/core/helpers.ts
|
|
100
|
+
// src/core/helpers.ts
|
|
92
101
|
/**
|
|
93
102
|
* Formats a resolved fill value for substitution into a template's `content`.
|
|
94
103
|
*
|
|
@@ -231,7 +240,6 @@ function fillTemplate(content, values, options) {
|
|
|
231
240
|
const locale = options?.locale ?? "en-US";
|
|
232
241
|
const record = values ?? {};
|
|
233
242
|
const missingNames = [];
|
|
234
|
-
const seen = /* @__PURE__ */ new Set();
|
|
235
243
|
const pattern = new RegExp(FILL_PATTERN.source, FILL_PATTERN.flags);
|
|
236
244
|
const result = content.replace(pattern, (matchText, rawToken) => {
|
|
237
245
|
if (rawToken === void 0) return "{{";
|
|
@@ -241,17 +249,13 @@ function fillTemplate(content, values, options) {
|
|
|
241
249
|
if (declared?.fallback !== void 0) return formatValue(declared.fallback, locale);
|
|
242
250
|
if (missing === "literal") return matchText;
|
|
243
251
|
if (missing === "empty") return "";
|
|
244
|
-
if (required && !
|
|
245
|
-
seen.add(token);
|
|
246
|
-
missingNames.push(token);
|
|
247
|
-
}
|
|
252
|
+
if (required && !missingNames.includes(token)) missingNames.push(token);
|
|
248
253
|
return "";
|
|
249
254
|
});
|
|
250
255
|
if (missing === "error" && missingNames.length > 0) throw new TemplateError("MISSING", `Missing required placeholder(s): ${missingNames.join(", ")}`, { missing: missingNames });
|
|
251
256
|
return result;
|
|
252
257
|
}
|
|
253
|
-
|
|
254
|
-
//#region src/core/shapers.ts
|
|
258
|
+
// src/core/shapers.ts
|
|
255
259
|
/**
|
|
256
260
|
* Builds the `@orkestrel/contract` object shape describing a template's
|
|
257
261
|
* declared placeholders.
|
|
@@ -281,8 +285,7 @@ function placeholderShape(placeholders) {
|
|
|
281
285
|
}
|
|
282
286
|
return (0, _orkestrel_contract.objectShape)(properties);
|
|
283
287
|
}
|
|
284
|
-
|
|
285
|
-
//#region src/core/templates/Template.ts
|
|
288
|
+
// src/core/templates/Template.ts
|
|
286
289
|
/**
|
|
287
290
|
* Represents a named, versionable template — `{{name}}` tokens in `content`,
|
|
288
291
|
* filled against a values record — implementing `TemplateInterface` exactly.
|
|
@@ -314,12 +317,12 @@ var Template = class {
|
|
|
314
317
|
tags;
|
|
315
318
|
constructor(options) {
|
|
316
319
|
const placeholders = options.placeholders ?? [];
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
seenNames.add(placeholder.name);
|
|
320
|
+
Iterator.from(placeholders).reduce((names, placeholder) => {
|
|
321
|
+
if (names.includes(placeholder.name)) throw new TemplateError("INVALID", `Duplicate placeholder name: ${placeholder.name}`, { name: placeholder.name });
|
|
322
|
+
const name = placeholder.name;
|
|
321
323
|
if ((0, _orkestrel_contract.isArray)(placeholder.path) && placeholder.path.length === 0) throw new TemplateError("INVALID", `Placeholder path must not be empty: ${placeholder.name}`, { name: placeholder.name });
|
|
322
|
-
|
|
324
|
+
return [...names, name];
|
|
325
|
+
}, []);
|
|
323
326
|
this.id = (0, _orkestrel_contract.isString)(options.id) ? options.id : crypto.randomUUID();
|
|
324
327
|
this.name = options.name;
|
|
325
328
|
this.content = options.content;
|
|
@@ -409,18 +412,11 @@ var Template = class {
|
|
|
409
412
|
*/
|
|
410
413
|
validate(values) {
|
|
411
414
|
const record = values ?? {};
|
|
412
|
-
const missing = [];
|
|
413
|
-
const seen = /* @__PURE__ */ new Set();
|
|
414
415
|
const pattern = new RegExp(FILL_PATTERN.source, FILL_PATTERN.flags);
|
|
415
|
-
|
|
416
|
-
const rawToken = match[1];
|
|
417
|
-
if (rawToken === void 0) continue;
|
|
418
|
-
const token = rawToken.trim();
|
|
419
|
-
if (seen.has(token)) continue;
|
|
420
|
-
seen.add(token);
|
|
416
|
+
const missing = [...new Set(Array.from(this.content.matchAll(pattern)).flatMap((match) => match[1] === void 0 ? [] : [match[1].trim()]))].filter((token) => {
|
|
421
417
|
const { value, declared, required } = resolveToken(record, this.placeholders, token);
|
|
422
|
-
|
|
423
|
-
}
|
|
418
|
+
return value === void 0 && declared?.fallback === void 0 && required;
|
|
419
|
+
});
|
|
424
420
|
const declaredNames = new Set(this.placeholders.map((placeholder) => placeholder.name));
|
|
425
421
|
const extra = Object.keys(record).filter((key) => !declaredNames.has(key));
|
|
426
422
|
return {
|
|
@@ -449,8 +445,7 @@ var Template = class {
|
|
|
449
445
|
return (0, _orkestrel_contract.schemaToParameters)(this.#contract.schema);
|
|
450
446
|
}
|
|
451
447
|
};
|
|
452
|
-
|
|
453
|
-
//#region src/core/templates/TemplateManager.ts
|
|
448
|
+
// src/core/templates/TemplateManager.ts
|
|
454
449
|
/**
|
|
455
450
|
* Represents the template registry — a self-owning, id-keyed record-holder for
|
|
456
451
|
* the {@link TemplateInterface} instances a consumer registers, looks up,
|
|
@@ -582,17 +577,7 @@ var TemplateManager = class {
|
|
|
582
577
|
this.#emitter.emit("remove", instance);
|
|
583
578
|
return true;
|
|
584
579
|
}
|
|
585
|
-
|
|
586
|
-
for (const id of target) {
|
|
587
|
-
const instance = this.#templates.get(id);
|
|
588
|
-
if (instance === void 0) {
|
|
589
|
-
all = false;
|
|
590
|
-
continue;
|
|
591
|
-
}
|
|
592
|
-
this.#templates.delete(id);
|
|
593
|
-
this.#emitter.emit("remove", instance);
|
|
594
|
-
}
|
|
595
|
-
return all;
|
|
580
|
+
return this.#remove(target).reduce((all, removed) => removed && all, true);
|
|
596
581
|
}
|
|
597
582
|
/** Removes every registered template, emitting `clear`. */
|
|
598
583
|
clear() {
|
|
@@ -661,6 +646,18 @@ var TemplateManager = class {
|
|
|
661
646
|
locale: template.locale ?? this.#locale
|
|
662
647
|
});
|
|
663
648
|
}
|
|
649
|
+
*#remove(target) {
|
|
650
|
+
for (const id of target) {
|
|
651
|
+
const instance = this.#templates.get(id);
|
|
652
|
+
if (instance === void 0) {
|
|
653
|
+
yield false;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
this.#templates.delete(id);
|
|
657
|
+
this.#emitter.emit("remove", instance);
|
|
658
|
+
yield true;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
664
661
|
#isInstance(template) {
|
|
665
662
|
return "fill" in template && (0, _orkestrel_contract.isFunction)(template.fill) && "validate" in template && (0, _orkestrel_contract.isFunction)(template.validate) && "parameters" in template && (0, _orkestrel_contract.isFunction)(template.parameters);
|
|
666
663
|
}
|
|
@@ -670,8 +667,7 @@ var TemplateManager = class {
|
|
|
670
667
|
return instance;
|
|
671
668
|
}
|
|
672
669
|
};
|
|
673
|
-
|
|
674
|
-
//#region src/core/factories.ts
|
|
670
|
+
// src/core/factories.ts
|
|
675
671
|
/**
|
|
676
672
|
* Creates a working {@link TemplateInterface} from a {@link TemplateOptions}
|
|
677
673
|
* bag, backed by the `Template` class.
|
|
@@ -728,7 +724,6 @@ function createTemplate(options) {
|
|
|
728
724
|
function createTemplateManager(options) {
|
|
729
725
|
return new TemplateManager(options);
|
|
730
726
|
}
|
|
731
|
-
//#endregion
|
|
732
727
|
exports.DEFAULT_LOCALE = DEFAULT_LOCALE;
|
|
733
728
|
exports.DEFAULT_MISSING_POLICY = DEFAULT_MISSING_POLICY;
|
|
734
729
|
exports.FILL_PATTERN = FILL_PATTERN;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["isInstance","isFiniteNumber","isArray","resolveField","stringShape","optionalShape","objectShape","isArray","isString","createContract","schemaToParameters","Emitter","isString","isFunction"],"sources":["../../../src/core/constants.ts","../../../src/core/errors.ts","../../../src/core/helpers.ts","../../../src/core/shapers.ts","../../../src/core/templates/Template.ts","../../../src/core/templates/TemplateManager.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { MissingPolicy } from './types.js'\n\n// Frozen default data for the template module — constants are\n// UPPER_SNAKE_CASE data, the sole home for module-scope literal defaults.\n\n/**\n * Holds the single-pass `{{name}}` substitution pattern shared by\n * `Template#fill` and `Template#validate`.\n *\n * @remarks\n * Global-flagged, two-alternative pattern: a match of the first alternative\n * (`\\{{` — a literal backslash followed by `{{`) means \"emit a literal\n * `{{`\" — the escape hatch for content that must show `{{` without\n * triggering substitution. A match that instead populates capture group 1\n * (`\\{{([^{}]+?)\\}\\}`) means \"substitute the named token\" — group 1 is the\n * untrimmed token text between the braces; every call site trims it\n * (`token.trim()`) before using it as a lookup name, so `'{{ name }}'` still\n * resolves `'name'`. The pattern deliberately does not wrap the token in\n * `\\s*` — an unclosed `'{{' + ' '.repeat(n)` with no closing `}}` would\n * otherwise force the regex engine into catastrophic backtracking over the\n * whitespace run (O(n^2)); trimming after the match keeps the same\n * whitespace tolerance without the backtracking hazard. Every call site\n * builds a fresh `RegExp` from `.source` / `.flags` rather than sharing this\n * instance's mutable `lastIndex` across scans.\n */\nexport const FILL_PATTERN = /\\\\\\{\\{|\\{\\{([^{}]+?)\\}\\}/g\n\n/**\n * Holds `'error'`, the default `missing` policy for `Template#fill` /\n * `TemplateManager#fill` when unspecified.\n */\nexport const DEFAULT_MISSING_POLICY: MissingPolicy = 'error'\n\n/**\n * Holds `'en-US'`, the default `locale` for `Template#fill` /\n * `TemplateManager#fill` when unspecified.\n */\nexport const DEFAULT_LOCALE = 'en-US'\n\n/**\n * Lists the prototype-pollution-unsafe field-path segments `'__proto__'`,\n * `'constructor'`, and `'prototype'` — a fill lookup refuses to resolve a path\n * containing one of them, treating the placeholder as unresolved.\n */\nexport const UNSAFE_FIELD_SEGMENTS: readonly string[] = Object.freeze([\n\t'__proto__',\n\t'constructor',\n\t'prototype',\n])\n","import type { TemplateErrorCode } from './types.js'\nimport { isInstance } from '@orkestrel/contract'\n\n// Misuse of the template layer `throw`s a `TemplateError` carrying a\n// machine-readable `code`, so a `catch` branches on `error.code`.\n\n/**\n * Represents an error thrown by the template layer — a machine-readable\n * {@link TemplateErrorCode} and an optional `context` record naming the\n * offending id or placeholder name.\n *\n * @remarks\n * Thrown for: a required placeholder staying unresolved under the `error`\n * {@link MissingPolicy} (`MISSING`), an unknown template id\n * (`NOTFOUND`), `createTemplate` handed invalid data (`INVALID`), and\n * `TemplateManagerInterface#register` handed an id already present without\n * `options.replace` (`CONFLICT`).\n */\nexport class TemplateError extends Error {\n\treadonly code: TemplateErrorCode\n\treadonly context?: Readonly<Record<string, unknown>>\n\n\tconstructor(\n\t\tcode: TemplateErrorCode,\n\t\tmessage: string,\n\t\tcontext?: Readonly<Record<string, unknown>>,\n\t) {\n\t\tsuper(message)\n\t\tthis.name = 'TemplateError'\n\t\tthis.code = code\n\t\tif (context !== undefined) this.context = context\n\t}\n}\n\n/**\n * Narrows an unknown caught value to a {@link TemplateError}.\n *\n * @param value - The value to test (typically a `catch` binding)\n * @returns True if `value` is a {@link TemplateError}; false otherwise\n *\n * @example\n * ```ts\n * import { isTemplateError } from '@src/core'\n *\n * try {\n * \tmanager.fill('missing')\n * } catch (error) {\n * \tif (isTemplateError(error) && error.code === 'NOTFOUND') return\n * }\n * ```\n */\nexport function isTemplateError(value: unknown): value is TemplateError {\n\treturn isInstance(value, TemplateError)\n}\n","import type { FieldPath } from '@orkestrel/contract'\nimport type {\n\tTemplateFillContext,\n\tTemplateFillValues,\n\tTemplatePlaceholder,\n\tTemplateTokenResolution,\n} from './types.js'\nimport { isArray, isFiniteNumber, resolveField } from '@orkestrel/contract'\nimport {\n\tDEFAULT_LOCALE,\n\tDEFAULT_MISSING_POLICY,\n\tFILL_PATTERN,\n\tUNSAFE_FIELD_SEGMENTS,\n} from './constants.js'\nimport { TemplateError } from './errors.js'\n\n// The templates pure-leaf inventory — every function here is a\n// referentially-transparent computation with no instance state, exported and\n// independently unit-testable. `Template#fill` / `#validate` route through\n// these leaves rather than duplicating the substitution logic.\n\n/**\n * Formats a resolved fill value for substitution into a template's `content`.\n *\n * @remarks\n * A finite number renders with the given locale's thousand grouping (through\n * `toLocaleString`); every other value — including `null` — String-coerces.\n * `null` therefore renders as the literal string `'null'`, matching\n * `String(value)` exactly, so a resolved `null` is visible in the output\n * rather than silently empty. An\n * invalid BCP-47 `locale` tag throws a `RangeError` from the underlying\n * `toLocaleString` call when `value` is a finite number — this is a caller\n * error (an invalid locale argument), by design, and is not caught here.\n *\n * @param value - The resolved value to format\n * @param locale - The locale used for finite-number formatting\n * @returns The formatted string\n *\n * @example\n * ```ts\n * import { formatValue } from '@src/core'\n *\n * formatValue(5010, 'en-US') // '5,010'\n * formatValue(null, 'en-US') // 'null'\n * ```\n */\nexport function formatValue(value: unknown, locale: string): string {\n\tif (isFiniteNumber(value)) return value.toLocaleString(locale)\n\treturn String(value)\n}\n\n/**\n * Resolves a field path against a fill-values record, refusing any path that\n * touches a prototype-pollution-unsafe segment.\n *\n * @remarks\n * A prototype-pollution guard shared by `fillTemplate` and `Template#validate`\n * so the two stay in lockstep: `path` normalizes to a segment array (a bare\n * string `path` becomes a single-segment array); if any segment appears in\n * `UNSAFE_FIELD_SEGMENTS` (`'__proto__'`, `'constructor'`, `'prototype'`), the\n * lookup is refused and `undefined` is returned without ever calling\n * `resolveField` — a path like `['__proto__', 'polluted']` can never reach\n * the record's actual prototype chain through this function. Every other\n * path resolves through `@orkestrel/contract`'s `resolveField`.\n *\n * @param record - The fill-values record to resolve against\n * @param path - The field path — a single segment or a segment array\n * @returns The resolved value, or `undefined` when unresolved or the path is unsafe\n *\n * @example\n * ```ts\n * import { resolveSafeField } from '@src/core'\n *\n * resolveSafeField({ a: { b: 1 } }, ['a', 'b']) // 1\n * resolveSafeField({}, ['__proto__', 'polluted']) // undefined\n * ```\n */\nexport function resolveSafeField(record: TemplateFillValues, path: FieldPath): unknown {\n\tconst segments = isArray(path) ? path : [path]\n\tif (segments.some((segment) => UNSAFE_FIELD_SEGMENTS.includes(segment))) return undefined\n\treturn resolveField(record, path)\n}\n\n/**\n * Resolves one `{{name}}` token against the declared placeholders and the\n * fill-values record.\n *\n * @remarks\n * The single implementation of the token rule `fillTemplate` and\n * `Template#validate` both apply, so the two can never drift: the declared\n * {@link TemplatePlaceholder} sharing the token's `name` (exact match)\n * supplies its `path`, falling back to the token split on `.`; the value\n * resolves through `resolveSafeField`, so any segment in\n * `UNSAFE_FIELD_SEGMENTS` yields `undefined` without ever calling\n * `resolveField`; `required` is `true` for an undeclared token and for a\n * declared placeholder whose `required` is not `false`. The token is passed\n * already trimmed. `fallback` is not applied here — it is read from\n * `declared` by each caller, because `fill` substitutes it and `validate`\n * only counts it.\n *\n * @param record - The fill-values record the token resolves against\n * @param placeholders - The declared placeholders the token matches by name\n * @param token - The trimmed token text, without its `{{` / `}}` delimiters\n * @returns The {@link TemplateTokenResolution} for the token\n *\n * @example\n * ```ts\n * import { resolveToken } from '@src/core'\n *\n * resolveToken({ name: 'Ada' }, [], 'name').value // 'Ada'\n * resolveToken({}, [{ name: 'nickname', required: false }], 'nickname').required // false\n * ```\n */\nexport function resolveToken(\n\trecord: TemplateFillValues,\n\tplaceholders: readonly TemplatePlaceholder[],\n\ttoken: string,\n): TemplateTokenResolution {\n\tconst declared = placeholders.find((placeholder) => placeholder.name === token)\n\tconst path = declared?.path ?? token.split('.')\n\treturn {\n\t\tvalue: resolveSafeField(record, path),\n\t\tdeclared,\n\t\trequired: declared === undefined || declared.required !== false,\n\t}\n}\n\n/**\n * Substitutes every `{{name}}` token in `content` in a single pass.\n *\n * @remarks\n * Uses a fresh `RegExp` clone of `FILL_PATTERN` per call (never sharing its\n * `lastIndex`) and a single `String#replace` scan — substituted output is\n * never re-scanned. Each token resolves through `resolveToken`, the one rule\n * `Template#validate` also applies: the matching declared\n * {@link TemplatePlaceholder} (exact `name`) supplies its `path` (falling\n * back to the token split on `.`); any path segment in `UNSAFE_FIELD_SEGMENTS`\n * makes the token unresolved without ever calling `resolveField` (a\n * prototype-pollution guard). A resolved value formats through `formatValue`; an\n * unresolved value falls back to the placeholder's `fallback` when declared;\n * otherwise `options.missing` governs — `'literal'` re-emits the original\n * `{{name}}` text, `'empty'` emits `''`, and `'error'` emits `''` for every\n * token but collects every unresolved required token (an undeclared token, or\n * a declared token with `required !== false`) and throws one\n * {@link TemplateError} coded `MISSING` listing them all, in first-appearance\n * order, once the scan completes. An escaped `\\{{` emits a literal `{{`.\n *\n * Called with no declared `placeholders` and `{ missing: 'empty' }`, this is a\n * bare interpolation over `content` — every token resolves by dotted path\n * against the values record and every unresolved token emits `''`.\n * `FILL_PATTERN`'s token class (`[^{}]`) excludes `{`, so a token containing\n * `{` never matches and the surrounding `{{` stays literal.\n *\n * @param content - The template content carrying `{{name}}` tokens\n * @param values - The values tokens resolve against\n * @param options - `missing` (default `'error'`), `locale` (default `'en-US'`), and the declared `placeholders` (default none) tokens resolve against\n * @returns The substituted content\n *\n * @example\n * ```ts\n * import { fillTemplate } from '@src/core'\n *\n * fillTemplate('Hi {{name}}', { name: 'Ada' }) // 'Hi Ada'\n * fillTemplate('Limit {{limit}}', { limit: 5010 }, { missing: 'empty' }) // 'Limit 5,010'\n * ```\n */\nexport function fillTemplate(\n\tcontent: string,\n\tvalues?: TemplateFillValues,\n\toptions?: TemplateFillContext,\n): string {\n\tconst placeholders = options?.placeholders ?? []\n\tconst missing = options?.missing ?? DEFAULT_MISSING_POLICY\n\tconst locale = options?.locale ?? DEFAULT_LOCALE\n\tconst record = values ?? {}\n\n\tconst missingNames: string[] = []\n\tconst seen = new Set<string>()\n\n\tconst pattern = new RegExp(FILL_PATTERN.source, FILL_PATTERN.flags)\n\tconst result = content.replace(pattern, (matchText: string, rawToken: string | undefined) => {\n\t\tif (rawToken === undefined) return '{{'\n\t\tconst token = rawToken.trim()\n\n\t\tconst { value, declared, required } = resolveToken(record, placeholders, token)\n\n\t\tif (value !== undefined) return formatValue(value, locale)\n\t\tif (declared?.fallback !== undefined) return formatValue(declared.fallback, locale)\n\n\t\tif (missing === 'literal') return matchText\n\t\tif (missing === 'empty') return ''\n\n\t\tif (required && !seen.has(token)) {\n\t\t\tseen.add(token)\n\t\t\tmissingNames.push(token)\n\t\t}\n\t\treturn ''\n\t})\n\n\tif (missing === 'error' && missingNames.length > 0) {\n\t\tthrow new TemplateError(\n\t\t\t'MISSING',\n\t\t\t`Missing required placeholder(s): ${missingNames.join(', ')}`,\n\t\t\t{ missing: missingNames },\n\t\t)\n\t}\n\n\treturn result\n}\n","import type { ContractShape } from '@orkestrel/contract'\nimport type { TemplatePlaceholder } from './types.js'\nimport { objectShape, optionalShape, stringShape } from '@orkestrel/contract'\n\n// The templates shape-value inventory — every function here builds an\n// `@orkestrel/contract` shape from declared template data. Shapers sit above\n// the `helpers.ts` leaf pair: they consume it, and it never consumes them.\n\n/**\n * Builds the `@orkestrel/contract` object shape describing a template's\n * declared placeholders.\n *\n * @remarks\n * Each placeholder becomes a `stringShape` carrying its `description`;\n * `required === false` wraps it in `optionalShape`. Used by `Template` to\n * compile its `parameters()` contract once per instance.\n *\n * @param placeholders - The declared placeholders to shape\n * @returns The contract shape for `createContract`\n *\n * @example\n * ```ts\n * import { placeholderShape } from '@src/core'\n * import { createContract } from '@orkestrel/contract'\n *\n * const contract = createContract(placeholderShape([{ name: 'city' }]))\n * ```\n */\nexport function placeholderShape(placeholders: readonly TemplatePlaceholder[]): ContractShape {\n\tconst properties: Record<string, ContractShape> = {}\n\tfor (const placeholder of placeholders) {\n\t\tconst description = placeholder.description\n\t\tconst field = stringShape({\n\t\t\t...(description !== undefined ? { description } : {}),\n\t\t})\n\t\tproperties[placeholder.name] = placeholder.required === false ? optionalShape(field) : field\n\t}\n\treturn objectShape(properties)\n}\n","import type { ContractInterface } from '@orkestrel/contract'\nimport type {\n\tMissingPolicy,\n\tTemplateDefinition,\n\tTemplateFillOptions,\n\tTemplateFillValues,\n\tTemplateInterface,\n\tTemplateOptions,\n\tTemplatePlaceholder,\n\tTemplateValidationResult,\n} from '../types.js'\nimport { createContract, isArray, isString, schemaToParameters } from '@orkestrel/contract'\nimport { DEFAULT_LOCALE, DEFAULT_MISSING_POLICY, FILL_PATTERN } from '../constants.js'\nimport { fillTemplate, resolveToken } from '../helpers.js'\nimport { placeholderShape } from '../shapers.js'\nimport { TemplateError } from '../errors.js'\n\n/**\n * Represents a named, versionable template — `{{name}}` tokens in `content`,\n * filled against a values record — implementing `TemplateInterface` exactly.\n *\n * @remarks\n * `missing` / `locale` seed this instance's default {@link TemplateFillOptions},\n * overridable per `fill` call. Its `parameters()` contract (built from\n * `placeholders` through `placeholderShape`) compiles once, in the constructor.\n *\n * @throws {@link TemplateError} Thrown when `options.placeholders` declares a duplicate `name` or an empty `path` (coded `INVALID`)\n *\n * @example\n * ```ts\n * const greeting = new Template({ name: 'greeting', content: 'Hi {{name}}' })\n * greeting.fill({ name: 'Ada' }) // 'Hi Ada'\n * ```\n */\nexport class Template implements TemplateInterface {\n\treadonly #missing: MissingPolicy\n\treadonly #locale: string\n\treadonly #contract: ContractInterface<unknown>\n\treadonly id: string\n\treadonly name: string\n\treadonly content: string\n\treadonly placeholders: readonly TemplatePlaceholder[]\n\treadonly summary?: string\n\treadonly description?: string\n\treadonly category?: string\n\treadonly tags?: readonly string[]\n\n\tconstructor(options: TemplateOptions) {\n\t\tconst placeholders = options.placeholders ?? []\n\t\tconst seenNames = new Set<string>()\n\t\tfor (const placeholder of placeholders) {\n\t\t\tif (seenNames.has(placeholder.name)) {\n\t\t\t\tthrow new TemplateError('INVALID', `Duplicate placeholder name: ${placeholder.name}`, {\n\t\t\t\t\tname: placeholder.name,\n\t\t\t\t})\n\t\t\t}\n\t\t\tseenNames.add(placeholder.name)\n\t\t\tif (isArray(placeholder.path) && placeholder.path.length === 0) {\n\t\t\t\tthrow new TemplateError(\n\t\t\t\t\t'INVALID',\n\t\t\t\t\t`Placeholder path must not be empty: ${placeholder.name}`,\n\t\t\t\t\t{ name: placeholder.name },\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\tthis.id = isString(options.id) ? options.id : crypto.randomUUID()\n\t\tthis.name = options.name\n\t\tthis.content = options.content\n\t\tthis.placeholders = placeholders\n\t\tif (options.summary !== undefined) this.summary = options.summary\n\t\tif (options.description !== undefined) this.description = options.description\n\t\tif (options.category !== undefined) this.category = options.category\n\t\tif (options.tags !== undefined) this.tags = options.tags\n\t\tthis.#missing = options.missing ?? DEFAULT_MISSING_POLICY\n\t\tthis.#locale = options.locale ?? DEFAULT_LOCALE\n\t\tthis.#contract = createContract(placeholderShape(this.placeholders))\n\t}\n\n\t/**\n\t * Returns the plain, JSON-serializable data this template carries.\n\t *\n\t * @returns The {@link TemplateDefinition} record\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({ name: 'greeting', content: 'Hi {{name}}' })\n\t * instance.definition().name // 'greeting'\n\t * ```\n\t */\n\tdefinition(): TemplateDefinition {\n\t\treturn {\n\t\t\tid: this.id,\n\t\t\tname: this.name,\n\t\t\tcontent: this.content,\n\t\t\tplaceholders: this.placeholders,\n\t\t\t...(this.summary !== undefined ? { summary: this.summary } : {}),\n\t\t\t...(this.description !== undefined ? { description: this.description } : {}),\n\t\t\t...(this.category !== undefined ? { category: this.category } : {}),\n\t\t\t...(this.tags !== undefined ? { tags: this.tags } : {}),\n\t\t}\n\t}\n\n\t/**\n\t * Substitutes every `{{name}}` token in `content` against `values`.\n\t *\n\t * @param values - The values tokens resolve against\n\t * @param options - Per-call overrides for this instance's `missing` / `locale` defaults\n\t * @returns The substituted content\n\t * @throws {@link TemplateError} Thrown when a required placeholder stays unresolved under the `'error'` policy (coded `MISSING`)\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({ name: 'greeting', content: 'Hi {{name}}' })\n\t * instance.fill({ name: 'Ada' }) // 'Hi Ada'\n\t * ```\n\t */\n\tfill(values?: TemplateFillValues, options?: TemplateFillOptions): string {\n\t\treturn fillTemplate(this.content, values, {\n\t\t\tmissing: options?.missing ?? this.#missing,\n\t\t\tlocale: options?.locale ?? this.#locale,\n\t\t\tplaceholders: this.placeholders,\n\t\t})\n\t}\n\n\t/**\n\t * Reports which required placeholders would stay unresolved, and which\n\t * `values` keys go unused, without producing output.\n\t *\n\t * @remarks\n\t * Content-token driven: scans `this.content` for every `{{name}}` token\n\t * (skipping escaped `\\{{` matches) the same way `fill` does, so `validate`\n\t * predicts `fill`'s `'error'`-{@link MissingPolicy} outcome exactly — a\n\t * token reported here as missing is precisely a token that would throw\n\t * under `fill(values, { missing: 'error' })`. For each distinct token\n\t * (first-appearance order, trimmed): `resolveToken` applies the one shared\n\t * token rule `fill` also applies — a declared {@link TemplatePlaceholder}\n\t * sharing its `name` supplies `path` (falling back to the token split on\n\t * `.`), and the value resolves through `resolveSafeField`. The token is `missing`\n\t * only when the value is unresolved, no `fallback` is declared, and the\n\t * placeholder is required (`required !== false`, including undeclared\n\t * tokens). `extra` lists every `values` key with no declared placeholder.\n\t *\n\t * @param values - The values to check\n\t * @returns The {@link TemplateValidationResult}\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({\n\t * \tname: 'greeting',\n\t * \tcontent: 'Hi {{name}}',\n\t * \tplaceholders: [{ name: 'name' }],\n\t * })\n\t * instance.validate({}).missing // ['name']\n\t * ```\n\t */\n\tvalidate(values?: TemplateFillValues): TemplateValidationResult {\n\t\tconst record = values ?? {}\n\t\tconst missing: string[] = []\n\t\tconst seen = new Set<string>()\n\n\t\tconst pattern = new RegExp(FILL_PATTERN.source, FILL_PATTERN.flags)\n\t\tfor (const match of this.content.matchAll(pattern)) {\n\t\t\tconst rawToken = match[1]\n\t\t\tif (rawToken === undefined) continue\n\t\t\tconst token = rawToken.trim()\n\t\t\tif (seen.has(token)) continue\n\t\t\tseen.add(token)\n\n\t\t\tconst { value, declared, required } = resolveToken(record, this.placeholders, token)\n\n\t\t\tif (value === undefined && declared?.fallback === undefined && required) {\n\t\t\t\tmissing.push(token)\n\t\t\t}\n\t\t}\n\n\t\tconst declaredNames = new Set(this.placeholders.map((placeholder) => placeholder.name))\n\t\tconst extra = Object.keys(record).filter((key) => !declaredNames.has(key))\n\n\t\treturn { valid: missing.length === 0, missing, extra }\n\t}\n\n\t/**\n\t * Projects this template's placeholders to the open tool-parameters record\n\t * shape.\n\t *\n\t * @returns The compiled parameters record, or `undefined` when `schemaToParameters` yields none\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({\n\t * \tname: 'greeting',\n\t * \tcontent: 'Hi {{name}}',\n\t * \tplaceholders: [{ name: 'name' }],\n\t * })\n\t * instance.parameters()\n\t * ```\n\t */\n\tparameters(): Readonly<Record<string, unknown>> | undefined {\n\t\treturn schemaToParameters(this.#contract.schema)\n\t}\n}\n","import type {\n\tMissingPolicy,\n\tTemplateFillValues,\n\tTemplateFillOptions,\n\tTemplateInterface,\n\tTemplateManagerEventMap,\n\tTemplateManagerInterface,\n\tTemplateManagerOptions,\n\tTemplateOptions,\n\tTemplateQuery,\n\tTemplateRegisterOptions,\n\tTemplateValidationResult,\n} from '../types.js'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport { isFunction, isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport { DEFAULT_LOCALE, DEFAULT_MISSING_POLICY } from '../constants.js'\nimport { TemplateError } from '../errors.js'\nimport { Template } from './Template.js'\n\n/**\n * Represents the template registry — a self-owning, id-keyed record-holder for\n * the {@link TemplateInterface} instances a consumer registers, looks up,\n * fills, and validates by id — implementing `TemplateManagerInterface`\n * exactly.\n *\n * @remarks\n * Singular and plural accessors, the batch `remove` overloads, and ownership\n * of the emitter all sit here. `register` accepts either a constructed {@link TemplateInterface} (kept\n * as-is, including its own `missing` / `locale` defaults) or a plain\n * {@link TemplateOptions} bag — constructed into a `Template` with this\n * manager's `missing` / `locale` defaults applied wherever the bag omits\n * them. A duplicate `id` throws a {@link TemplateError} coded `CONFLICT`\n * unless `options.replace` is `true`, in which case the existing entry is\n * overwritten. `options.templates` seeds the registry at construction without\n * emitting `register` — only calls to `register` after construction emit.\n * The batch `remove(ids)` form removes every present id and returns\n * `true` only when every listed id was present.\n *\n * @example\n * ```ts\n * import { TemplateManager } from '@src/core'\n *\n * const manager = new TemplateManager()\n * const instance = manager.register({ name: 'greeting', content: 'Hi {{name}}' })\n * manager.fill(instance.id, { name: 'Ada' }) // 'Hi Ada'\n * ```\n */\nexport class TemplateManager implements TemplateManagerInterface {\n\treadonly #templates = new Map<string, TemplateInterface>()\n\treadonly #emitter: Emitter<TemplateManagerEventMap>\n\treadonly #missing: MissingPolicy\n\treadonly #locale: string\n\n\tconstructor(options?: TemplateManagerOptions) {\n\t\tconst on = options?.on\n\t\tconst error = options?.error\n\t\tthis.#emitter = new Emitter<TemplateManagerEventMap>({\n\t\t\t...(on !== undefined ? { on } : {}),\n\t\t\t...(error !== undefined ? { error } : {}),\n\t\t})\n\t\tthis.#missing = options?.missing ?? DEFAULT_MISSING_POLICY\n\t\tthis.#locale = options?.locale ?? DEFAULT_LOCALE\n\t\tfor (const template of options?.templates ?? []) {\n\t\t\tconst instance = this.#instantiate(template)\n\t\t\tthis.#templates.set(instance.id, instance)\n\t\t}\n\t}\n\n\tget emitter(): EmitterInterface<TemplateManagerEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget count(): number {\n\t\treturn this.#templates.size\n\t}\n\n\t/**\n\t * Registers a template — a constructed {@link TemplateInterface} (kept\n\t * as-is) or a plain {@link TemplateOptions} bag (constructed into a\n\t * `Template` with this manager's `missing` / `locale` defaults applied\n\t * wherever the bag omits them).\n\t *\n\t * @param template - The template instance or options to register\n\t * @param options - `replace` — overwrite an existing entry sharing the same id instead of throwing\n\t * @returns The registered {@link TemplateInterface}\n\t * @throws {@link TemplateError} Thrown when the id is already registered and `options.replace` is not `true` (coded `CONFLICT`), or when an options bag declares a duplicate placeholder `name` or an empty `path` (coded `INVALID`)\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = manager.register({ id: 'greeting', name: 'greeting', content: 'Hi {{name}}' })\n\t * ```\n\t */\n\tregister(\n\t\ttemplate: TemplateInterface | TemplateOptions,\n\t\toptions?: TemplateRegisterOptions,\n\t): TemplateInterface {\n\t\tconst instance = this.#instantiate(template)\n\t\tconst existing = this.#templates.get(instance.id)\n\t\tif (existing !== undefined && options?.replace !== true) {\n\t\t\tthrow new TemplateError('CONFLICT', `Template already registered: ${instance.id}`, {\n\t\t\t\tid: instance.id,\n\t\t\t})\n\t\t}\n\t\tthis.#templates.set(instance.id, instance)\n\t\tthis.#emitter.emit('register', instance)\n\t\treturn instance\n\t}\n\n\t/**\n\t * Returns one registered {@link TemplateInterface} by id.\n\t *\n\t * @param id - The template id\n\t * @returns The registered {@link TemplateInterface}, or `undefined` when `id` is unregistered\n\t */\n\ttemplate(id: string): TemplateInterface | undefined {\n\t\treturn this.#templates.get(id)\n\t}\n\n\t/**\n\t * Lists every registered template.\n\t *\n\t * @returns A snapshot array of every registered {@link TemplateInterface}\n\t */\n\ttemplates(): readonly TemplateInterface[] {\n\t\treturn [...this.#templates.values()]\n\t}\n\n\t/**\n\t * Filters registered templates by `name`, `category`, and `tag` — every\n\t * supplied field must match.\n\t *\n\t * @param query - The {@link TemplateQuery} to filter by; omit for every registered template\n\t * @returns The matching templates\n\t */\n\tfind(query?: TemplateQuery): readonly TemplateInterface[] {\n\t\tif (query === undefined) return this.templates()\n\t\treturn this.templates().filter((instance) => {\n\t\t\tif (query.name !== undefined && instance.name !== query.name) return false\n\t\t\tif (query.category !== undefined && instance.category !== query.category) return false\n\t\t\tif (query.tag !== undefined && !(instance.tags ?? []).includes(query.tag)) return false\n\t\t\treturn true\n\t\t})\n\t}\n\n\t/**\n\t * Tests whether a template id is registered.\n\t *\n\t * @param id - The template id\n\t * @returns True if `id` is registered; false otherwise\n\t */\n\thas(id: string): boolean {\n\t\treturn this.#templates.has(id)\n\t}\n\n\t/**\n\t * Removes one, several, or every registered template.\n\t *\n\t * @remarks\n\t * `remove()` removes every registered template, emitting `remove` once per\n\t * instance. `remove(id)` removes one template by id, emitting `remove` and\n\t * returning `true` when it existed, `false` otherwise. `remove(ids)`\n\t * removes every listed id that is present, emitting `remove` once per\n\t * removed instance, and returns `true` only when every listed id was\n\t * present.\n\t *\n\t * @param target - Omit to remove all, a single id, or a list of ids\n\t * @returns `boolean` for the single-id / list-of-ids forms; `void` for the remove-all form\n\t */\n\t// `readonly string[]` is not assignable to `id: string`, so a list resolves to the\n\t// batch signature whatever order the signatures are declared in.\n\tremove(ids: readonly string[]): boolean\n\tremove(id: string): boolean\n\tremove(): void\n\tremove(target?: string | readonly string[]): boolean | void {\n\t\tif (target === undefined) {\n\t\t\tfor (const instance of this.#templates.values()) this.#emitter.emit('remove', instance)\n\t\t\tthis.#templates.clear()\n\t\t\treturn\n\t\t}\n\t\tif (isString(target)) {\n\t\t\tconst instance = this.#templates.get(target)\n\t\t\tif (instance === undefined) return false\n\t\t\tthis.#templates.delete(target)\n\t\t\tthis.#emitter.emit('remove', instance)\n\t\t\treturn true\n\t\t}\n\t\tlet all = true\n\t\tfor (const id of target) {\n\t\t\tconst instance = this.#templates.get(id)\n\t\t\tif (instance === undefined) {\n\t\t\t\tall = false\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tthis.#templates.delete(id)\n\t\t\tthis.#emitter.emit('remove', instance)\n\t\t}\n\t\treturn all\n\t}\n\n\t/** Removes every registered template, emitting `clear`. */\n\tclear(): void {\n\t\tthis.#templates.clear()\n\t\tthis.#emitter.emit('clear')\n\t}\n\n\t/**\n\t * Tears down the registry: drops every registered template and destroys the\n\t * owned emitter. Idempotent.\n\t *\n\t * @remarks\n\t * Teardown is not an observable registry operation and the emitter is being\n\t * released, so this emits neither `clear` nor `remove`. The emitter is torn\n\t * down last, after the registry is dropped.\n\t *\n\t * @example\n\t * ```ts\n\t * const manager = new TemplateManager()\n\t * manager.destroy()\n\t * manager.emitter.destroyed // true\n\t * ```\n\t */\n\tdestroy(): void {\n\t\tthis.#templates.clear()\n\t\tthis.#emitter.destroy()\n\t}\n\n\t/**\n\t * Fills a registered template by id.\n\t *\n\t * @param id - The template id\n\t * @param values - The values tokens resolve against\n\t * @param options - Per-call overrides for the template's `missing` / `locale` defaults\n\t * @returns The substituted content\n\t * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown\n\t * @throws {@link TemplateError} Thrown when a required placeholder stays unresolved under the `'error'` policy (coded `MISSING`)\n\t */\n\tfill(id: string, values?: TemplateFillValues, options?: TemplateFillOptions): string {\n\t\treturn this.#require(id).fill(values, options)\n\t}\n\n\t/**\n\t * Validates values against a registered template by id.\n\t *\n\t * @param id - The template id\n\t * @param values - The values to check\n\t * @returns The {@link TemplateValidationResult}\n\t * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown\n\t */\n\tvalidate(id: string, values?: TemplateFillValues): TemplateValidationResult {\n\t\treturn this.#require(id).validate(values)\n\t}\n\n\t/**\n\t * Projects a registered template's parameters by id.\n\t *\n\t * @param id - The template id\n\t * @returns The compiled parameters record, or `undefined` when the template has none\n\t * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown\n\t */\n\tparameters(id: string): Readonly<Record<string, unknown>> | undefined {\n\t\treturn this.#require(id).parameters()\n\t}\n\n\t#instantiate(template: TemplateInterface | TemplateOptions): TemplateInterface {\n\t\tif (this.#isInstance(template)) return template\n\t\treturn new Template({\n\t\t\t...template,\n\t\t\tmissing: template.missing ?? this.#missing,\n\t\t\tlocale: template.locale ?? this.#locale,\n\t\t})\n\t}\n\n\t// A TemplateOptions bag is plain data with no `fill` / `validate` /\n\t// `parameters` methods; a TemplateInterface instance always exposes all three.\n\t#isInstance(template: TemplateInterface | TemplateOptions): template is TemplateInterface {\n\t\treturn (\n\t\t\t'fill' in template &&\n\t\t\tisFunction(template.fill) &&\n\t\t\t'validate' in template &&\n\t\t\tisFunction(template.validate) &&\n\t\t\t'parameters' in template &&\n\t\t\tisFunction(template.parameters)\n\t\t)\n\t}\n\n\t// Every by-id operation that needs a template to proceed shares this lookup.\n\t// The `template` accessor deliberately does not, and returns `undefined`.\n\t#require(id: string): TemplateInterface {\n\t\tconst instance = this.#templates.get(id)\n\t\tif (instance === undefined) {\n\t\t\tthrow new TemplateError('NOTFOUND', `Unknown template id: ${id}`, { id })\n\t\t}\n\t\treturn instance\n\t}\n}\n","import type {\n\tTemplateInterface,\n\tTemplateManagerInterface,\n\tTemplateManagerOptions,\n\tTemplateOptions,\n} from './types.js'\nimport { Template } from './templates/Template.js'\nimport { TemplateManager } from './templates/TemplateManager.js'\n\n/**\n * Creates a working {@link TemplateInterface} from a {@link TemplateOptions}\n * bag, backed by the `Template` class.\n *\n * @param options - The template's `name` / `content`, an optional `id`\n * (defaults to a generated UUID), `placeholders`, catalog metadata, and\n * `missing` / `locale` fill defaults\n * @returns A working {@link TemplateInterface}\n * @throws {@link TemplateError} Thrown when `options.placeholders` declares a duplicate `name` or an empty `path` (coded `INVALID`)\n *\n * @example Create a template and a registry\n * ```ts\n * import { createTemplate, createTemplateManager } from '@orkestrel/template'\n *\n * const greeting = createTemplate({ name: 'greeting', content: 'Hi {{name}}' })\n * greeting.fill({ name: 'Ada' }) // 'Hi Ada'\n *\n * const templates = createTemplateManager({\n * \ttemplates: [\n * \t\t{ id: 'greeting', name: 'greeting', content: 'Hi {{name}}', category: 'mail' },\n * \t\t{ id: 'farewell', name: 'farewell', content: 'Bye {{name}}', category: 'mail' },\n * \t\t{ id: 'alert', name: 'alert', content: 'Alert: {{reason}}', category: 'ops' },\n * \t],\n * })\n * templates.fill('greeting', { name: 'Ada' }) // 'Hi Ada'\n * templates.find({ category: 'mail' }).map((one) => one.id) // ['greeting', 'farewell']\n * templates.has('alert') // true\n * templates.has('missing') // false\n * ```\n */\nexport function createTemplate(options: TemplateOptions): TemplateInterface {\n\treturn new Template(options)\n}\n\n/**\n * Creates a working {@link TemplateManagerInterface}, optionally seeded with\n * the templates the options carry, backed by the `TemplateManager` class.\n *\n * @param options - Optional initial `templates` seed collection and\n * manager-wide `missing` / `locale` fill defaults, emitter `on` hooks, and\n * an `error` handler\n * @returns A working {@link TemplateManagerInterface}\n * @throws {@link TemplateError} Thrown when a seeded `options.templates` bag declares a duplicate placeholder `name` or an empty `path` (coded `INVALID`)\n *\n * @example\n * ```ts\n * import { createTemplateManager } from '@src/core'\n *\n * const templates = createTemplateManager({\n * \ttemplates: [{ id: 'greeting', name: 'greeting', content: 'Hi {{name}}' }],\n * })\n * templates.fill('greeting', { name: 'Ada' }) // 'Hi Ada'\n * ```\n */\nexport function createTemplateManager(options?: TemplateManagerOptions): TemplateManagerInterface {\n\treturn new TemplateManager(options)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["holds","INTRINSICS","isObject","isFunction","isFiniteNumber","isArray","resolveField","stringShape","optionalShape","objectShape","isArray","isString","createContract","schemaToParameters","Emitter","isString","isFunction"],"sources":["../../../src/core/constants.ts","../../../src/core/errors.ts","../../../src/core/helpers.ts","../../../src/core/shapers.ts","../../../src/core/templates/Template.ts","../../../src/core/templates/TemplateManager.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { MissingPolicy } from './types.js'\n\n// Frozen default data for the template module — constants are\n// UPPER_SNAKE_CASE data, the sole home for module-scope literal defaults.\n\n/**\n * Holds the single-pass `{{name}}` substitution pattern shared by\n * `Template#fill` and `Template#validate`.\n *\n * @remarks\n * Global-flagged, two-alternative pattern: a match of the first alternative\n * (`\\{{` — a literal backslash followed by `{{`) means \"emit a literal\n * `{{`\" — the escape hatch for content that must show `{{` without\n * triggering substitution. A match that instead populates capture group 1\n * (`\\{{([^{}]+?)\\}\\}`) means \"substitute the named token\" — group 1 is the\n * untrimmed token text between the braces; every call site trims it\n * (`token.trim()`) before using it as a lookup name, so `'{{ name }}'` still\n * resolves `'name'`. The pattern deliberately does not wrap the token in\n * `\\s*` — an unclosed `'{{' + ' '.repeat(n)` with no closing `}}` would\n * otherwise force the regex engine into catastrophic backtracking over the\n * whitespace run (O(n^2)); trimming after the match keeps the same\n * whitespace tolerance without the backtracking hazard. Every call site\n * builds a fresh `RegExp` from `.source` / `.flags` rather than sharing this\n * instance's mutable `lastIndex` across scans.\n */\nexport const FILL_PATTERN = /\\\\\\{\\{|\\{\\{([^{}]+?)\\}\\}/g\n\n/**\n * Holds `'error'`, the default `missing` policy for `Template#fill` /\n * `TemplateManager#fill` when unspecified.\n */\nexport const DEFAULT_MISSING_POLICY: MissingPolicy = 'error'\n\n/**\n * Holds `'en-US'`, the default `locale` for `Template#fill` /\n * `TemplateManager#fill` when unspecified.\n */\nexport const DEFAULT_LOCALE = 'en-US'\n\n/**\n * Lists the prototype-pollution-unsafe field-path segments `'__proto__'`,\n * `'constructor'`, and `'prototype'` — a fill lookup refuses to resolve a path\n * containing one of them, treating the placeholder as unresolved.\n */\nexport const UNSAFE_FIELD_SEGMENTS: readonly string[] = Object.freeze([\n\t'__proto__',\n\t'constructor',\n\t'prototype',\n])\n","import type { TemplateErrorCode } from './types.js'\nimport { holds, INTRINSICS, isFunction, isObject } from '@orkestrel/contract'\n\n// Misuse of the template layer `throw`s a `TemplateError` carrying a\n// machine-readable `code`, so a `catch` branches on `error.code`.\n\n/**\n * Represents an error thrown by the template layer — a machine-readable\n * {@link TemplateErrorCode} and an optional `context` record naming the\n * offending id or placeholder name.\n *\n * @remarks\n * Thrown for: a required placeholder staying unresolved under the `error`\n * {@link MissingPolicy} (`MISSING`), an unknown template id\n * (`NOTFOUND`), `createTemplate` handed invalid data (`INVALID`), and\n * `TemplateManagerInterface#register` handed an id already present without\n * `options.replace` (`CONFLICT`).\n */\nexport class TemplateError extends Error {\n\treadonly code: TemplateErrorCode\n\treadonly context?: Readonly<Record<string, unknown>>\n\n\tconstructor(\n\t\tcode: TemplateErrorCode,\n\t\tmessage: string,\n\t\tcontext?: Readonly<Record<string, unknown>>,\n\t) {\n\t\tsuper(message)\n\t\tINTRINSICS.define(this, Symbol.for('@orkestrel/template.TemplateError'), { value: this })\n\t\tthis.name = 'TemplateError'\n\t\tthis.code = code\n\t\tif (context !== undefined) this.context = context\n\t}\n}\n\n/**\n * Narrows an unknown caught value to a {@link TemplateError}.\n *\n * @remarks\n * Recognizes self-branded errors from other package copies and subclasses, and\n * refuses transparent proxies and unbranded look-alikes.\n *\n * @param value - The value to test (typically a `catch` binding)\n * @returns True if `value` is a {@link TemplateError}; false otherwise\n *\n * @example\n * ```ts\n * import { isTemplateError } from '@src/core'\n *\n * try {\n * \tmanager.fill('missing')\n * } catch (error) {\n * \tif (isTemplateError(error) && error.code === 'NOTFOUND') return\n * }\n * ```\n */\nexport function isTemplateError(value: unknown): value is TemplateError {\n\treturn holds(() => {\n\t\tif (!(value instanceof Error) || INTRINSICS.prototype(value) === Error.prototype) return false\n\t\tif (\n\t\t\tINTRINSICS.describe(value, Symbol.for('@orkestrel/template.TemplateError'))?.value !== value\n\t\t)\n\t\t\treturn false\n\t\tconst code: unknown = INTRINSICS.reflect.read(value, 'code')\n\t\tconst context: unknown = INTRINSICS.reflect.read(value, 'context')\n\t\treturn (\n\t\t\t(code === 'MISSING' || code === 'NOTFOUND' || code === 'INVALID' || code === 'CONFLICT') &&\n\t\t\t(context === undefined || isObject(context) || isFunction(context))\n\t\t)\n\t})\n}\n","import type { FieldPath } from '@orkestrel/contract'\nimport type {\n\tTemplateFillContext,\n\tTemplateFillValues,\n\tTemplatePlaceholder,\n\tTemplateTokenResolution,\n} from './types.js'\nimport { isArray, isFiniteNumber, resolveField } from '@orkestrel/contract'\nimport {\n\tDEFAULT_LOCALE,\n\tDEFAULT_MISSING_POLICY,\n\tFILL_PATTERN,\n\tUNSAFE_FIELD_SEGMENTS,\n} from './constants.js'\nimport { TemplateError } from './errors.js'\n\n// The templates pure-leaf inventory — every function here is a\n// referentially-transparent computation with no instance state, exported and\n// independently unit-testable. `Template#fill` / `#validate` route through\n// these leaves rather than duplicating the substitution logic.\n\n/**\n * Formats a resolved fill value for substitution into a template's `content`.\n *\n * @remarks\n * A finite number renders with the given locale's thousand grouping (through\n * `toLocaleString`); every other value — including `null` — String-coerces.\n * `null` therefore renders as the literal string `'null'`, matching\n * `String(value)` exactly, so a resolved `null` is visible in the output\n * rather than silently empty. An\n * invalid BCP-47 `locale` tag throws a `RangeError` from the underlying\n * `toLocaleString` call when `value` is a finite number — this is a caller\n * error (an invalid locale argument), by design, and is not caught here.\n *\n * @param value - The resolved value to format\n * @param locale - The locale used for finite-number formatting\n * @returns The formatted string\n *\n * @example\n * ```ts\n * import { formatValue } from '@src/core'\n *\n * formatValue(5010, 'en-US') // '5,010'\n * formatValue(null, 'en-US') // 'null'\n * ```\n */\nexport function formatValue(value: unknown, locale: string): string {\n\tif (isFiniteNumber(value)) return value.toLocaleString(locale)\n\treturn String(value)\n}\n\n/**\n * Resolves a field path against a fill-values record, refusing any path that\n * touches a prototype-pollution-unsafe segment.\n *\n * @remarks\n * A prototype-pollution guard shared by `fillTemplate` and `Template#validate`\n * so the two stay in lockstep: `path` normalizes to a segment array (a bare\n * string `path` becomes a single-segment array); if any segment appears in\n * `UNSAFE_FIELD_SEGMENTS` (`'__proto__'`, `'constructor'`, `'prototype'`), the\n * lookup is refused and `undefined` is returned without ever calling\n * `resolveField` — a path like `['__proto__', 'polluted']` can never reach\n * the record's actual prototype chain through this function. Every other\n * path resolves through `@orkestrel/contract`'s `resolveField`.\n *\n * @param record - The fill-values record to resolve against\n * @param path - The field path — a single segment or a segment array\n * @returns The resolved value, or `undefined` when unresolved or the path is unsafe\n *\n * @example\n * ```ts\n * import { resolveSafeField } from '@src/core'\n *\n * resolveSafeField({ a: { b: 1 } }, ['a', 'b']) // 1\n * resolveSafeField({}, ['__proto__', 'polluted']) // undefined\n * ```\n */\nexport function resolveSafeField(record: TemplateFillValues, path: FieldPath): unknown {\n\tconst segments = isArray(path) ? path : [path]\n\tif (segments.some((segment) => UNSAFE_FIELD_SEGMENTS.includes(segment))) return undefined\n\treturn resolveField(record, path)\n}\n\n/**\n * Resolves one `{{name}}` token against the declared placeholders and the\n * fill-values record.\n *\n * @remarks\n * The single implementation of the token rule `fillTemplate` and\n * `Template#validate` both apply, so the two can never drift: the declared\n * {@link TemplatePlaceholder} sharing the token's `name` (exact match)\n * supplies its `path`, falling back to the token split on `.`; the value\n * resolves through `resolveSafeField`, so any segment in\n * `UNSAFE_FIELD_SEGMENTS` yields `undefined` without ever calling\n * `resolveField`; `required` is `true` for an undeclared token and for a\n * declared placeholder whose `required` is not `false`. The token is passed\n * already trimmed. `fallback` is not applied here — it is read from\n * `declared` by each caller, because `fill` substitutes it and `validate`\n * only counts it.\n *\n * @param record - The fill-values record the token resolves against\n * @param placeholders - The declared placeholders the token matches by name\n * @param token - The trimmed token text, without its `{{` / `}}` delimiters\n * @returns The {@link TemplateTokenResolution} for the token\n *\n * @example\n * ```ts\n * import { resolveToken } from '@src/core'\n *\n * resolveToken({ name: 'Ada' }, [], 'name').value // 'Ada'\n * resolveToken({}, [{ name: 'nickname', required: false }], 'nickname').required // false\n * ```\n */\nexport function resolveToken(\n\trecord: TemplateFillValues,\n\tplaceholders: readonly TemplatePlaceholder[],\n\ttoken: string,\n): TemplateTokenResolution {\n\tconst declared = placeholders.find((placeholder) => placeholder.name === token)\n\tconst path = declared?.path ?? token.split('.')\n\treturn {\n\t\tvalue: resolveSafeField(record, path),\n\t\tdeclared,\n\t\trequired: declared === undefined || declared.required !== false,\n\t}\n}\n\n/**\n * Substitutes every `{{name}}` token in `content` in a single pass.\n *\n * @remarks\n * Uses a fresh `RegExp` clone of `FILL_PATTERN` per call (never sharing its\n * `lastIndex`) and a single `String#replace` scan — substituted output is\n * never re-scanned. Each token resolves through `resolveToken`, the one rule\n * `Template#validate` also applies: the matching declared\n * {@link TemplatePlaceholder} (exact `name`) supplies its `path` (falling\n * back to the token split on `.`); any path segment in `UNSAFE_FIELD_SEGMENTS`\n * makes the token unresolved without ever calling `resolveField` (a\n * prototype-pollution guard). A resolved value formats through `formatValue`; an\n * unresolved value falls back to the placeholder's `fallback` when declared;\n * otherwise `options.missing` governs — `'literal'` re-emits the original\n * `{{name}}` text, `'empty'` emits `''`, and `'error'` emits `''` for every\n * token but collects every unresolved required token (an undeclared token, or\n * a declared token with `required !== false`) and throws one\n * {@link TemplateError} coded `MISSING` listing them all, in first-appearance\n * order, once the scan completes. An escaped `\\{{` emits a literal `{{`.\n *\n * Called with no declared `placeholders` and `{ missing: 'empty' }`, this is a\n * bare interpolation over `content` — every token resolves by dotted path\n * against the values record and every unresolved token emits `''`.\n * `FILL_PATTERN`'s token class (`[^{}]`) excludes `{`, so a token containing\n * `{` never matches and the surrounding `{{` stays literal.\n *\n * @param content - The template content carrying `{{name}}` tokens\n * @param values - The values tokens resolve against\n * @param options - `missing` (default `'error'`), `locale` (default `'en-US'`), and the declared `placeholders` (default none) tokens resolve against\n * @returns The substituted content\n *\n * @example\n * ```ts\n * import { fillTemplate } from '@src/core'\n *\n * fillTemplate('Hi {{name}}', { name: 'Ada' }) // 'Hi Ada'\n * fillTemplate('Limit {{limit}}', { limit: 5010 }, { missing: 'empty' }) // 'Limit 5,010'\n * ```\n */\nexport function fillTemplate(\n\tcontent: string,\n\tvalues?: TemplateFillValues,\n\toptions?: TemplateFillContext,\n): string {\n\tconst placeholders = options?.placeholders ?? []\n\tconst missing = options?.missing ?? DEFAULT_MISSING_POLICY\n\tconst locale = options?.locale ?? DEFAULT_LOCALE\n\tconst record = values ?? {}\n\n\tconst missingNames: string[] = []\n\n\tconst pattern = new RegExp(FILL_PATTERN.source, FILL_PATTERN.flags)\n\tconst result = content.replace(pattern, (matchText: string, rawToken: string | undefined) => {\n\t\tif (rawToken === undefined) return '{{'\n\t\tconst token = rawToken.trim()\n\n\t\tconst { value, declared, required } = resolveToken(record, placeholders, token)\n\n\t\tif (value !== undefined) return formatValue(value, locale)\n\t\tif (declared?.fallback !== undefined) return formatValue(declared.fallback, locale)\n\n\t\tif (missing === 'literal') return matchText\n\t\tif (missing === 'empty') return ''\n\n\t\tif (required && !missingNames.includes(token)) {\n\t\t\tmissingNames.push(token)\n\t\t}\n\t\treturn ''\n\t})\n\n\tif (missing === 'error' && missingNames.length > 0) {\n\t\tthrow new TemplateError(\n\t\t\t'MISSING',\n\t\t\t`Missing required placeholder(s): ${missingNames.join(', ')}`,\n\t\t\t{ missing: missingNames },\n\t\t)\n\t}\n\n\treturn result\n}\n","import type { ContractShape } from '@orkestrel/contract'\nimport type { TemplatePlaceholder } from './types.js'\nimport { objectShape, optionalShape, stringShape } from '@orkestrel/contract'\n\n// The templates shape-value inventory — every function here builds an\n// `@orkestrel/contract` shape from declared template data. Shapers sit above\n// the `helpers.ts` leaf pair: they consume it, and it never consumes them.\n\n/**\n * Builds the `@orkestrel/contract` object shape describing a template's\n * declared placeholders.\n *\n * @remarks\n * Each placeholder becomes a `stringShape` carrying its `description`;\n * `required === false` wraps it in `optionalShape`. Used by `Template` to\n * compile its `parameters()` contract once per instance.\n *\n * @param placeholders - The declared placeholders to shape\n * @returns The contract shape for `createContract`\n *\n * @example\n * ```ts\n * import { placeholderShape } from '@src/core'\n * import { createContract } from '@orkestrel/contract'\n *\n * const contract = createContract(placeholderShape([{ name: 'city' }]))\n * ```\n */\nexport function placeholderShape(placeholders: readonly TemplatePlaceholder[]): ContractShape {\n\tconst properties: Record<string, ContractShape> = {}\n\tfor (const placeholder of placeholders) {\n\t\tconst description = placeholder.description\n\t\tconst field = stringShape({\n\t\t\t...(description !== undefined ? { description } : {}),\n\t\t})\n\t\tproperties[placeholder.name] = placeholder.required === false ? optionalShape(field) : field\n\t}\n\treturn objectShape(properties)\n}\n","import type { ContractInterface } from '@orkestrel/contract'\nimport type {\n\tMissingPolicy,\n\tTemplateDefinition,\n\tTemplateFillOptions,\n\tTemplateFillValues,\n\tTemplateInterface,\n\tTemplateOptions,\n\tTemplatePlaceholder,\n\tTemplateValidationResult,\n} from '../types.js'\nimport { createContract, isArray, isString, schemaToParameters } from '@orkestrel/contract'\nimport { DEFAULT_LOCALE, DEFAULT_MISSING_POLICY, FILL_PATTERN } from '../constants.js'\nimport { fillTemplate, resolveToken } from '../helpers.js'\nimport { placeholderShape } from '../shapers.js'\nimport { TemplateError } from '../errors.js'\n\n/**\n * Represents a named, versionable template — `{{name}}` tokens in `content`,\n * filled against a values record — implementing `TemplateInterface` exactly.\n *\n * @remarks\n * `missing` / `locale` seed this instance's default {@link TemplateFillOptions},\n * overridable per `fill` call. Its `parameters()` contract (built from\n * `placeholders` through `placeholderShape`) compiles once, in the constructor.\n *\n * @throws {@link TemplateError} Thrown when `options.placeholders` declares a duplicate `name` or an empty `path` (coded `INVALID`)\n *\n * @example\n * ```ts\n * const greeting = new Template({ name: 'greeting', content: 'Hi {{name}}' })\n * greeting.fill({ name: 'Ada' }) // 'Hi Ada'\n * ```\n */\nexport class Template implements TemplateInterface {\n\treadonly #missing: MissingPolicy\n\treadonly #locale: string\n\treadonly #contract: ContractInterface<unknown>\n\treadonly id: string\n\treadonly name: string\n\treadonly content: string\n\treadonly placeholders: readonly TemplatePlaceholder[]\n\treadonly summary?: string\n\treadonly description?: string\n\treadonly category?: string\n\treadonly tags?: readonly string[]\n\n\tconstructor(options: TemplateOptions) {\n\t\tconst placeholders = options.placeholders ?? []\n\t\tIterator.from(placeholders).reduce<readonly string[]>((names, placeholder) => {\n\t\t\tif (names.includes(placeholder.name)) {\n\t\t\t\tthrow new TemplateError('INVALID', `Duplicate placeholder name: ${placeholder.name}`, {\n\t\t\t\t\tname: placeholder.name,\n\t\t\t\t})\n\t\t\t}\n\t\t\tconst name = placeholder.name\n\t\t\tif (isArray(placeholder.path) && placeholder.path.length === 0) {\n\t\t\t\tthrow new TemplateError(\n\t\t\t\t\t'INVALID',\n\t\t\t\t\t`Placeholder path must not be empty: ${placeholder.name}`,\n\t\t\t\t\t{ name: placeholder.name },\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn [...names, name]\n\t\t}, [])\n\n\t\tthis.id = isString(options.id) ? options.id : crypto.randomUUID()\n\t\tthis.name = options.name\n\t\tthis.content = options.content\n\t\tthis.placeholders = placeholders\n\t\tif (options.summary !== undefined) this.summary = options.summary\n\t\tif (options.description !== undefined) this.description = options.description\n\t\tif (options.category !== undefined) this.category = options.category\n\t\tif (options.tags !== undefined) this.tags = options.tags\n\t\tthis.#missing = options.missing ?? DEFAULT_MISSING_POLICY\n\t\tthis.#locale = options.locale ?? DEFAULT_LOCALE\n\t\tthis.#contract = createContract(placeholderShape(this.placeholders))\n\t}\n\n\t/**\n\t * Returns the plain, JSON-serializable data this template carries.\n\t *\n\t * @returns The {@link TemplateDefinition} record\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({ name: 'greeting', content: 'Hi {{name}}' })\n\t * instance.definition().name // 'greeting'\n\t * ```\n\t */\n\tdefinition(): TemplateDefinition {\n\t\treturn {\n\t\t\tid: this.id,\n\t\t\tname: this.name,\n\t\t\tcontent: this.content,\n\t\t\tplaceholders: this.placeholders,\n\t\t\t...(this.summary !== undefined ? { summary: this.summary } : {}),\n\t\t\t...(this.description !== undefined ? { description: this.description } : {}),\n\t\t\t...(this.category !== undefined ? { category: this.category } : {}),\n\t\t\t...(this.tags !== undefined ? { tags: this.tags } : {}),\n\t\t}\n\t}\n\n\t/**\n\t * Substitutes every `{{name}}` token in `content` against `values`.\n\t *\n\t * @param values - The values tokens resolve against\n\t * @param options - Per-call overrides for this instance's `missing` / `locale` defaults\n\t * @returns The substituted content\n\t * @throws {@link TemplateError} Thrown when a required placeholder stays unresolved under the `'error'` policy (coded `MISSING`)\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({ name: 'greeting', content: 'Hi {{name}}' })\n\t * instance.fill({ name: 'Ada' }) // 'Hi Ada'\n\t * ```\n\t */\n\tfill(values?: TemplateFillValues, options?: TemplateFillOptions): string {\n\t\treturn fillTemplate(this.content, values, {\n\t\t\tmissing: options?.missing ?? this.#missing,\n\t\t\tlocale: options?.locale ?? this.#locale,\n\t\t\tplaceholders: this.placeholders,\n\t\t})\n\t}\n\n\t/**\n\t * Reports which required placeholders would stay unresolved, and which\n\t * `values` keys go unused, without producing output.\n\t *\n\t * @remarks\n\t * Content-token driven: scans `this.content` for every `{{name}}` token\n\t * (skipping escaped `\\{{` matches) the same way `fill` does, so `validate`\n\t * predicts `fill`'s `'error'`-{@link MissingPolicy} outcome exactly — a\n\t * token reported here as missing is precisely a token that would throw\n\t * under `fill(values, { missing: 'error' })`. For each distinct token\n\t * (first-appearance order, trimmed): `resolveToken` applies the one shared\n\t * token rule `fill` also applies — a declared {@link TemplatePlaceholder}\n\t * sharing its `name` supplies `path` (falling back to the token split on\n\t * `.`), and the value resolves through `resolveSafeField`. The token is `missing`\n\t * only when the value is unresolved, no `fallback` is declared, and the\n\t * placeholder is required (`required !== false`, including undeclared\n\t * tokens). `extra` lists every `values` key with no declared placeholder.\n\t *\n\t * @param values - The values to check\n\t * @returns The {@link TemplateValidationResult}\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({\n\t * \tname: 'greeting',\n\t * \tcontent: 'Hi {{name}}',\n\t * \tplaceholders: [{ name: 'name' }],\n\t * })\n\t * instance.validate({}).missing // ['name']\n\t * ```\n\t */\n\tvalidate(values?: TemplateFillValues): TemplateValidationResult {\n\t\tconst record = values ?? {}\n\t\tconst pattern = new RegExp(FILL_PATTERN.source, FILL_PATTERN.flags)\n\t\tconst tokens = new Set(\n\t\t\tArray.from(this.content.matchAll(pattern)).flatMap((match) =>\n\t\t\t\tmatch[1] === undefined ? [] : [match[1].trim()],\n\t\t\t),\n\t\t)\n\t\tconst missing = [...tokens].filter((token) => {\n\t\t\tconst { value, declared, required } = resolveToken(record, this.placeholders, token)\n\t\t\treturn value === undefined && declared?.fallback === undefined && required\n\t\t})\n\n\t\tconst declaredNames = new Set(this.placeholders.map((placeholder) => placeholder.name))\n\t\tconst extra = Object.keys(record).filter((key) => !declaredNames.has(key))\n\n\t\treturn { valid: missing.length === 0, missing, extra }\n\t}\n\n\t/**\n\t * Projects this template's placeholders to the open tool-parameters record\n\t * shape.\n\t *\n\t * @returns The compiled parameters record, or `undefined` when `schemaToParameters` yields none\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({\n\t * \tname: 'greeting',\n\t * \tcontent: 'Hi {{name}}',\n\t * \tplaceholders: [{ name: 'name' }],\n\t * })\n\t * instance.parameters()\n\t * ```\n\t */\n\tparameters(): Readonly<Record<string, unknown>> | undefined {\n\t\treturn schemaToParameters(this.#contract.schema)\n\t}\n}\n","import type {\n\tMissingPolicy,\n\tTemplateFillValues,\n\tTemplateFillOptions,\n\tTemplateInterface,\n\tTemplateManagerEventMap,\n\tTemplateManagerInterface,\n\tTemplateManagerOptions,\n\tTemplateOptions,\n\tTemplateQuery,\n\tTemplateRegisterOptions,\n\tTemplateValidationResult,\n} from '../types.js'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport { isFunction, isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport { DEFAULT_LOCALE, DEFAULT_MISSING_POLICY } from '../constants.js'\nimport { TemplateError } from '../errors.js'\nimport { Template } from './Template.js'\n\n/**\n * Represents the template registry — a self-owning, id-keyed record-holder for\n * the {@link TemplateInterface} instances a consumer registers, looks up,\n * fills, and validates by id — implementing `TemplateManagerInterface`\n * exactly.\n *\n * @remarks\n * Singular and plural accessors, the batch `remove` overloads, and ownership\n * of the emitter all sit here. `register` accepts either a constructed {@link TemplateInterface} (kept\n * as-is, including its own `missing` / `locale` defaults) or a plain\n * {@link TemplateOptions} bag — constructed into a `Template` with this\n * manager's `missing` / `locale` defaults applied wherever the bag omits\n * them. A duplicate `id` throws a {@link TemplateError} coded `CONFLICT`\n * unless `options.replace` is `true`, in which case the existing entry is\n * overwritten. `options.templates` seeds the registry at construction without\n * emitting `register` — only calls to `register` after construction emit.\n * The batch `remove(ids)` form removes every present id and returns\n * `true` only when every listed id was present.\n *\n * @example\n * ```ts\n * import { TemplateManager } from '@src/core'\n *\n * const manager = new TemplateManager()\n * const instance = manager.register({ name: 'greeting', content: 'Hi {{name}}' })\n * manager.fill(instance.id, { name: 'Ada' }) // 'Hi Ada'\n * ```\n */\nexport class TemplateManager implements TemplateManagerInterface {\n\treadonly #templates = new Map<string, TemplateInterface>()\n\treadonly #emitter: Emitter<TemplateManagerEventMap>\n\treadonly #missing: MissingPolicy\n\treadonly #locale: string\n\n\tconstructor(options?: TemplateManagerOptions) {\n\t\tconst on = options?.on\n\t\tconst error = options?.error\n\t\tthis.#emitter = new Emitter<TemplateManagerEventMap>({\n\t\t\t...(on !== undefined ? { on } : {}),\n\t\t\t...(error !== undefined ? { error } : {}),\n\t\t})\n\t\tthis.#missing = options?.missing ?? DEFAULT_MISSING_POLICY\n\t\tthis.#locale = options?.locale ?? DEFAULT_LOCALE\n\t\tfor (const template of options?.templates ?? []) {\n\t\t\tconst instance = this.#instantiate(template)\n\t\t\tthis.#templates.set(instance.id, instance)\n\t\t}\n\t}\n\n\tget emitter(): EmitterInterface<TemplateManagerEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget count(): number {\n\t\treturn this.#templates.size\n\t}\n\n\t/**\n\t * Registers a template — a constructed {@link TemplateInterface} (kept\n\t * as-is) or a plain {@link TemplateOptions} bag (constructed into a\n\t * `Template` with this manager's `missing` / `locale` defaults applied\n\t * wherever the bag omits them).\n\t *\n\t * @param template - The template instance or options to register\n\t * @param options - `replace` — overwrite an existing entry sharing the same id instead of throwing\n\t * @returns The registered {@link TemplateInterface}\n\t * @throws {@link TemplateError} Thrown when the id is already registered and `options.replace` is not `true` (coded `CONFLICT`), or when an options bag declares a duplicate placeholder `name` or an empty `path` (coded `INVALID`)\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = manager.register({ id: 'greeting', name: 'greeting', content: 'Hi {{name}}' })\n\t * ```\n\t */\n\tregister(\n\t\ttemplate: TemplateInterface | TemplateOptions,\n\t\toptions?: TemplateRegisterOptions,\n\t): TemplateInterface {\n\t\tconst instance = this.#instantiate(template)\n\t\tconst existing = this.#templates.get(instance.id)\n\t\tif (existing !== undefined && options?.replace !== true) {\n\t\t\tthrow new TemplateError('CONFLICT', `Template already registered: ${instance.id}`, {\n\t\t\t\tid: instance.id,\n\t\t\t})\n\t\t}\n\t\tthis.#templates.set(instance.id, instance)\n\t\tthis.#emitter.emit('register', instance)\n\t\treturn instance\n\t}\n\n\t/**\n\t * Returns one registered {@link TemplateInterface} by id.\n\t *\n\t * @param id - The template id\n\t * @returns The registered {@link TemplateInterface}, or `undefined` when `id` is unregistered\n\t */\n\ttemplate(id: string): TemplateInterface | undefined {\n\t\treturn this.#templates.get(id)\n\t}\n\n\t/**\n\t * Lists every registered template.\n\t *\n\t * @returns A snapshot array of every registered {@link TemplateInterface}\n\t */\n\ttemplates(): readonly TemplateInterface[] {\n\t\treturn [...this.#templates.values()]\n\t}\n\n\t/**\n\t * Filters registered templates by `name`, `category`, and `tag` — every\n\t * supplied field must match.\n\t *\n\t * @param query - The {@link TemplateQuery} to filter by; omit for every registered template\n\t * @returns The matching templates\n\t */\n\tfind(query?: TemplateQuery): readonly TemplateInterface[] {\n\t\tif (query === undefined) return this.templates()\n\t\treturn this.templates().filter((instance) => {\n\t\t\tif (query.name !== undefined && instance.name !== query.name) return false\n\t\t\tif (query.category !== undefined && instance.category !== query.category) return false\n\t\t\tif (query.tag !== undefined && !(instance.tags ?? []).includes(query.tag)) return false\n\t\t\treturn true\n\t\t})\n\t}\n\n\t/**\n\t * Tests whether a template id is registered.\n\t *\n\t * @param id - The template id\n\t * @returns True if `id` is registered; false otherwise\n\t */\n\thas(id: string): boolean {\n\t\treturn this.#templates.has(id)\n\t}\n\n\t/**\n\t * Removes one, several, or every registered template.\n\t *\n\t * @remarks\n\t * `remove()` removes every registered template, emitting `remove` once per\n\t * instance. `remove(id)` removes one template by id, emitting `remove` and\n\t * returning `true` when it existed, `false` otherwise. `remove(ids)`\n\t * removes every listed id that is present, emitting `remove` once per\n\t * removed instance, and returns `true` only when every listed id was\n\t * present.\n\t *\n\t * @param target - Omit to remove all, a single id, or a list of ids\n\t * @returns `boolean` for the single-id / list-of-ids forms; `void` for the remove-all form\n\t */\n\t// `readonly string[]` is not assignable to `id: string`, so a list resolves to the\n\t// batch signature whatever order the signatures are declared in.\n\tremove(ids: readonly string[]): boolean\n\tremove(id: string): boolean\n\tremove(): void\n\tremove(target?: string | readonly string[]): boolean | void {\n\t\tif (target === undefined) {\n\t\t\tfor (const instance of this.#templates.values()) this.#emitter.emit('remove', instance)\n\t\t\tthis.#templates.clear()\n\t\t\treturn\n\t\t}\n\t\tif (isString(target)) {\n\t\t\tconst instance = this.#templates.get(target)\n\t\t\tif (instance === undefined) return false\n\t\t\tthis.#templates.delete(target)\n\t\t\tthis.#emitter.emit('remove', instance)\n\t\t\treturn true\n\t\t}\n\t\treturn this.#remove(target).reduce((all, removed) => removed && all, true)\n\t}\n\n\t/** Removes every registered template, emitting `clear`. */\n\tclear(): void {\n\t\tthis.#templates.clear()\n\t\tthis.#emitter.emit('clear')\n\t}\n\n\t/**\n\t * Tears down the registry: drops every registered template and destroys the\n\t * owned emitter. Idempotent.\n\t *\n\t * @remarks\n\t * Teardown is not an observable registry operation and the emitter is being\n\t * released, so this emits neither `clear` nor `remove`. The emitter is torn\n\t * down last, after the registry is dropped.\n\t *\n\t * @example\n\t * ```ts\n\t * const manager = new TemplateManager()\n\t * manager.destroy()\n\t * manager.emitter.destroyed // true\n\t * ```\n\t */\n\tdestroy(): void {\n\t\tthis.#templates.clear()\n\t\tthis.#emitter.destroy()\n\t}\n\n\t/**\n\t * Fills a registered template by id.\n\t *\n\t * @param id - The template id\n\t * @param values - The values tokens resolve against\n\t * @param options - Per-call overrides for the template's `missing` / `locale` defaults\n\t * @returns The substituted content\n\t * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown\n\t * @throws {@link TemplateError} Thrown when a required placeholder stays unresolved under the `'error'` policy (coded `MISSING`)\n\t */\n\tfill(id: string, values?: TemplateFillValues, options?: TemplateFillOptions): string {\n\t\treturn this.#require(id).fill(values, options)\n\t}\n\n\t/**\n\t * Validates values against a registered template by id.\n\t *\n\t * @param id - The template id\n\t * @param values - The values to check\n\t * @returns The {@link TemplateValidationResult}\n\t * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown\n\t */\n\tvalidate(id: string, values?: TemplateFillValues): TemplateValidationResult {\n\t\treturn this.#require(id).validate(values)\n\t}\n\n\t/**\n\t * Projects a registered template's parameters by id.\n\t *\n\t * @param id - The template id\n\t * @returns The compiled parameters record, or `undefined` when the template has none\n\t * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown\n\t */\n\tparameters(id: string): Readonly<Record<string, unknown>> | undefined {\n\t\treturn this.#require(id).parameters()\n\t}\n\n\t#instantiate(template: TemplateInterface | TemplateOptions): TemplateInterface {\n\t\tif (this.#isInstance(template)) return template\n\t\treturn new Template({\n\t\t\t...template,\n\t\t\tmissing: template.missing ?? this.#missing,\n\t\t\tlocale: template.locale ?? this.#locale,\n\t\t})\n\t}\n\n\t*#remove(target: readonly string[]): Generator<boolean> {\n\t\tfor (const id of target) {\n\t\t\tconst instance = this.#templates.get(id)\n\t\t\tif (instance === undefined) {\n\t\t\t\tyield false\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tthis.#templates.delete(id)\n\t\t\tthis.#emitter.emit('remove', instance)\n\t\t\tyield true\n\t\t}\n\t}\n\n\t// A TemplateOptions bag is plain data with no `fill` / `validate` /\n\t// `parameters` methods; a TemplateInterface instance always exposes all three.\n\t#isInstance(template: TemplateInterface | TemplateOptions): template is TemplateInterface {\n\t\treturn (\n\t\t\t'fill' in template &&\n\t\t\tisFunction(template.fill) &&\n\t\t\t'validate' in template &&\n\t\t\tisFunction(template.validate) &&\n\t\t\t'parameters' in template &&\n\t\t\tisFunction(template.parameters)\n\t\t)\n\t}\n\n\t// Every by-id operation that needs a template to proceed shares this lookup.\n\t// The `template` accessor deliberately does not, and returns `undefined`.\n\t#require(id: string): TemplateInterface {\n\t\tconst instance = this.#templates.get(id)\n\t\tif (instance === undefined) {\n\t\t\tthrow new TemplateError('NOTFOUND', `Unknown template id: ${id}`, { id })\n\t\t}\n\t\treturn instance\n\t}\n}\n","import type {\n\tTemplateInterface,\n\tTemplateManagerInterface,\n\tTemplateManagerOptions,\n\tTemplateOptions,\n} from './types.js'\nimport { Template } from './templates/Template.js'\nimport { TemplateManager } from './templates/TemplateManager.js'\n\n/**\n * Creates a working {@link TemplateInterface} from a {@link TemplateOptions}\n * bag, backed by the `Template` class.\n *\n * @param options - The template's `name` / `content`, an optional `id`\n * (defaults to a generated UUID), `placeholders`, catalog metadata, and\n * `missing` / `locale` fill defaults\n * @returns A working {@link TemplateInterface}\n * @throws {@link TemplateError} Thrown when `options.placeholders` declares a duplicate `name` or an empty `path` (coded `INVALID`)\n *\n * @example Create a template and a registry\n * ```ts\n * import { createTemplate, createTemplateManager } from '@orkestrel/template'\n *\n * const greeting = createTemplate({ name: 'greeting', content: 'Hi {{name}}' })\n * greeting.fill({ name: 'Ada' }) // 'Hi Ada'\n *\n * const templates = createTemplateManager({\n * \ttemplates: [\n * \t\t{ id: 'greeting', name: 'greeting', content: 'Hi {{name}}', category: 'mail' },\n * \t\t{ id: 'farewell', name: 'farewell', content: 'Bye {{name}}', category: 'mail' },\n * \t\t{ id: 'alert', name: 'alert', content: 'Alert: {{reason}}', category: 'ops' },\n * \t],\n * })\n * templates.fill('greeting', { name: 'Ada' }) // 'Hi Ada'\n * templates.find({ category: 'mail' }).map((one) => one.id) // ['greeting', 'farewell']\n * templates.has('alert') // true\n * templates.has('missing') // false\n * ```\n */\nexport function createTemplate(options: TemplateOptions): TemplateInterface {\n\treturn new Template(options)\n}\n\n/**\n * Creates a working {@link TemplateManagerInterface}, optionally seeded with\n * the templates the options carry, backed by the `TemplateManager` class.\n *\n * @param options - Optional initial `templates` seed collection and\n * manager-wide `missing` / `locale` fill defaults, emitter `on` hooks, and\n * an `error` handler\n * @returns A working {@link TemplateManagerInterface}\n * @throws {@link TemplateError} Thrown when a seeded `options.templates` bag declares a duplicate placeholder `name` or an empty `path` (coded `INVALID`)\n *\n * @example\n * ```ts\n * import { createTemplateManager } from '@src/core'\n *\n * const templates = createTemplateManager({\n * \ttemplates: [{ id: 'greeting', name: 'greeting', content: 'Hi {{name}}' }],\n * })\n * templates.fill('greeting', { name: 'Ada' }) // 'Hi Ada'\n * ```\n */\nexport function createTemplateManager(options?: TemplateManagerOptions): TemplateManagerInterface {\n\treturn new TemplateManager(options)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyBA,IAAa,eAAe;;;;;AAM5B,IAAa,yBAAwC;;;;;AAMrD,IAAa,iBAAiB;;;;;;AAO9B,IAAa,wBAA2C,OAAO,OAAO;CACrE;CACA;CACA;AACD,CAAC;;;;;;;;;;;;;;AC9BD,IAAa,gBAAb,cAAmC,MAAM;CACxC;CACA;CAEA,YACC,MACA,SACA,SACC;EACD,MAAM,OAAO;EACb,oBAAA,WAAW,OAAO,MAAM,OAAO,IAAI,mCAAmC,GAAG,EAAE,OAAO,KAAK,CAAC;EACxF,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,IAAI,YAAY,KAAA,GAAW,KAAK,UAAU;CAC3C;AACD;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,gBAAgB,OAAwC;CACvE,QAAA,GAAOA,oBAAAA,MAAAA,OAAY;EAClB,IAAI,EAAE,iBAAiB,UAAUC,oBAAAA,WAAW,UAAU,KAAK,MAAM,MAAM,WAAW,OAAO;EACzF,IACCA,oBAAAA,WAAW,SAAS,OAAO,OAAO,IAAI,mCAAmC,CAAC,CAAC,EAAE,UAAU,OAEvF,OAAO;EACR,MAAM,OAAgBA,oBAAAA,WAAW,QAAQ,KAAK,OAAO,MAAM;EAC3D,MAAM,UAAmBA,oBAAAA,WAAW,QAAQ,KAAK,OAAO,SAAS;EACjE,QACE,SAAS,aAAa,SAAS,cAAc,SAAS,aAAa,SAAS,gBAC5E,YAAY,KAAA,MAAA,GAAaC,oBAAAA,SAAAA,CAAS,OAAO,MAAA,GAAKC,oBAAAA,WAAAA,CAAW,OAAO;CAEnE,CAAC;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;ACxBA,SAAgB,YAAY,OAAgB,QAAwB;CACnE,KAAA,GAAIC,oBAAAA,eAAAA,CAAe,KAAK,GAAG,OAAO,MAAM,eAAe,MAAM;CAC7D,OAAO,OAAO,KAAK;AACpB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,iBAAiB,QAA4B,MAA0B;CAEtF,MAAA,GADiBC,oBAAAA,QAAAA,CAAQ,IAAI,IAAI,OAAO,CAAC,IAAI,EAAA,CAChC,MAAM,YAAY,sBAAsB,SAAS,OAAO,CAAC,GAAG,OAAO,KAAA;CAChF,QAAA,GAAOC,oBAAAA,aAAAA,CAAa,QAAQ,IAAI;AACjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,aACf,QACA,cACA,OAC0B;CAC1B,MAAM,WAAW,aAAa,MAAM,gBAAgB,YAAY,SAAS,KAAK;CAE9E,OAAO;EACN,OAAO,iBAAiB,QAFZ,UAAU,QAAQ,MAAM,MAAM,GAAG,CAET;EACpC;EACA,UAAU,aAAa,KAAA,KAAa,SAAS,aAAa;CAC3D;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,aACf,SACA,QACA,SACS;CACT,MAAM,eAAe,SAAS,gBAAgB,CAAC;CAC/C,MAAM,UAAU,SAAS,WAAA;CACzB,MAAM,SAAS,SAAS,UAAA;CACxB,MAAM,SAAS,UAAU,CAAC;CAE1B,MAAM,eAAyB,CAAC;CAEhC,MAAM,UAAU,IAAI,OAAO,aAAa,QAAQ,aAAa,KAAK;CAClE,MAAM,SAAS,QAAQ,QAAQ,UAAU,WAAmB,aAAiC;EAC5F,IAAI,aAAa,KAAA,GAAW,OAAO;EACnC,MAAM,QAAQ,SAAS,KAAK;EAE5B,MAAM,EAAE,OAAO,UAAU,aAAa,aAAa,QAAQ,cAAc,KAAK;EAE9E,IAAI,UAAU,KAAA,GAAW,OAAO,YAAY,OAAO,MAAM;EACzD,IAAI,UAAU,aAAa,KAAA,GAAW,OAAO,YAAY,SAAS,UAAU,MAAM;EAElF,IAAI,YAAY,WAAW,OAAO;EAClC,IAAI,YAAY,SAAS,OAAO;EAEhC,IAAI,YAAY,CAAC,aAAa,SAAS,KAAK,GAC3C,aAAa,KAAK,KAAK;EAExB,OAAO;CACR,CAAC;CAED,IAAI,YAAY,WAAW,aAAa,SAAS,GAChD,MAAM,IAAI,cACT,WACA,oCAAoC,aAAa,KAAK,IAAI,KAC1D,EAAE,SAAS,aAAa,CACzB;CAGD,OAAO;AACR;;;;;;;;;;;;;;;;;;;;;;AClLA,SAAgB,iBAAiB,cAA6D;CAC7F,MAAM,aAA4C,CAAC;CACnD,KAAK,MAAM,eAAe,cAAc;EACvC,MAAM,cAAc,YAAY;EAChC,MAAM,SAAA,GAAQC,oBAAAA,YAAAA,CAAY,EACzB,GAAI,gBAAgB,KAAA,IAAY,EAAE,YAAY,IAAI,CAAC,EACpD,CAAC;EACD,WAAW,YAAY,QAAQ,YAAY,aAAa,SAAA,GAAQC,oBAAAA,cAAAA,CAAc,KAAK,IAAI;CACxF;CACA,QAAA,GAAOC,oBAAAA,YAAAA,CAAY,UAAU;AAC9B;;;;;;;;;;;;;;;;;;;ACJA,IAAa,WAAb,MAAmD;CAClD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,SAA0B;EACrC,MAAM,eAAe,QAAQ,gBAAgB,CAAC;EAC9C,SAAS,KAAK,YAAY,CAAC,CAAC,QAA2B,OAAO,gBAAgB;GAC7E,IAAI,MAAM,SAAS,YAAY,IAAI,GAClC,MAAM,IAAI,cAAc,WAAW,+BAA+B,YAAY,QAAQ,EACrF,MAAM,YAAY,KACnB,CAAC;GAEF,MAAM,OAAO,YAAY;GACzB,KAAA,GAAIC,oBAAAA,QAAAA,CAAQ,YAAY,IAAI,KAAK,YAAY,KAAK,WAAW,GAC5D,MAAM,IAAI,cACT,WACA,uCAAuC,YAAY,QACnD,EAAE,MAAM,YAAY,KAAK,CAC1B;GAED,OAAO,CAAC,GAAG,OAAO,IAAI;EACvB,GAAG,CAAC,CAAC;EAEL,KAAK,MAAA,GAAKC,oBAAAA,SAAAA,CAAS,QAAQ,EAAE,IAAI,QAAQ,KAAK,OAAO,WAAW;EAChE,KAAK,OAAO,QAAQ;EACpB,KAAK,UAAU,QAAQ;EACvB,KAAK,eAAe;EACpB,IAAI,QAAQ,YAAY,KAAA,GAAW,KAAK,UAAU,QAAQ;EAC1D,IAAI,QAAQ,gBAAgB,KAAA,GAAW,KAAK,cAAc,QAAQ;EAClE,IAAI,QAAQ,aAAa,KAAA,GAAW,KAAK,WAAW,QAAQ;EAC5D,IAAI,QAAQ,SAAS,KAAA,GAAW,KAAK,OAAO,QAAQ;EACpD,KAAK,WAAW,QAAQ,WAAA;EACxB,KAAK,UAAU,QAAQ,UAAA;EACvB,KAAK,aAAA,GAAYC,oBAAAA,eAAAA,CAAe,iBAAiB,KAAK,YAAY,CAAC;CACpE;;;;;;;;;;;;CAaA,aAAiC;EAChC,OAAO;GACN,IAAI,KAAK;GACT,MAAM,KAAK;GACX,SAAS,KAAK;GACd,cAAc,KAAK;GACnB,GAAI,KAAK,YAAY,KAAA,IAAY,EAAE,SAAS,KAAK,QAAQ,IAAI,CAAC;GAC9D,GAAI,KAAK,gBAAgB,KAAA,IAAY,EAAE,aAAa,KAAK,YAAY,IAAI,CAAC;GAC1E,GAAI,KAAK,aAAa,KAAA,IAAY,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;GACjE,GAAI,KAAK,SAAS,KAAA,IAAY,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;EACtD;CACD;;;;;;;;;;;;;;;CAgBA,KAAK,QAA6B,SAAuC;EACxE,OAAO,aAAa,KAAK,SAAS,QAAQ;GACzC,SAAS,SAAS,WAAW,KAAK;GAClC,QAAQ,SAAS,UAAU,KAAK;GAChC,cAAc,KAAK;EACpB,CAAC;CACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCA,SAAS,QAAuD;EAC/D,MAAM,SAAS,UAAU,CAAC;EAC1B,MAAM,UAAU,IAAI,OAAO,aAAa,QAAQ,aAAa,KAAK;EAMlE,MAAM,UAAU,CAAC,GAAG,IALD,IAClB,MAAM,KAAK,KAAK,QAAQ,SAAS,OAAO,CAAC,CAAC,CAAC,SAAS,UACnD,MAAM,OAAO,KAAA,IAAY,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAC/C,CAEmB,CAAM,CAAC,CAAC,QAAQ,UAAU;GAC7C,MAAM,EAAE,OAAO,UAAU,aAAa,aAAa,QAAQ,KAAK,cAAc,KAAK;GACnF,OAAO,UAAU,KAAA,KAAa,UAAU,aAAa,KAAA,KAAa;EACnE,CAAC;EAED,MAAM,gBAAgB,IAAI,IAAI,KAAK,aAAa,KAAK,gBAAgB,YAAY,IAAI,CAAC;EACtF,MAAM,QAAQ,OAAO,KAAK,MAAM,CAAC,CAAC,QAAQ,QAAQ,CAAC,cAAc,IAAI,GAAG,CAAC;EAEzE,OAAO;GAAE,OAAO,QAAQ,WAAW;GAAG;GAAS;EAAM;CACtD;;;;;;;;;;;;;;;;;CAkBA,aAA4D;EAC3D,QAAA,GAAOC,oBAAAA,mBAAAA,CAAmB,KAAK,UAAU,MAAM;CAChD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AClJA,IAAa,kBAAb,MAAiE;CAChE,6BAAsB,IAAI,IAA+B;CACzD;CACA;CACA;CAEA,YAAY,SAAkC;EAC7C,MAAM,KAAK,SAAS;EACpB,MAAM,QAAQ,SAAS;EACvB,KAAK,WAAW,IAAIC,mBAAAA,QAAiC;GACpD,GAAI,OAAO,KAAA,IAAY,EAAE,GAAG,IAAI,CAAC;GACjC,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;EACxC,CAAC;EACD,KAAK,WAAW,SAAS,WAAA;EACzB,KAAK,UAAU,SAAS,UAAA;EACxB,KAAK,MAAM,YAAY,SAAS,aAAa,CAAC,GAAG;GAChD,MAAM,WAAW,KAAK,aAAa,QAAQ;GAC3C,KAAK,WAAW,IAAI,SAAS,IAAI,QAAQ;EAC1C;CACD;CAEA,IAAI,UAAqD;EACxD,OAAO,KAAK;CACb;CAEA,IAAI,QAAgB;EACnB,OAAO,KAAK,WAAW;CACxB;;;;;;;;;;;;;;;;;CAkBA,SACC,UACA,SACoB;EACpB,MAAM,WAAW,KAAK,aAAa,QAAQ;EAE3C,IADiB,KAAK,WAAW,IAAI,SAAS,EAC1C,MAAa,KAAA,KAAa,SAAS,YAAY,MAClD,MAAM,IAAI,cAAc,YAAY,gCAAgC,SAAS,MAAM,EAClF,IAAI,SAAS,GACd,CAAC;EAEF,KAAK,WAAW,IAAI,SAAS,IAAI,QAAQ;EACzC,KAAK,SAAS,KAAK,YAAY,QAAQ;EACvC,OAAO;CACR;;;;;;;CAQA,SAAS,IAA2C;EACnD,OAAO,KAAK,WAAW,IAAI,EAAE;CAC9B;;;;;;CAOA,YAA0C;EACzC,OAAO,CAAC,GAAG,KAAK,WAAW,OAAO,CAAC;CACpC;;;;;;;;CASA,KAAK,OAAqD;EACzD,IAAI,UAAU,KAAA,GAAW,OAAO,KAAK,UAAU;EAC/C,OAAO,KAAK,UAAU,CAAC,CAAC,QAAQ,aAAa;GAC5C,IAAI,MAAM,SAAS,KAAA,KAAa,SAAS,SAAS,MAAM,MAAM,OAAO;GACrE,IAAI,MAAM,aAAa,KAAA,KAAa,SAAS,aAAa,MAAM,UAAU,OAAO;GACjF,IAAI,MAAM,QAAQ,KAAA,KAAa,EAAE,SAAS,QAAQ,CAAC,EAAA,CAAG,SAAS,MAAM,GAAG,GAAG,OAAO;GAClF,OAAO;EACR,CAAC;CACF;;;;;;;CAQA,IAAI,IAAqB;EACxB,OAAO,KAAK,WAAW,IAAI,EAAE;CAC9B;CAqBA,OAAO,QAAqD;EAC3D,IAAI,WAAW,KAAA,GAAW;GACzB,KAAK,MAAM,YAAY,KAAK,WAAW,OAAO,GAAG,KAAK,SAAS,KAAK,UAAU,QAAQ;GACtF,KAAK,WAAW,MAAM;GACtB;EACD;EACA,KAAA,GAAIC,oBAAAA,SAAAA,CAAS,MAAM,GAAG;GACrB,MAAM,WAAW,KAAK,WAAW,IAAI,MAAM;GAC3C,IAAI,aAAa,KAAA,GAAW,OAAO;GACnC,KAAK,WAAW,OAAO,MAAM;GAC7B,KAAK,SAAS,KAAK,UAAU,QAAQ;GACrC,OAAO;EACR;EACA,OAAO,KAAK,QAAQ,MAAM,CAAC,CAAC,QAAQ,KAAK,YAAY,WAAW,KAAK,IAAI;CAC1E;;CAGA,QAAc;EACb,KAAK,WAAW,MAAM;EACtB,KAAK,SAAS,KAAK,OAAO;CAC3B;;;;;;;;;;;;;;;;;CAkBA,UAAgB;EACf,KAAK,WAAW,MAAM;EACtB,KAAK,SAAS,QAAQ;CACvB;;;;;;;;;;;CAYA,KAAK,IAAY,QAA6B,SAAuC;EACpF,OAAO,KAAK,SAAS,EAAE,CAAC,CAAC,KAAK,QAAQ,OAAO;CAC9C;;;;;;;;;CAUA,SAAS,IAAY,QAAuD;EAC3E,OAAO,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,MAAM;CACzC;;;;;;;;CASA,WAAW,IAA2D;EACrE,OAAO,KAAK,SAAS,EAAE,CAAC,CAAC,WAAW;CACrC;CAEA,aAAa,UAAkE;EAC9E,IAAI,KAAK,YAAY,QAAQ,GAAG,OAAO;EACvC,OAAO,IAAI,SAAS;GACnB,GAAG;GACH,SAAS,SAAS,WAAW,KAAK;GAClC,QAAQ,SAAS,UAAU,KAAK;EACjC,CAAC;CACF;CAEA,CAAC,QAAQ,QAA+C;EACvD,KAAK,MAAM,MAAM,QAAQ;GACxB,MAAM,WAAW,KAAK,WAAW,IAAI,EAAE;GACvC,IAAI,aAAa,KAAA,GAAW;IAC3B,MAAM;IACN;GACD;GACA,KAAK,WAAW,OAAO,EAAE;GACzB,KAAK,SAAS,KAAK,UAAU,QAAQ;GACrC,MAAM;EACP;CACD;CAIA,YAAY,UAA8E;EACzF,OACC,UAAU,aAAA,GACVC,oBAAAA,WAAAA,CAAW,SAAS,IAAI,KACxB,cAAc,aAAA,GACdA,oBAAAA,WAAAA,CAAW,SAAS,QAAQ,KAC5B,gBAAgB,aAAA,GAChBA,oBAAAA,WAAAA,CAAW,SAAS,UAAU;CAEhC;CAIA,SAAS,IAA+B;EACvC,MAAM,WAAW,KAAK,WAAW,IAAI,EAAE;EACvC,IAAI,aAAa,KAAA,GAChB,MAAM,IAAI,cAAc,YAAY,wBAAwB,MAAM,EAAE,GAAG,CAAC;EAEzE,OAAO;CACR;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACnQA,SAAgB,eAAe,SAA6C;CAC3E,OAAO,IAAI,SAAS,OAAO;AAC5B;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,sBAAsB,SAA4D;CACjG,OAAO,IAAI,gBAAgB,OAAO;AACnC"}
|
|
@@ -163,6 +163,10 @@ import type { FieldPath } from '@orkestrel/contract';
|
|
|
163
163
|
/**
|
|
164
164
|
* Narrows an unknown caught value to a {@link TemplateError}.
|
|
165
165
|
*
|
|
166
|
+
* @remarks
|
|
167
|
+
* Recognizes self-branded errors from other package copies and subclasses, and
|
|
168
|
+
* refuses transparent proxies and unbranded look-alikes.
|
|
169
|
+
*
|
|
166
170
|
* @param value - The value to test (typically a `catch` binding)
|
|
167
171
|
* @returns True if `value` is a {@link TemplateError}; false otherwise
|
|
168
172
|
*
|
package/dist/src/core/index.d.ts
CHANGED
|
@@ -163,6 +163,10 @@ import type { FieldPath } from '@orkestrel/contract';
|
|
|
163
163
|
/**
|
|
164
164
|
* Narrows an unknown caught value to a {@link TemplateError}.
|
|
165
165
|
*
|
|
166
|
+
* @remarks
|
|
167
|
+
* Recognizes self-branded errors from other package copies and subclasses, and
|
|
168
|
+
* refuses transparent proxies and unbranded look-alikes.
|
|
169
|
+
*
|
|
166
170
|
* @param value - The value to test (typically a `catch` binding)
|
|
167
171
|
* @returns True if `value` is a {@link TemplateError}; false otherwise
|
|
168
172
|
*
|
package/dist/src/core/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createContract, isArray, isFiniteNumber, isFunction,
|
|
1
|
+
import { INTRINSICS, createContract, holds, isArray, isFiniteNumber, isFunction, isObject, isString, objectShape, optionalShape, resolveField, schemaToParameters, stringShape } from "@orkestrel/contract";
|
|
2
2
|
import { Emitter } from "@orkestrel/emitter";
|
|
3
|
-
|
|
3
|
+
// src/core/constants.ts
|
|
4
4
|
/**
|
|
5
5
|
* Holds the single-pass `{{name}}` substitution pattern shared by
|
|
6
6
|
* `Template#fill` and `Template#validate`.
|
|
@@ -42,8 +42,7 @@ var UNSAFE_FIELD_SEGMENTS = Object.freeze([
|
|
|
42
42
|
"constructor",
|
|
43
43
|
"prototype"
|
|
44
44
|
]);
|
|
45
|
-
|
|
46
|
-
//#region src/core/errors.ts
|
|
45
|
+
// src/core/errors.ts
|
|
47
46
|
/**
|
|
48
47
|
* Represents an error thrown by the template layer — a machine-readable
|
|
49
48
|
* {@link TemplateErrorCode} and an optional `context` record naming the
|
|
@@ -61,6 +60,7 @@ var TemplateError = class extends Error {
|
|
|
61
60
|
context;
|
|
62
61
|
constructor(code, message, context) {
|
|
63
62
|
super(message);
|
|
63
|
+
INTRINSICS.define(this, Symbol.for("@orkestrel/template.TemplateError"), { value: this });
|
|
64
64
|
this.name = "TemplateError";
|
|
65
65
|
this.code = code;
|
|
66
66
|
if (context !== void 0) this.context = context;
|
|
@@ -69,6 +69,10 @@ var TemplateError = class extends Error {
|
|
|
69
69
|
/**
|
|
70
70
|
* Narrows an unknown caught value to a {@link TemplateError}.
|
|
71
71
|
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* Recognizes self-branded errors from other package copies and subclasses, and
|
|
74
|
+
* refuses transparent proxies and unbranded look-alikes.
|
|
75
|
+
*
|
|
72
76
|
* @param value - The value to test (typically a `catch` binding)
|
|
73
77
|
* @returns True if `value` is a {@link TemplateError}; false otherwise
|
|
74
78
|
*
|
|
@@ -84,10 +88,15 @@ var TemplateError = class extends Error {
|
|
|
84
88
|
* ```
|
|
85
89
|
*/
|
|
86
90
|
function isTemplateError(value) {
|
|
87
|
-
return
|
|
91
|
+
return holds(() => {
|
|
92
|
+
if (!(value instanceof Error) || INTRINSICS.prototype(value) === Error.prototype) return false;
|
|
93
|
+
if (INTRINSICS.describe(value, Symbol.for("@orkestrel/template.TemplateError"))?.value !== value) return false;
|
|
94
|
+
const code = INTRINSICS.reflect.read(value, "code");
|
|
95
|
+
const context = INTRINSICS.reflect.read(value, "context");
|
|
96
|
+
return (code === "MISSING" || code === "NOTFOUND" || code === "INVALID" || code === "CONFLICT") && (context === void 0 || isObject(context) || isFunction(context));
|
|
97
|
+
});
|
|
88
98
|
}
|
|
89
|
-
|
|
90
|
-
//#region src/core/helpers.ts
|
|
99
|
+
// src/core/helpers.ts
|
|
91
100
|
/**
|
|
92
101
|
* Formats a resolved fill value for substitution into a template's `content`.
|
|
93
102
|
*
|
|
@@ -230,7 +239,6 @@ function fillTemplate(content, values, options) {
|
|
|
230
239
|
const locale = options?.locale ?? "en-US";
|
|
231
240
|
const record = values ?? {};
|
|
232
241
|
const missingNames = [];
|
|
233
|
-
const seen = /* @__PURE__ */ new Set();
|
|
234
242
|
const pattern = new RegExp(FILL_PATTERN.source, FILL_PATTERN.flags);
|
|
235
243
|
const result = content.replace(pattern, (matchText, rawToken) => {
|
|
236
244
|
if (rawToken === void 0) return "{{";
|
|
@@ -240,17 +248,13 @@ function fillTemplate(content, values, options) {
|
|
|
240
248
|
if (declared?.fallback !== void 0) return formatValue(declared.fallback, locale);
|
|
241
249
|
if (missing === "literal") return matchText;
|
|
242
250
|
if (missing === "empty") return "";
|
|
243
|
-
if (required && !
|
|
244
|
-
seen.add(token);
|
|
245
|
-
missingNames.push(token);
|
|
246
|
-
}
|
|
251
|
+
if (required && !missingNames.includes(token)) missingNames.push(token);
|
|
247
252
|
return "";
|
|
248
253
|
});
|
|
249
254
|
if (missing === "error" && missingNames.length > 0) throw new TemplateError("MISSING", `Missing required placeholder(s): ${missingNames.join(", ")}`, { missing: missingNames });
|
|
250
255
|
return result;
|
|
251
256
|
}
|
|
252
|
-
|
|
253
|
-
//#region src/core/shapers.ts
|
|
257
|
+
// src/core/shapers.ts
|
|
254
258
|
/**
|
|
255
259
|
* Builds the `@orkestrel/contract` object shape describing a template's
|
|
256
260
|
* declared placeholders.
|
|
@@ -280,8 +284,7 @@ function placeholderShape(placeholders) {
|
|
|
280
284
|
}
|
|
281
285
|
return objectShape(properties);
|
|
282
286
|
}
|
|
283
|
-
|
|
284
|
-
//#region src/core/templates/Template.ts
|
|
287
|
+
// src/core/templates/Template.ts
|
|
285
288
|
/**
|
|
286
289
|
* Represents a named, versionable template — `{{name}}` tokens in `content`,
|
|
287
290
|
* filled against a values record — implementing `TemplateInterface` exactly.
|
|
@@ -313,12 +316,12 @@ var Template = class {
|
|
|
313
316
|
tags;
|
|
314
317
|
constructor(options) {
|
|
315
318
|
const placeholders = options.placeholders ?? [];
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
seenNames.add(placeholder.name);
|
|
319
|
+
Iterator.from(placeholders).reduce((names, placeholder) => {
|
|
320
|
+
if (names.includes(placeholder.name)) throw new TemplateError("INVALID", `Duplicate placeholder name: ${placeholder.name}`, { name: placeholder.name });
|
|
321
|
+
const name = placeholder.name;
|
|
320
322
|
if (isArray(placeholder.path) && placeholder.path.length === 0) throw new TemplateError("INVALID", `Placeholder path must not be empty: ${placeholder.name}`, { name: placeholder.name });
|
|
321
|
-
|
|
323
|
+
return [...names, name];
|
|
324
|
+
}, []);
|
|
322
325
|
this.id = isString(options.id) ? options.id : crypto.randomUUID();
|
|
323
326
|
this.name = options.name;
|
|
324
327
|
this.content = options.content;
|
|
@@ -408,18 +411,11 @@ var Template = class {
|
|
|
408
411
|
*/
|
|
409
412
|
validate(values) {
|
|
410
413
|
const record = values ?? {};
|
|
411
|
-
const missing = [];
|
|
412
|
-
const seen = /* @__PURE__ */ new Set();
|
|
413
414
|
const pattern = new RegExp(FILL_PATTERN.source, FILL_PATTERN.flags);
|
|
414
|
-
|
|
415
|
-
const rawToken = match[1];
|
|
416
|
-
if (rawToken === void 0) continue;
|
|
417
|
-
const token = rawToken.trim();
|
|
418
|
-
if (seen.has(token)) continue;
|
|
419
|
-
seen.add(token);
|
|
415
|
+
const missing = [...new Set(Array.from(this.content.matchAll(pattern)).flatMap((match) => match[1] === void 0 ? [] : [match[1].trim()]))].filter((token) => {
|
|
420
416
|
const { value, declared, required } = resolveToken(record, this.placeholders, token);
|
|
421
|
-
|
|
422
|
-
}
|
|
417
|
+
return value === void 0 && declared?.fallback === void 0 && required;
|
|
418
|
+
});
|
|
423
419
|
const declaredNames = new Set(this.placeholders.map((placeholder) => placeholder.name));
|
|
424
420
|
const extra = Object.keys(record).filter((key) => !declaredNames.has(key));
|
|
425
421
|
return {
|
|
@@ -448,8 +444,7 @@ var Template = class {
|
|
|
448
444
|
return schemaToParameters(this.#contract.schema);
|
|
449
445
|
}
|
|
450
446
|
};
|
|
451
|
-
|
|
452
|
-
//#region src/core/templates/TemplateManager.ts
|
|
447
|
+
// src/core/templates/TemplateManager.ts
|
|
453
448
|
/**
|
|
454
449
|
* Represents the template registry — a self-owning, id-keyed record-holder for
|
|
455
450
|
* the {@link TemplateInterface} instances a consumer registers, looks up,
|
|
@@ -581,17 +576,7 @@ var TemplateManager = class {
|
|
|
581
576
|
this.#emitter.emit("remove", instance);
|
|
582
577
|
return true;
|
|
583
578
|
}
|
|
584
|
-
|
|
585
|
-
for (const id of target) {
|
|
586
|
-
const instance = this.#templates.get(id);
|
|
587
|
-
if (instance === void 0) {
|
|
588
|
-
all = false;
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
|
-
this.#templates.delete(id);
|
|
592
|
-
this.#emitter.emit("remove", instance);
|
|
593
|
-
}
|
|
594
|
-
return all;
|
|
579
|
+
return this.#remove(target).reduce((all, removed) => removed && all, true);
|
|
595
580
|
}
|
|
596
581
|
/** Removes every registered template, emitting `clear`. */
|
|
597
582
|
clear() {
|
|
@@ -660,6 +645,18 @@ var TemplateManager = class {
|
|
|
660
645
|
locale: template.locale ?? this.#locale
|
|
661
646
|
});
|
|
662
647
|
}
|
|
648
|
+
*#remove(target) {
|
|
649
|
+
for (const id of target) {
|
|
650
|
+
const instance = this.#templates.get(id);
|
|
651
|
+
if (instance === void 0) {
|
|
652
|
+
yield false;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
this.#templates.delete(id);
|
|
656
|
+
this.#emitter.emit("remove", instance);
|
|
657
|
+
yield true;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
663
660
|
#isInstance(template) {
|
|
664
661
|
return "fill" in template && isFunction(template.fill) && "validate" in template && isFunction(template.validate) && "parameters" in template && isFunction(template.parameters);
|
|
665
662
|
}
|
|
@@ -669,8 +666,7 @@ var TemplateManager = class {
|
|
|
669
666
|
return instance;
|
|
670
667
|
}
|
|
671
668
|
};
|
|
672
|
-
|
|
673
|
-
//#region src/core/factories.ts
|
|
669
|
+
// src/core/factories.ts
|
|
674
670
|
/**
|
|
675
671
|
* Creates a working {@link TemplateInterface} from a {@link TemplateOptions}
|
|
676
672
|
* bag, backed by the `Template` class.
|
|
@@ -727,7 +723,6 @@ function createTemplate(options) {
|
|
|
727
723
|
function createTemplateManager(options) {
|
|
728
724
|
return new TemplateManager(options);
|
|
729
725
|
}
|
|
730
|
-
//#endregion
|
|
731
726
|
export { DEFAULT_LOCALE, DEFAULT_MISSING_POLICY, FILL_PATTERN, Template, TemplateError, TemplateManager, UNSAFE_FIELD_SEGMENTS, createTemplate, createTemplateManager, fillTemplate, formatValue, isTemplateError, placeholderShape, resolveSafeField, resolveToken };
|
|
732
727
|
|
|
733
728
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/core/constants.ts","../../../src/core/errors.ts","../../../src/core/helpers.ts","../../../src/core/shapers.ts","../../../src/core/templates/Template.ts","../../../src/core/templates/TemplateManager.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { MissingPolicy } from './types.js'\n\n// Frozen default data for the template module — constants are\n// UPPER_SNAKE_CASE data, the sole home for module-scope literal defaults.\n\n/**\n * Holds the single-pass `{{name}}` substitution pattern shared by\n * `Template#fill` and `Template#validate`.\n *\n * @remarks\n * Global-flagged, two-alternative pattern: a match of the first alternative\n * (`\\{{` — a literal backslash followed by `{{`) means \"emit a literal\n * `{{`\" — the escape hatch for content that must show `{{` without\n * triggering substitution. A match that instead populates capture group 1\n * (`\\{{([^{}]+?)\\}\\}`) means \"substitute the named token\" — group 1 is the\n * untrimmed token text between the braces; every call site trims it\n * (`token.trim()`) before using it as a lookup name, so `'{{ name }}'` still\n * resolves `'name'`. The pattern deliberately does not wrap the token in\n * `\\s*` — an unclosed `'{{' + ' '.repeat(n)` with no closing `}}` would\n * otherwise force the regex engine into catastrophic backtracking over the\n * whitespace run (O(n^2)); trimming after the match keeps the same\n * whitespace tolerance without the backtracking hazard. Every call site\n * builds a fresh `RegExp` from `.source` / `.flags` rather than sharing this\n * instance's mutable `lastIndex` across scans.\n */\nexport const FILL_PATTERN = /\\\\\\{\\{|\\{\\{([^{}]+?)\\}\\}/g\n\n/**\n * Holds `'error'`, the default `missing` policy for `Template#fill` /\n * `TemplateManager#fill` when unspecified.\n */\nexport const DEFAULT_MISSING_POLICY: MissingPolicy = 'error'\n\n/**\n * Holds `'en-US'`, the default `locale` for `Template#fill` /\n * `TemplateManager#fill` when unspecified.\n */\nexport const DEFAULT_LOCALE = 'en-US'\n\n/**\n * Lists the prototype-pollution-unsafe field-path segments `'__proto__'`,\n * `'constructor'`, and `'prototype'` — a fill lookup refuses to resolve a path\n * containing one of them, treating the placeholder as unresolved.\n */\nexport const UNSAFE_FIELD_SEGMENTS: readonly string[] = Object.freeze([\n\t'__proto__',\n\t'constructor',\n\t'prototype',\n])\n","import type { TemplateErrorCode } from './types.js'\nimport { isInstance } from '@orkestrel/contract'\n\n// Misuse of the template layer `throw`s a `TemplateError` carrying a\n// machine-readable `code`, so a `catch` branches on `error.code`.\n\n/**\n * Represents an error thrown by the template layer — a machine-readable\n * {@link TemplateErrorCode} and an optional `context` record naming the\n * offending id or placeholder name.\n *\n * @remarks\n * Thrown for: a required placeholder staying unresolved under the `error`\n * {@link MissingPolicy} (`MISSING`), an unknown template id\n * (`NOTFOUND`), `createTemplate` handed invalid data (`INVALID`), and\n * `TemplateManagerInterface#register` handed an id already present without\n * `options.replace` (`CONFLICT`).\n */\nexport class TemplateError extends Error {\n\treadonly code: TemplateErrorCode\n\treadonly context?: Readonly<Record<string, unknown>>\n\n\tconstructor(\n\t\tcode: TemplateErrorCode,\n\t\tmessage: string,\n\t\tcontext?: Readonly<Record<string, unknown>>,\n\t) {\n\t\tsuper(message)\n\t\tthis.name = 'TemplateError'\n\t\tthis.code = code\n\t\tif (context !== undefined) this.context = context\n\t}\n}\n\n/**\n * Narrows an unknown caught value to a {@link TemplateError}.\n *\n * @param value - The value to test (typically a `catch` binding)\n * @returns True if `value` is a {@link TemplateError}; false otherwise\n *\n * @example\n * ```ts\n * import { isTemplateError } from '@src/core'\n *\n * try {\n * \tmanager.fill('missing')\n * } catch (error) {\n * \tif (isTemplateError(error) && error.code === 'NOTFOUND') return\n * }\n * ```\n */\nexport function isTemplateError(value: unknown): value is TemplateError {\n\treturn isInstance(value, TemplateError)\n}\n","import type { FieldPath } from '@orkestrel/contract'\nimport type {\n\tTemplateFillContext,\n\tTemplateFillValues,\n\tTemplatePlaceholder,\n\tTemplateTokenResolution,\n} from './types.js'\nimport { isArray, isFiniteNumber, resolveField } from '@orkestrel/contract'\nimport {\n\tDEFAULT_LOCALE,\n\tDEFAULT_MISSING_POLICY,\n\tFILL_PATTERN,\n\tUNSAFE_FIELD_SEGMENTS,\n} from './constants.js'\nimport { TemplateError } from './errors.js'\n\n// The templates pure-leaf inventory — every function here is a\n// referentially-transparent computation with no instance state, exported and\n// independently unit-testable. `Template#fill` / `#validate` route through\n// these leaves rather than duplicating the substitution logic.\n\n/**\n * Formats a resolved fill value for substitution into a template's `content`.\n *\n * @remarks\n * A finite number renders with the given locale's thousand grouping (through\n * `toLocaleString`); every other value — including `null` — String-coerces.\n * `null` therefore renders as the literal string `'null'`, matching\n * `String(value)` exactly, so a resolved `null` is visible in the output\n * rather than silently empty. An\n * invalid BCP-47 `locale` tag throws a `RangeError` from the underlying\n * `toLocaleString` call when `value` is a finite number — this is a caller\n * error (an invalid locale argument), by design, and is not caught here.\n *\n * @param value - The resolved value to format\n * @param locale - The locale used for finite-number formatting\n * @returns The formatted string\n *\n * @example\n * ```ts\n * import { formatValue } from '@src/core'\n *\n * formatValue(5010, 'en-US') // '5,010'\n * formatValue(null, 'en-US') // 'null'\n * ```\n */\nexport function formatValue(value: unknown, locale: string): string {\n\tif (isFiniteNumber(value)) return value.toLocaleString(locale)\n\treturn String(value)\n}\n\n/**\n * Resolves a field path against a fill-values record, refusing any path that\n * touches a prototype-pollution-unsafe segment.\n *\n * @remarks\n * A prototype-pollution guard shared by `fillTemplate` and `Template#validate`\n * so the two stay in lockstep: `path` normalizes to a segment array (a bare\n * string `path` becomes a single-segment array); if any segment appears in\n * `UNSAFE_FIELD_SEGMENTS` (`'__proto__'`, `'constructor'`, `'prototype'`), the\n * lookup is refused and `undefined` is returned without ever calling\n * `resolveField` — a path like `['__proto__', 'polluted']` can never reach\n * the record's actual prototype chain through this function. Every other\n * path resolves through `@orkestrel/contract`'s `resolveField`.\n *\n * @param record - The fill-values record to resolve against\n * @param path - The field path — a single segment or a segment array\n * @returns The resolved value, or `undefined` when unresolved or the path is unsafe\n *\n * @example\n * ```ts\n * import { resolveSafeField } from '@src/core'\n *\n * resolveSafeField({ a: { b: 1 } }, ['a', 'b']) // 1\n * resolveSafeField({}, ['__proto__', 'polluted']) // undefined\n * ```\n */\nexport function resolveSafeField(record: TemplateFillValues, path: FieldPath): unknown {\n\tconst segments = isArray(path) ? path : [path]\n\tif (segments.some((segment) => UNSAFE_FIELD_SEGMENTS.includes(segment))) return undefined\n\treturn resolveField(record, path)\n}\n\n/**\n * Resolves one `{{name}}` token against the declared placeholders and the\n * fill-values record.\n *\n * @remarks\n * The single implementation of the token rule `fillTemplate` and\n * `Template#validate` both apply, so the two can never drift: the declared\n * {@link TemplatePlaceholder} sharing the token's `name` (exact match)\n * supplies its `path`, falling back to the token split on `.`; the value\n * resolves through `resolveSafeField`, so any segment in\n * `UNSAFE_FIELD_SEGMENTS` yields `undefined` without ever calling\n * `resolveField`; `required` is `true` for an undeclared token and for a\n * declared placeholder whose `required` is not `false`. The token is passed\n * already trimmed. `fallback` is not applied here — it is read from\n * `declared` by each caller, because `fill` substitutes it and `validate`\n * only counts it.\n *\n * @param record - The fill-values record the token resolves against\n * @param placeholders - The declared placeholders the token matches by name\n * @param token - The trimmed token text, without its `{{` / `}}` delimiters\n * @returns The {@link TemplateTokenResolution} for the token\n *\n * @example\n * ```ts\n * import { resolveToken } from '@src/core'\n *\n * resolveToken({ name: 'Ada' }, [], 'name').value // 'Ada'\n * resolveToken({}, [{ name: 'nickname', required: false }], 'nickname').required // false\n * ```\n */\nexport function resolveToken(\n\trecord: TemplateFillValues,\n\tplaceholders: readonly TemplatePlaceholder[],\n\ttoken: string,\n): TemplateTokenResolution {\n\tconst declared = placeholders.find((placeholder) => placeholder.name === token)\n\tconst path = declared?.path ?? token.split('.')\n\treturn {\n\t\tvalue: resolveSafeField(record, path),\n\t\tdeclared,\n\t\trequired: declared === undefined || declared.required !== false,\n\t}\n}\n\n/**\n * Substitutes every `{{name}}` token in `content` in a single pass.\n *\n * @remarks\n * Uses a fresh `RegExp` clone of `FILL_PATTERN` per call (never sharing its\n * `lastIndex`) and a single `String#replace` scan — substituted output is\n * never re-scanned. Each token resolves through `resolveToken`, the one rule\n * `Template#validate` also applies: the matching declared\n * {@link TemplatePlaceholder} (exact `name`) supplies its `path` (falling\n * back to the token split on `.`); any path segment in `UNSAFE_FIELD_SEGMENTS`\n * makes the token unresolved without ever calling `resolveField` (a\n * prototype-pollution guard). A resolved value formats through `formatValue`; an\n * unresolved value falls back to the placeholder's `fallback` when declared;\n * otherwise `options.missing` governs — `'literal'` re-emits the original\n * `{{name}}` text, `'empty'` emits `''`, and `'error'` emits `''` for every\n * token but collects every unresolved required token (an undeclared token, or\n * a declared token with `required !== false`) and throws one\n * {@link TemplateError} coded `MISSING` listing them all, in first-appearance\n * order, once the scan completes. An escaped `\\{{` emits a literal `{{`.\n *\n * Called with no declared `placeholders` and `{ missing: 'empty' }`, this is a\n * bare interpolation over `content` — every token resolves by dotted path\n * against the values record and every unresolved token emits `''`.\n * `FILL_PATTERN`'s token class (`[^{}]`) excludes `{`, so a token containing\n * `{` never matches and the surrounding `{{` stays literal.\n *\n * @param content - The template content carrying `{{name}}` tokens\n * @param values - The values tokens resolve against\n * @param options - `missing` (default `'error'`), `locale` (default `'en-US'`), and the declared `placeholders` (default none) tokens resolve against\n * @returns The substituted content\n *\n * @example\n * ```ts\n * import { fillTemplate } from '@src/core'\n *\n * fillTemplate('Hi {{name}}', { name: 'Ada' }) // 'Hi Ada'\n * fillTemplate('Limit {{limit}}', { limit: 5010 }, { missing: 'empty' }) // 'Limit 5,010'\n * ```\n */\nexport function fillTemplate(\n\tcontent: string,\n\tvalues?: TemplateFillValues,\n\toptions?: TemplateFillContext,\n): string {\n\tconst placeholders = options?.placeholders ?? []\n\tconst missing = options?.missing ?? DEFAULT_MISSING_POLICY\n\tconst locale = options?.locale ?? DEFAULT_LOCALE\n\tconst record = values ?? {}\n\n\tconst missingNames: string[] = []\n\tconst seen = new Set<string>()\n\n\tconst pattern = new RegExp(FILL_PATTERN.source, FILL_PATTERN.flags)\n\tconst result = content.replace(pattern, (matchText: string, rawToken: string | undefined) => {\n\t\tif (rawToken === undefined) return '{{'\n\t\tconst token = rawToken.trim()\n\n\t\tconst { value, declared, required } = resolveToken(record, placeholders, token)\n\n\t\tif (value !== undefined) return formatValue(value, locale)\n\t\tif (declared?.fallback !== undefined) return formatValue(declared.fallback, locale)\n\n\t\tif (missing === 'literal') return matchText\n\t\tif (missing === 'empty') return ''\n\n\t\tif (required && !seen.has(token)) {\n\t\t\tseen.add(token)\n\t\t\tmissingNames.push(token)\n\t\t}\n\t\treturn ''\n\t})\n\n\tif (missing === 'error' && missingNames.length > 0) {\n\t\tthrow new TemplateError(\n\t\t\t'MISSING',\n\t\t\t`Missing required placeholder(s): ${missingNames.join(', ')}`,\n\t\t\t{ missing: missingNames },\n\t\t)\n\t}\n\n\treturn result\n}\n","import type { ContractShape } from '@orkestrel/contract'\nimport type { TemplatePlaceholder } from './types.js'\nimport { objectShape, optionalShape, stringShape } from '@orkestrel/contract'\n\n// The templates shape-value inventory — every function here builds an\n// `@orkestrel/contract` shape from declared template data. Shapers sit above\n// the `helpers.ts` leaf pair: they consume it, and it never consumes them.\n\n/**\n * Builds the `@orkestrel/contract` object shape describing a template's\n * declared placeholders.\n *\n * @remarks\n * Each placeholder becomes a `stringShape` carrying its `description`;\n * `required === false` wraps it in `optionalShape`. Used by `Template` to\n * compile its `parameters()` contract once per instance.\n *\n * @param placeholders - The declared placeholders to shape\n * @returns The contract shape for `createContract`\n *\n * @example\n * ```ts\n * import { placeholderShape } from '@src/core'\n * import { createContract } from '@orkestrel/contract'\n *\n * const contract = createContract(placeholderShape([{ name: 'city' }]))\n * ```\n */\nexport function placeholderShape(placeholders: readonly TemplatePlaceholder[]): ContractShape {\n\tconst properties: Record<string, ContractShape> = {}\n\tfor (const placeholder of placeholders) {\n\t\tconst description = placeholder.description\n\t\tconst field = stringShape({\n\t\t\t...(description !== undefined ? { description } : {}),\n\t\t})\n\t\tproperties[placeholder.name] = placeholder.required === false ? optionalShape(field) : field\n\t}\n\treturn objectShape(properties)\n}\n","import type { ContractInterface } from '@orkestrel/contract'\nimport type {\n\tMissingPolicy,\n\tTemplateDefinition,\n\tTemplateFillOptions,\n\tTemplateFillValues,\n\tTemplateInterface,\n\tTemplateOptions,\n\tTemplatePlaceholder,\n\tTemplateValidationResult,\n} from '../types.js'\nimport { createContract, isArray, isString, schemaToParameters } from '@orkestrel/contract'\nimport { DEFAULT_LOCALE, DEFAULT_MISSING_POLICY, FILL_PATTERN } from '../constants.js'\nimport { fillTemplate, resolveToken } from '../helpers.js'\nimport { placeholderShape } from '../shapers.js'\nimport { TemplateError } from '../errors.js'\n\n/**\n * Represents a named, versionable template — `{{name}}` tokens in `content`,\n * filled against a values record — implementing `TemplateInterface` exactly.\n *\n * @remarks\n * `missing` / `locale` seed this instance's default {@link TemplateFillOptions},\n * overridable per `fill` call. Its `parameters()` contract (built from\n * `placeholders` through `placeholderShape`) compiles once, in the constructor.\n *\n * @throws {@link TemplateError} Thrown when `options.placeholders` declares a duplicate `name` or an empty `path` (coded `INVALID`)\n *\n * @example\n * ```ts\n * const greeting = new Template({ name: 'greeting', content: 'Hi {{name}}' })\n * greeting.fill({ name: 'Ada' }) // 'Hi Ada'\n * ```\n */\nexport class Template implements TemplateInterface {\n\treadonly #missing: MissingPolicy\n\treadonly #locale: string\n\treadonly #contract: ContractInterface<unknown>\n\treadonly id: string\n\treadonly name: string\n\treadonly content: string\n\treadonly placeholders: readonly TemplatePlaceholder[]\n\treadonly summary?: string\n\treadonly description?: string\n\treadonly category?: string\n\treadonly tags?: readonly string[]\n\n\tconstructor(options: TemplateOptions) {\n\t\tconst placeholders = options.placeholders ?? []\n\t\tconst seenNames = new Set<string>()\n\t\tfor (const placeholder of placeholders) {\n\t\t\tif (seenNames.has(placeholder.name)) {\n\t\t\t\tthrow new TemplateError('INVALID', `Duplicate placeholder name: ${placeholder.name}`, {\n\t\t\t\t\tname: placeholder.name,\n\t\t\t\t})\n\t\t\t}\n\t\t\tseenNames.add(placeholder.name)\n\t\t\tif (isArray(placeholder.path) && placeholder.path.length === 0) {\n\t\t\t\tthrow new TemplateError(\n\t\t\t\t\t'INVALID',\n\t\t\t\t\t`Placeholder path must not be empty: ${placeholder.name}`,\n\t\t\t\t\t{ name: placeholder.name },\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\tthis.id = isString(options.id) ? options.id : crypto.randomUUID()\n\t\tthis.name = options.name\n\t\tthis.content = options.content\n\t\tthis.placeholders = placeholders\n\t\tif (options.summary !== undefined) this.summary = options.summary\n\t\tif (options.description !== undefined) this.description = options.description\n\t\tif (options.category !== undefined) this.category = options.category\n\t\tif (options.tags !== undefined) this.tags = options.tags\n\t\tthis.#missing = options.missing ?? DEFAULT_MISSING_POLICY\n\t\tthis.#locale = options.locale ?? DEFAULT_LOCALE\n\t\tthis.#contract = createContract(placeholderShape(this.placeholders))\n\t}\n\n\t/**\n\t * Returns the plain, JSON-serializable data this template carries.\n\t *\n\t * @returns The {@link TemplateDefinition} record\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({ name: 'greeting', content: 'Hi {{name}}' })\n\t * instance.definition().name // 'greeting'\n\t * ```\n\t */\n\tdefinition(): TemplateDefinition {\n\t\treturn {\n\t\t\tid: this.id,\n\t\t\tname: this.name,\n\t\t\tcontent: this.content,\n\t\t\tplaceholders: this.placeholders,\n\t\t\t...(this.summary !== undefined ? { summary: this.summary } : {}),\n\t\t\t...(this.description !== undefined ? { description: this.description } : {}),\n\t\t\t...(this.category !== undefined ? { category: this.category } : {}),\n\t\t\t...(this.tags !== undefined ? { tags: this.tags } : {}),\n\t\t}\n\t}\n\n\t/**\n\t * Substitutes every `{{name}}` token in `content` against `values`.\n\t *\n\t * @param values - The values tokens resolve against\n\t * @param options - Per-call overrides for this instance's `missing` / `locale` defaults\n\t * @returns The substituted content\n\t * @throws {@link TemplateError} Thrown when a required placeholder stays unresolved under the `'error'` policy (coded `MISSING`)\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({ name: 'greeting', content: 'Hi {{name}}' })\n\t * instance.fill({ name: 'Ada' }) // 'Hi Ada'\n\t * ```\n\t */\n\tfill(values?: TemplateFillValues, options?: TemplateFillOptions): string {\n\t\treturn fillTemplate(this.content, values, {\n\t\t\tmissing: options?.missing ?? this.#missing,\n\t\t\tlocale: options?.locale ?? this.#locale,\n\t\t\tplaceholders: this.placeholders,\n\t\t})\n\t}\n\n\t/**\n\t * Reports which required placeholders would stay unresolved, and which\n\t * `values` keys go unused, without producing output.\n\t *\n\t * @remarks\n\t * Content-token driven: scans `this.content` for every `{{name}}` token\n\t * (skipping escaped `\\{{` matches) the same way `fill` does, so `validate`\n\t * predicts `fill`'s `'error'`-{@link MissingPolicy} outcome exactly — a\n\t * token reported here as missing is precisely a token that would throw\n\t * under `fill(values, { missing: 'error' })`. For each distinct token\n\t * (first-appearance order, trimmed): `resolveToken` applies the one shared\n\t * token rule `fill` also applies — a declared {@link TemplatePlaceholder}\n\t * sharing its `name` supplies `path` (falling back to the token split on\n\t * `.`), and the value resolves through `resolveSafeField`. The token is `missing`\n\t * only when the value is unresolved, no `fallback` is declared, and the\n\t * placeholder is required (`required !== false`, including undeclared\n\t * tokens). `extra` lists every `values` key with no declared placeholder.\n\t *\n\t * @param values - The values to check\n\t * @returns The {@link TemplateValidationResult}\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({\n\t * \tname: 'greeting',\n\t * \tcontent: 'Hi {{name}}',\n\t * \tplaceholders: [{ name: 'name' }],\n\t * })\n\t * instance.validate({}).missing // ['name']\n\t * ```\n\t */\n\tvalidate(values?: TemplateFillValues): TemplateValidationResult {\n\t\tconst record = values ?? {}\n\t\tconst missing: string[] = []\n\t\tconst seen = new Set<string>()\n\n\t\tconst pattern = new RegExp(FILL_PATTERN.source, FILL_PATTERN.flags)\n\t\tfor (const match of this.content.matchAll(pattern)) {\n\t\t\tconst rawToken = match[1]\n\t\t\tif (rawToken === undefined) continue\n\t\t\tconst token = rawToken.trim()\n\t\t\tif (seen.has(token)) continue\n\t\t\tseen.add(token)\n\n\t\t\tconst { value, declared, required } = resolveToken(record, this.placeholders, token)\n\n\t\t\tif (value === undefined && declared?.fallback === undefined && required) {\n\t\t\t\tmissing.push(token)\n\t\t\t}\n\t\t}\n\n\t\tconst declaredNames = new Set(this.placeholders.map((placeholder) => placeholder.name))\n\t\tconst extra = Object.keys(record).filter((key) => !declaredNames.has(key))\n\n\t\treturn { valid: missing.length === 0, missing, extra }\n\t}\n\n\t/**\n\t * Projects this template's placeholders to the open tool-parameters record\n\t * shape.\n\t *\n\t * @returns The compiled parameters record, or `undefined` when `schemaToParameters` yields none\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({\n\t * \tname: 'greeting',\n\t * \tcontent: 'Hi {{name}}',\n\t * \tplaceholders: [{ name: 'name' }],\n\t * })\n\t * instance.parameters()\n\t * ```\n\t */\n\tparameters(): Readonly<Record<string, unknown>> | undefined {\n\t\treturn schemaToParameters(this.#contract.schema)\n\t}\n}\n","import type {\n\tMissingPolicy,\n\tTemplateFillValues,\n\tTemplateFillOptions,\n\tTemplateInterface,\n\tTemplateManagerEventMap,\n\tTemplateManagerInterface,\n\tTemplateManagerOptions,\n\tTemplateOptions,\n\tTemplateQuery,\n\tTemplateRegisterOptions,\n\tTemplateValidationResult,\n} from '../types.js'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport { isFunction, isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport { DEFAULT_LOCALE, DEFAULT_MISSING_POLICY } from '../constants.js'\nimport { TemplateError } from '../errors.js'\nimport { Template } from './Template.js'\n\n/**\n * Represents the template registry — a self-owning, id-keyed record-holder for\n * the {@link TemplateInterface} instances a consumer registers, looks up,\n * fills, and validates by id — implementing `TemplateManagerInterface`\n * exactly.\n *\n * @remarks\n * Singular and plural accessors, the batch `remove` overloads, and ownership\n * of the emitter all sit here. `register` accepts either a constructed {@link TemplateInterface} (kept\n * as-is, including its own `missing` / `locale` defaults) or a plain\n * {@link TemplateOptions} bag — constructed into a `Template` with this\n * manager's `missing` / `locale` defaults applied wherever the bag omits\n * them. A duplicate `id` throws a {@link TemplateError} coded `CONFLICT`\n * unless `options.replace` is `true`, in which case the existing entry is\n * overwritten. `options.templates` seeds the registry at construction without\n * emitting `register` — only calls to `register` after construction emit.\n * The batch `remove(ids)` form removes every present id and returns\n * `true` only when every listed id was present.\n *\n * @example\n * ```ts\n * import { TemplateManager } from '@src/core'\n *\n * const manager = new TemplateManager()\n * const instance = manager.register({ name: 'greeting', content: 'Hi {{name}}' })\n * manager.fill(instance.id, { name: 'Ada' }) // 'Hi Ada'\n * ```\n */\nexport class TemplateManager implements TemplateManagerInterface {\n\treadonly #templates = new Map<string, TemplateInterface>()\n\treadonly #emitter: Emitter<TemplateManagerEventMap>\n\treadonly #missing: MissingPolicy\n\treadonly #locale: string\n\n\tconstructor(options?: TemplateManagerOptions) {\n\t\tconst on = options?.on\n\t\tconst error = options?.error\n\t\tthis.#emitter = new Emitter<TemplateManagerEventMap>({\n\t\t\t...(on !== undefined ? { on } : {}),\n\t\t\t...(error !== undefined ? { error } : {}),\n\t\t})\n\t\tthis.#missing = options?.missing ?? DEFAULT_MISSING_POLICY\n\t\tthis.#locale = options?.locale ?? DEFAULT_LOCALE\n\t\tfor (const template of options?.templates ?? []) {\n\t\t\tconst instance = this.#instantiate(template)\n\t\t\tthis.#templates.set(instance.id, instance)\n\t\t}\n\t}\n\n\tget emitter(): EmitterInterface<TemplateManagerEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget count(): number {\n\t\treturn this.#templates.size\n\t}\n\n\t/**\n\t * Registers a template — a constructed {@link TemplateInterface} (kept\n\t * as-is) or a plain {@link TemplateOptions} bag (constructed into a\n\t * `Template` with this manager's `missing` / `locale` defaults applied\n\t * wherever the bag omits them).\n\t *\n\t * @param template - The template instance or options to register\n\t * @param options - `replace` — overwrite an existing entry sharing the same id instead of throwing\n\t * @returns The registered {@link TemplateInterface}\n\t * @throws {@link TemplateError} Thrown when the id is already registered and `options.replace` is not `true` (coded `CONFLICT`), or when an options bag declares a duplicate placeholder `name` or an empty `path` (coded `INVALID`)\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = manager.register({ id: 'greeting', name: 'greeting', content: 'Hi {{name}}' })\n\t * ```\n\t */\n\tregister(\n\t\ttemplate: TemplateInterface | TemplateOptions,\n\t\toptions?: TemplateRegisterOptions,\n\t): TemplateInterface {\n\t\tconst instance = this.#instantiate(template)\n\t\tconst existing = this.#templates.get(instance.id)\n\t\tif (existing !== undefined && options?.replace !== true) {\n\t\t\tthrow new TemplateError('CONFLICT', `Template already registered: ${instance.id}`, {\n\t\t\t\tid: instance.id,\n\t\t\t})\n\t\t}\n\t\tthis.#templates.set(instance.id, instance)\n\t\tthis.#emitter.emit('register', instance)\n\t\treturn instance\n\t}\n\n\t/**\n\t * Returns one registered {@link TemplateInterface} by id.\n\t *\n\t * @param id - The template id\n\t * @returns The registered {@link TemplateInterface}, or `undefined` when `id` is unregistered\n\t */\n\ttemplate(id: string): TemplateInterface | undefined {\n\t\treturn this.#templates.get(id)\n\t}\n\n\t/**\n\t * Lists every registered template.\n\t *\n\t * @returns A snapshot array of every registered {@link TemplateInterface}\n\t */\n\ttemplates(): readonly TemplateInterface[] {\n\t\treturn [...this.#templates.values()]\n\t}\n\n\t/**\n\t * Filters registered templates by `name`, `category`, and `tag` — every\n\t * supplied field must match.\n\t *\n\t * @param query - The {@link TemplateQuery} to filter by; omit for every registered template\n\t * @returns The matching templates\n\t */\n\tfind(query?: TemplateQuery): readonly TemplateInterface[] {\n\t\tif (query === undefined) return this.templates()\n\t\treturn this.templates().filter((instance) => {\n\t\t\tif (query.name !== undefined && instance.name !== query.name) return false\n\t\t\tif (query.category !== undefined && instance.category !== query.category) return false\n\t\t\tif (query.tag !== undefined && !(instance.tags ?? []).includes(query.tag)) return false\n\t\t\treturn true\n\t\t})\n\t}\n\n\t/**\n\t * Tests whether a template id is registered.\n\t *\n\t * @param id - The template id\n\t * @returns True if `id` is registered; false otherwise\n\t */\n\thas(id: string): boolean {\n\t\treturn this.#templates.has(id)\n\t}\n\n\t/**\n\t * Removes one, several, or every registered template.\n\t *\n\t * @remarks\n\t * `remove()` removes every registered template, emitting `remove` once per\n\t * instance. `remove(id)` removes one template by id, emitting `remove` and\n\t * returning `true` when it existed, `false` otherwise. `remove(ids)`\n\t * removes every listed id that is present, emitting `remove` once per\n\t * removed instance, and returns `true` only when every listed id was\n\t * present.\n\t *\n\t * @param target - Omit to remove all, a single id, or a list of ids\n\t * @returns `boolean` for the single-id / list-of-ids forms; `void` for the remove-all form\n\t */\n\t// `readonly string[]` is not assignable to `id: string`, so a list resolves to the\n\t// batch signature whatever order the signatures are declared in.\n\tremove(ids: readonly string[]): boolean\n\tremove(id: string): boolean\n\tremove(): void\n\tremove(target?: string | readonly string[]): boolean | void {\n\t\tif (target === undefined) {\n\t\t\tfor (const instance of this.#templates.values()) this.#emitter.emit('remove', instance)\n\t\t\tthis.#templates.clear()\n\t\t\treturn\n\t\t}\n\t\tif (isString(target)) {\n\t\t\tconst instance = this.#templates.get(target)\n\t\t\tif (instance === undefined) return false\n\t\t\tthis.#templates.delete(target)\n\t\t\tthis.#emitter.emit('remove', instance)\n\t\t\treturn true\n\t\t}\n\t\tlet all = true\n\t\tfor (const id of target) {\n\t\t\tconst instance = this.#templates.get(id)\n\t\t\tif (instance === undefined) {\n\t\t\t\tall = false\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tthis.#templates.delete(id)\n\t\t\tthis.#emitter.emit('remove', instance)\n\t\t}\n\t\treturn all\n\t}\n\n\t/** Removes every registered template, emitting `clear`. */\n\tclear(): void {\n\t\tthis.#templates.clear()\n\t\tthis.#emitter.emit('clear')\n\t}\n\n\t/**\n\t * Tears down the registry: drops every registered template and destroys the\n\t * owned emitter. Idempotent.\n\t *\n\t * @remarks\n\t * Teardown is not an observable registry operation and the emitter is being\n\t * released, so this emits neither `clear` nor `remove`. The emitter is torn\n\t * down last, after the registry is dropped.\n\t *\n\t * @example\n\t * ```ts\n\t * const manager = new TemplateManager()\n\t * manager.destroy()\n\t * manager.emitter.destroyed // true\n\t * ```\n\t */\n\tdestroy(): void {\n\t\tthis.#templates.clear()\n\t\tthis.#emitter.destroy()\n\t}\n\n\t/**\n\t * Fills a registered template by id.\n\t *\n\t * @param id - The template id\n\t * @param values - The values tokens resolve against\n\t * @param options - Per-call overrides for the template's `missing` / `locale` defaults\n\t * @returns The substituted content\n\t * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown\n\t * @throws {@link TemplateError} Thrown when a required placeholder stays unresolved under the `'error'` policy (coded `MISSING`)\n\t */\n\tfill(id: string, values?: TemplateFillValues, options?: TemplateFillOptions): string {\n\t\treturn this.#require(id).fill(values, options)\n\t}\n\n\t/**\n\t * Validates values against a registered template by id.\n\t *\n\t * @param id - The template id\n\t * @param values - The values to check\n\t * @returns The {@link TemplateValidationResult}\n\t * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown\n\t */\n\tvalidate(id: string, values?: TemplateFillValues): TemplateValidationResult {\n\t\treturn this.#require(id).validate(values)\n\t}\n\n\t/**\n\t * Projects a registered template's parameters by id.\n\t *\n\t * @param id - The template id\n\t * @returns The compiled parameters record, or `undefined` when the template has none\n\t * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown\n\t */\n\tparameters(id: string): Readonly<Record<string, unknown>> | undefined {\n\t\treturn this.#require(id).parameters()\n\t}\n\n\t#instantiate(template: TemplateInterface | TemplateOptions): TemplateInterface {\n\t\tif (this.#isInstance(template)) return template\n\t\treturn new Template({\n\t\t\t...template,\n\t\t\tmissing: template.missing ?? this.#missing,\n\t\t\tlocale: template.locale ?? this.#locale,\n\t\t})\n\t}\n\n\t// A TemplateOptions bag is plain data with no `fill` / `validate` /\n\t// `parameters` methods; a TemplateInterface instance always exposes all three.\n\t#isInstance(template: TemplateInterface | TemplateOptions): template is TemplateInterface {\n\t\treturn (\n\t\t\t'fill' in template &&\n\t\t\tisFunction(template.fill) &&\n\t\t\t'validate' in template &&\n\t\t\tisFunction(template.validate) &&\n\t\t\t'parameters' in template &&\n\t\t\tisFunction(template.parameters)\n\t\t)\n\t}\n\n\t// Every by-id operation that needs a template to proceed shares this lookup.\n\t// The `template` accessor deliberately does not, and returns `undefined`.\n\t#require(id: string): TemplateInterface {\n\t\tconst instance = this.#templates.get(id)\n\t\tif (instance === undefined) {\n\t\t\tthrow new TemplateError('NOTFOUND', `Unknown template id: ${id}`, { id })\n\t\t}\n\t\treturn instance\n\t}\n}\n","import type {\n\tTemplateInterface,\n\tTemplateManagerInterface,\n\tTemplateManagerOptions,\n\tTemplateOptions,\n} from './types.js'\nimport { Template } from './templates/Template.js'\nimport { TemplateManager } from './templates/TemplateManager.js'\n\n/**\n * Creates a working {@link TemplateInterface} from a {@link TemplateOptions}\n * bag, backed by the `Template` class.\n *\n * @param options - The template's `name` / `content`, an optional `id`\n * (defaults to a generated UUID), `placeholders`, catalog metadata, and\n * `missing` / `locale` fill defaults\n * @returns A working {@link TemplateInterface}\n * @throws {@link TemplateError} Thrown when `options.placeholders` declares a duplicate `name` or an empty `path` (coded `INVALID`)\n *\n * @example Create a template and a registry\n * ```ts\n * import { createTemplate, createTemplateManager } from '@orkestrel/template'\n *\n * const greeting = createTemplate({ name: 'greeting', content: 'Hi {{name}}' })\n * greeting.fill({ name: 'Ada' }) // 'Hi Ada'\n *\n * const templates = createTemplateManager({\n * \ttemplates: [\n * \t\t{ id: 'greeting', name: 'greeting', content: 'Hi {{name}}', category: 'mail' },\n * \t\t{ id: 'farewell', name: 'farewell', content: 'Bye {{name}}', category: 'mail' },\n * \t\t{ id: 'alert', name: 'alert', content: 'Alert: {{reason}}', category: 'ops' },\n * \t],\n * })\n * templates.fill('greeting', { name: 'Ada' }) // 'Hi Ada'\n * templates.find({ category: 'mail' }).map((one) => one.id) // ['greeting', 'farewell']\n * templates.has('alert') // true\n * templates.has('missing') // false\n * ```\n */\nexport function createTemplate(options: TemplateOptions): TemplateInterface {\n\treturn new Template(options)\n}\n\n/**\n * Creates a working {@link TemplateManagerInterface}, optionally seeded with\n * the templates the options carry, backed by the `TemplateManager` class.\n *\n * @param options - Optional initial `templates` seed collection and\n * manager-wide `missing` / `locale` fill defaults, emitter `on` hooks, and\n * an `error` handler\n * @returns A working {@link TemplateManagerInterface}\n * @throws {@link TemplateError} Thrown when a seeded `options.templates` bag declares a duplicate placeholder `name` or an empty `path` (coded `INVALID`)\n *\n * @example\n * ```ts\n * import { createTemplateManager } from '@src/core'\n *\n * const templates = createTemplateManager({\n * \ttemplates: [{ id: 'greeting', name: 'greeting', content: 'Hi {{name}}' }],\n * })\n * templates.fill('greeting', { name: 'Ada' }) // 'Hi Ada'\n * ```\n */\nexport function createTemplateManager(options?: TemplateManagerOptions): TemplateManagerInterface {\n\treturn new TemplateManager(options)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/core/constants.ts","../../../src/core/errors.ts","../../../src/core/helpers.ts","../../../src/core/shapers.ts","../../../src/core/templates/Template.ts","../../../src/core/templates/TemplateManager.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { MissingPolicy } from './types.js'\n\n// Frozen default data for the template module — constants are\n// UPPER_SNAKE_CASE data, the sole home for module-scope literal defaults.\n\n/**\n * Holds the single-pass `{{name}}` substitution pattern shared by\n * `Template#fill` and `Template#validate`.\n *\n * @remarks\n * Global-flagged, two-alternative pattern: a match of the first alternative\n * (`\\{{` — a literal backslash followed by `{{`) means \"emit a literal\n * `{{`\" — the escape hatch for content that must show `{{` without\n * triggering substitution. A match that instead populates capture group 1\n * (`\\{{([^{}]+?)\\}\\}`) means \"substitute the named token\" — group 1 is the\n * untrimmed token text between the braces; every call site trims it\n * (`token.trim()`) before using it as a lookup name, so `'{{ name }}'` still\n * resolves `'name'`. The pattern deliberately does not wrap the token in\n * `\\s*` — an unclosed `'{{' + ' '.repeat(n)` with no closing `}}` would\n * otherwise force the regex engine into catastrophic backtracking over the\n * whitespace run (O(n^2)); trimming after the match keeps the same\n * whitespace tolerance without the backtracking hazard. Every call site\n * builds a fresh `RegExp` from `.source` / `.flags` rather than sharing this\n * instance's mutable `lastIndex` across scans.\n */\nexport const FILL_PATTERN = /\\\\\\{\\{|\\{\\{([^{}]+?)\\}\\}/g\n\n/**\n * Holds `'error'`, the default `missing` policy for `Template#fill` /\n * `TemplateManager#fill` when unspecified.\n */\nexport const DEFAULT_MISSING_POLICY: MissingPolicy = 'error'\n\n/**\n * Holds `'en-US'`, the default `locale` for `Template#fill` /\n * `TemplateManager#fill` when unspecified.\n */\nexport const DEFAULT_LOCALE = 'en-US'\n\n/**\n * Lists the prototype-pollution-unsafe field-path segments `'__proto__'`,\n * `'constructor'`, and `'prototype'` — a fill lookup refuses to resolve a path\n * containing one of them, treating the placeholder as unresolved.\n */\nexport const UNSAFE_FIELD_SEGMENTS: readonly string[] = Object.freeze([\n\t'__proto__',\n\t'constructor',\n\t'prototype',\n])\n","import type { TemplateErrorCode } from './types.js'\nimport { holds, INTRINSICS, isFunction, isObject } from '@orkestrel/contract'\n\n// Misuse of the template layer `throw`s a `TemplateError` carrying a\n// machine-readable `code`, so a `catch` branches on `error.code`.\n\n/**\n * Represents an error thrown by the template layer — a machine-readable\n * {@link TemplateErrorCode} and an optional `context` record naming the\n * offending id or placeholder name.\n *\n * @remarks\n * Thrown for: a required placeholder staying unresolved under the `error`\n * {@link MissingPolicy} (`MISSING`), an unknown template id\n * (`NOTFOUND`), `createTemplate` handed invalid data (`INVALID`), and\n * `TemplateManagerInterface#register` handed an id already present without\n * `options.replace` (`CONFLICT`).\n */\nexport class TemplateError extends Error {\n\treadonly code: TemplateErrorCode\n\treadonly context?: Readonly<Record<string, unknown>>\n\n\tconstructor(\n\t\tcode: TemplateErrorCode,\n\t\tmessage: string,\n\t\tcontext?: Readonly<Record<string, unknown>>,\n\t) {\n\t\tsuper(message)\n\t\tINTRINSICS.define(this, Symbol.for('@orkestrel/template.TemplateError'), { value: this })\n\t\tthis.name = 'TemplateError'\n\t\tthis.code = code\n\t\tif (context !== undefined) this.context = context\n\t}\n}\n\n/**\n * Narrows an unknown caught value to a {@link TemplateError}.\n *\n * @remarks\n * Recognizes self-branded errors from other package copies and subclasses, and\n * refuses transparent proxies and unbranded look-alikes.\n *\n * @param value - The value to test (typically a `catch` binding)\n * @returns True if `value` is a {@link TemplateError}; false otherwise\n *\n * @example\n * ```ts\n * import { isTemplateError } from '@src/core'\n *\n * try {\n * \tmanager.fill('missing')\n * } catch (error) {\n * \tif (isTemplateError(error) && error.code === 'NOTFOUND') return\n * }\n * ```\n */\nexport function isTemplateError(value: unknown): value is TemplateError {\n\treturn holds(() => {\n\t\tif (!(value instanceof Error) || INTRINSICS.prototype(value) === Error.prototype) return false\n\t\tif (\n\t\t\tINTRINSICS.describe(value, Symbol.for('@orkestrel/template.TemplateError'))?.value !== value\n\t\t)\n\t\t\treturn false\n\t\tconst code: unknown = INTRINSICS.reflect.read(value, 'code')\n\t\tconst context: unknown = INTRINSICS.reflect.read(value, 'context')\n\t\treturn (\n\t\t\t(code === 'MISSING' || code === 'NOTFOUND' || code === 'INVALID' || code === 'CONFLICT') &&\n\t\t\t(context === undefined || isObject(context) || isFunction(context))\n\t\t)\n\t})\n}\n","import type { FieldPath } from '@orkestrel/contract'\nimport type {\n\tTemplateFillContext,\n\tTemplateFillValues,\n\tTemplatePlaceholder,\n\tTemplateTokenResolution,\n} from './types.js'\nimport { isArray, isFiniteNumber, resolveField } from '@orkestrel/contract'\nimport {\n\tDEFAULT_LOCALE,\n\tDEFAULT_MISSING_POLICY,\n\tFILL_PATTERN,\n\tUNSAFE_FIELD_SEGMENTS,\n} from './constants.js'\nimport { TemplateError } from './errors.js'\n\n// The templates pure-leaf inventory — every function here is a\n// referentially-transparent computation with no instance state, exported and\n// independently unit-testable. `Template#fill` / `#validate` route through\n// these leaves rather than duplicating the substitution logic.\n\n/**\n * Formats a resolved fill value for substitution into a template's `content`.\n *\n * @remarks\n * A finite number renders with the given locale's thousand grouping (through\n * `toLocaleString`); every other value — including `null` — String-coerces.\n * `null` therefore renders as the literal string `'null'`, matching\n * `String(value)` exactly, so a resolved `null` is visible in the output\n * rather than silently empty. An\n * invalid BCP-47 `locale` tag throws a `RangeError` from the underlying\n * `toLocaleString` call when `value` is a finite number — this is a caller\n * error (an invalid locale argument), by design, and is not caught here.\n *\n * @param value - The resolved value to format\n * @param locale - The locale used for finite-number formatting\n * @returns The formatted string\n *\n * @example\n * ```ts\n * import { formatValue } from '@src/core'\n *\n * formatValue(5010, 'en-US') // '5,010'\n * formatValue(null, 'en-US') // 'null'\n * ```\n */\nexport function formatValue(value: unknown, locale: string): string {\n\tif (isFiniteNumber(value)) return value.toLocaleString(locale)\n\treturn String(value)\n}\n\n/**\n * Resolves a field path against a fill-values record, refusing any path that\n * touches a prototype-pollution-unsafe segment.\n *\n * @remarks\n * A prototype-pollution guard shared by `fillTemplate` and `Template#validate`\n * so the two stay in lockstep: `path` normalizes to a segment array (a bare\n * string `path` becomes a single-segment array); if any segment appears in\n * `UNSAFE_FIELD_SEGMENTS` (`'__proto__'`, `'constructor'`, `'prototype'`), the\n * lookup is refused and `undefined` is returned without ever calling\n * `resolveField` — a path like `['__proto__', 'polluted']` can never reach\n * the record's actual prototype chain through this function. Every other\n * path resolves through `@orkestrel/contract`'s `resolveField`.\n *\n * @param record - The fill-values record to resolve against\n * @param path - The field path — a single segment or a segment array\n * @returns The resolved value, or `undefined` when unresolved or the path is unsafe\n *\n * @example\n * ```ts\n * import { resolveSafeField } from '@src/core'\n *\n * resolveSafeField({ a: { b: 1 } }, ['a', 'b']) // 1\n * resolveSafeField({}, ['__proto__', 'polluted']) // undefined\n * ```\n */\nexport function resolveSafeField(record: TemplateFillValues, path: FieldPath): unknown {\n\tconst segments = isArray(path) ? path : [path]\n\tif (segments.some((segment) => UNSAFE_FIELD_SEGMENTS.includes(segment))) return undefined\n\treturn resolveField(record, path)\n}\n\n/**\n * Resolves one `{{name}}` token against the declared placeholders and the\n * fill-values record.\n *\n * @remarks\n * The single implementation of the token rule `fillTemplate` and\n * `Template#validate` both apply, so the two can never drift: the declared\n * {@link TemplatePlaceholder} sharing the token's `name` (exact match)\n * supplies its `path`, falling back to the token split on `.`; the value\n * resolves through `resolveSafeField`, so any segment in\n * `UNSAFE_FIELD_SEGMENTS` yields `undefined` without ever calling\n * `resolveField`; `required` is `true` for an undeclared token and for a\n * declared placeholder whose `required` is not `false`. The token is passed\n * already trimmed. `fallback` is not applied here — it is read from\n * `declared` by each caller, because `fill` substitutes it and `validate`\n * only counts it.\n *\n * @param record - The fill-values record the token resolves against\n * @param placeholders - The declared placeholders the token matches by name\n * @param token - The trimmed token text, without its `{{` / `}}` delimiters\n * @returns The {@link TemplateTokenResolution} for the token\n *\n * @example\n * ```ts\n * import { resolveToken } from '@src/core'\n *\n * resolveToken({ name: 'Ada' }, [], 'name').value // 'Ada'\n * resolveToken({}, [{ name: 'nickname', required: false }], 'nickname').required // false\n * ```\n */\nexport function resolveToken(\n\trecord: TemplateFillValues,\n\tplaceholders: readonly TemplatePlaceholder[],\n\ttoken: string,\n): TemplateTokenResolution {\n\tconst declared = placeholders.find((placeholder) => placeholder.name === token)\n\tconst path = declared?.path ?? token.split('.')\n\treturn {\n\t\tvalue: resolveSafeField(record, path),\n\t\tdeclared,\n\t\trequired: declared === undefined || declared.required !== false,\n\t}\n}\n\n/**\n * Substitutes every `{{name}}` token in `content` in a single pass.\n *\n * @remarks\n * Uses a fresh `RegExp` clone of `FILL_PATTERN` per call (never sharing its\n * `lastIndex`) and a single `String#replace` scan — substituted output is\n * never re-scanned. Each token resolves through `resolveToken`, the one rule\n * `Template#validate` also applies: the matching declared\n * {@link TemplatePlaceholder} (exact `name`) supplies its `path` (falling\n * back to the token split on `.`); any path segment in `UNSAFE_FIELD_SEGMENTS`\n * makes the token unresolved without ever calling `resolveField` (a\n * prototype-pollution guard). A resolved value formats through `formatValue`; an\n * unresolved value falls back to the placeholder's `fallback` when declared;\n * otherwise `options.missing` governs — `'literal'` re-emits the original\n * `{{name}}` text, `'empty'` emits `''`, and `'error'` emits `''` for every\n * token but collects every unresolved required token (an undeclared token, or\n * a declared token with `required !== false`) and throws one\n * {@link TemplateError} coded `MISSING` listing them all, in first-appearance\n * order, once the scan completes. An escaped `\\{{` emits a literal `{{`.\n *\n * Called with no declared `placeholders` and `{ missing: 'empty' }`, this is a\n * bare interpolation over `content` — every token resolves by dotted path\n * against the values record and every unresolved token emits `''`.\n * `FILL_PATTERN`'s token class (`[^{}]`) excludes `{`, so a token containing\n * `{` never matches and the surrounding `{{` stays literal.\n *\n * @param content - The template content carrying `{{name}}` tokens\n * @param values - The values tokens resolve against\n * @param options - `missing` (default `'error'`), `locale` (default `'en-US'`), and the declared `placeholders` (default none) tokens resolve against\n * @returns The substituted content\n *\n * @example\n * ```ts\n * import { fillTemplate } from '@src/core'\n *\n * fillTemplate('Hi {{name}}', { name: 'Ada' }) // 'Hi Ada'\n * fillTemplate('Limit {{limit}}', { limit: 5010 }, { missing: 'empty' }) // 'Limit 5,010'\n * ```\n */\nexport function fillTemplate(\n\tcontent: string,\n\tvalues?: TemplateFillValues,\n\toptions?: TemplateFillContext,\n): string {\n\tconst placeholders = options?.placeholders ?? []\n\tconst missing = options?.missing ?? DEFAULT_MISSING_POLICY\n\tconst locale = options?.locale ?? DEFAULT_LOCALE\n\tconst record = values ?? {}\n\n\tconst missingNames: string[] = []\n\n\tconst pattern = new RegExp(FILL_PATTERN.source, FILL_PATTERN.flags)\n\tconst result = content.replace(pattern, (matchText: string, rawToken: string | undefined) => {\n\t\tif (rawToken === undefined) return '{{'\n\t\tconst token = rawToken.trim()\n\n\t\tconst { value, declared, required } = resolveToken(record, placeholders, token)\n\n\t\tif (value !== undefined) return formatValue(value, locale)\n\t\tif (declared?.fallback !== undefined) return formatValue(declared.fallback, locale)\n\n\t\tif (missing === 'literal') return matchText\n\t\tif (missing === 'empty') return ''\n\n\t\tif (required && !missingNames.includes(token)) {\n\t\t\tmissingNames.push(token)\n\t\t}\n\t\treturn ''\n\t})\n\n\tif (missing === 'error' && missingNames.length > 0) {\n\t\tthrow new TemplateError(\n\t\t\t'MISSING',\n\t\t\t`Missing required placeholder(s): ${missingNames.join(', ')}`,\n\t\t\t{ missing: missingNames },\n\t\t)\n\t}\n\n\treturn result\n}\n","import type { ContractShape } from '@orkestrel/contract'\nimport type { TemplatePlaceholder } from './types.js'\nimport { objectShape, optionalShape, stringShape } from '@orkestrel/contract'\n\n// The templates shape-value inventory — every function here builds an\n// `@orkestrel/contract` shape from declared template data. Shapers sit above\n// the `helpers.ts` leaf pair: they consume it, and it never consumes them.\n\n/**\n * Builds the `@orkestrel/contract` object shape describing a template's\n * declared placeholders.\n *\n * @remarks\n * Each placeholder becomes a `stringShape` carrying its `description`;\n * `required === false` wraps it in `optionalShape`. Used by `Template` to\n * compile its `parameters()` contract once per instance.\n *\n * @param placeholders - The declared placeholders to shape\n * @returns The contract shape for `createContract`\n *\n * @example\n * ```ts\n * import { placeholderShape } from '@src/core'\n * import { createContract } from '@orkestrel/contract'\n *\n * const contract = createContract(placeholderShape([{ name: 'city' }]))\n * ```\n */\nexport function placeholderShape(placeholders: readonly TemplatePlaceholder[]): ContractShape {\n\tconst properties: Record<string, ContractShape> = {}\n\tfor (const placeholder of placeholders) {\n\t\tconst description = placeholder.description\n\t\tconst field = stringShape({\n\t\t\t...(description !== undefined ? { description } : {}),\n\t\t})\n\t\tproperties[placeholder.name] = placeholder.required === false ? optionalShape(field) : field\n\t}\n\treturn objectShape(properties)\n}\n","import type { ContractInterface } from '@orkestrel/contract'\nimport type {\n\tMissingPolicy,\n\tTemplateDefinition,\n\tTemplateFillOptions,\n\tTemplateFillValues,\n\tTemplateInterface,\n\tTemplateOptions,\n\tTemplatePlaceholder,\n\tTemplateValidationResult,\n} from '../types.js'\nimport { createContract, isArray, isString, schemaToParameters } from '@orkestrel/contract'\nimport { DEFAULT_LOCALE, DEFAULT_MISSING_POLICY, FILL_PATTERN } from '../constants.js'\nimport { fillTemplate, resolveToken } from '../helpers.js'\nimport { placeholderShape } from '../shapers.js'\nimport { TemplateError } from '../errors.js'\n\n/**\n * Represents a named, versionable template — `{{name}}` tokens in `content`,\n * filled against a values record — implementing `TemplateInterface` exactly.\n *\n * @remarks\n * `missing` / `locale` seed this instance's default {@link TemplateFillOptions},\n * overridable per `fill` call. Its `parameters()` contract (built from\n * `placeholders` through `placeholderShape`) compiles once, in the constructor.\n *\n * @throws {@link TemplateError} Thrown when `options.placeholders` declares a duplicate `name` or an empty `path` (coded `INVALID`)\n *\n * @example\n * ```ts\n * const greeting = new Template({ name: 'greeting', content: 'Hi {{name}}' })\n * greeting.fill({ name: 'Ada' }) // 'Hi Ada'\n * ```\n */\nexport class Template implements TemplateInterface {\n\treadonly #missing: MissingPolicy\n\treadonly #locale: string\n\treadonly #contract: ContractInterface<unknown>\n\treadonly id: string\n\treadonly name: string\n\treadonly content: string\n\treadonly placeholders: readonly TemplatePlaceholder[]\n\treadonly summary?: string\n\treadonly description?: string\n\treadonly category?: string\n\treadonly tags?: readonly string[]\n\n\tconstructor(options: TemplateOptions) {\n\t\tconst placeholders = options.placeholders ?? []\n\t\tIterator.from(placeholders).reduce<readonly string[]>((names, placeholder) => {\n\t\t\tif (names.includes(placeholder.name)) {\n\t\t\t\tthrow new TemplateError('INVALID', `Duplicate placeholder name: ${placeholder.name}`, {\n\t\t\t\t\tname: placeholder.name,\n\t\t\t\t})\n\t\t\t}\n\t\t\tconst name = placeholder.name\n\t\t\tif (isArray(placeholder.path) && placeholder.path.length === 0) {\n\t\t\t\tthrow new TemplateError(\n\t\t\t\t\t'INVALID',\n\t\t\t\t\t`Placeholder path must not be empty: ${placeholder.name}`,\n\t\t\t\t\t{ name: placeholder.name },\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn [...names, name]\n\t\t}, [])\n\n\t\tthis.id = isString(options.id) ? options.id : crypto.randomUUID()\n\t\tthis.name = options.name\n\t\tthis.content = options.content\n\t\tthis.placeholders = placeholders\n\t\tif (options.summary !== undefined) this.summary = options.summary\n\t\tif (options.description !== undefined) this.description = options.description\n\t\tif (options.category !== undefined) this.category = options.category\n\t\tif (options.tags !== undefined) this.tags = options.tags\n\t\tthis.#missing = options.missing ?? DEFAULT_MISSING_POLICY\n\t\tthis.#locale = options.locale ?? DEFAULT_LOCALE\n\t\tthis.#contract = createContract(placeholderShape(this.placeholders))\n\t}\n\n\t/**\n\t * Returns the plain, JSON-serializable data this template carries.\n\t *\n\t * @returns The {@link TemplateDefinition} record\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({ name: 'greeting', content: 'Hi {{name}}' })\n\t * instance.definition().name // 'greeting'\n\t * ```\n\t */\n\tdefinition(): TemplateDefinition {\n\t\treturn {\n\t\t\tid: this.id,\n\t\t\tname: this.name,\n\t\t\tcontent: this.content,\n\t\t\tplaceholders: this.placeholders,\n\t\t\t...(this.summary !== undefined ? { summary: this.summary } : {}),\n\t\t\t...(this.description !== undefined ? { description: this.description } : {}),\n\t\t\t...(this.category !== undefined ? { category: this.category } : {}),\n\t\t\t...(this.tags !== undefined ? { tags: this.tags } : {}),\n\t\t}\n\t}\n\n\t/**\n\t * Substitutes every `{{name}}` token in `content` against `values`.\n\t *\n\t * @param values - The values tokens resolve against\n\t * @param options - Per-call overrides for this instance's `missing` / `locale` defaults\n\t * @returns The substituted content\n\t * @throws {@link TemplateError} Thrown when a required placeholder stays unresolved under the `'error'` policy (coded `MISSING`)\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({ name: 'greeting', content: 'Hi {{name}}' })\n\t * instance.fill({ name: 'Ada' }) // 'Hi Ada'\n\t * ```\n\t */\n\tfill(values?: TemplateFillValues, options?: TemplateFillOptions): string {\n\t\treturn fillTemplate(this.content, values, {\n\t\t\tmissing: options?.missing ?? this.#missing,\n\t\t\tlocale: options?.locale ?? this.#locale,\n\t\t\tplaceholders: this.placeholders,\n\t\t})\n\t}\n\n\t/**\n\t * Reports which required placeholders would stay unresolved, and which\n\t * `values` keys go unused, without producing output.\n\t *\n\t * @remarks\n\t * Content-token driven: scans `this.content` for every `{{name}}` token\n\t * (skipping escaped `\\{{` matches) the same way `fill` does, so `validate`\n\t * predicts `fill`'s `'error'`-{@link MissingPolicy} outcome exactly — a\n\t * token reported here as missing is precisely a token that would throw\n\t * under `fill(values, { missing: 'error' })`. For each distinct token\n\t * (first-appearance order, trimmed): `resolveToken` applies the one shared\n\t * token rule `fill` also applies — a declared {@link TemplatePlaceholder}\n\t * sharing its `name` supplies `path` (falling back to the token split on\n\t * `.`), and the value resolves through `resolveSafeField`. The token is `missing`\n\t * only when the value is unresolved, no `fallback` is declared, and the\n\t * placeholder is required (`required !== false`, including undeclared\n\t * tokens). `extra` lists every `values` key with no declared placeholder.\n\t *\n\t * @param values - The values to check\n\t * @returns The {@link TemplateValidationResult}\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({\n\t * \tname: 'greeting',\n\t * \tcontent: 'Hi {{name}}',\n\t * \tplaceholders: [{ name: 'name' }],\n\t * })\n\t * instance.validate({}).missing // ['name']\n\t * ```\n\t */\n\tvalidate(values?: TemplateFillValues): TemplateValidationResult {\n\t\tconst record = values ?? {}\n\t\tconst pattern = new RegExp(FILL_PATTERN.source, FILL_PATTERN.flags)\n\t\tconst tokens = new Set(\n\t\t\tArray.from(this.content.matchAll(pattern)).flatMap((match) =>\n\t\t\t\tmatch[1] === undefined ? [] : [match[1].trim()],\n\t\t\t),\n\t\t)\n\t\tconst missing = [...tokens].filter((token) => {\n\t\t\tconst { value, declared, required } = resolveToken(record, this.placeholders, token)\n\t\t\treturn value === undefined && declared?.fallback === undefined && required\n\t\t})\n\n\t\tconst declaredNames = new Set(this.placeholders.map((placeholder) => placeholder.name))\n\t\tconst extra = Object.keys(record).filter((key) => !declaredNames.has(key))\n\n\t\treturn { valid: missing.length === 0, missing, extra }\n\t}\n\n\t/**\n\t * Projects this template's placeholders to the open tool-parameters record\n\t * shape.\n\t *\n\t * @returns The compiled parameters record, or `undefined` when `schemaToParameters` yields none\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = new Template({\n\t * \tname: 'greeting',\n\t * \tcontent: 'Hi {{name}}',\n\t * \tplaceholders: [{ name: 'name' }],\n\t * })\n\t * instance.parameters()\n\t * ```\n\t */\n\tparameters(): Readonly<Record<string, unknown>> | undefined {\n\t\treturn schemaToParameters(this.#contract.schema)\n\t}\n}\n","import type {\n\tMissingPolicy,\n\tTemplateFillValues,\n\tTemplateFillOptions,\n\tTemplateInterface,\n\tTemplateManagerEventMap,\n\tTemplateManagerInterface,\n\tTemplateManagerOptions,\n\tTemplateOptions,\n\tTemplateQuery,\n\tTemplateRegisterOptions,\n\tTemplateValidationResult,\n} from '../types.js'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport { isFunction, isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport { DEFAULT_LOCALE, DEFAULT_MISSING_POLICY } from '../constants.js'\nimport { TemplateError } from '../errors.js'\nimport { Template } from './Template.js'\n\n/**\n * Represents the template registry — a self-owning, id-keyed record-holder for\n * the {@link TemplateInterface} instances a consumer registers, looks up,\n * fills, and validates by id — implementing `TemplateManagerInterface`\n * exactly.\n *\n * @remarks\n * Singular and plural accessors, the batch `remove` overloads, and ownership\n * of the emitter all sit here. `register` accepts either a constructed {@link TemplateInterface} (kept\n * as-is, including its own `missing` / `locale` defaults) or a plain\n * {@link TemplateOptions} bag — constructed into a `Template` with this\n * manager's `missing` / `locale` defaults applied wherever the bag omits\n * them. A duplicate `id` throws a {@link TemplateError} coded `CONFLICT`\n * unless `options.replace` is `true`, in which case the existing entry is\n * overwritten. `options.templates` seeds the registry at construction without\n * emitting `register` — only calls to `register` after construction emit.\n * The batch `remove(ids)` form removes every present id and returns\n * `true` only when every listed id was present.\n *\n * @example\n * ```ts\n * import { TemplateManager } from '@src/core'\n *\n * const manager = new TemplateManager()\n * const instance = manager.register({ name: 'greeting', content: 'Hi {{name}}' })\n * manager.fill(instance.id, { name: 'Ada' }) // 'Hi Ada'\n * ```\n */\nexport class TemplateManager implements TemplateManagerInterface {\n\treadonly #templates = new Map<string, TemplateInterface>()\n\treadonly #emitter: Emitter<TemplateManagerEventMap>\n\treadonly #missing: MissingPolicy\n\treadonly #locale: string\n\n\tconstructor(options?: TemplateManagerOptions) {\n\t\tconst on = options?.on\n\t\tconst error = options?.error\n\t\tthis.#emitter = new Emitter<TemplateManagerEventMap>({\n\t\t\t...(on !== undefined ? { on } : {}),\n\t\t\t...(error !== undefined ? { error } : {}),\n\t\t})\n\t\tthis.#missing = options?.missing ?? DEFAULT_MISSING_POLICY\n\t\tthis.#locale = options?.locale ?? DEFAULT_LOCALE\n\t\tfor (const template of options?.templates ?? []) {\n\t\t\tconst instance = this.#instantiate(template)\n\t\t\tthis.#templates.set(instance.id, instance)\n\t\t}\n\t}\n\n\tget emitter(): EmitterInterface<TemplateManagerEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget count(): number {\n\t\treturn this.#templates.size\n\t}\n\n\t/**\n\t * Registers a template — a constructed {@link TemplateInterface} (kept\n\t * as-is) or a plain {@link TemplateOptions} bag (constructed into a\n\t * `Template` with this manager's `missing` / `locale` defaults applied\n\t * wherever the bag omits them).\n\t *\n\t * @param template - The template instance or options to register\n\t * @param options - `replace` — overwrite an existing entry sharing the same id instead of throwing\n\t * @returns The registered {@link TemplateInterface}\n\t * @throws {@link TemplateError} Thrown when the id is already registered and `options.replace` is not `true` (coded `CONFLICT`), or when an options bag declares a duplicate placeholder `name` or an empty `path` (coded `INVALID`)\n\t *\n\t * @example\n\t * ```ts\n\t * const instance = manager.register({ id: 'greeting', name: 'greeting', content: 'Hi {{name}}' })\n\t * ```\n\t */\n\tregister(\n\t\ttemplate: TemplateInterface | TemplateOptions,\n\t\toptions?: TemplateRegisterOptions,\n\t): TemplateInterface {\n\t\tconst instance = this.#instantiate(template)\n\t\tconst existing = this.#templates.get(instance.id)\n\t\tif (existing !== undefined && options?.replace !== true) {\n\t\t\tthrow new TemplateError('CONFLICT', `Template already registered: ${instance.id}`, {\n\t\t\t\tid: instance.id,\n\t\t\t})\n\t\t}\n\t\tthis.#templates.set(instance.id, instance)\n\t\tthis.#emitter.emit('register', instance)\n\t\treturn instance\n\t}\n\n\t/**\n\t * Returns one registered {@link TemplateInterface} by id.\n\t *\n\t * @param id - The template id\n\t * @returns The registered {@link TemplateInterface}, or `undefined` when `id` is unregistered\n\t */\n\ttemplate(id: string): TemplateInterface | undefined {\n\t\treturn this.#templates.get(id)\n\t}\n\n\t/**\n\t * Lists every registered template.\n\t *\n\t * @returns A snapshot array of every registered {@link TemplateInterface}\n\t */\n\ttemplates(): readonly TemplateInterface[] {\n\t\treturn [...this.#templates.values()]\n\t}\n\n\t/**\n\t * Filters registered templates by `name`, `category`, and `tag` — every\n\t * supplied field must match.\n\t *\n\t * @param query - The {@link TemplateQuery} to filter by; omit for every registered template\n\t * @returns The matching templates\n\t */\n\tfind(query?: TemplateQuery): readonly TemplateInterface[] {\n\t\tif (query === undefined) return this.templates()\n\t\treturn this.templates().filter((instance) => {\n\t\t\tif (query.name !== undefined && instance.name !== query.name) return false\n\t\t\tif (query.category !== undefined && instance.category !== query.category) return false\n\t\t\tif (query.tag !== undefined && !(instance.tags ?? []).includes(query.tag)) return false\n\t\t\treturn true\n\t\t})\n\t}\n\n\t/**\n\t * Tests whether a template id is registered.\n\t *\n\t * @param id - The template id\n\t * @returns True if `id` is registered; false otherwise\n\t */\n\thas(id: string): boolean {\n\t\treturn this.#templates.has(id)\n\t}\n\n\t/**\n\t * Removes one, several, or every registered template.\n\t *\n\t * @remarks\n\t * `remove()` removes every registered template, emitting `remove` once per\n\t * instance. `remove(id)` removes one template by id, emitting `remove` and\n\t * returning `true` when it existed, `false` otherwise. `remove(ids)`\n\t * removes every listed id that is present, emitting `remove` once per\n\t * removed instance, and returns `true` only when every listed id was\n\t * present.\n\t *\n\t * @param target - Omit to remove all, a single id, or a list of ids\n\t * @returns `boolean` for the single-id / list-of-ids forms; `void` for the remove-all form\n\t */\n\t// `readonly string[]` is not assignable to `id: string`, so a list resolves to the\n\t// batch signature whatever order the signatures are declared in.\n\tremove(ids: readonly string[]): boolean\n\tremove(id: string): boolean\n\tremove(): void\n\tremove(target?: string | readonly string[]): boolean | void {\n\t\tif (target === undefined) {\n\t\t\tfor (const instance of this.#templates.values()) this.#emitter.emit('remove', instance)\n\t\t\tthis.#templates.clear()\n\t\t\treturn\n\t\t}\n\t\tif (isString(target)) {\n\t\t\tconst instance = this.#templates.get(target)\n\t\t\tif (instance === undefined) return false\n\t\t\tthis.#templates.delete(target)\n\t\t\tthis.#emitter.emit('remove', instance)\n\t\t\treturn true\n\t\t}\n\t\treturn this.#remove(target).reduce((all, removed) => removed && all, true)\n\t}\n\n\t/** Removes every registered template, emitting `clear`. */\n\tclear(): void {\n\t\tthis.#templates.clear()\n\t\tthis.#emitter.emit('clear')\n\t}\n\n\t/**\n\t * Tears down the registry: drops every registered template and destroys the\n\t * owned emitter. Idempotent.\n\t *\n\t * @remarks\n\t * Teardown is not an observable registry operation and the emitter is being\n\t * released, so this emits neither `clear` nor `remove`. The emitter is torn\n\t * down last, after the registry is dropped.\n\t *\n\t * @example\n\t * ```ts\n\t * const manager = new TemplateManager()\n\t * manager.destroy()\n\t * manager.emitter.destroyed // true\n\t * ```\n\t */\n\tdestroy(): void {\n\t\tthis.#templates.clear()\n\t\tthis.#emitter.destroy()\n\t}\n\n\t/**\n\t * Fills a registered template by id.\n\t *\n\t * @param id - The template id\n\t * @param values - The values tokens resolve against\n\t * @param options - Per-call overrides for the template's `missing` / `locale` defaults\n\t * @returns The substituted content\n\t * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown\n\t * @throws {@link TemplateError} Thrown when a required placeholder stays unresolved under the `'error'` policy (coded `MISSING`)\n\t */\n\tfill(id: string, values?: TemplateFillValues, options?: TemplateFillOptions): string {\n\t\treturn this.#require(id).fill(values, options)\n\t}\n\n\t/**\n\t * Validates values against a registered template by id.\n\t *\n\t * @param id - The template id\n\t * @param values - The values to check\n\t * @returns The {@link TemplateValidationResult}\n\t * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown\n\t */\n\tvalidate(id: string, values?: TemplateFillValues): TemplateValidationResult {\n\t\treturn this.#require(id).validate(values)\n\t}\n\n\t/**\n\t * Projects a registered template's parameters by id.\n\t *\n\t * @param id - The template id\n\t * @returns The compiled parameters record, or `undefined` when the template has none\n\t * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown\n\t */\n\tparameters(id: string): Readonly<Record<string, unknown>> | undefined {\n\t\treturn this.#require(id).parameters()\n\t}\n\n\t#instantiate(template: TemplateInterface | TemplateOptions): TemplateInterface {\n\t\tif (this.#isInstance(template)) return template\n\t\treturn new Template({\n\t\t\t...template,\n\t\t\tmissing: template.missing ?? this.#missing,\n\t\t\tlocale: template.locale ?? this.#locale,\n\t\t})\n\t}\n\n\t*#remove(target: readonly string[]): Generator<boolean> {\n\t\tfor (const id of target) {\n\t\t\tconst instance = this.#templates.get(id)\n\t\t\tif (instance === undefined) {\n\t\t\t\tyield false\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tthis.#templates.delete(id)\n\t\t\tthis.#emitter.emit('remove', instance)\n\t\t\tyield true\n\t\t}\n\t}\n\n\t// A TemplateOptions bag is plain data with no `fill` / `validate` /\n\t// `parameters` methods; a TemplateInterface instance always exposes all three.\n\t#isInstance(template: TemplateInterface | TemplateOptions): template is TemplateInterface {\n\t\treturn (\n\t\t\t'fill' in template &&\n\t\t\tisFunction(template.fill) &&\n\t\t\t'validate' in template &&\n\t\t\tisFunction(template.validate) &&\n\t\t\t'parameters' in template &&\n\t\t\tisFunction(template.parameters)\n\t\t)\n\t}\n\n\t// Every by-id operation that needs a template to proceed shares this lookup.\n\t// The `template` accessor deliberately does not, and returns `undefined`.\n\t#require(id: string): TemplateInterface {\n\t\tconst instance = this.#templates.get(id)\n\t\tif (instance === undefined) {\n\t\t\tthrow new TemplateError('NOTFOUND', `Unknown template id: ${id}`, { id })\n\t\t}\n\t\treturn instance\n\t}\n}\n","import type {\n\tTemplateInterface,\n\tTemplateManagerInterface,\n\tTemplateManagerOptions,\n\tTemplateOptions,\n} from './types.js'\nimport { Template } from './templates/Template.js'\nimport { TemplateManager } from './templates/TemplateManager.js'\n\n/**\n * Creates a working {@link TemplateInterface} from a {@link TemplateOptions}\n * bag, backed by the `Template` class.\n *\n * @param options - The template's `name` / `content`, an optional `id`\n * (defaults to a generated UUID), `placeholders`, catalog metadata, and\n * `missing` / `locale` fill defaults\n * @returns A working {@link TemplateInterface}\n * @throws {@link TemplateError} Thrown when `options.placeholders` declares a duplicate `name` or an empty `path` (coded `INVALID`)\n *\n * @example Create a template and a registry\n * ```ts\n * import { createTemplate, createTemplateManager } from '@orkestrel/template'\n *\n * const greeting = createTemplate({ name: 'greeting', content: 'Hi {{name}}' })\n * greeting.fill({ name: 'Ada' }) // 'Hi Ada'\n *\n * const templates = createTemplateManager({\n * \ttemplates: [\n * \t\t{ id: 'greeting', name: 'greeting', content: 'Hi {{name}}', category: 'mail' },\n * \t\t{ id: 'farewell', name: 'farewell', content: 'Bye {{name}}', category: 'mail' },\n * \t\t{ id: 'alert', name: 'alert', content: 'Alert: {{reason}}', category: 'ops' },\n * \t],\n * })\n * templates.fill('greeting', { name: 'Ada' }) // 'Hi Ada'\n * templates.find({ category: 'mail' }).map((one) => one.id) // ['greeting', 'farewell']\n * templates.has('alert') // true\n * templates.has('missing') // false\n * ```\n */\nexport function createTemplate(options: TemplateOptions): TemplateInterface {\n\treturn new Template(options)\n}\n\n/**\n * Creates a working {@link TemplateManagerInterface}, optionally seeded with\n * the templates the options carry, backed by the `TemplateManager` class.\n *\n * @param options - Optional initial `templates` seed collection and\n * manager-wide `missing` / `locale` fill defaults, emitter `on` hooks, and\n * an `error` handler\n * @returns A working {@link TemplateManagerInterface}\n * @throws {@link TemplateError} Thrown when a seeded `options.templates` bag declares a duplicate placeholder `name` or an empty `path` (coded `INVALID`)\n *\n * @example\n * ```ts\n * import { createTemplateManager } from '@src/core'\n *\n * const templates = createTemplateManager({\n * \ttemplates: [{ id: 'greeting', name: 'greeting', content: 'Hi {{name}}' }],\n * })\n * templates.fill('greeting', { name: 'Ada' }) // 'Hi Ada'\n * ```\n */\nexport function createTemplateManager(options?: TemplateManagerOptions): TemplateManagerInterface {\n\treturn new TemplateManager(options)\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orkestrel/template",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "Stateful template registry and filler with typed placeholders. Part of the @orkestrel line.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"template",
|
|
@@ -61,26 +61,26 @@
|
|
|
61
61
|
"prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test && npm run test:distribution -- --mode release",
|
|
62
62
|
"test:distribution": "vitest run --config vite.config.ts --no-cache --reporter=dot --project distribution",
|
|
63
63
|
"test:probe": "vitest run --config vite.config.ts --no-cache --reporter=verbose --project probe",
|
|
64
|
-
"test:bench": "vitest bench --config vite.config.ts --no-cache --project probe",
|
|
64
|
+
"test:bench": "vitest bench --config vite.config.ts --no-cache --project \"probe (bench)\" --mode benchmark",
|
|
65
65
|
"prepack": "npm run build",
|
|
66
66
|
"test:setup": "vitest run --config vite.config.ts --no-cache --reporter=dot --project setup"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@orkestrel/contract": "^0.0.
|
|
70
|
-
"@orkestrel/emitter": "^0.0.
|
|
69
|
+
"@orkestrel/contract": "^0.0.20",
|
|
70
|
+
"@orkestrel/emitter": "^0.0.13"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
|
-
"@microsoft/api-extractor": "^7.
|
|
74
|
-
"@orkestrel/guide": "^0.0.
|
|
75
|
-
"@orkestrel/probe": "^0.0.
|
|
76
|
-
"@orkestrel/scaffold": "^0.0.
|
|
77
|
-
"@orkestrel/test": "^0.0.
|
|
78
|
-
"@types/node": "^26.6.
|
|
79
|
-
"oxfmt": "^0.
|
|
80
|
-
"oxlint": "^1.
|
|
81
|
-
"typescript": "^
|
|
82
|
-
"vite": "^8.3.
|
|
83
|
-
"vitest": "^
|
|
73
|
+
"@microsoft/api-extractor": "^7.60.0",
|
|
74
|
+
"@orkestrel/guide": "^0.0.25",
|
|
75
|
+
"@orkestrel/probe": "^0.0.25",
|
|
76
|
+
"@orkestrel/scaffold": "^0.0.113",
|
|
77
|
+
"@orkestrel/test": "^0.0.31",
|
|
78
|
+
"@types/node": "^26.6.6",
|
|
79
|
+
"oxfmt": "^0.75.0",
|
|
80
|
+
"oxlint": "^1.90.0",
|
|
81
|
+
"typescript": "^7.0.2",
|
|
82
|
+
"vite": "^8.3.5",
|
|
83
|
+
"vitest": "^5.0.5"
|
|
84
84
|
},
|
|
85
85
|
"engines": {
|
|
86
86
|
"node": ">=22.12.0"
|