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