@bovinphang/craft-common 0.0.0-stage → 1.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/LICENSE +21 -0
- package/README.md +12 -2
- package/dist/content/catalog.d.ts +4 -0
- package/dist/content/resources.d.ts +2 -0
- package/dist/content/types.d.ts +21 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +668 -0
- package/dist/install/identity.d.ts +5 -0
- package/dist/install/planner.d.ts +6 -0
- package/dist/install/transaction.d.ts +7 -0
- package/dist/install/types.d.ts +44 -0
- package/docs/consuming-common.md +33 -0
- package/localized/zh-CN/methods/debug.md +13 -0
- package/localized/zh-CN/methods/doc-sync.md +12 -0
- package/localized/zh-CN/methods/refactoring.md +16 -0
- package/localized/zh-CN/methods/source-driven-development.md +19 -0
- package/localized/zh-CN/methods/tdd.md +13 -0
- package/localized/zh-CN/skills/cc-debug-methodology/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-debug-methodology/references/method.md +13 -0
- package/localized/zh-CN/skills/cc-doc-sync-core/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-doc-sync-core/references/method.md +12 -0
- package/localized/zh-CN/skills/cc-drawio-studio/SKILL.md +73 -0
- package/localized/zh-CN/skills/cc-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
- package/localized/zh-CN/skills/cc-drawio-studio/data/brand-icons.json +878 -0
- package/localized/zh-CN/skills/cc-drawio-studio/data/shape-index.json +83570 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/data-residency.md +27 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/diagram-patterns.md +60 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/flowchart-quality.md +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/layout-and-export.md +72 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/troubleshooting.md +29 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/xml-and-mermaid.md +58 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/brand-symbols.mjs +99 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/diagram-lint.mjs +205 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/diagram-url.mjs +102 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/drawio-export.mjs +134 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/layout-graph.mjs +107 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/png-embed-fix.mjs +26 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-go-packages.mjs +36 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-js-modules.mjs +52 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-python-classes.mjs +30 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-python-modules.mjs +55 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/shape-query.mjs +121 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/studio-core.mjs +139 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/corporate.json +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/default.json +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/handdrawn.json +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/schema.json +112 -0
- package/localized/zh-CN/skills/cc-refactoring-core/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-refactoring-core/references/method.md +16 -0
- package/localized/zh-CN/skills/cc-source-driven-development/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-source-driven-development/references/method.md +19 -0
- package/localized/zh-CN/skills/cc-tdd-methodology/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-tdd-methodology/references/method.md +13 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/SKILL.md +34 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/interactive-diagram.html +371 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/README.md +14 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/architecture.json +121 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/invalid-explicit.json +41 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/sequence.json +53 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/sequence.mmd +21 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/workflow.json +98 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/workflow.mmd +7 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/references/diagram-workflows.md +81 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/references/html-technical-diagrams.md +243 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/references/png-qa-autofix.md +37 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/diagram-browser.mjs +153 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/diagram-layout.mjs +176 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/export-diagram.mjs +332 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/interactive-diagram-server.mjs +654 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/mermaid-render.mjs +64 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/png-qa.mjs +462 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/tech-diagram-render.mjs +1537 -0
- package/localized/zh-CN/skills/eval_queries.json +160 -0
- package/localized/zh-CN/skills/metadata.json +223 -0
- package/localized/zh-CN/skills/relations.json +53 -0
- package/methods/debug.md +13 -0
- package/methods/doc-sync.md +12 -0
- package/methods/refactoring.md +16 -0
- package/methods/source-driven-development.md +19 -0
- package/methods/tdd.md +13 -0
- package/package.json +51 -4
- package/skills/cc-debug-methodology/SKILL.md +8 -0
- package/skills/cc-debug-methodology/references/method.md +13 -0
- package/skills/cc-doc-sync-core/SKILL.md +8 -0
- package/skills/cc-doc-sync-core/references/method.md +12 -0
- package/skills/cc-drawio-studio/SKILL.md +73 -0
- package/skills/cc-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
- package/skills/cc-drawio-studio/data/brand-icons.json +878 -0
- package/skills/cc-drawio-studio/data/shape-index.json +83570 -0
- package/skills/cc-drawio-studio/references/data-residency.md +27 -0
- package/skills/cc-drawio-studio/references/diagram-patterns.md +60 -0
- package/skills/cc-drawio-studio/references/flowchart-quality.md +49 -0
- package/skills/cc-drawio-studio/references/layout-and-export.md +72 -0
- package/skills/cc-drawio-studio/references/troubleshooting.md +29 -0
- package/skills/cc-drawio-studio/references/xml-and-mermaid.md +58 -0
- package/skills/cc-drawio-studio/scripts/brand-symbols.mjs +99 -0
- package/skills/cc-drawio-studio/scripts/diagram-lint.mjs +205 -0
- package/skills/cc-drawio-studio/scripts/diagram-url.mjs +102 -0
- package/skills/cc-drawio-studio/scripts/drawio-export.mjs +134 -0
- package/skills/cc-drawio-studio/scripts/layout-graph.mjs +107 -0
- package/skills/cc-drawio-studio/scripts/png-embed-fix.mjs +26 -0
- package/skills/cc-drawio-studio/scripts/scan-go-packages.mjs +36 -0
- package/skills/cc-drawio-studio/scripts/scan-js-modules.mjs +52 -0
- package/skills/cc-drawio-studio/scripts/scan-python-classes.mjs +30 -0
- package/skills/cc-drawio-studio/scripts/scan-python-modules.mjs +55 -0
- package/skills/cc-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
- package/skills/cc-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
- package/skills/cc-drawio-studio/scripts/shape-query.mjs +121 -0
- package/skills/cc-drawio-studio/scripts/studio-core.mjs +139 -0
- package/skills/cc-drawio-studio/styles/built-in/corporate.json +49 -0
- package/skills/cc-drawio-studio/styles/built-in/default.json +49 -0
- package/skills/cc-drawio-studio/styles/built-in/handdrawn.json +49 -0
- package/skills/cc-drawio-studio/styles/schema.json +112 -0
- package/skills/cc-refactoring-core/SKILL.md +8 -0
- package/skills/cc-refactoring-core/references/method.md +16 -0
- package/skills/cc-source-driven-development/SKILL.md +8 -0
- package/skills/cc-source-driven-development/references/method.md +19 -0
- package/skills/cc-tdd-methodology/SKILL.md +8 -0
- package/skills/cc-tdd-methodology/references/method.md +13 -0
- package/skills/cc-technical-diagrams/SKILL.md +34 -0
- package/skills/cc-technical-diagrams/assets/interactive-diagram.html +371 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/README.md +14 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/architecture.json +121 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/invalid-explicit.json +41 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/sequence.json +53 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/sequence.mmd +21 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/workflow.json +98 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/workflow.mmd +7 -0
- package/skills/cc-technical-diagrams/references/diagram-workflows.md +81 -0
- package/skills/cc-technical-diagrams/references/html-technical-diagrams.md +243 -0
- package/skills/cc-technical-diagrams/references/png-qa-autofix.md +37 -0
- package/skills/cc-technical-diagrams/scripts/diagram-browser.mjs +153 -0
- package/skills/cc-technical-diagrams/scripts/diagram-layout.mjs +176 -0
- package/skills/cc-technical-diagrams/scripts/export-diagram.mjs +332 -0
- package/skills/cc-technical-diagrams/scripts/interactive-diagram-server.mjs +654 -0
- package/skills/cc-technical-diagrams/scripts/mermaid-render.mjs +64 -0
- package/skills/cc-technical-diagrams/scripts/png-qa.mjs +462 -0
- package/skills/cc-technical-diagrams/scripts/tech-diagram-render.mjs +1537 -0
- package/skills/eval_queries.json +160 -0
- package/skills/metadata.json +223 -0
- package/skills/relations.json +53 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
11
|
+
const DEFAULT_PORT = 6100;
|
|
12
|
+
const PORT_TRIES = 20;
|
|
13
|
+
const MAX_BODY_BYTES = 1024 * 1024;
|
|
14
|
+
const SESSION_DIR = path.join(os.tmpdir(), "frontend-craft-interactive-diagrams");
|
|
15
|
+
|
|
16
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
const rendererPath = path.join(root, "assets", "interactive-diagram.html");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {{ cmd: string, [key: string]: unknown }} DiagramCommand
|
|
21
|
+
* @typedef {{ commands: DiagramCommand[], clients: Set<http.ServerResponse> }} DiagramSession
|
|
22
|
+
* @typedef {{ id: string, label: string, type: string, x?: number, y?: number, width?: number, height?: number, children?: string[], [key: string]: unknown }} GraphNode
|
|
23
|
+
* @typedef {{ id: string, from: string, to: string, label?: string, [key: string]: unknown }} GraphEdge
|
|
24
|
+
* @typedef {{ title: string, direction: string, nodes: GraphNode[], edges: GraphEdge[] }} GraphModel
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** @type {Map<string, DiagramSession>} */
|
|
28
|
+
const sessions = new Map();
|
|
29
|
+
|
|
30
|
+
const options = parseArgs(process.argv.slice(2));
|
|
31
|
+
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
32
|
+
|
|
33
|
+
if (options.help) {
|
|
34
|
+
printHelp();
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const server = http.createServer((req, res) => {
|
|
39
|
+
void route(req, res);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
server.on("clientError", (_err, socket) => {
|
|
43
|
+
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
listenWithFallback(server, options.port, options.host)
|
|
47
|
+
.then((port) => {
|
|
48
|
+
const status = { ok: true, host: options.host, port, url: `http://${options.host}:${port}/` };
|
|
49
|
+
console.log(JSON.stringify(status));
|
|
50
|
+
})
|
|
51
|
+
.catch((error) => {
|
|
52
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string[]} args
|
|
58
|
+
* @returns {{ host: string, port: number, help: boolean }}
|
|
59
|
+
*/
|
|
60
|
+
function parseArgs(args) {
|
|
61
|
+
let host = DEFAULT_HOST;
|
|
62
|
+
let port = DEFAULT_PORT;
|
|
63
|
+
let help = false;
|
|
64
|
+
|
|
65
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
66
|
+
const arg = args[index];
|
|
67
|
+
if (arg === "--help" || arg === "-h") help = true;
|
|
68
|
+
else if (arg === "--host") host = args[++index] ?? host;
|
|
69
|
+
else if (arg === "--port") port = Number(args[++index] ?? port);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
73
|
+
throw new Error(`Invalid --port value: ${port}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { host, port, help };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function printHelp() {
|
|
80
|
+
console.log(`Usage: node interactive-diagram-server.mjs [--host 127.0.0.1] [--port 6100]
|
|
81
|
+
|
|
82
|
+
Starts a local interactive diagram server with session-scoped SSE updates.
|
|
83
|
+
Open http://127.0.0.1:6100/?s=my-session and POST commands to /cmd?s=my-session.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {http.Server} target
|
|
88
|
+
* @param {number} startPort
|
|
89
|
+
* @param {string} host
|
|
90
|
+
* @returns {Promise<number>}
|
|
91
|
+
*/
|
|
92
|
+
function listenWithFallback(target, startPort, host) {
|
|
93
|
+
if (startPort === 0) {
|
|
94
|
+
return listenOnce(target, 0, host);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let nextPort = startPort;
|
|
98
|
+
let attempts = 0;
|
|
99
|
+
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const tryListen = () => {
|
|
102
|
+
attempts += 1;
|
|
103
|
+
target.once("error", onError);
|
|
104
|
+
target.listen(nextPort, host, () => {
|
|
105
|
+
target.off("error", onError);
|
|
106
|
+
resolve(nextPort);
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** @param {NodeJS.ErrnoException} error */
|
|
111
|
+
const onError = (error) => {
|
|
112
|
+
target.off("error", onError);
|
|
113
|
+
if (error.code === "EADDRINUSE" && attempts < PORT_TRIES) {
|
|
114
|
+
nextPort += 1;
|
|
115
|
+
tryListen();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
reject(error);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
tryListen();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {http.Server} target
|
|
127
|
+
* @param {number} port
|
|
128
|
+
* @param {string} host
|
|
129
|
+
* @returns {Promise<number>}
|
|
130
|
+
*/
|
|
131
|
+
function listenOnce(target, port, host) {
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
target.once("error", reject);
|
|
134
|
+
target.listen(port, host, () => {
|
|
135
|
+
target.off("error", reject);
|
|
136
|
+
const address = target.address();
|
|
137
|
+
if (!address || typeof address === "string") {
|
|
138
|
+
reject(new Error("Unable to resolve listening port"));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
resolve(address.port);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {http.IncomingMessage} req
|
|
148
|
+
* @param {http.ServerResponse} res
|
|
149
|
+
*/
|
|
150
|
+
async function route(req, res) {
|
|
151
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
152
|
+
const sessionId = sanitizeSessionId(url.searchParams.get("s") ?? "default");
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
if (req.method === "GET" && url.pathname === "/") {
|
|
156
|
+
serveRenderer(res);
|
|
157
|
+
} else if (req.method === "GET" && url.pathname === "/events") {
|
|
158
|
+
serveEvents(sessionId, res);
|
|
159
|
+
} else if (req.method === "GET" && url.pathname === "/state") {
|
|
160
|
+
sendJson(res, 200, getSession(sessionId).commands);
|
|
161
|
+
} else if (req.method === "GET" && url.pathname === "/status") {
|
|
162
|
+
sendJson(res, 200, getStatus());
|
|
163
|
+
} else if (req.method === "GET" && url.pathname === "/sessions") {
|
|
164
|
+
sendJson(res, 200, listSessions());
|
|
165
|
+
} else if (req.method === "POST" && url.pathname === "/cmd") {
|
|
166
|
+
await handleCommand(req, res, sessionId);
|
|
167
|
+
} else if (req.method === "POST" && url.pathname === "/clear") {
|
|
168
|
+
clearSession(sessionId);
|
|
169
|
+
sendJson(res, 200, { ok: true, session: sessionId, commands: 0 });
|
|
170
|
+
} else if (req.method === "GET" && url.pathname === "/export") {
|
|
171
|
+
handleExport(res, sessionId, url.searchParams.get("format") ?? "json");
|
|
172
|
+
} else if (req.method === "POST" && url.pathname === "/export") {
|
|
173
|
+
const payload = await readJsonBody(req);
|
|
174
|
+
const format = typeof payload.format === "string" ? payload.format : "json";
|
|
175
|
+
handleExport(res, sessionId, format);
|
|
176
|
+
} else {
|
|
177
|
+
sendJson(res, 404, { ok: false, error: `Unknown route ${req.method ?? "GET"} ${url.pathname}` });
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
sendJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** @param {http.ServerResponse} res */
|
|
185
|
+
function serveRenderer(res) {
|
|
186
|
+
const html = fs.readFileSync(rendererPath, "utf8");
|
|
187
|
+
res.writeHead(200, {
|
|
188
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
189
|
+
"Cache-Control": "no-store",
|
|
190
|
+
});
|
|
191
|
+
res.end(html);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {string} sessionId
|
|
196
|
+
* @param {http.ServerResponse} res
|
|
197
|
+
*/
|
|
198
|
+
function serveEvents(sessionId, res) {
|
|
199
|
+
const session = getSession(sessionId);
|
|
200
|
+
res.writeHead(200, {
|
|
201
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
202
|
+
"Cache-Control": "no-cache, no-transform",
|
|
203
|
+
Connection: "keep-alive",
|
|
204
|
+
"X-Accel-Buffering": "no",
|
|
205
|
+
});
|
|
206
|
+
res.write(`event: ready\ndata: ${JSON.stringify({ session: sessionId })}\n\n`);
|
|
207
|
+
session.clients.add(res);
|
|
208
|
+
res.on("close", () => {
|
|
209
|
+
session.clients.delete(res);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param {http.IncomingMessage} req
|
|
215
|
+
* @param {http.ServerResponse} res
|
|
216
|
+
* @param {string} sessionId
|
|
217
|
+
*/
|
|
218
|
+
async function handleCommand(req, res, sessionId) {
|
|
219
|
+
let command;
|
|
220
|
+
try {
|
|
221
|
+
command = await readJsonBody(req);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
sendJson(res, 400, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!isCommand(command)) {
|
|
228
|
+
sendJson(res, 400, { ok: false, error: "Body must be a JSON object with a string cmd field" });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const session = getSession(sessionId);
|
|
233
|
+
if (command.cmd === "clear") {
|
|
234
|
+
session.commands = [];
|
|
235
|
+
} else if (command.cmd === "init") {
|
|
236
|
+
session.commands = [command];
|
|
237
|
+
} else {
|
|
238
|
+
session.commands.push(command);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
saveSession(sessionId, session.commands);
|
|
242
|
+
broadcast(session, command);
|
|
243
|
+
sendJson(res, 200, { ok: true, session: sessionId, commands: session.commands.length });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @param {http.ServerResponse} res
|
|
248
|
+
* @param {string} sessionId
|
|
249
|
+
* @param {string} format
|
|
250
|
+
*/
|
|
251
|
+
function handleExport(res, sessionId, format) {
|
|
252
|
+
const session = getSession(sessionId);
|
|
253
|
+
const model = buildGraphModel(session.commands);
|
|
254
|
+
if (format === "json") {
|
|
255
|
+
sendJson(res, 200, { ok: true, session: sessionId, commands: session.commands, graph: model });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (format === "svg") {
|
|
259
|
+
const svg = renderStaticSvg(model);
|
|
260
|
+
res.writeHead(200, {
|
|
261
|
+
"Content-Type": "image/svg+xml; charset=utf-8",
|
|
262
|
+
"Cache-Control": "no-store",
|
|
263
|
+
});
|
|
264
|
+
res.end(svg);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (format === "drawio") {
|
|
268
|
+
res.writeHead(200, {
|
|
269
|
+
"Content-Type": "application/xml; charset=utf-8",
|
|
270
|
+
"Cache-Control": "no-store",
|
|
271
|
+
});
|
|
272
|
+
res.end(renderDrawioXml(model));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
sendJson(res, 422, { ok: false, error: `Unsupported export format: ${format}` });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @param {http.IncomingMessage} req
|
|
280
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
281
|
+
*/
|
|
282
|
+
function readJsonBody(req) {
|
|
283
|
+
return new Promise((resolve, reject) => {
|
|
284
|
+
/** @type {Buffer[]} */
|
|
285
|
+
const chunks = [];
|
|
286
|
+
let size = 0;
|
|
287
|
+
req.on("data", (chunk) => {
|
|
288
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
289
|
+
size += buffer.length;
|
|
290
|
+
if (size > MAX_BODY_BYTES) {
|
|
291
|
+
reject(new Error("Request body is too large"));
|
|
292
|
+
req.destroy();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
chunks.push(buffer);
|
|
296
|
+
});
|
|
297
|
+
req.on("end", () => {
|
|
298
|
+
try {
|
|
299
|
+
const body = Buffer.concat(chunks).toString("utf8").trim();
|
|
300
|
+
resolve(body ? JSON.parse(body) : {});
|
|
301
|
+
} catch {
|
|
302
|
+
reject(new Error("Request body must be valid JSON"));
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
req.on("error", reject);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @param {unknown} value
|
|
311
|
+
* @returns {value is DiagramCommand}
|
|
312
|
+
*/
|
|
313
|
+
function isCommand(value) {
|
|
314
|
+
return Boolean(value && typeof value === "object" && typeof /** @type {{ cmd?: unknown }} */ (value).cmd === "string");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @param {string} sessionId
|
|
319
|
+
* @returns {DiagramSession}
|
|
320
|
+
*/
|
|
321
|
+
function getSession(sessionId) {
|
|
322
|
+
const existing = sessions.get(sessionId);
|
|
323
|
+
if (existing) return existing;
|
|
324
|
+
const loaded = loadSession(sessionId);
|
|
325
|
+
const session = { commands: loaded, clients: new Set() };
|
|
326
|
+
sessions.set(sessionId, session);
|
|
327
|
+
return session;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** @param {string} value */
|
|
331
|
+
function sanitizeSessionId(value) {
|
|
332
|
+
const clean = value.trim().replace(/[^A-Za-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
333
|
+
return clean.slice(0, 80) || "default";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** @param {string} sessionId */
|
|
337
|
+
function sessionFile(sessionId) {
|
|
338
|
+
return path.join(SESSION_DIR, `${sessionId}.json`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** @param {string} sessionId */
|
|
342
|
+
function loadSession(sessionId) {
|
|
343
|
+
try {
|
|
344
|
+
const parsed = JSON.parse(fs.readFileSync(sessionFile(sessionId), "utf8"));
|
|
345
|
+
return Array.isArray(parsed) ? parsed.filter(isCommand) : [];
|
|
346
|
+
} catch {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @param {string} sessionId
|
|
353
|
+
* @param {DiagramCommand[]} commands
|
|
354
|
+
*/
|
|
355
|
+
function saveSession(sessionId, commands) {
|
|
356
|
+
fs.writeFileSync(sessionFile(sessionId), `${JSON.stringify(commands, null, 2)}\n`, "utf8");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** @param {string} sessionId */
|
|
360
|
+
function clearSession(sessionId) {
|
|
361
|
+
const session = getSession(sessionId);
|
|
362
|
+
session.commands = [];
|
|
363
|
+
saveSession(sessionId, []);
|
|
364
|
+
broadcast(session, { cmd: "clear" });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* @param {DiagramSession} session
|
|
369
|
+
* @param {DiagramCommand} command
|
|
370
|
+
*/
|
|
371
|
+
function broadcast(session, command) {
|
|
372
|
+
const payload = `data: ${JSON.stringify(command)}\n\n`;
|
|
373
|
+
for (const client of session.clients) {
|
|
374
|
+
client.write(payload);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function getStatus() {
|
|
379
|
+
let clientCount = 0;
|
|
380
|
+
let commandCount = 0;
|
|
381
|
+
for (const session of sessions.values()) {
|
|
382
|
+
clientCount += session.clients.size;
|
|
383
|
+
commandCount += session.commands.length;
|
|
384
|
+
}
|
|
385
|
+
return { ok: true, sessions: sessions.size, clients: clientCount, commands: commandCount };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function listSessions() {
|
|
389
|
+
return [...sessions.entries()]
|
|
390
|
+
.map(([id, session]) => ({ id, commands: session.commands.length, clients: session.clients.size }))
|
|
391
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** @param {DiagramCommand[]} commands */
|
|
395
|
+
function buildGraphModel(commands) {
|
|
396
|
+
/** @type {GraphModel} */
|
|
397
|
+
const model = { title: "Interactive Diagram", direction: "TB", nodes: [], edges: [] };
|
|
398
|
+
/** @type {Map<string, GraphNode>} */
|
|
399
|
+
const nodes = new Map();
|
|
400
|
+
/** @type {GraphEdge[]} */
|
|
401
|
+
const edges = [];
|
|
402
|
+
|
|
403
|
+
for (const command of commands) {
|
|
404
|
+
if (command.cmd === "init") {
|
|
405
|
+
model.title = stringField(command.title, model.title);
|
|
406
|
+
model.direction = stringField(command.direction, model.direction);
|
|
407
|
+
nodes.clear();
|
|
408
|
+
edges.length = 0;
|
|
409
|
+
} else if (command.cmd === "title") {
|
|
410
|
+
model.title = stringField(command.text, model.title);
|
|
411
|
+
} else if (command.cmd === "node" || command.cmd === "container") {
|
|
412
|
+
const id = stringField(command.id, "");
|
|
413
|
+
if (!id) continue;
|
|
414
|
+
const type = command.cmd === "container" ? "container" : stringField(command.type, "process");
|
|
415
|
+
const node = {
|
|
416
|
+
id,
|
|
417
|
+
label: stringField(command.label, id),
|
|
418
|
+
type,
|
|
419
|
+
width: numberField(command.width),
|
|
420
|
+
height: numberField(command.height),
|
|
421
|
+
x: numberField(command.x),
|
|
422
|
+
y: numberField(command.y),
|
|
423
|
+
children: arrayStringField(command.children),
|
|
424
|
+
...command,
|
|
425
|
+
};
|
|
426
|
+
nodes.set(id, node);
|
|
427
|
+
} else if (command.cmd === "edge") {
|
|
428
|
+
const from = stringField(command.from, "");
|
|
429
|
+
const to = stringField(command.to, "");
|
|
430
|
+
if (!from || !to) continue;
|
|
431
|
+
edges.push({
|
|
432
|
+
id: stringField(command.id, `${from}-${to}-${edges.length + 1}`),
|
|
433
|
+
from,
|
|
434
|
+
to,
|
|
435
|
+
label: stringField(command.label, ""),
|
|
436
|
+
...command,
|
|
437
|
+
});
|
|
438
|
+
} else if (command.cmd === "remove") {
|
|
439
|
+
const id = stringField(command.id, "");
|
|
440
|
+
nodes.delete(id);
|
|
441
|
+
for (let index = edges.length - 1; index >= 0; index -= 1) {
|
|
442
|
+
if (edges[index]?.from === id || edges[index]?.to === id) edges.splice(index, 1);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
model.nodes = [...nodes.values()];
|
|
448
|
+
model.edges = edges;
|
|
449
|
+
return applyLayeredLayout(model);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** @param {GraphModel} model */
|
|
453
|
+
function applyLayeredLayout(model) {
|
|
454
|
+
const placed = new Set(model.nodes.filter((node) => Number.isFinite(node.x) && Number.isFinite(node.y)).map((node) => node.id));
|
|
455
|
+
const byId = new Map(model.nodes.map((node) => [node.id, node]));
|
|
456
|
+
const incoming = new Map(model.nodes.map((node) => [node.id, 0]));
|
|
457
|
+
const outgoing = new Map(model.nodes.map((node) => [node.id, /** @type {string[]} */ ([])]));
|
|
458
|
+
|
|
459
|
+
for (const edge of model.edges) {
|
|
460
|
+
if (!byId.has(edge.from) || !byId.has(edge.to)) continue;
|
|
461
|
+
incoming.set(edge.to, (incoming.get(edge.to) ?? 0) + 1);
|
|
462
|
+
outgoing.get(edge.from)?.push(edge.to);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const queue = model.nodes.filter((node) => (incoming.get(node.id) ?? 0) === 0);
|
|
466
|
+
/** @type {Map<string, number>} */
|
|
467
|
+
const layer = new Map();
|
|
468
|
+
for (const node of queue) layer.set(node.id, 0);
|
|
469
|
+
|
|
470
|
+
while (queue.length > 0) {
|
|
471
|
+
const node = queue.shift();
|
|
472
|
+
if (!node) break;
|
|
473
|
+
const nextLayer = (layer.get(node.id) ?? 0) + 1;
|
|
474
|
+
for (const target of outgoing.get(node.id) ?? []) {
|
|
475
|
+
layer.set(target, Math.max(layer.get(target) ?? 0, nextLayer));
|
|
476
|
+
const targetNode = byId.get(target);
|
|
477
|
+
if (targetNode) queue.push(targetNode);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
for (const node of model.nodes) {
|
|
482
|
+
if (!layer.has(node.id)) layer.set(node.id, 0);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const horizontal = model.direction === "LR" || model.direction === "RL";
|
|
486
|
+
/** @type {Map<number, GraphNode[]>} */
|
|
487
|
+
const groups = new Map();
|
|
488
|
+
for (const node of model.nodes.filter((candidate) => candidate.type !== "container")) {
|
|
489
|
+
const rank = layer.get(node.id) ?? 0;
|
|
490
|
+
const group = groups.get(rank) ?? [];
|
|
491
|
+
group.push(node);
|
|
492
|
+
groups.set(rank, group);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
for (const [rank, nodes] of groups) {
|
|
496
|
+
nodes.forEach((node, index) => {
|
|
497
|
+
if (placed.has(node.id)) return;
|
|
498
|
+
const main = 80 + rank * 190;
|
|
499
|
+
const cross = 80 + index * 120;
|
|
500
|
+
node.x = horizontal ? main : cross;
|
|
501
|
+
node.y = horizontal ? cross : main;
|
|
502
|
+
node.width = node.width ?? defaultNodeWidth(node);
|
|
503
|
+
node.height = node.height ?? defaultNodeHeight(node);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
for (const node of model.nodes.filter((candidate) => candidate.type === "container")) {
|
|
508
|
+
/** @type {GraphNode[]} */
|
|
509
|
+
const children = [];
|
|
510
|
+
for (const childId of node.children ?? []) {
|
|
511
|
+
const child = byId.get(childId);
|
|
512
|
+
if (child && child.type !== "container") children.push(child);
|
|
513
|
+
}
|
|
514
|
+
if (children.length === 0) {
|
|
515
|
+
node.x = node.x ?? 40;
|
|
516
|
+
node.y = node.y ?? 40;
|
|
517
|
+
node.width = node.width ?? 240;
|
|
518
|
+
node.height = node.height ?? 160;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const left = Math.min(...children.map((child) => child.x ?? 0));
|
|
522
|
+
const top = Math.min(...children.map((child) => child.y ?? 0));
|
|
523
|
+
const right = Math.max(...children.map((child) => (child.x ?? 0) + (child.width ?? defaultNodeWidth(child))));
|
|
524
|
+
const bottom = Math.max(...children.map((child) => (child.y ?? 0) + (child.height ?? defaultNodeHeight(child))));
|
|
525
|
+
node.x = left - 36;
|
|
526
|
+
node.y = top - 54;
|
|
527
|
+
node.width = right - left + 72;
|
|
528
|
+
node.height = bottom - top + 90;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return model;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** @param {GraphNode} node */
|
|
535
|
+
function defaultNodeWidth(node) {
|
|
536
|
+
return node.type === "decision" ? 132 : node.type === "database" ? 136 : 150;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** @param {GraphNode} node */
|
|
540
|
+
function defaultNodeHeight(node) {
|
|
541
|
+
return node.type === "decision" ? 88 : node.type === "container" ? 160 : 62;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** @param {unknown} value @param {string} fallback */
|
|
545
|
+
function stringField(value, fallback) {
|
|
546
|
+
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** @param {unknown} value */
|
|
550
|
+
function numberField(value) {
|
|
551
|
+
const parsed = Number(value);
|
|
552
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** @param {unknown} value */
|
|
556
|
+
function arrayStringField(value) {
|
|
557
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : undefined;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* @param {http.ServerResponse} res
|
|
562
|
+
* @param {number} status
|
|
563
|
+
* @param {unknown} value
|
|
564
|
+
*/
|
|
565
|
+
function sendJson(res, status, value) {
|
|
566
|
+
res.writeHead(status, {
|
|
567
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
568
|
+
"Cache-Control": "no-store",
|
|
569
|
+
});
|
|
570
|
+
res.end(`${JSON.stringify(value, null, 2)}\n`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** @param {GraphModel} model */
|
|
574
|
+
function renderStaticSvg(model) {
|
|
575
|
+
const width = Math.max(720, ...model.nodes.map((node) => (node.x ?? 0) + (node.width ?? defaultNodeWidth(node)) + 80));
|
|
576
|
+
const height = Math.max(420, ...model.nodes.map((node) => (node.y ?? 0) + (node.height ?? defaultNodeHeight(node)) + 80));
|
|
577
|
+
const nodes = model.nodes.map(renderSvgNode).join("\n");
|
|
578
|
+
const byId = new Map(model.nodes.map((node) => [node.id, node]));
|
|
579
|
+
const edges = model.edges.map((edge) => renderSvgEdge(edge, byId)).join("\n");
|
|
580
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
|
581
|
+
<style>text{font-family:Arial,sans-serif;font-size:13px;fill:#172033}.node{fill:#f8fafc;stroke:#4064d7;stroke-width:2}.container{fill:#f8fbff;stroke:#7a8aa0;stroke-dasharray:7 5}.edge{stroke:#718096;stroke-width:2;fill:none}.label-bg{fill:#fff;stroke:#d7dde8}</style>
|
|
582
|
+
<defs><marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L0,6 L9,3 z" fill="#718096"/></marker></defs>
|
|
583
|
+
<text x="32" y="36" font-size="20" font-weight="700">${escapeXml(model.title)}</text>
|
|
584
|
+
${edges}
|
|
585
|
+
${nodes}
|
|
586
|
+
</svg>`;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** @param {GraphNode} node */
|
|
590
|
+
function renderSvgNode(node) {
|
|
591
|
+
const x = node.x ?? 0;
|
|
592
|
+
const y = node.y ?? 0;
|
|
593
|
+
const width = node.width ?? defaultNodeWidth(node);
|
|
594
|
+
const height = node.height ?? defaultNodeHeight(node);
|
|
595
|
+
const label = escapeXml(node.label);
|
|
596
|
+
if (node.type === "container") {
|
|
597
|
+
return `<g><rect class="container" x="${x}" y="${y}" width="${width}" height="${height}" rx="8"/><text x="${x + 14}" y="${y + 24}" font-weight="700">${label}</text></g>`;
|
|
598
|
+
}
|
|
599
|
+
if (node.type === "decision") {
|
|
600
|
+
const points = `${x + width / 2},${y} ${x + width},${y + height / 2} ${x + width / 2},${y + height} ${x},${y + height / 2}`;
|
|
601
|
+
return `<g><polygon class="node" points="${points}" fill="#fff8dc" stroke="#d69e2e"/><text x="${x + width / 2}" y="${y + height / 2 + 5}" text-anchor="middle">${label}</text></g>`;
|
|
602
|
+
}
|
|
603
|
+
if (node.type === "terminal" || node.type === "terminal-end") {
|
|
604
|
+
const stroke = node.type === "terminal" ? "#2f9e44" : "#d9480f";
|
|
605
|
+
return `<g><ellipse cx="${x + width / 2}" cy="${y + height / 2}" rx="${width / 2}" ry="${height / 2}" fill="#f8fafc" stroke="${stroke}" stroke-width="2"/><text x="${x + width / 2}" y="${y + height / 2 + 5}" text-anchor="middle">${label}</text></g>`;
|
|
606
|
+
}
|
|
607
|
+
return `<g><rect class="node" x="${x}" y="${y}" width="${width}" height="${height}" rx="8"/><text x="${x + width / 2}" y="${y + height / 2 + 5}" text-anchor="middle">${label}</text></g>`;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* @param {GraphEdge} edge
|
|
612
|
+
* @param {Map<string, GraphNode>} byId
|
|
613
|
+
*/
|
|
614
|
+
function renderSvgEdge(edge, byId) {
|
|
615
|
+
const from = byId.get(edge.from);
|
|
616
|
+
const to = byId.get(edge.to);
|
|
617
|
+
if (!from || !to) return "";
|
|
618
|
+
const x1 = (from.x ?? 0) + (from.width ?? defaultNodeWidth(from)) / 2;
|
|
619
|
+
const y1 = (from.y ?? 0) + (from.height ?? defaultNodeHeight(from));
|
|
620
|
+
const x2 = (to.x ?? 0) + (to.width ?? defaultNodeWidth(to)) / 2;
|
|
621
|
+
const y2 = to.y ?? 0;
|
|
622
|
+
const label = edge.label ? `<text x="${(x1 + x2) / 2}" y="${(y1 + y2) / 2 - 6}" text-anchor="middle">${escapeXml(edge.label)}</text>` : "";
|
|
623
|
+
return `<g><path class="edge" marker-end="url(#arrow)" d="M${x1},${y1} C${x1},${(y1 + y2) / 2} ${x2},${(y1 + y2) / 2} ${x2},${y2}"/>${label}</g>`;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** @param {GraphModel} model */
|
|
627
|
+
function renderDrawioXml(model) {
|
|
628
|
+
let id = 2;
|
|
629
|
+
/** @type {Map<string, number>} */
|
|
630
|
+
const ids = new Map();
|
|
631
|
+
const cells = ['<mxCell id="0"/>', '<mxCell id="1" parent="0"/>'];
|
|
632
|
+
for (const node of model.nodes) {
|
|
633
|
+
const mxId = id++;
|
|
634
|
+
ids.set(node.id, mxId);
|
|
635
|
+
const style = node.type === "decision" ? "rhombus;whiteSpace=wrap;html=1;" : "rounded=1;whiteSpace=wrap;html=1;";
|
|
636
|
+
cells.push(`<mxCell id="${mxId}" value="${escapeXml(node.label)}" style="${style}" vertex="1" parent="1"><mxGeometry x="${node.x ?? 0}" y="${node.y ?? 0}" width="${node.width ?? defaultNodeWidth(node)}" height="${node.height ?? defaultNodeHeight(node)}" as="geometry"/></mxCell>`);
|
|
637
|
+
}
|
|
638
|
+
for (const edge of model.edges) {
|
|
639
|
+
const source = ids.get(edge.from);
|
|
640
|
+
const target = ids.get(edge.to);
|
|
641
|
+
if (!source || !target) continue;
|
|
642
|
+
cells.push(`<mxCell id="${id++}" value="${escapeXml(edge.label ?? "")}" edge="1" parent="1" source="${source}" target="${target}" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;"><mxGeometry relative="1" as="geometry"/></mxCell>`);
|
|
643
|
+
}
|
|
644
|
+
return `<mxfile><diagram name="${escapeXml(model.title)}"><mxGraphModel><root>${cells.join("")}</root></mxGraphModel></diagram></mxfile>`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** @param {unknown} value */
|
|
648
|
+
function escapeXml(value) {
|
|
649
|
+
return String(value)
|
|
650
|
+
.replaceAll("&", "&")
|
|
651
|
+
.replaceAll("<", "<")
|
|
652
|
+
.replaceAll(">", ">")
|
|
653
|
+
.replaceAll('"', """);
|
|
654
|
+
}
|