@orkestrel/template 0.0.5 → 0.0.7

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/README.md CHANGED
@@ -1,6 +1,14 @@
1
1
  # @orkestrel/template
2
2
 
3
- TODO: one-line description. Part of the `@orkestrel` line.
3
+ > A named, versionable template layer: `{{name}}` tokens in a `content`
4
+ > string, resolved against a values record by a single-pass fill engine, and
5
+ > registered and looked up by id through a self-owning `TemplateManager`.
6
+
7
+ Declare a template with the `createTemplate` function, fill it against the
8
+ values record your caller supplies, and register it in a `TemplateManager`
9
+ where several templates are looked up by id. Reach for `validate` where you
10
+ need to know which placeholders a fill would reject before you run it. Part of
11
+ the `@orkestrel` line.
4
12
 
5
13
  ## Install
6
14
 
@@ -8,17 +16,31 @@ TODO: one-line description. Part of the `@orkestrel` line.
8
16
  npm install @orkestrel/template
9
17
  ```
10
18
 
19
+ ## Requirements
20
+
21
+ - Node.js >= 22.12.0
22
+ - Runtime dependencies `@orkestrel/contract` and `@orkestrel/emitter`
23
+
11
24
  ## Usage
12
25
 
13
26
  ```ts
14
- import { createTemplate } from '@orkestrel/template'
27
+ import { createTemplate, createTemplateManager } from '@orkestrel/template'
15
28
 
16
- const instance = createTemplate({ id: 'example' })
29
+ const greeting = createTemplate({ name: 'greeting', content: 'Hi {{name}}' })
30
+ greeting.fill({ name: 'Ada' }) // 'Hi Ada'
31
+
32
+ const templates = createTemplateManager({ templates: [greeting] })
33
+ templates.fill(greeting.id, { name: 'Grace' }) // 'Hi Grace'
17
34
  ```
18
35
 
36
+ An unresolved required placeholder is governed by `TemplateFillOptions.missing`,
37
+ which defaults to `'error'` and throws a `TemplateError` coded `MISSING`.
38
+ `'empty'` substitutes `''` instead, and `'literal'` re-emits the original
39
+ `{{name}}` token.
40
+
19
41
  ## Guide
20
42
 
21
- For the full surface, see [`guides/src/template.md`](guides/src/template.md).
43
+ For the full surface, see [`guides/template.md`](guides/template.md).
22
44
 
23
45
  ## License
24
46
 
@@ -3,18 +3,18 @@ let _orkestrel_contract = require("@orkestrel/contract");
3
3
  let _orkestrel_emitter = require("@orkestrel/emitter");
4
4
  //#region src/core/constants.ts
5
5
  /**
6
- * The single-pass `{{name}}` substitution pattern shared by `Template#fill`
7
- * and `Template#validate`.
6
+ * Holds the single-pass `{{name}}` substitution pattern shared by
7
+ * `Template#fill` and `Template#validate`.
8
8
  *
9
9
  * @remarks
10
- * Global-flagged, two-alternative pattern: a match of the FIRST alternative
10
+ * Global-flagged, two-alternative pattern: a match of the first alternative
11
11
  * (`\{{` — a literal backslash followed by `{{`) means "emit a literal
12
12
  * `{{`" — the escape hatch for content that must show `{{` without
13
13
  * triggering substitution. A match that instead populates capture group 1
14
14
  * (`\{{([^{}]+?)\}\}`) means "substitute the named token" — group 1 is the
15
- * RAW (untrimmed) token text between the braces; every call site trims it
15
+ * untrimmed token text between the braces; every call site trims it
16
16
  * (`token.trim()`) before using it as a lookup name, so `'{{ name }}'` still
17
- * resolves `'name'`. The pattern intentionally does NOT wrap the token in
17
+ * resolves `'name'`. The pattern deliberately does not wrap the token in
18
18
  * `\s*` — an unclosed `'{{' + ' '.repeat(n)` with no closing `}}` would
19
19
  * otherwise force the regex engine into catastrophic backtracking over the
20
20
  * whitespace run (O(n^2)); trimming after the match keeps the same
@@ -23,13 +23,20 @@ let _orkestrel_emitter = require("@orkestrel/emitter");
23
23
  * instance's mutable `lastIndex` across scans.
24
24
  */
25
25
  var FILL_PATTERN = /\\\{\{|\{\{([^{}]+?)\}\}/g;
26
- /** Default `missing` policy for `Template#fill` / `TemplateManager#fill` when unspecified. */
26
+ /**
27
+ * Holds `'error'`, the default `missing` policy for `Template#fill` /
28
+ * `TemplateManager#fill` when unspecified.
29
+ */
27
30
  var DEFAULT_MISSING_POLICY = "error";
28
- /** Default `locale` for `Template#fill` / `TemplateManager#fill` when unspecified. */
31
+ /**
32
+ * Holds `'en-US'`, the default `locale` for `Template#fill` /
33
+ * `TemplateManager#fill` when unspecified.
34
+ */
29
35
  var DEFAULT_LOCALE = "en-US";
30
36
  /**
31
- * Prototype-pollution-unsafe field-path segments — a fill lookup refuses to
32
- * resolve ANY path containing one, treating the placeholder as unresolved.
37
+ * Lists the prototype-pollution-unsafe field-path segments `'__proto__'`,
38
+ * `'constructor'`, and `'prototype'` a fill lookup refuses to resolve a path
39
+ * containing one of them, treating the placeholder as unresolved.
33
40
  */
34
41
  var UNSAFE_FIELD_SEGMENTS = Object.freeze([
35
42
  "__proto__",
@@ -39,15 +46,16 @@ var UNSAFE_FIELD_SEGMENTS = Object.freeze([
39
46
  //#endregion
40
47
  //#region src/core/errors.ts
41
48
  /**
42
- * An error thrown by the template layer.
49
+ * Represents an error thrown by the template layer — a machine-readable
50
+ * {@link TemplateErrorCode} and an optional `context` record naming the
51
+ * offending id or placeholder name.
43
52
  *
44
53
  * @remarks
45
54
  * Thrown for: a required placeholder staying unresolved under the `error`
46
55
  * {@link MissingPolicy} (`MISSING`), an unknown template id
47
56
  * (`NOTFOUND`), `createTemplate` handed invalid data (`INVALID`), and
48
57
  * `TemplateManagerInterface#register` handed an id already present without
49
- * `options.replace` (`CONFLICT`). `context`, when present, carries the
50
- * offending id / name.
58
+ * `options.replace` (`CONFLICT`).
51
59
  */
52
60
  var TemplateError = class extends Error {
53
61
  code;
@@ -60,17 +68,17 @@ var TemplateError = class extends Error {
60
68
  }
61
69
  };
62
70
  /**
63
- * Narrow an unknown caught value to a {@link TemplateError}.
71
+ * Narrows an unknown caught value to a {@link TemplateError}.
64
72
  *
65
73
  * @param value - The value to test (typically a `catch` binding)
66
- * @returns `true` when `value` is a {@link TemplateError}
74
+ * @returns True if `value` is a {@link TemplateError}; false otherwise
67
75
  *
68
76
  * @example
69
77
  * ```ts
70
78
  * import { isTemplateError } from '@src/core'
71
79
  *
72
80
  * try {
73
- * manager.template('missing')
81
+ * manager.fill('missing')
74
82
  * } catch (error) {
75
83
  * if (isTemplateError(error) && error.code === 'NOTFOUND') return
76
84
  * }
@@ -82,13 +90,14 @@ function isTemplateError(value) {
82
90
  //#endregion
83
91
  //#region src/core/helpers.ts
84
92
  /**
85
- * Format a resolved fill value for substitution into a template's `content`.
93
+ * Formats a resolved fill value for substitution into a template's `content`.
86
94
  *
87
95
  * @remarks
88
- * A finite number renders with the given locale's thousand grouping (via
96
+ * A finite number renders with the given locale's thousand grouping (through
89
97
  * `toLocaleString`); every other value — including `null` — String-coerces.
90
- * `null` therefore renders as the literal string `'null'`, intentionally
91
- * mirroring `interpolateMessage`'s coercion parity (see `fillTemplate`). An
98
+ * `null` therefore renders as the literal string `'null'`, matching
99
+ * `String(value)` exactly, so a resolved `null` is visible in the output
100
+ * rather than silently empty. An
92
101
  * invalid BCP-47 `locale` tag throws a `RangeError` from the underlying
93
102
  * `toLocaleString` call when `value` is a finite number — this is a caller
94
103
  * error (an invalid locale argument), by design, and is not caught here.
@@ -110,15 +119,15 @@ function formatValue(value, locale) {
110
119
  return String(value);
111
120
  }
112
121
  /**
113
- * Resolve a field path against a fill-values record, refusing any path that
122
+ * Resolves a field path against a fill-values record, refusing any path that
114
123
  * touches a prototype-pollution-unsafe segment.
115
124
  *
116
125
  * @remarks
117
126
  * A prototype-pollution guard shared by `fillTemplate` and `Template#validate`
118
127
  * so the two stay in lockstep: `path` normalizes to a segment array (a bare
119
- * string `path` becomes a single-segment array); if ANY segment appears in
128
+ * string `path` becomes a single-segment array); if any segment appears in
120
129
  * `UNSAFE_FIELD_SEGMENTS` (`'__proto__'`, `'constructor'`, `'prototype'`), the
121
- * lookup is refused and `undefined` is returned WITHOUT ever calling
130
+ * lookup is refused and `undefined` is returned without ever calling
122
131
  * `resolveField` — a path like `['__proto__', 'polluted']` can never reach
123
132
  * the record's actual prototype chain through this function. Every other
124
133
  * path resolves through `@orkestrel/contract`'s `resolveField`.
@@ -140,29 +149,68 @@ function resolveSafeField(record, path) {
140
149
  return (0, _orkestrel_contract.resolveField)(record, path);
141
150
  }
142
151
  /**
143
- * Substitute every `{{name}}` token in `content` in a single pass.
152
+ * Resolves one `{{name}}` token against the declared placeholders and the
153
+ * fill-values record.
154
+ *
155
+ * @remarks
156
+ * The single implementation of the token rule `fillTemplate` and
157
+ * `Template#validate` both apply, so the two can never drift: the declared
158
+ * {@link TemplatePlaceholder} sharing the token's `name` (exact match)
159
+ * supplies its `path`, falling back to the token split on `.`; the value
160
+ * resolves through `resolveSafeField`, so any segment in
161
+ * `UNSAFE_FIELD_SEGMENTS` yields `undefined` without ever calling
162
+ * `resolveField`; `required` is `true` for an undeclared token and for a
163
+ * declared placeholder whose `required` is not `false`. The token is passed
164
+ * already trimmed. `fallback` is not applied here — it is read from
165
+ * `declared` by each caller, because `fill` substitutes it and `validate`
166
+ * only counts it.
167
+ *
168
+ * @param record - The fill-values record the token resolves against
169
+ * @param placeholders - The declared placeholders the token matches by name
170
+ * @param token - The trimmed token text, without its `{{` / `}}` delimiters
171
+ * @returns The {@link TemplateTokenResolution} for the token
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * import { resolveToken } from '@src/core'
176
+ *
177
+ * resolveToken({ name: 'Ada' }, [], 'name').value // 'Ada'
178
+ * resolveToken({}, [{ name: 'nickname', required: false }], 'nickname').required // false
179
+ * ```
180
+ */
181
+ function resolveToken(record, placeholders, token) {
182
+ const declared = placeholders.find((placeholder) => placeholder.name === token);
183
+ return {
184
+ value: resolveSafeField(record, declared?.path ?? token.split(".")),
185
+ declared,
186
+ required: declared === void 0 || declared.required !== false
187
+ };
188
+ }
189
+ /**
190
+ * Substitutes every `{{name}}` token in `content` in a single pass.
144
191
  *
145
192
  * @remarks
146
193
  * Uses a fresh `RegExp` clone of `FILL_PATTERN` per call (never sharing its
147
194
  * `lastIndex`) and a single `String#replace` scan — substituted output is
148
- * never re-scanned. For each token: the matching declared
195
+ * never re-scanned. Each token resolves through `resolveToken`, the one rule
196
+ * `Template#validate` also applies: the matching declared
149
197
  * {@link TemplatePlaceholder} (exact `name`) supplies its `path` (falling
150
- * back to the token split on `.`); ANY path segment in `UNSAFE_FIELD_SEGMENTS`
198
+ * back to the token split on `.`); any path segment in `UNSAFE_FIELD_SEGMENTS`
151
199
  * makes the token unresolved without ever calling `resolveField` (a
152
- * prototype-pollution guard). A resolved value formats via `formatValue`; an
200
+ * prototype-pollution guard). A resolved value formats through `formatValue`; an
153
201
  * unresolved value falls back to the placeholder's `fallback` when declared;
154
202
  * otherwise `options.missing` governs — `'literal'` re-emits the original
155
203
  * `{{name}}` text, `'empty'` emits `''`, and `'error'` emits `''` for every
156
- * token but collects EVERY unresolved required token (an undeclared token, or
204
+ * token but collects every unresolved required token (an undeclared token, or
157
205
  * a declared token with `required !== false`) and throws one
158
206
  * {@link TemplateError} coded `MISSING` listing them all, in first-appearance
159
207
  * order, once the scan completes. An escaped `\{{` emits a literal `{{`.
160
208
  *
161
- * PARITY: called with no declared `placeholders` and `{ missing: 'empty' }`,
162
- * this reproduces `interpolateMessage` (`@src/core` sibling
163
- * `interpret`) vector-for-vector. KNOWN DIVERGENCE: `FILL_PATTERN`'s token
164
- * class (`[^{}]`) excludes `{`, where `interpolateMessage`'s (`[^}]`) allows
165
- * it a token containing `{` therefore behaves differently here.
209
+ * Called with no declared `placeholders` and `{ missing: 'empty' }`, this is a
210
+ * bare interpolation over `content` every token resolves by dotted path
211
+ * against the values record and every unresolved token emits `''`.
212
+ * `FILL_PATTERN`'s token class (`[^{}]`) excludes `{`, so a token containing
213
+ * `{` never matches and the surrounding `{{` stays literal.
166
214
  *
167
215
  * @param content - The template content carrying `{{name}}` tokens
168
216
  * @param values - The values tokens resolve against
@@ -188,14 +236,12 @@ function fillTemplate(content, values, options) {
188
236
  const result = content.replace(pattern, (matchText, rawToken) => {
189
237
  if (rawToken === void 0) return "{{";
190
238
  const token = rawToken.trim();
191
- const declared = placeholders.find((placeholder) => placeholder.name === token);
192
- const path = declared?.path ?? token.split(".");
193
- const value = resolveSafeField(record, path);
239
+ const { value, declared, required } = resolveToken(record, placeholders, token);
194
240
  if (value !== void 0) return formatValue(value, locale);
195
241
  if (declared?.fallback !== void 0) return formatValue(declared.fallback, locale);
196
242
  if (missing === "literal") return matchText;
197
243
  if (missing === "empty") return "";
198
- if ((declared === void 0 || declared.required !== false) && !seen.has(token)) {
244
+ if (required && !seen.has(token)) {
199
245
  seen.add(token);
200
246
  missingNames.push(token);
201
247
  }
@@ -204,8 +250,10 @@ function fillTemplate(content, values, options) {
204
250
  if (missing === "error" && missingNames.length > 0) throw new TemplateError("MISSING", `Missing required placeholder(s): ${missingNames.join(", ")}`, { missing: missingNames });
205
251
  return result;
206
252
  }
253
+ //#endregion
254
+ //#region src/core/shapers.ts
207
255
  /**
208
- * Build the `@orkestrel/contract` object shape describing a template's
256
+ * Builds the `@orkestrel/contract` object shape describing a template's
209
257
  * declared placeholders.
210
258
  *
211
259
  * @remarks
@@ -234,15 +282,17 @@ function placeholderShape(placeholders) {
234
282
  return (0, _orkestrel_contract.objectShape)(properties);
235
283
  }
236
284
  //#endregion
237
- //#region src/core/Template.ts
285
+ //#region src/core/templates/Template.ts
238
286
  /**
239
- * A named, versionable template — `{{name}}` tokens in `content`, filled
240
- * against a values record.
287
+ * Represents a named, versionable template — `{{name}}` tokens in `content`,
288
+ * filled against a values record — implementing `TemplateInterface` exactly.
241
289
  *
242
290
  * @remarks
243
291
  * `missing` / `locale` seed this instance's default {@link TemplateFillOptions},
244
292
  * overridable per `fill` call. Its `parameters()` contract (built from
245
- * `placeholders` via `placeholderShape`) compiles once, in the constructor.
293
+ * `placeholders` through `placeholderShape`) compiles once, in the constructor.
294
+ *
295
+ * @throws {@link TemplateError} Thrown when `options.placeholders` declares a duplicate `name` or an empty `path` (coded `INVALID`)
246
296
  *
247
297
  * @example
248
298
  * ```ts
@@ -251,6 +301,9 @@ function placeholderShape(placeholders) {
251
301
  * ```
252
302
  */
253
303
  var Template = class {
304
+ #missing;
305
+ #locale;
306
+ #contract;
254
307
  id;
255
308
  name;
256
309
  content;
@@ -259,9 +312,6 @@ var Template = class {
259
312
  description;
260
313
  category;
261
314
  tags;
262
- #missing;
263
- #locale;
264
- #contract;
265
315
  constructor(options) {
266
316
  const placeholders = options.placeholders ?? [];
267
317
  const seenNames = /* @__PURE__ */ new Set();
@@ -283,7 +333,7 @@ var Template = class {
283
333
  this.#contract = (0, _orkestrel_contract.createContract)(placeholderShape(this.placeholders));
284
334
  }
285
335
  /**
286
- * The plain, JSON-serializable data this template carries.
336
+ * Returns the plain, JSON-serializable data this template carries.
287
337
  *
288
338
  * @returns The {@link TemplateDefinition} record
289
339
  *
@@ -306,11 +356,12 @@ var Template = class {
306
356
  };
307
357
  }
308
358
  /**
309
- * Substitute every `{{name}}` token in `content` against `values`.
359
+ * Substitutes every `{{name}}` token in `content` against `values`.
310
360
  *
311
361
  * @param values - The values tokens resolve against
312
362
  * @param options - Per-call overrides for this instance's `missing` / `locale` defaults
313
363
  * @returns The substituted content
364
+ * @throws {@link TemplateError} Thrown when a required placeholder stays unresolved under the `'error'` policy (coded `MISSING`)
314
365
  *
315
366
  * @example
316
367
  * ```ts
@@ -326,7 +377,7 @@ var Template = class {
326
377
  });
327
378
  }
328
379
  /**
329
- * Report which required placeholders would stay unresolved, and which
380
+ * Reports which required placeholders would stay unresolved, and which
330
381
  * `values` keys go unused, without producing output.
331
382
  *
332
383
  * @remarks
@@ -335,10 +386,11 @@ var Template = class {
335
386
  * predicts `fill`'s `'error'`-{@link MissingPolicy} outcome exactly — a
336
387
  * token reported here as missing is precisely a token that would throw
337
388
  * under `fill(values, { missing: 'error' })`. For each distinct token
338
- * (first-appearance order, trimmed): a declared {@link TemplatePlaceholder}
389
+ * (first-appearance order, trimmed): `resolveToken` applies the one shared
390
+ * token rule `fill` also applies — a declared {@link TemplatePlaceholder}
339
391
  * sharing its `name` supplies `path` (falling back to the token split on
340
- * `.`); the value resolves via `resolveSafeField`. The token is `missing`
341
- * only when the value is unresolved AND no `fallback` is declared AND the
392
+ * `.`), and the value resolves through `resolveSafeField`. The token is `missing`
393
+ * only when the value is unresolved, no `fallback` is declared, and the
342
394
  * placeholder is required (`required !== false`, including undeclared
343
395
  * tokens). `extra` lists every `values` key with no declared placeholder.
344
396
  *
@@ -366,10 +418,8 @@ var Template = class {
366
418
  const token = rawToken.trim();
367
419
  if (seen.has(token)) continue;
368
420
  seen.add(token);
369
- const declared = this.placeholders.find((placeholder) => placeholder.name === token);
370
- const resolved = resolveSafeField(record, declared?.path ?? token.split("."));
371
- const required = declared === void 0 || declared.required !== false;
372
- if (resolved === void 0 && declared?.fallback === void 0 && required) missing.push(token);
421
+ const { value, declared, required } = resolveToken(record, this.placeholders, token);
422
+ if (value === void 0 && declared?.fallback === void 0 && required) missing.push(token);
373
423
  }
374
424
  const declaredNames = new Set(this.placeholders.map((placeholder) => placeholder.name));
375
425
  const extra = Object.keys(record).filter((key) => !declaredNames.has(key));
@@ -380,7 +430,7 @@ var Template = class {
380
430
  };
381
431
  }
382
432
  /**
383
- * Project this template's placeholders to the open tool-parameters record
433
+ * Projects this template's placeholders to the open tool-parameters record
384
434
  * shape.
385
435
  *
386
436
  * @returns The compiled parameters record, or `undefined` when `schemaToParameters` yields none
@@ -400,24 +450,25 @@ var Template = class {
400
450
  }
401
451
  };
402
452
  //#endregion
403
- //#region src/core/TemplateManager.ts
453
+ //#region src/core/templates/TemplateManager.ts
404
454
  /**
405
- * The template registry — a self-owning, id-keyed record-holder for the
406
- * {@link TemplateInterface} instances a consumer registers, looks up, fills,
407
- * and validates by id (AGENTS §9.1 singular/plural accessors, §9.2 batch
408
- * `remove` overloads, §13 emitter ownership).
455
+ * Represents the template registry — a self-owning, id-keyed record-holder for
456
+ * the {@link TemplateInterface} instances a consumer registers, looks up,
457
+ * fills, and validates by id implementing `TemplateManagerInterface`
458
+ * exactly.
409
459
  *
410
460
  * @remarks
411
- * `register` accepts either a constructed {@link TemplateInterface} (kept
461
+ * Singular and plural accessors, the batch `remove` overloads, and ownership
462
+ * of the emitter all sit here. `register` accepts either a constructed {@link TemplateInterface} (kept
412
463
  * as-is, including its own `missing` / `locale` defaults) or a plain
413
464
  * {@link TemplateOptions} bag — constructed into a `Template` with this
414
465
  * manager's `missing` / `locale` defaults applied wherever the bag omits
415
466
  * them. A duplicate `id` throws a {@link TemplateError} coded `CONFLICT`
416
467
  * unless `options.replace` is `true`, in which case the existing entry is
417
- * overwritten. `options.templates` SEEDS the registry at construction
418
- * WITHOUT emitting `register` — only calls to `register` after construction
419
- * emit. The batch `remove(ids)` form is ALL-OR-NOTHING: any id absent from
420
- * the registry leaves the collection untouched and returns `false`.
468
+ * overwritten. `options.templates` seeds the registry at construction without
469
+ * emitting `register` — only calls to `register` after construction emit.
470
+ * The batch `remove(ids)` form removes every present id and returns
471
+ * `true` only when every listed id was present.
421
472
  *
422
473
  * @example
423
474
  * ```ts
@@ -450,11 +501,11 @@ var TemplateManager = class {
450
501
  get emitter() {
451
502
  return this.#emitter;
452
503
  }
453
- get size() {
504
+ get count() {
454
505
  return this.#templates.size;
455
506
  }
456
507
  /**
457
- * Register a template — a constructed {@link TemplateInterface} (kept
508
+ * Registers a template — a constructed {@link TemplateInterface} (kept
458
509
  * as-is) or a plain {@link TemplateOptions} bag (constructed into a
459
510
  * `Template` with this manager's `missing` / `locale` defaults applied
460
511
  * wherever the bag omits them).
@@ -462,6 +513,7 @@ var TemplateManager = class {
462
513
  * @param template - The template instance or options to register
463
514
  * @param options - `replace` — overwrite an existing entry sharing the same id instead of throwing
464
515
  * @returns The registered {@link TemplateInterface}
516
+ * @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`)
465
517
  *
466
518
  * @example
467
519
  * ```ts
@@ -476,19 +528,16 @@ var TemplateManager = class {
476
528
  return instance;
477
529
  }
478
530
  /**
479
- * Look up a registered template by id.
531
+ * Returns one registered {@link TemplateInterface} by id.
480
532
  *
481
533
  * @param id - The template id
482
- * @returns The registered {@link TemplateInterface}
483
- * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
534
+ * @returns The registered {@link TemplateInterface}, or `undefined` when `id` is unregistered
484
535
  */
485
536
  template(id) {
486
- const instance = this.#templates.get(id);
487
- if (instance === void 0) this.#throwNotFound(id);
488
- return instance;
537
+ return this.#templates.get(id);
489
538
  }
490
539
  /**
491
- * List every registered template.
540
+ * Lists every registered template.
492
541
  *
493
542
  * @returns A snapshot array of every registered {@link TemplateInterface}
494
543
  */
@@ -496,8 +545,8 @@ var TemplateManager = class {
496
545
  return [...this.#templates.values()];
497
546
  }
498
547
  /**
499
- * Filter registered templates by name / category / tag — every supplied
500
- * field must match (logical AND).
548
+ * Filters registered templates by `name`, `category`, and `tag` — every
549
+ * supplied field must match.
501
550
  *
502
551
  * @param query - The {@link TemplateQuery} to filter by; omit for every registered template
503
552
  * @returns The matching templates
@@ -512,10 +561,10 @@ var TemplateManager = class {
512
561
  });
513
562
  }
514
563
  /**
515
- * Test whether a template id is registered.
564
+ * Tests whether a template id is registered.
516
565
  *
517
566
  * @param id - The template id
518
- * @returns `true` when `id` is registered
567
+ * @returns True if `id` is registered; false otherwise
519
568
  */
520
569
  has(id) {
521
570
  return this.#templates.has(id);
@@ -533,34 +582,58 @@ var TemplateManager = class {
533
582
  this.#emitter.emit("remove", instance);
534
583
  return true;
535
584
  }
536
- for (const id of target) if (!this.#templates.has(id)) return false;
585
+ let all = true;
537
586
  for (const id of target) {
538
587
  const instance = this.#templates.get(id);
539
- if (instance === void 0) continue;
588
+ if (instance === void 0) {
589
+ all = false;
590
+ continue;
591
+ }
540
592
  this.#templates.delete(id);
541
593
  this.#emitter.emit("remove", instance);
542
594
  }
543
- return true;
595
+ return all;
544
596
  }
545
- /** Remove every registered template, emitting `clear`. */
597
+ /** Removes every registered template, emitting `clear`. */
546
598
  clear() {
547
599
  this.#templates.clear();
548
600
  this.#emitter.emit("clear");
549
601
  }
550
602
  /**
551
- * Fill a registered template by id.
603
+ * Tears down the registry: drops every registered template and destroys the
604
+ * owned emitter. Idempotent.
605
+ *
606
+ * @remarks
607
+ * Teardown is not an observable registry operation and the emitter is being
608
+ * released, so this emits neither `clear` nor `remove`. The emitter is torn
609
+ * down last, after the registry is dropped.
610
+ *
611
+ * @example
612
+ * ```ts
613
+ * const manager = new TemplateManager()
614
+ * manager.destroy()
615
+ * manager.emitter.destroyed // true
616
+ * ```
617
+ */
618
+ destroy() {
619
+ this.#templates.clear();
620
+ this.#emitter.destroy();
621
+ }
622
+ /**
623
+ * Fills a registered template by id.
552
624
  *
553
625
  * @param id - The template id
554
626
  * @param values - The values tokens resolve against
555
627
  * @param options - Per-call overrides for the template's `missing` / `locale` defaults
556
628
  * @returns The substituted content
557
629
  * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
630
+ * @throws {@link TemplateError} Thrown when a required placeholder stays unresolved under the `'error'` policy (coded `MISSING`)
558
631
  */
559
632
  fill(id, values, options) {
560
- return this.template(id).fill(values, options);
633
+ return this.#require(id).fill(values, options);
561
634
  }
562
635
  /**
563
- * Validate values against a registered template by id.
636
+ * Validates values against a registered template by id.
564
637
  *
565
638
  * @param id - The template id
566
639
  * @param values - The values to check
@@ -568,17 +641,17 @@ var TemplateManager = class {
568
641
  * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
569
642
  */
570
643
  validate(id, values) {
571
- return this.template(id).validate(values);
644
+ return this.#require(id).validate(values);
572
645
  }
573
646
  /**
574
- * Project a registered template's parameters by id.
647
+ * Projects a registered template's parameters by id.
575
648
  *
576
649
  * @param id - The template id
577
650
  * @returns The compiled parameters record, or `undefined` when the template has none
578
651
  * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
579
652
  */
580
653
  parameters(id) {
581
- return this.template(id).parameters();
654
+ return this.#require(id).parameters();
582
655
  }
583
656
  #instantiate(template) {
584
657
  if (this.#isInstance(template)) return template;
@@ -591,38 +664,56 @@ var TemplateManager = class {
591
664
  #isInstance(template) {
592
665
  return "fill" in template && typeof template.fill === "function" && "validate" in template && typeof template.validate === "function" && "parameters" in template && typeof template.parameters === "function";
593
666
  }
594
- #throwNotFound(id) {
595
- throw new TemplateError("NOTFOUND", `Unknown template id: ${id}`, { id });
667
+ #require(id) {
668
+ const instance = this.#templates.get(id);
669
+ if (instance === void 0) throw new TemplateError("NOTFOUND", `Unknown template id: ${id}`, { id });
670
+ return instance;
596
671
  }
597
672
  };
598
673
  //#endregion
599
674
  //#region src/core/factories.ts
600
675
  /**
601
- * Create a template.
676
+ * Creates a working {@link TemplateInterface} from a {@link TemplateOptions}
677
+ * bag, backed by the `Template` class.
602
678
  *
603
679
  * @param options - The template's `name` / `content`, an optional `id`
604
680
  * (defaults to a generated UUID), `placeholders`, catalog metadata, and
605
681
  * `missing` / `locale` fill defaults
606
682
  * @returns A working {@link TemplateInterface}
683
+ * @throws {@link TemplateError} Thrown when `options.placeholders` declares a duplicate `name` or an empty `path` (coded `INVALID`)
607
684
  *
608
- * @example
685
+ * @example Create a template and a registry
609
686
  * ```ts
610
- * import { createTemplate } from '@src/core'
687
+ * import { createTemplate, createTemplateManager } from '@orkestrel/template'
611
688
  *
612
689
  * const greeting = createTemplate({ name: 'greeting', content: 'Hi {{name}}' })
613
690
  * greeting.fill({ name: 'Ada' }) // 'Hi Ada'
691
+ *
692
+ * const templates = createTemplateManager({
693
+ * templates: [
694
+ * { id: 'greeting', name: 'greeting', content: 'Hi {{name}}', category: 'mail' },
695
+ * { id: 'farewell', name: 'farewell', content: 'Bye {{name}}', category: 'mail' },
696
+ * { id: 'alert', name: 'alert', content: 'Alert: {{reason}}', category: 'ops' },
697
+ * ],
698
+ * })
699
+ * templates.fill('greeting', { name: 'Ada' }) // 'Hi Ada'
700
+ * templates.find({ category: 'mail' }).map((one) => one.id) // ['greeting', 'farewell']
701
+ * templates.has('alert') // true
702
+ * templates.has('missing') // false
614
703
  * ```
615
704
  */
616
705
  function createTemplate(options) {
617
706
  return new Template(options);
618
707
  }
619
708
  /**
620
- * Create a template registry.
709
+ * Creates a working {@link TemplateManagerInterface}, optionally seeded with
710
+ * the templates the options carry, backed by the `TemplateManager` class.
621
711
  *
622
712
  * @param options - Optional initial `templates` seed collection and
623
713
  * manager-wide `missing` / `locale` fill defaults, emitter `on` hooks, and
624
714
  * an `error` handler
625
715
  * @returns A working {@link TemplateManagerInterface}
716
+ * @throws {@link TemplateError} Thrown when a seeded `options.templates` bag declares a duplicate placeholder `name` or an empty `path` (coded `INVALID`)
626
717
  *
627
718
  * @example
628
719
  * ```ts
@@ -652,5 +743,6 @@ exports.formatValue = formatValue;
652
743
  exports.isTemplateError = isTemplateError;
653
744
  exports.placeholderShape = placeholderShape;
654
745
  exports.resolveSafeField = resolveSafeField;
746
+ exports.resolveToken = resolveToken;
655
747
 
656
748
  //# sourceMappingURL=index.cjs.map