@kumwe/studio-core 0.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +27 -0
- package/dist/canonical.d.ts +20 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +100 -0
- package/dist/canonical.js.map +1 -0
- package/dist/clone.d.ts +2 -0
- package/dist/clone.d.ts.map +1 -0
- package/dist/clone.js +4 -0
- package/dist/clone.js.map +1 -0
- package/dist/commands.d.ts +12 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +679 -0
- package/dist/commands.js.map +1 -0
- package/dist/contributions.d.ts +65 -0
- package/dist/contributions.d.ts.map +1 -0
- package/dist/contributions.js +302 -0
- package/dist/contributions.js.map +1 -0
- package/dist/entry-commands.d.ts +3 -0
- package/dist/entry-commands.d.ts.map +1 -0
- package/dist/entry-commands.js +36 -0
- package/dist/entry-commands.js.map +1 -0
- package/dist/history.d.ts +13 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +63 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations.d.ts +47 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +83 -0
- package/dist/migrations.js.map +1 -0
- package/dist/model-commands.d.ts +3 -0
- package/dist/model-commands.d.ts.map +1 -0
- package/dist/model-commands.js +21 -0
- package/dist/model-commands.js.map +1 -0
- package/dist/negotiation.d.ts +23 -0
- package/dist/negotiation.d.ts.map +1 -0
- package/dist/negotiation.js +65 -0
- package/dist/negotiation.js.map +1 -0
- package/dist/recipes.d.ts +15 -0
- package/dist/recipes.d.ts.map +1 -0
- package/dist/recipes.js +34 -0
- package/dist/recipes.js.map +1 -0
- package/dist/registry.d.ts +17 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +56 -0
- package/dist/registry.js.map +1 -0
- package/dist/schema-profile.d.ts +9 -0
- package/dist/schema-profile.d.ts.map +1 -0
- package/dist/schema-profile.js +387 -0
- package/dist/schema-profile.js.map +1 -0
- package/dist/semver.d.ts +20 -0
- package/dist/semver.d.ts.map +1 -0
- package/dist/semver.js +128 -0
- package/dist/semver.js.map +1 -0
- package/dist/session.d.ts +33 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +113 -0
- package/dist/session.js.map +1 -0
- package/dist/validation.d.ts +13 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +398 -0
- package/dist/validation.js.map +1 -0
- package/package.json +40 -0
package/dist/commands.js
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import { cloneContractValue } from './clone.js';
|
|
2
|
+
export class StudioCommandError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'StudioCommandError';
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function applyCommand(document, command) {
|
|
11
|
+
if (command.artifactId !== document.id) {
|
|
12
|
+
throw new StudioCommandError('node-not-found', `Command targets ${command.artifactId}, not Blueprint ${document.id}.`);
|
|
13
|
+
}
|
|
14
|
+
const next = cloneContractValue(document);
|
|
15
|
+
if (command.type === 'studio.command/batch') {
|
|
16
|
+
for (const operation of assertBatchOperations(command.payload.operations)) {
|
|
17
|
+
applyOperation(next, operation);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else if (command.type === 'studio.command/apply-pattern') {
|
|
21
|
+
applyPattern(next, command.payload);
|
|
22
|
+
}
|
|
23
|
+
else if (command.type === 'studio.command/reset-inherited-property') {
|
|
24
|
+
resetInheritedProperty(next, command.payload);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
applyOperation(next, command);
|
|
28
|
+
}
|
|
29
|
+
return next;
|
|
30
|
+
}
|
|
31
|
+
export function invertCommand(document, command, options) {
|
|
32
|
+
if (command.artifactId !== document.id) {
|
|
33
|
+
throw new StudioCommandError('node-not-found', `Command targets ${command.artifactId}, not Blueprint ${document.id}.`);
|
|
34
|
+
}
|
|
35
|
+
let inverse;
|
|
36
|
+
if (command.type === 'studio.command/batch') {
|
|
37
|
+
const working = cloneContractValue(document);
|
|
38
|
+
const operations = [];
|
|
39
|
+
for (const operation of assertBatchOperations(command.payload.operations)) {
|
|
40
|
+
operations.unshift(invertOperation(working, operation));
|
|
41
|
+
applyOperation(working, operation);
|
|
42
|
+
}
|
|
43
|
+
inverse = { operations };
|
|
44
|
+
}
|
|
45
|
+
else if (command.type === 'studio.command/apply-pattern') {
|
|
46
|
+
const removals = command.payload.nodes.map((node) => {
|
|
47
|
+
const mapped = ownMapValue(command.payload.idMap, node.id);
|
|
48
|
+
if (mapped === undefined) {
|
|
49
|
+
throw incompleteIdMap();
|
|
50
|
+
}
|
|
51
|
+
return { payload: { nodeId: mapped }, type: 'studio.command/remove-node' };
|
|
52
|
+
});
|
|
53
|
+
const [single] = removals;
|
|
54
|
+
inverse = removals.length === 1 && single !== undefined ? single : { operations: removals };
|
|
55
|
+
}
|
|
56
|
+
else if (command.type === 'studio.command/reset-inherited-property') {
|
|
57
|
+
const restorations = resetInheritedPropertyInverseOperations(document, command.payload);
|
|
58
|
+
const [single] = restorations;
|
|
59
|
+
inverse =
|
|
60
|
+
restorations.length === 1 && single !== undefined ? single : { operations: restorations };
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
inverse = invertOperation(document, command);
|
|
64
|
+
}
|
|
65
|
+
const envelope = {
|
|
66
|
+
artifactId: command.artifactId,
|
|
67
|
+
baseStateVersion: command.baseStateVersion + 1,
|
|
68
|
+
contractVersion: command.contractVersion,
|
|
69
|
+
id: options.id,
|
|
70
|
+
kind: 'command',
|
|
71
|
+
sessionGeneration: command.sessionGeneration,
|
|
72
|
+
};
|
|
73
|
+
const grouped = command.groupId === undefined ? {} : { groupId: command.groupId };
|
|
74
|
+
if ('operations' in inverse) {
|
|
75
|
+
return {
|
|
76
|
+
...envelope,
|
|
77
|
+
...grouped,
|
|
78
|
+
payload: inverse,
|
|
79
|
+
type: 'studio.command/batch',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
...envelope,
|
|
84
|
+
...grouped,
|
|
85
|
+
payload: inverse.payload,
|
|
86
|
+
type: inverse.type,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function assertBatchOperations(operations) {
|
|
90
|
+
if (operations.length === 0 || operations.length > 100) {
|
|
91
|
+
throw new StudioCommandError('invalid-batch', `A batch must contain between 1 and 100 operations, not ${operations.length}.`);
|
|
92
|
+
}
|
|
93
|
+
for (const operation of operations) {
|
|
94
|
+
const type = operation.type;
|
|
95
|
+
if (type === 'studio.command/batch' ||
|
|
96
|
+
type === 'studio.command/apply-pattern' ||
|
|
97
|
+
type === 'studio.command/reset-inherited-property') {
|
|
98
|
+
throw new StudioCommandError('invalid-batch', `A batch cannot contain a ${type.slice(type.indexOf('/') + 1)} operation.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return operations;
|
|
102
|
+
}
|
|
103
|
+
function applyOperation(document, operation) {
|
|
104
|
+
switch (operation.type) {
|
|
105
|
+
case 'studio.command/insert-node':
|
|
106
|
+
case 'studio.command/restore-node': {
|
|
107
|
+
assertSubtreeIdsAbsent(document, operation.payload.node);
|
|
108
|
+
const collection = resolveTargetCollection(document, operation.payload.destination);
|
|
109
|
+
insertAt(collection, operation.payload.destination.position, cloneContractValue(operation.payload.node));
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case 'studio.command/remove-node': {
|
|
113
|
+
const location = findNode(document.roots, operation.payload.nodeId);
|
|
114
|
+
if (location === undefined) {
|
|
115
|
+
throw nodeNotFound(operation.payload.nodeId);
|
|
116
|
+
}
|
|
117
|
+
location.collection.splice(location.index, 1);
|
|
118
|
+
dropEmptySlotCollection(document, location);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case 'studio.command/move-node': {
|
|
122
|
+
const source = findNode(document.roots, operation.payload.nodeId);
|
|
123
|
+
if (source === undefined) {
|
|
124
|
+
throw nodeNotFound(operation.payload.nodeId);
|
|
125
|
+
}
|
|
126
|
+
const parentNodeId = operation.payload.destination.parentNodeId;
|
|
127
|
+
if (parentNodeId === operation.payload.nodeId ||
|
|
128
|
+
(parentNodeId !== undefined && findNode([source.node], parentNodeId) !== undefined)) {
|
|
129
|
+
throw new StudioCommandError('illegal-move', 'A node cannot be moved into itself.');
|
|
130
|
+
}
|
|
131
|
+
const [moving] = source.collection.splice(source.index, 1);
|
|
132
|
+
if (moving === undefined) {
|
|
133
|
+
throw nodeNotFound(operation.payload.nodeId);
|
|
134
|
+
}
|
|
135
|
+
dropEmptySlotCollection(document, source);
|
|
136
|
+
const collection = resolveTargetCollection(document, operation.payload.destination);
|
|
137
|
+
insertAt(collection, operation.payload.destination.position, moving);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'studio.command/duplicate-node': {
|
|
141
|
+
const source = findNode(document.roots, operation.payload.nodeId);
|
|
142
|
+
if (source === undefined) {
|
|
143
|
+
throw nodeNotFound(operation.payload.nodeId);
|
|
144
|
+
}
|
|
145
|
+
const idMap = assertCompleteIdMap(document, source.node, operation.payload.idMap);
|
|
146
|
+
const copy = remapSubtree(cloneContractValue(source.node), idMap);
|
|
147
|
+
if (operation.payload.destination === undefined) {
|
|
148
|
+
insertAt(source.collection, source.index + 1, copy);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const collection = resolveTargetCollection(document, operation.payload.destination);
|
|
152
|
+
insertAt(collection, operation.payload.destination.position, copy);
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case 'studio.command/reorder-children': {
|
|
157
|
+
const collection = resolveExistingCollection(document, operation.payload.parentNodeId, operation.payload.slot);
|
|
158
|
+
const currentIds = collection.map((node) => node.id);
|
|
159
|
+
if (!isPermutation(currentIds, operation.payload.order)) {
|
|
160
|
+
throw new StudioCommandError('invalid-order', 'The requested order is not a permutation of the current children.');
|
|
161
|
+
}
|
|
162
|
+
const byId = new Map(collection.map((node) => [node.id, node]));
|
|
163
|
+
const reordered = operation.payload.order.map((nodeId) => {
|
|
164
|
+
const node = byId.get(nodeId);
|
|
165
|
+
if (node === undefined) {
|
|
166
|
+
throw new StudioCommandError('invalid-order', 'The requested order is not a permutation of the current children.');
|
|
167
|
+
}
|
|
168
|
+
return node;
|
|
169
|
+
});
|
|
170
|
+
collection.splice(0, collection.length, ...reordered);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case 'studio.command/set-property': {
|
|
174
|
+
const location = findNode(document.roots, operation.payload.nodeId);
|
|
175
|
+
if (location === undefined) {
|
|
176
|
+
throw nodeNotFound(operation.payload.nodeId);
|
|
177
|
+
}
|
|
178
|
+
if (operation.payload.viewport === undefined) {
|
|
179
|
+
setOwnMapValue(location.node.properties, operation.payload.property, cloneContractValue(operation.payload.value));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// Canonical documents use plain JSON objects; member-name safety is
|
|
183
|
+
// enforced by the schemas and by own-property map access.
|
|
184
|
+
const responsive = (location.node.responsive ??= {});
|
|
185
|
+
let values = ownMapValue(responsive, operation.payload.property);
|
|
186
|
+
if (values === undefined) {
|
|
187
|
+
values = {};
|
|
188
|
+
setOwnMapValue(responsive, operation.payload.property, values);
|
|
189
|
+
}
|
|
190
|
+
setOwnMapValue(values, operation.payload.viewport, cloneContractValue(operation.payload.value));
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case 'studio.command/unset-property': {
|
|
195
|
+
const location = findNode(document.roots, operation.payload.nodeId);
|
|
196
|
+
if (location === undefined) {
|
|
197
|
+
throw nodeNotFound(operation.payload.nodeId);
|
|
198
|
+
}
|
|
199
|
+
if (operation.payload.viewport === undefined) {
|
|
200
|
+
if (ownMapValue(location.node.properties, operation.payload.property) === undefined) {
|
|
201
|
+
throw propertyNotFound(operation.payload.nodeId, operation.payload.property);
|
|
202
|
+
}
|
|
203
|
+
deleteOwnMapValue(location.node.properties, operation.payload.property);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
const responsive = location.node.responsive;
|
|
207
|
+
const values = responsive === undefined
|
|
208
|
+
? undefined
|
|
209
|
+
: ownMapValue(responsive, operation.payload.property);
|
|
210
|
+
if (responsive === undefined ||
|
|
211
|
+
values === undefined ||
|
|
212
|
+
ownMapValue(values, operation.payload.viewport) === undefined) {
|
|
213
|
+
throw propertyNotFound(operation.payload.nodeId, operation.payload.property, operation.payload.viewport);
|
|
214
|
+
}
|
|
215
|
+
deleteOwnMapValue(values, operation.payload.viewport);
|
|
216
|
+
if (Object.keys(values).length === 0) {
|
|
217
|
+
deleteOwnMapValue(responsive, operation.payload.property);
|
|
218
|
+
}
|
|
219
|
+
if (Object.keys(responsive).length === 0) {
|
|
220
|
+
delete location.node.responsive;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case 'studio.command/set-binding': {
|
|
226
|
+
const location = findNode(document.roots, operation.payload.nodeId);
|
|
227
|
+
if (location === undefined) {
|
|
228
|
+
throw nodeNotFound(operation.payload.nodeId);
|
|
229
|
+
}
|
|
230
|
+
setOwnMapValue(location.node.bindings, operation.payload.port, cloneContractValue(operation.payload.binding));
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case 'studio.command/remove-binding': {
|
|
234
|
+
const location = findNode(document.roots, operation.payload.nodeId);
|
|
235
|
+
if (location === undefined) {
|
|
236
|
+
throw nodeNotFound(operation.payload.nodeId);
|
|
237
|
+
}
|
|
238
|
+
if (ownMapValue(location.node.bindings, operation.payload.port) === undefined) {
|
|
239
|
+
throw bindingNotFound(operation.payload.nodeId, operation.payload.port);
|
|
240
|
+
}
|
|
241
|
+
deleteOwnMapValue(location.node.bindings, operation.payload.port);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
default:
|
|
245
|
+
assertNever(operation);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function invertOperation(document, operation) {
|
|
249
|
+
switch (operation.type) {
|
|
250
|
+
case 'studio.command/insert-node':
|
|
251
|
+
case 'studio.command/restore-node': {
|
|
252
|
+
return {
|
|
253
|
+
payload: { nodeId: operation.payload.node.id },
|
|
254
|
+
type: 'studio.command/remove-node',
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
case 'studio.command/remove-node': {
|
|
258
|
+
const location = findNode(document.roots, operation.payload.nodeId);
|
|
259
|
+
if (location === undefined) {
|
|
260
|
+
throw nodeNotFound(operation.payload.nodeId);
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
payload: {
|
|
264
|
+
destination: destinationOf(location),
|
|
265
|
+
node: cloneContractValue(location.node),
|
|
266
|
+
},
|
|
267
|
+
type: 'studio.command/restore-node',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
case 'studio.command/move-node': {
|
|
271
|
+
const location = findNode(document.roots, operation.payload.nodeId);
|
|
272
|
+
if (location === undefined) {
|
|
273
|
+
throw nodeNotFound(operation.payload.nodeId);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
payload: {
|
|
277
|
+
destination: destinationOf(location),
|
|
278
|
+
nodeId: operation.payload.nodeId,
|
|
279
|
+
},
|
|
280
|
+
type: 'studio.command/move-node',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
case 'studio.command/duplicate-node': {
|
|
284
|
+
const duplicateRootId = ownMapValue(operation.payload.idMap, operation.payload.nodeId);
|
|
285
|
+
if (duplicateRootId === undefined) {
|
|
286
|
+
throw new StudioCommandError('invalid-id-map', `The identifier map does not remap the duplicated root ${operation.payload.nodeId}.`);
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
payload: { nodeId: duplicateRootId },
|
|
290
|
+
type: 'studio.command/remove-node',
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
case 'studio.command/reorder-children': {
|
|
294
|
+
const collection = resolveExistingCollection(document, operation.payload.parentNodeId, operation.payload.slot);
|
|
295
|
+
const payload = { order: collection.map((node) => node.id) };
|
|
296
|
+
if (operation.payload.parentNodeId !== undefined && operation.payload.slot !== undefined) {
|
|
297
|
+
payload.parentNodeId = operation.payload.parentNodeId;
|
|
298
|
+
payload.slot = operation.payload.slot;
|
|
299
|
+
}
|
|
300
|
+
return { payload, type: 'studio.command/reorder-children' };
|
|
301
|
+
}
|
|
302
|
+
case 'studio.command/set-property': {
|
|
303
|
+
const location = findNode(document.roots, operation.payload.nodeId);
|
|
304
|
+
if (location === undefined) {
|
|
305
|
+
throw nodeNotFound(operation.payload.nodeId);
|
|
306
|
+
}
|
|
307
|
+
const previous = previousPropertyValue(location.node, operation.payload.property, operation.payload.viewport);
|
|
308
|
+
if (previous === undefined) {
|
|
309
|
+
const payload = {
|
|
310
|
+
nodeId: operation.payload.nodeId,
|
|
311
|
+
property: operation.payload.property,
|
|
312
|
+
};
|
|
313
|
+
if (operation.payload.viewport !== undefined) {
|
|
314
|
+
payload.viewport = operation.payload.viewport;
|
|
315
|
+
}
|
|
316
|
+
return { payload, type: 'studio.command/unset-property' };
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
payload: restorePropertyPayload(operation.payload, previous),
|
|
320
|
+
type: 'studio.command/set-property',
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
case 'studio.command/unset-property': {
|
|
324
|
+
const location = findNode(document.roots, operation.payload.nodeId);
|
|
325
|
+
if (location === undefined) {
|
|
326
|
+
throw nodeNotFound(operation.payload.nodeId);
|
|
327
|
+
}
|
|
328
|
+
const previous = previousPropertyValue(location.node, operation.payload.property, operation.payload.viewport);
|
|
329
|
+
if (previous === undefined) {
|
|
330
|
+
throw propertyNotFound(operation.payload.nodeId, operation.payload.property, operation.payload.viewport);
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
payload: restorePropertyPayload(operation.payload, previous),
|
|
334
|
+
type: 'studio.command/set-property',
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
case 'studio.command/set-binding': {
|
|
338
|
+
const location = findNode(document.roots, operation.payload.nodeId);
|
|
339
|
+
if (location === undefined) {
|
|
340
|
+
throw nodeNotFound(operation.payload.nodeId);
|
|
341
|
+
}
|
|
342
|
+
const previous = ownMapValue(location.node.bindings, operation.payload.port);
|
|
343
|
+
if (previous === undefined) {
|
|
344
|
+
return {
|
|
345
|
+
payload: { nodeId: operation.payload.nodeId, port: operation.payload.port },
|
|
346
|
+
type: 'studio.command/remove-binding',
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
payload: {
|
|
351
|
+
binding: cloneContractValue(previous),
|
|
352
|
+
nodeId: operation.payload.nodeId,
|
|
353
|
+
port: operation.payload.port,
|
|
354
|
+
},
|
|
355
|
+
type: 'studio.command/set-binding',
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
case 'studio.command/remove-binding': {
|
|
359
|
+
const location = findNode(document.roots, operation.payload.nodeId);
|
|
360
|
+
if (location === undefined) {
|
|
361
|
+
throw nodeNotFound(operation.payload.nodeId);
|
|
362
|
+
}
|
|
363
|
+
const previous = ownMapValue(location.node.bindings, operation.payload.port);
|
|
364
|
+
if (previous === undefined) {
|
|
365
|
+
throw bindingNotFound(operation.payload.nodeId, operation.payload.port);
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
payload: {
|
|
369
|
+
binding: cloneContractValue(previous),
|
|
370
|
+
nodeId: operation.payload.nodeId,
|
|
371
|
+
port: operation.payload.port,
|
|
372
|
+
},
|
|
373
|
+
type: 'studio.command/set-binding',
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
default:
|
|
377
|
+
return assertNever(operation);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function restorePropertyPayload(payload, value) {
|
|
381
|
+
const restored = {
|
|
382
|
+
nodeId: payload.nodeId,
|
|
383
|
+
property: payload.property,
|
|
384
|
+
value: cloneContractValue(value),
|
|
385
|
+
};
|
|
386
|
+
if (payload.viewport !== undefined) {
|
|
387
|
+
restored.viewport = payload.viewport;
|
|
388
|
+
}
|
|
389
|
+
return restored;
|
|
390
|
+
}
|
|
391
|
+
function previousPropertyValue(node, property, viewport) {
|
|
392
|
+
if (viewport === undefined) {
|
|
393
|
+
return ownMapValue(node.properties, property);
|
|
394
|
+
}
|
|
395
|
+
const values = node.responsive === undefined ? undefined : ownMapValue(node.responsive, property);
|
|
396
|
+
return values === undefined ? undefined : ownMapValue(values, viewport);
|
|
397
|
+
}
|
|
398
|
+
function destinationOf(location) {
|
|
399
|
+
if (location.parentNodeId !== undefined && location.slot !== undefined) {
|
|
400
|
+
return {
|
|
401
|
+
parentNodeId: location.parentNodeId,
|
|
402
|
+
position: location.index,
|
|
403
|
+
slot: location.slot,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return { position: location.index };
|
|
407
|
+
}
|
|
408
|
+
function assertCompleteIdMap(document, source, idMap) {
|
|
409
|
+
const subtreeIds = collectSubtreeIds(source);
|
|
410
|
+
const provided = new Map();
|
|
411
|
+
for (const [from, to] of Object.entries(idMap)) {
|
|
412
|
+
provided.set(from, to);
|
|
413
|
+
}
|
|
414
|
+
if (provided.size !== subtreeIds.size) {
|
|
415
|
+
throw incompleteIdMap();
|
|
416
|
+
}
|
|
417
|
+
const assigned = new Set();
|
|
418
|
+
for (const from of subtreeIds) {
|
|
419
|
+
const to = provided.get(from);
|
|
420
|
+
if (to === undefined) {
|
|
421
|
+
throw incompleteIdMap();
|
|
422
|
+
}
|
|
423
|
+
if (assigned.has(to)) {
|
|
424
|
+
throw new StudioCommandError('invalid-id-map', `The identifier map assigns ${to} more than once.`);
|
|
425
|
+
}
|
|
426
|
+
assigned.add(to);
|
|
427
|
+
if (findNode(document.roots, to) !== undefined) {
|
|
428
|
+
throw new StudioCommandError('duplicate-node', `Node identifier ${to} is already present.`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return provided;
|
|
432
|
+
}
|
|
433
|
+
function incompleteIdMap() {
|
|
434
|
+
return new StudioCommandError('invalid-id-map', 'The identifier map must remap every node of the duplicated subtree exactly once.');
|
|
435
|
+
}
|
|
436
|
+
function collectSubtreeIds(node) {
|
|
437
|
+
const identifiers = new Set();
|
|
438
|
+
const stack = [node];
|
|
439
|
+
while (stack.length > 0) {
|
|
440
|
+
const current = stack.pop();
|
|
441
|
+
if (current === undefined) {
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
identifiers.add(current.id);
|
|
445
|
+
for (const children of Object.values(current.slots)) {
|
|
446
|
+
stack.push(...children);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return identifiers;
|
|
450
|
+
}
|
|
451
|
+
function assertSubtreeIdsAbsent(document, node) {
|
|
452
|
+
for (const nodeId of collectSubtreeIds(node)) {
|
|
453
|
+
if (findNode(document.roots, nodeId) !== undefined) {
|
|
454
|
+
throw new StudioCommandError('duplicate-node', `Node identifier ${nodeId} is already present.`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function remapSubtree(node, idMap) {
|
|
459
|
+
const stack = [node];
|
|
460
|
+
while (stack.length > 0) {
|
|
461
|
+
const current = stack.pop();
|
|
462
|
+
if (current === undefined) {
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
const mapped = idMap.get(current.id);
|
|
466
|
+
if (mapped === undefined) {
|
|
467
|
+
throw incompleteIdMap();
|
|
468
|
+
}
|
|
469
|
+
current.id = mapped;
|
|
470
|
+
for (const children of Object.values(current.slots)) {
|
|
471
|
+
stack.push(...children);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return node;
|
|
475
|
+
}
|
|
476
|
+
function applyPattern(document, payload) {
|
|
477
|
+
const subtreeIds = new Set();
|
|
478
|
+
for (const node of payload.nodes) {
|
|
479
|
+
for (const nodeId of collectSubtreeIds(node)) {
|
|
480
|
+
subtreeIds.add(nodeId);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const provided = new Map();
|
|
484
|
+
for (const [from, to] of Object.entries(payload.idMap)) {
|
|
485
|
+
provided.set(from, to);
|
|
486
|
+
}
|
|
487
|
+
if (provided.size !== subtreeIds.size) {
|
|
488
|
+
throw incompleteIdMap();
|
|
489
|
+
}
|
|
490
|
+
const assigned = new Set();
|
|
491
|
+
for (const from of subtreeIds) {
|
|
492
|
+
const to = provided.get(from);
|
|
493
|
+
if (to === undefined) {
|
|
494
|
+
throw incompleteIdMap();
|
|
495
|
+
}
|
|
496
|
+
if (assigned.has(to)) {
|
|
497
|
+
throw new StudioCommandError('invalid-id-map', `The identifier map assigns ${to} more than once.`);
|
|
498
|
+
}
|
|
499
|
+
assigned.add(to);
|
|
500
|
+
if (findNode(document.roots, to) !== undefined) {
|
|
501
|
+
throw new StudioCommandError('duplicate-node', `Node identifier ${to} is already present.`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const collection = resolveTargetCollection(document, payload.destination);
|
|
505
|
+
for (const [index, node] of payload.nodes.entries()) {
|
|
506
|
+
const copy = remapSubtree(cloneContractValue(node), provided);
|
|
507
|
+
const extensions = (copy.extensions ??= {});
|
|
508
|
+
setOwnMapValue(extensions, 'studio.pattern/source', {
|
|
509
|
+
id: payload.pattern.id,
|
|
510
|
+
revision: payload.pattern.revision,
|
|
511
|
+
version: payload.pattern.version,
|
|
512
|
+
});
|
|
513
|
+
insertAt(collection, payload.destination.position + index, copy);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function resolveInheritedPropertyOverrides(document, payload) {
|
|
517
|
+
const location = findNode(document.roots, payload.nodeId);
|
|
518
|
+
if (location === undefined) {
|
|
519
|
+
throw nodeNotFound(payload.nodeId);
|
|
520
|
+
}
|
|
521
|
+
const responsive = location.node.responsive;
|
|
522
|
+
const values = responsive === undefined ? undefined : ownMapValue(responsive, payload.property);
|
|
523
|
+
if (responsive === undefined || values === undefined || Object.keys(values).length === 0) {
|
|
524
|
+
throw new StudioCommandError('property-not-found', `Property ${payload.property} has no responsive overrides on node ${payload.nodeId}.`);
|
|
525
|
+
}
|
|
526
|
+
return { node: location.node, responsive, values };
|
|
527
|
+
}
|
|
528
|
+
function resetInheritedProperty(document, payload) {
|
|
529
|
+
const { node, responsive } = resolveInheritedPropertyOverrides(document, payload);
|
|
530
|
+
deleteOwnMapValue(responsive, payload.property);
|
|
531
|
+
if (Object.keys(responsive).length === 0) {
|
|
532
|
+
delete node.responsive;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function resetInheritedPropertyInverseOperations(document, payload) {
|
|
536
|
+
const { values } = resolveInheritedPropertyOverrides(document, payload);
|
|
537
|
+
return Object.entries(values)
|
|
538
|
+
.sort(([left], [right]) => (left < right ? -1 : 1))
|
|
539
|
+
.map(([viewport, value]) => ({
|
|
540
|
+
payload: {
|
|
541
|
+
nodeId: payload.nodeId,
|
|
542
|
+
property: payload.property,
|
|
543
|
+
value: cloneContractValue(value),
|
|
544
|
+
viewport,
|
|
545
|
+
},
|
|
546
|
+
type: 'studio.command/set-property',
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
function dropEmptySlotCollection(document, location) {
|
|
550
|
+
if (location.collection.length > 0 ||
|
|
551
|
+
location.parentNodeId === undefined ||
|
|
552
|
+
location.slot === undefined) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const parent = findNode(document.roots, location.parentNodeId)?.node;
|
|
556
|
+
if (parent !== undefined && ownMapValue(parent.slots, location.slot) === location.collection) {
|
|
557
|
+
deleteOwnMapValue(parent.slots, location.slot);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function isPermutation(current, requested) {
|
|
561
|
+
if (current.length !== requested.length) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
const remaining = new Map();
|
|
565
|
+
for (const nodeId of current) {
|
|
566
|
+
remaining.set(nodeId, (remaining.get(nodeId) ?? 0) + 1);
|
|
567
|
+
}
|
|
568
|
+
for (const nodeId of requested) {
|
|
569
|
+
const count = remaining.get(nodeId);
|
|
570
|
+
if (count === undefined || count === 0) {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
remaining.set(nodeId, count - 1);
|
|
574
|
+
}
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
function resolveExistingCollection(document, parentNodeId, slot) {
|
|
578
|
+
if (parentNodeId === undefined) {
|
|
579
|
+
return document.roots;
|
|
580
|
+
}
|
|
581
|
+
if (slot === undefined) {
|
|
582
|
+
throw new StudioCommandError('parent-not-found', 'A parent destination requires a named slot.');
|
|
583
|
+
}
|
|
584
|
+
const parent = findNode(document.roots, parentNodeId)?.node;
|
|
585
|
+
if (parent === undefined) {
|
|
586
|
+
throw new StudioCommandError('parent-not-found', `Parent node ${parentNodeId} was not found.`);
|
|
587
|
+
}
|
|
588
|
+
const collection = ownMapValue(parent.slots, slot);
|
|
589
|
+
if (collection === undefined) {
|
|
590
|
+
throw new StudioCommandError('invalid-order', `Slot ${slot} on node ${parentNodeId} has no children to reorder.`);
|
|
591
|
+
}
|
|
592
|
+
return collection;
|
|
593
|
+
}
|
|
594
|
+
function assertNever(value) {
|
|
595
|
+
const commandType = safeCommandType(value);
|
|
596
|
+
throw new StudioCommandError('unsupported-command', `Unsupported Blueprint command type: ${commandType}.`);
|
|
597
|
+
}
|
|
598
|
+
function safeCommandType(value) {
|
|
599
|
+
if (typeof value === 'object' &&
|
|
600
|
+
value !== null &&
|
|
601
|
+
'type' in value &&
|
|
602
|
+
typeof value.type === 'string' &&
|
|
603
|
+
value.type.length <= 160 &&
|
|
604
|
+
/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*\/[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u.test(value.type)) {
|
|
605
|
+
return value.type;
|
|
606
|
+
}
|
|
607
|
+
return 'unknown';
|
|
608
|
+
}
|
|
609
|
+
function resolveTargetCollection(document, destination) {
|
|
610
|
+
if (destination.parentNodeId === undefined) {
|
|
611
|
+
return document.roots;
|
|
612
|
+
}
|
|
613
|
+
if (destination.slot === undefined) {
|
|
614
|
+
throw new StudioCommandError('parent-not-found', 'A parent destination requires a named slot.');
|
|
615
|
+
}
|
|
616
|
+
const parent = findNode(document.roots, destination.parentNodeId)?.node;
|
|
617
|
+
if (parent === undefined) {
|
|
618
|
+
throw new StudioCommandError('parent-not-found', `Parent node ${destination.parentNodeId} was not found.`);
|
|
619
|
+
}
|
|
620
|
+
let collection = ownMapValue(parent.slots, destination.slot);
|
|
621
|
+
if (collection === undefined) {
|
|
622
|
+
collection = [];
|
|
623
|
+
setOwnMapValue(parent.slots, destination.slot, collection);
|
|
624
|
+
}
|
|
625
|
+
return collection;
|
|
626
|
+
}
|
|
627
|
+
function ownMapValue(map, key) {
|
|
628
|
+
return Object.hasOwn(map, key) ? map[key] : undefined;
|
|
629
|
+
}
|
|
630
|
+
function setOwnMapValue(map, key, value) {
|
|
631
|
+
Object.defineProperty(map, key, {
|
|
632
|
+
configurable: true,
|
|
633
|
+
enumerable: true,
|
|
634
|
+
value,
|
|
635
|
+
writable: true,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
function deleteOwnMapValue(map, key) {
|
|
639
|
+
if (Object.hasOwn(map, key)) {
|
|
640
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
641
|
+
delete map[key];
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function insertAt(collection, index, node) {
|
|
645
|
+
if (!Number.isInteger(index) || index < 0 || index > collection.length) {
|
|
646
|
+
throw new StudioCommandError('invalid-index', `Insertion index ${index} is outside the slot.`);
|
|
647
|
+
}
|
|
648
|
+
collection.splice(index, 0, node);
|
|
649
|
+
}
|
|
650
|
+
function findNode(nodes, nodeId, parentNodeId, slot) {
|
|
651
|
+
for (const [index, node] of nodes.entries()) {
|
|
652
|
+
if (node.id === nodeId) {
|
|
653
|
+
const location = { collection: nodes, index, node };
|
|
654
|
+
if (parentNodeId !== undefined && slot !== undefined) {
|
|
655
|
+
location.parentNodeId = parentNodeId;
|
|
656
|
+
location.slot = slot;
|
|
657
|
+
}
|
|
658
|
+
return location;
|
|
659
|
+
}
|
|
660
|
+
for (const [slotName, children] of Object.entries(node.slots)) {
|
|
661
|
+
const nested = findNode(children, nodeId, node.id, slotName);
|
|
662
|
+
if (nested !== undefined) {
|
|
663
|
+
return nested;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return undefined;
|
|
668
|
+
}
|
|
669
|
+
function nodeNotFound(nodeId) {
|
|
670
|
+
return new StudioCommandError('node-not-found', `Node ${nodeId} was not found.`);
|
|
671
|
+
}
|
|
672
|
+
function propertyNotFound(nodeId, property, viewport) {
|
|
673
|
+
const target = viewport === undefined ? property : `${property} for viewport ${viewport}`;
|
|
674
|
+
return new StudioCommandError('property-not-found', `Property ${target} is not set on node ${nodeId}.`);
|
|
675
|
+
}
|
|
676
|
+
function bindingNotFound(nodeId, port) {
|
|
677
|
+
return new StudioCommandError('binding-not-found', `Binding ${port} is not present on node ${nodeId}.`);
|
|
678
|
+
}
|
|
679
|
+
//# sourceMappingURL=commands.js.map
|