@antglobal/copilot-cards-core 1.0.5 → 1.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 +57 -0
- package/dist/index.cjs +349 -128
- package/dist/index.d.ts +33 -7
- package/dist/index.js +349 -129
- package/dist/validation-text.cjs +1298 -0
- package/dist/validation-text.d.ts +215 -0
- package/dist/validation-text.js +1293 -0
- package/package.json +7 -1
|
@@ -0,0 +1,1293 @@
|
|
|
1
|
+
import { parseTree, getNodeValue, createScanner, printParseErrorCode, getLocation, findNodeAtLocation } from 'jsonc-parser';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Legacy Schema Compatibility — converts old-format card JSON into
|
|
5
|
+
* the current CardSchema format so it can be rendered by the SDK.
|
|
6
|
+
*
|
|
7
|
+
* Old format:
|
|
8
|
+
* ```json
|
|
9
|
+
* {
|
|
10
|
+
* "cardType": "common",
|
|
11
|
+
* "cardContents": [{ "type": "text", "content": { "text": "hello" } }],
|
|
12
|
+
* "text": "card text",
|
|
13
|
+
* "extInfo": { "language": "zh-CN" }
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { convertLegacySchema } from '@antglobal/copilot-cards-core';
|
|
20
|
+
* const schema = convertLegacySchema(oldJson);
|
|
21
|
+
* renderCard(container, schema);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
// ─── Detection ──────────────────────────────────────────────────
|
|
25
|
+
/**
|
|
26
|
+
* Detect whether an unknown input is a legacy card schema.
|
|
27
|
+
*
|
|
28
|
+
* Checks for the presence of legacy-specific fields (`cardContents`, `cardType`)
|
|
29
|
+
* and the absence of new-format fields (`rootID`, `elements`).
|
|
30
|
+
*/
|
|
31
|
+
function isLegacySchema(input) {
|
|
32
|
+
if (input == null || typeof input !== 'object')
|
|
33
|
+
return false;
|
|
34
|
+
const obj = input;
|
|
35
|
+
return ('cardContents' in obj &&
|
|
36
|
+
'cardType' in obj &&
|
|
37
|
+
!('rootID' in obj) &&
|
|
38
|
+
!('elements' in obj));
|
|
39
|
+
}
|
|
40
|
+
// ─── ID Generator ───────────────────────────────────────────────
|
|
41
|
+
let _idCounter = 0;
|
|
42
|
+
function uid(prefix = 'el') {
|
|
43
|
+
return `${prefix}_${++_idCounter}`;
|
|
44
|
+
}
|
|
45
|
+
/** Reset counter (useful for deterministic tests). */
|
|
46
|
+
function resetIdCounter() {
|
|
47
|
+
_idCounter = 0;
|
|
48
|
+
}
|
|
49
|
+
// ─── Type Mapping ───────────────────────────────────────────────
|
|
50
|
+
/**
|
|
51
|
+
* Map legacy component type names to new schema type names.
|
|
52
|
+
* Extensible — add more mappings as new component types are built.
|
|
53
|
+
*/
|
|
54
|
+
const TYPE_MAP = {
|
|
55
|
+
text: 'Text',
|
|
56
|
+
button: 'Button',
|
|
57
|
+
image: 'Image',
|
|
58
|
+
form: 'Form',
|
|
59
|
+
timeline: 'Timeline',
|
|
60
|
+
group: 'ColumnSet',
|
|
61
|
+
custom: 'Custom',
|
|
62
|
+
};
|
|
63
|
+
function mapType(legacyType) {
|
|
64
|
+
return TYPE_MAP[legacyType] ?? legacyType;
|
|
65
|
+
}
|
|
66
|
+
// ─── Content → Props Converters ─────────────────────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Convert a legacy `text` content item to ElementNode props.
|
|
69
|
+
*
|
|
70
|
+
* Legacy text content may look like:
|
|
71
|
+
* ```json
|
|
72
|
+
* { "text": "hello", "color": "red", "fontSize": 16, "bold": true, ... }
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
function convertTextContent(content) {
|
|
76
|
+
const { text, color, fontSize, fontWeight, bold, align, maxLines, style, ...rest } = content;
|
|
77
|
+
const props = { ...rest };
|
|
78
|
+
// content field
|
|
79
|
+
if (text != null) {
|
|
80
|
+
props.content = staticValue(String(text));
|
|
81
|
+
}
|
|
82
|
+
// Merge style
|
|
83
|
+
const mergedStyle = { ...style };
|
|
84
|
+
if (color)
|
|
85
|
+
mergedStyle.color = color;
|
|
86
|
+
if (fontSize)
|
|
87
|
+
mergedStyle.fontSize = fontSize;
|
|
88
|
+
if (fontWeight)
|
|
89
|
+
mergedStyle.fontWeight = fontWeight;
|
|
90
|
+
if (bold)
|
|
91
|
+
mergedStyle.fontWeight = 'bold';
|
|
92
|
+
if (align)
|
|
93
|
+
mergedStyle.textAlign = align;
|
|
94
|
+
if (Object.keys(mergedStyle).length > 0) {
|
|
95
|
+
props.style = mergedStyle;
|
|
96
|
+
}
|
|
97
|
+
if (maxLines)
|
|
98
|
+
props.maxLines = maxLines;
|
|
99
|
+
return props;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Convert a legacy `button` content item to ElementNode props.
|
|
103
|
+
*
|
|
104
|
+
* Legacy button content may look like:
|
|
105
|
+
* ```json
|
|
106
|
+
* { "label": "Submit", "url": "https://...", "style": { ... } }
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
function convertButtonContent(content) {
|
|
110
|
+
const { label, text, url, actionType: _actionType, style, ...rest } = content;
|
|
111
|
+
const props = { ...rest };
|
|
112
|
+
// Button display text
|
|
113
|
+
const displayText = label ?? text;
|
|
114
|
+
if (displayText != null) {
|
|
115
|
+
props.content = staticValue(String(displayText));
|
|
116
|
+
}
|
|
117
|
+
if (style)
|
|
118
|
+
props.style = style;
|
|
119
|
+
// Convert URL / action to events
|
|
120
|
+
let events;
|
|
121
|
+
if (url) {
|
|
122
|
+
events = {
|
|
123
|
+
onClick: [{ type: 'url', params: { url } }],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return { props, events };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Convert a legacy `image` content item to ElementNode props.
|
|
130
|
+
*
|
|
131
|
+
* Legacy image content may look like:
|
|
132
|
+
* ```json
|
|
133
|
+
* { "src": "https://...", "alt": "desc", "width": 200, "height": 100 }
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
function convertImageContent(content) {
|
|
137
|
+
const { src, url, alt, width, height, style, ...rest } = content;
|
|
138
|
+
const props = { ...rest };
|
|
139
|
+
props.src = src ?? url;
|
|
140
|
+
if (alt)
|
|
141
|
+
props.alt = alt;
|
|
142
|
+
const mergedStyle = { ...style };
|
|
143
|
+
if (width)
|
|
144
|
+
mergedStyle.width = width;
|
|
145
|
+
if (height)
|
|
146
|
+
mergedStyle.height = height;
|
|
147
|
+
if (Object.keys(mergedStyle).length > 0) {
|
|
148
|
+
props.style = mergedStyle;
|
|
149
|
+
}
|
|
150
|
+
return props;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Convert a legacy `form` content item to ElementNode props.
|
|
154
|
+
* Passes through all content properties as props.
|
|
155
|
+
*/
|
|
156
|
+
function convertFormContent(content) {
|
|
157
|
+
return { ...content };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Generic fallback converter — passes content through as props.
|
|
161
|
+
*/
|
|
162
|
+
function convertGenericContent(content) {
|
|
163
|
+
return { ...content };
|
|
164
|
+
}
|
|
165
|
+
// ─── Main Converter ─────────────────────────────────────────────
|
|
166
|
+
/**
|
|
167
|
+
* Convert a legacy CardSchema to the current CardSchema format.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```ts
|
|
171
|
+
* const newSchema = convertLegacySchema(oldJson);
|
|
172
|
+
* renderCard(container, newSchema);
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
function convertLegacySchema(legacy) {
|
|
176
|
+
resetIdCounter();
|
|
177
|
+
const elements = {};
|
|
178
|
+
// Normalise cardContents to array
|
|
179
|
+
const contents = Array.isArray(legacy.cardContents)
|
|
180
|
+
? legacy.cardContents
|
|
181
|
+
: [legacy.cardContents];
|
|
182
|
+
// Convert each content item, collecting top-level child IDs
|
|
183
|
+
const childIds = [];
|
|
184
|
+
for (const item of contents) {
|
|
185
|
+
const converted = convertContentItem(item, elements, legacy.tracking);
|
|
186
|
+
childIds.push(converted.id);
|
|
187
|
+
}
|
|
188
|
+
// Create root container element
|
|
189
|
+
const rootId = uid('root');
|
|
190
|
+
if (childIds.length === 1) {
|
|
191
|
+
// Single child — promote it as root directly
|
|
192
|
+
const onlyChild = elements[childIds[0]];
|
|
193
|
+
onlyChild.id = rootId;
|
|
194
|
+
delete elements[childIds[0]];
|
|
195
|
+
elements[rootId] = onlyChild;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// Multiple children — wrap in a ColumnSet container
|
|
199
|
+
elements[rootId] = {
|
|
200
|
+
id: rootId,
|
|
201
|
+
type: 'ColumnSet',
|
|
202
|
+
props: {
|
|
203
|
+
slots: {
|
|
204
|
+
default: { children: childIds },
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// Build variables from legacy metadata
|
|
210
|
+
const variables = {
|
|
211
|
+
_legacy: {
|
|
212
|
+
cardType: legacy.cardType,
|
|
213
|
+
cardName: legacy.cardName,
|
|
214
|
+
text: legacy.text,
|
|
215
|
+
description: legacy.description,
|
|
216
|
+
language: legacy.extInfo?.language,
|
|
217
|
+
extInfo: legacy.extInfo,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
return {
|
|
221
|
+
version: '1.0',
|
|
222
|
+
rootID: rootId,
|
|
223
|
+
elements,
|
|
224
|
+
variables,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// ─── Recursive Item Converter ───────────────────────────────────
|
|
228
|
+
function convertContentItem(item, elements, tracking) {
|
|
229
|
+
const id = uid(item.type);
|
|
230
|
+
const type = mapType(item.type);
|
|
231
|
+
const content = item.content ?? {};
|
|
232
|
+
let props;
|
|
233
|
+
let events;
|
|
234
|
+
// Type-specific conversion
|
|
235
|
+
switch (item.type) {
|
|
236
|
+
case 'text':
|
|
237
|
+
props = convertTextContent(content);
|
|
238
|
+
break;
|
|
239
|
+
case 'button': {
|
|
240
|
+
const result = convertButtonContent(content);
|
|
241
|
+
props = result.props;
|
|
242
|
+
events = result.events;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case 'image':
|
|
246
|
+
props = convertImageContent(content);
|
|
247
|
+
break;
|
|
248
|
+
case 'form':
|
|
249
|
+
props = convertFormContent(content);
|
|
250
|
+
break;
|
|
251
|
+
case 'group': {
|
|
252
|
+
// Group contains nested items
|
|
253
|
+
const groupChildren = Array.isArray(content.items)
|
|
254
|
+
? content.items
|
|
255
|
+
: content.children
|
|
256
|
+
? (Array.isArray(content.children) ? content.children : [content.children])
|
|
257
|
+
: [];
|
|
258
|
+
const groupChildIds = [];
|
|
259
|
+
for (const child of groupChildren) {
|
|
260
|
+
const childNode = convertContentItem(child, elements, tracking);
|
|
261
|
+
groupChildIds.push(childNode.id);
|
|
262
|
+
}
|
|
263
|
+
props = {
|
|
264
|
+
slots: {
|
|
265
|
+
default: { children: groupChildIds },
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
// Merge groupInfo
|
|
269
|
+
if (item.groupInfo) {
|
|
270
|
+
props.groupInfo = item.groupInfo;
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
default:
|
|
275
|
+
props = convertGenericContent(content);
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
// Apply tracking as lifecycle / events
|
|
279
|
+
const lifecycle = convertTracking(tracking);
|
|
280
|
+
const element = {
|
|
281
|
+
id,
|
|
282
|
+
type,
|
|
283
|
+
props,
|
|
284
|
+
...(events ? { events } : {}),
|
|
285
|
+
...(lifecycle ? { lifecycle } : {}),
|
|
286
|
+
};
|
|
287
|
+
elements[id] = element;
|
|
288
|
+
return element;
|
|
289
|
+
}
|
|
290
|
+
// ─── Tracking → Lifecycle/Events ────────────────────────────────
|
|
291
|
+
function convertTracking(tracking) {
|
|
292
|
+
if (!tracking)
|
|
293
|
+
return undefined;
|
|
294
|
+
const lifecycle = {};
|
|
295
|
+
if (tracking.type === 'expo') {
|
|
296
|
+
lifecycle.onExposed = [
|
|
297
|
+
{
|
|
298
|
+
type: 'emit',
|
|
299
|
+
params: { event: 'tracking', payload: { spm: tracking.spm, type: 'expo' } },
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
if (tracking.type === 'click') {
|
|
304
|
+
// Click tracking is handled at the element level via events,
|
|
305
|
+
// but we also attach an onMount hook to register the tracking context
|
|
306
|
+
lifecycle.onMount = [
|
|
307
|
+
{
|
|
308
|
+
type: 'emit',
|
|
309
|
+
params: { event: 'tracking:register', payload: { spm: tracking.spm, type: 'click' } },
|
|
310
|
+
},
|
|
311
|
+
];
|
|
312
|
+
}
|
|
313
|
+
return Object.keys(lifecycle).length > 0 ? lifecycle : undefined;
|
|
314
|
+
}
|
|
315
|
+
// ─── Helpers ────────────────────────────────────────────────────
|
|
316
|
+
function staticValue(value) {
|
|
317
|
+
return { type: 'static', value };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const A2UI_CHILD_BINDING = Symbol('copilot-cards.a2ui-child-binding');
|
|
321
|
+
/** Read adapter-owned dynamic-child metadata from a slot. */
|
|
322
|
+
function getA2UIChildBinding(slot) {
|
|
323
|
+
if (!((typeof slot === 'object' && slot !== null)
|
|
324
|
+
|| typeof slot === 'function')) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
const descriptor = Object.getOwnPropertyDescriptor(slot, A2UI_CHILD_BINDING);
|
|
329
|
+
if (!descriptor
|
|
330
|
+
|| !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
return descriptor.value;
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Schema Parser — resolves a CardSchema into a renderable tree.
|
|
342
|
+
*
|
|
343
|
+
* A CardSchema contains a flat `elements` map plus a `rootID` entry point,
|
|
344
|
+
* global `variables`, and optional global `actions`.
|
|
345
|
+
*/
|
|
346
|
+
// ─── Normalize ──────────────────────────────────────────────────
|
|
347
|
+
/**
|
|
348
|
+
* Normalize any supported schema input into the current CardSchema.
|
|
349
|
+
*
|
|
350
|
+
* If the input is already a CardSchema, it is returned as-is.
|
|
351
|
+
* If it is a legacy format, it is automatically converted.
|
|
352
|
+
*/
|
|
353
|
+
function normalizeSchema(input) {
|
|
354
|
+
if (isLegacySchema(input)) {
|
|
355
|
+
return convertLegacySchema(input);
|
|
356
|
+
}
|
|
357
|
+
return input;
|
|
358
|
+
}
|
|
359
|
+
function encodePointerSegment$1(segment) {
|
|
360
|
+
return String(segment).replace(/~/g, '~0').replace(/\//g, '~1');
|
|
361
|
+
}
|
|
362
|
+
function schemaPath(...segments) {
|
|
363
|
+
return segments.length > 0
|
|
364
|
+
? `/${segments.map(encodePointerSegment$1).join('/')}`
|
|
365
|
+
: '';
|
|
366
|
+
}
|
|
367
|
+
function elementPath(id, ...segments) {
|
|
368
|
+
return schemaPath('elements', id, ...segments);
|
|
369
|
+
}
|
|
370
|
+
function slotPath(id, slotName, ...segments) {
|
|
371
|
+
return elementPath(id, 'props', 'slots', slotName, ...segments);
|
|
372
|
+
}
|
|
373
|
+
function addIssue(issues, code, path, message, params, anchorPath) {
|
|
374
|
+
issues.push({
|
|
375
|
+
code,
|
|
376
|
+
path,
|
|
377
|
+
...(anchorPath === undefined ? {} : { anchorPath }),
|
|
378
|
+
message,
|
|
379
|
+
...(params === undefined ? {} : { params }),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
function displayValue(value) {
|
|
383
|
+
try {
|
|
384
|
+
return String(value);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
return '<unprintable>';
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function displayError(error) {
|
|
391
|
+
const message = readOwnData(error, 'message');
|
|
392
|
+
return displayValue(message.kind === 'value' ? message.value : error);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Validate an unknown schema input and return structured diagnostics.
|
|
396
|
+
*
|
|
397
|
+
* Component names and component-specific props are deliberately not checked;
|
|
398
|
+
* this validator only owns the shared card graph and binding protocol.
|
|
399
|
+
*/
|
|
400
|
+
function validateSchemaDetailed(input) {
|
|
401
|
+
const issues = [];
|
|
402
|
+
if (!isPlainObject(input)) {
|
|
403
|
+
addIssue(issues, 'SCHEMA_TYPE_MISMATCH', '', 'Schema must be a non-null plain object');
|
|
404
|
+
return issues;
|
|
405
|
+
}
|
|
406
|
+
let schema;
|
|
407
|
+
try {
|
|
408
|
+
schema = normalizeSchema(input);
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
addIssue(issues, 'LEGACY_CONVERSION_FAILED', '', `Legacy schema conversion failed: ${displayError(error)}`);
|
|
412
|
+
return issues;
|
|
413
|
+
}
|
|
414
|
+
const version = readOwnData(schema, 'version');
|
|
415
|
+
if (version.kind !== 'value' || !version.value) {
|
|
416
|
+
addIssue(issues, 'SCHEMA_REQUIRED_FIELD', schemaPath('version'), 'Missing "version" field', undefined, '');
|
|
417
|
+
}
|
|
418
|
+
const rootID = readOwnData(schema, 'rootID');
|
|
419
|
+
if (rootID.kind !== 'value' || typeof rootID.value !== 'string') {
|
|
420
|
+
addIssue(issues, 'ROOT_TYPE_MISMATCH', schemaPath('rootID'), '"rootID" field must be a string', undefined, rootID.kind === 'missing' ? '' : undefined);
|
|
421
|
+
}
|
|
422
|
+
else if (!rootID.value) {
|
|
423
|
+
addIssue(issues, 'SCHEMA_REQUIRED_FIELD', schemaPath('rootID'), 'Missing "rootID" field');
|
|
424
|
+
}
|
|
425
|
+
const elementsRead = readOwnData(schema, 'elements');
|
|
426
|
+
if (elementsRead.kind === 'missing') {
|
|
427
|
+
addIssue(issues, 'SCHEMA_REQUIRED_FIELD', schemaPath('elements'), 'Missing "elements" field', undefined, '');
|
|
428
|
+
return issues;
|
|
429
|
+
}
|
|
430
|
+
const rawElements = elementsRead.kind === 'value'
|
|
431
|
+
? elementsRead.value
|
|
432
|
+
: undefined;
|
|
433
|
+
if (!isPlainObject(rawElements)) {
|
|
434
|
+
addIssue(issues, 'SCHEMA_TYPE_MISMATCH', schemaPath('elements'), '"elements" field must be an object');
|
|
435
|
+
return issues;
|
|
436
|
+
}
|
|
437
|
+
const elements = rawElements;
|
|
438
|
+
const elementEntries = ownStringDataEntries(elements);
|
|
439
|
+
for (const id of elementEntries.accessors) {
|
|
440
|
+
addIssue(issues, 'ELEMENT_ACCESSOR_NOT_ALLOWED', elementPath(id), `Element "${id}" must be an own data property`, { elementId: id });
|
|
441
|
+
}
|
|
442
|
+
if (elementEntries.opaque) {
|
|
443
|
+
addIssue(issues, 'ELEMENTS_UNINSPECTABLE', schemaPath('elements'), 'The "elements" field could not be inspected safely');
|
|
444
|
+
return issues;
|
|
445
|
+
}
|
|
446
|
+
const allIds = new Set([
|
|
447
|
+
...elementEntries.entries.map(([id]) => id),
|
|
448
|
+
...elementEntries.accessors,
|
|
449
|
+
]);
|
|
450
|
+
const validationElements = Object.create(null);
|
|
451
|
+
for (const [id, element] of elementEntries.entries) {
|
|
452
|
+
Object.defineProperty(validationElements, id, {
|
|
453
|
+
configurable: true,
|
|
454
|
+
enumerable: true,
|
|
455
|
+
value: element,
|
|
456
|
+
writable: true,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
const validationSchema = {
|
|
460
|
+
rootID: rootID.kind === 'value' && typeof rootID.value === 'string'
|
|
461
|
+
? rootID.value
|
|
462
|
+
: '',
|
|
463
|
+
elements: validationElements};
|
|
464
|
+
if (rootID.kind === 'value'
|
|
465
|
+
&& typeof rootID.value === 'string'
|
|
466
|
+
&& rootID.value
|
|
467
|
+
&& !allIds.has(rootID.value)) {
|
|
468
|
+
addIssue(issues, 'ROOT_REFERENCE_NOT_FOUND', schemaPath('rootID'), `Root element "${rootID.value}" not found in elements`, { rootID: rootID.value });
|
|
469
|
+
}
|
|
470
|
+
for (const [id, rawElement] of elementEntries.entries) {
|
|
471
|
+
if (!isPlainObject(rawElement)) {
|
|
472
|
+
addIssue(issues, 'ELEMENT_TYPE_MISMATCH', elementPath(id), `Element "${id}" must be an object`, { elementId: id });
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
const element = rawElement;
|
|
476
|
+
const type = readOwnData(element, 'type');
|
|
477
|
+
if (type.kind !== 'value' || !type.value) {
|
|
478
|
+
addIssue(issues, 'ELEMENT_TYPE_MISSING', elementPath(id, 'type'), `Element "${id}" is missing a "type" field`, { elementId: id }, type.kind === 'missing' ? elementPath(id) : undefined);
|
|
479
|
+
}
|
|
480
|
+
const props = readOwnData(element, 'props');
|
|
481
|
+
if (props.kind !== 'value' || !isPlainObject(props.value)) {
|
|
482
|
+
addIssue(issues, 'ELEMENT_PROPS_TYPE_MISMATCH', elementPath(id, 'props'), `Element "${id}" "props" field must be an object`, { elementId: id }, props.kind === 'missing' ? elementPath(id) : undefined);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
// Validate slot children and groups references without invoking accessors.
|
|
486
|
+
const rawSlots = readOwnData(props.value, 'slots');
|
|
487
|
+
if (rawSlots.kind === 'accessor' || rawSlots.kind === 'opaque') {
|
|
488
|
+
addIssue(issues, 'SLOTS_UNINSPECTABLE', elementPath(id, 'props', 'slots'), `Element "${id}" slots could not be inspected safely`, { elementId: id });
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (rawSlots.kind === 'value' && !isPlainObject(rawSlots.value)) {
|
|
492
|
+
addIssue(issues, 'SLOTS_TYPE_MISMATCH', elementPath(id, 'props', 'slots'), `Element "${id}" "slots" field must be an object`, { elementId: id });
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const slots = rawSlots.kind === 'value'
|
|
496
|
+
? ownStringDataEntries(rawSlots.value)
|
|
497
|
+
: ownStringDataEntries(undefined);
|
|
498
|
+
for (const slotName of slots.accessors) {
|
|
499
|
+
addIssue(issues, 'SLOT_ACCESSOR_NOT_ALLOWED', slotPath(id, slotName), `Element "${id}" slot "${slotName}" must be an own data property`, { elementId: id, slotName });
|
|
500
|
+
}
|
|
501
|
+
if (slots.opaque) {
|
|
502
|
+
addIssue(issues, 'SLOTS_UNINSPECTABLE', elementPath(id, 'props', 'slots'), `Element "${id}" slots could not be inspected safely`, { elementId: id });
|
|
503
|
+
}
|
|
504
|
+
for (const [slotName, slot] of slots.entries) {
|
|
505
|
+
if (!isPlainObject(slot)) {
|
|
506
|
+
addIssue(issues, 'SLOT_TYPE_MISMATCH', slotPath(id, slotName), `Element "${id}" slot "${slotName}" must be an object`, { elementId: id, slotName });
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const children = readOwnData(slot, 'children');
|
|
510
|
+
if (children.kind === 'value') {
|
|
511
|
+
const values = ownArrayDataValues(children.value);
|
|
512
|
+
if (!values) {
|
|
513
|
+
addIssue(issues, 'SLOT_CHILDREN_TYPE_MISMATCH', slotPath(id, slotName, 'children'), `Element "${id}" slot "${slotName}" children must be an array`, { elementId: id, slotName });
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
for (const [index, childId] of values.entries()) {
|
|
517
|
+
if (typeof childId !== 'string' || childId.length === 0) {
|
|
518
|
+
addIssue(issues, 'SLOT_CHILD_ID_INVALID', slotPath(id, slotName, 'children', index), `Element "${id}" slot "${slotName}" child IDs must be non-empty strings; received "${displayValue(childId)}"`, { elementId: id, slotName, value: childId });
|
|
519
|
+
}
|
|
520
|
+
else if (!allIds.has(childId)) {
|
|
521
|
+
addIssue(issues, 'ELEMENT_REFERENCE_NOT_FOUND', slotPath(id, slotName, 'children', index), `Element "${id}" slot "${slotName}" references unknown child "${displayValue(childId)}"`, { elementId: id, slotName, reference: childId });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else if (children.kind === 'accessor' || children.kind === 'opaque') {
|
|
527
|
+
addIssue(issues, 'SLOT_ACCESSOR_NOT_ALLOWED', slotPath(id, slotName, 'children'), `Element "${id}" slot "${slotName}" children must be an own data property`, { elementId: id, slotName, field: 'children' });
|
|
528
|
+
}
|
|
529
|
+
const groups = readOwnData(slot, 'groups');
|
|
530
|
+
if (groups.kind === 'value') {
|
|
531
|
+
const groupValues = ownArrayDataValues(groups.value);
|
|
532
|
+
if (!groupValues) {
|
|
533
|
+
addIssue(issues, 'SLOT_GROUPS_TYPE_MISMATCH', slotPath(id, slotName, 'groups'), `Element "${id}" slot "${slotName}" groups must be an array`, { elementId: id, slotName });
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
for (const [groupIndex, group] of groupValues.entries()) {
|
|
537
|
+
const childrenInGroup = ownArrayDataValues(group);
|
|
538
|
+
if (!childrenInGroup) {
|
|
539
|
+
addIssue(issues, 'SLOT_GROUP_TYPE_MISMATCH', slotPath(id, slotName, 'groups', groupIndex), `Element "${id}" slot "${slotName}" groups must contain arrays`, { elementId: id, slotName });
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
for (const [childIndex, childId] of childrenInGroup.entries()) {
|
|
543
|
+
if (typeof childId !== 'string' || childId.length === 0) {
|
|
544
|
+
addIssue(issues, 'SLOT_GROUP_CHILD_ID_INVALID', slotPath(id, slotName, 'groups', groupIndex, childIndex), `Element "${id}" slot "${slotName}" group child IDs must be non-empty strings; received "${displayValue(childId)}"`, { elementId: id, slotName, value: childId });
|
|
545
|
+
}
|
|
546
|
+
else if (!allIds.has(childId)) {
|
|
547
|
+
addIssue(issues, 'ELEMENT_REFERENCE_NOT_FOUND', slotPath(id, slotName, 'groups', groupIndex, childIndex), `Element "${id}" slot "${slotName}" group references unknown child "${displayValue(childId)}"`, { elementId: id, slotName, reference: childId });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
else if (groups.kind === 'accessor' || groups.kind === 'opaque') {
|
|
554
|
+
addIssue(issues, 'SLOT_ACCESSOR_NOT_ALLOWED', slotPath(id, slotName, 'groups'), `Element "${id}" slot "${slotName}" groups must be an own data property`, { elementId: id, slotName, field: 'groups' });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
validateRepeatBindings(validationSchema, issues);
|
|
559
|
+
return issues;
|
|
560
|
+
}
|
|
561
|
+
const REPEAT_SLOT_LAYOUTS = new Set([
|
|
562
|
+
'default',
|
|
563
|
+
'flex',
|
|
564
|
+
'list',
|
|
565
|
+
'grid',
|
|
566
|
+
'horizontalScroll',
|
|
567
|
+
'carousel',
|
|
568
|
+
]);
|
|
569
|
+
const ALIAS_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
570
|
+
const RESERVED_ALIASES = new Set(['__proto__', 'constructor', 'prototype']);
|
|
571
|
+
function hasOwn(value, key) {
|
|
572
|
+
try {
|
|
573
|
+
return ((typeof value === 'object' && value !== null) ||
|
|
574
|
+
typeof value === 'function') && Object.prototype.hasOwnProperty.call(value, key);
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function isPlainObject(value) {
|
|
581
|
+
try {
|
|
582
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
const prototype = Object.getPrototypeOf(value);
|
|
586
|
+
return prototype === Object.prototype || prototype === null;
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function readOwnData(value, key) {
|
|
593
|
+
if (!((typeof value === 'object' && value !== null)
|
|
594
|
+
|| typeof value === 'function')) {
|
|
595
|
+
return { kind: 'missing' };
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
599
|
+
if (!descriptor)
|
|
600
|
+
return { kind: 'missing' };
|
|
601
|
+
if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
602
|
+
return { kind: 'accessor' };
|
|
603
|
+
}
|
|
604
|
+
return { kind: 'value', value: descriptor.value };
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
return { kind: 'opaque' };
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function ownStringDataEntries(value) {
|
|
611
|
+
const result = {
|
|
612
|
+
entries: [],
|
|
613
|
+
accessors: [],
|
|
614
|
+
opaque: false,
|
|
615
|
+
};
|
|
616
|
+
if (!value || typeof value !== 'object')
|
|
617
|
+
return result;
|
|
618
|
+
try {
|
|
619
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
620
|
+
if (typeof key !== 'string')
|
|
621
|
+
continue;
|
|
622
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
623
|
+
if (!descriptor) {
|
|
624
|
+
result.opaque = true;
|
|
625
|
+
}
|
|
626
|
+
else if (Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
627
|
+
result.entries.push([key, descriptor.value]);
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
result.accessors.push(key);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
result.opaque = true;
|
|
636
|
+
}
|
|
637
|
+
return result;
|
|
638
|
+
}
|
|
639
|
+
function ownArrayDataValues(value) {
|
|
640
|
+
try {
|
|
641
|
+
if (!Array.isArray(value))
|
|
642
|
+
return undefined;
|
|
643
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
|
|
644
|
+
const length = lengthDescriptor?.value;
|
|
645
|
+
if (!Number.isSafeInteger(length) || length < 0)
|
|
646
|
+
return undefined;
|
|
647
|
+
const output = [];
|
|
648
|
+
for (let index = 0; index < length; index += 1) {
|
|
649
|
+
const entry = readOwnData(value, String(index));
|
|
650
|
+
if (entry.kind !== 'value')
|
|
651
|
+
return undefined;
|
|
652
|
+
output.push(entry.value);
|
|
653
|
+
}
|
|
654
|
+
return output;
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
return undefined;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function elementSlotEntries(element) {
|
|
661
|
+
const props = readOwnData(element, 'props');
|
|
662
|
+
const slots = props.kind === 'value'
|
|
663
|
+
? readOwnData(props.value, 'slots')
|
|
664
|
+
: props;
|
|
665
|
+
return slots.kind === 'value'
|
|
666
|
+
? ownStringDataEntries(slots.value)
|
|
667
|
+
: {
|
|
668
|
+
entries: [],
|
|
669
|
+
accessors: slots.kind === 'accessor' ? ['slots'] : [],
|
|
670
|
+
opaque: slots.kind === 'opaque',
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function readRepeatBindingFields(value) {
|
|
674
|
+
if (!isPlainObject(value))
|
|
675
|
+
return undefined;
|
|
676
|
+
const source = readOwnData(value, 'source');
|
|
677
|
+
const template = readOwnData(value, 'template');
|
|
678
|
+
const item = readOwnData(value, 'item');
|
|
679
|
+
const index = readOwnData(value, 'index');
|
|
680
|
+
const emptyTemplate = readOwnData(value, 'emptyTemplate');
|
|
681
|
+
return {
|
|
682
|
+
source: source.kind === 'value' ? source.value : undefined,
|
|
683
|
+
template: template.kind === 'value' ? template.value : undefined,
|
|
684
|
+
...(item.kind === 'value' ? { item: item.value } : {}),
|
|
685
|
+
...(index.kind === 'value' ? { index: index.value } : {}),
|
|
686
|
+
...(emptyTemplate.kind === 'value'
|
|
687
|
+
? { emptyTemplate: emptyTemplate.value }
|
|
688
|
+
: {}),
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
function slotStaticReferences(slot) {
|
|
692
|
+
const references = [];
|
|
693
|
+
const children = readOwnData(slot, 'children');
|
|
694
|
+
if (children.kind === 'value') {
|
|
695
|
+
for (const child of ownArrayDataValues(children.value) ?? []) {
|
|
696
|
+
if (typeof child === 'string')
|
|
697
|
+
references.push(child);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const groups = readOwnData(slot, 'groups');
|
|
701
|
+
if (groups.kind === 'value') {
|
|
702
|
+
for (const group of ownArrayDataValues(groups.value) ?? []) {
|
|
703
|
+
for (const child of ownArrayDataValues(group) ?? []) {
|
|
704
|
+
if (typeof child === 'string')
|
|
705
|
+
references.push(child);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const config = readOwnData(slot, 'config');
|
|
710
|
+
const overlays = config.kind === 'value'
|
|
711
|
+
? readOwnData(config.value, 'overlays')
|
|
712
|
+
: { kind: 'missing' };
|
|
713
|
+
if (overlays.kind === 'value') {
|
|
714
|
+
for (const overlay of ownArrayDataValues(overlays.value) ?? []) {
|
|
715
|
+
const overlayChildren = readOwnData(overlay, 'children');
|
|
716
|
+
if (overlayChildren.kind !== 'value')
|
|
717
|
+
continue;
|
|
718
|
+
for (const child of ownArrayDataValues(overlayChildren.value) ?? []) {
|
|
719
|
+
if (typeof child === 'string')
|
|
720
|
+
references.push(child);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return references;
|
|
725
|
+
}
|
|
726
|
+
function getOwnElement(schema, id) {
|
|
727
|
+
const element = readOwnData(schema.elements, id);
|
|
728
|
+
return element.kind === 'value'
|
|
729
|
+
? element.value
|
|
730
|
+
: undefined;
|
|
731
|
+
}
|
|
732
|
+
function isCompleteExpression(source) {
|
|
733
|
+
if (typeof source !== 'string')
|
|
734
|
+
return false;
|
|
735
|
+
const trimmed = source.trim();
|
|
736
|
+
if (!trimmed.startsWith('${'))
|
|
737
|
+
return false;
|
|
738
|
+
if (!trimmed.slice(2, -1).trim())
|
|
739
|
+
return false;
|
|
740
|
+
let depth = 1;
|
|
741
|
+
let quote;
|
|
742
|
+
let escaped = false;
|
|
743
|
+
for (let index = 2; index < trimmed.length; index += 1) {
|
|
744
|
+
const character = trimmed[index];
|
|
745
|
+
if (quote) {
|
|
746
|
+
if (escaped) {
|
|
747
|
+
escaped = false;
|
|
748
|
+
}
|
|
749
|
+
else if (character === '\\') {
|
|
750
|
+
escaped = true;
|
|
751
|
+
}
|
|
752
|
+
else if (character === quote) {
|
|
753
|
+
quote = undefined;
|
|
754
|
+
}
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (character === "'" || character === '"' || character === '`') {
|
|
758
|
+
quote = character;
|
|
759
|
+
}
|
|
760
|
+
else if (character === '{') {
|
|
761
|
+
depth += 1;
|
|
762
|
+
}
|
|
763
|
+
else if (character === '}') {
|
|
764
|
+
depth -= 1;
|
|
765
|
+
if (depth === 0)
|
|
766
|
+
return index === trimmed.length - 1;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
function elementReferences(element) {
|
|
772
|
+
const references = [];
|
|
773
|
+
for (const [, slot] of elementSlotEntries(element).entries) {
|
|
774
|
+
references.push(...slotStaticReferences(slot));
|
|
775
|
+
const repeat = readOwnData(slot, 'repeat');
|
|
776
|
+
const binding = repeat.kind === 'value'
|
|
777
|
+
? readRepeatBindingFields(repeat.value)
|
|
778
|
+
: undefined;
|
|
779
|
+
if (typeof binding?.template === 'string' && binding.template) {
|
|
780
|
+
references.push(binding.template);
|
|
781
|
+
}
|
|
782
|
+
if (typeof binding?.emptyTemplate === 'string'
|
|
783
|
+
&& binding.emptyTemplate) {
|
|
784
|
+
references.push(binding.emptyTemplate);
|
|
785
|
+
}
|
|
786
|
+
const a2ui = getA2UIChildBinding(slot);
|
|
787
|
+
if (typeof a2ui?.templateId === 'string' && a2ui.templateId) {
|
|
788
|
+
references.push(a2ui.templateId);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return references;
|
|
792
|
+
}
|
|
793
|
+
function validateRepeatBindings(schema, issues) {
|
|
794
|
+
const dynamicBindings = [];
|
|
795
|
+
for (const [id, element] of Object.entries(schema.elements)) {
|
|
796
|
+
const elementRepeats = elementSlotEntries(element).entries
|
|
797
|
+
.filter((entry) => hasOwn(entry[1], 'repeat')
|
|
798
|
+
|| getA2UIChildBinding(entry[1]) !== undefined);
|
|
799
|
+
if (elementRepeats.length > 1) {
|
|
800
|
+
addIssue(issues, 'DYNAMIC_SLOT_MULTIPLE', elementPath(id, 'props', 'slots'), `Element "${id}" has more than one dynamic slot`, { elementId: id });
|
|
801
|
+
}
|
|
802
|
+
for (const [slotName, slot] of elementRepeats) {
|
|
803
|
+
const a2ui = getA2UIChildBinding(slot);
|
|
804
|
+
const repeat = readOwnData(slot, 'repeat');
|
|
805
|
+
if (repeat.kind === 'missing' && a2ui) {
|
|
806
|
+
dynamicBindings.push({
|
|
807
|
+
owner: id,
|
|
808
|
+
slotName,
|
|
809
|
+
dialect: 'a2ui',
|
|
810
|
+
template: a2ui.templateId,
|
|
811
|
+
});
|
|
812
|
+
if (typeof a2ui.templateId !== 'string'
|
|
813
|
+
|| a2ui.templateId.length === 0) {
|
|
814
|
+
addIssue(issues, 'DYNAMIC_TEMPLATE_INVALID', slotPath(id, slotName), `Element "${id}" slot "${slotName}" dynamic children template must be a non-empty string`, { elementId: id, slotName });
|
|
815
|
+
}
|
|
816
|
+
else if (!getOwnElement(schema, a2ui.templateId)) {
|
|
817
|
+
addIssue(issues, 'DYNAMIC_TEMPLATE_NOT_FOUND', slotPath(id, slotName), `Element "${id}" slot "${slotName}" dynamic children reference unknown template "${a2ui.templateId}"`, { elementId: id, slotName, reference: a2ui.templateId });
|
|
818
|
+
}
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const rawBinding = repeat.kind === 'value'
|
|
822
|
+
? repeat.value
|
|
823
|
+
: undefined;
|
|
824
|
+
if (!isPlainObject(rawBinding)) {
|
|
825
|
+
addIssue(issues, 'REPEAT_TYPE_MISMATCH', slotPath(id, slotName, 'repeat'), `Element "${id}" slot "${slotName}" repeat must be a non-null plain object`, { elementId: id, slotName });
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const binding = readRepeatBindingFields(rawBinding);
|
|
829
|
+
dynamicBindings.push({
|
|
830
|
+
owner: id,
|
|
831
|
+
slotName,
|
|
832
|
+
dialect: 'native',
|
|
833
|
+
template: binding.template,
|
|
834
|
+
binding,
|
|
835
|
+
});
|
|
836
|
+
if (!REPEAT_SLOT_LAYOUTS.has(slotName)) {
|
|
837
|
+
addIssue(issues, 'REPEAT_SLOT_UNSUPPORTED', slotPath(id, slotName, 'repeat'), `Element "${id}" slot "${slotName}" does not support repeat`, { elementId: id, slotName });
|
|
838
|
+
}
|
|
839
|
+
const children = readOwnData(slot, 'children');
|
|
840
|
+
const groups = readOwnData(slot, 'groups');
|
|
841
|
+
if ((children.kind === 'value' && !!children.value)
|
|
842
|
+
|| children.kind === 'accessor'
|
|
843
|
+
|| children.kind === 'opaque'
|
|
844
|
+
|| (groups.kind === 'value' && !!groups.value)
|
|
845
|
+
|| groups.kind === 'accessor'
|
|
846
|
+
|| groups.kind === 'opaque') {
|
|
847
|
+
addIssue(issues, 'REPEAT_CONTENT_CONFLICT', slotPath(id, slotName, 'repeat'), `Element "${id}" slot "${slotName}" cannot combine repeat with children or groups`, { elementId: id, slotName });
|
|
848
|
+
}
|
|
849
|
+
if (typeof binding.source !== 'string') {
|
|
850
|
+
addIssue(issues, 'REPEAT_SOURCE_TYPE_MISMATCH', slotPath(id, slotName, 'repeat', 'source'), `Element "${id}" slot "${slotName}" repeat source must be a string`, { elementId: id, slotName }, hasOwn(rawBinding, 'source')
|
|
851
|
+
? undefined
|
|
852
|
+
: slotPath(id, slotName, 'repeat'));
|
|
853
|
+
}
|
|
854
|
+
else if (!isCompleteExpression(binding.source)) {
|
|
855
|
+
addIssue(issues, 'REPEAT_SOURCE_INVALID', slotPath(id, slotName, 'repeat', 'source'), `Element "${id}" slot "${slotName}" repeat source must be one complete non-empty \${...} expression`, { elementId: id, slotName });
|
|
856
|
+
}
|
|
857
|
+
if (typeof binding.template !== 'string') {
|
|
858
|
+
addIssue(issues, 'REPEAT_TEMPLATE_TYPE_MISMATCH', slotPath(id, slotName, 'repeat', 'template'), `Element "${id}" slot "${slotName}" repeat template must be a string`, { elementId: id, slotName }, hasOwn(rawBinding, 'template')
|
|
859
|
+
? undefined
|
|
860
|
+
: slotPath(id, slotName, 'repeat'));
|
|
861
|
+
}
|
|
862
|
+
else if (!binding.template) {
|
|
863
|
+
addIssue(issues, 'REPEAT_TEMPLATE_INVALID', slotPath(id, slotName, 'repeat', 'template'), `Element "${id}" slot "${slotName}" repeat template must be a non-empty string`, { elementId: id, slotName });
|
|
864
|
+
}
|
|
865
|
+
else if (!getOwnElement(schema, binding.template)) {
|
|
866
|
+
addIssue(issues, 'REPEAT_TEMPLATE_NOT_FOUND', slotPath(id, slotName, 'repeat', 'template'), `Element "${id}" slot "${slotName}" repeat references unknown template "${binding.template}"`, { elementId: id, slotName, reference: binding.template });
|
|
867
|
+
}
|
|
868
|
+
if (binding.item !== undefined) {
|
|
869
|
+
validateAlias(id, slotName, 'item', binding.item, issues);
|
|
870
|
+
}
|
|
871
|
+
if (binding.index !== undefined) {
|
|
872
|
+
validateAlias(id, slotName, 'index', binding.index, issues);
|
|
873
|
+
}
|
|
874
|
+
if (binding.index !== undefined
|
|
875
|
+
&& (binding.item ?? '$item') === binding.index) {
|
|
876
|
+
addIssue(issues, 'REPEAT_ALIAS_COLLISION', slotPath(id, slotName, 'repeat', 'index'), `Element "${id}" slot "${slotName}" repeat item and index aliases must be different`, { elementId: id, slotName, alias: binding.index });
|
|
877
|
+
}
|
|
878
|
+
if (binding.emptyTemplate !== undefined) {
|
|
879
|
+
if (typeof binding.emptyTemplate !== 'string') {
|
|
880
|
+
addIssue(issues, 'REPEAT_EMPTY_TEMPLATE_TYPE_MISMATCH', slotPath(id, slotName, 'repeat', 'emptyTemplate'), `Element "${id}" slot "${slotName}" repeat emptyTemplate must be a string`, { elementId: id, slotName });
|
|
881
|
+
}
|
|
882
|
+
else if (!binding.emptyTemplate) {
|
|
883
|
+
addIssue(issues, 'REPEAT_EMPTY_TEMPLATE_INVALID', slotPath(id, slotName, 'repeat', 'emptyTemplate'), `Element "${id}" slot "${slotName}" repeat emptyTemplate must be a non-empty string`, { elementId: id, slotName });
|
|
884
|
+
}
|
|
885
|
+
else if (!getOwnElement(schema, binding.emptyTemplate)) {
|
|
886
|
+
addIssue(issues, 'REPEAT_EMPTY_TEMPLATE_NOT_FOUND', slotPath(id, slotName, 'repeat', 'emptyTemplate'), `Element "${id}" slot "${slotName}" repeat references unknown empty template "${binding.emptyTemplate}"`, { elementId: id, slotName, reference: binding.emptyTemplate });
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
validateRepeatNesting(schema, dynamicBindings, issues);
|
|
892
|
+
validateDynamicClosures(schema, dynamicBindings, issues);
|
|
893
|
+
}
|
|
894
|
+
function validateAlias(owner, slotName, kind, alias, issues) {
|
|
895
|
+
if (typeof alias !== 'string') {
|
|
896
|
+
addIssue(issues, 'REPEAT_ALIAS_TYPE_MISMATCH', slotPath(owner, slotName, 'repeat', kind), `Element "${owner}" slot "${slotName}" repeat ${kind} must be a string`, { elementId: owner, slotName, field: kind });
|
|
897
|
+
}
|
|
898
|
+
else if (!ALIAS_PATTERN.test(alias)) {
|
|
899
|
+
addIssue(issues, 'REPEAT_ALIAS_INVALID', slotPath(owner, slotName, 'repeat', kind), `Element "${owner}" slot "${slotName}" repeat ${kind} alias "${alias}" is invalid`, { elementId: owner, slotName, field: kind, alias });
|
|
900
|
+
}
|
|
901
|
+
else if (RESERVED_ALIASES.has(alias)) {
|
|
902
|
+
addIssue(issues, 'REPEAT_ALIAS_RESERVED', slotPath(owner, slotName, 'repeat', kind), `Element "${owner}" slot "${slotName}" repeat ${kind} alias "${alias}" is reserved`, { elementId: owner, slotName, field: kind, alias });
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function validateRepeatNesting(schema, bindings, issues) {
|
|
906
|
+
const reportedCycles = new Set();
|
|
907
|
+
const reportedCollisions = new Set();
|
|
908
|
+
function effectiveAliases(binding) {
|
|
909
|
+
if (!binding)
|
|
910
|
+
return [];
|
|
911
|
+
if (binding.item !== undefined) {
|
|
912
|
+
return [binding.item, binding.index].filter((alias) => typeof alias === 'string');
|
|
913
|
+
}
|
|
914
|
+
return [
|
|
915
|
+
'$item',
|
|
916
|
+
binding.index === undefined ? '$index' : binding.index,
|
|
917
|
+
].filter((alias) => typeof alias === 'string');
|
|
918
|
+
}
|
|
919
|
+
function visitElement(id, activeAliases, repeatPath, visitedStatic) {
|
|
920
|
+
const element = getOwnElement(schema, id);
|
|
921
|
+
if (!element || visitedStatic.has(id))
|
|
922
|
+
return;
|
|
923
|
+
const nextVisitedStatic = new Set(visitedStatic).add(id);
|
|
924
|
+
for (const [slotName, slotValue] of elementSlotEntries(element).entries) {
|
|
925
|
+
if (!slotValue || typeof slotValue !== 'object')
|
|
926
|
+
continue;
|
|
927
|
+
const slot = slotValue;
|
|
928
|
+
const repeat = readOwnData(slot, 'repeat');
|
|
929
|
+
const nativeBinding = repeat.kind === 'value'
|
|
930
|
+
? readRepeatBindingFields(repeat.value)
|
|
931
|
+
: undefined;
|
|
932
|
+
const a2uiBinding = getA2UIChildBinding(slot);
|
|
933
|
+
const itemTemplate = nativeBinding?.template ?? a2uiBinding?.templateId;
|
|
934
|
+
const visitDynamicTemplate = (template, aliases) => {
|
|
935
|
+
if (!template)
|
|
936
|
+
return;
|
|
937
|
+
const cycleAt = repeatPath.indexOf(template);
|
|
938
|
+
if (cycleAt >= 0) {
|
|
939
|
+
const cycle = [...repeatPath.slice(cycleAt), template].join(' -> ');
|
|
940
|
+
if (!reportedCycles.has(cycle)) {
|
|
941
|
+
reportedCycles.add(cycle);
|
|
942
|
+
addIssue(issues, nativeBinding
|
|
943
|
+
? 'REPEAT_TEMPLATE_CYCLE'
|
|
944
|
+
: 'DYNAMIC_TEMPLATE_CYCLE', nativeBinding
|
|
945
|
+
? slotPath(id, slotName, 'repeat', 'template')
|
|
946
|
+
: slotPath(id, slotName), nativeBinding
|
|
947
|
+
? `Element "${id}" slot "${slotName}" has repeat template cycle: ${cycle}`
|
|
948
|
+
: `Element "${id}" slot "${slotName}" has dynamic template cycle: ${cycle}`, { elementId: id, slotName, cycle });
|
|
949
|
+
}
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
for (const alias of aliases) {
|
|
953
|
+
if (activeAliases.has(alias)) {
|
|
954
|
+
const key = `${id}:${slotName}:${alias}`;
|
|
955
|
+
if (!reportedCollisions.has(key)) {
|
|
956
|
+
reportedCollisions.add(key);
|
|
957
|
+
addIssue(issues, 'REPEAT_ALIAS_COLLISION', slotPath(id, slotName, 'repeat'), `Element "${id}" slot "${slotName}" repeat alias "${alias}" conflicts with an active repeat alias`, { elementId: id, slotName, alias });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
const nestedAliases = new Set(activeAliases);
|
|
962
|
+
for (const alias of aliases)
|
|
963
|
+
nestedAliases.add(alias);
|
|
964
|
+
visitElement(template, nestedAliases, [...repeatPath, template], new Set());
|
|
965
|
+
};
|
|
966
|
+
visitDynamicTemplate(itemTemplate, effectiveAliases(nativeBinding));
|
|
967
|
+
visitDynamicTemplate(nativeBinding?.emptyTemplate, []);
|
|
968
|
+
for (const child of slotStaticReferences(slot)) {
|
|
969
|
+
visitElement(child, activeAliases, repeatPath, nextVisitedStatic);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
for (const dynamic of bindings) {
|
|
974
|
+
const { owner, binding, template } = dynamic;
|
|
975
|
+
const aliases = new Set(effectiveAliases(binding));
|
|
976
|
+
if (typeof template === 'string' && template) {
|
|
977
|
+
visitElement(template, aliases, [owner, template], new Set());
|
|
978
|
+
}
|
|
979
|
+
if (typeof binding?.emptyTemplate === 'string'
|
|
980
|
+
&& binding.emptyTemplate) {
|
|
981
|
+
visitElement(binding.emptyTemplate, new Set(), [owner, binding.emptyTemplate], new Set());
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function validateDynamicClosures(schema, repeats, issues) {
|
|
986
|
+
for (const { owner } of repeats) {
|
|
987
|
+
const visited = new Set();
|
|
988
|
+
function visit(id) {
|
|
989
|
+
if (visited.has(id))
|
|
990
|
+
return;
|
|
991
|
+
visited.add(id);
|
|
992
|
+
const element = getOwnElement(schema, id);
|
|
993
|
+
if (!element)
|
|
994
|
+
return;
|
|
995
|
+
const lifecycle = readOwnData(element, 'lifecycle');
|
|
996
|
+
if ((lifecycle.kind === 'value' && !!lifecycle.value)
|
|
997
|
+
|| lifecycle.kind === 'accessor'
|
|
998
|
+
|| lifecycle.kind === 'opaque') {
|
|
999
|
+
addIssue(issues, 'DYNAMIC_LIFECYCLE_UNSUPPORTED', elementPath(id, 'lifecycle'), `Element "${id}" in dynamic patch closure owned by "${owner}" cannot use lifecycle`, { elementId: id, owner });
|
|
1000
|
+
}
|
|
1001
|
+
const props = readOwnData(element, 'props');
|
|
1002
|
+
if (props.kind === 'value' && hasOwn(props.value, 'variableKey')) {
|
|
1003
|
+
addIssue(issues, 'DYNAMIC_VARIABLE_KEY_UNSUPPORTED', elementPath(id, 'props', 'variableKey'), `Element "${id}" in dynamic patch closure owned by "${owner}" cannot use props.variableKey`, { elementId: id, owner });
|
|
1004
|
+
}
|
|
1005
|
+
for (const reference of elementReferences(element))
|
|
1006
|
+
visit(reference);
|
|
1007
|
+
}
|
|
1008
|
+
visit(owner);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Optional, editor-oriented validation for JSON source text.
|
|
1014
|
+
*
|
|
1015
|
+
* Kept out of the Core main entry so render-only consumers do not bundle a
|
|
1016
|
+
* fault-tolerant JSON parser they never use.
|
|
1017
|
+
*/
|
|
1018
|
+
const SCHEMA_TEXT_MAX_CHARACTERS = 1000000;
|
|
1019
|
+
const SCHEMA_TEXT_MAX_DEPTH = 128;
|
|
1020
|
+
const SCHEMA_TEXT_MAX_ERRORS = 50;
|
|
1021
|
+
// jsonc-parser exposes these as an ambient const enum, which cannot be read
|
|
1022
|
+
// directly when this package is compiled with isolatedModules.
|
|
1023
|
+
const OPEN_BRACE_TOKEN = 1;
|
|
1024
|
+
const CLOSE_BRACE_TOKEN = 2;
|
|
1025
|
+
const OPEN_BRACKET_TOKEN = 3;
|
|
1026
|
+
const CLOSE_BRACKET_TOKEN = 4;
|
|
1027
|
+
const END_OF_FILE_TOKEN = 17;
|
|
1028
|
+
function encodePointerSegment(segment) {
|
|
1029
|
+
return String(segment).replace(/~/g, '~0').replace(/\//g, '~1');
|
|
1030
|
+
}
|
|
1031
|
+
function toJsonPointer(path) {
|
|
1032
|
+
return path.length > 0
|
|
1033
|
+
? `/${path.map(encodePointerSegment).join('/')}`
|
|
1034
|
+
: '';
|
|
1035
|
+
}
|
|
1036
|
+
function pointerSegments(pointer) {
|
|
1037
|
+
if (!pointer)
|
|
1038
|
+
return [];
|
|
1039
|
+
if (!pointer.startsWith('/'))
|
|
1040
|
+
return [];
|
|
1041
|
+
return pointer.slice(1).split('/').map(segment => segment.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
1042
|
+
}
|
|
1043
|
+
function resolvePointerNode(root, segments) {
|
|
1044
|
+
let current = root;
|
|
1045
|
+
for (const segment of segments) {
|
|
1046
|
+
if (!current)
|
|
1047
|
+
return undefined;
|
|
1048
|
+
const locationSegment = current.type === 'array'
|
|
1049
|
+
&& /^(0|[1-9]\d*)$/.test(segment)
|
|
1050
|
+
? Number(segment)
|
|
1051
|
+
: segment;
|
|
1052
|
+
current = findNodeAtLocation(current, [locationSegment]);
|
|
1053
|
+
}
|
|
1054
|
+
return current;
|
|
1055
|
+
}
|
|
1056
|
+
function lineStarts(text, endOffset = text.length) {
|
|
1057
|
+
const starts = [0];
|
|
1058
|
+
const end = Math.min(text.length, endOffset);
|
|
1059
|
+
for (let offset = 0; offset < end; offset += 1) {
|
|
1060
|
+
if (text.charCodeAt(offset) === 10)
|
|
1061
|
+
starts.push(offset + 1);
|
|
1062
|
+
}
|
|
1063
|
+
return starts;
|
|
1064
|
+
}
|
|
1065
|
+
function positionAt(starts, textLength, rawOffset) {
|
|
1066
|
+
const offset = Math.min(Math.max(rawOffset, 0), textLength);
|
|
1067
|
+
let low = 0;
|
|
1068
|
+
let high = starts.length;
|
|
1069
|
+
while (low + 1 < high) {
|
|
1070
|
+
const middle = Math.floor((low + high) / 2);
|
|
1071
|
+
if (starts[middle] <= offset)
|
|
1072
|
+
low = middle;
|
|
1073
|
+
else
|
|
1074
|
+
high = middle;
|
|
1075
|
+
}
|
|
1076
|
+
return {
|
|
1077
|
+
line: low + 1,
|
|
1078
|
+
column: offset - starts[low] + 1,
|
|
1079
|
+
offset,
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function sourceRange(text, starts, rawOffset, rawLength) {
|
|
1083
|
+
const offset = Math.min(Math.max(rawOffset, 0), text.length);
|
|
1084
|
+
const length = rawLength > 0
|
|
1085
|
+
? rawLength
|
|
1086
|
+
: (offset < text.length ? 1 : 0);
|
|
1087
|
+
const endOffset = Math.min(offset + length, text.length);
|
|
1088
|
+
return {
|
|
1089
|
+
start: positionAt(starts, text.length, offset),
|
|
1090
|
+
end: positionAt(starts, text.length, endOffset),
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
function inspectDepthBeforeParsing(text) {
|
|
1094
|
+
const scanner = createScanner(text, false);
|
|
1095
|
+
let depth = 0;
|
|
1096
|
+
let maximumDepth = 0;
|
|
1097
|
+
let firstTooDeepOffset;
|
|
1098
|
+
for (let token = scanner.scan(); token !== END_OF_FILE_TOKEN; token = scanner.scan()) {
|
|
1099
|
+
if (token === OPEN_BRACE_TOKEN
|
|
1100
|
+
|| token === OPEN_BRACKET_TOKEN) {
|
|
1101
|
+
depth += 1;
|
|
1102
|
+
maximumDepth = Math.max(maximumDepth, depth);
|
|
1103
|
+
if (depth > SCHEMA_TEXT_MAX_DEPTH && firstTooDeepOffset === undefined) {
|
|
1104
|
+
firstTooDeepOffset = scanner.getTokenOffset();
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
else if (token === CLOSE_BRACE_TOKEN
|
|
1108
|
+
|| token === CLOSE_BRACKET_TOKEN) {
|
|
1109
|
+
depth = Math.max(0, depth - 1);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return { maximumDepth, firstTooDeepOffset };
|
|
1113
|
+
}
|
|
1114
|
+
function nodeRange(text, starts, node, anchorOnly = false) {
|
|
1115
|
+
return sourceRange(text, starts, node.offset, anchorOnly ? 1 : node.length);
|
|
1116
|
+
}
|
|
1117
|
+
function inspectStructure(text, starts, root) {
|
|
1118
|
+
const duplicates = [];
|
|
1119
|
+
let maximumDepth = 0;
|
|
1120
|
+
let firstTooDeep;
|
|
1121
|
+
const pending = [{
|
|
1122
|
+
node: root,
|
|
1123
|
+
path: [],
|
|
1124
|
+
depth: 0,
|
|
1125
|
+
}];
|
|
1126
|
+
while (pending.length > 0) {
|
|
1127
|
+
const current = pending.pop();
|
|
1128
|
+
const { node, path } = current;
|
|
1129
|
+
const container = node.type === 'object' || node.type === 'array';
|
|
1130
|
+
const depth = container ? current.depth + 1 : current.depth;
|
|
1131
|
+
if (container && depth > maximumDepth)
|
|
1132
|
+
maximumDepth = depth;
|
|
1133
|
+
if (container && depth > SCHEMA_TEXT_MAX_DEPTH && !firstTooDeep) {
|
|
1134
|
+
firstTooDeep = node;
|
|
1135
|
+
}
|
|
1136
|
+
if (node.type === 'object') {
|
|
1137
|
+
const seen = new Set();
|
|
1138
|
+
const children = node.children ?? [];
|
|
1139
|
+
const nested = [];
|
|
1140
|
+
for (const property of children) {
|
|
1141
|
+
const keyNode = property.children?.[0];
|
|
1142
|
+
const valueNode = property.children?.[1];
|
|
1143
|
+
if (!keyNode || typeof keyNode.value !== 'string')
|
|
1144
|
+
continue;
|
|
1145
|
+
const key = keyNode.value;
|
|
1146
|
+
const propertyPath = [...path, key];
|
|
1147
|
+
if (seen.has(key)
|
|
1148
|
+
&& duplicates.length <= SCHEMA_TEXT_MAX_ERRORS) {
|
|
1149
|
+
duplicates.push({
|
|
1150
|
+
code: 'JSON_DUPLICATE_PROPERTY',
|
|
1151
|
+
path: toJsonPointer(propertyPath),
|
|
1152
|
+
message: `Duplicate property "${key}"`,
|
|
1153
|
+
params: { property: key },
|
|
1154
|
+
range: nodeRange(text, starts, keyNode),
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
else {
|
|
1158
|
+
seen.add(key);
|
|
1159
|
+
}
|
|
1160
|
+
if (valueNode)
|
|
1161
|
+
nested.push({ node: valueNode, path: propertyPath, depth });
|
|
1162
|
+
}
|
|
1163
|
+
for (let index = nested.length - 1; index >= 0; index -= 1) {
|
|
1164
|
+
pending.push(nested[index]);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
else if (node.type === 'array') {
|
|
1168
|
+
const children = node.children ?? [];
|
|
1169
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
1170
|
+
pending.push({ node: children[index], path: [...path, index], depth });
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
duplicates.sort((left, right) => left.range.start.offset - right.range.start.offset);
|
|
1175
|
+
return { duplicates, maximumDepth, firstTooDeep };
|
|
1176
|
+
}
|
|
1177
|
+
function syntaxIssues(text, starts, parseErrors) {
|
|
1178
|
+
return parseErrors
|
|
1179
|
+
.slice(0, SCHEMA_TEXT_MAX_ERRORS + 1)
|
|
1180
|
+
.map(error => ({
|
|
1181
|
+
code: 'JSON_SYNTAX_ERROR',
|
|
1182
|
+
path: toJsonPointer(getLocation(text, error.offset).path),
|
|
1183
|
+
message: `Invalid JSON: ${printParseErrorCode(error.error)}`,
|
|
1184
|
+
params: { parseError: printParseErrorCode(error.error) },
|
|
1185
|
+
range: sourceRange(text, starts, error.offset, error.length),
|
|
1186
|
+
}));
|
|
1187
|
+
}
|
|
1188
|
+
function findSemanticNode(root, issue) {
|
|
1189
|
+
const exactPath = pointerSegments(issue.path);
|
|
1190
|
+
const exact = resolvePointerNode(root, exactPath);
|
|
1191
|
+
if (exact)
|
|
1192
|
+
return { node: exact, anchorOnly: false };
|
|
1193
|
+
if (issue.anchorPath !== undefined) {
|
|
1194
|
+
const anchor = resolvePointerNode(root, pointerSegments(issue.anchorPath));
|
|
1195
|
+
if (anchor)
|
|
1196
|
+
return { node: anchor, anchorOnly: true };
|
|
1197
|
+
}
|
|
1198
|
+
for (let length = exactPath.length - 1; length >= 0; length -= 1) {
|
|
1199
|
+
const ancestor = resolvePointerNode(root, exactPath.slice(0, length));
|
|
1200
|
+
if (ancestor)
|
|
1201
|
+
return { node: ancestor, anchorOnly: true };
|
|
1202
|
+
}
|
|
1203
|
+
return { node: root, anchorOnly: true };
|
|
1204
|
+
}
|
|
1205
|
+
function locateSemanticIssue(text, starts, root, issue) {
|
|
1206
|
+
const location = findSemanticNode(root, issue);
|
|
1207
|
+
return {
|
|
1208
|
+
...issue,
|
|
1209
|
+
range: nodeRange(text, starts, location.node, location.anchorOnly),
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
function invalidResult(errors) {
|
|
1213
|
+
return {
|
|
1214
|
+
valid: false,
|
|
1215
|
+
errors: errors.slice(0, SCHEMA_TEXT_MAX_ERRORS),
|
|
1216
|
+
truncated: errors.length > SCHEMA_TEXT_MAX_ERRORS,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
/** Parse strict JSON and return syntax plus SDK-owned schema diagnostics. */
|
|
1220
|
+
function validateSchemaText(text) {
|
|
1221
|
+
if (text.length > SCHEMA_TEXT_MAX_CHARACTERS) {
|
|
1222
|
+
const starts = lineStarts(text, SCHEMA_TEXT_MAX_CHARACTERS + 1);
|
|
1223
|
+
return invalidResult([{
|
|
1224
|
+
code: 'JSON_INPUT_TOO_LARGE',
|
|
1225
|
+
path: '',
|
|
1226
|
+
message: `JSON input exceeds ${SCHEMA_TEXT_MAX_CHARACTERS} characters`,
|
|
1227
|
+
params: { actual: text.length, limit: SCHEMA_TEXT_MAX_CHARACTERS },
|
|
1228
|
+
range: sourceRange(text, starts, SCHEMA_TEXT_MAX_CHARACTERS, 1),
|
|
1229
|
+
}]);
|
|
1230
|
+
}
|
|
1231
|
+
const starts = lineStarts(text);
|
|
1232
|
+
// jsonc-parser builds nested nodes recursively. Scan tokens first so even
|
|
1233
|
+
// malformed, unclosed input cannot exhaust the parser stack.
|
|
1234
|
+
const depthInspection = inspectDepthBeforeParsing(text);
|
|
1235
|
+
if (depthInspection.firstTooDeepOffset !== undefined) {
|
|
1236
|
+
return invalidResult([{
|
|
1237
|
+
code: 'JSON_MAX_DEPTH_EXCEEDED',
|
|
1238
|
+
path: '',
|
|
1239
|
+
message: `JSON nesting exceeds ${SCHEMA_TEXT_MAX_DEPTH} levels`,
|
|
1240
|
+
params: {
|
|
1241
|
+
actual: depthInspection.maximumDepth,
|
|
1242
|
+
limit: SCHEMA_TEXT_MAX_DEPTH,
|
|
1243
|
+
},
|
|
1244
|
+
range: sourceRange(text, starts, depthInspection.firstTooDeepOffset, 1),
|
|
1245
|
+
}]);
|
|
1246
|
+
}
|
|
1247
|
+
const parseErrors = [];
|
|
1248
|
+
const root = parseTree(text, parseErrors, {
|
|
1249
|
+
allowEmptyContent: false,
|
|
1250
|
+
allowTrailingComma: false,
|
|
1251
|
+
disallowComments: true,
|
|
1252
|
+
});
|
|
1253
|
+
if (parseErrors.length > 0 || !root) {
|
|
1254
|
+
const errors = syntaxIssues(text, starts, parseErrors);
|
|
1255
|
+
if (errors.length === 0) {
|
|
1256
|
+
errors.push({
|
|
1257
|
+
code: 'JSON_SYNTAX_ERROR',
|
|
1258
|
+
path: '',
|
|
1259
|
+
message: 'Invalid JSON: ValueExpected',
|
|
1260
|
+
params: { parseError: 'ValueExpected' },
|
|
1261
|
+
range: sourceRange(text, starts, 0, 0),
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
return invalidResult(errors);
|
|
1265
|
+
}
|
|
1266
|
+
const inspection = inspectStructure(text, starts, root);
|
|
1267
|
+
const structuralIssues = [...inspection.duplicates];
|
|
1268
|
+
if (inspection.firstTooDeep) {
|
|
1269
|
+
structuralIssues.push({
|
|
1270
|
+
code: 'JSON_MAX_DEPTH_EXCEEDED',
|
|
1271
|
+
path: toJsonPointer(getLocation(text, inspection.firstTooDeep.offset).path),
|
|
1272
|
+
message: `JSON nesting exceeds ${SCHEMA_TEXT_MAX_DEPTH} levels`,
|
|
1273
|
+
params: {
|
|
1274
|
+
actual: inspection.maximumDepth,
|
|
1275
|
+
limit: SCHEMA_TEXT_MAX_DEPTH,
|
|
1276
|
+
},
|
|
1277
|
+
range: nodeRange(text, starts, inspection.firstTooDeep, true),
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
if (structuralIssues.length > 0) {
|
|
1281
|
+
structuralIssues.sort((left, right) => left.range.start.offset - right.range.start.offset);
|
|
1282
|
+
return invalidResult(structuralIssues);
|
|
1283
|
+
}
|
|
1284
|
+
const schema = getNodeValue(root);
|
|
1285
|
+
const semanticIssues = validateSchemaDetailed(schema)
|
|
1286
|
+
.slice(0, SCHEMA_TEXT_MAX_ERRORS + 1)
|
|
1287
|
+
.map(issue => locateSemanticIssue(text, starts, root, issue));
|
|
1288
|
+
if (semanticIssues.length > 0)
|
|
1289
|
+
return invalidResult(semanticIssues);
|
|
1290
|
+
return { valid: true, schema, errors: [] };
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
export { SCHEMA_TEXT_MAX_CHARACTERS, SCHEMA_TEXT_MAX_DEPTH, SCHEMA_TEXT_MAX_ERRORS, validateSchemaText };
|