@antglobal/copilot-cards-core 1.0.2 → 1.0.3
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/dist/index.cjs +2403 -96
- package/dist/index.d.ts +192 -7
- package/dist/index.js +2383 -97
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -385,8 +385,14 @@ async function parseResponseBody(response) {
|
|
|
385
385
|
*
|
|
386
386
|
* @param silent - If true, only write to ctx.variables without calling
|
|
387
387
|
* setVariable (avoids triggering re-render during polling iterations).
|
|
388
|
+
* When variableWriter is present it owns the write and must synchronously
|
|
389
|
+
* update the bound variables draft while honoring this flag.
|
|
388
390
|
*/
|
|
389
391
|
function writeResponseVariable(ctx, responseKey, data, silent = false) {
|
|
392
|
+
if (ctx.variableWriter) {
|
|
393
|
+
ctx.variableWriter(responseKey, data, { silent, source: 'request' });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
390
396
|
if (ctx.variables) {
|
|
391
397
|
ctx.variables[responseKey] = data;
|
|
392
398
|
}
|
|
@@ -569,6 +575,10 @@ const handleUrl = (step, ctx) => {
|
|
|
569
575
|
};
|
|
570
576
|
const handleSetVariable = (step, ctx) => {
|
|
571
577
|
const { key, value } = step.params;
|
|
578
|
+
if (ctx.variableWriter) {
|
|
579
|
+
ctx.variableWriter(key, value, { silent: false, source: 'setVariable' });
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
572
582
|
if (ctx.setVariable) {
|
|
573
583
|
ctx.setVariable(key, value);
|
|
574
584
|
}
|
|
@@ -628,9 +638,15 @@ async function runActionStep(step, context = {}) {
|
|
|
628
638
|
return;
|
|
629
639
|
}
|
|
630
640
|
// Resolve expression variables in params (e.g. '${order_id}' → 'ORDER_666')
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
641
|
+
const resolutionContext = context.expressionContext ?? context.variables;
|
|
642
|
+
const resolvedParams = context.parameterResolver
|
|
643
|
+
? context.parameterResolver(step.params)
|
|
644
|
+
: resolutionContext
|
|
645
|
+
? resolveDeep(step.params, resolutionContext)
|
|
646
|
+
: step.params;
|
|
647
|
+
const resolvedStep = resolvedParams === step.params
|
|
648
|
+
? step
|
|
649
|
+
: { ...step, params: resolvedParams };
|
|
634
650
|
await handler(resolvedStep, context);
|
|
635
651
|
}
|
|
636
652
|
/**
|
|
@@ -1098,6 +1114,578 @@ function staticValue(value) {
|
|
|
1098
1114
|
return { type: 'static', value };
|
|
1099
1115
|
}
|
|
1100
1116
|
|
|
1117
|
+
const UNSAFE_POINTER_SEGMENTS = new Set([
|
|
1118
|
+
'__proto__',
|
|
1119
|
+
'constructor',
|
|
1120
|
+
'prototype',
|
|
1121
|
+
]);
|
|
1122
|
+
const ARRAY_INDEX_PATTERN = /^(0|[1-9]\d*)$/;
|
|
1123
|
+
const MAX_ARRAY_INDEX = 4294967294;
|
|
1124
|
+
/** A rejected pointer spelling or array location, rather than a data failure. */
|
|
1125
|
+
class JsonPointerPathError extends Error {
|
|
1126
|
+
constructor(code, message) {
|
|
1127
|
+
super(message);
|
|
1128
|
+
this.name = 'JsonPointerPathError';
|
|
1129
|
+
this.code = code;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function isRecord(value) {
|
|
1133
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1137
|
+
return prototype === Object.prototype || prototype === null;
|
|
1138
|
+
}
|
|
1139
|
+
function isContainer(value) {
|
|
1140
|
+
return Array.isArray(value) || isRecord(value);
|
|
1141
|
+
}
|
|
1142
|
+
function isReadableContainer(value) {
|
|
1143
|
+
return value !== null && typeof value === 'object';
|
|
1144
|
+
}
|
|
1145
|
+
function assertSafeSegment(segment) {
|
|
1146
|
+
if (UNSAFE_POINTER_SEGMENTS.has(segment)) {
|
|
1147
|
+
throw new JsonPointerPathError('UNSAFE_SEGMENT', `Unsafe JSON Pointer segment "${segment}"`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function arrayIndex(segment) {
|
|
1151
|
+
if (!ARRAY_INDEX_PATTERN.test(segment))
|
|
1152
|
+
return undefined;
|
|
1153
|
+
const index = Number(segment);
|
|
1154
|
+
return index <= MAX_ARRAY_INDEX ? index : undefined;
|
|
1155
|
+
}
|
|
1156
|
+
function assertContainerSegment(container, segment) {
|
|
1157
|
+
if (Array.isArray(container) && arrayIndex(segment) === undefined) {
|
|
1158
|
+
throw new JsonPointerPathError('INVALID_ARRAY_INDEX', `Invalid array index "${segment}" in JSON Pointer`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function assertWritableContainerSegment(container, segment) {
|
|
1162
|
+
assertContainerSegment(container, segment);
|
|
1163
|
+
if (!Array.isArray(container))
|
|
1164
|
+
return;
|
|
1165
|
+
const index = arrayIndex(segment);
|
|
1166
|
+
if (index !== undefined && index > container.length) {
|
|
1167
|
+
throw new JsonPointerPathError('INVALID_ARRAY_INDEX', `Array index "${segment}" exceeds length ${container.length} in JSON Pointer`);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
function readOwnDataProperty(container, key) {
|
|
1171
|
+
const descriptor = Object.getOwnPropertyDescriptor(container, key);
|
|
1172
|
+
if (!descriptor)
|
|
1173
|
+
return { found: false };
|
|
1174
|
+
if (!('value' in descriptor)) {
|
|
1175
|
+
throw new TypeError(`Cannot traverse JSON Pointer accessor member "${key}"`);
|
|
1176
|
+
}
|
|
1177
|
+
return { found: true, value: descriptor.value };
|
|
1178
|
+
}
|
|
1179
|
+
function defineDataProperty(target, key, value, recordMutation) {
|
|
1180
|
+
assertWritableContainerSegment(target, key);
|
|
1181
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
1182
|
+
if (descriptor) {
|
|
1183
|
+
if ('value' in descriptor && descriptor.writable) {
|
|
1184
|
+
recordMutation?.(target, key);
|
|
1185
|
+
Object.defineProperty(target, key, { value });
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (!descriptor.configurable) {
|
|
1189
|
+
throw new Error(`Cannot write JSON Pointer member "${key}"`);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
else if (!Object.isExtensible(target)) {
|
|
1193
|
+
throw new Error(`Cannot create JSON Pointer member "${key}"`);
|
|
1194
|
+
}
|
|
1195
|
+
recordMutation?.(target, key);
|
|
1196
|
+
Object.defineProperty(target, key, {
|
|
1197
|
+
value,
|
|
1198
|
+
enumerable: true,
|
|
1199
|
+
configurable: true,
|
|
1200
|
+
writable: true,
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
function createDetachedBranch(segments, value) {
|
|
1204
|
+
let branch = value;
|
|
1205
|
+
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
1206
|
+
const segment = segments[index];
|
|
1207
|
+
const child = arrayIndex(segment) === undefined ? {} : [];
|
|
1208
|
+
defineDataProperty(child, segment, branch);
|
|
1209
|
+
branch = child;
|
|
1210
|
+
}
|
|
1211
|
+
return branch;
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Decode one RFC 6901 JSON Pointer reference token.
|
|
1215
|
+
*
|
|
1216
|
+
* Only `~0` and `~1` are legal escapes. URI/percent decoding is deliberately
|
|
1217
|
+
* outside this helper's contract.
|
|
1218
|
+
*/
|
|
1219
|
+
function decodeJsonPointerSegment(segment) {
|
|
1220
|
+
let decoded = '';
|
|
1221
|
+
for (let index = 0; index < segment.length; index += 1) {
|
|
1222
|
+
const character = segment[index];
|
|
1223
|
+
if (character !== '~') {
|
|
1224
|
+
decoded += character;
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
const escape = segment[index + 1];
|
|
1228
|
+
if (escape === '0') {
|
|
1229
|
+
decoded += '~';
|
|
1230
|
+
}
|
|
1231
|
+
else if (escape === '1') {
|
|
1232
|
+
decoded += '/';
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
throw new JsonPointerPathError('SYNTAX', `Invalid JSON Pointer escape "~${escape ?? ''}"`);
|
|
1236
|
+
}
|
|
1237
|
+
index += 1;
|
|
1238
|
+
}
|
|
1239
|
+
return decoded;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Parse a strict RFC 6901 JSON Pointer.
|
|
1243
|
+
*
|
|
1244
|
+
* The canonical root is `''`; `/` names an empty-string property.
|
|
1245
|
+
*/
|
|
1246
|
+
function parseJsonPointer(pointer) {
|
|
1247
|
+
if (pointer === '')
|
|
1248
|
+
return [];
|
|
1249
|
+
if (typeof pointer !== 'string' || !pointer.startsWith('/')) {
|
|
1250
|
+
throw new JsonPointerPathError('SYNTAX', 'JSON Pointer must be empty or start with "/"');
|
|
1251
|
+
}
|
|
1252
|
+
return pointer.slice(1).split('/').map((encoded) => {
|
|
1253
|
+
const segment = decodeJsonPointerSegment(encoded);
|
|
1254
|
+
assertSafeSegment(segment);
|
|
1255
|
+
return segment;
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
/** Read an own-property value through a strict JSON Pointer. */
|
|
1259
|
+
function getByJsonPointer(root, pointer) {
|
|
1260
|
+
const segments = parseJsonPointer(pointer);
|
|
1261
|
+
let current = root;
|
|
1262
|
+
for (const segment of segments) {
|
|
1263
|
+
if (!isReadableContainer(current))
|
|
1264
|
+
return undefined;
|
|
1265
|
+
assertContainerSegment(current, segment);
|
|
1266
|
+
const property = readOwnDataProperty(current, segment);
|
|
1267
|
+
if (!property.found)
|
|
1268
|
+
return undefined;
|
|
1269
|
+
current = property.value;
|
|
1270
|
+
}
|
|
1271
|
+
return current;
|
|
1272
|
+
}
|
|
1273
|
+
function cloneJsonDataInternal(value, ancestors) {
|
|
1274
|
+
if (value === null ||
|
|
1275
|
+
typeof value === 'string' ||
|
|
1276
|
+
typeof value === 'boolean') {
|
|
1277
|
+
return value;
|
|
1278
|
+
}
|
|
1279
|
+
if (typeof value === 'number') {
|
|
1280
|
+
if (!Number.isFinite(value)) {
|
|
1281
|
+
throw new TypeError('JSON data numbers must be finite');
|
|
1282
|
+
}
|
|
1283
|
+
return value;
|
|
1284
|
+
}
|
|
1285
|
+
if (typeof value !== 'object') {
|
|
1286
|
+
throw new TypeError(`Unsupported JSON data value: ${typeof value}`);
|
|
1287
|
+
}
|
|
1288
|
+
if (ancestors.has(value)) {
|
|
1289
|
+
throw new TypeError('Cannot clone cyclic JSON data');
|
|
1290
|
+
}
|
|
1291
|
+
ancestors.add(value);
|
|
1292
|
+
try {
|
|
1293
|
+
if (Array.isArray(value)) {
|
|
1294
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
1295
|
+
if (ownKeys.some(key => typeof key === 'symbol')) {
|
|
1296
|
+
throw new TypeError('JSON data arrays cannot contain symbol keys');
|
|
1297
|
+
}
|
|
1298
|
+
const keys = ownKeys.filter(key => key !== 'length');
|
|
1299
|
+
if (keys.length !== value.length ||
|
|
1300
|
+
keys.some((key) => {
|
|
1301
|
+
const index = arrayIndex(key);
|
|
1302
|
+
return index === undefined || index >= value.length;
|
|
1303
|
+
})) {
|
|
1304
|
+
throw new TypeError('JSON data arrays must be dense and contain only indexed values');
|
|
1305
|
+
}
|
|
1306
|
+
const result = [];
|
|
1307
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1308
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
1309
|
+
if (!descriptor || !descriptor.enumerable) {
|
|
1310
|
+
throw new TypeError('JSON data arrays must be dense and contain only indexed values');
|
|
1311
|
+
}
|
|
1312
|
+
if (!('value' in descriptor)) {
|
|
1313
|
+
throw new TypeError('JSON data arrays cannot contain accessors');
|
|
1314
|
+
}
|
|
1315
|
+
result.push(cloneJsonDataInternal(descriptor.value, ancestors));
|
|
1316
|
+
}
|
|
1317
|
+
return result;
|
|
1318
|
+
}
|
|
1319
|
+
if (!isRecord(value)) {
|
|
1320
|
+
throw new TypeError('JSON data objects must be plain records');
|
|
1321
|
+
}
|
|
1322
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
1323
|
+
throw new TypeError('JSON data objects cannot contain symbol keys');
|
|
1324
|
+
}
|
|
1325
|
+
const result = {};
|
|
1326
|
+
for (const key of Object.keys(value)) {
|
|
1327
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1328
|
+
if (!descriptor || !('value' in descriptor)) {
|
|
1329
|
+
throw new TypeError('JSON data objects cannot contain accessors');
|
|
1330
|
+
}
|
|
1331
|
+
Object.defineProperty(result, key, {
|
|
1332
|
+
value: cloneJsonDataInternal(descriptor.value, ancestors),
|
|
1333
|
+
enumerable: true,
|
|
1334
|
+
configurable: true,
|
|
1335
|
+
writable: true,
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
return result;
|
|
1339
|
+
}
|
|
1340
|
+
finally {
|
|
1341
|
+
ancestors.delete(value);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
/** Deep-clone a value from the JSON data-model domain. */
|
|
1345
|
+
function cloneJsonData(value) {
|
|
1346
|
+
return cloneJsonDataInternal(value, new Set());
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Replace a root record's safe own contents while preserving its identity.
|
|
1350
|
+
*
|
|
1351
|
+
* For ordinary non-Proxy JSON records, validation and cloning finish before
|
|
1352
|
+
* the first mutation, so a rejected candidate leaves the target untouched.
|
|
1353
|
+
* JavaScript Proxy traps cannot be identified reliably and are outside this
|
|
1354
|
+
* atomicity guarantee.
|
|
1355
|
+
*/
|
|
1356
|
+
function replaceRootContentsInternal(root, next, recordMutation) {
|
|
1357
|
+
if (!isRecord(root) || !isRecord(next)) {
|
|
1358
|
+
throw new TypeError('JSON Pointer root replacement requires a record');
|
|
1359
|
+
}
|
|
1360
|
+
const nextKeys = Object.keys(next);
|
|
1361
|
+
for (const key of nextKeys) {
|
|
1362
|
+
if (UNSAFE_POINTER_SEGMENTS.has(key)) {
|
|
1363
|
+
throw new Error(`Unsafe root key "${key}"`);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const cloned = cloneJsonData(next);
|
|
1367
|
+
const currentSafeKeys = Object.keys(root).filter(key => !UNSAFE_POINTER_SEGMENTS.has(key));
|
|
1368
|
+
for (const key of currentSafeKeys) {
|
|
1369
|
+
if (!Object.prototype.hasOwnProperty.call(cloned, key)) {
|
|
1370
|
+
const descriptor = Object.getOwnPropertyDescriptor(root, key);
|
|
1371
|
+
if (!descriptor?.configurable) {
|
|
1372
|
+
throw new Error(`Cannot remove non-configurable root key "${key}"`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
for (const key of nextKeys) {
|
|
1377
|
+
const descriptor = Object.getOwnPropertyDescriptor(root, key);
|
|
1378
|
+
if (!descriptor && !Object.isExtensible(root)) {
|
|
1379
|
+
throw new Error(`Cannot create root key "${key}"`);
|
|
1380
|
+
}
|
|
1381
|
+
if (descriptor &&
|
|
1382
|
+
!descriptor.configurable &&
|
|
1383
|
+
(!('value' in descriptor) || !descriptor.writable)) {
|
|
1384
|
+
throw new Error(`Cannot replace non-writable root key "${key}"`);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
for (const key of currentSafeKeys) {
|
|
1388
|
+
if (!Object.prototype.hasOwnProperty.call(cloned, key)) {
|
|
1389
|
+
recordMutation?.(root, key);
|
|
1390
|
+
delete root[key];
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
for (const key of nextKeys) {
|
|
1394
|
+
const descriptor = Object.getOwnPropertyDescriptor(root, key);
|
|
1395
|
+
recordMutation?.(root, key);
|
|
1396
|
+
if (descriptor && !descriptor.configurable) {
|
|
1397
|
+
Object.defineProperty(root, key, { value: cloned[key] });
|
|
1398
|
+
}
|
|
1399
|
+
else {
|
|
1400
|
+
Object.defineProperty(root, key, {
|
|
1401
|
+
value: cloned[key],
|
|
1402
|
+
enumerable: true,
|
|
1403
|
+
configurable: true,
|
|
1404
|
+
writable: true,
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
function replaceRootContents(root, next) {
|
|
1410
|
+
replaceRootContentsInternal(root, next);
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Transactionally write JSON data through a strict JSON Pointer when the
|
|
1414
|
+
* target is an ordinary non-Proxy JSON record. JavaScript Proxy traps are
|
|
1415
|
+
* outside the atomicity guarantee because they cannot be identified reliably.
|
|
1416
|
+
*
|
|
1417
|
+
* The root pointer `''` replaces the root record. Non-root values are cloned
|
|
1418
|
+
* before they become reachable from the target.
|
|
1419
|
+
*/
|
|
1420
|
+
function setByJsonPointerInternal(root, pointer, value, recordMutation) {
|
|
1421
|
+
if (!isRecord(root)) {
|
|
1422
|
+
throw new TypeError('JSON Pointer writes require a record root');
|
|
1423
|
+
}
|
|
1424
|
+
const segments = parseJsonPointer(pointer);
|
|
1425
|
+
if (segments.length === 0) {
|
|
1426
|
+
if (!isRecord(value)) {
|
|
1427
|
+
throw new TypeError('JSON Pointer root replacement requires a record value');
|
|
1428
|
+
}
|
|
1429
|
+
replaceRootContentsInternal(root, value, recordMutation);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const clonedValue = cloneJsonData(value);
|
|
1433
|
+
let current = root;
|
|
1434
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
1435
|
+
const segment = segments[index];
|
|
1436
|
+
assertWritableContainerSegment(current, segment);
|
|
1437
|
+
const property = readOwnDataProperty(current, segment);
|
|
1438
|
+
if (!property.found) {
|
|
1439
|
+
const branch = createDetachedBranch(segments.slice(index + 1), clonedValue);
|
|
1440
|
+
defineDataProperty(current, segment, branch, recordMutation);
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
const next = property.value;
|
|
1444
|
+
if (next === null || next === undefined) {
|
|
1445
|
+
const branch = createDetachedBranch(segments.slice(index + 1), clonedValue);
|
|
1446
|
+
defineDataProperty(current, segment, branch, recordMutation);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
if (!isContainer(next)) {
|
|
1450
|
+
throw new Error(`Cannot traverse JSON Pointer member "${segment}"`);
|
|
1451
|
+
}
|
|
1452
|
+
current = next;
|
|
1453
|
+
}
|
|
1454
|
+
defineDataProperty(current, segments[segments.length - 1], clonedValue, recordMutation);
|
|
1455
|
+
}
|
|
1456
|
+
function setByJsonPointer(root, pointer, value) {
|
|
1457
|
+
setByJsonPointerInternal(root, pointer, value);
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Engine-internal variant that records only properties the strict writer is
|
|
1461
|
+
* about to mutate. It is intentionally not re-exported from the Core package.
|
|
1462
|
+
*/
|
|
1463
|
+
function setByJsonPointerWithMutationRecorder(root, pointer, value, recordMutation) {
|
|
1464
|
+
setByJsonPointerInternal(root, pointer, value, recordMutation);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function isPlainObject$1(value) {
|
|
1468
|
+
try {
|
|
1469
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1470
|
+
return false;
|
|
1471
|
+
}
|
|
1472
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1473
|
+
return prototype === Object.prototype || prototype === null;
|
|
1474
|
+
}
|
|
1475
|
+
catch {
|
|
1476
|
+
return false;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
function getExactOwnDataValues(value, expected) {
|
|
1480
|
+
try {
|
|
1481
|
+
const keys = Reflect.ownKeys(value);
|
|
1482
|
+
if (keys.length !== expected.length
|
|
1483
|
+
|| !expected.every(key => Object.prototype.hasOwnProperty.call(value, key))) {
|
|
1484
|
+
return undefined;
|
|
1485
|
+
}
|
|
1486
|
+
const values = [];
|
|
1487
|
+
for (const key of expected) {
|
|
1488
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1489
|
+
if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
1490
|
+
return undefined;
|
|
1491
|
+
}
|
|
1492
|
+
values.push(descriptor.value);
|
|
1493
|
+
}
|
|
1494
|
+
return values;
|
|
1495
|
+
}
|
|
1496
|
+
catch {
|
|
1497
|
+
return undefined;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function readDynamicChildList(value) {
|
|
1501
|
+
if (!isPlainObject$1(value))
|
|
1502
|
+
return undefined;
|
|
1503
|
+
const fields = getExactOwnDataValues(value, ['path', 'componentId']);
|
|
1504
|
+
if (!fields
|
|
1505
|
+
|| typeof fields[0] !== 'string'
|
|
1506
|
+
|| typeof fields[1] !== 'string') {
|
|
1507
|
+
return undefined;
|
|
1508
|
+
}
|
|
1509
|
+
return { path: fields[0], componentId: fields[1] };
|
|
1510
|
+
}
|
|
1511
|
+
/** Runtime guard for the exact dynamic ChildList common-type shape. */
|
|
1512
|
+
function isA2UIDynamicChildList(value) {
|
|
1513
|
+
return readDynamicChildList(value) !== undefined;
|
|
1514
|
+
}
|
|
1515
|
+
/** Runtime guard for either supported ChildList form. */
|
|
1516
|
+
function isA2UIChildList(value) {
|
|
1517
|
+
try {
|
|
1518
|
+
if (Array.isArray(value)) {
|
|
1519
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
|
|
1520
|
+
const length = lengthDescriptor?.value;
|
|
1521
|
+
if (!Number.isSafeInteger(length) || length < 0)
|
|
1522
|
+
return false;
|
|
1523
|
+
for (let index = 0; index < length; index += 1) {
|
|
1524
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
1525
|
+
if (!descriptor
|
|
1526
|
+
|| !Object.prototype.hasOwnProperty.call(descriptor, 'value')
|
|
1527
|
+
|| typeof descriptor.value !== 'string') {
|
|
1528
|
+
return false;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
return true;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
catch {
|
|
1535
|
+
return false;
|
|
1536
|
+
}
|
|
1537
|
+
return isA2UIDynamicChildList(value);
|
|
1538
|
+
}
|
|
1539
|
+
/** Runtime guard for the exact A2UI DataBinding.path common-type shape. */
|
|
1540
|
+
function isA2UIPathBinding(value) {
|
|
1541
|
+
return readPathBinding(value) !== undefined;
|
|
1542
|
+
}
|
|
1543
|
+
function readPathBinding(value) {
|
|
1544
|
+
if (!isPlainObject$1(value))
|
|
1545
|
+
return undefined;
|
|
1546
|
+
const fields = getExactOwnDataValues(value, ['path']);
|
|
1547
|
+
if (!fields || typeof fields[0] !== 'string')
|
|
1548
|
+
return undefined;
|
|
1549
|
+
return { path: fields[0] };
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Resolve one A2UI path from the data-model root or the current item scope.
|
|
1553
|
+
*
|
|
1554
|
+
* Absolute paths use strict JSON Pointer syntax. Relative paths are an A2UI
|
|
1555
|
+
* extension and are appended to the current strict-pointer scope.
|
|
1556
|
+
*/
|
|
1557
|
+
function resolveA2UIPath(root, path, scopePath) {
|
|
1558
|
+
if (typeof path !== 'string' || typeof scopePath !== 'string') {
|
|
1559
|
+
throw new TypeError('[A2UI] path and scopePath must be strings');
|
|
1560
|
+
}
|
|
1561
|
+
const pointer = path.startsWith('/')
|
|
1562
|
+
? path
|
|
1563
|
+
: path === ''
|
|
1564
|
+
? scopePath
|
|
1565
|
+
: `${scopePath}/${path}`;
|
|
1566
|
+
return getByJsonPointer(root, pointer);
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Resolve every exact A2UI `{ path }` binding in a nested value.
|
|
1570
|
+
*
|
|
1571
|
+
* Plain records and arrays are cloned with their property descriptors, so
|
|
1572
|
+
* accessors are preserved but never invoked. Cycles in non-JSON direct API
|
|
1573
|
+
* inputs remain cycles in the cloned output.
|
|
1574
|
+
*/
|
|
1575
|
+
function resolveA2UIDeep(value, root, scopePath) {
|
|
1576
|
+
return resolveA2UIDeepInternal(value, root, scopePath, new WeakMap());
|
|
1577
|
+
}
|
|
1578
|
+
function resolveA2UIDeepInternal(value, root, scopePath, seen) {
|
|
1579
|
+
const binding = readPathBinding(value);
|
|
1580
|
+
if (binding) {
|
|
1581
|
+
return resolveA2UIPath(root, binding.path, scopePath);
|
|
1582
|
+
}
|
|
1583
|
+
if (typeof value !== 'object' || value === null)
|
|
1584
|
+
return value;
|
|
1585
|
+
const existing = seen.get(value);
|
|
1586
|
+
if (existing)
|
|
1587
|
+
return existing;
|
|
1588
|
+
let isArray;
|
|
1589
|
+
let prototype;
|
|
1590
|
+
let descriptors;
|
|
1591
|
+
try {
|
|
1592
|
+
isArray = Array.isArray(value);
|
|
1593
|
+
prototype = Object.getPrototypeOf(value);
|
|
1594
|
+
if (!isArray
|
|
1595
|
+
&& prototype !== Object.prototype
|
|
1596
|
+
&& prototype !== null) {
|
|
1597
|
+
return value;
|
|
1598
|
+
}
|
|
1599
|
+
descriptors = [];
|
|
1600
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
1601
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1602
|
+
if (!descriptor)
|
|
1603
|
+
return value;
|
|
1604
|
+
descriptors.push([key, descriptor]);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
catch {
|
|
1608
|
+
return value;
|
|
1609
|
+
}
|
|
1610
|
+
const output = isArray
|
|
1611
|
+
? []
|
|
1612
|
+
: Object.create(prototype);
|
|
1613
|
+
seen.set(value, output);
|
|
1614
|
+
let arrayLength;
|
|
1615
|
+
for (const [key, descriptor] of descriptors) {
|
|
1616
|
+
if (isArray && key === 'length') {
|
|
1617
|
+
arrayLength = descriptor;
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
const nextDescriptor = Object.prototype.hasOwnProperty.call(descriptor, 'value')
|
|
1621
|
+
? {
|
|
1622
|
+
...descriptor,
|
|
1623
|
+
value: resolveA2UIDeepInternal(descriptor.value, root, scopePath, seen),
|
|
1624
|
+
}
|
|
1625
|
+
: descriptor;
|
|
1626
|
+
Object.defineProperty(output, key, nextDescriptor);
|
|
1627
|
+
}
|
|
1628
|
+
if (isArray && arrayLength) {
|
|
1629
|
+
Object.defineProperty(output, 'length', {
|
|
1630
|
+
value: arrayLength.value,
|
|
1631
|
+
writable: arrayLength.writable,
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
return output;
|
|
1635
|
+
}
|
|
1636
|
+
/** Create an ActionRunner-compatible resolver bound to one item scope. */
|
|
1637
|
+
function createA2UIParameterResolver(root, scopePath) {
|
|
1638
|
+
return params => resolveA2UIDeep(params, root, scopePath);
|
|
1639
|
+
}
|
|
1640
|
+
const A2UI_CHILD_BINDING = Symbol('copilot-cards.a2ui-child-binding');
|
|
1641
|
+
/** Attach normalized dynamic-child metadata without widening native JSON. */
|
|
1642
|
+
function setA2UIChildBinding(slot, value) {
|
|
1643
|
+
const binding = readDynamicChildList(value);
|
|
1644
|
+
if (!binding) {
|
|
1645
|
+
throw new TypeError('[A2UI] Invalid dynamic ChildList binding');
|
|
1646
|
+
}
|
|
1647
|
+
slot[A2UI_CHILD_BINDING] = {
|
|
1648
|
+
dialect: 'a2ui',
|
|
1649
|
+
path: binding.path,
|
|
1650
|
+
templateId: binding.componentId,
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
/** Read adapter-owned dynamic-child metadata from a slot. */
|
|
1654
|
+
function getA2UIChildBinding(slot) {
|
|
1655
|
+
if (!((typeof slot === 'object' && slot !== null)
|
|
1656
|
+
|| typeof slot === 'function')) {
|
|
1657
|
+
return undefined;
|
|
1658
|
+
}
|
|
1659
|
+
try {
|
|
1660
|
+
const descriptor = Object.getOwnPropertyDescriptor(slot, A2UI_CHILD_BINDING);
|
|
1661
|
+
if (!descriptor
|
|
1662
|
+
|| !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
1663
|
+
return undefined;
|
|
1664
|
+
}
|
|
1665
|
+
return descriptor.value;
|
|
1666
|
+
}
|
|
1667
|
+
catch {
|
|
1668
|
+
return undefined;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
const A2UI_BINDING_DIALECT = Symbol('copilot-cards.a2ui-binding-dialect');
|
|
1672
|
+
/** Mark an adapter-created element as using A2UI path semantics. */
|
|
1673
|
+
function markA2UIBindingDialect(element) {
|
|
1674
|
+
element[A2UI_BINDING_DIALECT] = true;
|
|
1675
|
+
}
|
|
1676
|
+
/** Whether this exact element owns the private adapter dialect marker. */
|
|
1677
|
+
function hasA2UIBindingDialect(element) {
|
|
1678
|
+
try {
|
|
1679
|
+
const descriptor = Object.getOwnPropertyDescriptor(element, A2UI_BINDING_DIALECT);
|
|
1680
|
+
return !!descriptor
|
|
1681
|
+
&& Object.prototype.hasOwnProperty.call(descriptor, 'value')
|
|
1682
|
+
&& descriptor.value === true;
|
|
1683
|
+
}
|
|
1684
|
+
catch {
|
|
1685
|
+
return false;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1101
1689
|
/**
|
|
1102
1690
|
* Schema Parser — resolves a CardSchema into a renderable tree.
|
|
1103
1691
|
*
|
|
@@ -1117,6 +1705,85 @@ function normalizeSchema(input) {
|
|
|
1117
1705
|
}
|
|
1118
1706
|
return input;
|
|
1119
1707
|
}
|
|
1708
|
+
/** Whether a schema contains native repeat or adapter-owned dynamic children. */
|
|
1709
|
+
function hasDynamicChildren(input) {
|
|
1710
|
+
const schema = normalizeSchema(input);
|
|
1711
|
+
return Object.values(schema.elements).some(element => elementSlotEntries(element).entries.some(([, slot]) => hasOwn(slot, 'repeat') || getA2UIChildBinding(slot) !== undefined));
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Whether a renderer must use the scoped binding/materialization path.
|
|
1715
|
+
*
|
|
1716
|
+
* Literal native/static schemas deliberately remain on the legacy parser path.
|
|
1717
|
+
* The private A2UI marker cannot be forged by JSON input.
|
|
1718
|
+
*/
|
|
1719
|
+
function requiresBindingMaterialization(input) {
|
|
1720
|
+
const schema = normalizeSchema(input);
|
|
1721
|
+
return hasDynamicChildren(schema)
|
|
1722
|
+
|| Object.values(schema.elements).some(hasA2UIBindingDialect);
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Stable fingerprint of the definition graph that controls binding topology.
|
|
1726
|
+
* Runtime data and materialized occurrence counts are intentionally excluded.
|
|
1727
|
+
*/
|
|
1728
|
+
function bindingTopologyFingerprint(input) {
|
|
1729
|
+
const schema = normalizeSchema(input);
|
|
1730
|
+
const topology = Object.keys(schema.elements)
|
|
1731
|
+
.sort()
|
|
1732
|
+
.map(id => {
|
|
1733
|
+
const element = schema.elements[id];
|
|
1734
|
+
const slotEntries = elementSlotEntries(element);
|
|
1735
|
+
const slots = slotEntries.entries
|
|
1736
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1737
|
+
.map(([slotName, slot]) => {
|
|
1738
|
+
const a2ui = getA2UIChildBinding(slot);
|
|
1739
|
+
const config = readOwnData(slot, 'config');
|
|
1740
|
+
const overlays = config.kind === 'value'
|
|
1741
|
+
? readOwnData(config.value, 'overlays')
|
|
1742
|
+
: config;
|
|
1743
|
+
const repeat = readOwnData(slot, 'repeat');
|
|
1744
|
+
const repeatFields = repeat.kind === 'value'
|
|
1745
|
+
&& isPlainObject(repeat.value)
|
|
1746
|
+
? {
|
|
1747
|
+
source: topologyValue(readOwnData(repeat.value, 'source')),
|
|
1748
|
+
template: topologyValue(readOwnData(repeat.value, 'template')),
|
|
1749
|
+
item: topologyValue(readOwnData(repeat.value, 'item')),
|
|
1750
|
+
index: topologyValue(readOwnData(repeat.value, 'index')),
|
|
1751
|
+
}
|
|
1752
|
+
: topologyValue(repeat);
|
|
1753
|
+
return {
|
|
1754
|
+
name: slotName,
|
|
1755
|
+
children: topologyValue(readOwnData(slot, 'children')),
|
|
1756
|
+
groups: topologyValue(readOwnData(slot, 'groups')),
|
|
1757
|
+
overlays: topologyValue(overlays),
|
|
1758
|
+
repeat: hasOwn(slot, 'repeat')
|
|
1759
|
+
? repeatFields
|
|
1760
|
+
: null,
|
|
1761
|
+
a2ui: a2ui
|
|
1762
|
+
? {
|
|
1763
|
+
path: a2ui.path,
|
|
1764
|
+
templateId: a2ui.templateId,
|
|
1765
|
+
}
|
|
1766
|
+
: null,
|
|
1767
|
+
};
|
|
1768
|
+
});
|
|
1769
|
+
for (const slotName of slotEntries.accessors.sort()) {
|
|
1770
|
+
slots.push({ name: slotName, opaque: '<accessor>' });
|
|
1771
|
+
}
|
|
1772
|
+
if (slotEntries.opaque) {
|
|
1773
|
+
slots.push({ name: '<opaque-slots>', opaque: '<opaque>' });
|
|
1774
|
+
}
|
|
1775
|
+
return {
|
|
1776
|
+
id,
|
|
1777
|
+
type: element.type,
|
|
1778
|
+
a2ui: hasA2UIBindingDialect(element),
|
|
1779
|
+
slots,
|
|
1780
|
+
};
|
|
1781
|
+
});
|
|
1782
|
+
return JSON.stringify({
|
|
1783
|
+
rootID: schema.rootID,
|
|
1784
|
+
topology,
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1120
1787
|
// ─── Parser ──────────────────────────────────────────────────────
|
|
1121
1788
|
/**
|
|
1122
1789
|
* Parse a CardSchema into a nested RenderTreeNode starting from `rootID`.
|
|
@@ -1153,74 +1820,1230 @@ function parseSchema(input) {
|
|
|
1153
1820
|
}
|
|
1154
1821
|
}
|
|
1155
1822
|
}
|
|
1156
|
-
if (slot.config?.overlays) {
|
|
1157
|
-
for (const overlay of slot.config.overlays) {
|
|
1158
|
-
if (overlay.children) {
|
|
1159
|
-
for (const childId of overlay.children) {
|
|
1160
|
-
children.push(buildNode(childId));
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1823
|
+
if (slot.config?.overlays) {
|
|
1824
|
+
for (const overlay of slot.config.overlays) {
|
|
1825
|
+
if (overlay.children) {
|
|
1826
|
+
for (const childId of overlay.children) {
|
|
1827
|
+
children.push(buildNode(childId));
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
visited.delete(id); // allow same element across different branches
|
|
1835
|
+
return {
|
|
1836
|
+
id,
|
|
1837
|
+
type: element.type,
|
|
1838
|
+
props: element.props,
|
|
1839
|
+
children,
|
|
1840
|
+
lifecycle: element.lifecycle,
|
|
1841
|
+
events: element.events,
|
|
1842
|
+
directives: element.directives,
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
return buildNode(rootID);
|
|
1846
|
+
}
|
|
1847
|
+
// ─── Validation ──────────────────────────────────────────────────
|
|
1848
|
+
/**
|
|
1849
|
+
* Validate a CardSchema and return any error messages.
|
|
1850
|
+
*/
|
|
1851
|
+
function validateSchema(input) {
|
|
1852
|
+
const schema = normalizeSchema(input);
|
|
1853
|
+
const errors = [];
|
|
1854
|
+
if (!schema.version) {
|
|
1855
|
+
errors.push('Missing "version" field');
|
|
1856
|
+
}
|
|
1857
|
+
if (typeof schema.rootID !== 'string') {
|
|
1858
|
+
errors.push('"rootID" field must be a string');
|
|
1859
|
+
}
|
|
1860
|
+
else if (!schema.rootID) {
|
|
1861
|
+
errors.push('Missing "rootID" field');
|
|
1862
|
+
}
|
|
1863
|
+
else if (!schema.elements[schema.rootID]) {
|
|
1864
|
+
errors.push(`Root element "${schema.rootID}" not found in elements`);
|
|
1865
|
+
}
|
|
1866
|
+
const allIds = new Set(Object.keys(schema.elements));
|
|
1867
|
+
for (const [id, element] of Object.entries(schema.elements)) {
|
|
1868
|
+
if (!element.type) {
|
|
1869
|
+
errors.push(`Element "${id}" is missing a "type" field`);
|
|
1870
|
+
}
|
|
1871
|
+
// Validate slot children and groups references without invoking accessors.
|
|
1872
|
+
const slots = elementSlotEntries(element);
|
|
1873
|
+
for (const slotName of slots.accessors) {
|
|
1874
|
+
errors.push(`Element "${id}" slot "${slotName}" must be an own data property`);
|
|
1875
|
+
}
|
|
1876
|
+
if (slots.opaque) {
|
|
1877
|
+
errors.push(`Element "${id}" slots could not be inspected safely`);
|
|
1878
|
+
}
|
|
1879
|
+
for (const [slotName, slot] of slots.entries) {
|
|
1880
|
+
if (!slot || typeof slot !== 'object') {
|
|
1881
|
+
errors.push(`Element "${id}" slot "${slotName}" must be an object`);
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
const children = readOwnData(slot, 'children');
|
|
1885
|
+
if (children.kind === 'value') {
|
|
1886
|
+
const values = ownArrayDataValues(children.value);
|
|
1887
|
+
if (!values) {
|
|
1888
|
+
errors.push(`Element "${id}" slot "${slotName}" children must be an array`);
|
|
1889
|
+
}
|
|
1890
|
+
else {
|
|
1891
|
+
for (const childId of values) {
|
|
1892
|
+
if (typeof childId !== 'string' || childId.length === 0) {
|
|
1893
|
+
errors.push(`Element "${id}" slot "${slotName}" child IDs must be non-empty strings; received "${String(childId)}"`);
|
|
1894
|
+
}
|
|
1895
|
+
else if (!allIds.has(childId)) {
|
|
1896
|
+
errors.push(`Element "${id}" slot "${slotName}" references unknown child "${String(childId)}"`);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
else if (children.kind === 'accessor' || children.kind === 'opaque') {
|
|
1902
|
+
errors.push(`Element "${id}" slot "${slotName}" children must be an own data property`);
|
|
1903
|
+
}
|
|
1904
|
+
const groups = readOwnData(slot, 'groups');
|
|
1905
|
+
if (groups.kind === 'value') {
|
|
1906
|
+
const groupValues = ownArrayDataValues(groups.value);
|
|
1907
|
+
if (!groupValues) {
|
|
1908
|
+
errors.push(`Element "${id}" slot "${slotName}" groups must be an array`);
|
|
1909
|
+
}
|
|
1910
|
+
else {
|
|
1911
|
+
for (const group of groupValues) {
|
|
1912
|
+
const childrenInGroup = ownArrayDataValues(group);
|
|
1913
|
+
if (!childrenInGroup) {
|
|
1914
|
+
errors.push(`Element "${id}" slot "${slotName}" groups must contain arrays`);
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
for (const childId of childrenInGroup) {
|
|
1918
|
+
if (typeof childId !== 'string' || childId.length === 0) {
|
|
1919
|
+
errors.push(`Element "${id}" slot "${slotName}" group child IDs must be non-empty strings; received "${String(childId)}"`);
|
|
1920
|
+
}
|
|
1921
|
+
else if (!allIds.has(childId)) {
|
|
1922
|
+
errors.push(`Element "${id}" slot "${slotName}" group references unknown child "${String(childId)}"`);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
else if (groups.kind === 'accessor' || groups.kind === 'opaque') {
|
|
1929
|
+
errors.push(`Element "${id}" slot "${slotName}" groups must be an own data property`);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
validateRepeatBindings(schema, errors);
|
|
1934
|
+
return errors;
|
|
1935
|
+
}
|
|
1936
|
+
const REPEAT_SLOT_LAYOUTS = new Set([
|
|
1937
|
+
'default',
|
|
1938
|
+
'list',
|
|
1939
|
+
'grid',
|
|
1940
|
+
'horizontalScroll',
|
|
1941
|
+
'carousel',
|
|
1942
|
+
]);
|
|
1943
|
+
const ALIAS_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
1944
|
+
const RESERVED_ALIASES = new Set(['__proto__', 'constructor', 'prototype']);
|
|
1945
|
+
function hasOwn(value, key) {
|
|
1946
|
+
try {
|
|
1947
|
+
return ((typeof value === 'object' && value !== null) ||
|
|
1948
|
+
typeof value === 'function') && Object.prototype.hasOwnProperty.call(value, key);
|
|
1949
|
+
}
|
|
1950
|
+
catch {
|
|
1951
|
+
return false;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
function isPlainObject(value) {
|
|
1955
|
+
try {
|
|
1956
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1957
|
+
return false;
|
|
1958
|
+
}
|
|
1959
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1960
|
+
return prototype === Object.prototype || prototype === null;
|
|
1961
|
+
}
|
|
1962
|
+
catch {
|
|
1963
|
+
return false;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
function readOwnData(value, key) {
|
|
1967
|
+
if (!((typeof value === 'object' && value !== null)
|
|
1968
|
+
|| typeof value === 'function')) {
|
|
1969
|
+
return { kind: 'missing' };
|
|
1970
|
+
}
|
|
1971
|
+
try {
|
|
1972
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1973
|
+
if (!descriptor)
|
|
1974
|
+
return { kind: 'missing' };
|
|
1975
|
+
if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
1976
|
+
return { kind: 'accessor' };
|
|
1977
|
+
}
|
|
1978
|
+
return { kind: 'value', value: descriptor.value };
|
|
1979
|
+
}
|
|
1980
|
+
catch {
|
|
1981
|
+
return { kind: 'opaque' };
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
function ownStringDataEntries(value) {
|
|
1985
|
+
const result = {
|
|
1986
|
+
entries: [],
|
|
1987
|
+
accessors: [],
|
|
1988
|
+
opaque: false,
|
|
1989
|
+
};
|
|
1990
|
+
if (!value || typeof value !== 'object')
|
|
1991
|
+
return result;
|
|
1992
|
+
try {
|
|
1993
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
1994
|
+
if (typeof key !== 'string')
|
|
1995
|
+
continue;
|
|
1996
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1997
|
+
if (!descriptor) {
|
|
1998
|
+
result.opaque = true;
|
|
1999
|
+
}
|
|
2000
|
+
else if (Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
2001
|
+
result.entries.push([key, descriptor.value]);
|
|
2002
|
+
}
|
|
2003
|
+
else {
|
|
2004
|
+
result.accessors.push(key);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
catch {
|
|
2009
|
+
result.opaque = true;
|
|
2010
|
+
}
|
|
2011
|
+
return result;
|
|
2012
|
+
}
|
|
2013
|
+
function ownArrayDataValues(value) {
|
|
2014
|
+
try {
|
|
2015
|
+
if (!Array.isArray(value))
|
|
2016
|
+
return undefined;
|
|
2017
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
|
|
2018
|
+
const length = lengthDescriptor?.value;
|
|
2019
|
+
if (!Number.isSafeInteger(length) || length < 0)
|
|
2020
|
+
return undefined;
|
|
2021
|
+
const output = [];
|
|
2022
|
+
for (let index = 0; index < length; index += 1) {
|
|
2023
|
+
const entry = readOwnData(value, String(index));
|
|
2024
|
+
if (entry.kind !== 'value')
|
|
2025
|
+
return undefined;
|
|
2026
|
+
output.push(entry.value);
|
|
2027
|
+
}
|
|
2028
|
+
return output;
|
|
2029
|
+
}
|
|
2030
|
+
catch {
|
|
2031
|
+
return undefined;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
function elementSlotEntries(element) {
|
|
2035
|
+
const slots = readOwnData(element.props, 'slots');
|
|
2036
|
+
return slots.kind === 'value'
|
|
2037
|
+
? ownStringDataEntries(slots.value)
|
|
2038
|
+
: {
|
|
2039
|
+
entries: [],
|
|
2040
|
+
accessors: slots.kind === 'accessor' ? ['slots'] : [],
|
|
2041
|
+
opaque: slots.kind === 'opaque',
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
function readRepeatBindingFields(value) {
|
|
2045
|
+
if (!isPlainObject(value))
|
|
2046
|
+
return undefined;
|
|
2047
|
+
const source = readOwnData(value, 'source');
|
|
2048
|
+
const template = readOwnData(value, 'template');
|
|
2049
|
+
const item = readOwnData(value, 'item');
|
|
2050
|
+
const index = readOwnData(value, 'index');
|
|
2051
|
+
return {
|
|
2052
|
+
source: source.kind === 'value' ? source.value : undefined,
|
|
2053
|
+
template: template.kind === 'value' ? template.value : undefined,
|
|
2054
|
+
item: item.kind === 'value' ? item.value : undefined,
|
|
2055
|
+
...(index.kind === 'value' ? { index: index.value } : {}),
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
function slotStaticReferences(slot) {
|
|
2059
|
+
const references = [];
|
|
2060
|
+
const children = readOwnData(slot, 'children');
|
|
2061
|
+
if (children.kind === 'value') {
|
|
2062
|
+
for (const child of ownArrayDataValues(children.value) ?? []) {
|
|
2063
|
+
if (typeof child === 'string')
|
|
2064
|
+
references.push(child);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
const groups = readOwnData(slot, 'groups');
|
|
2068
|
+
if (groups.kind === 'value') {
|
|
2069
|
+
for (const group of ownArrayDataValues(groups.value) ?? []) {
|
|
2070
|
+
for (const child of ownArrayDataValues(group) ?? []) {
|
|
2071
|
+
if (typeof child === 'string')
|
|
2072
|
+
references.push(child);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
const config = readOwnData(slot, 'config');
|
|
2077
|
+
const overlays = config.kind === 'value'
|
|
2078
|
+
? readOwnData(config.value, 'overlays')
|
|
2079
|
+
: { kind: 'missing' };
|
|
2080
|
+
if (overlays.kind === 'value') {
|
|
2081
|
+
for (const overlay of ownArrayDataValues(overlays.value) ?? []) {
|
|
2082
|
+
const overlayChildren = readOwnData(overlay, 'children');
|
|
2083
|
+
if (overlayChildren.kind !== 'value')
|
|
2084
|
+
continue;
|
|
2085
|
+
for (const child of ownArrayDataValues(overlayChildren.value) ?? []) {
|
|
2086
|
+
if (typeof child === 'string')
|
|
2087
|
+
references.push(child);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
return references;
|
|
2092
|
+
}
|
|
2093
|
+
function topologyValue(read, seen = new WeakSet()) {
|
|
2094
|
+
if (read.kind !== 'value') {
|
|
2095
|
+
if (read.kind === 'missing')
|
|
2096
|
+
return null;
|
|
2097
|
+
if (read.kind === 'accessor')
|
|
2098
|
+
return '<accessor>';
|
|
2099
|
+
return '<opaque>';
|
|
2100
|
+
}
|
|
2101
|
+
const value = read.value;
|
|
2102
|
+
if (!value || typeof value !== 'object')
|
|
2103
|
+
return value;
|
|
2104
|
+
if (seen.has(value))
|
|
2105
|
+
return '<cycle>';
|
|
2106
|
+
seen.add(value);
|
|
2107
|
+
const array = ownArrayDataValues(value);
|
|
2108
|
+
if (array) {
|
|
2109
|
+
return array.map(entry => topologyValue({ kind: 'value', value: entry }, seen));
|
|
2110
|
+
}
|
|
2111
|
+
if (!isPlainObject(value))
|
|
2112
|
+
return '<opaque>';
|
|
2113
|
+
const entries = ownStringDataEntries(value);
|
|
2114
|
+
if (entries.opaque)
|
|
2115
|
+
return '<opaque>';
|
|
2116
|
+
const output = {};
|
|
2117
|
+
for (const key of entries.accessors.sort())
|
|
2118
|
+
output[key] = '<accessor>';
|
|
2119
|
+
for (const [key, entry] of entries.entries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
2120
|
+
output[key] = topologyValue({ kind: 'value', value: entry }, seen);
|
|
2121
|
+
}
|
|
2122
|
+
return output;
|
|
2123
|
+
}
|
|
2124
|
+
function getOwnElement(schema, id) {
|
|
2125
|
+
const element = readOwnData(schema.elements, id);
|
|
2126
|
+
return element.kind === 'value'
|
|
2127
|
+
? element.value
|
|
2128
|
+
: undefined;
|
|
2129
|
+
}
|
|
2130
|
+
function isCompleteExpression(source) {
|
|
2131
|
+
if (typeof source !== 'string')
|
|
2132
|
+
return false;
|
|
2133
|
+
const trimmed = source.trim();
|
|
2134
|
+
if (!trimmed.startsWith('${'))
|
|
2135
|
+
return false;
|
|
2136
|
+
if (!trimmed.slice(2, -1).trim())
|
|
2137
|
+
return false;
|
|
2138
|
+
let depth = 1;
|
|
2139
|
+
let quote;
|
|
2140
|
+
let escaped = false;
|
|
2141
|
+
for (let index = 2; index < trimmed.length; index += 1) {
|
|
2142
|
+
const character = trimmed[index];
|
|
2143
|
+
if (quote) {
|
|
2144
|
+
if (escaped) {
|
|
2145
|
+
escaped = false;
|
|
2146
|
+
}
|
|
2147
|
+
else if (character === '\\') {
|
|
2148
|
+
escaped = true;
|
|
2149
|
+
}
|
|
2150
|
+
else if (character === quote) {
|
|
2151
|
+
quote = undefined;
|
|
2152
|
+
}
|
|
2153
|
+
continue;
|
|
2154
|
+
}
|
|
2155
|
+
if (character === "'" || character === '"' || character === '`') {
|
|
2156
|
+
quote = character;
|
|
2157
|
+
}
|
|
2158
|
+
else if (character === '{') {
|
|
2159
|
+
depth += 1;
|
|
2160
|
+
}
|
|
2161
|
+
else if (character === '}') {
|
|
2162
|
+
depth -= 1;
|
|
2163
|
+
if (depth === 0)
|
|
2164
|
+
return index === trimmed.length - 1;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
return false;
|
|
2168
|
+
}
|
|
2169
|
+
function elementReferences(element) {
|
|
2170
|
+
const references = [];
|
|
2171
|
+
for (const [, slot] of elementSlotEntries(element).entries) {
|
|
2172
|
+
references.push(...slotStaticReferences(slot));
|
|
2173
|
+
const repeat = readOwnData(slot, 'repeat');
|
|
2174
|
+
const binding = repeat.kind === 'value'
|
|
2175
|
+
? readRepeatBindingFields(repeat.value)
|
|
2176
|
+
: undefined;
|
|
2177
|
+
if (typeof binding?.template === 'string' && binding.template) {
|
|
2178
|
+
references.push(binding.template);
|
|
2179
|
+
}
|
|
2180
|
+
const a2ui = getA2UIChildBinding(slot);
|
|
2181
|
+
if (typeof a2ui?.templateId === 'string' && a2ui.templateId) {
|
|
2182
|
+
references.push(a2ui.templateId);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
return references;
|
|
2186
|
+
}
|
|
2187
|
+
function validateRepeatBindings(schema, errors) {
|
|
2188
|
+
const dynamicBindings = [];
|
|
2189
|
+
for (const [id, element] of Object.entries(schema.elements)) {
|
|
2190
|
+
const elementRepeats = elementSlotEntries(element).entries
|
|
2191
|
+
.filter((entry) => hasOwn(entry[1], 'repeat')
|
|
2192
|
+
|| getA2UIChildBinding(entry[1]) !== undefined);
|
|
2193
|
+
if (elementRepeats.length > 1) {
|
|
2194
|
+
errors.push(`Element "${id}" has more than one dynamic slot`);
|
|
2195
|
+
}
|
|
2196
|
+
for (const [slotName, slot] of elementRepeats) {
|
|
2197
|
+
const a2ui = getA2UIChildBinding(slot);
|
|
2198
|
+
const repeat = readOwnData(slot, 'repeat');
|
|
2199
|
+
if (repeat.kind === 'missing' && a2ui) {
|
|
2200
|
+
dynamicBindings.push({
|
|
2201
|
+
owner: id,
|
|
2202
|
+
slotName,
|
|
2203
|
+
dialect: 'a2ui',
|
|
2204
|
+
template: a2ui.templateId,
|
|
2205
|
+
});
|
|
2206
|
+
if (typeof a2ui.templateId !== 'string'
|
|
2207
|
+
|| a2ui.templateId.length === 0) {
|
|
2208
|
+
errors.push(`Element "${id}" slot "${slotName}" dynamic children template must be a non-empty string`);
|
|
2209
|
+
}
|
|
2210
|
+
else if (!getOwnElement(schema, a2ui.templateId)) {
|
|
2211
|
+
errors.push(`Element "${id}" slot "${slotName}" dynamic children reference unknown template "${a2ui.templateId}"`);
|
|
2212
|
+
}
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2215
|
+
const rawBinding = repeat.kind === 'value'
|
|
2216
|
+
? repeat.value
|
|
2217
|
+
: undefined;
|
|
2218
|
+
if (!isPlainObject(rawBinding)) {
|
|
2219
|
+
errors.push(`Element "${id}" slot "${slotName}" repeat must be a non-null plain object`);
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
const binding = readRepeatBindingFields(rawBinding);
|
|
2223
|
+
dynamicBindings.push({
|
|
2224
|
+
owner: id,
|
|
2225
|
+
slotName,
|
|
2226
|
+
dialect: 'native',
|
|
2227
|
+
template: binding.template,
|
|
2228
|
+
binding,
|
|
2229
|
+
});
|
|
2230
|
+
if (!REPEAT_SLOT_LAYOUTS.has(slotName)) {
|
|
2231
|
+
errors.push(`Element "${id}" slot "${slotName}" does not support repeat`);
|
|
2232
|
+
}
|
|
2233
|
+
const children = readOwnData(slot, 'children');
|
|
2234
|
+
const groups = readOwnData(slot, 'groups');
|
|
2235
|
+
if ((children.kind === 'value' && !!children.value)
|
|
2236
|
+
|| children.kind === 'accessor'
|
|
2237
|
+
|| children.kind === 'opaque'
|
|
2238
|
+
|| (groups.kind === 'value' && !!groups.value)
|
|
2239
|
+
|| groups.kind === 'accessor'
|
|
2240
|
+
|| groups.kind === 'opaque') {
|
|
2241
|
+
errors.push(`Element "${id}" slot "${slotName}" cannot combine repeat with children or groups`);
|
|
2242
|
+
}
|
|
2243
|
+
if (typeof binding.source !== 'string') {
|
|
2244
|
+
errors.push(`Element "${id}" slot "${slotName}" repeat source must be a string`);
|
|
2245
|
+
}
|
|
2246
|
+
else if (!isCompleteExpression(binding.source)) {
|
|
2247
|
+
errors.push(`Element "${id}" slot "${slotName}" repeat source must be one complete non-empty \${...} expression`);
|
|
2248
|
+
}
|
|
2249
|
+
if (typeof binding.template !== 'string') {
|
|
2250
|
+
errors.push(`Element "${id}" slot "${slotName}" repeat template must be a string`);
|
|
2251
|
+
}
|
|
2252
|
+
else if (!binding.template) {
|
|
2253
|
+
errors.push(`Element "${id}" slot "${slotName}" repeat template must be a non-empty string`);
|
|
2254
|
+
}
|
|
2255
|
+
else if (!getOwnElement(schema, binding.template)) {
|
|
2256
|
+
errors.push(`Element "${id}" slot "${slotName}" repeat references unknown template "${binding.template}"`);
|
|
2257
|
+
}
|
|
2258
|
+
validateAlias(id, slotName, 'item', binding.item, errors);
|
|
2259
|
+
if (binding.index !== undefined) {
|
|
2260
|
+
validateAlias(id, slotName, 'index', binding.index, errors);
|
|
2261
|
+
}
|
|
2262
|
+
if (binding.index !== undefined && binding.item === binding.index) {
|
|
2263
|
+
errors.push(`Element "${id}" slot "${slotName}" repeat item and index aliases must be different`);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
validateRepeatNesting(schema, dynamicBindings, errors);
|
|
2268
|
+
validateDynamicClosures(schema, dynamicBindings, errors);
|
|
2269
|
+
}
|
|
2270
|
+
function validateAlias(owner, slotName, kind, alias, errors) {
|
|
2271
|
+
if (typeof alias !== 'string') {
|
|
2272
|
+
errors.push(`Element "${owner}" slot "${slotName}" repeat ${kind} must be a string`);
|
|
2273
|
+
}
|
|
2274
|
+
else if (!ALIAS_PATTERN.test(alias)) {
|
|
2275
|
+
errors.push(`Element "${owner}" slot "${slotName}" repeat ${kind} alias "${alias}" is invalid`);
|
|
2276
|
+
}
|
|
2277
|
+
else if (RESERVED_ALIASES.has(alias)) {
|
|
2278
|
+
errors.push(`Element "${owner}" slot "${slotName}" repeat ${kind} alias "${alias}" is reserved`);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
function validateRepeatNesting(schema, bindings, errors) {
|
|
2282
|
+
const reportedCycles = new Set();
|
|
2283
|
+
const reportedCollisions = new Set();
|
|
2284
|
+
function visitElement(id, activeAliases, repeatPath, visitedStatic) {
|
|
2285
|
+
const element = getOwnElement(schema, id);
|
|
2286
|
+
if (!element || visitedStatic.has(id))
|
|
2287
|
+
return;
|
|
2288
|
+
const nextVisitedStatic = new Set(visitedStatic).add(id);
|
|
2289
|
+
for (const [slotName, slotValue] of elementSlotEntries(element).entries) {
|
|
2290
|
+
if (!slotValue || typeof slotValue !== 'object')
|
|
2291
|
+
continue;
|
|
2292
|
+
const slot = slotValue;
|
|
2293
|
+
const repeat = readOwnData(slot, 'repeat');
|
|
2294
|
+
const nativeBinding = repeat.kind === 'value'
|
|
2295
|
+
? readRepeatBindingFields(repeat.value)
|
|
2296
|
+
: undefined;
|
|
2297
|
+
const a2uiBinding = getA2UIChildBinding(slot);
|
|
2298
|
+
const template = nativeBinding?.template ?? a2uiBinding?.templateId;
|
|
2299
|
+
if (typeof template === 'string' && template) {
|
|
2300
|
+
const cycleAt = repeatPath.indexOf(template);
|
|
2301
|
+
if (cycleAt >= 0) {
|
|
2302
|
+
const cycle = [...repeatPath.slice(cycleAt), template].join(' -> ');
|
|
2303
|
+
if (!reportedCycles.has(cycle)) {
|
|
2304
|
+
reportedCycles.add(cycle);
|
|
2305
|
+
errors.push(nativeBinding
|
|
2306
|
+
? `Element "${id}" slot "${slotName}" has repeat template cycle: ${cycle}`
|
|
2307
|
+
: `Element "${id}" slot "${slotName}" has dynamic template cycle: ${cycle}`);
|
|
2308
|
+
}
|
|
2309
|
+
continue;
|
|
2310
|
+
}
|
|
2311
|
+
const aliases = [nativeBinding?.item, nativeBinding?.index].filter((alias) => typeof alias === 'string');
|
|
2312
|
+
for (const alias of aliases) {
|
|
2313
|
+
if (activeAliases.has(alias)) {
|
|
2314
|
+
const key = `${id}:${slotName}:${alias}`;
|
|
2315
|
+
if (!reportedCollisions.has(key)) {
|
|
2316
|
+
reportedCollisions.add(key);
|
|
2317
|
+
errors.push(`Element "${id}" slot "${slotName}" repeat alias "${alias}" conflicts with an active repeat alias`);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
const nestedAliases = new Set(activeAliases);
|
|
2322
|
+
for (const alias of aliases)
|
|
2323
|
+
nestedAliases.add(alias);
|
|
2324
|
+
visitElement(template, nestedAliases, [...repeatPath, template], new Set());
|
|
2325
|
+
}
|
|
2326
|
+
for (const child of slotStaticReferences(slot)) {
|
|
2327
|
+
visitElement(child, activeAliases, repeatPath, nextVisitedStatic);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
for (const dynamic of bindings) {
|
|
2332
|
+
const { owner, binding, template } = dynamic;
|
|
2333
|
+
const aliases = new Set([binding?.item, binding?.index].filter((alias) => typeof alias === 'string'));
|
|
2334
|
+
if (typeof template === 'string' && template) {
|
|
2335
|
+
visitElement(template, aliases, [owner, template], new Set());
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
function validateDynamicClosures(schema, repeats, errors) {
|
|
2340
|
+
for (const { owner } of repeats) {
|
|
2341
|
+
const visited = new Set();
|
|
2342
|
+
function visit(id) {
|
|
2343
|
+
if (visited.has(id))
|
|
2344
|
+
return;
|
|
2345
|
+
visited.add(id);
|
|
2346
|
+
const element = getOwnElement(schema, id);
|
|
2347
|
+
if (!element)
|
|
2348
|
+
return;
|
|
2349
|
+
const lifecycle = readOwnData(element, 'lifecycle');
|
|
2350
|
+
if ((lifecycle.kind === 'value' && !!lifecycle.value)
|
|
2351
|
+
|| lifecycle.kind === 'accessor'
|
|
2352
|
+
|| lifecycle.kind === 'opaque') {
|
|
2353
|
+
errors.push(`Element "${id}" in dynamic patch closure owned by "${owner}" cannot use lifecycle`);
|
|
2354
|
+
}
|
|
2355
|
+
if (hasOwn(element.props, 'variableKey')) {
|
|
2356
|
+
errors.push(`Element "${id}" in dynamic patch closure owned by "${owner}" cannot use props.variableKey`);
|
|
2357
|
+
}
|
|
2358
|
+
for (const reference of elementReferences(element))
|
|
2359
|
+
visit(reference);
|
|
2360
|
+
}
|
|
2361
|
+
visit(owner);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
/** Build a live expression object whose own properties are the current aliases. */
|
|
2366
|
+
function createExpressionContext(scope) {
|
|
2367
|
+
const prototype = scope.parent
|
|
2368
|
+
? createExpressionContext(scope.parent)
|
|
2369
|
+
: scope.root;
|
|
2370
|
+
return Object.assign(Object.create(prototype), scope.locals);
|
|
2371
|
+
}
|
|
2372
|
+
function isBoundRenderTreeNode(value) {
|
|
2373
|
+
if (!value || typeof value !== 'object')
|
|
2374
|
+
return false;
|
|
2375
|
+
const node = value;
|
|
2376
|
+
return (typeof node.id === 'string' &&
|
|
2377
|
+
typeof node.sourceId === 'string' &&
|
|
2378
|
+
typeof node.instancePath === 'string' &&
|
|
2379
|
+
typeof node.dataPath === 'string' &&
|
|
2380
|
+
!!node.scope &&
|
|
2381
|
+
(node.bindingDialect === 'native' || node.bindingDialect === 'a2ui') &&
|
|
2382
|
+
Array.isArray(node.children));
|
|
2383
|
+
}
|
|
2384
|
+
function cloneValue(value, seen = new WeakMap()) {
|
|
2385
|
+
if (!value || typeof value !== 'object')
|
|
2386
|
+
return value;
|
|
2387
|
+
const existing = seen.get(value);
|
|
2388
|
+
if (existing)
|
|
2389
|
+
return existing;
|
|
2390
|
+
let isArray;
|
|
2391
|
+
let prototype;
|
|
2392
|
+
let descriptors;
|
|
2393
|
+
try {
|
|
2394
|
+
isArray = Array.isArray(value);
|
|
2395
|
+
prototype = Object.getPrototypeOf(value);
|
|
2396
|
+
descriptors = [];
|
|
2397
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
2398
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
2399
|
+
if (descriptor)
|
|
2400
|
+
descriptors.push([key, descriptor]);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
catch {
|
|
2404
|
+
return value;
|
|
2405
|
+
}
|
|
2406
|
+
const result = isArray ? [] : Object.create(prototype);
|
|
2407
|
+
seen.set(value, result);
|
|
2408
|
+
for (const [key, descriptor] of descriptors) {
|
|
2409
|
+
if (isArray && key === 'length')
|
|
2410
|
+
continue;
|
|
2411
|
+
if (Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
2412
|
+
Object.defineProperty(result, key, {
|
|
2413
|
+
value: cloneValue(descriptor.value, seen),
|
|
2414
|
+
enumerable: descriptor.enumerable,
|
|
2415
|
+
configurable: true,
|
|
2416
|
+
writable: true,
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
else {
|
|
2420
|
+
Object.defineProperty(result, key, descriptor);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
return result;
|
|
2424
|
+
}
|
|
2425
|
+
function escapeRuntimePart(value) {
|
|
2426
|
+
return Array.from(value, character => character.codePointAt(0).toString(16)).join('-');
|
|
2427
|
+
}
|
|
2428
|
+
function appendOccurrence(outerPath, kind, ownerId, slot, position) {
|
|
2429
|
+
const segments = [
|
|
2430
|
+
kind === 'item' ? 'i' : 'c',
|
|
2431
|
+
ownerId,
|
|
2432
|
+
slot,
|
|
2433
|
+
position,
|
|
2434
|
+
];
|
|
2435
|
+
return outerPath + segments
|
|
2436
|
+
.map(segment => `${segment.length}:${segment}`)
|
|
2437
|
+
.join('');
|
|
2438
|
+
}
|
|
2439
|
+
function runtimeId(sourceId, instancePath) {
|
|
2440
|
+
return `r_${escapeRuntimePart(sourceId)}_${escapeRuntimePart(instancePath)}`;
|
|
2441
|
+
}
|
|
2442
|
+
function pointerSegment(value) {
|
|
2443
|
+
return value.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
2444
|
+
}
|
|
2445
|
+
function joinPointer(base, segments) {
|
|
2446
|
+
return `${base}${segments.map(segment => `/${pointerSegment(segment)}`).join('')}`;
|
|
2447
|
+
}
|
|
2448
|
+
function collectSlotReferences(slot) {
|
|
2449
|
+
const references = [];
|
|
2450
|
+
slot.children?.forEach((sourceId, index) => {
|
|
2451
|
+
references.push({
|
|
2452
|
+
sourceId,
|
|
2453
|
+
position: `children:${index}`,
|
|
2454
|
+
assign: id => { slot.children[index] = id; },
|
|
2455
|
+
});
|
|
2456
|
+
});
|
|
2457
|
+
slot.groups?.forEach((group, groupIndex) => {
|
|
2458
|
+
group.forEach((sourceId, childIndex) => {
|
|
2459
|
+
references.push({
|
|
2460
|
+
sourceId,
|
|
2461
|
+
position: `groups:${groupIndex}:${childIndex}`,
|
|
2462
|
+
assign: id => { slot.groups[groupIndex][childIndex] = id; },
|
|
2463
|
+
});
|
|
2464
|
+
});
|
|
2465
|
+
});
|
|
2466
|
+
const overlays = slot.config?.overlays;
|
|
2467
|
+
if (Array.isArray(overlays)) {
|
|
2468
|
+
overlays.forEach((overlay, overlayIndex) => {
|
|
2469
|
+
if (!Array.isArray(overlay?.children))
|
|
2470
|
+
return;
|
|
2471
|
+
overlay.children.forEach((sourceId, childIndex) => {
|
|
2472
|
+
references.push({
|
|
2473
|
+
sourceId,
|
|
2474
|
+
position: `overlays:${overlayIndex}:${childIndex}`,
|
|
2475
|
+
assign: id => { overlay.children[childIndex] = id; },
|
|
2476
|
+
});
|
|
2477
|
+
});
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
return references;
|
|
2481
|
+
}
|
|
2482
|
+
function isPointerAncestor(ancestor, descendant) {
|
|
2483
|
+
if (ancestor === '')
|
|
2484
|
+
return descendant !== '';
|
|
2485
|
+
return descendant.startsWith(`${ancestor}/`);
|
|
2486
|
+
}
|
|
2487
|
+
function pointersOverlap(left, right) {
|
|
2488
|
+
return (left === right ||
|
|
2489
|
+
isPointerAncestor(left, right) ||
|
|
2490
|
+
isPointerAncestor(right, left));
|
|
2491
|
+
}
|
|
2492
|
+
function ownersInInsertionOrder(card, selectedKeys) {
|
|
2493
|
+
const result = [];
|
|
2494
|
+
for (const [key, owner] of card.repeatOwners) {
|
|
2495
|
+
if (selectedKeys.has(key))
|
|
2496
|
+
result.push(owner);
|
|
2497
|
+
}
|
|
2498
|
+
return result;
|
|
2499
|
+
}
|
|
2500
|
+
function removeOwnersCoveredByAncestors(card, selectedKeys) {
|
|
2501
|
+
const result = new Set(selectedKeys);
|
|
2502
|
+
for (const key of selectedKeys) {
|
|
2503
|
+
let parentKey = card.repeatOwners.get(key)?.parentKey;
|
|
2504
|
+
while (parentKey) {
|
|
2505
|
+
if (selectedKeys.has(parentKey)) {
|
|
2506
|
+
result.delete(key);
|
|
2507
|
+
break;
|
|
2508
|
+
}
|
|
2509
|
+
parentKey = card.repeatOwners.get(parentKey)?.parentKey;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
return result;
|
|
2513
|
+
}
|
|
2514
|
+
/** Select repeat owners affected by a JSON Pointer data update. */
|
|
2515
|
+
function findAffectedRepeatOwners(card, updatePath) {
|
|
2516
|
+
if (card.unresolvedRepeatOwners.size > 0)
|
|
2517
|
+
return [];
|
|
2518
|
+
const selected = new Set();
|
|
2519
|
+
for (const [dependency, owners] of card.valueDependencies) {
|
|
2520
|
+
if (!dependency.startsWith('data:'))
|
|
2521
|
+
continue;
|
|
2522
|
+
const dependencyPath = dependency.slice('data:'.length);
|
|
2523
|
+
if (!pointersOverlap(updatePath, dependencyPath))
|
|
2524
|
+
continue;
|
|
2525
|
+
for (const owner of owners)
|
|
2526
|
+
selected.add(owner);
|
|
2527
|
+
}
|
|
2528
|
+
const exactStructuralOwners = new Set();
|
|
2529
|
+
const containingStructuralOwners = new Set();
|
|
2530
|
+
let nearestStructuralLength = -1;
|
|
2531
|
+
const nearestStructuralOwners = new Set();
|
|
2532
|
+
for (const owner of card.repeatOwners.values()) {
|
|
2533
|
+
const sourcePath = owner.sourcePath;
|
|
2534
|
+
if (sourcePath === undefined)
|
|
2535
|
+
continue;
|
|
2536
|
+
if (updatePath === sourcePath) {
|
|
2537
|
+
exactStructuralOwners.add(owner.key);
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2540
|
+
if (isPointerAncestor(updatePath, sourcePath)) {
|
|
2541
|
+
containingStructuralOwners.add(owner.key);
|
|
2542
|
+
continue;
|
|
2543
|
+
}
|
|
2544
|
+
if (isPointerAncestor(sourcePath, updatePath)) {
|
|
2545
|
+
if (sourcePath.length > nearestStructuralLength) {
|
|
2546
|
+
nearestStructuralLength = sourcePath.length;
|
|
2547
|
+
nearestStructuralOwners.clear();
|
|
2548
|
+
}
|
|
2549
|
+
if (sourcePath.length === nearestStructuralLength) {
|
|
2550
|
+
nearestStructuralOwners.add(owner.key);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
if (exactStructuralOwners.size > 0) {
|
|
2555
|
+
for (const owner of exactStructuralOwners)
|
|
2556
|
+
selected.add(owner);
|
|
2557
|
+
for (const owner of containingStructuralOwners)
|
|
2558
|
+
selected.add(owner);
|
|
2559
|
+
}
|
|
2560
|
+
else {
|
|
2561
|
+
for (const owner of containingStructuralOwners)
|
|
2562
|
+
selected.add(owner);
|
|
2563
|
+
for (const owner of nearestStructuralOwners)
|
|
2564
|
+
selected.add(owner);
|
|
2565
|
+
}
|
|
2566
|
+
return ownersInInsertionOrder(card, removeOwnersCoveredByAncestors(card, selected));
|
|
2567
|
+
}
|
|
2568
|
+
/** Select repeat owners whose materialized closure contains a source definition. */
|
|
2569
|
+
function findTemplateRepeatOwners(card, sourceIds) {
|
|
2570
|
+
const selected = new Set();
|
|
2571
|
+
for (const sourceId of sourceIds) {
|
|
2572
|
+
const owners = card.dependencies.get(`component:${sourceId}`);
|
|
2573
|
+
if (!owners)
|
|
2574
|
+
continue;
|
|
2575
|
+
for (const owner of owners)
|
|
2576
|
+
selected.add(owner);
|
|
2577
|
+
}
|
|
2578
|
+
const runtimeOwnerCounts = new Map();
|
|
2579
|
+
for (const owner of card.repeatOwners.values()) {
|
|
2580
|
+
runtimeOwnerCounts.set(owner.runtimeOwnerId, (runtimeOwnerCounts.get(owner.runtimeOwnerId) ?? 0) + 1);
|
|
2581
|
+
}
|
|
2582
|
+
for (const key of selected) {
|
|
2583
|
+
const runtimeOwnerId = card.repeatOwners.get(key)?.runtimeOwnerId;
|
|
2584
|
+
if (runtimeOwnerId && (runtimeOwnerCounts.get(runtimeOwnerId) ?? 0) > 1) {
|
|
2585
|
+
return [];
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
return ownersInInsertionOrder(card, removeOwnersCoveredByAncestors(card, selected));
|
|
2589
|
+
}
|
|
2590
|
+
/**
|
|
2591
|
+
* Normalize a card and recursively turn native repeat declarations into a
|
|
2592
|
+
* renderer-ready tree with ordinary children and occurrence-specific IDs.
|
|
2593
|
+
*/
|
|
2594
|
+
function materializeCard(input, variables, options = {}) {
|
|
2595
|
+
const schema = normalizeSchema(input);
|
|
2596
|
+
const rootVariables = variables ?? schema.variables;
|
|
2597
|
+
const maxDepth = options.maxDepth ?? 3;
|
|
2598
|
+
const maxItems = options.maxItems ?? 100;
|
|
2599
|
+
const instances = new Map();
|
|
2600
|
+
const schemaSourceIds = new Set(Object.keys(schema.elements));
|
|
2601
|
+
const publicIds = new Set(schemaSourceIds);
|
|
2602
|
+
const repeatOwners = new Map();
|
|
2603
|
+
const dependencies = new Map();
|
|
2604
|
+
const valueDependencies = new Map();
|
|
2605
|
+
const unresolvedRepeatOwners = new Set();
|
|
2606
|
+
const scopeAliasPaths = new WeakMap();
|
|
2607
|
+
const scopeIndexAliases = new WeakMap();
|
|
2608
|
+
const runtimeOwnerKeys = new Map();
|
|
2609
|
+
const diagnostics = [];
|
|
2610
|
+
let repeatedItemCount = 0;
|
|
2611
|
+
let hasRepeat = false;
|
|
2612
|
+
const rootScope = {
|
|
2613
|
+
root: rootVariables,
|
|
2614
|
+
locals: {},
|
|
2615
|
+
dataPath: '',
|
|
2616
|
+
bindingDialect: 'native',
|
|
2617
|
+
};
|
|
2618
|
+
scopeAliasPaths.set(rootScope, new Map());
|
|
2619
|
+
scopeIndexAliases.set(rootScope, new Set());
|
|
2620
|
+
function addDependency(type, value, ownerKey) {
|
|
2621
|
+
const key = `${type}:${value}`;
|
|
2622
|
+
const owners = dependencies.get(key);
|
|
2623
|
+
if (owners) {
|
|
2624
|
+
owners.add(ownerKey);
|
|
2625
|
+
}
|
|
2626
|
+
else {
|
|
2627
|
+
dependencies.set(key, new Set([ownerKey]));
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
function addValueDependency(pointer, ownerKey) {
|
|
2631
|
+
addDependency('data', pointer, ownerKey);
|
|
2632
|
+
const key = `data:${pointer}`;
|
|
2633
|
+
const owners = valueDependencies.get(key);
|
|
2634
|
+
if (owners) {
|
|
2635
|
+
owners.add(ownerKey);
|
|
2636
|
+
}
|
|
2637
|
+
else {
|
|
2638
|
+
valueDependencies.set(key, new Set([ownerKey]));
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
function resolveDependencyPath(path, scope) {
|
|
2642
|
+
const segments = path.split('.');
|
|
2643
|
+
const alias = segments[0];
|
|
2644
|
+
let cursor = scope;
|
|
2645
|
+
while (cursor) {
|
|
2646
|
+
if (Object.prototype.hasOwnProperty.call(cursor.locals, alias)) {
|
|
2647
|
+
const aliases = scopeAliasPaths.get(cursor);
|
|
2648
|
+
if (!aliases?.has(alias))
|
|
2649
|
+
return undefined;
|
|
2650
|
+
const base = aliases.get(alias);
|
|
2651
|
+
if (base === undefined)
|
|
2652
|
+
return undefined;
|
|
2653
|
+
if (scopeIndexAliases.get(cursor)?.has(alias))
|
|
2654
|
+
return base;
|
|
2655
|
+
return joinPointer(base, segments.slice(1));
|
|
2656
|
+
}
|
|
2657
|
+
cursor = cursor.parent;
|
|
2658
|
+
}
|
|
2659
|
+
return joinPointer('', segments);
|
|
2660
|
+
}
|
|
2661
|
+
function resolveSimpleExpressionPath(expression, scope) {
|
|
2662
|
+
const match = expression.trim().match(/^\$\{\s*([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)\s*\}$/);
|
|
2663
|
+
return match ? resolveDependencyPath(match[1], scope) : undefined;
|
|
2664
|
+
}
|
|
2665
|
+
function resolveA2UIPointer(path, scopePath) {
|
|
2666
|
+
return path.startsWith('/')
|
|
2667
|
+
? path
|
|
2668
|
+
: path === ''
|
|
2669
|
+
? scopePath
|
|
2670
|
+
: `${scopePath}/${path}`;
|
|
2671
|
+
}
|
|
2672
|
+
function ownDataEntries(value) {
|
|
2673
|
+
try {
|
|
2674
|
+
const entries = [];
|
|
2675
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
2676
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
2677
|
+
if (descriptor &&
|
|
2678
|
+
Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
2679
|
+
entries.push([key, descriptor.value]);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
return entries;
|
|
2683
|
+
}
|
|
2684
|
+
catch {
|
|
2685
|
+
return undefined;
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
function isIndexAliasPath(path, scope) {
|
|
2689
|
+
const alias = path.split('.')[0];
|
|
2690
|
+
let cursor = scope;
|
|
2691
|
+
while (cursor) {
|
|
2692
|
+
if (Object.prototype.hasOwnProperty.call(cursor.locals, alias)) {
|
|
2693
|
+
return scopeIndexAliases.get(cursor)?.has(alias) ?? false;
|
|
2694
|
+
}
|
|
2695
|
+
cursor = cursor.parent;
|
|
2696
|
+
}
|
|
2697
|
+
return false;
|
|
2698
|
+
}
|
|
2699
|
+
function scanExpressionString(value, scope, ownerKey) {
|
|
2700
|
+
if (!value.includes('${'))
|
|
2701
|
+
return;
|
|
2702
|
+
const expressions = [...value.matchAll(/\$\{([^{}]*)\}/g)];
|
|
2703
|
+
const unmatched = value.replace(/\$\{[^{}]*\}/g, '').includes('${');
|
|
2704
|
+
if (expressions.length === 0 || unmatched) {
|
|
2705
|
+
unresolvedRepeatOwners.add(ownerKey);
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
for (const expression of expressions) {
|
|
2709
|
+
const path = expression[1].trim();
|
|
2710
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(path)) {
|
|
2711
|
+
unresolvedRepeatOwners.add(ownerKey);
|
|
2712
|
+
continue;
|
|
2713
|
+
}
|
|
2714
|
+
const pointer = resolveDependencyPath(path, scope);
|
|
2715
|
+
if (pointer === undefined) {
|
|
2716
|
+
unresolvedRepeatOwners.add(ownerKey);
|
|
2717
|
+
}
|
|
2718
|
+
else if (isIndexAliasPath(path, scope)) {
|
|
2719
|
+
addDependency('data', pointer, ownerKey);
|
|
2720
|
+
}
|
|
2721
|
+
else {
|
|
2722
|
+
addValueDependency(pointer, ownerKey);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
function scanDependencyValue(value, scope, ownerKey, visited) {
|
|
2727
|
+
if (typeof value === 'string') {
|
|
2728
|
+
scanExpressionString(value, scope, ownerKey);
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
if (!value || typeof value !== 'object')
|
|
2732
|
+
return;
|
|
2733
|
+
if (visited.has(value))
|
|
2734
|
+
return;
|
|
2735
|
+
visited.add(value);
|
|
2736
|
+
if (scope.bindingDialect === 'a2ui' && isA2UIPathBinding(value)) {
|
|
2737
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, 'path');
|
|
2738
|
+
const path = descriptor?.value;
|
|
2739
|
+
if (typeof path === 'string') {
|
|
2740
|
+
addValueDependency(resolveA2UIPointer(path, scope.dataPath), ownerKey);
|
|
2741
|
+
}
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
const entries = ownDataEntries(value);
|
|
2745
|
+
if (!entries) {
|
|
2746
|
+
unresolvedRepeatOwners.add(ownerKey);
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
for (const [key, child] of entries) {
|
|
2750
|
+
if (Array.isArray(value) && key === 'length')
|
|
2751
|
+
continue;
|
|
2752
|
+
scanDependencyValue(child, scope, ownerKey, visited);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
function scanProps(props, scope, ownerKey, visited) {
|
|
2756
|
+
if (visited.has(props))
|
|
2757
|
+
return;
|
|
2758
|
+
visited.add(props);
|
|
2759
|
+
const propEntries = ownDataEntries(props);
|
|
2760
|
+
if (!propEntries) {
|
|
2761
|
+
unresolvedRepeatOwners.add(ownerKey);
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
for (const [key, value] of propEntries) {
|
|
2765
|
+
if (key !== 'slots') {
|
|
2766
|
+
scanDependencyValue(value, scope, ownerKey, visited);
|
|
2767
|
+
continue;
|
|
2768
|
+
}
|
|
2769
|
+
if (!value || typeof value !== 'object')
|
|
2770
|
+
continue;
|
|
2771
|
+
const slotEntries = ownDataEntries(value);
|
|
2772
|
+
if (!slotEntries) {
|
|
2773
|
+
unresolvedRepeatOwners.add(ownerKey);
|
|
2774
|
+
continue;
|
|
2775
|
+
}
|
|
2776
|
+
for (const [, slot] of slotEntries) {
|
|
2777
|
+
if (!slot || typeof slot !== 'object')
|
|
2778
|
+
continue;
|
|
2779
|
+
const entries = ownDataEntries(slot);
|
|
2780
|
+
if (!entries) {
|
|
2781
|
+
unresolvedRepeatOwners.add(ownerKey);
|
|
2782
|
+
continue;
|
|
2783
|
+
}
|
|
2784
|
+
for (const [slotKey, slotValue] of entries) {
|
|
2785
|
+
if (slotKey === 'repeat' || slotKey === A2UI_CHILD_BINDING)
|
|
2786
|
+
continue;
|
|
2787
|
+
scanDependencyValue(slotValue, scope, ownerKey, visited);
|
|
1164
2788
|
}
|
|
1165
2789
|
}
|
|
1166
2790
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
2791
|
+
}
|
|
2792
|
+
function allocateRuntimeId(sourceId, instancePath) {
|
|
2793
|
+
const base = runtimeId(sourceId, instancePath);
|
|
2794
|
+
let id = base;
|
|
2795
|
+
let suffix = 1;
|
|
2796
|
+
while (publicIds.has(id) || instances.has(id)) {
|
|
2797
|
+
id = `${base}_${suffix}`;
|
|
2798
|
+
suffix += 1;
|
|
2799
|
+
}
|
|
2800
|
+
publicIds.add(id);
|
|
2801
|
+
return id;
|
|
2802
|
+
}
|
|
2803
|
+
function allocateStaticInstanceKey(id) {
|
|
2804
|
+
const base = `s_${escapeRuntimePart(id)}`;
|
|
2805
|
+
let suffix = 1;
|
|
2806
|
+
let key = `${base}_${suffix}`;
|
|
2807
|
+
while (schemaSourceIds.has(key) ||
|
|
2808
|
+
publicIds.has(key) ||
|
|
2809
|
+
instances.has(key)) {
|
|
2810
|
+
suffix += 1;
|
|
2811
|
+
key = `${base}_${suffix}`;
|
|
2812
|
+
}
|
|
2813
|
+
return key;
|
|
2814
|
+
}
|
|
2815
|
+
function buildNode(sourceId, scope, instancePath, structuralPath, repeatedOccurrence, parentId, repeatDepth, nearestOwnerKey, activeDefinitions) {
|
|
2816
|
+
if (activeDefinitions.has(sourceId)) {
|
|
2817
|
+
throw new Error(`[CardMaterializer] Circular reference detected at "${sourceId}"`);
|
|
2818
|
+
}
|
|
2819
|
+
const nextActiveDefinitions = new Set(activeDefinitions).add(sourceId);
|
|
2820
|
+
const element = schema.elements[sourceId];
|
|
2821
|
+
if (!element) {
|
|
2822
|
+
throw new Error(`[CardMaterializer] Missing element for id "${sourceId}"`);
|
|
2823
|
+
}
|
|
2824
|
+
const bindingDialect = (scope.bindingDialect === 'a2ui' ||
|
|
2825
|
+
hasA2UIBindingDialect(element)) ? 'a2ui' : 'native';
|
|
2826
|
+
const activeScope = bindingDialect === scope.bindingDialect
|
|
2827
|
+
? scope
|
|
2828
|
+
: { ...scope, bindingDialect };
|
|
2829
|
+
if (activeScope !== scope) {
|
|
2830
|
+
scopeAliasPaths.set(activeScope, new Map(scopeAliasPaths.get(scope) ?? []));
|
|
2831
|
+
scopeIndexAliases.set(activeScope, new Set(scopeIndexAliases.get(scope) ?? []));
|
|
2832
|
+
}
|
|
2833
|
+
const id = repeatedOccurrence
|
|
2834
|
+
? allocateRuntimeId(sourceId, instancePath)
|
|
2835
|
+
: sourceId;
|
|
2836
|
+
const props = cloneValue(element.props);
|
|
2837
|
+
const node = {
|
|
1169
2838
|
id,
|
|
2839
|
+
sourceId,
|
|
2840
|
+
instancePath,
|
|
2841
|
+
dataPath: activeScope.dataPath,
|
|
2842
|
+
scope: activeScope,
|
|
2843
|
+
bindingDialect,
|
|
1170
2844
|
type: element.type,
|
|
1171
|
-
props
|
|
1172
|
-
children,
|
|
2845
|
+
props,
|
|
2846
|
+
children: [],
|
|
1173
2847
|
lifecycle: element.lifecycle,
|
|
1174
2848
|
events: element.events,
|
|
1175
2849
|
directives: element.directives,
|
|
1176
2850
|
};
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
// ─── Validation ──────────────────────────────────────────────────
|
|
1181
|
-
/**
|
|
1182
|
-
* Validate a CardSchema and return any error messages.
|
|
1183
|
-
*/
|
|
1184
|
-
function validateSchema(input) {
|
|
1185
|
-
const schema = normalizeSchema(input);
|
|
1186
|
-
const errors = [];
|
|
1187
|
-
if (!schema.version) {
|
|
1188
|
-
errors.push('Missing "version" field');
|
|
1189
|
-
}
|
|
1190
|
-
if (!schema.rootID) {
|
|
1191
|
-
errors.push('Missing "rootID" field');
|
|
1192
|
-
}
|
|
1193
|
-
else if (!schema.elements[schema.rootID]) {
|
|
1194
|
-
errors.push(`Root element "${schema.rootID}" not found in elements`);
|
|
1195
|
-
}
|
|
1196
|
-
const allIds = new Set(Object.keys(schema.elements));
|
|
1197
|
-
for (const [id, element] of Object.entries(schema.elements)) {
|
|
1198
|
-
if (!element.type) {
|
|
1199
|
-
errors.push(`Element "${id}" is missing a "type" field`);
|
|
2851
|
+
let instanceKey = id;
|
|
2852
|
+
if (!repeatedOccurrence && instances.has(instanceKey)) {
|
|
2853
|
+
instanceKey = allocateStaticInstanceKey(id);
|
|
1200
2854
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
2855
|
+
instances.set(instanceKey, { id, sourceId, parentId, node });
|
|
2856
|
+
const slotsDescriptor = Object.getOwnPropertyDescriptor(props, 'slots');
|
|
2857
|
+
const slots = (slotsDescriptor &&
|
|
2858
|
+
Object.prototype.hasOwnProperty.call(slotsDescriptor, 'value'))
|
|
2859
|
+
? slotsDescriptor.value
|
|
2860
|
+
: undefined;
|
|
2861
|
+
const slotOwners = new Map();
|
|
2862
|
+
if (slots) {
|
|
2863
|
+
for (const [rawSlotName, rawSlot] of ownDataEntries(slots) ?? []) {
|
|
2864
|
+
if (typeof rawSlotName !== 'string' || !rawSlot || typeof rawSlot !== 'object') {
|
|
2865
|
+
continue;
|
|
2866
|
+
}
|
|
2867
|
+
const slotName = rawSlotName;
|
|
2868
|
+
const slot = rawSlot;
|
|
2869
|
+
const repeatDescriptor = Object.getOwnPropertyDescriptor(slot, 'repeat');
|
|
2870
|
+
let binding;
|
|
2871
|
+
if (repeatDescriptor &&
|
|
2872
|
+
Object.prototype.hasOwnProperty.call(repeatDescriptor, 'value')) {
|
|
2873
|
+
const native = repeatDescriptor.value;
|
|
2874
|
+
if (!native || typeof native !== 'object') {
|
|
2875
|
+
throw new Error(`[CardMaterializer] Invalid repeat on "${sourceId}" slot "${slotName}"`);
|
|
1209
2876
|
}
|
|
2877
|
+
binding = {
|
|
2878
|
+
dialect: 'native',
|
|
2879
|
+
source: native.source,
|
|
2880
|
+
template: native.template,
|
|
2881
|
+
item: native.item,
|
|
2882
|
+
index: native.index,
|
|
2883
|
+
};
|
|
1210
2884
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
2885
|
+
else {
|
|
2886
|
+
const a2ui = getA2UIChildBinding(slot);
|
|
2887
|
+
if (a2ui) {
|
|
2888
|
+
binding = {
|
|
2889
|
+
dialect: 'a2ui',
|
|
2890
|
+
source: a2ui.path,
|
|
2891
|
+
template: a2ui.templateId,
|
|
2892
|
+
};
|
|
1218
2893
|
}
|
|
1219
2894
|
}
|
|
2895
|
+
if (!binding)
|
|
2896
|
+
continue;
|
|
2897
|
+
hasRepeat = true;
|
|
2898
|
+
const depth = repeatDepth + 1;
|
|
2899
|
+
const sourcePath = binding.dialect === 'a2ui'
|
|
2900
|
+
? resolveA2UIPointer(binding.source, activeScope.dataPath)
|
|
2901
|
+
: resolveSimpleExpressionPath(binding.source, activeScope);
|
|
2902
|
+
const ownerKey = `o_${escapeRuntimePart(id)}_${escapeRuntimePart(slotName)}_${escapeRuntimePart(structuralPath)}`;
|
|
2903
|
+
const owner = {
|
|
2904
|
+
key: ownerKey,
|
|
2905
|
+
...(nearestOwnerKey ? { parentKey: nearestOwnerKey } : {}),
|
|
2906
|
+
runtimeOwnerId: id,
|
|
2907
|
+
sourceOwnerId: sourceId,
|
|
2908
|
+
slot: slotName,
|
|
2909
|
+
templateId: binding.template,
|
|
2910
|
+
...(sourcePath === undefined ? {} : { sourcePath }),
|
|
2911
|
+
dataPath: activeScope.dataPath,
|
|
2912
|
+
instancePath,
|
|
2913
|
+
depth,
|
|
2914
|
+
};
|
|
2915
|
+
if (repeatOwners.has(ownerKey)) {
|
|
2916
|
+
throw new Error(`[CardMaterializer] Repeat owner key collision at "${ownerKey}"`);
|
|
2917
|
+
}
|
|
2918
|
+
repeatOwners.set(ownerKey, owner);
|
|
2919
|
+
slotOwners.set(slotName, { owner, binding, sourcePath, depth });
|
|
2920
|
+
addDependency('component', binding.template, ownerKey);
|
|
2921
|
+
const runtimeKeys = runtimeOwnerKeys.get(id) ?? [];
|
|
2922
|
+
if (runtimeKeys.length > 0) {
|
|
2923
|
+
for (const key of runtimeKeys)
|
|
2924
|
+
unresolvedRepeatOwners.add(key);
|
|
2925
|
+
unresolvedRepeatOwners.add(ownerKey);
|
|
2926
|
+
}
|
|
2927
|
+
runtimeKeys.push(ownerKey);
|
|
2928
|
+
runtimeOwnerKeys.set(id, runtimeKeys);
|
|
2929
|
+
if (sourcePath === undefined) {
|
|
2930
|
+
unresolvedRepeatOwners.add(ownerKey);
|
|
2931
|
+
}
|
|
2932
|
+
else {
|
|
2933
|
+
addDependency('data', sourcePath, ownerKey);
|
|
2934
|
+
}
|
|
1220
2935
|
}
|
|
1221
2936
|
}
|
|
2937
|
+
const ownOwnerKeys = [...slotOwners.values()].map(entry => entry.owner.key);
|
|
2938
|
+
const effectiveOwnerKey = ownOwnerKeys[0] ?? nearestOwnerKey;
|
|
2939
|
+
if (ownOwnerKeys.length > 1) {
|
|
2940
|
+
for (const key of ownOwnerKeys)
|
|
2941
|
+
unresolvedRepeatOwners.add(key);
|
|
2942
|
+
}
|
|
2943
|
+
if (effectiveOwnerKey) {
|
|
2944
|
+
addDependency('component', sourceId, effectiveOwnerKey);
|
|
2945
|
+
const visited = new WeakSet();
|
|
2946
|
+
scanProps(element.props, activeScope, effectiveOwnerKey, visited);
|
|
2947
|
+
scanDependencyValue(element.directives?.visible, activeScope, effectiveOwnerKey, visited);
|
|
2948
|
+
scanDependencyValue(element.directives?.disabled, activeScope, effectiveOwnerKey, visited);
|
|
2949
|
+
}
|
|
2950
|
+
if (!slots)
|
|
2951
|
+
return node;
|
|
2952
|
+
for (const [rawSlotName, rawSlot] of ownDataEntries(slots) ?? []) {
|
|
2953
|
+
if (typeof rawSlotName !== 'string' || !rawSlot || typeof rawSlot !== 'object') {
|
|
2954
|
+
continue;
|
|
2955
|
+
}
|
|
2956
|
+
const slotName = rawSlotName;
|
|
2957
|
+
const slot = rawSlot;
|
|
2958
|
+
const references = collectSlotReferences(slot);
|
|
2959
|
+
for (const reference of references) {
|
|
2960
|
+
const childStructuralPath = appendOccurrence(structuralPath, 'child', sourceId, slotName, reference.position);
|
|
2961
|
+
const childPath = repeatedOccurrence ? childStructuralPath : '';
|
|
2962
|
+
const child = buildNode(reference.sourceId, activeScope, childPath, childStructuralPath, repeatedOccurrence, id, repeatDepth, effectiveOwnerKey, nextActiveDefinitions);
|
|
2963
|
+
reference.assign(child.id);
|
|
2964
|
+
node.children.push(child);
|
|
2965
|
+
}
|
|
2966
|
+
const slotOwner = slotOwners.get(slotName);
|
|
2967
|
+
if (!slotOwner)
|
|
2968
|
+
continue;
|
|
2969
|
+
const { owner, binding, sourcePath, depth } = slotOwner;
|
|
2970
|
+
const value = binding.dialect === 'a2ui'
|
|
2971
|
+
? resolveA2UIPath(rootVariables, binding.source, activeScope.dataPath)
|
|
2972
|
+
: resolveExpression(binding.source, createExpressionContext(activeScope));
|
|
2973
|
+
let items;
|
|
2974
|
+
if (value == null) {
|
|
2975
|
+
items = [];
|
|
2976
|
+
}
|
|
2977
|
+
else if (!Array.isArray(value)) {
|
|
2978
|
+
const diagnostic = {
|
|
2979
|
+
code: 'REPEAT_SOURCE_NOT_ARRAY',
|
|
2980
|
+
message: `Repeat source "${binding.source}" on "${sourceId}" did not resolve to an array`,
|
|
2981
|
+
ownerId: id,
|
|
2982
|
+
source: binding.source,
|
|
2983
|
+
};
|
|
2984
|
+
diagnostics.push(diagnostic);
|
|
2985
|
+
options.onDiagnostic?.(diagnostic);
|
|
2986
|
+
items = [];
|
|
2987
|
+
}
|
|
2988
|
+
else {
|
|
2989
|
+
items = value;
|
|
2990
|
+
}
|
|
2991
|
+
if (items.length > 0 && depth > maxDepth) {
|
|
2992
|
+
throw new Error(`[CardMaterializer] maxDepth ${maxDepth} exceeded at "${sourceId}" slot "${slotName}"`);
|
|
2993
|
+
}
|
|
2994
|
+
if (repeatedItemCount + items.length > maxItems) {
|
|
2995
|
+
throw new Error(`[CardMaterializer] maxItems ${maxItems} exceeded at "${sourceId}" slot "${slotName}"`);
|
|
2996
|
+
}
|
|
2997
|
+
repeatedItemCount += items.length;
|
|
2998
|
+
Reflect.deleteProperty(slot, 'repeat');
|
|
2999
|
+
Reflect.deleteProperty(slot, A2UI_CHILD_BINDING);
|
|
3000
|
+
slot.children = [];
|
|
3001
|
+
items.forEach((item, index) => {
|
|
3002
|
+
const dataPath = sourcePath === undefined
|
|
3003
|
+
? activeScope.dataPath
|
|
3004
|
+
: joinPointer(sourcePath, [String(index)]);
|
|
3005
|
+
const locals = binding.dialect === 'native' && binding.item
|
|
3006
|
+
? {
|
|
3007
|
+
[binding.item]: item,
|
|
3008
|
+
...(binding.index ? { [binding.index]: index } : {}),
|
|
3009
|
+
}
|
|
3010
|
+
: {};
|
|
3011
|
+
const childScope = {
|
|
3012
|
+
root: rootVariables,
|
|
3013
|
+
locals,
|
|
3014
|
+
parent: activeScope,
|
|
3015
|
+
dataPath,
|
|
3016
|
+
bindingDialect: binding.dialect,
|
|
3017
|
+
};
|
|
3018
|
+
const aliases = new Map();
|
|
3019
|
+
if (binding.dialect === 'native' && binding.item) {
|
|
3020
|
+
aliases.set(binding.item, sourcePath === undefined ? undefined : dataPath);
|
|
3021
|
+
if (binding.index)
|
|
3022
|
+
aliases.set(binding.index, sourcePath);
|
|
3023
|
+
}
|
|
3024
|
+
scopeAliasPaths.set(childScope, aliases);
|
|
3025
|
+
scopeIndexAliases.set(childScope, new Set(binding.dialect === 'native' && binding.index
|
|
3026
|
+
? [binding.index]
|
|
3027
|
+
: []));
|
|
3028
|
+
const itemPath = appendOccurrence(structuralPath, 'item', sourceId, slotName, `${binding.template}:${index}`);
|
|
3029
|
+
const child = buildNode(binding.template, childScope, itemPath, itemPath, true, id, depth, owner.key, nextActiveDefinitions);
|
|
3030
|
+
slot.children.push(child.id);
|
|
3031
|
+
node.children.push(child);
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
return node;
|
|
1222
3035
|
}
|
|
1223
|
-
|
|
3036
|
+
const root = buildNode(schema.rootID, rootScope, '', '', false, undefined, 0, undefined, new Set());
|
|
3037
|
+
return {
|
|
3038
|
+
root,
|
|
3039
|
+
hasRepeat,
|
|
3040
|
+
instances,
|
|
3041
|
+
repeatOwners,
|
|
3042
|
+
dependencies,
|
|
3043
|
+
valueDependencies,
|
|
3044
|
+
unresolvedRepeatOwners,
|
|
3045
|
+
diagnostics,
|
|
3046
|
+
};
|
|
1224
3047
|
}
|
|
1225
3048
|
|
|
1226
3049
|
/**
|
|
@@ -1361,12 +3184,29 @@ function isA2UIEnvelope(msg) {
|
|
|
1361
3184
|
*/
|
|
1362
3185
|
function a2uiComponentToElement(comp) {
|
|
1363
3186
|
const { id, component, children, slots, directives, events, lifecycle, ...props } = comp;
|
|
3187
|
+
if (children !== undefined && !isA2UIChildList(children)) {
|
|
3188
|
+
throw new Error(`[A2UI] Component "${id}" children must be a valid ChildList `
|
|
3189
|
+
+ '(string[] or exact { path, componentId } object)');
|
|
3190
|
+
}
|
|
1364
3191
|
const p = { ...props };
|
|
1365
|
-
if (children) {
|
|
3192
|
+
if (Array.isArray(children)) {
|
|
1366
3193
|
p.slots = { ...(p.slots ?? {}), default: { children } };
|
|
1367
3194
|
}
|
|
1368
3195
|
if (slots) {
|
|
1369
|
-
p.slots = {
|
|
3196
|
+
p.slots = {
|
|
3197
|
+
...(p.slots ?? {}),
|
|
3198
|
+
...(isA2UIDynamicChildList(children)
|
|
3199
|
+
? copyDynamicSlots(id, slots)
|
|
3200
|
+
: slots),
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
if (isA2UIDynamicChildList(children)) {
|
|
3204
|
+
p.slots = { ...(p.slots ?? {}) };
|
|
3205
|
+
const defaultSlot = Object.prototype.hasOwnProperty.call(p.slots, 'default')
|
|
3206
|
+
? cloneDynamicDefaultSlot(id, p.slots.default)
|
|
3207
|
+
: {};
|
|
3208
|
+
p.slots.default = defaultSlot;
|
|
3209
|
+
setA2UIChildBinding(defaultSlot, children);
|
|
1370
3210
|
}
|
|
1371
3211
|
const element = { id, type: component, props: p };
|
|
1372
3212
|
if (directives)
|
|
@@ -1375,8 +3215,121 @@ function a2uiComponentToElement(comp) {
|
|
|
1375
3215
|
element.events = events;
|
|
1376
3216
|
if (lifecycle)
|
|
1377
3217
|
element.lifecycle = lifecycle;
|
|
3218
|
+
if (isA2UIDynamicChildList(children)
|
|
3219
|
+
|| containsA2UIPathBinding$1(p)
|
|
3220
|
+
|| containsA2UIPathBinding$1(directives)
|
|
3221
|
+
|| containsA2UIPathBinding$1(events)
|
|
3222
|
+
|| containsA2UIPathBinding$1(lifecycle)) {
|
|
3223
|
+
markA2UIBindingDialect(element);
|
|
3224
|
+
}
|
|
1378
3225
|
return element;
|
|
1379
3226
|
}
|
|
3227
|
+
function containsA2UIPathBinding$1(value, seen = new WeakSet()) {
|
|
3228
|
+
if (isA2UIPathBinding(value))
|
|
3229
|
+
return true;
|
|
3230
|
+
if (typeof value !== 'object' || value === null)
|
|
3231
|
+
return false;
|
|
3232
|
+
if (seen.has(value))
|
|
3233
|
+
return false;
|
|
3234
|
+
seen.add(value);
|
|
3235
|
+
try {
|
|
3236
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
3237
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
3238
|
+
if (descriptor
|
|
3239
|
+
&& Object.prototype.hasOwnProperty.call(descriptor, 'value')
|
|
3240
|
+
&& containsA2UIPathBinding$1(descriptor.value, seen)) {
|
|
3241
|
+
return true;
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
catch {
|
|
3246
|
+
return false;
|
|
3247
|
+
}
|
|
3248
|
+
return false;
|
|
3249
|
+
}
|
|
3250
|
+
function isPlainRecord(value) {
|
|
3251
|
+
try {
|
|
3252
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
3253
|
+
return false;
|
|
3254
|
+
}
|
|
3255
|
+
const prototype = Object.getPrototypeOf(value);
|
|
3256
|
+
return prototype === Object.prototype || prototype === null;
|
|
3257
|
+
}
|
|
3258
|
+
catch {
|
|
3259
|
+
return false;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
function copyDynamicSlots(componentId, slots) {
|
|
3263
|
+
const output = {};
|
|
3264
|
+
let keys;
|
|
3265
|
+
try {
|
|
3266
|
+
keys = Reflect.ownKeys(slots);
|
|
3267
|
+
}
|
|
3268
|
+
catch {
|
|
3269
|
+
throw new Error(`[A2UI] Component "${componentId}" slots must be a readable record`);
|
|
3270
|
+
}
|
|
3271
|
+
for (const key of keys) {
|
|
3272
|
+
let descriptor;
|
|
3273
|
+
try {
|
|
3274
|
+
descriptor = Object.getOwnPropertyDescriptor(slots, key);
|
|
3275
|
+
}
|
|
3276
|
+
catch {
|
|
3277
|
+
throw new Error(`[A2UI] Component "${componentId}" slots must be a readable record`);
|
|
3278
|
+
}
|
|
3279
|
+
if (!descriptor || !descriptor.enumerable)
|
|
3280
|
+
continue;
|
|
3281
|
+
if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
3282
|
+
throw new Error(`[A2UI] Component "${componentId}" slots must use data properties`);
|
|
3283
|
+
}
|
|
3284
|
+
Object.defineProperty(output, key, {
|
|
3285
|
+
configurable: true,
|
|
3286
|
+
enumerable: true,
|
|
3287
|
+
value: descriptor.value,
|
|
3288
|
+
writable: true,
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
return output;
|
|
3292
|
+
}
|
|
3293
|
+
function cloneDynamicDefaultSlot(componentId, source) {
|
|
3294
|
+
if (!isPlainRecord(source)) {
|
|
3295
|
+
throw new Error(`[A2UI] Component "${componentId}" slots.default must be a plain record`);
|
|
3296
|
+
}
|
|
3297
|
+
let keys;
|
|
3298
|
+
try {
|
|
3299
|
+
keys = Reflect.ownKeys(source);
|
|
3300
|
+
}
|
|
3301
|
+
catch {
|
|
3302
|
+
throw new Error(`[A2UI] Component "${componentId}" slots.default must be a plain record`);
|
|
3303
|
+
}
|
|
3304
|
+
const conflicts = new Set(['children', 'groups', 'repeat']);
|
|
3305
|
+
const output = {};
|
|
3306
|
+
for (const key of keys) {
|
|
3307
|
+
if (conflicts.has(key)) {
|
|
3308
|
+
throw new Error(`[A2UI] Component "${componentId}" slots.default.${String(key)} `
|
|
3309
|
+
+ 'conflicts with dynamic children');
|
|
3310
|
+
}
|
|
3311
|
+
let descriptor;
|
|
3312
|
+
try {
|
|
3313
|
+
descriptor = Object.getOwnPropertyDescriptor(source, key);
|
|
3314
|
+
}
|
|
3315
|
+
catch {
|
|
3316
|
+
throw new Error(`[A2UI] Component "${componentId}" slots.default must be a plain record`);
|
|
3317
|
+
}
|
|
3318
|
+
if (!descriptor || !descriptor.enumerable)
|
|
3319
|
+
continue;
|
|
3320
|
+
if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
3321
|
+
throw new Error(`[A2UI] Component "${componentId}" slots.default must be a plain `
|
|
3322
|
+
+ 'record with data properties');
|
|
3323
|
+
}
|
|
3324
|
+
Object.defineProperty(output, key, {
|
|
3325
|
+
configurable: true,
|
|
3326
|
+
enumerable: true,
|
|
3327
|
+
value: descriptor.value,
|
|
3328
|
+
writable: true,
|
|
3329
|
+
});
|
|
3330
|
+
}
|
|
3331
|
+
return output;
|
|
3332
|
+
}
|
|
1380
3333
|
/**
|
|
1381
3334
|
* Convert an A2UI v0.9 envelope message into an internal StreamingCommand.
|
|
1382
3335
|
* Returns null for unrecognized messages.
|
|
@@ -1407,8 +3360,17 @@ function a2uiToCommand(msg) {
|
|
|
1407
3360
|
return { type: 'updateComponents', surfaceId, elements, ...(rootID ? { rootID } : {}) };
|
|
1408
3361
|
}
|
|
1409
3362
|
if (msg.updateDataModel) {
|
|
1410
|
-
const { surfaceId, path
|
|
1411
|
-
|
|
3363
|
+
const { surfaceId, path: envelopePath, value = null } = msg.updateDataModel;
|
|
3364
|
+
const path = envelopePath === undefined || envelopePath === '/'
|
|
3365
|
+
? ''
|
|
3366
|
+
: envelopePath;
|
|
3367
|
+
return {
|
|
3368
|
+
type: 'updateDataModel',
|
|
3369
|
+
surfaceId,
|
|
3370
|
+
path,
|
|
3371
|
+
pathDialect: 'a2ui',
|
|
3372
|
+
value,
|
|
3373
|
+
};
|
|
1412
3374
|
}
|
|
1413
3375
|
if (msg.appendContent) {
|
|
1414
3376
|
const { surfaceId, elementId, content } = msg.appendContent;
|
|
@@ -1553,6 +3515,109 @@ class StreamingParser {
|
|
|
1553
3515
|
* - Compute parent-child relationships for component changes
|
|
1554
3516
|
* - Emit typed events for the rendering layer
|
|
1555
3517
|
*/
|
|
3518
|
+
function cloneStreamingValue(value, seen = new WeakMap()) {
|
|
3519
|
+
if (value === null || typeof value !== 'object')
|
|
3520
|
+
return value;
|
|
3521
|
+
const cached = seen.get(value);
|
|
3522
|
+
if (cached !== undefined)
|
|
3523
|
+
return cached;
|
|
3524
|
+
const output = Array.isArray(value)
|
|
3525
|
+
? []
|
|
3526
|
+
: Object.create(Object.getPrototypeOf(value));
|
|
3527
|
+
seen.set(value, output);
|
|
3528
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
3529
|
+
if (Array.isArray(value) && key === 'length')
|
|
3530
|
+
continue;
|
|
3531
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
3532
|
+
if (!descriptor)
|
|
3533
|
+
continue;
|
|
3534
|
+
if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
3535
|
+
throw new TypeError(`Cannot clone streaming state accessor "${String(key)}"`);
|
|
3536
|
+
}
|
|
3537
|
+
Object.defineProperty(output, key, {
|
|
3538
|
+
value: cloneStreamingValue(descriptor.value, seen),
|
|
3539
|
+
enumerable: descriptor.enumerable,
|
|
3540
|
+
configurable: true,
|
|
3541
|
+
writable: true,
|
|
3542
|
+
});
|
|
3543
|
+
}
|
|
3544
|
+
return output;
|
|
3545
|
+
}
|
|
3546
|
+
function restoreOwnProperty(target, key) {
|
|
3547
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
3548
|
+
return descriptor
|
|
3549
|
+
? () => {
|
|
3550
|
+
Object.defineProperty(target, key, descriptor);
|
|
3551
|
+
}
|
|
3552
|
+
: () => {
|
|
3553
|
+
Reflect.deleteProperty(target, key);
|
|
3554
|
+
};
|
|
3555
|
+
}
|
|
3556
|
+
function arrayIndexWriteExtendsLength(target, key) {
|
|
3557
|
+
if (!Array.isArray(target))
|
|
3558
|
+
return false;
|
|
3559
|
+
const text = String(key);
|
|
3560
|
+
if (!/^(0|[1-9]\d*)$/.test(text))
|
|
3561
|
+
return false;
|
|
3562
|
+
const index = Number(text);
|
|
3563
|
+
return index <= 4294967294 && index >= target.length;
|
|
3564
|
+
}
|
|
3565
|
+
function writeProperty(target, key, value, journal) {
|
|
3566
|
+
if (!journal) {
|
|
3567
|
+
target[key] = value;
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3570
|
+
recordPropertyMutation(target, key, journal);
|
|
3571
|
+
target[key] = value;
|
|
3572
|
+
}
|
|
3573
|
+
function recordPropertyMutation(target, key, journal) {
|
|
3574
|
+
if (arrayIndexWriteExtendsLength(target, key)) {
|
|
3575
|
+
journal.push(restoreOwnProperty(target, 'length'));
|
|
3576
|
+
}
|
|
3577
|
+
journal.push(restoreOwnProperty(target, key));
|
|
3578
|
+
}
|
|
3579
|
+
function deleteProperty(target, key, journal) {
|
|
3580
|
+
if (!journal)
|
|
3581
|
+
return Reflect.deleteProperty(target, key);
|
|
3582
|
+
const undo = restoreOwnProperty(target, key);
|
|
3583
|
+
const deleted = Reflect.deleteProperty(target, key);
|
|
3584
|
+
if (deleted)
|
|
3585
|
+
journal.push(undo);
|
|
3586
|
+
return deleted;
|
|
3587
|
+
}
|
|
3588
|
+
function spliceOne(target, index, journal) {
|
|
3589
|
+
if (!journal) {
|
|
3590
|
+
target.splice(index, 1);
|
|
3591
|
+
return;
|
|
3592
|
+
}
|
|
3593
|
+
const removed = target[index];
|
|
3594
|
+
target.splice(index, 1);
|
|
3595
|
+
journal.push(() => {
|
|
3596
|
+
target.splice(index, 0, removed);
|
|
3597
|
+
});
|
|
3598
|
+
}
|
|
3599
|
+
function rollbackMutations(journal) {
|
|
3600
|
+
for (let index = journal.length - 1; index >= 0; index -= 1) {
|
|
3601
|
+
journal[index]();
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
function containsA2UIPathBinding(value, seen = new WeakSet()) {
|
|
3605
|
+
if (isA2UIPathBinding(value))
|
|
3606
|
+
return true;
|
|
3607
|
+
if (value === null || typeof value !== 'object' || seen.has(value)) {
|
|
3608
|
+
return false;
|
|
3609
|
+
}
|
|
3610
|
+
seen.add(value);
|
|
3611
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
3612
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
3613
|
+
if (descriptor
|
|
3614
|
+
&& Object.prototype.hasOwnProperty.call(descriptor, 'value')
|
|
3615
|
+
&& containsA2UIPathBinding(descriptor.value, seen)) {
|
|
3616
|
+
return true;
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
return false;
|
|
3620
|
+
}
|
|
1556
3621
|
// ─── Engine ──────────────────────────────────────────────────────
|
|
1557
3622
|
class StreamingEngine {
|
|
1558
3623
|
constructor(listeners) {
|
|
@@ -1628,25 +3693,90 @@ class StreamingEngine {
|
|
|
1628
3693
|
const schema = cmd.schema
|
|
1629
3694
|
? normalizeSchema(cmd.schema)
|
|
1630
3695
|
: { version: '1.0', rootID: 'root', elements: {}, variables: {} };
|
|
3696
|
+
const previousSchema = this.surfaces.get(cmd.surfaceId);
|
|
1631
3697
|
this.surfaces.set(cmd.surfaceId, schema);
|
|
1632
|
-
|
|
3698
|
+
try {
|
|
3699
|
+
this.listeners.onSurfaceCreated(cmd.surfaceId, cmd.schema ?? null);
|
|
3700
|
+
}
|
|
3701
|
+
catch (error) {
|
|
3702
|
+
if (previousSchema) {
|
|
3703
|
+
this.surfaces.set(cmd.surfaceId, previousSchema);
|
|
3704
|
+
}
|
|
3705
|
+
else {
|
|
3706
|
+
this.surfaces.delete(cmd.surfaceId);
|
|
3707
|
+
}
|
|
3708
|
+
throw error;
|
|
3709
|
+
}
|
|
1633
3710
|
}
|
|
1634
3711
|
handleUpdateComponents(cmd) {
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
3712
|
+
const previousSchema = this.surfaces.get(cmd.surfaceId);
|
|
3713
|
+
const baseSchema = previousSchema ?? {
|
|
3714
|
+
version: '1.0',
|
|
3715
|
+
rootID: cmd.rootID ?? 'root',
|
|
3716
|
+
elements: {},
|
|
3717
|
+
variables: {},
|
|
3718
|
+
};
|
|
3719
|
+
const previousRequiresBinding = requiresBindingMaterialization(baseSchema);
|
|
3720
|
+
const commandMayIntroduceBinding = this.commandMayIntroduceBinding(cmd);
|
|
3721
|
+
// Preserve the original mutation order, references, and event timing for
|
|
3722
|
+
// the overwhelmingly common static-to-static path.
|
|
3723
|
+
if (!previousRequiresBinding && !commandMayIntroduceBinding) {
|
|
3724
|
+
this.applyStaticComponentMutation(cmd, previousSchema, baseSchema);
|
|
3725
|
+
return;
|
|
3726
|
+
}
|
|
3727
|
+
const candidate = cloneStreamingValue(baseSchema);
|
|
3728
|
+
const candidateChanges = this.applyComponentMutation(candidate, cmd, true);
|
|
3729
|
+
const rootIDChanged = candidate.rootID !== baseSchema.rootID;
|
|
3730
|
+
const candidateRequiresBinding = requiresBindingMaterialization(candidate);
|
|
3731
|
+
// A command may briefly carry binding metadata and remove it again in the
|
|
3732
|
+
// same batch. The final candidate, not that intermediate form, decides
|
|
3733
|
+
// whether the legacy static path remains applicable.
|
|
3734
|
+
if (!previousRequiresBinding && !candidateRequiresBinding) {
|
|
3735
|
+
this.applyStaticComponentMutation(cmd, previousSchema, baseSchema);
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3738
|
+
if (candidateChanges.length === 0 && !rootIDChanged)
|
|
3739
|
+
return;
|
|
3740
|
+
this.surfaces.set(cmd.surfaceId, candidate);
|
|
3741
|
+
try {
|
|
3742
|
+
this.listeners.onComponentsUpdated(cmd.surfaceId, candidateChanges);
|
|
3743
|
+
}
|
|
3744
|
+
catch (error) {
|
|
3745
|
+
if (previousSchema) {
|
|
3746
|
+
this.surfaces.set(cmd.surfaceId, previousSchema);
|
|
3747
|
+
}
|
|
3748
|
+
else {
|
|
3749
|
+
this.surfaces.delete(cmd.surfaceId);
|
|
3750
|
+
}
|
|
3751
|
+
throw error;
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
applyStaticComponentMutation(cmd, previousSchema, schema) {
|
|
3755
|
+
const journal = [];
|
|
3756
|
+
if (!previousSchema)
|
|
1644
3757
|
this.surfaces.set(cmd.surfaceId, schema);
|
|
3758
|
+
try {
|
|
3759
|
+
const changes = this.applyComponentMutation(schema, cmd, false, journal);
|
|
3760
|
+
if (changes.length > 0) {
|
|
3761
|
+
this.listeners.onComponentsUpdated(cmd.surfaceId, changes);
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
catch (error) {
|
|
3765
|
+
rollbackMutations(journal);
|
|
3766
|
+
if (previousSchema) {
|
|
3767
|
+
this.surfaces.set(cmd.surfaceId, previousSchema);
|
|
3768
|
+
}
|
|
3769
|
+
else {
|
|
3770
|
+
this.surfaces.delete(cmd.surfaceId);
|
|
3771
|
+
}
|
|
3772
|
+
throw error;
|
|
1645
3773
|
}
|
|
3774
|
+
}
|
|
3775
|
+
applyComponentMutation(schema, cmd, cloneIncoming, journal) {
|
|
1646
3776
|
const changes = [];
|
|
1647
3777
|
// Update rootID if provided
|
|
1648
3778
|
if (cmd.rootID) {
|
|
1649
|
-
schema
|
|
3779
|
+
writeProperty(schema, 'rootID', cmd.rootID, journal);
|
|
1650
3780
|
}
|
|
1651
3781
|
// Process element additions/updates.
|
|
1652
3782
|
// Merge ALL elements first, THEN compute parent/index against the final
|
|
@@ -1654,12 +3784,15 @@ class StreamingEngine {
|
|
|
1654
3784
|
// processed before its parent's updated children array would resolve to
|
|
1655
3785
|
// no parent, and incremental renderers would silently drop it.
|
|
1656
3786
|
if (cmd.elements) {
|
|
3787
|
+
const incomingElements = cloneIncoming
|
|
3788
|
+
? cloneStreamingValue(cmd.elements)
|
|
3789
|
+
: cmd.elements;
|
|
1657
3790
|
const isNewMap = new Map();
|
|
1658
|
-
for (const [id, element] of Object.entries(
|
|
3791
|
+
for (const [id, element] of Object.entries(incomingElements)) {
|
|
1659
3792
|
isNewMap.set(id, !schema.elements[id]);
|
|
1660
|
-
schema.elements
|
|
3793
|
+
writeProperty(schema.elements, id, element, journal);
|
|
1661
3794
|
}
|
|
1662
|
-
for (const [id, element] of Object.entries(
|
|
3795
|
+
for (const [id, element] of Object.entries(incomingElements)) {
|
|
1663
3796
|
const parentId = this.findParentId(schema, id);
|
|
1664
3797
|
const index = parentId ? this.findChildIndex(schema, parentId, id) : undefined;
|
|
1665
3798
|
changes.push({
|
|
@@ -1680,28 +3813,94 @@ class StreamingEngine {
|
|
|
1680
3813
|
elementId: id,
|
|
1681
3814
|
parentId: this.findParentId(schema, id),
|
|
1682
3815
|
});
|
|
1683
|
-
|
|
3816
|
+
deleteProperty(schema.elements, id, journal);
|
|
1684
3817
|
// Also remove from any parent's children lists
|
|
1685
|
-
this.removeFromParentSlots(schema, id);
|
|
3818
|
+
this.removeFromParentSlots(schema, id, journal);
|
|
1686
3819
|
}
|
|
1687
3820
|
}
|
|
1688
3821
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
3822
|
+
return changes;
|
|
3823
|
+
}
|
|
3824
|
+
commandMayIntroduceBinding(cmd) {
|
|
3825
|
+
if (!cmd.elements || Object.keys(cmd.elements).length === 0)
|
|
3826
|
+
return false;
|
|
3827
|
+
return requiresBindingMaterialization({
|
|
3828
|
+
version: '1.0',
|
|
3829
|
+
rootID: cmd.rootID ?? 'root',
|
|
3830
|
+
elements: cmd.elements,
|
|
3831
|
+
variables: {},
|
|
3832
|
+
});
|
|
1692
3833
|
}
|
|
1693
3834
|
handleUpdateDataModel(cmd) {
|
|
1694
3835
|
const schema = this.surfaces.get(cmd.surfaceId);
|
|
1695
3836
|
if (!schema)
|
|
1696
3837
|
return;
|
|
1697
|
-
|
|
1698
|
-
|
|
3838
|
+
if (!requiresBindingMaterialization(schema)) {
|
|
3839
|
+
// Preserve legacy mutation and notification behavior on static cards.
|
|
3840
|
+
// A2UI commands remain strict even when the current surface has no
|
|
3841
|
+
// binding-bearing component.
|
|
3842
|
+
const journal = [];
|
|
3843
|
+
try {
|
|
3844
|
+
this.applyDataModelWrite(schema.variables, cmd, journal);
|
|
3845
|
+
this.emitDataModelUpdated(cmd);
|
|
3846
|
+
}
|
|
3847
|
+
catch (error) {
|
|
3848
|
+
rollbackMutations(journal);
|
|
3849
|
+
throw error;
|
|
3850
|
+
}
|
|
3851
|
+
return;
|
|
3852
|
+
}
|
|
3853
|
+
const candidate = cloneStreamingValue(schema);
|
|
3854
|
+
const candidateCommand = {
|
|
3855
|
+
...cmd,
|
|
3856
|
+
value: cloneStreamingValue(cmd.value),
|
|
3857
|
+
};
|
|
3858
|
+
if (!this.applyDataModelWrite(candidate.variables, candidateCommand))
|
|
3859
|
+
return;
|
|
3860
|
+
this.surfaces.set(cmd.surfaceId, candidate);
|
|
3861
|
+
try {
|
|
3862
|
+
this.emitDataModelUpdated(cmd);
|
|
3863
|
+
}
|
|
3864
|
+
catch (error) {
|
|
3865
|
+
this.surfaces.set(cmd.surfaceId, schema);
|
|
3866
|
+
throw error;
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
applyDataModelWrite(variables, cmd, journal) {
|
|
3870
|
+
if (cmd.pathDialect === 'a2ui') {
|
|
3871
|
+
if (journal) {
|
|
3872
|
+
setByJsonPointerWithMutationRecorder(variables, cmd.path, cmd.value, (target, key) => {
|
|
3873
|
+
recordPropertyMutation(target, key, journal);
|
|
3874
|
+
});
|
|
3875
|
+
}
|
|
3876
|
+
else {
|
|
3877
|
+
setByJsonPointer(variables, cmd.path, cmd.value);
|
|
3878
|
+
}
|
|
3879
|
+
return true;
|
|
3880
|
+
}
|
|
3881
|
+
return setByLegacyPath(variables, cmd.path, cmd.value, journal);
|
|
3882
|
+
}
|
|
3883
|
+
emitDataModelUpdated(cmd) {
|
|
3884
|
+
if (cmd.pathDialect !== undefined) {
|
|
3885
|
+
this.listeners.onDataModelUpdated(cmd.surfaceId, cmd.path, cmd.value, cmd.pathDialect);
|
|
3886
|
+
return;
|
|
3887
|
+
}
|
|
1699
3888
|
this.listeners.onDataModelUpdated(cmd.surfaceId, cmd.path, cmd.value);
|
|
1700
3889
|
}
|
|
1701
3890
|
handleAppendContent(cmd) {
|
|
1702
3891
|
const schema = this.surfaces.get(cmd.surfaceId);
|
|
1703
3892
|
if (!schema)
|
|
1704
3893
|
return;
|
|
3894
|
+
const isBoundSurface = requiresBindingMaterialization(schema);
|
|
3895
|
+
const isUnknownRuntimeId = (cmd.elementId.startsWith('r_')
|
|
3896
|
+
&& !Object.prototype.hasOwnProperty.call(schema.elements, cmd.elementId));
|
|
3897
|
+
if (isBoundSurface
|
|
3898
|
+
&& (isUnknownRuntimeId
|
|
3899
|
+
|| this.dynamicClosureSourceIds(schema).has(cmd.elementId))) {
|
|
3900
|
+
console.warn(`[StreamingEngine] Rejected appendContent target "${cmd.elementId}" `
|
|
3901
|
+
+ 'because dynamic instances must be updated through their data model');
|
|
3902
|
+
return;
|
|
3903
|
+
}
|
|
1705
3904
|
// Update internal schema state
|
|
1706
3905
|
const element = schema.elements[cmd.elementId];
|
|
1707
3906
|
if (element) {
|
|
@@ -1764,6 +3963,10 @@ class StreamingEngine {
|
|
|
1764
3963
|
return true;
|
|
1765
3964
|
if (slot.groups?.some(group => group.includes(childId)))
|
|
1766
3965
|
return true;
|
|
3966
|
+
if (slot.repeat?.template === childId)
|
|
3967
|
+
return true;
|
|
3968
|
+
if (getA2UIChildBinding(slot)?.templateId === childId)
|
|
3969
|
+
return true;
|
|
1767
3970
|
if (slot.config?.overlays) {
|
|
1768
3971
|
for (const overlay of slot.config.overlays) {
|
|
1769
3972
|
if (overlay.children?.includes(childId))
|
|
@@ -1776,7 +3979,7 @@ class StreamingEngine {
|
|
|
1776
3979
|
/**
|
|
1777
3980
|
* Remove a child ID from all parent element slot references.
|
|
1778
3981
|
*/
|
|
1779
|
-
removeFromParentSlots(schema, childId) {
|
|
3982
|
+
removeFromParentSlots(schema, childId, journal) {
|
|
1780
3983
|
for (const element of Object.values(schema.elements)) {
|
|
1781
3984
|
if (!element.props.slots)
|
|
1782
3985
|
continue;
|
|
@@ -1784,17 +3987,84 @@ class StreamingEngine {
|
|
|
1784
3987
|
if (slot.children) {
|
|
1785
3988
|
const idx = slot.children.indexOf(childId);
|
|
1786
3989
|
if (idx >= 0)
|
|
1787
|
-
slot.children
|
|
3990
|
+
spliceOne(slot.children, idx, journal);
|
|
1788
3991
|
}
|
|
1789
3992
|
if (slot.groups) {
|
|
1790
3993
|
for (const group of slot.groups) {
|
|
1791
3994
|
const idx = group.indexOf(childId);
|
|
1792
3995
|
if (idx >= 0)
|
|
1793
|
-
group
|
|
3996
|
+
spliceOne(group, idx, journal);
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
if (slot.repeat?.template === childId) {
|
|
4000
|
+
deleteProperty(slot, 'repeat', journal);
|
|
4001
|
+
}
|
|
4002
|
+
if (getA2UIChildBinding(slot)?.templateId === childId) {
|
|
4003
|
+
deleteProperty(slot, A2UI_CHILD_BINDING, journal);
|
|
4004
|
+
if (!this.elementStillUsesA2UIBinding(element)) {
|
|
4005
|
+
deleteProperty(element, A2UI_BINDING_DIALECT, journal);
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
elementStillUsesA2UIBinding(element) {
|
|
4012
|
+
if (Object.values(element.props.slots ?? {}).some(slot => getA2UIChildBinding(slot) !== undefined)) {
|
|
4013
|
+
return true;
|
|
4014
|
+
}
|
|
4015
|
+
return containsA2UIPathBinding(element.props)
|
|
4016
|
+
|| containsA2UIPathBinding(element.directives)
|
|
4017
|
+
|| containsA2UIPathBinding(element.events)
|
|
4018
|
+
|| containsA2UIPathBinding(element.lifecycle);
|
|
4019
|
+
}
|
|
4020
|
+
/**
|
|
4021
|
+
* Source definitions inside a dynamic template closure have no unique
|
|
4022
|
+
* runtime address. Token appends must therefore target their data model,
|
|
4023
|
+
* not the shared source definition.
|
|
4024
|
+
*/
|
|
4025
|
+
dynamicClosureSourceIds(schema) {
|
|
4026
|
+
const dynamicSources = new Set();
|
|
4027
|
+
const pending = [];
|
|
4028
|
+
for (const element of Object.values(schema.elements)) {
|
|
4029
|
+
for (const slot of Object.values(element.props.slots ?? {})) {
|
|
4030
|
+
if (typeof slot.repeat?.template === 'string') {
|
|
4031
|
+
pending.push(slot.repeat.template);
|
|
4032
|
+
}
|
|
4033
|
+
const a2ui = getA2UIChildBinding(slot);
|
|
4034
|
+
if (typeof a2ui?.templateId === 'string') {
|
|
4035
|
+
pending.push(a2ui.templateId);
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
while (pending.length > 0) {
|
|
4040
|
+
const sourceId = pending.pop();
|
|
4041
|
+
if (dynamicSources.has(sourceId))
|
|
4042
|
+
continue;
|
|
4043
|
+
dynamicSources.add(sourceId);
|
|
4044
|
+
const source = schema.elements[sourceId];
|
|
4045
|
+
if (!source)
|
|
4046
|
+
continue;
|
|
4047
|
+
for (const slot of Object.values(source.props.slots ?? {})) {
|
|
4048
|
+
pending.push(...(slot.children ?? []));
|
|
4049
|
+
for (const group of slot.groups ?? [])
|
|
4050
|
+
pending.push(...group);
|
|
4051
|
+
if (Array.isArray(slot.config?.overlays)) {
|
|
4052
|
+
for (const overlay of slot.config.overlays) {
|
|
4053
|
+
if (Array.isArray(overlay?.children)) {
|
|
4054
|
+
pending.push(...overlay.children);
|
|
4055
|
+
}
|
|
1794
4056
|
}
|
|
1795
4057
|
}
|
|
4058
|
+
if (typeof slot.repeat?.template === 'string') {
|
|
4059
|
+
pending.push(slot.repeat.template);
|
|
4060
|
+
}
|
|
4061
|
+
const a2ui = getA2UIChildBinding(slot);
|
|
4062
|
+
if (typeof a2ui?.templateId === 'string') {
|
|
4063
|
+
pending.push(a2ui.templateId);
|
|
4064
|
+
}
|
|
1796
4065
|
}
|
|
1797
4066
|
}
|
|
4067
|
+
return dynamicSources;
|
|
1798
4068
|
}
|
|
1799
4069
|
}
|
|
1800
4070
|
// ─── Utility Functions ────────────────────────────────────────────
|
|
@@ -1807,21 +4077,22 @@ class StreamingEngine {
|
|
|
1807
4077
|
*/
|
|
1808
4078
|
const UNSAFE_PATH_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
1809
4079
|
/** Merge only own, non-dangerous keys of `src` into `target`. */
|
|
1810
|
-
function safeMerge(target, src) {
|
|
4080
|
+
function safeMerge(target, src, journal) {
|
|
1811
4081
|
for (const key of Object.keys(src)) {
|
|
1812
4082
|
if (UNSAFE_PATH_KEYS.has(key))
|
|
1813
4083
|
continue;
|
|
1814
|
-
target
|
|
4084
|
+
writeProperty(target, key, src[key], journal);
|
|
1815
4085
|
}
|
|
1816
4086
|
}
|
|
1817
4087
|
/**
|
|
1818
4088
|
* Set a value at a JSON Pointer-like path in an object.
|
|
1819
4089
|
*
|
|
1820
4090
|
* Path format: '/key1/key2/key3' → obj.key1.key2.key3 = value
|
|
1821
|
-
*
|
|
4091
|
+
* Legacy root path '/' merges the value's own safe fields into the object.
|
|
1822
4092
|
*
|
|
1823
4093
|
* Prototype-polluting segments (`__proto__` / `constructor` / `prototype`) are
|
|
1824
|
-
* rejected — the whole write is dropped rather than silently
|
|
4094
|
+
* rejected — the whole write is dropped rather than silently retargeting it.
|
|
4095
|
+
* Other assignments preserve the legacy writer's direct-reference behavior.
|
|
1825
4096
|
*
|
|
1826
4097
|
* @example
|
|
1827
4098
|
* ```ts
|
|
@@ -1831,29 +4102,36 @@ function safeMerge(target, src) {
|
|
|
1831
4102
|
* ```
|
|
1832
4103
|
*/
|
|
1833
4104
|
function setByPath(obj, path, value) {
|
|
1834
|
-
|
|
4105
|
+
setByLegacyPath(obj, path, value);
|
|
4106
|
+
}
|
|
4107
|
+
function setByLegacyPath(obj, path, value, journal) {
|
|
4108
|
+
// Preserve the legacy dialect: `/` (and any other all-empty spelling)
|
|
4109
|
+
// means a root merge, paths may omit the leading slash, and empty segments
|
|
4110
|
+
// are ignored.
|
|
1835
4111
|
const parts = path.replace(/^\//, '').split('/').filter(Boolean);
|
|
1836
|
-
//
|
|
1837
|
-
|
|
4112
|
+
// Preserve the existing prototype-pollution guard without changing the
|
|
4113
|
+
// historical writer's reference, accessor, or sparse-array semantics.
|
|
4114
|
+
if (parts.some(part => UNSAFE_PATH_KEYS.has(part))) {
|
|
1838
4115
|
console.warn(`[StreamingEngine] Rejected unsafe data-model path "${path}"`);
|
|
1839
|
-
return;
|
|
4116
|
+
return false;
|
|
1840
4117
|
}
|
|
1841
4118
|
if (parts.length === 0) {
|
|
1842
4119
|
// Root-level update: merge value into obj (own, safe keys only)
|
|
1843
4120
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
1844
|
-
safeMerge(obj, value);
|
|
4121
|
+
safeMerge(obj, value, journal);
|
|
1845
4122
|
}
|
|
1846
|
-
return;
|
|
4123
|
+
return true;
|
|
1847
4124
|
}
|
|
1848
4125
|
let current = obj;
|
|
1849
|
-
for (let
|
|
1850
|
-
const key = parts[
|
|
4126
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
4127
|
+
const key = parts[index];
|
|
1851
4128
|
if (current[key] === undefined || current[key] === null) {
|
|
1852
|
-
current
|
|
4129
|
+
writeProperty(current, key, {}, journal);
|
|
1853
4130
|
}
|
|
1854
4131
|
current = current[key];
|
|
1855
4132
|
}
|
|
1856
|
-
current
|
|
4133
|
+
writeProperty(current, parts[parts.length - 1], value, journal);
|
|
4134
|
+
return true;
|
|
1857
4135
|
}
|
|
1858
4136
|
|
|
1859
4137
|
/**
|
|
@@ -2145,6 +4423,14 @@ const BUILTIN_ICONS = Object.freeze({
|
|
|
2145
4423
|
viewBox: "0 0 32 32",
|
|
2146
4424
|
body: "<path d=\"M17.5 16C17.5 16.2967 17.412 16.5867 17.2472 16.8334C17.0824 17.0801 16.8481 17.2723 16.574 17.3859C16.2999 17.4994 15.9983 17.5291 15.7074 17.4712C15.4164 17.4133 15.1491 17.2705 14.9393 17.0607C14.7296 16.8509 14.5867 16.5836 14.5288 16.2927C14.4709 16.0017 14.5006 15.7001 14.6142 15.426C14.7277 15.1519 14.92 14.9177 15.1666 14.7528C15.4133 14.588 15.7033 14.5 16 14.5C16.3978 14.5 16.7794 14.6581 17.0607 14.9394C17.342 15.2207 17.5 15.6022 17.5 16ZM10.5 14.5C10.2033 14.5 9.91332 14.588 9.66664 14.7528C9.41997 14.9177 9.22771 15.1519 9.11418 15.426C9.00065 15.7001 8.97094 16.0017 9.02882 16.2927C9.0867 16.5836 9.22956 16.8509 9.43934 17.0607C9.64912 17.2705 9.91639 17.4133 10.2074 17.4712C10.4983 17.5291 10.7999 17.4994 11.074 17.3859C11.3481 17.2723 11.5824 17.0801 11.7472 16.8334C11.912 16.5867 12 16.2967 12 16C12 15.6022 11.842 15.2207 11.5607 14.9394C11.2794 14.6581 10.8978 14.5 10.5 14.5ZM21.5 14.5C21.2033 14.5 20.9133 14.588 20.6666 14.7528C20.42 14.9177 20.2277 15.1519 20.1142 15.426C20.0007 15.7001 19.9709 16.0017 20.0288 16.2927C20.0867 16.5836 20.2296 16.8509 20.4393 17.0607C20.6491 17.2705 20.9164 17.4133 21.2074 17.4712C21.4983 17.5291 21.7999 17.4994 22.074 17.3859C22.3481 17.2723 22.5824 17.0801 22.7472 16.8334C22.912 16.5867 23 16.2967 23 16C23 15.6022 22.842 15.2207 22.5607 14.9394C22.2794 14.6581 21.8978 14.5 21.5 14.5ZM29 16C29.0005 18.2445 28.4199 20.4508 27.3147 22.4042C26.2095 24.3577 24.6174 25.9917 22.6934 27.1473C20.7693 28.3029 18.5788 28.9407 16.3352 28.9986C14.0915 29.0564 11.8711 28.5324 9.89 27.4775L5.63375 28.8963C5.28136 29.0138 4.9032 29.0309 4.54166 28.9455C4.18012 28.8602 3.84948 28.6759 3.58681 28.4132C3.32414 28.1506 3.13982 27.8199 3.0545 27.4584C2.96918 27.0968 2.98623 26.7187 3.10375 26.3663L4.5225 22.11C3.59519 20.3666 3.07725 18.4348 3.008 16.4613C2.93875 14.4877 3.32001 12.5244 4.12284 10.7202C4.92567 8.91604 6.12897 7.31847 7.6414 6.04878C9.15383 4.77909 10.9356 3.87063 12.8516 3.39238C14.7675 2.91413 16.7672 2.87865 18.699 3.28862C20.6307 3.6986 22.4436 4.54327 24.0001 5.7585C25.5566 6.97374 26.8158 8.52761 27.6822 10.3022C28.5485 12.0767 28.9992 14.0253 29 16ZM27 16C26.9995 14.3127 26.6109 12.6481 25.8641 11.135C25.1174 9.62186 24.0325 8.30083 22.6935 7.27408C21.3545 6.24733 19.7973 5.54238 18.1422 5.21377C16.4872 4.88517 14.7787 4.94171 13.149 5.37904C11.5194 5.81636 10.0121 6.62274 8.74394 7.73578C7.47577 8.84882 6.48065 10.2387 5.83558 11.7979C5.1905 13.357 4.91277 15.0437 5.02387 16.7274C5.13496 18.4111 5.63191 20.0466 6.47625 21.5075C6.54712 21.6302 6.59112 21.7665 6.60534 21.9074C6.61956 22.0484 6.60368 22.1907 6.55875 22.325L5 27L9.675 25.4413C9.77683 25.4066 9.88367 25.3888 9.99125 25.3888C10.1669 25.3891 10.3393 25.4357 10.4912 25.5238C12.1635 26.4913 14.0611 27.0013 15.9931 27.0026C17.925 27.0038 19.8232 26.4961 21.4967 25.5307C23.1702 24.5653 24.5599 23.1762 25.526 21.5031C26.492 19.83 27.0004 17.932 27 16Z\" fill=\"currentColor\" fill-rule=\"evenodd\"></path>",
|
|
2147
4425
|
}),
|
|
4426
|
+
"check": Object.freeze({
|
|
4427
|
+
viewBox: "0 0 32 32",
|
|
4428
|
+
body: "<path d=\"M27.7071 8.29289C28.0976 8.68342 28.0976 9.31658 27.7071 9.70711L13.7071 23.7071C13.3166 24.0976 12.6834 24.0976 12.2929 23.7071L5.29289 16.7071C4.90237 16.3166 4.90237 15.6834 5.29289 15.2929C5.68342 14.9024 6.31658 14.9024 6.70711 15.2929L13 21.5858L26.2929 8.29289C26.6834 7.90237 27.3166 7.90237 27.7071 8.29289Z\" fill=\"currentColor\" fill-rule=\"evenodd\"></path>",
|
|
4429
|
+
}),
|
|
4430
|
+
"check_bold": Object.freeze({
|
|
4431
|
+
viewBox: "0 0 32 32",
|
|
4432
|
+
body: "<path d=\"M26.66453 8L11.99933 22.6656L5.33333 15.99941\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"5.33333\" stroke-linecap=\"round\"></path>",
|
|
4433
|
+
}),
|
|
2148
4434
|
"check_circle": Object.freeze({
|
|
2149
4435
|
viewBox: "0 0 32 32",
|
|
2150
4436
|
body: "<path d=\"M21.7075 12.2925C21.8005 12.3854 21.8742 12.4957 21.9246 12.6171C21.9749 12.7385 22.0008 12.8686 22.0008 13C22.0008 13.1314 21.9749 13.2615 21.9246 13.3829C21.8742 13.5043 21.8005 13.6146 21.7075 13.7075L14.7075 20.7075C14.6146 20.8005 14.5043 20.8742 14.3829 20.9246C14.2615 20.9749 14.1314 21.0008 14 21.0008C13.8686 21.0008 13.7385 20.9749 13.6171 20.9246C13.4957 20.8742 13.3854 20.8005 13.2925 20.7075L10.2925 17.7075C10.1049 17.5199 9.99945 17.2654 9.99945 17C9.99945 16.7346 10.1049 16.4801 10.2925 16.2925C10.4801 16.1049 10.7346 15.9994 11 15.9994C11.2654 15.9994 11.5199 16.1049 11.7075 16.2925L14 18.5863L20.2925 12.2925C20.3854 12.1995 20.4957 12.1258 20.6171 12.0754C20.7385 12.0251 20.8686 11.9992 21 11.9992C21.1314 11.9992 21.2615 12.0251 21.3829 12.0754C21.5043 12.1258 21.6146 12.1995 21.7075 12.2925ZM29 16C29 18.5712 28.2376 21.0846 26.8091 23.2224C25.3807 25.3603 23.3503 27.0265 20.9749 28.0104C18.5995 28.9944 15.9856 29.2518 13.4638 28.7502C10.9421 28.2486 8.6257 27.0105 6.80762 25.1924C4.98953 23.3743 3.75141 21.0579 3.2498 18.5362C2.74819 16.0144 3.00563 13.4006 3.98957 11.0251C4.97351 8.64968 6.63975 6.61935 8.77759 5.1909C10.9154 3.76244 13.4288 3 16 3C19.4467 3.00364 22.7512 4.37445 25.1884 6.81163C27.6256 9.24882 28.9964 12.5533 29 16ZM27 16C27 13.8244 26.3549 11.6977 25.1462 9.88873C23.9375 8.07979 22.2195 6.66989 20.2095 5.83733C18.1995 5.00476 15.9878 4.78692 13.854 5.21136C11.7202 5.6358 9.76021 6.68345 8.22183 8.22183C6.68345 9.7602 5.63581 11.7202 5.21137 13.854C4.78693 15.9878 5.00477 18.1995 5.83733 20.2095C6.66989 22.2195 8.07979 23.9375 9.88873 25.1462C11.6977 26.3549 13.8244 27 16 27C18.9164 26.9967 21.7123 25.8367 23.7745 23.7745C25.8367 21.7123 26.9967 18.9164 27 16Z\" fill=\"currentColor\" fill-rule=\"evenodd\"></path>",
|
|
@@ -2463,4 +4749,4 @@ function getBuiltinIcon(name) {
|
|
|
2463
4749
|
return BUILTIN_ICONS[name];
|
|
2464
4750
|
}
|
|
2465
4751
|
|
|
2466
|
-
export { ActionRegistry, BUILTIN_ICONS, BUILTIN_ICON_NAMES, LifecycleManager, StreamingEngine, StreamingParser, a2uiComponentToElement, a2uiToCommand, convertLegacySchema, createLifecycleManager, extractPartialSchema, getBuiltinIcon, getByPath$1 as getByPath, hasExpression, interpolate, isA2UIEnvelope, isLegacySchema, normalizeSchema, parseSchema, registerActionHandler, registry, resetIdCounter, resolveActionRef, resolveDeep, resolveExpression, resolveExpressionValue, runActionStep, runActionSteps, setByPath, validateSchema };
|
|
4752
|
+
export { ActionRegistry, BUILTIN_ICONS, BUILTIN_ICON_NAMES, JsonPointerPathError, LifecycleManager, StreamingEngine, StreamingParser, a2uiComponentToElement, a2uiToCommand, bindingTopologyFingerprint, cloneJsonData, convertLegacySchema, createA2UIParameterResolver, createExpressionContext, createLifecycleManager, decodeJsonPointerSegment, extractPartialSchema, findAffectedRepeatOwners, findTemplateRepeatOwners, getBuiltinIcon, getByJsonPointer, getByPath$1 as getByPath, hasDynamicChildren, hasExpression, interpolate, isA2UIChildList, isA2UIDynamicChildList, isA2UIEnvelope, isA2UIPathBinding, isBoundRenderTreeNode, isLegacySchema, materializeCard, normalizeSchema, parseJsonPointer, parseSchema, registerActionHandler, registry, replaceRootContents, requiresBindingMaterialization, resetIdCounter, resolveA2UIDeep, resolveA2UIPath, resolveActionRef, resolveDeep, resolveExpression, resolveExpressionValue, runActionStep, runActionSteps, setByJsonPointer, setByPath, validateSchema };
|