@stocksharp/diagram 0.1.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/LICENSE +30 -0
- package/NOTICE +21 -0
- package/README.md +346 -0
- package/dist/esm/canvas-renderer.js +3226 -0
- package/dist/esm/canvas-renderer.js.map +1 -0
- package/dist/esm/core/action-registry.js +35 -0
- package/dist/esm/core/action-registry.js.map +1 -0
- package/dist/esm/core/document.js +350 -0
- package/dist/esm/core/document.js.map +1 -0
- package/dist/esm/core/history.js +136 -0
- package/dist/esm/core/history.js.map +1 -0
- package/dist/esm/core/model.js +2 -0
- package/dist/esm/core/model.js.map +1 -0
- package/dist/esm/core/state.js +73 -0
- package/dist/esm/core/state.js.map +1 -0
- package/dist/esm/core/view-state.js +70 -0
- package/dist/esm/core/view-state.js.map +1 -0
- package/dist/esm/diagram/api.js +2 -0
- package/dist/esm/diagram/api.js.map +1 -0
- package/dist/esm/diagram/catalog.js +45 -0
- package/dist/esm/diagram/catalog.js.map +1 -0
- package/dist/esm/diagram/event-emitter.js +42 -0
- package/dist/esm/diagram/event-emitter.js.map +1 -0
- package/dist/esm/diagram/palette.js +269 -0
- package/dist/esm/diagram/palette.js.map +1 -0
- package/dist/esm/diagram/stocksharp-diagram.js +784 -0
- package/dist/esm/diagram/stocksharp-diagram.js.map +1 -0
- package/dist/esm/diagram/types.js +105 -0
- package/dist/esm/diagram/types.js.map +1 -0
- package/dist/esm/embed.js +398 -0
- package/dist/esm/embed.js.map +1 -0
- package/dist/esm/i18n.js +12 -0
- package/dist/esm/i18n.js.map +1 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/ssdiagram.js +5141 -0
- package/dist/ssdiagram.js.map +7 -0
- package/dist/types/canvas-renderer.d.ts +512 -0
- package/dist/types/canvas-renderer.d.ts.map +1 -0
- package/dist/types/core/action-registry.d.ts +18 -0
- package/dist/types/core/action-registry.d.ts.map +1 -0
- package/dist/types/core/document.d.ts +12 -0
- package/dist/types/core/document.d.ts.map +1 -0
- package/dist/types/core/history.d.ts +33 -0
- package/dist/types/core/history.d.ts.map +1 -0
- package/dist/types/core/model.d.ts +85 -0
- package/dist/types/core/model.d.ts.map +1 -0
- package/dist/types/core/state.d.ts +68 -0
- package/dist/types/core/state.d.ts.map +1 -0
- package/dist/types/core/view-state.d.ts +14 -0
- package/dist/types/core/view-state.d.ts.map +1 -0
- package/dist/types/diagram/api.d.ts +213 -0
- package/dist/types/diagram/api.d.ts.map +1 -0
- package/dist/types/diagram/catalog.d.ts +19 -0
- package/dist/types/diagram/catalog.d.ts.map +1 -0
- package/dist/types/diagram/event-emitter.d.ts +9 -0
- package/dist/types/diagram/event-emitter.d.ts.map +1 -0
- package/dist/types/diagram/palette.d.ts +63 -0
- package/dist/types/diagram/palette.d.ts.map +1 -0
- package/dist/types/diagram/stocksharp-diagram.d.ts +157 -0
- package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -0
- package/dist/types/diagram/types.d.ts +171 -0
- package/dist/types/diagram/types.d.ts.map +1 -0
- package/dist/types/embed.d.ts +35 -0
- package/dist/types/embed.d.ts.map +1 -0
- package/dist/types/i18n.d.ts +167 -0
- package/dist/types/i18n.d.ts.map +1 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +126 -0
- package/sample.png +0 -0
- package/src/canvas-renderer.ts +3249 -0
- package/src/core/action-registry.ts +46 -0
- package/src/core/document.ts +387 -0
- package/src/core/history.ts +154 -0
- package/src/core/model.ts +99 -0
- package/src/core/state.ts +148 -0
- package/src/core/view-state.ts +82 -0
- package/src/diagram/api.ts +257 -0
- package/src/diagram/catalog.ts +55 -0
- package/src/diagram/event-emitter.ts +44 -0
- package/src/diagram/palette.ts +312 -0
- package/src/diagram/stocksharp-diagram.ts +945 -0
- package/src/diagram/types.ts +332 -0
- package/src/embed.ts +508 -0
- package/src/i18n.ts +210 -0
- package/src/index.ts +171 -0
package/src/embed.ts
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
// Shared read-only diagram embed layer. Rendering, palette loading and theming
|
|
2
|
+
// live next to the diagram engine so web applications do not copy that logic.
|
|
3
|
+
//
|
|
4
|
+
// renderScheme draws a scheme supplied by the caller. renderAll,
|
|
5
|
+
// renderFromSource and renderFromInline discover .ss-diagram-host elements and
|
|
6
|
+
// load a schema from a URL or embedded JSON. Every failure degrades to a short
|
|
7
|
+
// inline note instead of throwing.
|
|
8
|
+
// StockSharpDiagram uses the canvas renderer directly, so this module is
|
|
9
|
+
// self-contained and uses the same public component as the editor.
|
|
10
|
+
import { StockSharpDiagram } from './diagram/stocksharp-diagram.js';
|
|
11
|
+
import { StockSharpCatalog } from './diagram/catalog.js';
|
|
12
|
+
import { DiagramNode, Link, Node, Port, PortType } from './diagram/types.js';
|
|
13
|
+
import type { FullscreenRequestedPayload } from './diagram/api.js';
|
|
14
|
+
|
|
15
|
+
interface PalettePort {
|
|
16
|
+
key: string;
|
|
17
|
+
name: string;
|
|
18
|
+
type: string;
|
|
19
|
+
maxLinks?: number;
|
|
20
|
+
availableTypes?: string[];
|
|
21
|
+
isDynamic?: boolean;
|
|
22
|
+
dynamicMode?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PaletteElement {
|
|
26
|
+
typeId: string;
|
|
27
|
+
name: string;
|
|
28
|
+
groupName: string;
|
|
29
|
+
icon: string;
|
|
30
|
+
inPorts: PalettePort[];
|
|
31
|
+
outPorts: PalettePort[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Palette {
|
|
35
|
+
socketTypes: { name: string; color: string }[];
|
|
36
|
+
elements: PaletteElement[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DiagramEmbedSchemeNode { id: string; typeId: string; name: string; x: number; y: number; }
|
|
40
|
+
export interface DiagramEmbedSchemeLink { from: string; fromPort: string; to: string; toPort: string; }
|
|
41
|
+
export interface DiagramEmbedScheme {
|
|
42
|
+
nodes: DiagramEmbedSchemeNode[];
|
|
43
|
+
links: DiagramEmbedSchemeLink[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DiagramEmbedHandle {
|
|
47
|
+
readonly host: HTMLElement;
|
|
48
|
+
readonly diagram: StockSharpDiagram;
|
|
49
|
+
readonly destroyed: boolean;
|
|
50
|
+
destroy(): void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface DiagramEmbedOptions {
|
|
54
|
+
onFullscreenRequested?: (
|
|
55
|
+
request: FullscreenRequestedPayload,
|
|
56
|
+
handle: DiagramEmbedHandle,
|
|
57
|
+
) => void;
|
|
58
|
+
onDestroyed?: (handle: DiagramEmbedHandle) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const PALETTE_URL = '/data/designer-palette.json';
|
|
62
|
+
const activeRenders = new WeakMap<HTMLElement, DiagramEmbedHandle>();
|
|
63
|
+
const connectedRenders = new Set<DiagramEmbedHandle>();
|
|
64
|
+
const renderRevisions = new WeakMap<HTMLElement, number>();
|
|
65
|
+
let disconnectedHostObserver: MutationObserver | null = null;
|
|
66
|
+
|
|
67
|
+
function iconUrl(name: string): string {
|
|
68
|
+
return name ? `/diagram-icons/${name}.svg` : '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Perceived luminance (0..255) of a #rrggbb colour; used to tell a light diagram canvas from a dark
|
|
72
|
+
// one so the (dark-canvas-tuned) link palette can be darkened when the canvas is light.
|
|
73
|
+
function luminance(hex: string): number {
|
|
74
|
+
const m = hex.match(/^#?([0-9a-fA-F]{6})$/);
|
|
75
|
+
if (m === null)
|
|
76
|
+
return 0;
|
|
77
|
+
const n = parseInt(m[1], 16);
|
|
78
|
+
return 0.299 * ((n >> 16) & 255) + 0.587 * ((n >> 8) & 255) + 0.114 * (n & 255);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function makePort(p: PalettePort): Port {
|
|
82
|
+
return new Port({
|
|
83
|
+
id: p.key,
|
|
84
|
+
name: p.name,
|
|
85
|
+
type: p.type,
|
|
86
|
+
maxLinks: p.maxLinks ?? 0,
|
|
87
|
+
availableTypes: p.availableTypes ?? [],
|
|
88
|
+
isDynamic: p.isDynamic ?? false,
|
|
89
|
+
dynamicMode: p.dynamicMode ?? '',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildCatalog(palette: Palette): StockSharpCatalog {
|
|
94
|
+
const catalog = new StockSharpCatalog();
|
|
95
|
+
|
|
96
|
+
for (const st of palette.socketTypes)
|
|
97
|
+
catalog.addPortType(new PortType({ name: st.name, color: st.color }));
|
|
98
|
+
|
|
99
|
+
for (const el of palette.elements) {
|
|
100
|
+
catalog.addNodeType(new Node({
|
|
101
|
+
id: el.typeId,
|
|
102
|
+
name: el.name,
|
|
103
|
+
groupName: el.groupName,
|
|
104
|
+
icon: iconUrl(el.icon),
|
|
105
|
+
inPorts: el.inPorts.map(makePort),
|
|
106
|
+
outPorts: el.outPorts.map(makePort),
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return catalog;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface ConvertedDiagramNodes {
|
|
114
|
+
nodes: DiagramNode[];
|
|
115
|
+
nodeErrors: Record<string, string>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function toDiagramNodes(
|
|
119
|
+
scheme: DiagramEmbedScheme,
|
|
120
|
+
catalog: StockSharpCatalog,
|
|
121
|
+
missingTypeMessage: (typeId: string) => string,
|
|
122
|
+
): ConvertedDiagramNodes {
|
|
123
|
+
const portOf = (p: Port) => ({ id: p.id, name: p.name, type: p.type, maxLinks: p.maxLinks });
|
|
124
|
+
const nodeErrors: Record<string, string> = {};
|
|
125
|
+
|
|
126
|
+
const usedIn = new Map<string, Set<string>>();
|
|
127
|
+
const usedOut = new Map<string, Set<string>>();
|
|
128
|
+
const add = (m: Map<string, Set<string>>, id: string, port: string) => {
|
|
129
|
+
(m.get(id) ?? m.set(id, new Set()).get(id)!).add(port);
|
|
130
|
+
};
|
|
131
|
+
for (const l of scheme.links) {
|
|
132
|
+
add(usedOut, l.from, l.fromPort);
|
|
133
|
+
add(usedIn, l.to, l.toPort);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const nodes = scheme.nodes.map((n) => {
|
|
137
|
+
const t = catalog.getNodeType(n.typeId);
|
|
138
|
+
const inPorts = t ? t.inPorts.map(portOf) : [];
|
|
139
|
+
const outPorts = t ? t.outPorts.map(portOf) : [];
|
|
140
|
+
const anchorType = (t?.inPorts.find((p) => p.isDynamic)?.type) ?? 'Any';
|
|
141
|
+
|
|
142
|
+
for (const key of usedIn.get(n.id) ?? [])
|
|
143
|
+
if (!inPorts.some((p) => p.id === key))
|
|
144
|
+
inPorts.push({ id: key, name: key, type: anchorType, maxLinks: 0 });
|
|
145
|
+
|
|
146
|
+
for (const key of usedOut.get(n.id) ?? [])
|
|
147
|
+
if (!outPorts.some((p) => p.id === key))
|
|
148
|
+
outPorts.push({ id: key, name: key, type: 'Any', maxLinks: 0 });
|
|
149
|
+
|
|
150
|
+
const data = new DiagramNode({ id: n.id, typeId: n.typeId, name: n.name, x: n.x, y: n.y, inPorts, outPorts });
|
|
151
|
+
|
|
152
|
+
if (t?.icon)
|
|
153
|
+
data.icon = t.icon;
|
|
154
|
+
|
|
155
|
+
if (!t) {
|
|
156
|
+
data.isPlaceholder = true;
|
|
157
|
+
nodeErrors[n.id] = missingTypeMessage(n.typeId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return data;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return { nodes, nodeErrors };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Walk the persisted Content.Value.Scheme.Model.{Nodes,Links} structure into
|
|
167
|
+
// the thin scheme. Defensive at every field so a partially
|
|
168
|
+
// broken document yields as many nodes as are readable; a wholly unusable one yields an empty scheme.
|
|
169
|
+
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 : '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parseRawScheme(raw: unknown): DiagramEmbedScheme {
|
|
182
|
+
const nodes: DiagramEmbedSchemeNode[] = [];
|
|
183
|
+
const links: DiagramEmbedSchemeLink[] = [];
|
|
184
|
+
|
|
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)
|
|
191
|
+
return { nodes, links };
|
|
192
|
+
|
|
193
|
+
for (const raw of (model.Nodes as Record<string, unknown>[] ?? [])) {
|
|
194
|
+
const key = raw?.Key;
|
|
195
|
+
if (typeof key !== 'string' || key.length === 0)
|
|
196
|
+
continue;
|
|
197
|
+
nodes.push({
|
|
198
|
+
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,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const raw of (model.Links as Record<string, unknown>[] ?? [])) {
|
|
207
|
+
const from = raw?.From;
|
|
208
|
+
const to = raw?.To;
|
|
209
|
+
if (typeof from !== 'string' || typeof to !== 'string')
|
|
210
|
+
continue;
|
|
211
|
+
links.push({
|
|
212
|
+
from,
|
|
213
|
+
fromPort: typeof raw.FromPort === 'string' ? raw.FromPort : '',
|
|
214
|
+
to,
|
|
215
|
+
toPort: typeof raw.ToPort === 'string' ? raw.ToPort : '',
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { nodes, links };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function renderScheme(
|
|
223
|
+
div: HTMLElement,
|
|
224
|
+
paletteUrl: string,
|
|
225
|
+
scheme: DiagramEmbedScheme,
|
|
226
|
+
options: DiagramEmbedOptions = {},
|
|
227
|
+
): Promise<DiagramEmbedHandle | null> {
|
|
228
|
+
return renderSchemeAtRevision(div, paletteUrl, scheme, beginRender(div), options);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function renderSchemeAtRevision(
|
|
232
|
+
div: HTMLElement,
|
|
233
|
+
paletteUrl: string,
|
|
234
|
+
scheme: DiagramEmbedScheme,
|
|
235
|
+
revision: number,
|
|
236
|
+
options: DiagramEmbedOptions,
|
|
237
|
+
): Promise<DiagramEmbedHandle | null> {
|
|
238
|
+
if (renderRevisions.get(div) !== revision)
|
|
239
|
+
return null;
|
|
240
|
+
const palette = (await fetch(paletteUrl).then((r) => r.json())) as Palette;
|
|
241
|
+
if (renderRevisions.get(div) !== revision)
|
|
242
|
+
return null;
|
|
243
|
+
const catalog = buildCatalog(palette);
|
|
244
|
+
|
|
245
|
+
disposeActiveRender(div);
|
|
246
|
+
div.classList.remove('ss-diagram-error');
|
|
247
|
+
div.replaceChildren();
|
|
248
|
+
let diagram: StockSharpDiagram;
|
|
249
|
+
try {
|
|
250
|
+
diagram = new StockSharpDiagram({ div, catalog });
|
|
251
|
+
} catch (error) {
|
|
252
|
+
div.replaceChildren();
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
const cleanups: Array<() => void> = [];
|
|
256
|
+
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
257
|
+
let destroyed = false;
|
|
258
|
+
const schedule = (callback: () => void, delay: number) => {
|
|
259
|
+
const timer = setTimeout(() => {
|
|
260
|
+
timers.delete(timer);
|
|
261
|
+
if (!destroyed) callback();
|
|
262
|
+
}, delay);
|
|
263
|
+
timers.add(timer);
|
|
264
|
+
};
|
|
265
|
+
const handle: DiagramEmbedHandle = {
|
|
266
|
+
host: div,
|
|
267
|
+
diagram,
|
|
268
|
+
get destroyed() { return destroyed; },
|
|
269
|
+
destroy() {
|
|
270
|
+
if (destroyed) return;
|
|
271
|
+
destroyed = true;
|
|
272
|
+
if (options.onDestroyed !== undefined) {
|
|
273
|
+
try { options.onDestroyed(handle); }
|
|
274
|
+
catch (error) { console.error(error); }
|
|
275
|
+
}
|
|
276
|
+
for (const timer of timers) clearTimeout(timer);
|
|
277
|
+
timers.clear();
|
|
278
|
+
for (const cleanup of cleanups.splice(0).reverse()) cleanup();
|
|
279
|
+
diagram.destroy();
|
|
280
|
+
connectedRenders.delete(handle);
|
|
281
|
+
if (activeRenders.get(div) === handle) {
|
|
282
|
+
activeRenders.delete(div);
|
|
283
|
+
delete div.dataset.rendered;
|
|
284
|
+
}
|
|
285
|
+
stopDisconnectedHostObserverWhenIdle();
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
activeRenders.set(div, handle);
|
|
289
|
+
connectedRenders.add(handle);
|
|
290
|
+
ensureDisconnectedHostObserver();
|
|
291
|
+
if (options.onFullscreenRequested !== undefined) {
|
|
292
|
+
const notifyHost = options.onFullscreenRequested;
|
|
293
|
+
cleanups.push(diagram.on('fullscreenRequested', (request) => notifyHost(request, handle)));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
// Follow the site theme: the canvas colour comes from the live --diagram-bg CSS token; a theme toggle
|
|
298
|
+
// repaints it. Nodes are self-contained light-grey boxes, so only the canvas behind them is themed.
|
|
299
|
+
const applyDiagramTheme = () => {
|
|
300
|
+
const cs = getComputedStyle(document.documentElement);
|
|
301
|
+
const bg = cs.getPropertyValue('--diagram-bg').trim() || '#1b1b1f';
|
|
302
|
+
const gridColor = cs.getPropertyValue('--diagram-grid').trim() || '#26262c';
|
|
303
|
+
diagram.setTheme({
|
|
304
|
+
diagramBackground: bg,
|
|
305
|
+
gridColor,
|
|
306
|
+
linkMaxLightness: luminance(bg) > 140 ? 0.42 : 1,
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
applyDiagramTheme();
|
|
310
|
+
const themeObserver = new MutationObserver(applyDiagramTheme);
|
|
311
|
+
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
312
|
+
cleanups.push(() => themeObserver.disconnect());
|
|
313
|
+
diagram.setReadOnly(true);
|
|
314
|
+
|
|
315
|
+
diagram.setLinkValidator(() => true);
|
|
316
|
+
diagram.setOverviewVisible(false);
|
|
317
|
+
const missingTypeTemplate = div.dataset.diagramMissingElement
|
|
318
|
+
?? 'Element type "{typeId}" is missing from the palette.';
|
|
319
|
+
const converted = toDiagramNodes(
|
|
320
|
+
scheme,
|
|
321
|
+
catalog,
|
|
322
|
+
(typeId) => missingTypeTemplate.replace('{typeId}', typeId || '(empty)'),
|
|
323
|
+
);
|
|
324
|
+
diagram.load(
|
|
325
|
+
converted.nodes,
|
|
326
|
+
scheme.links.map((link) => new Link({
|
|
327
|
+
outNode: link.from,
|
|
328
|
+
outPort: link.fromPort,
|
|
329
|
+
inNode: link.to,
|
|
330
|
+
inPort: link.toPort,
|
|
331
|
+
})),
|
|
332
|
+
{ nodeErrors: converted.nodeErrors },
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const fit = () => {
|
|
336
|
+
if (!div.isConnected) {
|
|
337
|
+
handle.destroy();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const w = div.clientWidth, h = div.clientHeight;
|
|
341
|
+
if (w < 2 || h < 2)
|
|
342
|
+
return;
|
|
343
|
+
diagram.resize(w, h);
|
|
344
|
+
diagram.zoomToFit();
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
schedule(fit, 0);
|
|
348
|
+
|
|
349
|
+
let scheduled = false;
|
|
350
|
+
const scheduleFit = (delay = 50) => {
|
|
351
|
+
if (scheduled) return;
|
|
352
|
+
scheduled = true;
|
|
353
|
+
schedule(() => { scheduled = false; fit(); }, delay);
|
|
354
|
+
};
|
|
355
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
356
|
+
const observer = new ResizeObserver(() => scheduleFit());
|
|
357
|
+
observer.observe(div);
|
|
358
|
+
cleanups.push(() => observer.disconnect());
|
|
359
|
+
} else {
|
|
360
|
+
const onResize = () => scheduleFit();
|
|
361
|
+
window.addEventListener('resize', onResize);
|
|
362
|
+
cleanups.push(() => window.removeEventListener('resize', onResize));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
div.dataset.rendered = '1';
|
|
366
|
+
return handle;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
handle.destroy();
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function destroyRenderedDiagram(div: HTMLElement): boolean {
|
|
374
|
+
beginRender(div);
|
|
375
|
+
return disposeActiveRender(div);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function disposeActiveRender(div: HTMLElement): boolean {
|
|
379
|
+
const handle = activeRenders.get(div);
|
|
380
|
+
if (handle === undefined) return false;
|
|
381
|
+
handle.destroy();
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function beginRender(div: HTMLElement): number {
|
|
386
|
+
const revision = (renderRevisions.get(div) ?? 0) + 1;
|
|
387
|
+
renderRevisions.set(div, revision);
|
|
388
|
+
return revision;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function ensureDisconnectedHostObserver(): void {
|
|
392
|
+
if (disconnectedHostObserver !== null || typeof MutationObserver === 'undefined') return;
|
|
393
|
+
disconnectedHostObserver = new MutationObserver(() => {
|
|
394
|
+
for (const handle of [...connectedRenders]) {
|
|
395
|
+
if (!handle.host.isConnected) handle.destroy();
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
disconnectedHostObserver.observe(document.documentElement, { childList: true, subtree: true });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function stopDisconnectedHostObserverWhenIdle(): void {
|
|
402
|
+
if (connectedRenders.size > 0 || disconnectedHostObserver === null) return;
|
|
403
|
+
disconnectedHostObserver.disconnect();
|
|
404
|
+
disconnectedHostObserver = null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function note(div: HTMLElement, message: string, revision: number): void {
|
|
408
|
+
if (renderRevisions.get(div) !== revision) return;
|
|
409
|
+
disposeActiveRender(div);
|
|
410
|
+
div.textContent = message;
|
|
411
|
+
div.classList.add('ss-diagram-error');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Fetch the raw schema JSON from srcUrl, transform it and draw it. Every failure mode -- an
|
|
415
|
+
// unreachable source, a non-JSON / malformed body, or an empty schema -- degrades to a short inline note
|
|
416
|
+
// instead of throwing, so a bad @diagram never breaks the surrounding page.
|
|
417
|
+
export async function renderFromSource(
|
|
418
|
+
div: HTMLElement,
|
|
419
|
+
paletteUrl: string,
|
|
420
|
+
srcUrl: string,
|
|
421
|
+
options: DiagramEmbedOptions = {},
|
|
422
|
+
): Promise<DiagramEmbedHandle | null> {
|
|
423
|
+
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;
|
|
426
|
+
|
|
427
|
+
let raw: unknown;
|
|
428
|
+
try {
|
|
429
|
+
const resp = await fetch(srcUrl);
|
|
430
|
+
if (!resp.ok) { note(div, errLoad, revision); return null; }
|
|
431
|
+
const text = (await resp.text()).replace(/^/, '');
|
|
432
|
+
raw = JSON.parse(text);
|
|
433
|
+
} catch {
|
|
434
|
+
note(div, errLoad, revision);
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const scheme = parseRawScheme(raw);
|
|
439
|
+
if (scheme.nodes.length === 0) {
|
|
440
|
+
note(div, errEmpty, revision);
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
return await renderSchemeAtRevision(div, paletteUrl, scheme, revision, options);
|
|
446
|
+
} catch {
|
|
447
|
+
note(div, errDraw, revision);
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Draw a host whose schema JSON is embedded inline (a
|
|
453
|
+
// <script type="application/json"> child holds the schema). Same degradation as renderFromSource: a
|
|
454
|
+
// malformed/empty schema shows an inline note instead of throwing.
|
|
455
|
+
export async function renderFromInline(
|
|
456
|
+
div: HTMLElement,
|
|
457
|
+
paletteUrl: string,
|
|
458
|
+
json: string,
|
|
459
|
+
options: DiagramEmbedOptions = {},
|
|
460
|
+
): Promise<DiagramEmbedHandle | null> {
|
|
461
|
+
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;
|
|
464
|
+
|
|
465
|
+
let raw: unknown;
|
|
466
|
+
try {
|
|
467
|
+
raw = JSON.parse(json);
|
|
468
|
+
} catch {
|
|
469
|
+
note(div, errEmpty, revision);
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const scheme = parseRawScheme(raw);
|
|
474
|
+
if (scheme.nodes.length === 0) {
|
|
475
|
+
note(div, errEmpty, revision);
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
return await renderSchemeAtRevision(div, paletteUrl, scheme, revision, options);
|
|
481
|
+
} catch {
|
|
482
|
+
note(div, errDraw, revision);
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Render every not-yet-rendered diagram host under root (default: the whole document). Callable again after
|
|
488
|
+
// dynamic HTML is injected (e.g. the editor preview) to draw hosts that just appeared. A host either points
|
|
489
|
+
// at a source URL (data-diagram-src) or embeds its schema JSON inline (a <script type="application/json">).
|
|
490
|
+
export function renderAll(root: ParentNode = document, options: DiagramEmbedOptions = {}): void {
|
|
491
|
+
root.querySelectorAll<HTMLElement>('.ss-diagram-host').forEach((host) => {
|
|
492
|
+
if (host.dataset.rendered === '1')
|
|
493
|
+
return;
|
|
494
|
+
|
|
495
|
+
const src = host.dataset.diagramSrc;
|
|
496
|
+
if (src) {
|
|
497
|
+
host.dataset.rendered = '1';
|
|
498
|
+
void renderFromSource(host, PALETTE_URL, src, options);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const inline = host.querySelector('script[type="application/json"]');
|
|
503
|
+
if (inline) {
|
|
504
|
+
host.dataset.rendered = '1';
|
|
505
|
+
void renderFromInline(host, PALETTE_URL, inline.textContent ?? '', options);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
}
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Shared localization bridge. A host can provide localized values through
|
|
2
|
+
// `window.__designerI18n`; every module reads them through `t(key, fallback)`.
|
|
3
|
+
// The English fallback prevents a missing or stale key from rendering blank.
|
|
4
|
+
|
|
5
|
+
export interface DesignerI18n {
|
|
6
|
+
// Panel titles (GoldenLayout tabs).
|
|
7
|
+
panelStrategies?: string;
|
|
8
|
+
panelPalette?: string;
|
|
9
|
+
panelProperties?: string;
|
|
10
|
+
panelDiagram?: string;
|
|
11
|
+
panelOutput?: string;
|
|
12
|
+
panelErrors?: string;
|
|
13
|
+
panelCode?: string;
|
|
14
|
+
panelBacktest?: string;
|
|
15
|
+
panelOptimizer?: string;
|
|
16
|
+
panelLive?: string;
|
|
17
|
+
liveNoRuns?: string;
|
|
18
|
+
selectAnElement?: string;
|
|
19
|
+
noErrors?: string;
|
|
20
|
+
|
|
21
|
+
// Solution explorer.
|
|
22
|
+
strategies?: string;
|
|
23
|
+
noStrategiesYet?: string;
|
|
24
|
+
rename?: string;
|
|
25
|
+
newStrategy?: string;
|
|
26
|
+
refresh?: string;
|
|
27
|
+
run?: string;
|
|
28
|
+
stop?: string;
|
|
29
|
+
newIndicator?: string;
|
|
30
|
+
newComposite?: string;
|
|
31
|
+
newCodeStrategy?: string;
|
|
32
|
+
codeLanguagePrompt?: string;
|
|
33
|
+
groupStrategies?: string;
|
|
34
|
+
groupComposites?: string;
|
|
35
|
+
groupIndicators?: string;
|
|
36
|
+
noComposites?: string;
|
|
37
|
+
noIndicators?: string;
|
|
38
|
+
groupOptimizations?: string;
|
|
39
|
+
groupLive?: string;
|
|
40
|
+
optReady?: string;
|
|
41
|
+
optNoStrategy?: string;
|
|
42
|
+
btReady?: string;
|
|
43
|
+
btNoStrategy?: string;
|
|
44
|
+
noOptimizations?: string;
|
|
45
|
+
newOptimization?: string;
|
|
46
|
+
optimize?: string;
|
|
47
|
+
duplicate?: string;
|
|
48
|
+
delete?: string;
|
|
49
|
+
create?: string;
|
|
50
|
+
deleteStrategyTitle?: string;
|
|
51
|
+
deleteStrategyMsg?: string;
|
|
52
|
+
ctxExport?: string;
|
|
53
|
+
encryptTitle?: string;
|
|
54
|
+
encryptLabel?: string;
|
|
55
|
+
importEncTitle?: string;
|
|
56
|
+
importEncLabel?: string;
|
|
57
|
+
importEncRetry?: string;
|
|
58
|
+
|
|
59
|
+
// Property grid.
|
|
60
|
+
openDocumentation?: string;
|
|
61
|
+
selectPlaceholder?: string;
|
|
62
|
+
notANumber?: string;
|
|
63
|
+
expectedTimeSpan?: string;
|
|
64
|
+
basicSettings?: string;
|
|
65
|
+
advancedSettings?: string;
|
|
66
|
+
noParameters?: string;
|
|
67
|
+
noBasicParameters?: string;
|
|
68
|
+
noneOption?: string;
|
|
69
|
+
loadingParameters?: string;
|
|
70
|
+
|
|
71
|
+
// Modals / generic.
|
|
72
|
+
loading?: string;
|
|
73
|
+
cancel?: string;
|
|
74
|
+
|
|
75
|
+
// Discard-confirm dialog.
|
|
76
|
+
discardTitle?: string;
|
|
77
|
+
discardOpenMsg?: string;
|
|
78
|
+
discardCreateMsg?: string;
|
|
79
|
+
discardOpenOk?: string;
|
|
80
|
+
discardCreateOk?: string;
|
|
81
|
+
|
|
82
|
+
// Node / diagram authoring.
|
|
83
|
+
nodeMessage?: string;
|
|
84
|
+
nodeName?: string;
|
|
85
|
+
strategyNamePrompt?: string;
|
|
86
|
+
untitledStrategy?: string;
|
|
87
|
+
annotationPlaceholder?: string;
|
|
88
|
+
searchPlaceholder?: string;
|
|
89
|
+
noMatches?: string;
|
|
90
|
+
|
|
91
|
+
// Diagram context menu.
|
|
92
|
+
undo?: string;
|
|
93
|
+
redo?: string;
|
|
94
|
+
cut?: string;
|
|
95
|
+
copy?: string;
|
|
96
|
+
paste?: string;
|
|
97
|
+
ctxOpen?: string;
|
|
98
|
+
properties?: string;
|
|
99
|
+
ctxHelp?: string;
|
|
100
|
+
collapse?: string;
|
|
101
|
+
expand?: string;
|
|
102
|
+
|
|
103
|
+
// Sign-in popup + run-action gating.
|
|
104
|
+
fillEmailPassword?: string;
|
|
105
|
+
signingIn?: string;
|
|
106
|
+
loginFailed?: string;
|
|
107
|
+
connectionError?: string;
|
|
108
|
+
signInToBacktest?: string;
|
|
109
|
+
signInToExport?: string;
|
|
110
|
+
signInToOptimize?: string;
|
|
111
|
+
signInToRunLive?: string;
|
|
112
|
+
|
|
113
|
+
// Strategy-level properties.
|
|
114
|
+
statusLabel?: string;
|
|
115
|
+
statusSaved?: string;
|
|
116
|
+
statusModified?: string;
|
|
117
|
+
|
|
118
|
+
// Backtest / optimizer chrome.
|
|
119
|
+
loadingEllipsis?: string;
|
|
120
|
+
failed?: string;
|
|
121
|
+
optGrid?: string;
|
|
122
|
+
optHeatmap?: string;
|
|
123
|
+
opt3d?: string;
|
|
124
|
+
optPickRow?: string;
|
|
125
|
+
|
|
126
|
+
// Backtest/optimizer statistic names (StatisticParameterTypes), shown in the
|
|
127
|
+
// backtest stats grid and the optimizer result columns / chart axes.
|
|
128
|
+
stat_winning_trades?: string;
|
|
129
|
+
stat_trade_count?: string;
|
|
130
|
+
stat_roundtrip_count?: string;
|
|
131
|
+
stat_avg_trade_profit?: string;
|
|
132
|
+
stat_avg_win?: string;
|
|
133
|
+
stat_avg_loss?: string;
|
|
134
|
+
stat_losing_trades?: string;
|
|
135
|
+
stat_max_long_position?: string;
|
|
136
|
+
stat_max_short_position?: string;
|
|
137
|
+
stat_max_profit?: string;
|
|
138
|
+
stat_max_drawdown?: string;
|
|
139
|
+
stat_max_relative_drawdown?: string;
|
|
140
|
+
stat_return?: string;
|
|
141
|
+
stat_recovery_factor?: string;
|
|
142
|
+
stat_net_profit?: string;
|
|
143
|
+
stat_max_latency_reg?: string;
|
|
144
|
+
stat_max_latency_cancel?: string;
|
|
145
|
+
stat_min_latency_reg?: string;
|
|
146
|
+
stat_min_latency_cancel?: string;
|
|
147
|
+
stat_order_count?: string;
|
|
148
|
+
stat_order_error_count?: string;
|
|
149
|
+
stat_insufficient_fund_errors?: string;
|
|
150
|
+
stat_trades_per_month?: string;
|
|
151
|
+
stat_trades_per_day?: string;
|
|
152
|
+
stat_max_drawdown_date?: string;
|
|
153
|
+
stat_max_profit_date?: string;
|
|
154
|
+
stat_commission?: string;
|
|
155
|
+
stat_max_drawdown_percent?: string;
|
|
156
|
+
stat_net_profit_percent?: string;
|
|
157
|
+
stat_sharpe_ratio?: string;
|
|
158
|
+
stat_sortino_ratio?: string;
|
|
159
|
+
stat_profit_factor?: string;
|
|
160
|
+
stat_expectancy?: string;
|
|
161
|
+
stat_calmar_ratio?: string;
|
|
162
|
+
stat_sterling_ratio?: string;
|
|
163
|
+
stat_avg_drawdown?: string;
|
|
164
|
+
stat_order_cancel_errors?: string;
|
|
165
|
+
stat_gross_loss?: string;
|
|
166
|
+
stat_gross_profit?: string;
|
|
167
|
+
stat_max_profit_percent?: string;
|
|
168
|
+
stat_sharpe?: string; // short grid-header form
|
|
169
|
+
stat_trades?: string; // short grid-header form
|
|
170
|
+
|
|
171
|
+
// Backtest run status / errors.
|
|
172
|
+
btstat_starting?: string;
|
|
173
|
+
btstat_done?: string;
|
|
174
|
+
btstat_stopped?: string;
|
|
175
|
+
btstat_failed?: string;
|
|
176
|
+
bterr_chart_runtime_missing?: string;
|
|
177
|
+
|
|
178
|
+
// Order side labels.
|
|
179
|
+
order_buy?: string;
|
|
180
|
+
order_sell?: string;
|
|
181
|
+
|
|
182
|
+
// Log level names.
|
|
183
|
+
loglevel_inherit?: string;
|
|
184
|
+
loglevel_verbose?: string;
|
|
185
|
+
loglevel_debug?: string;
|
|
186
|
+
loglevel_info?: string;
|
|
187
|
+
loglevel_warning?: string;
|
|
188
|
+
loglevel_error?: string;
|
|
189
|
+
loglevel_off?: string;
|
|
190
|
+
|
|
191
|
+
// Optimizer status / errors / messages.
|
|
192
|
+
opt_no_params?: string;
|
|
193
|
+
opt_bool_values?: string;
|
|
194
|
+
optstat_starting?: string;
|
|
195
|
+
optstat_done?: string;
|
|
196
|
+
optstat_stopped?: string;
|
|
197
|
+
optstat_failed?: string;
|
|
198
|
+
opterr_plotly_missing?: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const i18n: DesignerI18n =
|
|
202
|
+
typeof window === 'undefined'
|
|
203
|
+
? {}
|
|
204
|
+
: (window as unknown as { __designerI18n?: DesignerI18n }).__designerI18n ?? {};
|
|
205
|
+
|
|
206
|
+
/// Localized string for `key`, or `fallback` (English) when the host
|
|
207
|
+
/// didn't provide it.
|
|
208
|
+
export function t(key: keyof DesignerI18n, fallback: string): string {
|
|
209
|
+
return i18n[key] ?? fallback;
|
|
210
|
+
}
|