@particle-academy/fancy-echarts 2.0.2 → 3.0.1

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/README.md CHANGED
@@ -73,6 +73,194 @@ option={{
73
73
 
74
74
  This is consumer responsibility — the wrapper does not introspect `option` to identify HTML-bearing fields.
75
75
 
76
+ ## Recipes: ECharts × react-fancy
77
+
78
+ `fancy-echarts` is a thin wrapper — every interaction surface is reachable through `onEvents`, `useECharts().instance`, and `tooltip.formatter`. These recipes cover the patterns that hold up across charts.
79
+
80
+ ### Wrap any chart with a Popover, ContextMenu, and Action button
81
+
82
+ The pattern that scales: a single `ChartFrame` wrapper that gives every chart an info popover next to the title, action buttons in the header, and a right-click context menu on the body. Build it once, reuse for every chart type:
83
+
84
+ ```tsx
85
+ import { Card, Popover, Action, Badge, ContextMenu, Icon, useToast } from "@particle-academy/react-fancy";
86
+
87
+ function ChartFrame({ title, info, actions = [], onExport, extraMenu, children }) {
88
+ const { toast } = useToast();
89
+ return (
90
+ <Card>
91
+ <Card.Header>
92
+ <div className="flex items-center justify-between">
93
+ <div className="flex items-center gap-2">
94
+ <h3 className="font-semibold">{title}</h3>
95
+ <Popover hover placement="right">
96
+ <Popover.Trigger>
97
+ <button><Icon name="info" size="sm" /></button>
98
+ </Popover.Trigger>
99
+ <Popover.Content>
100
+ <p className="w-64 text-sm text-zinc-500">{info}</p>
101
+ </Popover.Content>
102
+ </Popover>
103
+ </div>
104
+ <div className="flex gap-2">
105
+ {actions.map((a) => <Action key={a.label} size="sm" onClick={a.onClick}>{a.label}</Action>)}
106
+ </div>
107
+ </div>
108
+ </Card.Header>
109
+ <Card.Body>
110
+ <ContextMenu>
111
+ <ContextMenu.Trigger><div>{children}</div></ContextMenu.Trigger>
112
+ <ContextMenu.Content>
113
+ <ContextMenu.Item onClick={onExport}>Export CSV</ContextMenu.Item>
114
+ <ContextMenu.Item onClick={() => toast({ title: "Exported PNG" })}>Export PNG</ContextMenu.Item>
115
+ {extraMenu && <ContextMenu.Separator />}
116
+ {extraMenu}
117
+ </ContextMenu.Content>
118
+ </ContextMenu>
119
+ </Card.Body>
120
+ </Card>
121
+ );
122
+ }
123
+
124
+ <ChartFrame title="Revenue" info="..." actions={[{ label: "Forecast", onClick: ... }]}>
125
+ <EChart option={lineOption} style={{ height: 320 }} />
126
+ </ChartFrame>
127
+ ```
128
+
129
+ Three reasons this works: `<ContextMenu.Trigger>` wraps the chart canvas without injecting DOM into ECharts, `Popover hover` is independent of any chart event, and the chart inside is unaware of the frame.
130
+
131
+ ### Right-click a data point → open a Modal drill-down
132
+
133
+ `onEvents.contextmenu` fires per-datum with the same `params` shape as `click`. Call `params.event.event.preventDefault()` to suppress the browser menu, then drive your own state.
134
+
135
+ ```tsx
136
+ <EChart
137
+ option={barOption}
138
+ onEvents={{
139
+ contextmenu: (params) => {
140
+ params.event.event.preventDefault();
141
+ const region = regions.find((r) => r.name === params.name);
142
+ setDrill(region); // opens a <Modal>
143
+ },
144
+ }}
145
+ />
146
+ ```
147
+
148
+ If you also wrap the chart in `<ContextMenu>` (chart-level actions), this still works — the per-bar `contextmenu` event fires *and* the wrapper menu opens. Suppress one or the other based on which event the cursor was over.
149
+
150
+ ### Click-to-toast and hover-popover on a slice
151
+
152
+ `onEvents.click` for actions, native ECharts `tooltip` for hover details — they don't conflict.
153
+
154
+ ```tsx
155
+ <EChart
156
+ option={{
157
+ tooltip: { trigger: "item", formatter: "{b}<br/>${c}k ({d}%)" },
158
+ series: [{ type: "pie", data: categories }],
159
+ }}
160
+ onEvents={{
161
+ click: (p) => toast({ title: `${p.name}: ${p.percent}%`, variant: "info" }),
162
+ }}
163
+ />
164
+ ```
165
+
166
+ ### Rich HTML tooltips with sanitized data
167
+
168
+ `tooltip.formatter` accepts a function returning an HTML string. Compose colored arrows, badges, and metadata — but always escape user input.
169
+
170
+ ```tsx
171
+ tooltip: {
172
+ trigger: "axis",
173
+ formatter: (params) => {
174
+ const p = params[0];
175
+ const region = regions.find((r) => r.name === p.name);
176
+ const arrow = region.growth >= 0 ? "▲" : "▼";
177
+ const color = region.growth >= 15 ? "#10b981" : "#3b82f6";
178
+ return `<div style="font-weight:600">${escape(p.name)}</div>
179
+ <div>Revenue: <b>$${p.value.toLocaleString()}k</b></div>
180
+ <div style="color:${color}">${arrow} ${region.growth}% YoY</div>`;
181
+ },
182
+ }
183
+ ```
184
+
185
+ For interactive content inside a tooltip (buttons that fire React state), use a `Popover` keyed off `onEvents.mouseover` instead — ECharts tooltips are detached HTML and lose React handlers.
186
+
187
+ ### Per-datum styling from an array of objects
188
+
189
+ Pass `data` as `{ value, itemStyle }[]` to color each bar/slice individually based on a property of the underlying record:
190
+
191
+ ```tsx
192
+ series: [{
193
+ type: "bar",
194
+ data: regions.map((r) => ({
195
+ value: r.value,
196
+ itemStyle: {
197
+ color: r.growth >= 15 ? "#10b981" : "#3b82f6",
198
+ borderRadius: [0, 6, 6, 0],
199
+ },
200
+ })),
201
+ }]
202
+ ```
203
+
204
+ ### Wrap the page in `<Toast.Provider>` for toast feedback
205
+
206
+ `useToast()` only works inside a provider. Wrap the demo's outermost element so chart-event toasts have somewhere to render:
207
+
208
+ ```tsx
209
+ export function Showcase() {
210
+ return (
211
+ <Toast.Provider position="bottom-right">
212
+ <ShowcaseInner />
213
+ </Toast.Provider>
214
+ );
215
+ }
216
+ ```
217
+
218
+ ### Keep the chart `option` memoized
219
+
220
+ Re-creating the option object on every render forces ECharts to diff and reapply. `useMemo` keeps the reference stable so the chart only updates when its inputs actually change:
221
+
222
+ ```tsx
223
+ const lineOption = useMemo(() => ({ /* ... */ }), [revenue, expenses]);
224
+ <EChart option={lineOption} />
225
+ ```
226
+
227
+ ### Theme-toggling is a prop swap
228
+
229
+ Pass `theme="dark-preset"` (after `registerBuiltinThemes()`) or `theme="light"` and re-render — the wrapper rebuilds the chart with the new theme automatically. No `dispose()` calls needed.
230
+
231
+ ```tsx
232
+ const [theme, setTheme] = useState<"light" | "dark-preset">("light");
233
+
234
+ <Action onClick={() => setTheme((t) => t === "light" ? "dark-preset" : "light")}>
235
+ Toggle theme
236
+ </Action>
237
+ <EChart theme={theme} option={option} />
238
+ ```
239
+
240
+ ### Pitfalls
241
+
242
+ - **Badge takes `color`, not `variant`.** Valid colors: `zinc | red | blue | green | amber | violet | rose`. The `variant` prop selects the *style* (`soft | solid | outline`), not the semantic intent. Don't confuse this with `Toast.toast({ variant: "success" })`, which uses semantic names.
243
+ - **`<ContextMenu.Trigger>` needs a single DOM child.** Wrap `<EChart>` in a plain `<div>` if you have additional siblings (or none — Trigger forwards refs through the wrapper).
244
+ - **`params.event` vs `params.event.event`.** ECharts wraps the native event. Call `params.event.event.preventDefault()` to stop the browser context menu, not `params.event.preventDefault()`.
245
+ - **Don't render React inside `tooltip.formatter`.** Returning an HTML string is fine; expecting React state or handlers to attach to that HTML is not. For interactive tooltips, use `Popover` driven by `onEvents.mouseover` / `mouseout` instead.
246
+
247
+ ## Diagrams
248
+
249
+ Beyond charts, fancy-echarts ships four schema-driven diagram components for data-modeling, process flows, mindmapping, and hierarchies. They share one routing/marker engine — same import surface, same theming.
250
+
251
+ ```tsx
252
+ import { DataDiagram, Flowchart, Mindmap, OrgChart } from "@particle-academy/fancy-echarts";
253
+ ```
254
+
255
+ | Component | Use case | Default routing |
256
+ |-----------|----------|-----------------|
257
+ | `<DataDiagram>` | ERD / UML class diagrams with fields, primary/foreign keys, exports | manhattan |
258
+ | `<Flowchart>` | Boxes + typed arrows, no fields | manhattan |
259
+ | `<Mindmap>` | Radial single-root tree with bezier connectors | bezier |
260
+ | `<OrgChart>` | Top-down hierarchy, tidy-tree layout, inheritance markers | manhattan |
261
+
262
+ See [docs/Diagram.md](docs/Diagram.md) for schemas, props, and layout details.
263
+
76
264
  ## Documentation
77
265
 
78
266
  Full component documentation is available in the [docs/](docs/) folder:
@@ -82,6 +270,7 @@ Full component documentation is available in the [docs/](docs/) folder:
82
270
  | [EChart](docs/EChart.md) | Base chart component + all 20 series sub-components |
83
271
  | [EChart3D](docs/EChart3D.md) | 3D charts (Bar, Scatter, Line, Surface, Globe) |
84
272
  | [EChartGraphic](docs/EChartGraphic.md) | Custom drawing with the graphic API |
273
+ | [Diagram](docs/Diagram.md) | Diagram engine + DataDiagram, Flowchart, Mindmap, OrgChart presets |
85
274
  | [useECharts](docs/useECharts.md) | Core hook for custom integrations |
86
275
  | [Registration](docs/registration.md) | Tree shaking and selective chart registration |
87
276
  | [Themes](docs/themes.md) | Built-in themes and custom theme creation |
@@ -0,0 +1,277 @@
1
+ // src/components/Diagram/diagram.serializers.ts
2
+ function serializeToERD(schema) {
3
+ const lines = [];
4
+ for (const entity of schema.entities) {
5
+ lines.push(`[${entity.name}]`);
6
+ if (entity.fields) {
7
+ for (const field of entity.fields) {
8
+ const parts = [` ${field.name}`];
9
+ if (field.type) parts.push(field.type);
10
+ if (field.primary) parts.push("PK");
11
+ if (field.foreign) parts.push("FK");
12
+ if (field.nullable) parts.push("?");
13
+ lines.push(parts.join(" "));
14
+ }
15
+ }
16
+ lines.push("");
17
+ }
18
+ for (const rel of schema.relations) {
19
+ const fromEntity = schema.entities.find((e) => e.id === rel.from);
20
+ const toEntity = schema.entities.find((e) => e.id === rel.to);
21
+ if (!fromEntity || !toEntity) continue;
22
+ const marker = getERDMarker(rel.type);
23
+ const parts = [fromEntity.name, marker, toEntity.name];
24
+ if (rel.label) parts.push(`: ${rel.label}`);
25
+ lines.push(parts.join(" "));
26
+ }
27
+ return lines.join("\n").trim();
28
+ }
29
+ function getERDMarker(type) {
30
+ switch (type) {
31
+ case "one-to-one":
32
+ return "1--1";
33
+ case "one-to-many":
34
+ return "1--*";
35
+ case "many-to-many":
36
+ return "*--*";
37
+ default:
38
+ return "--";
39
+ }
40
+ }
41
+ function serializeToUML(schema) {
42
+ const lines = ["@startuml"];
43
+ for (const entity of schema.entities) {
44
+ lines.push(`class ${entity.name} {`);
45
+ if (entity.fields) {
46
+ for (const field of entity.fields) {
47
+ const typeStr = field.type ?? "any";
48
+ const nullable = field.nullable ? "?" : "";
49
+ const stereotype = field.primary ? " <<PK>>" : field.foreign ? " <<FK>>" : "";
50
+ lines.push(` ${field.name} : ${typeStr}${nullable}${stereotype}`);
51
+ }
52
+ }
53
+ lines.push("}");
54
+ lines.push("");
55
+ }
56
+ for (const rel of schema.relations) {
57
+ const fromEntity = schema.entities.find((e) => e.id === rel.from);
58
+ const toEntity = schema.entities.find((e) => e.id === rel.to);
59
+ if (!fromEntity || !toEntity) continue;
60
+ const arrow = getUMLArrow(rel.type);
61
+ const label = rel.label ? ` : ${rel.label}` : "";
62
+ lines.push(`${fromEntity.name} ${arrow} ${toEntity.name}${label}`);
63
+ }
64
+ lines.push("@enduml");
65
+ return lines.join("\n");
66
+ }
67
+ function getUMLArrow(type) {
68
+ switch (type) {
69
+ case "one-to-one":
70
+ return '"1" -- "1"';
71
+ case "one-to-many":
72
+ return '"1" -- "*"';
73
+ case "many-to-many":
74
+ return '"*" -- "*"';
75
+ default:
76
+ return "--";
77
+ }
78
+ }
79
+ function serializeToDFD(schema) {
80
+ const lines = [];
81
+ for (const entity of schema.entities) {
82
+ lines.push(`entity ${entity.name}`);
83
+ }
84
+ lines.push("");
85
+ for (const rel of schema.relations) {
86
+ const fromEntity = schema.entities.find((e) => e.id === rel.from);
87
+ const toEntity = schema.entities.find((e) => e.id === rel.to);
88
+ if (!fromEntity || !toEntity) continue;
89
+ const label = rel.label ? ` "${rel.label}"` : "";
90
+ lines.push(`${fromEntity.name} -> ${toEntity.name}${label}`);
91
+ }
92
+ return lines.join("\n").trim();
93
+ }
94
+ function deserializeSchema(input, format) {
95
+ switch (format) {
96
+ case "erd":
97
+ return deserializeERD(input);
98
+ case "uml":
99
+ return deserializeUML(input);
100
+ case "dfd":
101
+ return deserializeDFD(input);
102
+ }
103
+ }
104
+ function deserializeERD(input) {
105
+ const entities = [];
106
+ const relations = [];
107
+ const lines = input.split("\n");
108
+ let currentEntity = null;
109
+ for (const rawLine of lines) {
110
+ const line = rawLine.trim();
111
+ const entityMatch = line.match(/^\[(.+)\]$/);
112
+ if (entityMatch) {
113
+ currentEntity = {
114
+ id: entityMatch[1].toLowerCase().replace(/\s+/g, "_"),
115
+ name: entityMatch[1],
116
+ fields: []
117
+ };
118
+ entities.push(currentEntity);
119
+ continue;
120
+ }
121
+ if (currentEntity && rawLine.startsWith(" ") && line.length > 0) {
122
+ const parts = line.split(/\s+/);
123
+ const field = { name: parts[0] };
124
+ if (parts.length > 1 && !["PK", "FK", "?"].includes(parts[1])) {
125
+ field.type = parts[1];
126
+ }
127
+ if (parts.includes("PK")) field.primary = true;
128
+ if (parts.includes("FK")) field.foreign = true;
129
+ if (parts.includes("?")) field.nullable = true;
130
+ currentEntity.fields.push(field);
131
+ continue;
132
+ }
133
+ const relMatch = line.match(
134
+ /^(\S+)\s+(1--1|1--\*|\*--\*)\s+(\S+)(?:\s*:\s*(.+))?$/
135
+ );
136
+ if (relMatch) {
137
+ currentEntity = null;
138
+ const fromName = relMatch[1];
139
+ const marker = relMatch[2];
140
+ const toName = relMatch[3];
141
+ const label = relMatch[4];
142
+ const fromEntity = entities.find((e) => e.name === fromName);
143
+ const toEntity = entities.find((e) => e.name === toName);
144
+ if (fromEntity && toEntity) {
145
+ relations.push({
146
+ id: `${fromEntity.id ?? fromEntity.name}_${toEntity.id ?? toEntity.name}`,
147
+ from: fromEntity.id ?? fromEntity.name,
148
+ to: toEntity.id ?? toEntity.name,
149
+ type: parseERDMarker(marker),
150
+ label
151
+ });
152
+ }
153
+ continue;
154
+ }
155
+ if (line === "") {
156
+ currentEntity = null;
157
+ }
158
+ }
159
+ return { entities, relations };
160
+ }
161
+ function parseERDMarker(marker) {
162
+ switch (marker) {
163
+ case "1--1":
164
+ return "one-to-one";
165
+ case "1--*":
166
+ return "one-to-many";
167
+ case "*--*":
168
+ return "many-to-many";
169
+ default:
170
+ return "one-to-many";
171
+ }
172
+ }
173
+ function deserializeUML(input) {
174
+ const entities = [];
175
+ const relations = [];
176
+ const lines = input.split("\n");
177
+ let currentEntity = null;
178
+ for (const rawLine of lines) {
179
+ const line = rawLine.trim();
180
+ if (line === "@startuml" || line === "@enduml" || line === "") continue;
181
+ const classMatch = line.match(/^class\s+(\S+)\s*\{$/);
182
+ if (classMatch) {
183
+ currentEntity = {
184
+ id: classMatch[1].toLowerCase().replace(/\s+/g, "_"),
185
+ name: classMatch[1],
186
+ fields: []
187
+ };
188
+ entities.push(currentEntity);
189
+ continue;
190
+ }
191
+ if (line === "}") {
192
+ currentEntity = null;
193
+ continue;
194
+ }
195
+ if (currentEntity) {
196
+ const fieldMatch = line.match(
197
+ /^(\S+)\s*:\s*(\S+?)(\?)?(?:\s*<<(PK|FK)>>)?$/
198
+ );
199
+ if (fieldMatch) {
200
+ const field = {
201
+ name: fieldMatch[1],
202
+ type: fieldMatch[2]
203
+ };
204
+ if (fieldMatch[3]) field.nullable = true;
205
+ if (fieldMatch[4] === "PK") field.primary = true;
206
+ if (fieldMatch[4] === "FK") field.foreign = true;
207
+ currentEntity.fields.push(field);
208
+ }
209
+ continue;
210
+ }
211
+ const relMatch = line.match(
212
+ /^(\S+)\s+"([1*])"\s+--\s+"([1*])"\s+(\S+)(?:\s*:\s*(.+))?$/
213
+ );
214
+ if (relMatch) {
215
+ const fromName = relMatch[1];
216
+ const fromCard = relMatch[2];
217
+ const toCard = relMatch[3];
218
+ const toName = relMatch[4];
219
+ const label = relMatch[5];
220
+ const fromEntity = entities.find((e) => e.name === fromName);
221
+ const toEntity = entities.find((e) => e.name === toName);
222
+ if (fromEntity && toEntity) {
223
+ const type = fromCard === "1" && toCard === "1" ? "one-to-one" : fromCard === "1" && toCard === "*" ? "one-to-many" : "many-to-many";
224
+ relations.push({
225
+ id: `${fromEntity.id ?? fromEntity.name}_${toEntity.id ?? toEntity.name}`,
226
+ from: fromEntity.id ?? fromEntity.name,
227
+ to: toEntity.id ?? toEntity.name,
228
+ type,
229
+ label
230
+ });
231
+ }
232
+ }
233
+ }
234
+ return { entities, relations };
235
+ }
236
+ function deserializeDFD(input) {
237
+ const entities = [];
238
+ const relations = [];
239
+ const lines = input.split("\n");
240
+ for (const rawLine of lines) {
241
+ const line = rawLine.trim();
242
+ if (line === "") continue;
243
+ const entityMatch = line.match(/^entity\s+(\S+)$/);
244
+ if (entityMatch) {
245
+ entities.push({
246
+ id: entityMatch[1].toLowerCase().replace(/\s+/g, "_"),
247
+ name: entityMatch[1],
248
+ fields: []
249
+ });
250
+ continue;
251
+ }
252
+ const flowMatch = line.match(
253
+ /^(\S+)\s+->\s+(\S+)(?:\s+"(.+)")?$/
254
+ );
255
+ if (flowMatch) {
256
+ const fromName = flowMatch[1];
257
+ const toName = flowMatch[2];
258
+ const label = flowMatch[3];
259
+ const fromEntity = entities.find((e) => e.name === fromName);
260
+ const toEntity = entities.find((e) => e.name === toName);
261
+ if (fromEntity && toEntity) {
262
+ relations.push({
263
+ id: `${fromEntity.id ?? fromEntity.name}_${toEntity.id ?? toEntity.name}`,
264
+ from: fromEntity.id ?? fromEntity.name,
265
+ to: toEntity.id ?? toEntity.name,
266
+ type: "one-to-many",
267
+ label
268
+ });
269
+ }
270
+ }
271
+ }
272
+ return { entities, relations };
273
+ }
274
+
275
+ export { deserializeSchema, serializeToDFD, serializeToERD, serializeToUML };
276
+ //# sourceMappingURL=diagram.serializers-DSYRR3LI.js.map
277
+ //# sourceMappingURL=diagram.serializers-DSYRR3LI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/Diagram/diagram.serializers.ts"],"names":[],"mappings":";AAyBO,SAAS,eAAe,MAAA,EAA+B;AAC5D,EAAA,MAAM,QAAkB,EAAC;AAEzB,EAAA,KAAA,MAAW,MAAA,IAAU,OAAO,QAAA,EAAU;AACpC,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,CAAA,EAAI,MAAA,CAAO,IAAI,CAAA,CAAA,CAAG,CAAA;AAC7B,IAAA,IAAI,OAAO,MAAA,EAAQ;AACjB,MAAA,KAAA,MAAW,KAAA,IAAS,OAAO,MAAA,EAAQ;AACjC,QAAA,MAAM,KAAA,GAAQ,CAAC,CAAA,EAAA,EAAK,KAAA,CAAM,IAAI,CAAA,CAAE,CAAA;AAChC,QAAA,IAAI,KAAA,CAAM,IAAA,EAAM,KAAA,CAAM,IAAA,CAAK,MAAM,IAAI,CAAA;AACrC,QAAA,IAAI,KAAA,CAAM,OAAA,EAAS,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA;AAClC,QAAA,IAAI,KAAA,CAAM,OAAA,EAAS,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA;AAClC,QAAA,IAAI,KAAA,CAAM,QAAA,EAAU,KAAA,CAAM,IAAA,CAAK,GAAG,CAAA;AAClC,QAAA,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAG,CAAC,CAAA;AAAA,MAC5B;AAAA,IACF;AACA,IAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AAAA,EACf;AAEA,EAAA,KAAA,MAAW,GAAA,IAAO,OAAO,SAAA,EAAW;AAClC,IAAA,MAAM,UAAA,GAAa,OAAO,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,GAAA,CAAI,IAAI,CAAA;AAChE,IAAA,MAAM,QAAA,GAAW,OAAO,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,GAAA,CAAI,EAAE,CAAA;AAC5D,IAAA,IAAI,CAAC,UAAA,IAAc,CAAC,QAAA,EAAU;AAE9B,IAAA,MAAM,MAAA,GAAS,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AACpC,IAAA,MAAM,QAAQ,CAAC,UAAA,CAAW,IAAA,EAAM,MAAA,EAAQ,SAAS,IAAI,CAAA;AACrD,IAAA,IAAI,IAAI,KAAA,EAAO,KAAA,CAAM,KAAK,CAAA,EAAA,EAAK,GAAA,CAAI,KAAK,CAAA,CAAE,CAAA;AAC1C,IAAA,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAG,CAAC,CAAA;AAAA,EAC5B;AAEA,EAAA,OAAO,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA,CAAE,IAAA,EAAK;AAC/B;AAEA,SAAS,aAAa,IAAA,EAA4B;AAChD,EAAA,QAAQ,IAAA;AAAM,IACZ,KAAK,YAAA;AACH,MAAA,OAAO,MAAA;AAAA,IACT,KAAK,aAAA;AACH,MAAA,OAAO,MAAA;AAAA,IACT,KAAK,cAAA;AACH,MAAA,OAAO,MAAA;AAAA,IACT;AACE,MAAA,OAAO,IAAA;AAAA;AAEb;AAKO,SAAS,eAAe,MAAA,EAA+B;AAC5D,EAAA,MAAM,KAAA,GAAkB,CAAC,WAAW,CAAA;AAEpC,EAAA,KAAA,MAAW,MAAA,IAAU,OAAO,QAAA,EAAU;AACpC,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,MAAA,EAAS,MAAA,CAAO,IAAI,CAAA,EAAA,CAAI,CAAA;AACnC,IAAA,IAAI,OAAO,MAAA,EAAQ;AACjB,MAAA,KAAA,MAAW,KAAA,IAAS,OAAO,MAAA,EAAQ;AACjC,QAAA,MAAM,OAAA,GAAU,MAAM,IAAA,IAAQ,KAAA;AAC9B,QAAA,MAAM,QAAA,GAAW,KAAA,CAAM,QAAA,GAAW,GAAA,GAAM,EAAA;AACxC,QAAA,MAAM,aAAa,KAAA,CAAM,OAAA,GACrB,SAAA,GACA,KAAA,CAAM,UACJ,SAAA,GACA,EAAA;AACN,QAAA,KAAA,CAAM,IAAA,CAAK,CAAA,EAAA,EAAK,KAAA,CAAM,IAAI,CAAA,GAAA,EAAM,OAAO,CAAA,EAAG,QAAQ,CAAA,EAAG,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AAAA,IACF;AACA,IAAA,KAAA,CAAM,KAAK,GAAG,CAAA;AACd,IAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AAAA,EACf;AAEA,EAAA,KAAA,MAAW,GAAA,IAAO,OAAO,SAAA,EAAW;AAClC,IAAA,MAAM,UAAA,GAAa,OAAO,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,GAAA,CAAI,IAAI,CAAA;AAChE,IAAA,MAAM,QAAA,GAAW,OAAO,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,GAAA,CAAI,EAAE,CAAA;AAC5D,IAAA,IAAI,CAAC,UAAA,IAAc,CAAC,QAAA,EAAU;AAE9B,IAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,GAAA,CAAI,IAAI,CAAA;AAClC,IAAA,MAAM,QAAQ,GAAA,CAAI,KAAA,GAAQ,CAAA,GAAA,EAAM,GAAA,CAAI,KAAK,CAAA,CAAA,GAAK,EAAA;AAC9C,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,EAAG,UAAA,CAAW,IAAI,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA,EAAI,QAAA,CAAS,IAAI,CAAA,EAAG,KAAK,CAAA,CAAE,CAAA;AAAA,EACnE;AAEA,EAAA,KAAA,CAAM,KAAK,SAAS,CAAA;AACpB,EAAA,OAAO,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB;AAEA,SAAS,YAAY,IAAA,EAA4B;AAC/C,EAAA,QAAQ,IAAA;AAAM,IACZ,KAAK,YAAA;AACH,MAAA,OAAO,YAAA;AAAA,IACT,KAAK,aAAA;AACH,MAAA,OAAO,YAAA;AAAA,IACT,KAAK,cAAA;AACH,MAAA,OAAO,YAAA;AAAA,IACT;AACE,MAAA,OAAO,IAAA;AAAA;AAEb;AAKO,SAAS,eAAe,MAAA,EAA+B;AAC5D,EAAA,MAAM,QAAkB,EAAC;AAEzB,EAAA,KAAA,MAAW,MAAA,IAAU,OAAO,QAAA,EAAU;AACpC,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,OAAA,EAAU,MAAA,CAAO,IAAI,CAAA,CAAE,CAAA;AAAA,EACpC;AAEA,EAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AAEb,EAAA,KAAA,MAAW,GAAA,IAAO,OAAO,SAAA,EAAW;AAClC,IAAA,MAAM,UAAA,GAAa,OAAO,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,GAAA,CAAI,IAAI,CAAA;AAChE,IAAA,MAAM,QAAA,GAAW,OAAO,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,GAAA,CAAI,EAAE,CAAA;AAC5D,IAAA,IAAI,CAAC,UAAA,IAAc,CAAC,QAAA,EAAU;AAE9B,IAAA,MAAM,QAAQ,GAAA,CAAI,KAAA,GAAQ,CAAA,EAAA,EAAK,GAAA,CAAI,KAAK,CAAA,CAAA,CAAA,GAAM,EAAA;AAC9C,IAAA,KAAA,CAAM,IAAA,CAAK,GAAG,UAAA,CAAW,IAAI,OAAO,QAAA,CAAS,IAAI,CAAA,EAAG,KAAK,CAAA,CAAE,CAAA;AAAA,EAC7D;AAEA,EAAA,OAAO,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA,CAAE,IAAA,EAAK;AAC/B;AAKO,SAAS,iBAAA,CACd,OACA,MAAA,EACe;AACf,EAAA,QAAQ,MAAA;AAAQ,IACd,KAAK,KAAA;AACH,MAAA,OAAO,eAAe,KAAK,CAAA;AAAA,IAC7B,KAAK,KAAA;AACH,MAAA,OAAO,eAAe,KAAK,CAAA;AAAA,IAC7B,KAAK,KAAA;AACH,MAAA,OAAO,eAAe,KAAK,CAAA;AAAA;AAEjC;AAEA,SAAS,eAAe,KAAA,EAA8B;AACpD,EAAA,MAAM,WAAgC,EAAC;AACvC,EAAA,MAAM,YAAmC,EAAC;AAC1C,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA;AAE9B,EAAA,IAAI,aAAA,GAA0C,IAAA;AAE9C,EAAA,KAAA,MAAW,WAAW,KAAA,EAAO;AAC3B,IAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,EAAK;AAG1B,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,KAAA,CAAM,YAAY,CAAA;AAC3C,IAAA,IAAI,WAAA,EAAa;AACf,MAAA,aAAA,GAAgB;AAAA,QACd,EAAA,EAAI,YAAY,CAAC,CAAA,CAAE,aAAY,CAAE,OAAA,CAAQ,QAAQ,GAAG,CAAA;AAAA,QACpD,IAAA,EAAM,YAAY,CAAC,CAAA;AAAA,QACnB,QAAQ;AAAC,OACX;AACA,MAAA,QAAA,CAAS,KAAK,aAAa,CAAA;AAC3B,MAAA;AAAA,IACF;AAGA,IAAA,IAAI,iBAAiB,OAAA,CAAQ,UAAA,CAAW,IAAI,CAAA,IAAK,IAAA,CAAK,SAAS,CAAA,EAAG;AAChE,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA;AAC9B,MAAA,MAAM,KAAA,GAA0B,EAAE,IAAA,EAAM,KAAA,CAAM,CAAC,CAAA,EAAE;AACjD,MAAA,IAAI,KAAA,CAAM,MAAA,GAAS,CAAA,IAAK,CAAC,CAAC,IAAA,EAAM,IAAA,EAAM,GAAG,CAAA,CAAE,QAAA,CAAS,KAAA,CAAM,CAAC,CAAC,CAAA,EAAG;AAC7D,QAAA,KAAA,CAAM,IAAA,GAAO,MAAM,CAAC,CAAA;AAAA,MACtB;AACA,MAAA,IAAI,KAAA,CAAM,QAAA,CAAS,IAAI,CAAA,QAAS,OAAA,GAAU,IAAA;AAC1C,MAAA,IAAI,KAAA,CAAM,QAAA,CAAS,IAAI,CAAA,QAAS,OAAA,GAAU,IAAA;AAC1C,MAAA,IAAI,KAAA,CAAM,QAAA,CAAS,GAAG,CAAA,QAAS,QAAA,GAAW,IAAA;AAC1C,MAAA,aAAA,CAAc,MAAA,CAAQ,KAAK,KAAK,CAAA;AAChC,MAAA;AAAA,IACF;AAGA,IAAA,MAAM,WAAW,IAAA,CAAK,KAAA;AAAA,MACpB;AAAA,KACF;AACA,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,aAAA,GAAgB,IAAA;AAChB,MAAA,MAAM,QAAA,GAAW,SAAS,CAAC,CAAA;AAC3B,MAAA,MAAM,MAAA,GAAS,SAAS,CAAC,CAAA;AACzB,MAAA,MAAM,MAAA,GAAS,SAAS,CAAC,CAAA;AACzB,MAAA,MAAM,KAAA,GAAQ,SAAS,CAAC,CAAA;AAExB,MAAA,MAAM,aAAa,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,QAAQ,CAAA;AAC3D,MAAA,MAAM,WAAW,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,MAAM,CAAA;AACvD,MAAA,IAAI,cAAc,QAAA,EAAU;AAC1B,QAAA,SAAA,CAAU,IAAA,CAAK;AAAA,UACb,EAAA,EAAI,CAAA,EAAG,UAAA,CAAW,EAAA,IAAM,UAAA,CAAW,IAAI,CAAA,CAAA,EAAI,QAAA,CAAS,EAAA,IAAM,QAAA,CAAS,IAAI,CAAA,CAAA;AAAA,UACvE,IAAA,EAAM,UAAA,CAAW,EAAA,IAAM,UAAA,CAAW,IAAA;AAAA,UAClC,EAAA,EAAI,QAAA,CAAS,EAAA,IAAM,QAAA,CAAS,IAAA;AAAA,UAC5B,IAAA,EAAM,eAAe,MAAM,CAAA;AAAA,UAC3B;AAAA,SACD,CAAA;AAAA,MACH;AACA,MAAA;AAAA,IACF;AAGA,IAAA,IAAI,SAAS,EAAA,EAAI;AACf,MAAA,aAAA,GAAgB,IAAA;AAAA,IAClB;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,UAAU,SAAA,EAAU;AAC/B;AAEA,SAAS,eAAe,MAAA,EAA8B;AACpD,EAAA,QAAQ,MAAA;AAAQ,IACd,KAAK,MAAA;AACH,MAAA,OAAO,YAAA;AAAA,IACT,KAAK,MAAA;AACH,MAAA,OAAO,aAAA;AAAA,IACT,KAAK,MAAA;AACH,MAAA,OAAO,cAAA;AAAA,IACT;AACE,MAAA,OAAO,aAAA;AAAA;AAEb;AAEA,SAAS,eAAe,KAAA,EAA8B;AACpD,EAAA,MAAM,WAAgC,EAAC;AACvC,EAAA,MAAM,YAAmC,EAAC;AAC1C,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA;AAE9B,EAAA,IAAI,aAAA,GAA0C,IAAA;AAE9C,EAAA,KAAA,MAAW,WAAW,KAAA,EAAO;AAC3B,IAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,EAAK;AAE1B,IAAA,IAAI,IAAA,KAAS,WAAA,IAAe,IAAA,KAAS,SAAA,IAAa,SAAS,EAAA,EAAI;AAG/D,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,KAAA,CAAM,sBAAsB,CAAA;AACpD,IAAA,IAAI,UAAA,EAAY;AACd,MAAA,aAAA,GAAgB;AAAA,QACd,EAAA,EAAI,WAAW,CAAC,CAAA,CAAE,aAAY,CAAE,OAAA,CAAQ,QAAQ,GAAG,CAAA;AAAA,QACnD,IAAA,EAAM,WAAW,CAAC,CAAA;AAAA,QAClB,QAAQ;AAAC,OACX;AACA,MAAA,QAAA,CAAS,KAAK,aAAa,CAAA;AAC3B,MAAA;AAAA,IACF;AAGA,IAAA,IAAI,SAAS,GAAA,EAAK;AAChB,MAAA,aAAA,GAAgB,IAAA;AAChB,MAAA;AAAA,IACF;AAGA,IAAA,IAAI,aAAA,EAAe;AACjB,MAAA,MAAM,aAAa,IAAA,CAAK,KAAA;AAAA,QACtB;AAAA,OACF;AACA,MAAA,IAAI,UAAA,EAAY;AACd,QAAA,MAAM,KAAA,GAA0B;AAAA,UAC9B,IAAA,EAAM,WAAW,CAAC,CAAA;AAAA,UAClB,IAAA,EAAM,WAAW,CAAC;AAAA,SACpB;AACA,QAAA,IAAI,UAAA,CAAW,CAAC,CAAA,EAAG,KAAA,CAAM,QAAA,GAAW,IAAA;AACpC,QAAA,IAAI,UAAA,CAAW,CAAC,CAAA,KAAM,IAAA,QAAY,OAAA,GAAU,IAAA;AAC5C,QAAA,IAAI,UAAA,CAAW,CAAC,CAAA,KAAM,IAAA,QAAY,OAAA,GAAU,IAAA;AAC5C,QAAA,aAAA,CAAc,MAAA,CAAQ,KAAK,KAAK,CAAA;AAAA,MAClC;AACA,MAAA;AAAA,IACF;AAGA,IAAA,MAAM,WAAW,IAAA,CAAK,KAAA;AAAA,MACpB;AAAA,KACF;AACA,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,MAAM,QAAA,GAAW,SAAS,CAAC,CAAA;AAC3B,MAAA,MAAM,QAAA,GAAW,SAAS,CAAC,CAAA;AAC3B,MAAA,MAAM,MAAA,GAAS,SAAS,CAAC,CAAA;AACzB,MAAA,MAAM,MAAA,GAAS,SAAS,CAAC,CAAA;AACzB,MAAA,MAAM,KAAA,GAAQ,SAAS,CAAC,CAAA;AAExB,MAAA,MAAM,aAAa,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,QAAQ,CAAA;AAC3D,MAAA,MAAM,WAAW,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,MAAM,CAAA;AACvD,MAAA,IAAI,cAAc,QAAA,EAAU;AAC1B,QAAA,MAAM,IAAA,GACJ,QAAA,KAAa,GAAA,IAAO,MAAA,KAAW,GAAA,GAC3B,eACA,QAAA,KAAa,GAAA,IAAO,MAAA,KAAW,GAAA,GAC7B,aAAA,GACA,cAAA;AACR,QAAA,SAAA,CAAU,IAAA,CAAK;AAAA,UACb,EAAA,EAAI,CAAA,EAAG,UAAA,CAAW,EAAA,IAAM,UAAA,CAAW,IAAI,CAAA,CAAA,EAAI,QAAA,CAAS,EAAA,IAAM,QAAA,CAAS,IAAI,CAAA,CAAA;AAAA,UACvE,IAAA,EAAM,UAAA,CAAW,EAAA,IAAM,UAAA,CAAW,IAAA;AAAA,UAClC,EAAA,EAAI,QAAA,CAAS,EAAA,IAAM,QAAA,CAAS,IAAA;AAAA,UAC5B,IAAA;AAAA,UACA;AAAA,SACD,CAAA;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,UAAU,SAAA,EAAU;AAC/B;AAEA,SAAS,eAAe,KAAA,EAA8B;AACpD,EAAA,MAAM,WAAgC,EAAC;AACvC,EAAA,MAAM,YAAmC,EAAC;AAC1C,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA;AAE9B,EAAA,KAAA,MAAW,WAAW,KAAA,EAAO;AAC3B,IAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,EAAK;AAC1B,IAAA,IAAI,SAAS,EAAA,EAAI;AAGjB,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,KAAA,CAAM,kBAAkB,CAAA;AACjD,IAAA,IAAI,WAAA,EAAa;AACf,MAAA,QAAA,CAAS,IAAA,CAAK;AAAA,QACZ,EAAA,EAAI,YAAY,CAAC,CAAA,CAAE,aAAY,CAAE,OAAA,CAAQ,QAAQ,GAAG,CAAA;AAAA,QACpD,IAAA,EAAM,YAAY,CAAC,CAAA;AAAA,QACnB,QAAQ;AAAC,OACV,CAAA;AACD,MAAA;AAAA,IACF;AAGA,IAAA,MAAM,YAAY,IAAA,CAAK,KAAA;AAAA,MACrB;AAAA,KACF;AACA,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,MAAM,QAAA,GAAW,UAAU,CAAC,CAAA;AAC5B,MAAA,MAAM,MAAA,GAAS,UAAU,CAAC,CAAA;AAC1B,MAAA,MAAM,KAAA,GAAQ,UAAU,CAAC,CAAA;AAEzB,MAAA,MAAM,aAAa,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,QAAQ,CAAA;AAC3D,MAAA,MAAM,WAAW,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,MAAM,CAAA;AACvD,MAAA,IAAI,cAAc,QAAA,EAAU;AAC1B,QAAA,SAAA,CAAU,IAAA,CAAK;AAAA,UACb,EAAA,EAAI,CAAA,EAAG,UAAA,CAAW,EAAA,IAAM,UAAA,CAAW,IAAI,CAAA,CAAA,EAAI,QAAA,CAAS,EAAA,IAAM,QAAA,CAAS,IAAI,CAAA,CAAA;AAAA,UACvE,IAAA,EAAM,UAAA,CAAW,EAAA,IAAM,UAAA,CAAW,IAAA;AAAA,UAClC,EAAA,EAAI,QAAA,CAAS,EAAA,IAAM,QAAA,CAAS,IAAA;AAAA,UAC5B,IAAA,EAAM,aAAA;AAAA,UACN;AAAA,SACD,CAAA;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,UAAU,SAAA,EAAU;AAC/B","file":"diagram.serializers-DSYRR3LI.js","sourcesContent":["import type {\n DiagramSchema,\n DiagramEntityData,\n DiagramRelationData,\n DiagramFieldData,\n ExportFormat,\n RelationType,\n} from \"./Diagram.types\";\n\n/**\n * Serialize a diagram schema to PlantUML-style ERD text.\n *\n * Example output:\n * ```\n * [Users]\n * id int PK\n * email varchar\n *\n * [Posts]\n * id int PK\n * user_id int FK\n *\n * Users 1--* Posts\n * ```\n */\nexport function serializeToERD(schema: DiagramSchema): string {\n const lines: string[] = [];\n\n for (const entity of schema.entities) {\n lines.push(`[${entity.name}]`);\n if (entity.fields) {\n for (const field of entity.fields) {\n const parts = [` ${field.name}`];\n if (field.type) parts.push(field.type);\n if (field.primary) parts.push(\"PK\");\n if (field.foreign) parts.push(\"FK\");\n if (field.nullable) parts.push(\"?\");\n lines.push(parts.join(\" \"));\n }\n }\n lines.push(\"\");\n }\n\n for (const rel of schema.relations) {\n const fromEntity = schema.entities.find((e) => e.id === rel.from);\n const toEntity = schema.entities.find((e) => e.id === rel.to);\n if (!fromEntity || !toEntity) continue;\n\n const marker = getERDMarker(rel.type);\n const parts = [fromEntity.name, marker, toEntity.name];\n if (rel.label) parts.push(`: ${rel.label}`);\n lines.push(parts.join(\" \"));\n }\n\n return lines.join(\"\\n\").trim();\n}\n\nfunction getERDMarker(type: RelationType): string {\n switch (type) {\n case \"one-to-one\":\n return \"1--1\";\n case \"one-to-many\":\n return \"1--*\";\n case \"many-to-many\":\n return \"*--*\";\n default:\n return \"--\";\n }\n}\n\n/**\n * Serialize a diagram schema to PlantUML class diagram text.\n */\nexport function serializeToUML(schema: DiagramSchema): string {\n const lines: string[] = [\"@startuml\"];\n\n for (const entity of schema.entities) {\n lines.push(`class ${entity.name} {`);\n if (entity.fields) {\n for (const field of entity.fields) {\n const typeStr = field.type ?? \"any\";\n const nullable = field.nullable ? \"?\" : \"\";\n const stereotype = field.primary\n ? \" <<PK>>\"\n : field.foreign\n ? \" <<FK>>\"\n : \"\";\n lines.push(` ${field.name} : ${typeStr}${nullable}${stereotype}`);\n }\n }\n lines.push(\"}\");\n lines.push(\"\");\n }\n\n for (const rel of schema.relations) {\n const fromEntity = schema.entities.find((e) => e.id === rel.from);\n const toEntity = schema.entities.find((e) => e.id === rel.to);\n if (!fromEntity || !toEntity) continue;\n\n const arrow = getUMLArrow(rel.type);\n const label = rel.label ? ` : ${rel.label}` : \"\";\n lines.push(`${fromEntity.name} ${arrow} ${toEntity.name}${label}`);\n }\n\n lines.push(\"@enduml\");\n return lines.join(\"\\n\");\n}\n\nfunction getUMLArrow(type: RelationType): string {\n switch (type) {\n case \"one-to-one\":\n return '\"1\" -- \"1\"';\n case \"one-to-many\":\n return '\"1\" -- \"*\"';\n case \"many-to-many\":\n return '\"*\" -- \"*\"';\n default:\n return \"--\";\n }\n}\n\n/**\n * Serialize a diagram schema to a simple DFD text format.\n */\nexport function serializeToDFD(schema: DiagramSchema): string {\n const lines: string[] = [];\n\n for (const entity of schema.entities) {\n lines.push(`entity ${entity.name}`);\n }\n\n lines.push(\"\");\n\n for (const rel of schema.relations) {\n const fromEntity = schema.entities.find((e) => e.id === rel.from);\n const toEntity = schema.entities.find((e) => e.id === rel.to);\n if (!fromEntity || !toEntity) continue;\n\n const label = rel.label ? ` \"${rel.label}\"` : \"\";\n lines.push(`${fromEntity.name} -> ${toEntity.name}${label}`);\n }\n\n return lines.join(\"\\n\").trim();\n}\n\n/**\n * Parse a serialized schema string back into a DiagramSchema.\n */\nexport function deserializeSchema(\n input: string,\n format: ExportFormat,\n): DiagramSchema {\n switch (format) {\n case \"erd\":\n return deserializeERD(input);\n case \"uml\":\n return deserializeUML(input);\n case \"dfd\":\n return deserializeDFD(input);\n }\n}\n\nfunction deserializeERD(input: string): DiagramSchema {\n const entities: DiagramEntityData[] = [];\n const relations: DiagramRelationData[] = [];\n const lines = input.split(\"\\n\");\n\n let currentEntity: DiagramEntityData | null = null;\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n\n // Entity header: [EntityName]\n const entityMatch = line.match(/^\\[(.+)\\]$/);\n if (entityMatch) {\n currentEntity = {\n id: entityMatch[1].toLowerCase().replace(/\\s+/g, \"_\"),\n name: entityMatch[1],\n fields: [],\n };\n entities.push(currentEntity);\n continue;\n }\n\n // Field line (indented): name type PK FK ?\n if (currentEntity && rawLine.startsWith(\" \") && line.length > 0) {\n const parts = line.split(/\\s+/);\n const field: DiagramFieldData = { name: parts[0] };\n if (parts.length > 1 && ![\"PK\", \"FK\", \"?\"].includes(parts[1])) {\n field.type = parts[1];\n }\n if (parts.includes(\"PK\")) field.primary = true;\n if (parts.includes(\"FK\")) field.foreign = true;\n if (parts.includes(\"?\")) field.nullable = true;\n currentEntity.fields!.push(field);\n continue;\n }\n\n // Relation: EntityA 1--* EntityB : label\n const relMatch = line.match(\n /^(\\S+)\\s+(1--1|1--\\*|\\*--\\*)\\s+(\\S+)(?:\\s*:\\s*(.+))?$/,\n );\n if (relMatch) {\n currentEntity = null;\n const fromName = relMatch[1];\n const marker = relMatch[2];\n const toName = relMatch[3];\n const label = relMatch[4];\n\n const fromEntity = entities.find((e) => e.name === fromName);\n const toEntity = entities.find((e) => e.name === toName);\n if (fromEntity && toEntity) {\n relations.push({\n id: `${fromEntity.id ?? fromEntity.name}_${toEntity.id ?? toEntity.name}`,\n from: fromEntity.id ?? fromEntity.name,\n to: toEntity.id ?? toEntity.name,\n type: parseERDMarker(marker),\n label,\n });\n }\n continue;\n }\n\n // Empty line resets current entity context\n if (line === \"\") {\n currentEntity = null;\n }\n }\n\n return { entities, relations };\n}\n\nfunction parseERDMarker(marker: string): RelationType {\n switch (marker) {\n case \"1--1\":\n return \"one-to-one\";\n case \"1--*\":\n return \"one-to-many\";\n case \"*--*\":\n return \"many-to-many\";\n default:\n return \"one-to-many\";\n }\n}\n\nfunction deserializeUML(input: string): DiagramSchema {\n const entities: DiagramEntityData[] = [];\n const relations: DiagramRelationData[] = [];\n const lines = input.split(\"\\n\");\n\n let currentEntity: DiagramEntityData | null = null;\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n\n if (line === \"@startuml\" || line === \"@enduml\" || line === \"\") continue;\n\n // Class header: class EntityName {\n const classMatch = line.match(/^class\\s+(\\S+)\\s*\\{$/);\n if (classMatch) {\n currentEntity = {\n id: classMatch[1].toLowerCase().replace(/\\s+/g, \"_\"),\n name: classMatch[1],\n fields: [],\n };\n entities.push(currentEntity);\n continue;\n }\n\n // Closing brace\n if (line === \"}\") {\n currentEntity = null;\n continue;\n }\n\n // Field: name : type<<stereotype>>\n if (currentEntity) {\n const fieldMatch = line.match(\n /^(\\S+)\\s*:\\s*(\\S+?)(\\?)?(?:\\s*<<(PK|FK)>>)?$/,\n );\n if (fieldMatch) {\n const field: DiagramFieldData = {\n name: fieldMatch[1],\n type: fieldMatch[2],\n };\n if (fieldMatch[3]) field.nullable = true;\n if (fieldMatch[4] === \"PK\") field.primary = true;\n if (fieldMatch[4] === \"FK\") field.foreign = true;\n currentEntity.fields!.push(field);\n }\n continue;\n }\n\n // Relation: EntityA \"1\" -- \"*\" EntityB : label\n const relMatch = line.match(\n /^(\\S+)\\s+\"([1*])\"\\s+--\\s+\"([1*])\"\\s+(\\S+)(?:\\s*:\\s*(.+))?$/,\n );\n if (relMatch) {\n const fromName = relMatch[1];\n const fromCard = relMatch[2];\n const toCard = relMatch[3];\n const toName = relMatch[4];\n const label = relMatch[5];\n\n const fromEntity = entities.find((e) => e.name === fromName);\n const toEntity = entities.find((e) => e.name === toName);\n if (fromEntity && toEntity) {\n const type: RelationType =\n fromCard === \"1\" && toCard === \"1\"\n ? \"one-to-one\"\n : fromCard === \"1\" && toCard === \"*\"\n ? \"one-to-many\"\n : \"many-to-many\";\n relations.push({\n id: `${fromEntity.id ?? fromEntity.name}_${toEntity.id ?? toEntity.name}`,\n from: fromEntity.id ?? fromEntity.name,\n to: toEntity.id ?? toEntity.name,\n type,\n label,\n });\n }\n }\n }\n\n return { entities, relations };\n}\n\nfunction deserializeDFD(input: string): DiagramSchema {\n const entities: DiagramEntityData[] = [];\n const relations: DiagramRelationData[] = [];\n const lines = input.split(\"\\n\");\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (line === \"\") continue;\n\n // Entity: entity EntityName\n const entityMatch = line.match(/^entity\\s+(\\S+)$/);\n if (entityMatch) {\n entities.push({\n id: entityMatch[1].toLowerCase().replace(/\\s+/g, \"_\"),\n name: entityMatch[1],\n fields: [],\n });\n continue;\n }\n\n // Flow: EntityA -> EntityB \"label\"\n const flowMatch = line.match(\n /^(\\S+)\\s+->\\s+(\\S+)(?:\\s+\"(.+)\")?$/,\n );\n if (flowMatch) {\n const fromName = flowMatch[1];\n const toName = flowMatch[2];\n const label = flowMatch[3];\n\n const fromEntity = entities.find((e) => e.name === fromName);\n const toEntity = entities.find((e) => e.name === toName);\n if (fromEntity && toEntity) {\n relations.push({\n id: `${fromEntity.id ?? fromEntity.name}_${toEntity.id ?? toEntity.name}`,\n from: fromEntity.id ?? fromEntity.name,\n to: toEntity.id ?? toEntity.name,\n type: \"one-to-many\",\n label,\n });\n }\n }\n }\n\n return { entities, relations };\n}\n"]}