@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/README.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# @orkestrel/template
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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/
|
|
43
|
+
For the full surface, see [`guides/template.md`](guides/template.md).
|
|
22
44
|
|
|
23
45
|
## License
|
|
24
46
|
|
package/dist/src/core/index.cjs
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
32
|
-
*
|
|
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
|
-
*
|
|
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`).
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
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 (
|
|
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'`,
|
|
91
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
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 `.`);
|
|
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
|
|
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
|
|
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
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* class (`[^{}]`) excludes `{`,
|
|
165
|
-
*
|
|
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 =
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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`
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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):
|
|
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
|
-
* `.`)
|
|
341
|
-
* only when the value is unresolved
|
|
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
|
|
370
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
406
|
-
* {@link TemplateInterface} instances a consumer registers, looks up,
|
|
407
|
-
* and validates by id
|
|
408
|
-
*
|
|
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
|
-
*
|
|
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`
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
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
|
|
504
|
+
get count() {
|
|
454
505
|
return this.#templates.size;
|
|
455
506
|
}
|
|
456
507
|
/**
|
|
457
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
487
|
-
if (instance === void 0) this.#throwNotFound(id);
|
|
488
|
-
return instance;
|
|
537
|
+
return this.#templates.get(id);
|
|
489
538
|
}
|
|
490
539
|
/**
|
|
491
|
-
*
|
|
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
|
-
*
|
|
500
|
-
* field must match
|
|
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
|
-
*
|
|
564
|
+
* Tests whether a template id is registered.
|
|
516
565
|
*
|
|
517
566
|
* @param id - The template id
|
|
518
|
-
* @returns
|
|
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
|
-
|
|
585
|
+
let all = true;
|
|
537
586
|
for (const id of target) {
|
|
538
587
|
const instance = this.#templates.get(id);
|
|
539
|
-
if (instance === void 0)
|
|
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
|
|
595
|
+
return all;
|
|
544
596
|
}
|
|
545
|
-
/**
|
|
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
|
-
*
|
|
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
|
|
633
|
+
return this.#require(id).fill(values, options);
|
|
561
634
|
}
|
|
562
635
|
/**
|
|
563
|
-
*
|
|
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
|
|
644
|
+
return this.#require(id).validate(values);
|
|
572
645
|
}
|
|
573
646
|
/**
|
|
574
|
-
*
|
|
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
|
|
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
|
-
#
|
|
595
|
-
|
|
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
|
-
*
|
|
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 '@
|
|
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
|
-
*
|
|
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
|