@orkestrel/template 0.0.5 → 0.0.6

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,12 @@
1
1
  # @orkestrel/template
2
2
 
3
- TODO: one-line description. Part of the `@orkestrel` line.
3
+ A stateful template registry and filler with typed placeholders — `{{name}}`
4
+ tokens in a `content` string, resolved against a values record by a
5
+ single-pass fill engine, and registered and looked up by id through
6
+ `TemplateManager`. Every fill lookup refuses a prototype-pollution-unsafe
7
+ field path: any segment in `UNSAFE_FIELD_SEGMENTS` (`__proto__`,
8
+ `constructor`, `prototype`) is refused before the record is ever read. Part of
9
+ the `@orkestrel` line.
4
10
 
5
11
  ## Install
6
12
 
@@ -8,17 +14,31 @@ TODO: one-line description. Part of the `@orkestrel` line.
8
14
  npm install @orkestrel/template
9
15
  ```
10
16
 
17
+ ## Requirements
18
+
19
+ - Node.js >= 22.12.0
20
+ - Runtime dependencies `@orkestrel/contract` and `@orkestrel/emitter`
21
+
11
22
  ## Usage
12
23
 
13
24
  ```ts
14
- import { createTemplate } from '@orkestrel/template'
25
+ import { createTemplate, createTemplateManager } from '@orkestrel/template'
15
26
 
16
- const instance = createTemplate({ id: 'example' })
27
+ const greeting = createTemplate({ name: 'greeting', content: 'Hi {{name}}' })
28
+ greeting.fill({ name: 'Ada' }) // 'Hi Ada'
29
+
30
+ const templates = createTemplateManager({ templates: [greeting] })
31
+ templates.fill(greeting.id, { name: 'Grace' }) // 'Hi Grace'
17
32
  ```
18
33
 
34
+ An unresolved required placeholder is governed by `TemplateFillOptions.missing`,
35
+ which defaults to `'error'` and throws a `TemplateError` coded `MISSING`.
36
+ `'empty'` substitutes `''` instead, and `'literal'` re-emits the original
37
+ `{{name}}` token.
38
+
19
39
  ## Guide
20
40
 
21
- For the full surface, see [`guides/src/template.md`](guides/src/template.md).
41
+ For the full surface, see [`guides/template.md`](guides/template.md).
22
42
 
23
43
  ## License
24
44
 
@@ -3,8 +3,8 @@ 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
10
  * Global-flagged, two-alternative pattern: a match of the FIRST alternative
@@ -23,13 +23,14 @@ 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
+ /** Holds the default `missing` policy for `Template#fill` / `TemplateManager#fill` when unspecified. */
27
27
  var DEFAULT_MISSING_POLICY = "error";
28
- /** Default `locale` for `Template#fill` / `TemplateManager#fill` when unspecified. */
28
+ /** Holds the default `locale` for `Template#fill` / `TemplateManager#fill` when unspecified. */
29
29
  var DEFAULT_LOCALE = "en-US";
30
30
  /**
31
- * Prototype-pollution-unsafe field-path segments — a fill lookup refuses to
32
- * resolve ANY path containing one, treating the placeholder as unresolved.
31
+ * Lists the prototype-pollution-unsafe field-path segments — a fill lookup
32
+ * refuses to resolve ANY path containing one, treating the placeholder as
33
+ * unresolved.
33
34
  */
34
35
  var UNSAFE_FIELD_SEGMENTS = Object.freeze([
35
36
  "__proto__",
@@ -39,7 +40,7 @@ var UNSAFE_FIELD_SEGMENTS = Object.freeze([
39
40
  //#endregion
40
41
  //#region src/core/errors.ts
41
42
  /**
42
- * An error thrown by the template layer.
43
+ * Represents an error thrown by the template layer.
43
44
  *
44
45
  * @remarks
45
46
  * Thrown for: a required placeholder staying unresolved under the `error`
@@ -60,17 +61,17 @@ var TemplateError = class extends Error {
60
61
  }
61
62
  };
62
63
  /**
63
- * Narrow an unknown caught value to a {@link TemplateError}.
64
+ * Narrows an unknown caught value to a {@link TemplateError}.
64
65
  *
65
66
  * @param value - The value to test (typically a `catch` binding)
66
- * @returns `true` when `value` is a {@link TemplateError}
67
+ * @returns True if `value` is a {@link TemplateError}; false otherwise
67
68
  *
68
69
  * @example
69
70
  * ```ts
70
71
  * import { isTemplateError } from '@src/core'
71
72
  *
72
73
  * try {
73
- * manager.template('missing')
74
+ * manager.fill('missing')
74
75
  * } catch (error) {
75
76
  * if (isTemplateError(error) && error.code === 'NOTFOUND') return
76
77
  * }
@@ -82,13 +83,14 @@ function isTemplateError(value) {
82
83
  //#endregion
83
84
  //#region src/core/helpers.ts
84
85
  /**
85
- * Format a resolved fill value for substitution into a template's `content`.
86
+ * Formats a resolved fill value for substitution into a template's `content`.
86
87
  *
87
88
  * @remarks
88
- * A finite number renders with the given locale's thousand grouping (via
89
+ * A finite number renders with the given locale's thousand grouping (through
89
90
  * `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
91
+ * `null` therefore renders as the literal string `'null'`, matching
92
+ * `String(value)` exactly, so a resolved `null` is visible in the output
93
+ * rather than silently empty. An
92
94
  * invalid BCP-47 `locale` tag throws a `RangeError` from the underlying
93
95
  * `toLocaleString` call when `value` is a finite number — this is a caller
94
96
  * error (an invalid locale argument), by design, and is not caught here.
@@ -110,7 +112,7 @@ function formatValue(value, locale) {
110
112
  return String(value);
111
113
  }
112
114
  /**
113
- * Resolve a field path against a fill-values record, refusing any path that
115
+ * Resolves a field path against a fill-values record, refusing any path that
114
116
  * touches a prototype-pollution-unsafe segment.
115
117
  *
116
118
  * @remarks
@@ -140,16 +142,55 @@ function resolveSafeField(record, path) {
140
142
  return (0, _orkestrel_contract.resolveField)(record, path);
141
143
  }
142
144
  /**
143
- * Substitute every `{{name}}` token in `content` in a single pass.
145
+ * Resolves one `{{name}}` token against the declared placeholders and the
146
+ * fill-values record.
147
+ *
148
+ * @remarks
149
+ * The single implementation of the token rule `fillTemplate` and
150
+ * `Template#validate` both apply, so the two can never drift: the declared
151
+ * {@link TemplatePlaceholder} sharing the token's `name` (exact match)
152
+ * supplies its `path`, falling back to the token split on `.`; the value
153
+ * resolves through `resolveSafeField`, so any segment in
154
+ * `UNSAFE_FIELD_SEGMENTS` yields `undefined` without ever calling
155
+ * `resolveField`; `required` is `true` for an undeclared token and for a
156
+ * declared placeholder whose `required` is not `false`. The token is passed
157
+ * already trimmed. `fallback` is not applied here — it is read from
158
+ * `declared` by each caller, because `fill` substitutes it and `validate`
159
+ * only counts it.
160
+ *
161
+ * @param record - The fill-values record the token resolves against
162
+ * @param placeholders - The declared placeholders the token matches by name
163
+ * @param token - The trimmed token text, without its `{{` / `}}` delimiters
164
+ * @returns The {@link TemplateTokenResolution} for the token
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * import { resolveToken } from '@src/core'
169
+ *
170
+ * resolveToken({ name: 'Ada' }, [], 'name').value // 'Ada'
171
+ * resolveToken({}, [{ name: 'nickname', required: false }], 'nickname').required // false
172
+ * ```
173
+ */
174
+ function resolveToken(record, placeholders, token) {
175
+ const declared = placeholders.find((placeholder) => placeholder.name === token);
176
+ return {
177
+ value: resolveSafeField(record, declared?.path ?? token.split(".")),
178
+ declared,
179
+ required: declared === void 0 || declared.required !== false
180
+ };
181
+ }
182
+ /**
183
+ * Substitutes every `{{name}}` token in `content` in a single pass.
144
184
  *
145
185
  * @remarks
146
186
  * Uses a fresh `RegExp` clone of `FILL_PATTERN` per call (never sharing its
147
187
  * `lastIndex`) and a single `String#replace` scan — substituted output is
148
- * never re-scanned. For each token: the matching declared
188
+ * never re-scanned. Each token resolves through `resolveToken`, the one rule
189
+ * `Template#validate` also applies: the matching declared
149
190
  * {@link TemplatePlaceholder} (exact `name`) supplies its `path` (falling
150
191
  * back to the token split on `.`); ANY path segment in `UNSAFE_FIELD_SEGMENTS`
151
192
  * makes the token unresolved without ever calling `resolveField` (a
152
- * prototype-pollution guard). A resolved value formats via `formatValue`; an
193
+ * prototype-pollution guard). A resolved value formats through `formatValue`; an
153
194
  * unresolved value falls back to the placeholder's `fallback` when declared;
154
195
  * otherwise `options.missing` governs — `'literal'` re-emits the original
155
196
  * `{{name}}` text, `'empty'` emits `''`, and `'error'` emits `''` for every
@@ -158,11 +199,11 @@ function resolveSafeField(record, path) {
158
199
  * {@link TemplateError} coded `MISSING` listing them all, in first-appearance
159
200
  * order, once the scan completes. An escaped `\{{` emits a literal `{{`.
160
201
  *
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.
202
+ * Called with no declared `placeholders` and `{ missing: 'empty' }`, this is a
203
+ * bare interpolation over `content` every token resolves by dotted path
204
+ * against the values record and every unresolved token emits `''`.
205
+ * `FILL_PATTERN`'s token class (`[^{}]`) excludes `{`, so a token containing
206
+ * `{` never matches and the surrounding `{{` stays literal.
166
207
  *
167
208
  * @param content - The template content carrying `{{name}}` tokens
168
209
  * @param values - The values tokens resolve against
@@ -188,14 +229,12 @@ function fillTemplate(content, values, options) {
188
229
  const result = content.replace(pattern, (matchText, rawToken) => {
189
230
  if (rawToken === void 0) return "{{";
190
231
  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);
232
+ const { value, declared, required } = resolveToken(record, placeholders, token);
194
233
  if (value !== void 0) return formatValue(value, locale);
195
234
  if (declared?.fallback !== void 0) return formatValue(declared.fallback, locale);
196
235
  if (missing === "literal") return matchText;
197
236
  if (missing === "empty") return "";
198
- if ((declared === void 0 || declared.required !== false) && !seen.has(token)) {
237
+ if (required && !seen.has(token)) {
199
238
  seen.add(token);
200
239
  missingNames.push(token);
201
240
  }
@@ -204,8 +243,10 @@ function fillTemplate(content, values, options) {
204
243
  if (missing === "error" && missingNames.length > 0) throw new TemplateError("MISSING", `Missing required placeholder(s): ${missingNames.join(", ")}`, { missing: missingNames });
205
244
  return result;
206
245
  }
246
+ //#endregion
247
+ //#region src/core/shapers.ts
207
248
  /**
208
- * Build the `@orkestrel/contract` object shape describing a template's
249
+ * Builds the `@orkestrel/contract` object shape describing a template's
209
250
  * declared placeholders.
210
251
  *
211
252
  * @remarks
@@ -234,15 +275,17 @@ function placeholderShape(placeholders) {
234
275
  return (0, _orkestrel_contract.objectShape)(properties);
235
276
  }
236
277
  //#endregion
237
- //#region src/core/Template.ts
278
+ //#region src/core/templates/Template.ts
238
279
  /**
239
- * A named, versionable template — `{{name}}` tokens in `content`, filled
240
- * against a values record.
280
+ * Represents a named, versionable template — `{{name}}` tokens in `content`,
281
+ * filled against a values record.
241
282
  *
242
283
  * @remarks
243
284
  * `missing` / `locale` seed this instance's default {@link TemplateFillOptions},
244
285
  * overridable per `fill` call. Its `parameters()` contract (built from
245
- * `placeholders` via `placeholderShape`) compiles once, in the constructor.
286
+ * `placeholders` through `placeholderShape`) compiles once, in the constructor.
287
+ *
288
+ * @throws {@link TemplateError} Thrown when `options.placeholders` declares a duplicate `name` or an empty `path` (coded `INVALID`)
246
289
  *
247
290
  * @example
248
291
  * ```ts
@@ -251,6 +294,9 @@ function placeholderShape(placeholders) {
251
294
  * ```
252
295
  */
253
296
  var Template = class {
297
+ #missing;
298
+ #locale;
299
+ #contract;
254
300
  id;
255
301
  name;
256
302
  content;
@@ -259,9 +305,6 @@ var Template = class {
259
305
  description;
260
306
  category;
261
307
  tags;
262
- #missing;
263
- #locale;
264
- #contract;
265
308
  constructor(options) {
266
309
  const placeholders = options.placeholders ?? [];
267
310
  const seenNames = /* @__PURE__ */ new Set();
@@ -283,7 +326,7 @@ var Template = class {
283
326
  this.#contract = (0, _orkestrel_contract.createContract)(placeholderShape(this.placeholders));
284
327
  }
285
328
  /**
286
- * The plain, JSON-serializable data this template carries.
329
+ * Returns the plain, JSON-serializable data this template carries.
287
330
  *
288
331
  * @returns The {@link TemplateDefinition} record
289
332
  *
@@ -306,11 +349,12 @@ var Template = class {
306
349
  };
307
350
  }
308
351
  /**
309
- * Substitute every `{{name}}` token in `content` against `values`.
352
+ * Substitutes every `{{name}}` token in `content` against `values`.
310
353
  *
311
354
  * @param values - The values tokens resolve against
312
355
  * @param options - Per-call overrides for this instance's `missing` / `locale` defaults
313
356
  * @returns The substituted content
357
+ * @throws {@link TemplateError} Thrown when a required placeholder stays unresolved under the `'error'` policy (coded `MISSING`)
314
358
  *
315
359
  * @example
316
360
  * ```ts
@@ -326,7 +370,7 @@ var Template = class {
326
370
  });
327
371
  }
328
372
  /**
329
- * Report which required placeholders would stay unresolved, and which
373
+ * Reports which required placeholders would stay unresolved, and which
330
374
  * `values` keys go unused, without producing output.
331
375
  *
332
376
  * @remarks
@@ -335,9 +379,10 @@ var Template = class {
335
379
  * predicts `fill`'s `'error'`-{@link MissingPolicy} outcome exactly — a
336
380
  * token reported here as missing is precisely a token that would throw
337
381
  * under `fill(values, { missing: 'error' })`. For each distinct token
338
- * (first-appearance order, trimmed): a declared {@link TemplatePlaceholder}
382
+ * (first-appearance order, trimmed): `resolveToken` applies the one shared
383
+ * token rule `fill` also applies — a declared {@link TemplatePlaceholder}
339
384
  * sharing its `name` supplies `path` (falling back to the token split on
340
- * `.`); the value resolves via `resolveSafeField`. The token is `missing`
385
+ * `.`), and the value resolves through `resolveSafeField`. The token is `missing`
341
386
  * only when the value is unresolved AND no `fallback` is declared AND the
342
387
  * placeholder is required (`required !== false`, including undeclared
343
388
  * tokens). `extra` lists every `values` key with no declared placeholder.
@@ -366,10 +411,8 @@ var Template = class {
366
411
  const token = rawToken.trim();
367
412
  if (seen.has(token)) continue;
368
413
  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);
414
+ const { value, declared, required } = resolveToken(record, this.placeholders, token);
415
+ if (value === void 0 && declared?.fallback === void 0 && required) missing.push(token);
373
416
  }
374
417
  const declaredNames = new Set(this.placeholders.map((placeholder) => placeholder.name));
375
418
  const extra = Object.keys(record).filter((key) => !declaredNames.has(key));
@@ -380,7 +423,7 @@ var Template = class {
380
423
  };
381
424
  }
382
425
  /**
383
- * Project this template's placeholders to the open tool-parameters record
426
+ * Projects this template's placeholders to the open tool-parameters record
384
427
  * shape.
385
428
  *
386
429
  * @returns The compiled parameters record, or `undefined` when `schemaToParameters` yields none
@@ -400,12 +443,12 @@ var Template = class {
400
443
  }
401
444
  };
402
445
  //#endregion
403
- //#region src/core/TemplateManager.ts
446
+ //#region src/core/templates/TemplateManager.ts
404
447
  /**
405
- * The template registry — a self-owning, id-keyed record-holder for the
448
+ * Represents the template registry — a self-owning, id-keyed record-holder for the
406
449
  * {@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).
450
+ * and validates by id, with singular/plural accessors, batch `remove`
451
+ * overloads, and emitter ownership.
409
452
  *
410
453
  * @remarks
411
454
  * `register` accepts either a constructed {@link TemplateInterface} (kept
@@ -416,8 +459,8 @@ var Template = class {
416
459
  * unless `options.replace` is `true`, in which case the existing entry is
417
460
  * overwritten. `options.templates` SEEDS the registry at construction
418
461
  * 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`.
462
+ * emit. The batch `remove(ids)` form removes every present id and returns
463
+ * `true` only when every listed id was present.
421
464
  *
422
465
  * @example
423
466
  * ```ts
@@ -450,11 +493,11 @@ var TemplateManager = class {
450
493
  get emitter() {
451
494
  return this.#emitter;
452
495
  }
453
- get size() {
496
+ get count() {
454
497
  return this.#templates.size;
455
498
  }
456
499
  /**
457
- * Register a template — a constructed {@link TemplateInterface} (kept
500
+ * Registers a template — a constructed {@link TemplateInterface} (kept
458
501
  * as-is) or a plain {@link TemplateOptions} bag (constructed into a
459
502
  * `Template` with this manager's `missing` / `locale` defaults applied
460
503
  * wherever the bag omits them).
@@ -462,6 +505,7 @@ var TemplateManager = class {
462
505
  * @param template - The template instance or options to register
463
506
  * @param options - `replace` — overwrite an existing entry sharing the same id instead of throwing
464
507
  * @returns The registered {@link TemplateInterface}
508
+ * @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
509
  *
466
510
  * @example
467
511
  * ```ts
@@ -476,19 +520,16 @@ var TemplateManager = class {
476
520
  return instance;
477
521
  }
478
522
  /**
479
- * Look up a registered template by id.
523
+ * Returns one registered {@link TemplateInterface} by id.
480
524
  *
481
525
  * @param id - The template id
482
- * @returns The registered {@link TemplateInterface}
483
- * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
526
+ * @returns The registered {@link TemplateInterface}, or `undefined` when `id` is unregistered
484
527
  */
485
528
  template(id) {
486
- const instance = this.#templates.get(id);
487
- if (instance === void 0) this.#throwNotFound(id);
488
- return instance;
529
+ return this.#templates.get(id);
489
530
  }
490
531
  /**
491
- * List every registered template.
532
+ * Lists every registered template.
492
533
  *
493
534
  * @returns A snapshot array of every registered {@link TemplateInterface}
494
535
  */
@@ -496,7 +537,7 @@ var TemplateManager = class {
496
537
  return [...this.#templates.values()];
497
538
  }
498
539
  /**
499
- * Filter registered templates by name / category / tag — every supplied
540
+ * Filters registered templates by name / category / tag — every supplied
500
541
  * field must match (logical AND).
501
542
  *
502
543
  * @param query - The {@link TemplateQuery} to filter by; omit for every registered template
@@ -512,10 +553,10 @@ var TemplateManager = class {
512
553
  });
513
554
  }
514
555
  /**
515
- * Test whether a template id is registered.
556
+ * Tests whether a template id is registered.
516
557
  *
517
558
  * @param id - The template id
518
- * @returns `true` when `id` is registered
559
+ * @returns True if `id` is registered; false otherwise
519
560
  */
520
561
  has(id) {
521
562
  return this.#templates.has(id);
@@ -533,34 +574,58 @@ var TemplateManager = class {
533
574
  this.#emitter.emit("remove", instance);
534
575
  return true;
535
576
  }
536
- for (const id of target) if (!this.#templates.has(id)) return false;
577
+ let all = true;
537
578
  for (const id of target) {
538
579
  const instance = this.#templates.get(id);
539
- if (instance === void 0) continue;
580
+ if (instance === void 0) {
581
+ all = false;
582
+ continue;
583
+ }
540
584
  this.#templates.delete(id);
541
585
  this.#emitter.emit("remove", instance);
542
586
  }
543
- return true;
587
+ return all;
544
588
  }
545
- /** Remove every registered template, emitting `clear`. */
589
+ /** Removes every registered template, emitting `clear`. */
546
590
  clear() {
547
591
  this.#templates.clear();
548
592
  this.#emitter.emit("clear");
549
593
  }
550
594
  /**
551
- * Fill a registered template by id.
595
+ * Tears down the registry: drops every registered template and destroys the
596
+ * owned emitter. Idempotent.
597
+ *
598
+ * @remarks
599
+ * Teardown is not an observable registry operation and the emitter is being
600
+ * released, so this emits neither `clear` nor `remove`. The emitter is torn
601
+ * down last, after the registry is dropped.
602
+ *
603
+ * @example
604
+ * ```ts
605
+ * const manager = new TemplateManager()
606
+ * manager.destroy()
607
+ * manager.emitter.destroyed // true
608
+ * ```
609
+ */
610
+ destroy() {
611
+ this.#templates.clear();
612
+ this.#emitter.destroy();
613
+ }
614
+ /**
615
+ * Fills a registered template by id.
552
616
  *
553
617
  * @param id - The template id
554
618
  * @param values - The values tokens resolve against
555
619
  * @param options - Per-call overrides for the template's `missing` / `locale` defaults
556
620
  * @returns The substituted content
557
621
  * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
622
+ * @throws {@link TemplateError} Thrown when a required placeholder stays unresolved under the `'error'` policy (coded `MISSING`)
558
623
  */
559
624
  fill(id, values, options) {
560
- return this.template(id).fill(values, options);
625
+ return this.#require(id).fill(values, options);
561
626
  }
562
627
  /**
563
- * Validate values against a registered template by id.
628
+ * Validates values against a registered template by id.
564
629
  *
565
630
  * @param id - The template id
566
631
  * @param values - The values to check
@@ -568,17 +633,17 @@ var TemplateManager = class {
568
633
  * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
569
634
  */
570
635
  validate(id, values) {
571
- return this.template(id).validate(values);
636
+ return this.#require(id).validate(values);
572
637
  }
573
638
  /**
574
- * Project a registered template's parameters by id.
639
+ * Projects a registered template's parameters by id.
575
640
  *
576
641
  * @param id - The template id
577
642
  * @returns The compiled parameters record, or `undefined` when the template has none
578
643
  * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
579
644
  */
580
645
  parameters(id) {
581
- return this.template(id).parameters();
646
+ return this.#require(id).parameters();
582
647
  }
583
648
  #instantiate(template) {
584
649
  if (this.#isInstance(template)) return template;
@@ -591,19 +656,22 @@ var TemplateManager = class {
591
656
  #isInstance(template) {
592
657
  return "fill" in template && typeof template.fill === "function" && "validate" in template && typeof template.validate === "function" && "parameters" in template && typeof template.parameters === "function";
593
658
  }
594
- #throwNotFound(id) {
595
- throw new TemplateError("NOTFOUND", `Unknown template id: ${id}`, { id });
659
+ #require(id) {
660
+ const instance = this.#templates.get(id);
661
+ if (instance === void 0) throw new TemplateError("NOTFOUND", `Unknown template id: ${id}`, { id });
662
+ return instance;
596
663
  }
597
664
  };
598
665
  //#endregion
599
666
  //#region src/core/factories.ts
600
667
  /**
601
- * Create a template.
668
+ * Creates a template.
602
669
  *
603
670
  * @param options - The template's `name` / `content`, an optional `id`
604
671
  * (defaults to a generated UUID), `placeholders`, catalog metadata, and
605
672
  * `missing` / `locale` fill defaults
606
673
  * @returns A working {@link TemplateInterface}
674
+ * @throws {@link TemplateError} Thrown when `options.placeholders` declares a duplicate `name` or an empty `path` (coded `INVALID`)
607
675
  *
608
676
  * @example
609
677
  * ```ts
@@ -617,12 +685,13 @@ function createTemplate(options) {
617
685
  return new Template(options);
618
686
  }
619
687
  /**
620
- * Create a template registry.
688
+ * Creates a template registry.
621
689
  *
622
690
  * @param options - Optional initial `templates` seed collection and
623
691
  * manager-wide `missing` / `locale` fill defaults, emitter `on` hooks, and
624
692
  * an `error` handler
625
693
  * @returns A working {@link TemplateManagerInterface}
694
+ * @throws {@link TemplateError} Thrown when a seeded `options.templates` bag declares a duplicate placeholder `name` or an empty `path` (coded `INVALID`)
626
695
  *
627
696
  * @example
628
697
  * ```ts
@@ -652,5 +721,6 @@ exports.formatValue = formatValue;
652
721
  exports.isTemplateError = isTemplateError;
653
722
  exports.placeholderShape = placeholderShape;
654
723
  exports.resolveSafeField = resolveSafeField;
724
+ exports.resolveToken = resolveToken;
655
725
 
656
726
  //# sourceMappingURL=index.cjs.map