@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 +26 -4
- package/dist/src/core/index.cjs +186 -94
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +779 -603
- package/dist/src/core/index.d.ts +779 -603
- package/dist/src/core/index.js +186 -95
- package/dist/src/core/index.js.map +1 -1
- package/package.json +19 -16
package/dist/src/core/index.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
31
|
-
*
|
|
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
|
-
*
|
|
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`).
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
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 (
|
|
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'`,
|
|
90
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
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 `.`);
|
|
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
|
|
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
|
|
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
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* class (`[^{}]`) excludes `{`,
|
|
164
|
-
*
|
|
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 =
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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`
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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):
|
|
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
|
-
* `.`)
|
|
340
|
-
* only when the value is unresolved
|
|
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
|
|
369
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
405
|
-
* {@link TemplateInterface} instances a consumer registers, looks up,
|
|
406
|
-
* and validates by id
|
|
407
|
-
*
|
|
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
|
-
*
|
|
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`
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
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
|
|
503
|
+
get count() {
|
|
453
504
|
return this.#templates.size;
|
|
454
505
|
}
|
|
455
506
|
/**
|
|
456
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
486
|
-
if (instance === void 0) this.#throwNotFound(id);
|
|
487
|
-
return instance;
|
|
536
|
+
return this.#templates.get(id);
|
|
488
537
|
}
|
|
489
538
|
/**
|
|
490
|
-
*
|
|
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
|
-
*
|
|
499
|
-
* field must match
|
|
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
|
-
*
|
|
563
|
+
* Tests whether a template id is registered.
|
|
515
564
|
*
|
|
516
565
|
* @param id - The template id
|
|
517
|
-
* @returns
|
|
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
|
-
|
|
584
|
+
let all = true;
|
|
536
585
|
for (const id of target) {
|
|
537
586
|
const instance = this.#templates.get(id);
|
|
538
|
-
if (instance === void 0)
|
|
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
|
|
594
|
+
return all;
|
|
543
595
|
}
|
|
544
|
-
/**
|
|
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
|
-
*
|
|
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
|
|
632
|
+
return this.#require(id).fill(values, options);
|
|
560
633
|
}
|
|
561
634
|
/**
|
|
562
|
-
*
|
|
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
|
|
643
|
+
return this.#require(id).validate(values);
|
|
571
644
|
}
|
|
572
645
|
/**
|
|
573
|
-
*
|
|
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
|
|
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
|
-
#
|
|
594
|
-
|
|
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
|
-
*
|
|
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 '@
|
|
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
|
-
*
|
|
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
|