@stocksharp/diagram 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/canvas-renderer.js +79 -28
- package/dist/esm/canvas-renderer.js.map +1 -1
- package/dist/esm/core/action-registry.js +24 -0
- package/dist/esm/core/action-registry.js.map +1 -1
- package/dist/esm/core/document.js +134 -28
- package/dist/esm/core/document.js.map +1 -1
- package/dist/esm/core/json.js +17 -0
- package/dist/esm/core/json.js.map +1 -0
- package/dist/esm/core/model.js +8 -0
- package/dist/esm/core/model.js.map +1 -1
- package/dist/esm/core/state.js +16 -2
- package/dist/esm/core/state.js.map +1 -1
- package/dist/esm/diagram/catalog.js +17 -5
- package/dist/esm/diagram/catalog.js.map +1 -1
- package/dist/esm/diagram/event-emitter.js +10 -0
- package/dist/esm/diagram/event-emitter.js.map +1 -1
- package/dist/esm/diagram/stocksharp-diagram.js +58 -56
- package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
- package/dist/esm/diagram/types.js +2 -1
- package/dist/esm/diagram/types.js.map +1 -1
- package/dist/esm/embed.js +121 -38
- package/dist/esm/embed.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/ssdiagram.js +380 -150
- package/dist/ssdiagram.js.map +4 -4
- package/dist/types/canvas-renderer.d.ts +11 -6
- package/dist/types/canvas-renderer.d.ts.map +1 -1
- package/dist/types/core/action-registry.d.ts +7 -0
- package/dist/types/core/action-registry.d.ts.map +1 -1
- package/dist/types/core/document.d.ts +1 -1
- package/dist/types/core/document.d.ts.map +1 -1
- package/dist/types/core/json.d.ts +11 -0
- package/dist/types/core/json.d.ts.map +1 -0
- package/dist/types/core/model.d.ts +45 -2
- package/dist/types/core/model.d.ts.map +1 -1
- package/dist/types/core/state.d.ts +24 -1
- package/dist/types/core/state.d.ts.map +1 -1
- package/dist/types/diagram/api.d.ts +6 -6
- package/dist/types/diagram/api.d.ts.map +1 -1
- package/dist/types/diagram/catalog.d.ts +2 -1
- package/dist/types/diagram/catalog.d.ts.map +1 -1
- package/dist/types/diagram/event-emitter.d.ts +6 -1
- package/dist/types/diagram/event-emitter.d.ts.map +1 -1
- package/dist/types/diagram/palette.d.ts +1 -1
- package/dist/types/diagram/palette.d.ts.map +1 -1
- package/dist/types/diagram/stocksharp-diagram.d.ts +29 -22
- package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
- package/dist/types/diagram/types.d.ts +4 -4
- package/dist/types/diagram/types.d.ts.map +1 -1
- package/dist/types/embed.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/canvas-renderer.ts +84 -29
- package/src/core/action-registry.ts +25 -0
- package/src/core/document.ts +147 -31
- package/src/core/json.ts +16 -0
- package/src/core/model.ts +55 -2
- package/src/core/state.ts +41 -3
- package/src/diagram/api.ts +6 -5
- package/src/diagram/catalog.ts +18 -6
- package/src/diagram/event-emitter.ts +12 -1
- package/src/diagram/palette.ts +1 -1
- package/src/diagram/stocksharp-diagram.ts +79 -76
- package/src/diagram/types.ts +6 -5
- package/src/embed.ts +140 -39
- package/src/index.ts +4 -0
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
parseDiagramViewState,
|
|
7
7
|
serializeDiagramViewState,
|
|
8
8
|
} from '../core/view-state.js';
|
|
9
|
-
import type { DiagramDocument } from '../core/model.js';
|
|
9
|
+
import type { DiagramDocument, PortDynamicMode } from '../core/model.js';
|
|
10
10
|
import { DiagramActionRegistry } from '../core/action-registry.js';
|
|
11
11
|
import type {
|
|
12
12
|
DiagramGlobalErrorKind,
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
DiagramPortRuntimeState,
|
|
15
15
|
DiagramRuntimeState,
|
|
16
16
|
DiagramSelection,
|
|
17
|
+
DiagramSnapshot,
|
|
17
18
|
DiagramViewState,
|
|
18
19
|
} from '../core/state.js';
|
|
19
20
|
import {
|
|
@@ -77,7 +78,6 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
77
78
|
private fullscreen = false;
|
|
78
79
|
private fullscreenButtonVisible = true;
|
|
79
80
|
private fullscreenLabels: DiagramFullscreenLabels = { enter: 'Enter fullscreen', exit: 'Exit fullscreen' };
|
|
80
|
-
private linkValidator: LinkValidator | null = null;
|
|
81
81
|
private readonly contextActions = new DiagramActionRegistry<ContextCommand, ContextActionContext>();
|
|
82
82
|
|
|
83
83
|
constructor(options: DiagramOptions) {
|
|
@@ -105,7 +105,6 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
setLinkValidator(validator: LinkValidator | null): void {
|
|
108
|
-
this.linkValidator = validator;
|
|
109
108
|
this.canvas.setLinkValidator(validator === null ? null : ({ fromNode, fromPort, toNode, toPort }) => validator({
|
|
110
109
|
fromNode: this.fromCanvasNode(fromNode),
|
|
111
110
|
fromPort: this.fromCanvasPort(fromPort),
|
|
@@ -178,7 +177,7 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
178
177
|
this.canvas.setGridSnap(enabled, size);
|
|
179
178
|
}
|
|
180
179
|
|
|
181
|
-
getGridSnap(): DiagramGridSettings {
|
|
180
|
+
getGridSnap(): DiagramSnapshot<DiagramGridSettings> {
|
|
182
181
|
return this.canvas.getGridSnap();
|
|
183
182
|
}
|
|
184
183
|
|
|
@@ -202,29 +201,33 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
202
201
|
this.canvas.removeLink(this.toCanvasLink(link));
|
|
203
202
|
}
|
|
204
203
|
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
/** False when nothing changed: no such node or port, or the edit was a no-op. */
|
|
205
|
+
addPort(nodeId: string, direction: PortDirection, port: Port): boolean {
|
|
206
|
+
return this.canvas.addPort(nodeId, direction, this.toCanvasPort(port));
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
/** False when nothing changed: no such node or port, or the edit was a no-op. */
|
|
210
|
+
removePort(nodeId: string, direction: PortDirection, portId: string): boolean {
|
|
211
|
+
return this.canvas.removePort(nodeId, direction, portId);
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
/** False when nothing changed: no such node or port, or the edit was a no-op. */
|
|
215
|
+
updatePortType(nodeId: string, direction: PortDirection, portId: string, type: string): boolean {
|
|
216
|
+
return this.canvas.updatePortType(nodeId, direction, portId, type);
|
|
215
217
|
}
|
|
216
218
|
|
|
217
219
|
updatePort(nodeId: string, direction: PortDirection, portId: string, patch: PortUpdate): boolean {
|
|
218
220
|
return this.canvas.updatePort(nodeId, direction, portId, patch);
|
|
219
221
|
}
|
|
220
222
|
|
|
223
|
+
/** False when nothing changed: no such node or port, or the edit was a no-op. */
|
|
221
224
|
setNodePorts(
|
|
222
225
|
nodeId: string,
|
|
223
|
-
inPorts: ReadonlyArray<{ key: string; name: string; description: string; type: string; maxLinks: number; availableTypes?: string[]; isDynamic?: boolean; dynamicMode?:
|
|
224
|
-
outPorts: ReadonlyArray<{ key: string; name: string; description: string; type: string; maxLinks: number; availableTypes?: string[]; isDynamic?: boolean; dynamicMode?:
|
|
225
|
-
):
|
|
226
|
+
inPorts: ReadonlyArray<{ key: string; name: string; description: string; type: string; maxLinks: number; availableTypes?: string[]; isDynamic?: boolean; dynamicMode?: PortDynamicMode }>,
|
|
227
|
+
outPorts: ReadonlyArray<{ key: string; name: string; description: string; type: string; maxLinks: number; availableTypes?: string[]; isDynamic?: boolean; dynamicMode?: PortDynamicMode }>,
|
|
228
|
+
): boolean {
|
|
226
229
|
const current = this.canvas.findNode(nodeId);
|
|
227
|
-
if (current === undefined) return;
|
|
230
|
+
if (current === undefined) return false;
|
|
228
231
|
const convert = (port: typeof inPorts[number]): CanvasPortInit => ({
|
|
229
232
|
id: port.key,
|
|
230
233
|
name: port.name,
|
|
@@ -243,15 +246,17 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
243
246
|
for (const sibling of current.outPorts.filter((port) => port.isSibling)) {
|
|
244
247
|
if (!nextOut.some((port) => port.id === sibling.id)) nextOut.push(sibling.toInit());
|
|
245
248
|
}
|
|
246
|
-
this.canvas.setNodePorts(nodeId, nextIn, nextOut);
|
|
249
|
+
return this.canvas.setNodePorts(nodeId, nextIn, nextOut);
|
|
247
250
|
}
|
|
248
251
|
|
|
249
|
-
|
|
250
|
-
|
|
252
|
+
/** False when nothing changed: no such node or port, or the edit was a no-op. */
|
|
253
|
+
updateNode(nodeId: string, patch: { name?: string; description?: string; color?: string; border?: string }): boolean {
|
|
254
|
+
return this.canvas.updateNode(nodeId, patch);
|
|
251
255
|
}
|
|
252
256
|
|
|
253
|
-
|
|
254
|
-
|
|
257
|
+
/** False when nothing changed: no such node or port, or the edit was a no-op. */
|
|
258
|
+
setNodeMessage(nodeId: string, message: string): boolean {
|
|
259
|
+
return this.canvas.updateNode(nodeId, { message });
|
|
255
260
|
}
|
|
256
261
|
|
|
257
262
|
setNodeError(nodeId: string, message: string, options: NodeErrorOptions = {}): boolean {
|
|
@@ -262,11 +267,11 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
262
267
|
return this.canvas.clearNodeError(nodeId, kind);
|
|
263
268
|
}
|
|
264
269
|
|
|
265
|
-
getRuntimeState(): DiagramRuntimeState {
|
|
270
|
+
getRuntimeState(): DiagramSnapshot<DiagramRuntimeState> {
|
|
266
271
|
return this.canvas.getRuntimeState();
|
|
267
272
|
}
|
|
268
273
|
|
|
269
|
-
setRuntimeState(state: DiagramRuntimeState): void {
|
|
274
|
+
setRuntimeState(state: DiagramSnapshot<DiagramRuntimeState>): void {
|
|
270
275
|
this.canvas.setRuntimeState(state);
|
|
271
276
|
}
|
|
272
277
|
|
|
@@ -291,12 +296,14 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
291
296
|
this.canvas.setGlobalError(message, kind);
|
|
292
297
|
}
|
|
293
298
|
|
|
294
|
-
|
|
295
|
-
|
|
299
|
+
/** False when nothing changed: no such node or port, or the edit was a no-op. */
|
|
300
|
+
setNodeParamValue(nodeId: string, paramName: string, value: string | undefined): boolean {
|
|
301
|
+
return this.canvas.setNodeParamValue(nodeId, paramName, value);
|
|
296
302
|
}
|
|
297
303
|
|
|
298
|
-
|
|
299
|
-
|
|
304
|
+
/** False when nothing changed: no such node or port, or the edit was a no-op. */
|
|
305
|
+
setNodeName(nodeId: string, value: string): boolean {
|
|
306
|
+
return this.canvas.updateNode(nodeId, { name: value });
|
|
300
307
|
}
|
|
301
308
|
|
|
302
309
|
/** Groups host-driven document edits into one undo/redo operation. */
|
|
@@ -312,7 +319,7 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
312
319
|
this.canvas.setReadOnly(readonly);
|
|
313
320
|
}
|
|
314
321
|
|
|
315
|
-
getInteractionPermissions(): DiagramInteractionPermissions {
|
|
322
|
+
getInteractionPermissions(): DiagramSnapshot<DiagramInteractionPermissions> {
|
|
316
323
|
return this.canvas.getInteractionPermissions();
|
|
317
324
|
}
|
|
318
325
|
|
|
@@ -341,11 +348,11 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
341
348
|
this.canvas.setZoom(scale);
|
|
342
349
|
}
|
|
343
350
|
|
|
344
|
-
getViewState(): DiagramViewState {
|
|
351
|
+
getViewState(): DiagramSnapshot<DiagramViewState> {
|
|
345
352
|
return this.canvas.getViewState();
|
|
346
353
|
}
|
|
347
354
|
|
|
348
|
-
setViewState(state: DiagramViewState): void {
|
|
355
|
+
setViewState(state: DiagramSnapshot<DiagramViewState>): void {
|
|
349
356
|
this.canvas.setViewState(state);
|
|
350
357
|
this.overviewContainer?.classList.toggle('hidden', !state.overviewVisible);
|
|
351
358
|
}
|
|
@@ -365,7 +372,7 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
365
372
|
return this.canvas.takeScreenshot(options);
|
|
366
373
|
}
|
|
367
374
|
|
|
368
|
-
getSelection(): DiagramSelection {
|
|
375
|
+
getSelection(): DiagramSnapshot<DiagramSelection> {
|
|
369
376
|
return this.canvas.getSelection();
|
|
370
377
|
}
|
|
371
378
|
|
|
@@ -400,12 +407,12 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
400
407
|
}
|
|
401
408
|
|
|
402
409
|
/** Text of the fullscreen button, for a host that renders in another language. */
|
|
403
|
-
setFullscreenLabels(labels: DiagramFullscreenLabels): void {
|
|
410
|
+
setFullscreenLabels(labels: DiagramSnapshot<DiagramFullscreenLabels>): void {
|
|
404
411
|
this.fullscreenLabels = labels;
|
|
405
412
|
this.updateFullscreenButton();
|
|
406
413
|
}
|
|
407
414
|
|
|
408
|
-
getFullscreenLabels(): DiagramFullscreenLabels {
|
|
415
|
+
getFullscreenLabels(): DiagramSnapshot<DiagramFullscreenLabels> {
|
|
409
416
|
return { ...this.fullscreenLabels };
|
|
410
417
|
}
|
|
411
418
|
|
|
@@ -760,51 +767,47 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
760
767
|
|
|
761
768
|
private registerContextActions(): void {
|
|
762
769
|
const permissions = () => this.canvas.getInteractionPermissions();
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
this.contextActions.register({
|
|
805
|
-
id: 'help',
|
|
806
|
-
canExecute: ({ nodes }) => this.helpEnabled && nodes.length > 0,
|
|
807
|
-
execute: ({ nodes }) => this.emit('nodeHelp', { nodes }),
|
|
770
|
+
// Keyed by ContextCommand, so a command added to the union without an
|
|
771
|
+
// entry here fails to compile rather than quietly missing from the menu.
|
|
772
|
+
// Key order is menu order.
|
|
773
|
+
this.contextActions.registerAll({
|
|
774
|
+
undo: {
|
|
775
|
+
canExecute: () => this.canUndo(),
|
|
776
|
+
execute: () => this.undo(),
|
|
777
|
+
},
|
|
778
|
+
redo: {
|
|
779
|
+
canExecute: () => this.canRedo(),
|
|
780
|
+
execute: () => this.redo(),
|
|
781
|
+
},
|
|
782
|
+
cut: {
|
|
783
|
+
canExecute: ({ nodes }) => nodes.length > 0 && permissions().copy && permissions().deleteSelection,
|
|
784
|
+
execute: () => this.cutSelection(),
|
|
785
|
+
},
|
|
786
|
+
copy: {
|
|
787
|
+
canExecute: ({ nodes }) => nodes.length > 0 && permissions().copy,
|
|
788
|
+
execute: () => this.copySelection(),
|
|
789
|
+
},
|
|
790
|
+
paste: {
|
|
791
|
+
canExecute: () => this.canvas.hasClipboard() && permissions().paste,
|
|
792
|
+
execute: () => this.pasteSelection(),
|
|
793
|
+
},
|
|
794
|
+
open: {
|
|
795
|
+
canExecute: ({ nodes }) => nodes.length === 1 && nodes[0].openAction.length > 0,
|
|
796
|
+
execute: ({ nodes }) => this.emit('nodeOpen', { nodes }),
|
|
797
|
+
},
|
|
798
|
+
delete: {
|
|
799
|
+
canExecute: ({ selection }) => permissions().deleteSelection
|
|
800
|
+
&& (selection.nodeIds.length > 0 || selection.linkIds.length > 0),
|
|
801
|
+
execute: () => this.canvas.deleteSelection(),
|
|
802
|
+
},
|
|
803
|
+
properties: {
|
|
804
|
+
canExecute: ({ nodes }) => nodes.length > 0,
|
|
805
|
+
execute: ({ nodes }) => this.emit('nodeProperties', { nodes }),
|
|
806
|
+
},
|
|
807
|
+
help: {
|
|
808
|
+
canExecute: ({ nodes }) => this.helpEnabled && nodes.length > 0,
|
|
809
|
+
execute: ({ nodes }) => this.emit('nodeHelp', { nodes }),
|
|
810
|
+
},
|
|
808
811
|
});
|
|
809
812
|
}
|
|
810
813
|
|
package/src/diagram/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { toPortDynamicMode } from '../core/model.js';
|
|
2
|
+
import type { JsonObject, PortDynamicMode } from '../core/model.js';
|
|
2
3
|
|
|
3
4
|
// Core data model — node/port catalog (palette) and the live diagram (DiagramNode + Link).
|
|
4
5
|
|
|
@@ -29,7 +30,7 @@ export interface PortInit {
|
|
|
29
30
|
/// True when this port can be added/removed at runtime on a node.
|
|
30
31
|
isDynamic?: boolean;
|
|
31
32
|
/// "" / "manual" / "onConnect" — see PalettePortDto.dynamicMode docs.
|
|
32
|
-
dynamicMode?:
|
|
33
|
+
dynamicMode?: PortDynamicMode;
|
|
33
34
|
/// True when this port was spawned by the grow-on-connect pipeline.
|
|
34
35
|
/// Distinguishes anchor + runtime siblings on save (only siblings are
|
|
35
36
|
/// persisted in the diagram blob; anchors come from the palette schema).
|
|
@@ -47,7 +48,7 @@ export class Port {
|
|
|
47
48
|
maxLinks: number;
|
|
48
49
|
availableTypes: string[];
|
|
49
50
|
isDynamic: boolean;
|
|
50
|
-
dynamicMode:
|
|
51
|
+
dynamicMode: PortDynamicMode;
|
|
51
52
|
isSibling: boolean;
|
|
52
53
|
|
|
53
54
|
constructor(init: PortInit) {
|
|
@@ -58,7 +59,7 @@ export class Port {
|
|
|
58
59
|
this.maxLinks = typeof init.maxLinks === 'number' ? init.maxLinks : 0;
|
|
59
60
|
this.availableTypes = init.availableTypes ?? [];
|
|
60
61
|
this.isDynamic = init.isDynamic ?? false;
|
|
61
|
-
this.dynamicMode = init.dynamicMode
|
|
62
|
+
this.dynamicMode = toPortDynamicMode(init.dynamicMode);
|
|
62
63
|
this.isSibling = init.isSibling ?? false;
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -268,7 +269,7 @@ export interface PortData {
|
|
|
268
269
|
/// the anchor port spawns a sibling
|
|
269
270
|
/// on each link drop and the link is rerouted to the sibling.
|
|
270
271
|
isDynamic?: boolean;
|
|
271
|
-
dynamicMode?:
|
|
272
|
+
dynamicMode?: PortDynamicMode;
|
|
272
273
|
/// True when this port was spawned by the dynamic-growth pipeline rather
|
|
273
274
|
/// than declared on the palette element. Persists in the diagram blob so
|
|
274
275
|
/// reload restores the same set of sibling sockets.
|
package/src/embed.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { StockSharpDiagram } from './diagram/stocksharp-diagram.js';
|
|
|
11
11
|
import { StockSharpCatalog } from './diagram/catalog.js';
|
|
12
12
|
import { DiagramNode, Link, Node, Port, PortType } from './diagram/types.js';
|
|
13
13
|
import type { FullscreenRequestedPayload } from './diagram/api.js';
|
|
14
|
+
import { toPortDynamicMode } from './core/model.js';
|
|
15
|
+
import type { PortDynamicMode } from './core/model.js';
|
|
14
16
|
|
|
15
17
|
interface PalettePort {
|
|
16
18
|
key: string;
|
|
@@ -19,7 +21,7 @@ interface PalettePort {
|
|
|
19
21
|
maxLinks?: number;
|
|
20
22
|
availableTypes?: string[];
|
|
21
23
|
isDynamic?: boolean;
|
|
22
|
-
dynamicMode?:
|
|
24
|
+
dynamicMode?: PortDynamicMode;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
interface PaletteElement {
|
|
@@ -166,59 +168,136 @@ function toDiagramNodes(
|
|
|
166
168
|
// Walk the persisted Content.Value.Scheme.Model.{Nodes,Links} structure into
|
|
167
169
|
// the thin scheme. Defensive at every field so a partially
|
|
168
170
|
// broken document yields as many nodes as are readable; a wholly unusable one yields an empty scheme.
|
|
171
|
+
// Every reader below narrows rather than asserts: the input is arbitrary JSON
|
|
172
|
+
// from a URL or an inline <script>, so a type assertion here would only be a
|
|
173
|
+
// promise the data never made. Anything unreadable is skipped, never thrown on.
|
|
174
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
175
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
179
|
+
return isRecord(value) ? value : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function asArray(value: unknown): unknown[] {
|
|
183
|
+
return Array.isArray(value) ? value : [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function asString(value: unknown): string {
|
|
187
|
+
return typeof value === 'string' ? value : '';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function asFiniteNumber(value: unknown): number {
|
|
191
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
169
194
|
function nodeName(node: Record<string, unknown>): string {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
return nameVal;
|
|
176
|
-
if (typeof node?.Figure === 'string' && (node.Figure as string).length > 0)
|
|
177
|
-
return node.Figure as string;
|
|
178
|
-
return typeof node?.Key === 'string' ? node.Key as string : '';
|
|
195
|
+
const named = asRecord(asRecord(asRecord(node.Settings)?.Parameters)?.Name)?.Value;
|
|
196
|
+
if (typeof named === 'string' && named.length > 0)
|
|
197
|
+
return named;
|
|
198
|
+
const figure = asString(node.Figure);
|
|
199
|
+
return figure.length > 0 ? figure : asString(node.Key);
|
|
179
200
|
}
|
|
180
201
|
|
|
181
202
|
function parseRawScheme(raw: unknown): DiagramEmbedScheme {
|
|
182
203
|
const nodes: DiagramEmbedSchemeNode[] = [];
|
|
183
204
|
const links: DiagramEmbedSchemeLink[] = [];
|
|
184
205
|
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const scheme = value?.Scheme as Record<string, unknown> | undefined;
|
|
189
|
-
const model = scheme?.Model as Record<string, unknown> | undefined;
|
|
190
|
-
if (!model)
|
|
206
|
+
const model = asRecord(asRecord(asRecord(asRecord(raw)?.Content)?.Value)?.Scheme)?.Model;
|
|
207
|
+
const fields = asRecord(model);
|
|
208
|
+
if (fields === undefined)
|
|
191
209
|
return { nodes, links };
|
|
192
210
|
|
|
193
|
-
for (const
|
|
194
|
-
const
|
|
211
|
+
for (const entry of asArray(fields.Nodes)) {
|
|
212
|
+
const node = asRecord(entry);
|
|
213
|
+
if (node === undefined)
|
|
214
|
+
continue;
|
|
215
|
+
const key = node.Key;
|
|
195
216
|
if (typeof key !== 'string' || key.length === 0)
|
|
196
217
|
continue;
|
|
197
218
|
nodes.push({
|
|
198
219
|
id: key,
|
|
199
|
-
typeId:
|
|
200
|
-
name: nodeName(
|
|
201
|
-
x:
|
|
202
|
-
y:
|
|
220
|
+
typeId: asString(node.TypeId),
|
|
221
|
+
name: nodeName(node),
|
|
222
|
+
x: asFiniteNumber(node.X),
|
|
223
|
+
y: asFiniteNumber(node.Y),
|
|
203
224
|
});
|
|
204
225
|
}
|
|
205
226
|
|
|
206
|
-
for (const
|
|
207
|
-
const
|
|
208
|
-
|
|
227
|
+
for (const entry of asArray(fields.Links)) {
|
|
228
|
+
const link = asRecord(entry);
|
|
229
|
+
if (link === undefined)
|
|
230
|
+
continue;
|
|
231
|
+
const from = link.From;
|
|
232
|
+
const to = link.To;
|
|
209
233
|
if (typeof from !== 'string' || typeof to !== 'string')
|
|
210
234
|
continue;
|
|
211
235
|
links.push({
|
|
212
236
|
from,
|
|
213
|
-
fromPort:
|
|
237
|
+
fromPort: asString(link.FromPort),
|
|
214
238
|
to,
|
|
215
|
-
toPort:
|
|
239
|
+
toPort: asString(link.ToPort),
|
|
216
240
|
});
|
|
217
241
|
}
|
|
218
242
|
|
|
219
243
|
return { nodes, links };
|
|
220
244
|
}
|
|
221
245
|
|
|
246
|
+
function parsePalettePorts(value: unknown): PalettePort[] {
|
|
247
|
+
const ports: PalettePort[] = [];
|
|
248
|
+
for (const entry of asArray(value)) {
|
|
249
|
+
const port = asRecord(entry);
|
|
250
|
+
if (port === undefined)
|
|
251
|
+
continue;
|
|
252
|
+
const key = asString(port.key);
|
|
253
|
+
if (key.length === 0)
|
|
254
|
+
continue;
|
|
255
|
+
ports.push({
|
|
256
|
+
key,
|
|
257
|
+
name: asString(port.name),
|
|
258
|
+
type: asString(port.type),
|
|
259
|
+
maxLinks: asFiniteNumber(port.maxLinks),
|
|
260
|
+
availableTypes: asArray(port.availableTypes).filter((item): item is string => typeof item === 'string'),
|
|
261
|
+
isDynamic: port.isDynamic === true,
|
|
262
|
+
dynamicMode: toPortDynamicMode(port.dynamicMode),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return ports;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function parsePalette(value: unknown): Palette {
|
|
269
|
+
const socketTypes: Palette['socketTypes'] = [];
|
|
270
|
+
const elements: PaletteElement[] = [];
|
|
271
|
+
const root = asRecord(value);
|
|
272
|
+
if (root === undefined)
|
|
273
|
+
return { socketTypes, elements };
|
|
274
|
+
|
|
275
|
+
for (const entry of asArray(root.socketTypes)) {
|
|
276
|
+
const socket = asRecord(entry);
|
|
277
|
+
const name = asString(socket?.name);
|
|
278
|
+
if (name.length === 0)
|
|
279
|
+
continue;
|
|
280
|
+
socketTypes.push({ name, color: asString(socket?.color) });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const entry of asArray(root.elements)) {
|
|
284
|
+
const element = asRecord(entry);
|
|
285
|
+
const typeId = asString(element?.typeId);
|
|
286
|
+
if (element === undefined || typeId.length === 0)
|
|
287
|
+
continue;
|
|
288
|
+
elements.push({
|
|
289
|
+
typeId,
|
|
290
|
+
name: asString(element.name),
|
|
291
|
+
groupName: asString(element.groupName),
|
|
292
|
+
icon: asString(element.icon),
|
|
293
|
+
inPorts: parsePalettePorts(element.inPorts),
|
|
294
|
+
outPorts: parsePalettePorts(element.outPorts),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { socketTypes, elements };
|
|
299
|
+
}
|
|
300
|
+
|
|
222
301
|
export async function renderScheme(
|
|
223
302
|
div: HTMLElement,
|
|
224
303
|
paletteUrl: string,
|
|
@@ -237,7 +316,12 @@ async function renderSchemeAtRevision(
|
|
|
237
316
|
): Promise<DiagramEmbedHandle | null> {
|
|
238
317
|
if (renderRevisions.get(div) !== revision)
|
|
239
318
|
return null;
|
|
240
|
-
|
|
319
|
+
// Same treatment the schema fetch gets: a non-2xx answer is a failure, not a
|
|
320
|
+
// body to parse. Callers that degrade already catch it.
|
|
321
|
+
const response = await fetch(paletteUrl);
|
|
322
|
+
if (!response.ok)
|
|
323
|
+
throw new Error(`Palette request to ${paletteUrl} failed with status ${response.status}.`);
|
|
324
|
+
const palette = parsePalette(await response.json());
|
|
241
325
|
if (renderRevisions.get(div) !== revision)
|
|
242
326
|
return null;
|
|
243
327
|
const catalog = buildCatalog(palette);
|
|
@@ -412,6 +496,25 @@ function stopDisconnectedHostObserverWhenIdle(): void {
|
|
|
412
496
|
disconnectedHostObserver = null;
|
|
413
497
|
}
|
|
414
498
|
|
|
499
|
+
interface EmbedErrorTexts {
|
|
500
|
+
load: string;
|
|
501
|
+
empty: string;
|
|
502
|
+
draw: string;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// The host overrides the inline notes with data-diagram-errors="load|empty|draw".
|
|
506
|
+
// Each slot falls back when it is empty rather than when it is absent: split on a
|
|
507
|
+
// missing attribute yields [''], one present empty string, so a destructuring
|
|
508
|
+
// default would never fire and the note would render blank.
|
|
509
|
+
function errorTexts(div: HTMLElement): EmbedErrorTexts {
|
|
510
|
+
const parts = (div.dataset.diagramErrors ?? '').split('|');
|
|
511
|
+
return {
|
|
512
|
+
load: parts[0] || 'Diagram source could not be loaded.',
|
|
513
|
+
empty: parts[1] || 'Diagram is empty or malformed.',
|
|
514
|
+
draw: parts[2] || 'Diagram could not be rendered.',
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
415
518
|
function note(div: HTMLElement, message: string, revision: number): void {
|
|
416
519
|
if (renderRevisions.get(div) !== revision) return;
|
|
417
520
|
disposeActiveRender(div);
|
|
@@ -429,30 +532,29 @@ export async function renderFromSource(
|
|
|
429
532
|
options: DiagramEmbedOptions = {},
|
|
430
533
|
): Promise<DiagramEmbedHandle | null> {
|
|
431
534
|
const revision = beginRender(div);
|
|
432
|
-
const errors = (div
|
|
433
|
-
const [errLoad = 'Diagram source could not be loaded.', errEmpty = 'Diagram is empty or malformed.', errDraw = 'Diagram could not be rendered.'] = errors;
|
|
535
|
+
const errors = errorTexts(div);
|
|
434
536
|
|
|
435
537
|
let raw: unknown;
|
|
436
538
|
try {
|
|
437
539
|
const resp = await fetch(srcUrl);
|
|
438
|
-
if (!resp.ok) { note(div,
|
|
540
|
+
if (!resp.ok) { note(div, errors.load, revision); return null; }
|
|
439
541
|
const text = (await resp.text()).replace(/^/, '');
|
|
440
542
|
raw = JSON.parse(text);
|
|
441
543
|
} catch {
|
|
442
|
-
note(div,
|
|
544
|
+
note(div, errors.load, revision);
|
|
443
545
|
return null;
|
|
444
546
|
}
|
|
445
547
|
|
|
446
548
|
const scheme = parseRawScheme(raw);
|
|
447
549
|
if (scheme.nodes.length === 0) {
|
|
448
|
-
note(div,
|
|
550
|
+
note(div, errors.empty, revision);
|
|
449
551
|
return null;
|
|
450
552
|
}
|
|
451
553
|
|
|
452
554
|
try {
|
|
453
555
|
return await renderSchemeAtRevision(div, paletteUrl, scheme, revision, options);
|
|
454
556
|
} catch {
|
|
455
|
-
note(div,
|
|
557
|
+
note(div, errors.draw, revision);
|
|
456
558
|
return null;
|
|
457
559
|
}
|
|
458
560
|
}
|
|
@@ -467,27 +569,26 @@ export async function renderFromInline(
|
|
|
467
569
|
options: DiagramEmbedOptions = {},
|
|
468
570
|
): Promise<DiagramEmbedHandle | null> {
|
|
469
571
|
const revision = beginRender(div);
|
|
470
|
-
const errors = (div
|
|
471
|
-
const [, errEmpty = 'Diagram is empty or malformed.', errDraw = 'Diagram could not be rendered.'] = errors;
|
|
572
|
+
const errors = errorTexts(div);
|
|
472
573
|
|
|
473
574
|
let raw: unknown;
|
|
474
575
|
try {
|
|
475
576
|
raw = JSON.parse(json);
|
|
476
577
|
} catch {
|
|
477
|
-
note(div,
|
|
578
|
+
note(div, errors.empty, revision);
|
|
478
579
|
return null;
|
|
479
580
|
}
|
|
480
581
|
|
|
481
582
|
const scheme = parseRawScheme(raw);
|
|
482
583
|
if (scheme.nodes.length === 0) {
|
|
483
|
-
note(div,
|
|
584
|
+
note(div, errors.empty, revision);
|
|
484
585
|
return null;
|
|
485
586
|
}
|
|
486
587
|
|
|
487
588
|
try {
|
|
488
589
|
return await renderSchemeAtRevision(div, paletteUrl, scheme, revision, options);
|
|
489
590
|
} catch {
|
|
490
|
-
note(div,
|
|
591
|
+
note(div, errors.draw, revision);
|
|
491
592
|
return null;
|
|
492
593
|
}
|
|
493
594
|
}
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export type {
|
|
|
36
36
|
JsonObject,
|
|
37
37
|
JsonPrimitive,
|
|
38
38
|
JsonValue,
|
|
39
|
+
PortDynamicMode,
|
|
39
40
|
} from './core/model.js';
|
|
40
41
|
|
|
41
42
|
export {
|
|
@@ -43,6 +44,7 @@ export {
|
|
|
43
44
|
createDiagramNodeRuntimeState,
|
|
44
45
|
createDiagramPortRuntimeState,
|
|
45
46
|
createDiagramRuntimeState,
|
|
47
|
+
cloneDiagramNodeErrors,
|
|
46
48
|
cloneDiagramRuntimeState,
|
|
47
49
|
createDiagramSelection,
|
|
48
50
|
createDiagramViewState,
|
|
@@ -64,6 +66,7 @@ export type {
|
|
|
64
66
|
DiagramGlobalErrorKind,
|
|
65
67
|
DiagramInteractionPermissions,
|
|
66
68
|
DiagramNodeErrorKind,
|
|
69
|
+
DiagramNodeErrors,
|
|
67
70
|
DiagramNodePortRuntimeState,
|
|
68
71
|
DiagramNodeRuntimeState,
|
|
69
72
|
DiagramPortDirection,
|
|
@@ -71,6 +74,7 @@ export type {
|
|
|
71
74
|
DiagramRuntimeState,
|
|
72
75
|
DiagramSelectedPort,
|
|
73
76
|
DiagramSelection,
|
|
77
|
+
DiagramSnapshot,
|
|
74
78
|
DiagramViewState,
|
|
75
79
|
} from './core/state.js';
|
|
76
80
|
|