@stocksharp/diagram 0.2.2 → 0.3.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.
Files changed (68) hide show
  1. package/README.md +12 -0
  2. package/dist/esm/canvas-renderer.js +28 -22
  3. package/dist/esm/canvas-renderer.js.map +1 -1
  4. package/dist/esm/core/action-registry.js +24 -0
  5. package/dist/esm/core/action-registry.js.map +1 -1
  6. package/dist/esm/core/document.js +85 -28
  7. package/dist/esm/core/document.js.map +1 -1
  8. package/dist/esm/core/json.js +17 -0
  9. package/dist/esm/core/json.js.map +1 -0
  10. package/dist/esm/core/model.js +4 -0
  11. package/dist/esm/core/model.js.map +1 -1
  12. package/dist/esm/core/state.js +16 -2
  13. package/dist/esm/core/state.js.map +1 -1
  14. package/dist/esm/diagram/catalog.js +17 -5
  15. package/dist/esm/diagram/catalog.js.map +1 -1
  16. package/dist/esm/diagram/event-emitter.js +10 -0
  17. package/dist/esm/diagram/event-emitter.js.map +1 -1
  18. package/dist/esm/diagram/stocksharp-diagram.js +69 -57
  19. package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
  20. package/dist/esm/diagram/types.js +2 -1
  21. package/dist/esm/diagram/types.js.map +1 -1
  22. package/dist/esm/embed.js +129 -39
  23. package/dist/esm/embed.js.map +1 -1
  24. package/dist/esm/index.js +1 -1
  25. package/dist/esm/index.js.map +1 -1
  26. package/dist/ssdiagram.js +314 -147
  27. package/dist/ssdiagram.js.map +4 -4
  28. package/dist/types/canvas-renderer.d.ts +3 -5
  29. package/dist/types/canvas-renderer.d.ts.map +1 -1
  30. package/dist/types/core/action-registry.d.ts +7 -0
  31. package/dist/types/core/action-registry.d.ts.map +1 -1
  32. package/dist/types/core/document.d.ts.map +1 -1
  33. package/dist/types/core/json.d.ts +11 -0
  34. package/dist/types/core/json.d.ts.map +1 -0
  35. package/dist/types/core/model.d.ts +10 -1
  36. package/dist/types/core/model.d.ts.map +1 -1
  37. package/dist/types/core/state.d.ts +24 -1
  38. package/dist/types/core/state.d.ts.map +1 -1
  39. package/dist/types/diagram/api.d.ts +13 -6
  40. package/dist/types/diagram/api.d.ts.map +1 -1
  41. package/dist/types/diagram/catalog.d.ts +2 -1
  42. package/dist/types/diagram/catalog.d.ts.map +1 -1
  43. package/dist/types/diagram/event-emitter.d.ts +6 -1
  44. package/dist/types/diagram/event-emitter.d.ts.map +1 -1
  45. package/dist/types/diagram/palette.d.ts +1 -1
  46. package/dist/types/diagram/palette.d.ts.map +1 -1
  47. package/dist/types/diagram/stocksharp-diagram.d.ts +32 -21
  48. package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
  49. package/dist/types/diagram/types.d.ts +4 -4
  50. package/dist/types/diagram/types.d.ts.map +1 -1
  51. package/dist/types/embed.d.ts.map +1 -1
  52. package/dist/types/index.d.ts +3 -3
  53. package/dist/types/index.d.ts.map +1 -1
  54. package/package.json +2 -2
  55. package/src/canvas-renderer.ts +26 -24
  56. package/src/core/action-registry.ts +25 -0
  57. package/src/core/document.ts +91 -31
  58. package/src/core/json.ts +16 -0
  59. package/src/core/model.ts +14 -1
  60. package/src/core/state.ts +41 -3
  61. package/src/diagram/api.ts +14 -5
  62. package/src/diagram/catalog.ts +18 -6
  63. package/src/diagram/event-emitter.ts +12 -1
  64. package/src/diagram/palette.ts +1 -1
  65. package/src/diagram/stocksharp-diagram.ts +91 -75
  66. package/src/diagram/types.ts +6 -5
  67. package/src/embed.ts +149 -40
  68. package/src/index.ts +4 -0
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?: string;
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 settings = node?.Settings as Record<string, unknown> | undefined;
171
- const parameters = settings?.Parameters as Record<string, unknown> | undefined;
172
- const nameParam = parameters?.Name as Record<string, unknown> | undefined;
173
- const nameVal = nameParam?.Value;
174
- if (typeof nameVal === 'string' && nameVal.length > 0)
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 root = raw as Record<string, unknown>;
186
- const content = root?.Content as Record<string, unknown> | undefined;
187
- const value = content?.Value as Record<string, unknown> | undefined;
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 raw of (model.Nodes as Record<string, unknown>[] ?? [])) {
194
- const key = raw?.Key;
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: typeof raw.TypeId === 'string' ? raw.TypeId : '',
200
- name: nodeName(raw),
201
- x: typeof raw.X === 'number' ? raw.X : 0,
202
- y: typeof raw.Y === 'number' ? raw.Y : 0,
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 raw of (model.Links as Record<string, unknown>[] ?? [])) {
207
- const from = raw?.From;
208
- const to = raw?.To;
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: typeof raw.FromPort === 'string' ? raw.FromPort : '',
237
+ fromPort: asString(link.FromPort),
214
238
  to,
215
- toPort: typeof raw.ToPort === 'string' ? raw.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
- const palette = (await fetch(paletteUrl).then((r) => r.json())) as Palette;
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);
@@ -245,9 +329,17 @@ async function renderSchemeAtRevision(
245
329
  disposeActiveRender(div);
246
330
  div.classList.remove('ss-diagram-error');
247
331
  div.replaceChildren();
332
+ // The host page passes its wording the same way it passes the error texts: through the dataset,
333
+ // "enter|exit". Left out, the button keeps its English defaults.
334
+ const [enterLabel, exitLabel] = (div.dataset.diagramFullscreen ?? '').split('|');
335
+
248
336
  let diagram: StockSharpDiagram;
249
337
  try {
250
- diagram = new StockSharpDiagram({ div, catalog });
338
+ diagram = new StockSharpDiagram({
339
+ div,
340
+ catalog,
341
+ ...(enterLabel && exitLabel ? { fullscreenLabels: { enter: enterLabel, exit: exitLabel } } : {}),
342
+ });
251
343
  } catch (error) {
252
344
  div.replaceChildren();
253
345
  throw error;
@@ -404,6 +496,25 @@ function stopDisconnectedHostObserverWhenIdle(): void {
404
496
  disconnectedHostObserver = null;
405
497
  }
406
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
+
407
518
  function note(div: HTMLElement, message: string, revision: number): void {
408
519
  if (renderRevisions.get(div) !== revision) return;
409
520
  disposeActiveRender(div);
@@ -421,30 +532,29 @@ export async function renderFromSource(
421
532
  options: DiagramEmbedOptions = {},
422
533
  ): Promise<DiagramEmbedHandle | null> {
423
534
  const revision = beginRender(div);
424
- const errors = (div.dataset.diagramErrors ?? '').split('|');
425
- 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);
426
536
 
427
537
  let raw: unknown;
428
538
  try {
429
539
  const resp = await fetch(srcUrl);
430
- if (!resp.ok) { note(div, errLoad, revision); return null; }
540
+ if (!resp.ok) { note(div, errors.load, revision); return null; }
431
541
  const text = (await resp.text()).replace(/^/, '');
432
542
  raw = JSON.parse(text);
433
543
  } catch {
434
- note(div, errLoad, revision);
544
+ note(div, errors.load, revision);
435
545
  return null;
436
546
  }
437
547
 
438
548
  const scheme = parseRawScheme(raw);
439
549
  if (scheme.nodes.length === 0) {
440
- note(div, errEmpty, revision);
550
+ note(div, errors.empty, revision);
441
551
  return null;
442
552
  }
443
553
 
444
554
  try {
445
555
  return await renderSchemeAtRevision(div, paletteUrl, scheme, revision, options);
446
556
  } catch {
447
- note(div, errDraw, revision);
557
+ note(div, errors.draw, revision);
448
558
  return null;
449
559
  }
450
560
  }
@@ -459,27 +569,26 @@ export async function renderFromInline(
459
569
  options: DiagramEmbedOptions = {},
460
570
  ): Promise<DiagramEmbedHandle | null> {
461
571
  const revision = beginRender(div);
462
- const errors = (div.dataset.diagramErrors ?? '').split('|');
463
- const [, errEmpty = 'Diagram is empty or malformed.', errDraw = 'Diagram could not be rendered.'] = errors;
572
+ const errors = errorTexts(div);
464
573
 
465
574
  let raw: unknown;
466
575
  try {
467
576
  raw = JSON.parse(json);
468
577
  } catch {
469
- note(div, errEmpty, revision);
578
+ note(div, errors.empty, revision);
470
579
  return null;
471
580
  }
472
581
 
473
582
  const scheme = parseRawScheme(raw);
474
583
  if (scheme.nodes.length === 0) {
475
- note(div, errEmpty, revision);
584
+ note(div, errors.empty, revision);
476
585
  return null;
477
586
  }
478
587
 
479
588
  try {
480
589
  return await renderSchemeAtRevision(div, paletteUrl, scheme, revision, options);
481
590
  } catch {
482
- note(div, errDraw, revision);
591
+ note(div, errors.draw, revision);
483
592
  return null;
484
593
  }
485
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