@texuli/deck 0.0.0-stage → 0.4.0
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/LICENSE +21 -0
- package/README.md +182 -2
- package/dist/authoring/builder.d.ts +351 -0
- package/dist/authoring/builder.js +64 -0
- package/dist/authoring/index.d.ts +6 -0
- package/dist/authoring/index.js +6 -0
- package/dist/authoring/lint.d.ts +36 -0
- package/dist/authoring/lint.js +325 -0
- package/dist/authoring/outlines.d.ts +34 -0
- package/dist/authoring/outlines.js +255 -0
- package/dist/authoring/patch.d.ts +23 -0
- package/dist/authoring/patch.js +166 -0
- package/dist/authoring/select-layouts.d.ts +19 -0
- package/dist/authoring/select-layouts.js +90 -0
- package/dist/engine/auto-slide.d.ts +32 -0
- package/dist/engine/auto-slide.js +67 -0
- package/dist/engine/canvas.d.ts +56 -0
- package/dist/engine/canvas.js +48 -0
- package/dist/engine/controller.d.ts +126 -0
- package/dist/engine/controller.js +248 -0
- package/dist/engine/controls.d.ts +128 -0
- package/dist/engine/controls.js +460 -0
- package/dist/engine/hash.d.ts +42 -0
- package/dist/engine/hash.js +86 -0
- package/dist/engine/index.d.ts +88 -0
- package/dist/engine/index.js +88 -0
- package/dist/engine/messages.d.ts +138 -0
- package/dist/engine/messages.js +140 -0
- package/dist/engine/motion.d.ts +64 -0
- package/dist/engine/motion.js +112 -0
- package/dist/engine/navigation.d.ts +87 -0
- package/dist/engine/navigation.js +156 -0
- package/dist/engine/presenter.d.ts +58 -0
- package/dist/engine/presenter.js +157 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/markdown/attributes.d.ts +24 -0
- package/dist/markdown/attributes.js +94 -0
- package/dist/markdown/attrs-closed.d.ts +52 -0
- package/dist/markdown/attrs-closed.js +520 -0
- package/dist/markdown/blocks.d.ts +112 -0
- package/dist/markdown/blocks.js +766 -0
- package/dist/markdown/frontmatter.d.ts +30 -0
- package/dist/markdown/frontmatter.js +165 -0
- package/dist/markdown/index.d.ts +29 -0
- package/dist/markdown/index.js +29 -0
- package/dist/markdown/issues.d.ts +68 -0
- package/dist/markdown/issues.js +83 -0
- package/dist/markdown/json.d.ts +21 -0
- package/dist/markdown/json.js +236 -0
- package/dist/markdown/limits.d.ts +32 -0
- package/dist/markdown/limits.js +32 -0
- package/dist/markdown/parse.d.ts +31 -0
- package/dist/markdown/parse.js +283 -0
- package/dist/markdown/scan.d.ts +26 -0
- package/dist/markdown/scan.js +138 -0
- package/dist/markdown/slides.d.ts +32 -0
- package/dist/markdown/slides.js +854 -0
- package/dist/markdown/text.d.ts +76 -0
- package/dist/markdown/text.js +447 -0
- package/dist/markdown/twin.d.ts +41 -0
- package/dist/markdown/twin.js +374 -0
- package/dist/markdown/write.d.ts +17 -0
- package/dist/markdown/write.js +688 -0
- package/dist/math/index.d.ts +14 -0
- package/dist/math/index.js +14 -0
- package/dist/math/parse.d.ts +9 -0
- package/dist/math/parse.js +648 -0
- package/dist/math/symbols.d.ts +44 -0
- package/dist/math/symbols.js +333 -0
- package/dist/math/types.d.ts +59 -0
- package/dist/math/types.js +69 -0
- package/dist/model/backgrounds/contrast.d.ts +77 -0
- package/dist/model/backgrounds/contrast.js +186 -0
- package/dist/model/backgrounds/index.d.ts +7 -0
- package/dist/model/backgrounds/index.js +7 -0
- package/dist/model/backgrounds/resolve.d.ts +86 -0
- package/dist/model/backgrounds/resolve.js +215 -0
- package/dist/model/backgrounds/schema.d.ts +178 -0
- package/dist/model/backgrounds/schema.js +203 -0
- package/dist/model/codes.d.ts +73 -0
- package/dist/model/codes.js +67 -0
- package/dist/model/deck.d.ts +392 -0
- package/dist/model/deck.js +338 -0
- package/dist/model/index.d.ts +19 -0
- package/dist/model/index.js +19 -0
- package/dist/model/layouts/catalog.d.ts +433 -0
- package/dist/model/layouts/catalog.js +495 -0
- package/dist/model/layouts/index.d.ts +5 -0
- package/dist/model/layouts/index.js +5 -0
- package/dist/model/layouts/props.d.ts +302 -0
- package/dist/model/layouts/props.js +272 -0
- package/dist/model/loader.d.ts +37 -0
- package/dist/model/loader.js +317 -0
- package/dist/model/migrate.d.ts +34 -0
- package/dist/model/migrate.js +453 -0
- package/dist/model/motion/index.d.ts +6 -0
- package/dist/model/motion/index.js +6 -0
- package/dist/model/motion/transition.d.ts +96 -0
- package/dist/model/motion/transition.js +127 -0
- package/dist/model/parse.d.ts +57 -0
- package/dist/model/parse.js +307 -0
- package/dist/model/plain.d.ts +40 -0
- package/dist/model/plain.js +275 -0
- package/dist/model/shared.d.ts +63 -0
- package/dist/model/shared.js +86 -0
- package/dist/model/steps/index.d.ts +7 -0
- package/dist/model/steps/index.js +7 -0
- package/dist/model/steps/phase.d.ts +34 -0
- package/dist/model/steps/phase.js +50 -0
- package/dist/model/steps/slide-steps.d.ts +129 -0
- package/dist/model/steps/slide-steps.js +256 -0
- package/dist/model/style/index.d.ts +8 -0
- package/dist/model/style/index.js +8 -0
- package/dist/model/style/resolve.d.ts +87 -0
- package/dist/model/style/resolve.js +174 -0
- package/dist/model/style/schema.d.ts +85 -0
- package/dist/model/style/schema.js +53 -0
- package/dist/model/style/theme.d.ts +69 -0
- package/dist/model/style/theme.js +84 -0
- package/dist/schema/builders.d.ts +146 -0
- package/dist/schema/builders.js +285 -0
- package/dist/schema/diagnostics.d.ts +78 -0
- package/dist/schema/diagnostics.js +81 -0
- package/dist/schema/index.d.ts +15 -0
- package/dist/schema/index.js +14 -0
- package/dist/schema/internal.d.ts +40 -0
- package/dist/schema/internal.js +209 -0
- package/dist/schema/json-schema.d.ts +12 -0
- package/dist/schema/json-schema.js +213 -0
- package/dist/schema/types.d.ts +166 -0
- package/dist/schema/types.js +1 -0
- package/dist/schema/validate.d.ts +51 -0
- package/dist/schema/validate.js +765 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.js +2 -0
- package/dist/templates/templates.d.ts +11 -0
- package/dist/templates/templates.js +47 -0
- package/package.json +60 -4
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The validator: one walk over the input, driven by the schema data, that
|
|
3
|
+
* collects every issue (up to the cap) and builds a fresh plain-data copy.
|
|
4
|
+
*
|
|
5
|
+
* It never throws and does bounded work on hostile input: own data
|
|
6
|
+
* properties only (getters, throwing proxies and class instances are
|
|
7
|
+
* refused), prototype keys refused, cycles detected, nesting capped by
|
|
8
|
+
* `maxDepth`, every visited value counted against `maxNodes`, arrays and
|
|
9
|
+
* records validated only up to their `max`, and the walk stops once the issue
|
|
10
|
+
* cap is reached (`truncated`). Recursion depth is bounded by the schema tree
|
|
11
|
+
* plus `maxDepth`; `t.json` values are walked iteratively.
|
|
12
|
+
*/
|
|
13
|
+
import { SCHEMA_CODES as C, createDiagnostic, MAX_DIAGNOSTICS, toJsonPath, toPointer, } from "./diagnostics.js";
|
|
14
|
+
import { acceptedTypes, codePointLength, compilePattern, describeChar, discriminatorIndex, exceedsCodePoints, isUnsafeKey, typeName, unsafeTextIndex, unwrapObject, } from "./internal.js";
|
|
15
|
+
export const DEFAULT_MAX_DEPTH = 64;
|
|
16
|
+
export const DEFAULT_MAX_NODES = 1_000_000;
|
|
17
|
+
/** Validate `input` against `schema`: every issue, a fresh value, never a throw. */
|
|
18
|
+
export function validate(schema, input, options = {}) {
|
|
19
|
+
const ctx = {
|
|
20
|
+
issues: [],
|
|
21
|
+
normalized: [],
|
|
22
|
+
errors: 0,
|
|
23
|
+
truncated: false,
|
|
24
|
+
aborted: false,
|
|
25
|
+
depth: 0,
|
|
26
|
+
path: [...(options.path ?? [])],
|
|
27
|
+
ancestors: [],
|
|
28
|
+
lazyDepth: new Map(),
|
|
29
|
+
budget: { nodes: 0, maxNodes: positive(options.maxNodes, DEFAULT_MAX_NODES) },
|
|
30
|
+
maxIssues: positive(options.maxIssues, MAX_DIAGNOSTICS),
|
|
31
|
+
maxDepth: positive(options.maxDepth, DEFAULT_MAX_DEPTH),
|
|
32
|
+
};
|
|
33
|
+
let value;
|
|
34
|
+
try {
|
|
35
|
+
value = visit(schema, input, ctx);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Last resort (an exotic input or a broken custom schema): report, never throw.
|
|
39
|
+
ctx.aborted = false;
|
|
40
|
+
report(ctx, C.invalidInput, 'Validation stopped: the input could not be read as plain data.');
|
|
41
|
+
}
|
|
42
|
+
const { issues, normalized, truncated } = ctx;
|
|
43
|
+
if (ctx.errors === 0)
|
|
44
|
+
return { ok: true, value: value, issues, normalized, truncated };
|
|
45
|
+
return { ok: false, issues, normalized, truncated };
|
|
46
|
+
}
|
|
47
|
+
/** True when `input` validates without errors (a type guard). */
|
|
48
|
+
export function is(schema, input) {
|
|
49
|
+
return validate(schema, input).ok;
|
|
50
|
+
}
|
|
51
|
+
function positive(value, fallback) {
|
|
52
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
53
|
+
}
|
|
54
|
+
function report(ctx, code, message, extra = {}) {
|
|
55
|
+
const severity = extra.severity ?? 'error';
|
|
56
|
+
if (severity === 'error')
|
|
57
|
+
ctx.errors++;
|
|
58
|
+
if (ctx.aborted)
|
|
59
|
+
return;
|
|
60
|
+
if (ctx.issues.length >= ctx.maxIssues) {
|
|
61
|
+
ctx.truncated = true;
|
|
62
|
+
ctx.aborted = true;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const diagnostic = { severity };
|
|
66
|
+
if (extra.fix !== undefined)
|
|
67
|
+
diagnostic.fix = extra.fix;
|
|
68
|
+
ctx.issues.push(createDiagnostic(code, message, extra.path ?? ctx.path, diagnostic));
|
|
69
|
+
}
|
|
70
|
+
function normalize(ctx, kind) {
|
|
71
|
+
if (ctx.normalized.length >= ctx.maxIssues)
|
|
72
|
+
return;
|
|
73
|
+
ctx.normalized.push({
|
|
74
|
+
kind,
|
|
75
|
+
path: [...ctx.path],
|
|
76
|
+
jsonPath: toJsonPath(ctx.path),
|
|
77
|
+
pointer: toPointer(ctx.path),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function typeError(ctx, expected, value) {
|
|
81
|
+
report(ctx, C.invalidType, `Expected ${expected}, received ${article(typeName(value))}.`);
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
function article(name) {
|
|
85
|
+
if (name === 'null' || name === 'undefined')
|
|
86
|
+
return name;
|
|
87
|
+
return /^[aeiou]/.test(name) ? `an ${name}` : `a ${name}`;
|
|
88
|
+
}
|
|
89
|
+
function list(values, limit = 24) {
|
|
90
|
+
const shown = values.slice(0, limit).map((value) => JSON.stringify(value));
|
|
91
|
+
return values.length > limit ? `${shown.join(', ')}, …` : shown.join(', ');
|
|
92
|
+
}
|
|
93
|
+
const MISSING = Symbol('missing');
|
|
94
|
+
const REFUSED = Symbol('refused');
|
|
95
|
+
/** An own data property, `MISSING`, or `REFUSED` (an accessor or a throwing read, reported). */
|
|
96
|
+
function readOwn(ctx, target, key) {
|
|
97
|
+
let descriptor;
|
|
98
|
+
try {
|
|
99
|
+
descriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
report(ctx, C.invalidInput, 'The property could not be read.');
|
|
103
|
+
return REFUSED;
|
|
104
|
+
}
|
|
105
|
+
if (descriptor === undefined)
|
|
106
|
+
return MISSING;
|
|
107
|
+
if (!('value' in descriptor)) {
|
|
108
|
+
report(ctx, C.invalidInput, 'Accessor properties are not data; use a plain value.');
|
|
109
|
+
return REFUSED;
|
|
110
|
+
}
|
|
111
|
+
return descriptor.value;
|
|
112
|
+
}
|
|
113
|
+
/** `null`-prototype or an `Object.prototype` from any realm; never arrays or class instances. */
|
|
114
|
+
function isPlainObject(value) {
|
|
115
|
+
if (value === null || typeof value !== 'object')
|
|
116
|
+
return false;
|
|
117
|
+
try {
|
|
118
|
+
if (Array.isArray(value))
|
|
119
|
+
return false;
|
|
120
|
+
const proto = Object.getPrototypeOf(value);
|
|
121
|
+
return proto === null || Object.getPrototypeOf(proto) === null;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Shared entry for containers: cycle and depth checks. Returns false when refused. */
|
|
128
|
+
function enter(ctx, value) {
|
|
129
|
+
if (ctx.ancestors.includes(value)) {
|
|
130
|
+
report(ctx, C.cycle, 'The value contains itself (a cyclic reference).');
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (ctx.depth >= ctx.maxDepth) {
|
|
134
|
+
report(ctx, C.tooDeep, `Nesting is deeper than ${ctx.maxDepth} levels.`);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
ctx.ancestors.push(value);
|
|
138
|
+
ctx.depth++;
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
function leave(ctx) {
|
|
142
|
+
ctx.ancestors.pop();
|
|
143
|
+
ctx.depth--;
|
|
144
|
+
}
|
|
145
|
+
function visit(node, value, ctx) {
|
|
146
|
+
if (ctx.aborted)
|
|
147
|
+
return undefined;
|
|
148
|
+
if (++ctx.budget.nodes > ctx.budget.maxNodes) {
|
|
149
|
+
report(ctx, C.tooLarge, `The input has more than ${ctx.budget.maxNodes} values.`);
|
|
150
|
+
ctx.aborted = true;
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
switch (node.kind) {
|
|
154
|
+
case 'string':
|
|
155
|
+
return visitString(node, value, ctx);
|
|
156
|
+
case 'number':
|
|
157
|
+
return visitNumber(node, value, ctx);
|
|
158
|
+
case 'boolean':
|
|
159
|
+
return typeof value === 'boolean' ? value : typeError(ctx, 'a boolean', value);
|
|
160
|
+
case 'null':
|
|
161
|
+
return value === null ? null : typeError(ctx, 'null', value);
|
|
162
|
+
case 'literal':
|
|
163
|
+
return visitLiteral(node, value, ctx);
|
|
164
|
+
case 'enum':
|
|
165
|
+
return visitEnum(node, value, ctx);
|
|
166
|
+
case 'array':
|
|
167
|
+
return visitArray(node, value, ctx);
|
|
168
|
+
case 'object':
|
|
169
|
+
return visitObject(node, value, ctx);
|
|
170
|
+
case 'record':
|
|
171
|
+
return visitRecord(node, value, ctx);
|
|
172
|
+
case 'union':
|
|
173
|
+
return node.discriminator === undefined
|
|
174
|
+
? visitUnion(node, value, ctx)
|
|
175
|
+
: visitDiscriminated(node, value, ctx);
|
|
176
|
+
case 'nullable':
|
|
177
|
+
return value === null ? null : visit(node.inner, value, ctx);
|
|
178
|
+
case 'optional':
|
|
179
|
+
if (value !== undefined)
|
|
180
|
+
return visit(node.inner, value, ctx);
|
|
181
|
+
if (!node.hasDefault)
|
|
182
|
+
return undefined;
|
|
183
|
+
normalize(ctx, 'default');
|
|
184
|
+
return copyDefault(node.default);
|
|
185
|
+
case 'lazy':
|
|
186
|
+
return visitLazy(node, value, ctx);
|
|
187
|
+
case 'refine':
|
|
188
|
+
return visitRefine(node, value, ctx);
|
|
189
|
+
case 'json':
|
|
190
|
+
return visitJson(node, value, ctx);
|
|
191
|
+
default:
|
|
192
|
+
report(ctx, C.invalidInput, 'Unknown schema node.');
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function visitString(node, value, ctx) {
|
|
197
|
+
if (typeof value !== 'string')
|
|
198
|
+
return typeError(ctx, 'a string', value);
|
|
199
|
+
let text = value;
|
|
200
|
+
if (node.trim) {
|
|
201
|
+
const trimmed = text.trim();
|
|
202
|
+
if (trimmed !== text) {
|
|
203
|
+
normalize(ctx, 'trim');
|
|
204
|
+
text = trimmed;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (node.max !== undefined && exceedsCodePoints(text, node.max)) {
|
|
208
|
+
// Too long: report and skip the scans (bounded work on huge strings).
|
|
209
|
+
report(ctx, C.tooLong, `Use at most ${node.max} characters.`, {
|
|
210
|
+
fix: `Shorten the text to ${node.max} characters or fewer.`,
|
|
211
|
+
});
|
|
212
|
+
return text;
|
|
213
|
+
}
|
|
214
|
+
if (node.min !== undefined && text.length < node.min * 2 && codePointLength(text) < node.min) {
|
|
215
|
+
report(ctx, C.tooShort, node.min === 1 ? 'Must not be empty.' : `Use at least ${node.min} characters.`);
|
|
216
|
+
}
|
|
217
|
+
if (node.text !== undefined) {
|
|
218
|
+
const index = unsafeTextIndex(text, node.text);
|
|
219
|
+
if (index >= 0) {
|
|
220
|
+
report(ctx, C.unsafeText, `Character ${describeChar(text, index)} at index ${index} is not allowed here.`, { fix: 'Remove control, line-break or bidi-override characters.' });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (node.pattern !== undefined && !compilePattern(node.pattern).test(text)) {
|
|
224
|
+
report(ctx, C.pattern, `Does not match the pattern ${node.pattern}.`);
|
|
225
|
+
}
|
|
226
|
+
if (node.not?.includes(text)) {
|
|
227
|
+
report(ctx, C.reserved, `${JSON.stringify(text)} is reserved here.`);
|
|
228
|
+
}
|
|
229
|
+
return text;
|
|
230
|
+
}
|
|
231
|
+
function visitNumber(node, value, ctx) {
|
|
232
|
+
if (typeof value !== 'number')
|
|
233
|
+
return typeError(ctx, 'a number', value);
|
|
234
|
+
if (Number.isNaN(value) || (node.finite && !Number.isFinite(value))) {
|
|
235
|
+
report(ctx, C.notFinite, 'Must be a finite number.');
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
if (node.int && !Number.isInteger(value))
|
|
239
|
+
report(ctx, C.notInteger, 'Must be an integer.');
|
|
240
|
+
if ((node.min !== undefined && value < node.min) ||
|
|
241
|
+
(node.max !== undefined && value > node.max)) {
|
|
242
|
+
const range = node.min !== undefined && node.max !== undefined
|
|
243
|
+
? `between ${node.min} and ${node.max}`
|
|
244
|
+
: node.min !== undefined
|
|
245
|
+
? `at least ${node.min}`
|
|
246
|
+
: `at most ${node.max}`;
|
|
247
|
+
report(ctx, C.outOfRange, `Must be ${range}.`);
|
|
248
|
+
}
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
function visitLiteral(node, value, ctx) {
|
|
252
|
+
if (value === node.value)
|
|
253
|
+
return value;
|
|
254
|
+
report(ctx, node.code ?? C.enum, `Expected ${JSON.stringify(node.value)}.`, {
|
|
255
|
+
fix: `Use ${JSON.stringify(node.value)}.`,
|
|
256
|
+
});
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
function visitEnum(node, value, ctx) {
|
|
260
|
+
if (typeof value === 'string' && node.values.includes(value))
|
|
261
|
+
return value;
|
|
262
|
+
report(ctx, node.code ?? C.enum, typeof value === 'string'
|
|
263
|
+
? `Unknown value ${JSON.stringify(value)}.`
|
|
264
|
+
: `Expected one of the listed strings, received ${article(typeName(value))}.`, { fix: `Use one of: ${list(node.values)}.` });
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
function visitArray(node, value, ctx) {
|
|
268
|
+
let length;
|
|
269
|
+
try {
|
|
270
|
+
if (!Array.isArray(value))
|
|
271
|
+
return typeError(ctx, 'an array', value);
|
|
272
|
+
length = value.length;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
report(ctx, C.invalidInput, 'The array could not be read.');
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
if (node.min !== undefined && length < node.min) {
|
|
279
|
+
report(ctx, C.tooFew, `Use at least ${node.min} item${node.min === 1 ? '' : 's'}.`);
|
|
280
|
+
}
|
|
281
|
+
let limit = length;
|
|
282
|
+
if (node.max !== undefined && length > node.max) {
|
|
283
|
+
report(ctx, C.tooMany, `Use at most ${node.max} item${node.max === 1 ? '' : 's'} (got ${length}).`);
|
|
284
|
+
limit = node.max;
|
|
285
|
+
}
|
|
286
|
+
if (!enter(ctx, value))
|
|
287
|
+
return undefined;
|
|
288
|
+
const out = [];
|
|
289
|
+
const item = node.item;
|
|
290
|
+
for (let i = 0; i < limit && !ctx.aborted; i++) {
|
|
291
|
+
ctx.path.push(i);
|
|
292
|
+
const raw = readOwn(ctx, value, i);
|
|
293
|
+
if (raw !== REFUSED)
|
|
294
|
+
out.push(visit(item, raw === MISSING ? undefined : raw, ctx));
|
|
295
|
+
ctx.path.pop();
|
|
296
|
+
}
|
|
297
|
+
leave(ctx);
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
function ownKeys(ctx, value) {
|
|
301
|
+
try {
|
|
302
|
+
return Object.keys(value);
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
report(ctx, C.invalidInput, 'The object keys could not be read.');
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function visitObject(node, value, ctx) {
|
|
310
|
+
if (!isPlainObject(value))
|
|
311
|
+
return typeError(ctx, 'an object', value);
|
|
312
|
+
if (!enter(ctx, value))
|
|
313
|
+
return undefined;
|
|
314
|
+
const out = {};
|
|
315
|
+
const shape = node.shape;
|
|
316
|
+
for (const key of Object.keys(shape)) {
|
|
317
|
+
if (ctx.aborted)
|
|
318
|
+
break;
|
|
319
|
+
const field = shape[key];
|
|
320
|
+
ctx.path.push(key);
|
|
321
|
+
const raw = readOwn(ctx, value, key);
|
|
322
|
+
if (raw !== REFUSED) {
|
|
323
|
+
if ((raw === MISSING || raw === undefined) && field.kind !== 'optional') {
|
|
324
|
+
report(ctx, C.required, `Missing required key ${JSON.stringify(key)}.`);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
const result = visit(field, raw === MISSING ? undefined : raw, ctx);
|
|
328
|
+
if (result !== undefined)
|
|
329
|
+
out[key] = result;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
ctx.path.pop();
|
|
333
|
+
}
|
|
334
|
+
const keys = ownKeys(ctx, value);
|
|
335
|
+
if (keys) {
|
|
336
|
+
for (const key of keys) {
|
|
337
|
+
if (ctx.aborted)
|
|
338
|
+
break;
|
|
339
|
+
if (Object.hasOwn(shape, key))
|
|
340
|
+
continue;
|
|
341
|
+
if (isUnsafeKey(key)) {
|
|
342
|
+
ctx.path.push(key);
|
|
343
|
+
report(ctx, C.unsafeKey, `The key ${JSON.stringify(key)} is not allowed.`, {
|
|
344
|
+
fix: 'Remove the key.',
|
|
345
|
+
});
|
|
346
|
+
ctx.path.pop();
|
|
347
|
+
}
|
|
348
|
+
else if (node.strict) {
|
|
349
|
+
ctx.path.push(key);
|
|
350
|
+
report(ctx, C.unknownKey, `Unknown key ${JSON.stringify(key)}.`, {
|
|
351
|
+
fix: `Remove it, or use one of: ${list(Object.keys(shape))}.`,
|
|
352
|
+
});
|
|
353
|
+
ctx.path.pop();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
leave(ctx);
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
function visitRecord(node, value, ctx) {
|
|
361
|
+
if (!isPlainObject(value))
|
|
362
|
+
return typeError(ctx, 'an object', value);
|
|
363
|
+
const keys = ownKeys(ctx, value);
|
|
364
|
+
if (!keys)
|
|
365
|
+
return undefined;
|
|
366
|
+
if (node.min !== undefined && keys.length < node.min) {
|
|
367
|
+
report(ctx, C.tooFew, `Use at least ${node.min} key${node.min === 1 ? '' : 's'}.`);
|
|
368
|
+
}
|
|
369
|
+
let limit = keys.length;
|
|
370
|
+
if (node.max !== undefined && keys.length > node.max) {
|
|
371
|
+
report(ctx, C.tooMany, `Use at most ${node.max} keys (got ${keys.length}).`);
|
|
372
|
+
limit = node.max;
|
|
373
|
+
}
|
|
374
|
+
if (!enter(ctx, value))
|
|
375
|
+
return undefined;
|
|
376
|
+
const out = {};
|
|
377
|
+
const item = node.value;
|
|
378
|
+
for (let i = 0; i < limit && !ctx.aborted; i++) {
|
|
379
|
+
const key = keys[i];
|
|
380
|
+
ctx.path.push(key);
|
|
381
|
+
if (isUnsafeKey(key)) {
|
|
382
|
+
report(ctx, C.unsafeKey, `The key ${JSON.stringify(key)} is not allowed.`, {
|
|
383
|
+
fix: 'Remove the key.',
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
if (node.key)
|
|
388
|
+
visitString(node.key, key, ctx);
|
|
389
|
+
const raw = readOwn(ctx, value, key);
|
|
390
|
+
if (raw !== REFUSED)
|
|
391
|
+
out[key] = visit(item, raw === MISSING ? undefined : raw, ctx);
|
|
392
|
+
}
|
|
393
|
+
ctx.path.pop();
|
|
394
|
+
}
|
|
395
|
+
leave(ctx);
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
/** A child context for a union trial: own issues, shared path, ancestors and budget. */
|
|
399
|
+
function trial(ctx) {
|
|
400
|
+
return {
|
|
401
|
+
...ctx,
|
|
402
|
+
issues: [],
|
|
403
|
+
normalized: [],
|
|
404
|
+
errors: 0,
|
|
405
|
+
truncated: false,
|
|
406
|
+
aborted: false,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function merge(ctx, from) {
|
|
410
|
+
for (const issue of from.issues) {
|
|
411
|
+
if (ctx.aborted)
|
|
412
|
+
break;
|
|
413
|
+
if (ctx.issues.length >= ctx.maxIssues) {
|
|
414
|
+
ctx.truncated = true;
|
|
415
|
+
ctx.aborted = true;
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
ctx.issues.push(issue);
|
|
419
|
+
}
|
|
420
|
+
// Errors dropped by the cap still count, so the result stays invalid.
|
|
421
|
+
ctx.errors += from.errors;
|
|
422
|
+
if (from.truncated)
|
|
423
|
+
ctx.truncated = true;
|
|
424
|
+
for (const entry of from.normalized) {
|
|
425
|
+
if (ctx.normalized.length >= ctx.maxIssues)
|
|
426
|
+
break;
|
|
427
|
+
ctx.normalized.push(entry);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function jsonType(value) {
|
|
431
|
+
const name = typeName(value);
|
|
432
|
+
return name === 'string' ||
|
|
433
|
+
name === 'number' ||
|
|
434
|
+
name === 'boolean' ||
|
|
435
|
+
name === 'null' ||
|
|
436
|
+
name === 'array' ||
|
|
437
|
+
name === 'object'
|
|
438
|
+
? name
|
|
439
|
+
: undefined;
|
|
440
|
+
}
|
|
441
|
+
function visitUnion(node, value, ctx) {
|
|
442
|
+
const type = jsonType(value);
|
|
443
|
+
const candidates = node.options.filter((option) => type !== undefined && acceptedTypes(option).has(type));
|
|
444
|
+
if (candidates.length === 0) {
|
|
445
|
+
const expected = new Set();
|
|
446
|
+
for (const option of node.options)
|
|
447
|
+
for (const name of acceptedTypes(option))
|
|
448
|
+
expected.add(name);
|
|
449
|
+
return typeError(ctx, `one of: ${[...expected].join(', ')}`, value);
|
|
450
|
+
}
|
|
451
|
+
const depth = ctx.depth;
|
|
452
|
+
const pathLength = ctx.path.length;
|
|
453
|
+
const ancestors = ctx.ancestors.length;
|
|
454
|
+
let best;
|
|
455
|
+
let bestValue;
|
|
456
|
+
for (const option of candidates) {
|
|
457
|
+
const attempt = trial(ctx);
|
|
458
|
+
const result = visit(option, value, attempt);
|
|
459
|
+
// Restore the shared stacks in case the attempt stopped early.
|
|
460
|
+
ctx.path.length = pathLength;
|
|
461
|
+
ctx.ancestors.length = ancestors;
|
|
462
|
+
ctx.depth = depth;
|
|
463
|
+
if (attempt.errors === 0 && !attempt.aborted) {
|
|
464
|
+
merge(ctx, attempt);
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
if (!best || attempt.errors < best.errors) {
|
|
468
|
+
best = attempt;
|
|
469
|
+
bestValue = result;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// No option fits: report the closest one (fewest errors, first on a tie).
|
|
473
|
+
if (best)
|
|
474
|
+
merge(ctx, best);
|
|
475
|
+
return bestValue;
|
|
476
|
+
}
|
|
477
|
+
function visitDiscriminated(node, value, ctx) {
|
|
478
|
+
const key = node.discriminator;
|
|
479
|
+
const index = discriminatorIndex(node);
|
|
480
|
+
if (!isPlainObject(value))
|
|
481
|
+
return typeError(ctx, 'an object', value);
|
|
482
|
+
ctx.path.push(key);
|
|
483
|
+
const raw = readOwn(ctx, value, key);
|
|
484
|
+
let chosen;
|
|
485
|
+
if (raw === MISSING || raw === undefined) {
|
|
486
|
+
report(ctx, C.required, `Missing required key ${JSON.stringify(key)}.`, {
|
|
487
|
+
fix: `Set ${JSON.stringify(key)} to one of: ${list(index.values)}.`,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
else if (raw !== REFUSED) {
|
|
491
|
+
chosen = index.map.get(raw);
|
|
492
|
+
if (chosen === undefined && typeof raw === 'string') {
|
|
493
|
+
for (const fallback of index.fallbacks) {
|
|
494
|
+
const field = unwrapObject(node.options[fallback])?.shape[key];
|
|
495
|
+
if (field && probe(field, raw, ctx)) {
|
|
496
|
+
chosen = fallback;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (chosen === undefined) {
|
|
502
|
+
report(ctx, node.code ?? C.unknownKind, `Unknown ${key} ${typeof raw === 'string' ? JSON.stringify(raw) : `(${typeName(raw)})`}.`, { fix: `Use one of: ${list(index.values)}.` });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
ctx.path.pop();
|
|
506
|
+
if (chosen === undefined)
|
|
507
|
+
return undefined;
|
|
508
|
+
return visit(node.options[chosen], value, ctx);
|
|
509
|
+
}
|
|
510
|
+
/** Whether a string fits a fallback discriminator, without reporting anything. */
|
|
511
|
+
function probe(field, value, ctx) {
|
|
512
|
+
const attempt = trial(ctx);
|
|
513
|
+
visitString(field, value, attempt);
|
|
514
|
+
return attempt.errors === 0;
|
|
515
|
+
}
|
|
516
|
+
function visitLazy(node, value, ctx) {
|
|
517
|
+
const limit = node.maxDepth ?? ctx.maxDepth;
|
|
518
|
+
const depth = (ctx.lazyDepth.get(node) ?? 0) + 1;
|
|
519
|
+
if (depth > limit) {
|
|
520
|
+
report(ctx, C.tooDeep, `Nesting is deeper than ${limit} levels.`);
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
let target;
|
|
524
|
+
try {
|
|
525
|
+
target = node.get();
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
report(ctx, C.invalidInput, 'The lazy schema could not be resolved.');
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
ctx.lazyDepth.set(node, depth);
|
|
532
|
+
const result = visit(target, value, ctx);
|
|
533
|
+
ctx.lazyDepth.set(node, depth - 1);
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
function visitRefine(node, value, ctx) {
|
|
537
|
+
const before = ctx.errors;
|
|
538
|
+
const result = visit(node.inner, value, ctx);
|
|
539
|
+
if (ctx.errors !== before || ctx.aborted)
|
|
540
|
+
return result;
|
|
541
|
+
const base = [...ctx.path];
|
|
542
|
+
try {
|
|
543
|
+
node.check(result, {
|
|
544
|
+
path: base,
|
|
545
|
+
issue(issue) {
|
|
546
|
+
const extra = {
|
|
547
|
+
path: issue.path ? [...base, ...issue.path] : base,
|
|
548
|
+
};
|
|
549
|
+
if (issue.fix !== undefined)
|
|
550
|
+
extra.fix = issue.fix;
|
|
551
|
+
if (issue.severity !== undefined)
|
|
552
|
+
extra.severity = issue.severity;
|
|
553
|
+
report(ctx, issue.code ?? node.code, issue.message, extra);
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
report(ctx, node.code, 'The check failed unexpectedly.', { path: base });
|
|
559
|
+
}
|
|
560
|
+
return result;
|
|
561
|
+
}
|
|
562
|
+
function copyDefault(value) {
|
|
563
|
+
if (value === null || typeof value !== 'object')
|
|
564
|
+
return value;
|
|
565
|
+
if (Array.isArray(value))
|
|
566
|
+
return value.map(copyDefault);
|
|
567
|
+
const out = {};
|
|
568
|
+
for (const key of Object.keys(value))
|
|
569
|
+
out[key] = copyDefault(value[key]);
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
572
|
+
function framePath(ctx, frame, extra) {
|
|
573
|
+
const rel = [];
|
|
574
|
+
if (extra !== undefined)
|
|
575
|
+
rel.push(extra);
|
|
576
|
+
for (let f = frame; f && f.key !== undefined; f = f.parent)
|
|
577
|
+
rel.push(f.key);
|
|
578
|
+
rel.reverse();
|
|
579
|
+
return [...ctx.path, ...rel];
|
|
580
|
+
}
|
|
581
|
+
/** Bounded JSON, walked with an explicit stack (no recursion on the input's depth). */
|
|
582
|
+
function visitJson(node, value, ctx) {
|
|
583
|
+
const holder = {};
|
|
584
|
+
const stack = [
|
|
585
|
+
{ value, depth: 0, parent: undefined, key: undefined, target: holder, slot: 'root' },
|
|
586
|
+
];
|
|
587
|
+
while (stack.length > 0 && !ctx.aborted) {
|
|
588
|
+
const frame = stack.pop();
|
|
589
|
+
if (++ctx.budget.nodes > ctx.budget.maxNodes) {
|
|
590
|
+
report(ctx, C.tooLarge, `The input has more than ${ctx.budget.maxNodes} values.`);
|
|
591
|
+
ctx.aborted = true;
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
const at = () => framePath(ctx, frame);
|
|
595
|
+
const current = frame.value;
|
|
596
|
+
switch (typeof current) {
|
|
597
|
+
case 'string': {
|
|
598
|
+
if (exceedsCodePoints(current, node.maxString)) {
|
|
599
|
+
report(ctx, C.tooLong, `Use at most ${node.maxString} characters.`, { path: at() });
|
|
600
|
+
}
|
|
601
|
+
else if (node.text !== undefined) {
|
|
602
|
+
const index = unsafeTextIndex(current, node.text);
|
|
603
|
+
if (index >= 0) {
|
|
604
|
+
report(ctx, C.unsafeText, `Character ${describeChar(current, index)} at index ${index} is not allowed here.`, { path: at() });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
frame.target[frame.slot] = current;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
case 'number':
|
|
611
|
+
if (!Number.isFinite(current)) {
|
|
612
|
+
report(ctx, C.notFinite, 'Must be a finite number.', { path: at() });
|
|
613
|
+
}
|
|
614
|
+
frame.target[frame.slot] = current;
|
|
615
|
+
continue;
|
|
616
|
+
case 'boolean':
|
|
617
|
+
frame.target[frame.slot] = current;
|
|
618
|
+
continue;
|
|
619
|
+
case 'object':
|
|
620
|
+
break;
|
|
621
|
+
default:
|
|
622
|
+
report(ctx, C.invalidType, `Expected JSON data, received ${article(typeName(current))}.`, {
|
|
623
|
+
path: at(),
|
|
624
|
+
});
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (current === null) {
|
|
628
|
+
frame.target[frame.slot] = null;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
let isArray;
|
|
632
|
+
try {
|
|
633
|
+
isArray = Array.isArray(current);
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
report(ctx, C.invalidInput, 'The value could not be read.', { path: at() });
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (!isArray && !isPlainObject(current)) {
|
|
640
|
+
report(ctx, C.invalidType, 'Expected JSON data, received a non-plain object.', {
|
|
641
|
+
path: at(),
|
|
642
|
+
});
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
let cyclic = false;
|
|
646
|
+
for (let f = frame.parent; f; f = f.parent) {
|
|
647
|
+
if (f.value === current) {
|
|
648
|
+
cyclic = true;
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (cyclic) {
|
|
653
|
+
report(ctx, C.cycle, 'The value contains itself (a cyclic reference).', { path: at() });
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (frame.depth >= node.depth || ctx.depth + frame.depth >= ctx.maxDepth) {
|
|
657
|
+
report(ctx, C.tooDeep, `Nesting is deeper than ${node.depth} levels.`, { path: at() });
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const children = [];
|
|
661
|
+
const childDepth = frame.depth + 1;
|
|
662
|
+
const read = (key) => {
|
|
663
|
+
let descriptor;
|
|
664
|
+
try {
|
|
665
|
+
descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
report(ctx, C.invalidInput, 'The property could not be read.', {
|
|
669
|
+
path: framePath(ctx, frame, key),
|
|
670
|
+
});
|
|
671
|
+
return REFUSED;
|
|
672
|
+
}
|
|
673
|
+
if (descriptor && !('value' in descriptor)) {
|
|
674
|
+
report(ctx, C.invalidInput, 'Accessor properties are not data; use a plain value.', {
|
|
675
|
+
path: framePath(ctx, frame, key),
|
|
676
|
+
});
|
|
677
|
+
return REFUSED;
|
|
678
|
+
}
|
|
679
|
+
return descriptor?.value;
|
|
680
|
+
};
|
|
681
|
+
if (isArray) {
|
|
682
|
+
const items = current;
|
|
683
|
+
let length;
|
|
684
|
+
try {
|
|
685
|
+
length = items.length;
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
report(ctx, C.invalidInput, 'The array could not be read.', { path: at() });
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
let limit = length;
|
|
692
|
+
if (length > node.maxItems) {
|
|
693
|
+
report(ctx, C.tooMany, `Use at most ${node.maxItems} items (got ${length}).`, {
|
|
694
|
+
path: at(),
|
|
695
|
+
});
|
|
696
|
+
limit = node.maxItems;
|
|
697
|
+
}
|
|
698
|
+
const out = [];
|
|
699
|
+
frame.target[frame.slot] = out;
|
|
700
|
+
for (let i = 0; i < limit; i++) {
|
|
701
|
+
const child = read(i);
|
|
702
|
+
if (child === REFUSED)
|
|
703
|
+
continue;
|
|
704
|
+
children.push({
|
|
705
|
+
value: child,
|
|
706
|
+
depth: childDepth,
|
|
707
|
+
parent: frame,
|
|
708
|
+
key: i,
|
|
709
|
+
target: out,
|
|
710
|
+
slot: children.length,
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
let keys;
|
|
716
|
+
try {
|
|
717
|
+
keys = Object.keys(current);
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
report(ctx, C.invalidInput, 'The object keys could not be read.', { path: at() });
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
let limit = keys.length;
|
|
724
|
+
if (keys.length > node.maxKeys) {
|
|
725
|
+
report(ctx, C.tooMany, `Use at most ${node.maxKeys} keys (got ${keys.length}).`, {
|
|
726
|
+
path: at(),
|
|
727
|
+
});
|
|
728
|
+
limit = node.maxKeys;
|
|
729
|
+
}
|
|
730
|
+
const out = {};
|
|
731
|
+
frame.target[frame.slot] = out;
|
|
732
|
+
for (let i = 0; i < limit; i++) {
|
|
733
|
+
const key = keys[i];
|
|
734
|
+
if (isUnsafeKey(key)) {
|
|
735
|
+
report(ctx, C.unsafeKey, `The key ${JSON.stringify(key)} is not allowed.`, {
|
|
736
|
+
path: framePath(ctx, frame, key),
|
|
737
|
+
fix: 'Remove the key.',
|
|
738
|
+
});
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (exceedsCodePoints(key, node.maxKeyLength)) {
|
|
742
|
+
report(ctx, C.tooLong, `Keys use at most ${node.maxKeyLength} characters.`, {
|
|
743
|
+
path: framePath(ctx, frame, key),
|
|
744
|
+
});
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
const child = read(key);
|
|
748
|
+
if (child === REFUSED)
|
|
749
|
+
continue;
|
|
750
|
+
children.push({
|
|
751
|
+
value: child,
|
|
752
|
+
depth: childDepth,
|
|
753
|
+
parent: frame,
|
|
754
|
+
key,
|
|
755
|
+
target: out,
|
|
756
|
+
slot: key,
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// Push in reverse so children are visited (and reported) in document order.
|
|
761
|
+
for (let i = children.length - 1; i >= 0; i--)
|
|
762
|
+
stack.push(children[i]);
|
|
763
|
+
}
|
|
764
|
+
return holder.root;
|
|
765
|
+
}
|