@likec4/generators 1.49.0 → 1.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,169 +1,10 @@
1
- import { RichText, flattenMarkdownOrString } from "@likec4/core/types";
2
- import { isEmptyish, isNullish, keys, map, pipe, values } from "remeda";
3
1
  import { CompositeGeneratorNode, NL, expandToNode, joinToNode, toString } from "langium/generate";
4
- import { nonexhaustive } from "@likec4/core";
2
+ import { isEmptyish, isNullish, keys, map, pipe, values } from "remeda";
3
+ import { LikeC4Styles, nonexhaustive } from "@likec4/core";
4
+ import { RichText, flattenMarkdownOrString } from "@likec4/core/types";
5
+ import pako from "pako";
5
6
  import JSON5 from "json5";
6
7
  import { compareNatural, invariant, sortNaturalByFqn } from "@likec4/core/utils";
7
- const DEFAULT_ELEMENT_COLORS = {
8
- primary: {
9
- fill: "#3b82f6",
10
- stroke: "#2563eb"
11
- },
12
- gray: {
13
- fill: "#6b7280",
14
- stroke: "#4b5563"
15
- },
16
- green: {
17
- fill: "#22c55e",
18
- stroke: "#16a34a"
19
- },
20
- red: {
21
- fill: "#ef4444",
22
- stroke: "#dc2626"
23
- },
24
- blue: {
25
- fill: "#3b82f6",
26
- stroke: "#2563eb"
27
- },
28
- indigo: {
29
- fill: "#6366f1",
30
- stroke: "#4f46e5"
31
- },
32
- muted: {
33
- fill: "#9ca3af",
34
- stroke: "#6b7280"
35
- }
36
- };
37
- const DEFAULT_EDGE_COLOR = "#6b7280";
38
- function escapeXml(unsafe) {
39
- return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
40
- }
41
- function drawioShape(shape) {
42
- switch (shape) {
43
- case "person": return "shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;";
44
- case "rectangle": return "shape=rectangle;";
45
- case "browser": return "shape=rectangle;rounded=1;";
46
- case "mobile": return "shape=rectangle;rounded=1;";
47
- case "cylinder": return "shape=cylinder3;whiteSpace=wrap;boundedLbl=1;backgroundOutline=1;size=15;";
48
- case "queue": return "shape=cylinder3;whiteSpace=wrap;boundedLbl=1;backgroundOutline=1;size=15;";
49
- case "storage": return "shape=cylinder3;whiteSpace=wrap;boundedLbl=1;backgroundOutline=1;size=15;";
50
- case "bucket": return "shape=rectangle;rounded=1;";
51
- case "document": return "shape=document;whiteSpace=wrap;html=1;boundedLbl=1;";
52
- case "component": return "shape=component;";
53
- default: return "shape=rectangle;";
54
- }
55
- }
56
- function getElementColors(viewmodel, color) {
57
- const styles = "$styles" in viewmodel && viewmodel.$styles ? viewmodel.$styles : null;
58
- if (styles) try {
59
- const values = styles.colors(color);
60
- return {
61
- fill: values.elements.fill,
62
- stroke: values.elements.stroke
63
- };
64
- } catch {}
65
- return DEFAULT_ELEMENT_COLORS[color] ?? DEFAULT_ELEMENT_COLORS["primary"];
66
- }
67
- function getEdgeStrokeColor(viewmodel, color) {
68
- const styles = "$styles" in viewmodel && viewmodel.$styles ? viewmodel.$styles : null;
69
- if (styles) try {
70
- return styles.colors(color).relationships.line;
71
- } catch {}
72
- return DEFAULT_EDGE_COLOR;
73
- }
74
- function generateDrawio(viewmodel) {
75
- const view = viewmodel.$view;
76
- const { nodes, edges } = view;
77
- const rootId = "0";
78
- const defaultParentId = "1";
79
- const nodeIds = /* @__PURE__ */ new Map();
80
- let cellId = 2;
81
- const getCellId = (nodeId) => {
82
- let id = nodeIds.get(nodeId);
83
- if (!id) {
84
- id = String(cellId++);
85
- nodeIds.set(nodeId, id);
86
- }
87
- return id;
88
- };
89
- const vertexCells = [];
90
- const edgeCells = [];
91
- const sortedNodes = [...nodes].sort((a, b) => {
92
- if (isNullish(a.parent) && isNullish(b.parent)) return 0;
93
- if (isNullish(a.parent)) return -1;
94
- if (isNullish(b.parent)) return 1;
95
- if (a.parent === b.parent) return 0;
96
- if (a.id.startsWith(b.id + ".")) return 1;
97
- if (b.id.startsWith(a.id + ".")) return -1;
98
- return 0;
99
- });
100
- const getBBox = (n) => {
101
- const d = n;
102
- return {
103
- x: typeof d.x === "number" ? d.x : Array.isArray(d.position) ? d.position[0] : 0,
104
- y: typeof d.y === "number" ? d.y : Array.isArray(d.position) ? d.position[1] : 0,
105
- width: typeof d.width === "number" ? d.width : d.size?.width ?? 120,
106
- height: typeof d.height === "number" ? d.height : d.size?.height ?? 60
107
- };
108
- };
109
- for (const node of sortedNodes) {
110
- const id = getCellId(node.id);
111
- const parentId = node.parent ? getCellId(node.parent) : defaultParentId;
112
- const label = escapeXml(node.title);
113
- const shapeStyle = drawioShape(node.shape);
114
- const { x, y, width, height } = getBBox(node);
115
- const elemColors = getElementColors(viewmodel, node.color);
116
- const colorStyle = elemColors != null ? `fillColor=${elemColors.fill};strokeColor=${elemColors.stroke};fontColor=${elemColors.stroke};` : "";
117
- const description = node.description && flattenMarkdownOrString(node.description);
118
- const desc = isEmptyish(description) ? "" : escapeXml(description);
119
- const technology = node.technology && flattenMarkdownOrString(node.technology);
120
- const tech = isEmptyish(technology) ? "" : escapeXml(technology);
121
- const userData = desc !== "" || tech !== "" ? `<mxUserObject><data key="likec4Description">${desc}</data><data key="likec4Technology">${tech}</data></mxUserObject>\n ` : "";
122
- vertexCells.push(`<mxCell id="${id}" value="${label}" style="${shapeStyle}${colorStyle}verticalAlign=middle;align=center;overflow=fill;spacingLeft=2;spacingRight=2;spacingTop=2;spacingBottom=2;" vertex="1" parent="${parentId}">
123
- ${userData}<mxGeometry x="${Math.round(x)}" y="${Math.round(y)}" width="${Math.round(width)}" height="${Math.round(height)}" as="geometry" />
124
- </mxCell>`);
125
- }
126
- for (const edge of edges) {
127
- const id = String(cellId++);
128
- const sourceId = getCellId(edge.source);
129
- const targetId = getCellId(edge.target);
130
- const label = edge.label ? escapeXml(edge.label) : "";
131
- const strokeColor = getEdgeStrokeColor(viewmodel, edge.color);
132
- const dashStyle = edge.line === "dashed" ? "dashed=1;" : edge.line === "dotted" ? "dashed=1;dashPattern=1 1;" : "";
133
- edgeCells.push(`<mxCell id="${id}" value="${label}" style="endArrow=block;html=1;rounded=0;exitX=1;exitY=0.5;entryX=0;entryY=0.5;strokeColor=${strokeColor};${dashStyle}" edge="1" parent="${defaultParentId}" source="${sourceId}" target="${targetId}">
134
- <mxGeometry relative="1" as="geometry" />
135
- </mxCell>`);
136
- }
137
- let bounds = {
138
- x: 0,
139
- y: 0,
140
- width: 800,
141
- height: 600
142
- };
143
- try {
144
- const b = viewmodel.bounds;
145
- if (b != null && typeof b.x === "number") bounds = b;
146
- } catch {}
147
- const allCells = [
148
- `<mxCell id="${defaultParentId}" vertex="1" parent="${rootId}">
149
- <mxGeometry x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" as="geometry" />
150
- </mxCell>`,
151
- ...vertexCells,
152
- ...edgeCells
153
- ].join("\n");
154
- return `<?xml version="1.0" encoding="UTF-8"?>
155
- <mxfile host="LikeC4" modified="${(/* @__PURE__ */ new Date()).toISOString()}" agent="LikeC4" version="1.0" etag="" type="device">
156
- <diagram name="${escapeXml(view.id)}" id="likec4-${escapeXml(view.id)}">
157
- <mxGraphModel dx="800" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale=1 pageWidth="827" pageHeight="1169" math="0" shadow="0">
158
- <root>
159
- <mxCell id="${rootId}" />
160
- ${allCells}
161
- </root>
162
- </mxGraphModel>
163
- </diagram>
164
- </mxfile>
165
- `;
166
- }
167
8
  const capitalizeFirstLetter$2 = (value) => value.charAt(0).toLocaleUpperCase() + value.slice(1);
168
9
  const fqnName$2 = (nodeId) => nodeId.split(".").map(capitalizeFirstLetter$2).join("");
169
10
  const nodeName$2 = (node) => {
@@ -211,66 +52,165 @@ function generateD2(viewmodel) {
211
52
  };
212
53
  return toString(new CompositeGeneratorNode().append("direction: ", d2direction(view), NL, NL).append(joinToNode(nodes.filter((n) => isNullish(n.parent)), (n) => printNode(n), { appendNewLineIfNotEmpty: true })).appendIf(edges.length > 0, NL, joinToNode(edges, (e) => printEdge(e), { appendNewLineIfNotEmpty: true })));
213
54
  }
214
- const capitalizeFirstLetter$1 = (value) => value.charAt(0).toLocaleUpperCase() + value.slice(1);
215
- const fqnName$1 = (nodeId) => nodeId.split(".").map(capitalizeFirstLetter$1).join("");
216
- const nodeName$1 = (node) => {
217
- return fqnName$1(node.parent ? node.id.slice(node.parent.length + 1) : node.id);
218
- };
219
- const toSingleQuotes = (str) => str.replace(/\\?"/g, `'`);
220
- const mmdshape = ({ shape, title }) => {
221
- const label = `label: ${JSON.stringify(title)}`;
222
- switch (shape) {
223
- case "queue": return `@{ shape: horizontal-cylinder, ${label} }`;
224
- case "person": return `@{ icon: "fa:user", shape: rounded, ${label} }`;
225
- case "storage": return `@{ shape: disk, ${label} }`;
226
- case "cylinder": return `@{ shape: cylinder, ${label} }`;
227
- case "mobile":
228
- case "browser": return `@{ shape: rounded, ${label} }`;
229
- case "bucket": return `@{ shape: trap-t, ${label} }`;
230
- case "rectangle": return `@{ shape: rectangle, ${label} }`;
231
- case "document": return `@{ shape: doc, ${label} }`;
232
- case "component": return `@{ shape: rectangle, ${label} }`;
233
- default: nonexhaustive(shape);
234
- }
235
- };
236
- function generateMermaid(viewmodel) {
237
- const view = viewmodel.$view;
238
- const { nodes, edges } = view;
239
- const names = /* @__PURE__ */ new Map();
240
- const printNode = (node, parentName) => {
241
- const name = nodeName$1(node);
242
- const fqnName = (parentName ? parentName + "." : "") + name;
243
- names.set(node.id, fqnName);
244
- const baseNode = new CompositeGeneratorNode();
245
- if (node.children.length > 0) {
246
- const label = toSingleQuotes(node.title);
247
- baseNode.append("subgraph ", fqnName, "[\"`", label, "`\"]", NL).indent({
248
- indentedChildren: [joinToNode(nodes.filter((n) => n.parent === node.id), (n) => printNode(n, fqnName), { appendNewLineIfNotEmpty: true })],
249
- indentation: 2
250
- }).append("end", NL);
251
- } else baseNode.append(fqnName, mmdshape(node));
252
- return baseNode;
253
- };
254
- const printEdge = (edge) => {
255
- return new CompositeGeneratorNode().append(names.get(edge.source), " -.", edge.label ? " \"`" + toSingleQuotes(edge.label) + "`\" .-" : "-", "> ", names.get(edge.target));
256
- };
257
- return toString(new CompositeGeneratorNode().append("---", NL, `title: ${JSON.stringify(toSingleQuotes(viewmodel.titleOrId))}`, NL, "---", NL).append("graph ", view.autoLayout.direction, NL).indent({
258
- indentedChildren: (indent) => {
259
- indent.append(joinToNode(nodes.filter((n) => isNullish(n.parent)), (n) => printNode(n), { appendNewLineIfNotEmpty: true })).appendIf(edges.length > 0, joinToNode(edges, (e) => printEdge(e), { appendNewLineIfNotEmpty: true }));
260
- },
261
- indentation: 2
262
- }));
55
+ /**
56
+ * DrawIO protocol and layout constants. Single source of truth so export/parse
57
+ * stay in sync and magic numbers are named for readability.
58
+ */
59
+ /** Draw.io internal page link prefix; cell link becomes "data:page/id,likec4-<viewId>". */
60
+ const DRAWIO_PAGE_LINK_PREFIX = "data:page/id,likec4-";
61
+ /** Diagram (tab) id inside mxfile; we use "likec4-<viewId>" so Draw.io opens the correct tab. */
62
+ const DRAWIO_DIAGRAM_ID_PREFIX = "likec4-";
63
+ /** Fixed canvas size so the diagram opens centered in Draw.io (layout bounds often equal content). */
64
+ const DEFAULT_CANVAS_WIDTH = 800;
65
+ const DEFAULT_CANVAS_HEIGHT = 600;
66
+ /** Default node bbox when layout has no position (used to detect "unlaid" nodes for spread/wrap). */
67
+ const DEFAULT_NODE_WIDTH = 120;
68
+ const DEFAULT_NODE_HEIGHT = 60;
69
+ /** Vertical gap when spreading multiple nodes that share the same default bbox. */
70
+ const NODES_SPREAD_GAP = 24;
71
+ /** First id assigned to container title cells (incremented per container). */
72
+ const CONTAINER_TITLE_CELL_ID_START = 1e4;
73
+ /** Container title text cell: min/max width (px), approximate width per character, height, inset from container edge. */
74
+ const CONTAINER_TITLE_MIN_WIDTH_PX = 60;
75
+ const CONTAINER_TITLE_MAX_WIDTH_PX = 260;
76
+ const CONTAINER_TITLE_CHAR_WIDTH_PX = 8;
77
+ const CONTAINER_TITLE_HEIGHT_PX = 18;
78
+ const CONTAINER_TITLE_INSET_X = 8;
79
+ const CONTAINER_TITLE_INSET_Y = 8;
80
+ /** Max height (px) for container title area when matching title cell to container (parse). */
81
+ const CONTAINER_TITLE_AREA_MAX_HEIGHT_PX = 40;
82
+ /** Ratio of container height used for title area when matching (parse). */
83
+ const CONTAINER_TITLE_AREA_HEIGHT_RATIO = .5;
84
+ /** Tolerance (px) for title cell position inside container bounds (parse). */
85
+ const CONTAINER_TITLE_AREA_TOLERANCE = 2;
86
+ /** Default container fill opacity (0–100) when not set in style. */
87
+ const DEFAULT_CONTAINER_OPACITY = 15;
88
+ /** Default node fill/stroke/font when no theme color (hex). */
89
+ const DEFAULT_NODE_FILL_HEX = "#dae8fc";
90
+ const DEFAULT_NODE_STROKE_HEX = "#2563eb";
91
+ const DEFAULT_NODE_FONT_HEX = "#1e40af";
92
+ /** mxGraphModel page dimensions (draw.io default A4-like). */
93
+ const MXGRAPH_PAGE_WIDTH = 827;
94
+ const MXGRAPH_PAGE_HEIGHT = 1169;
95
+ /** mxGraphModel default grid origin (dx, dy) in mxGraphModel attribute. */
96
+ const MXGRAPH_DEFAULT_DX = 800;
97
+ const MXGRAPH_DEFAULT_DY = 800;
98
+ /** Default filename when exporting all views into one .drawio file (CLI and playground). */
99
+ const DEFAULT_DRAWIO_ALL_FILENAME = "diagrams.drawio";
100
+ /** LikeC4 app font (matches --mantine-font-family / --likec4-app-font-default). Used in generate-drawio for cell text. */
101
+ const LIKEC4_FONT_FAMILY = "'IBM Plex Sans Variable',ui-sans-serif,system-ui,sans-serif";
102
+ /** Container title color in diagram (matches LikeC4 diagram compound title). Used in generate-drawio for container title cell. */
103
+ const CONTAINER_TITLE_COLOR = "#74c0fc";
104
+ /**
105
+ * Shared XML escape/decode for DrawIO generate and parse.
106
+ * Single place so escaping rules stay in sync (Clean Code 8.5.2).
107
+ */
108
+ /**
109
+ * Escape for use inside XML attributes and text.
110
+ * @param unsafe - Raw string that may contain &, <, >, ", '
111
+ * @returns XML-safe string with entities escaped
112
+ */
113
+ function escapeXml(unsafe) {
114
+ return unsafe.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
115
+ }
116
+ /**
117
+ * Decode XML entities (inverse of escapeXml for the five standard entities).
118
+ * @param s - String with &lt; &gt; &quot; &apos; &amp;
119
+ * @returns Decoded string
120
+ */
121
+ function decodeXmlEntities(s) {
122
+ return s.replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", "\"").replaceAll("&apos;", "'").replaceAll("&amp;", "&");
263
123
  }
124
+ /**
125
+ * Parse DrawIO (mxGraph) XML and generate LikeC4 source code.
126
+ * Extracts vertices as elements and edges as relations; preserves colors, descriptions,
127
+ * technology and other compatible attributes for full bidirectional compatibility.
128
+ * Supports both uncompressed (raw mxGraphModel inside <diagram>) and compressed
129
+ * (base64 + deflate, draw.io default) diagram content.
130
+ */
131
+ /**
132
+ * Normalize unknown to a string for error messages (Error → message; else String).
133
+ * @param err - Caught value (Error or other).
134
+ * @returns Human-readable string for logging or rethrow.
135
+ */
136
+ function toErrorMessage(err) {
137
+ return err instanceof Error ? err.message : String(err);
138
+ }
139
+ /** Type guard: true when cell is an edge with non-empty source and target. */
140
+ function isEdgeWithEndpoints(c) {
141
+ return c.edge === true && typeof c.source === "string" && typeof c.target === "string";
142
+ }
143
+ /** True if char is space, tab, or newline (word boundary for attr names). */
144
+ function isAttrBoundaryChar(c) {
145
+ return c === " " || c === " " || c === "\n" || c === "\r";
146
+ }
147
+ /** Get attribute value from open-tag string (e.g. "id=\"x\" vertex=\"1\""). No regex to avoid ReDoS. Matches only on word boundary so "id=" does not match "userid=". */
264
148
  function getAttr(attrs, name) {
265
- const re = new RegExp(`${name}="([^"]*)"`, "i");
266
- const m = attrs.match(re);
267
- return m ? m[1] : void 0;
149
+ const needle = `${name}=`;
150
+ const lower = attrs.toLowerCase();
151
+ const needleLower = needle.toLowerCase();
152
+ let i = 0;
153
+ while (i < attrs.length) {
154
+ const start = lower.indexOf(needleLower, i);
155
+ if (start === -1) return void 0;
156
+ if (!isAttrBoundaryChar(start === 0 ? " " : attrs[start - 1] ?? " ")) {
157
+ i = start + 1;
158
+ continue;
159
+ }
160
+ const quoteStart = start + needle.length;
161
+ if (quoteStart < attrs.length && attrs[quoteStart] === "\"") {
162
+ const valueEnd = attrs.indexOf("\"", quoteStart + 1);
163
+ if (valueEnd !== -1) return attrs.slice(quoteStart + 1, valueEnd);
164
+ }
165
+ i = start + 1;
166
+ }
167
+ }
168
+ /** Find end of XML open tag (first unquoted '>'). Handles both single- and double-quoted attributes. Avoids regex for S5852. */
169
+ function findOpenTagEnd(xml, start) {
170
+ let quoteChar = "";
171
+ let i = start;
172
+ while (i < xml.length) {
173
+ const c = xml[i];
174
+ if (c === "\"" || c === "'") {
175
+ if (quoteChar === "") quoteChar = c;
176
+ else if (quoteChar === c) quoteChar = "";
177
+ } else if (c === ">" && quoteChar === "") return i;
178
+ i += 1;
179
+ }
180
+ return -1;
268
181
  }
269
- function parseNum(s) {
270
- if (s === void 0 || s === "") return void 0;
271
- const n = Number.parseFloat(s);
272
- return Number.isNaN(n) ? void 0 : n;
182
+ /** Find start of tag '<tagName' (case-insensitive). */
183
+ function indexOfTagStart(xml, tagName, fromIndex) {
184
+ const lower = xml.toLowerCase();
185
+ const needle = `<${tagName.toLowerCase()}`;
186
+ return lower.indexOf(needle, fromIndex);
273
187
  }
188
+ /** Find start of closing tag '</tagName>' (case-insensitive). */
189
+ function indexOfClosingTag(xml, tagName, fromIndex) {
190
+ const lower = xml.toLowerCase();
191
+ const needle = `</${tagName.toLowerCase()}>`;
192
+ return lower.indexOf(needle, fromIndex);
193
+ }
194
+ /** Parse numeric attribute; returns undefined if missing or not a number. */
195
+ function parseNum(str) {
196
+ if (str === void 0 || str === "") return void 0;
197
+ const num = Number.parseFloat(str);
198
+ return Number.isNaN(num) ? void 0 : num;
199
+ }
200
+ /** Get style value decoded for URI component; undefined if missing or empty. Safe against malformed percent-encoding. */
201
+ function getDecodedStyle(styleMap, key) {
202
+ const v = styleMap.get(key);
203
+ if (v == null || v === "") return void 0;
204
+ try {
205
+ return decodeURIComponent(v);
206
+ } catch {
207
+ return v;
208
+ }
209
+ }
210
+ /**
211
+ * Parse DrawIO style string (semicolon-separated key=value) into a map.
212
+ * Entries with empty values are intentionally dropped (meaningful style values are non-empty).
213
+ */
274
214
  function parseStyle(style) {
275
215
  const map = /* @__PURE__ */ new Map();
276
216
  if (!style) return map;
@@ -284,198 +224,2026 @@ function parseStyle(style) {
284
224
  }
285
225
  return map;
286
226
  }
227
+ /** Keys we map to dedicated DrawioCell fields; other data keys go to customData. */
228
+ const MAPPED_DATA_KEYS = new Set(["likec4description", "likec4technology"]);
229
+ /** Extract all <data key="...">...</data> from mxUserObject inner XML. Index-based to avoid ReDoS. */
230
+ function parseAllUserData(fullTag) {
231
+ const out = {};
232
+ let from = 0;
233
+ for (;;) {
234
+ const tagStart = indexOfTagStart(fullTag, "data", from);
235
+ if (tagStart === -1) break;
236
+ const endOpen = findOpenTagEnd(fullTag, tagStart);
237
+ if (endOpen === -1) break;
238
+ const key = getAttr(fullTag.slice(tagStart, endOpen + 1), "key")?.trim();
239
+ const closeStart = indexOfClosingTag(fullTag, "data", endOpen + 1);
240
+ if (closeStart === -1) break;
241
+ const raw = fullTag.slice(endOpen + 1, closeStart).trim();
242
+ if (key) out[key] = decodeXmlEntities(raw);
243
+ from = closeStart + 7;
244
+ }
245
+ return out;
246
+ }
247
+ /** Extract open tag <mxGeometry ...> from cell XML. No regex to avoid ReDoS. */
248
+ function extractMxGeometryOpenTag(fullTag) {
249
+ const tagStart = indexOfTagStart(fullTag, "mxGeometry", 0);
250
+ if (tagStart === -1) return "";
251
+ const endOpen = findOpenTagEnd(fullTag, tagStart);
252
+ if (endOpen === -1) return "";
253
+ return fullTag.slice(tagStart, endOpen + 1);
254
+ }
255
+ /** Extract inner content of first <mxGeometry>...</mxGeometry> in cell XML. No regex to avoid ReDoS. */
256
+ function extractMxGeometryInner(fullTag) {
257
+ const tagStart = indexOfTagStart(fullTag, "mxGeometry", 0);
258
+ if (tagStart === -1) return void 0;
259
+ const endOpen = findOpenTagEnd(fullTag, tagStart);
260
+ if (endOpen === -1) return void 0;
261
+ const closeStart = indexOfClosingTag(fullTag, "mxGeometry", endOpen + 1);
262
+ if (closeStart === -1) return void 0;
263
+ return fullTag.slice(endOpen + 1, closeStart);
264
+ }
265
+ /** Extract edge waypoints from mxGeometry Array/mxPoint inside cell XML. Returns JSON array of [x,y][] or undefined. */
266
+ function parseEdgePoints(fullTag) {
267
+ const inner = extractMxGeometryInner(fullTag);
268
+ if (inner === void 0) return void 0;
269
+ const points = [];
270
+ let from = 0;
271
+ for (;;) {
272
+ const tagStart = indexOfTagStart(inner, "mxPoint", from);
273
+ if (tagStart === -1) break;
274
+ const endOpen = findOpenTagEnd(inner, tagStart);
275
+ if (endOpen === -1) break;
276
+ const tag = inner.slice(tagStart, endOpen + 1);
277
+ const px = parseNum(getAttr(tag, "x"));
278
+ const py = parseNum(getAttr(tag, "y"));
279
+ if (px !== void 0 && py !== void 0) points.push([px, py]);
280
+ from = endOpen + 1;
281
+ }
282
+ if (points.length === 0) return void 0;
283
+ return JSON.stringify(points);
284
+ }
285
+ /** Extract LikeC4 custom data from mxUserObject/data inside cell XML. */
287
286
  function parseUserData(fullTag) {
287
+ const all = parseAllUserData(fullTag);
288
288
  const out = {};
289
- const descMatch = fullTag.match(/<data\s+key="likec4Description"[^>]*>([\s\S]*?)<\/data>/i);
290
- if (descMatch?.[1]) out.description = decodeXmlEntities(descMatch[1].trim());
291
- const techMatch = fullTag.match(/<data\s+key="likec4Technology"[^>]*>([\s\S]*?)<\/data>/i);
292
- if (techMatch?.[1]) out.technology = decodeXmlEntities(techMatch[1].trim());
289
+ if (all["likec4Description"] != null) out.description = all["likec4Description"];
290
+ if (all["likec4Technology"] != null) out.technology = all["likec4Technology"];
291
+ const rest = {};
292
+ for (const [k, v] of Object.entries(all)) if (!MAPPED_DATA_KEYS.has(k.toLowerCase()) && v != null && v !== "") rest[k] = v;
293
+ if (Object.keys(rest).length > 0) out.customData = JSON.stringify(rest);
293
294
  return out;
294
295
  }
295
- function parseDrawioXml(xml) {
296
- const cells = [];
297
- const mxCellRe = /<mxCell\s+([^>]+?)(?:\s*\/>|>([\s\S]*?)<\/mxCell>)/gi;
298
- const geomAttr = (tag, name) => getAttr(tag, name);
299
- let m;
300
- while ((m = mxCellRe.exec(xml)) !== null) {
301
- const attrs = m[1] ?? "";
302
- const inner = m[2] ?? "";
303
- const id = getAttr(attrs, "id");
304
- if (!id) continue;
305
- const valueRaw = getAttr(attrs, "value");
306
- const parent = getAttr(attrs, "parent");
307
- const source = getAttr(attrs, "source");
308
- const target = getAttr(attrs, "target");
309
- const vertex = getAttr(attrs, "vertex") === "1";
310
- const edge = getAttr(attrs, "edge") === "1";
311
- const style = getAttr(attrs, "style");
312
- const geomMatch = m[0].match(/<mxGeometry[^>]*>/i);
313
- const geomStr = geomMatch ? geomMatch[0] : "";
314
- const styleMap = parseStyle(style ?? void 0);
315
- const userData = parseUserData(inner);
316
- const x = parseNum(geomAttr(geomStr, "x"));
317
- const y = parseNum(geomAttr(geomStr, "y"));
318
- const width = parseNum(geomAttr(geomStr, "width"));
319
- const height = parseNum(geomAttr(geomStr, "height"));
320
- const fillColor = styleMap.get("fillcolor") ?? styleMap.get("fillColor");
321
- const strokeColor = styleMap.get("strokecolor") ?? styleMap.get("strokeColor");
322
- const cell = {
323
- id,
324
- ...valueRaw != null && valueRaw !== "" ? { value: decodeXmlEntities(valueRaw) } : {},
325
- ...parent != null && parent !== "" ? { parent } : {},
326
- ...source != null && source !== "" ? { source } : {},
327
- ...target != null && target !== "" ? { target } : {},
296
+ /** Extract viewId from Draw.io internal page link for navigateTo round-trip. No regex to avoid ReDoS. */
297
+ function navigateToFromLink(link) {
298
+ if (!link || link === "") return void 0;
299
+ const prefix = DRAWIO_PAGE_LINK_PREFIX.toLowerCase();
300
+ if (!link.toLowerCase().startsWith(prefix)) return void 0;
301
+ return link.slice(20).trim();
302
+ }
303
+ /** Optional fields for DrawioCell (SOLID: single place for conditional spreads). */
304
+ function buildCellOptionalFields(params) {
305
+ const { styleMap, userData, geomStr, fullTag, vertex, edge, navigateTo } = params;
306
+ const x = parseNum(getAttr(geomStr, "x"));
307
+ const y = parseNum(getAttr(geomStr, "y"));
308
+ const width = parseNum(getAttr(geomStr, "width"));
309
+ const height = parseNum(getAttr(geomStr, "height"));
310
+ const fillColor = styleMap.get("fillcolor");
311
+ const strokeColor = styleMap.get("strokecolor");
312
+ const description = userData.description ?? getDecodedStyle(styleMap, "likec4description");
313
+ const technology = userData.technology ?? getDecodedStyle(styleMap, "likec4technology");
314
+ const notes = getDecodedStyle(styleMap, "likec4notes");
315
+ const tags = getDecodedStyle(styleMap, "likec4tags");
316
+ const icon = getDecodedStyle(styleMap, "likec4icon");
317
+ const endArrow = styleMap.get("endarrow");
318
+ const startArrow = styleMap.get("startarrow");
319
+ const dashed = styleMap.get("dashed");
320
+ const dashPattern = styleMap.get("dashpattern");
321
+ const summary = getDecodedStyle(styleMap, "likec4summary");
322
+ const links = getDecodedStyle(styleMap, "likec4links");
323
+ const border = getDecodedStyle(styleMap, "likec4border");
324
+ const colorName = getDecodedStyle(styleMap, "likec4colorname");
325
+ const opacityFromStyle = styleMap.get("opacity");
326
+ const opacityFromLikec4 = styleMap.get("likec4opacity");
327
+ const opacityFromFill = styleMap.get("fillopacity");
328
+ const opacity = (opacityFromLikec4 != null && opacityFromLikec4 !== "" ? opacityFromLikec4 : void 0) ?? (opacityFromStyle != null && opacityFromStyle !== "" ? opacityFromStyle : void 0) ?? (opacityFromFill != null && opacityFromFill !== "" ? opacityFromFill : void 0);
329
+ const strokeWidthRaw = styleMap.get("strokewidth");
330
+ const strokeWidth = strokeWidthRaw != null && strokeWidthRaw !== "" ? strokeWidthRaw : void 0;
331
+ const size = getDecodedStyle(styleMap, "likec4size");
332
+ const padding = getDecodedStyle(styleMap, "likec4padding");
333
+ const textSize = getDecodedStyle(styleMap, "likec4textsize");
334
+ const iconPosition = getDecodedStyle(styleMap, "likec4iconposition");
335
+ const link = getDecodedStyle(styleMap, "link");
336
+ const relationshipKind = getDecodedStyle(styleMap, "likec4relationshipkind");
337
+ const notation = getDecodedStyle(styleMap, "likec4notation");
338
+ const metadata = getDecodedStyle(styleMap, "likec4metadata");
339
+ const optional = {};
340
+ if (params.valueRaw != null && params.valueRaw !== "") optional.value = decodeXmlEntities(params.valueRaw);
341
+ if (params.parent != null && params.parent !== "") optional.parent = params.parent;
342
+ if (params.source != null && params.source !== "") optional.source = params.source;
343
+ if (params.target != null && params.target !== "") optional.target = params.target;
344
+ if (params.style != null && params.style !== "") optional.style = params.style;
345
+ if (x !== void 0) optional.x = x;
346
+ if (y !== void 0) optional.y = y;
347
+ if (width !== void 0) optional.width = width;
348
+ if (height !== void 0) optional.height = height;
349
+ if (fillColor !== void 0) optional.fillColor = fillColor;
350
+ if (strokeColor !== void 0) optional.strokeColor = strokeColor;
351
+ if (description != null) optional.description = description;
352
+ if (technology != null) optional.technology = technology;
353
+ if (notes != null) optional.notes = notes;
354
+ if (tags != null) optional.tags = tags;
355
+ if (navigateTo != null) optional.navigateTo = navigateTo;
356
+ if (icon != null) optional.icon = icon;
357
+ if (endArrow != null && endArrow !== "") optional.endArrow = endArrow;
358
+ if (startArrow != null && startArrow !== "") optional.startArrow = startArrow;
359
+ if (dashed != null && dashed !== "") optional.dashed = dashed;
360
+ if (dashPattern != null && dashPattern !== "") optional.dashPattern = dashPattern;
361
+ if (summary != null) optional.summary = summary;
362
+ if (links != null) optional.links = links;
363
+ if (link != null && vertex) optional.link = link;
364
+ if (border != null) optional.border = border;
365
+ if (strokeWidth != null && vertex) optional.strokeWidth = strokeWidth;
366
+ if (size != null && vertex) optional.size = size;
367
+ if (padding != null && vertex) optional.padding = padding;
368
+ if (textSize != null && vertex) optional.textSize = textSize;
369
+ if (iconPosition != null && vertex) optional.iconPosition = iconPosition;
370
+ if (colorName != null) optional.colorName = colorName;
371
+ if (opacity != null) optional.opacity = opacity;
372
+ if (relationshipKind != null) optional.relationshipKind = relationshipKind;
373
+ if (notation != null) optional.notation = notation;
374
+ if (metadata != null && edge) optional.metadata = metadata;
375
+ if (userData.customData != null) optional.customData = userData.customData;
376
+ if (edge) {
377
+ const pts = parseEdgePoints(fullTag);
378
+ if (pts != null) optional.edgePoints = pts;
379
+ }
380
+ return optional;
381
+ }
382
+ /**
383
+ * Build one DrawioCell from parsed mxCell tag parts.
384
+ * attrs: open-tag attribute string; inner: content between open/close; fullTag: full tag for geometry regex.
385
+ * overrides: optional id/navigateTo to override parsed values. Returns null if id missing.
386
+ */
387
+ function buildCellFromMxCell(attrs, inner, fullTag, overrides) {
388
+ const id = overrides?.id ?? getAttr(attrs, "id");
389
+ if (!id) return null;
390
+ const vertex = getAttr(attrs, "vertex") === "1";
391
+ const edge = getAttr(attrs, "edge") === "1";
392
+ const style = getAttr(attrs, "style");
393
+ const geomStr = extractMxGeometryOpenTag(fullTag);
394
+ const styleMap = parseStyle(style ?? void 0);
395
+ const userData = parseUserData(inner);
396
+ const navigateTo = overrides?.navigateTo ?? getDecodedStyle(styleMap, "likec4navigateto");
397
+ return {
398
+ id,
399
+ vertex,
400
+ edge,
401
+ ...buildCellOptionalFields({
402
+ valueRaw: getAttr(attrs, "value"),
403
+ parent: getAttr(attrs, "parent"),
404
+ source: getAttr(attrs, "source"),
405
+ target: getAttr(attrs, "target"),
406
+ style: getAttr(attrs, "style"),
407
+ styleMap,
408
+ userData,
409
+ geomStr,
410
+ fullTag,
328
411
  vertex,
329
412
  edge,
330
- ...style != null && style !== "" ? { style } : {},
331
- ...x !== void 0 ? { x } : {},
332
- ...y !== void 0 ? { y } : {},
333
- ...width !== void 0 ? { width } : {},
334
- ...height !== void 0 ? { height } : {},
335
- ...fillColor !== void 0 ? { fillColor } : {},
336
- ...strokeColor !== void 0 ? { strokeColor } : {},
337
- ...userData.description != null ? { description: userData.description } : {},
338
- ...userData.technology != null ? { technology: userData.technology } : {}
339
- };
340
- cells.push(cell);
413
+ navigateTo
414
+ })
415
+ };
416
+ }
417
+ /** Extract one mxCell from xml starting at tagStart. Returns attrs, inner, fullTag and next search index, or null. */
418
+ function extractOneMxCell(xml, tagStart) {
419
+ const endOpen = findOpenTagEnd(xml, tagStart);
420
+ if (endOpen === -1) return null;
421
+ const attrs = xml.slice(tagStart + 7, endOpen).trim();
422
+ const isSelfClosing = xml.slice(tagStart, endOpen).trimEnd().endsWith("/");
423
+ const afterBracket = endOpen + 1;
424
+ let inner;
425
+ let endTagPos;
426
+ if (isSelfClosing) {
427
+ inner = "";
428
+ endTagPos = endOpen;
429
+ } else {
430
+ const closeStart = indexOfClosingTag(xml, "mxCell", afterBracket);
431
+ if (closeStart === -1) return null;
432
+ inner = xml.slice(afterBracket, closeStart);
433
+ endTagPos = closeStart + 9 - 1;
434
+ }
435
+ const fullTag = xml.slice(tagStart, endTagPos + 1);
436
+ return {
437
+ attrs,
438
+ inner,
439
+ fullTag,
440
+ next: endTagPos + 1
441
+ };
442
+ }
443
+ /**
444
+ * Simple XML parser for DrawIO mxCell elements. Extracts cells with id, value, parent,
445
+ * source, target, vertex, edge, geometry, style colors and LikeC4 user data.
446
+ * Also parses <UserObject id="..." link="..."> wrapping mxCell (export format for navigateTo) so the inner mxCell gets id and navigateTo from the link.
447
+ * Uses indexOf-based extraction instead of regex to avoid S5852 (super-linear backtracking DoS).
448
+ */
449
+ function parseDrawioXml(xml) {
450
+ const cells = [];
451
+ const parsedIds = /* @__PURE__ */ new Set();
452
+ const closeUserObjectLen = 13;
453
+ let uoStart = indexOfTagStart(xml, "UserObject", 0);
454
+ while (uoStart !== -1) {
455
+ const endOpen = findOpenTagEnd(xml, uoStart);
456
+ if (endOpen === -1) break;
457
+ const openTag = xml.slice(uoStart, endOpen + 1);
458
+ const userObjId = getAttr(openTag, "id")?.trim();
459
+ const navigateTo = navigateToFromLink(getAttr(openTag, "link") ?? void 0);
460
+ const closeStart = indexOfClosingTag(xml, "UserObject", endOpen + 1);
461
+ if (closeStart === -1) break;
462
+ const innerXml = xml.slice(endOpen + 1, closeStart);
463
+ if (userObjId) {
464
+ const innerMxStart = indexOfTagStart(innerXml, "mxCell", 0);
465
+ if (innerMxStart !== -1) {
466
+ const mx = extractOneMxCell(innerXml, innerMxStart);
467
+ if (mx) {
468
+ const fullTag = `<mxCell id="${escapeXml(userObjId)}" ${mx.attrs}>${innerXml}</mxCell>`;
469
+ const cell = buildCellFromMxCell(mx.attrs, innerXml, fullTag, navigateTo != null && navigateTo !== "" ? {
470
+ id: userObjId,
471
+ navigateTo
472
+ } : { id: userObjId });
473
+ if (cell) {
474
+ cells.push(cell);
475
+ parsedIds.add(cell.id);
476
+ }
477
+ }
478
+ }
479
+ }
480
+ uoStart = indexOfTagStart(xml, "UserObject", closeStart + closeUserObjectLen);
481
+ }
482
+ let mxStart = indexOfTagStart(xml, "mxCell", 0);
483
+ while (mxStart !== -1) {
484
+ const mx = extractOneMxCell(xml, mxStart);
485
+ if (mx) {
486
+ const id = getAttr(mx.attrs, "id");
487
+ if (id && !parsedIds.has(id)) {
488
+ const cell = buildCellFromMxCell(mx.attrs, mx.inner, mx.fullTag);
489
+ if (cell) {
490
+ cells.push(cell);
491
+ parsedIds.add(cell.id);
492
+ }
493
+ }
494
+ mxStart = indexOfTagStart(xml, "mxCell", mx.next);
495
+ } else mxStart = indexOfTagStart(xml, "mxCell", mxStart + 7);
341
496
  }
342
497
  return cells;
343
498
  }
344
- function decodeXmlEntities(s) {
345
- return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&apos;/g, "'").replace(/&amp;/g, "&");
499
+ /** Map draw.io endArrow/startArrow style value to LikeC4 RelationshipArrowType. */
500
+ function likec4Arrow(drawioArrow) {
501
+ if (!drawioArrow || drawioArrow === "") return void 0;
502
+ switch (drawioArrow.toLowerCase()) {
503
+ case "none": return "none";
504
+ case "block": return "normal";
505
+ case "open": return "open";
506
+ case "diamond": return "diamond";
507
+ case "oval": return "dot";
508
+ default: return "normal";
509
+ }
510
+ }
511
+ /** Map draw.io dashed/dashPattern to LikeC4 line type. */
512
+ function likec4LineType(dashed, dashPattern) {
513
+ if (dashed === "1" || dashed === "true") {
514
+ const pattern = (dashPattern ?? "").trim();
515
+ if (pattern === "1 1" || pattern === "2 2") return "dotted";
516
+ return "dashed";
517
+ }
518
+ }
519
+ /**
520
+ * Infer LikeC4 element kind from DrawIO shape style. When parent is a container (container=1), child is component.
521
+ * Explicit container=1 in style → system (context box); others default to container unless actor/swimlane.
522
+ */
523
+ function inferKind(style, parentCell) {
524
+ const s = style?.toLowerCase() ?? "";
525
+ switch (true) {
526
+ case !style: return parentCell?.style?.toLowerCase().includes("container=1") ? "component" : "container";
527
+ case s.includes("umlactor") || s.includes("shape=person"): return "actor";
528
+ case s.includes("swimlane"):
529
+ case s.includes("container=1"): return "system";
530
+ case !!parentCell?.style?.toLowerCase().includes("container=1"): return "component";
531
+ default: return "container";
532
+ }
346
533
  }
347
- function inferKind(style) {
348
- if (!style) return "container";
534
+ /** Infer LikeC4 shape from DrawIO style when possible (cylinder, document, etc.). */
535
+ function inferShape(style) {
536
+ if (!style) return void 0;
349
537
  const s = style.toLowerCase();
350
- if (s.includes("umlactor") || s.includes("shape=person")) return "actor";
351
- if (s.includes("swimlane") || s.includes("shape=rectangle") && s.includes("rounded")) return "system";
352
- return "container";
538
+ if (s.includes("shape=cylinder") || s.includes("cylinder3")) return "cylinder";
539
+ if (s.includes("shape=document")) return "document";
540
+ if (s.includes("shape=rectangle") && s.includes("rounded")) return "rectangle";
353
541
  }
542
+ /**
543
+ * Sanitize a string for use as LikeC4 identifier (element name).
544
+ */
354
545
  function toId(name) {
355
- return name.trim().replace(/\s+/g, "_").replace(/[^a-zA-Z0-9_.-]/g, "").replace(/^[0-9]/, "_$&") || "element";
546
+ return name.trim().replaceAll(/\s+/g, "_").replaceAll(/[^\w-]/g, "").replace(/^[0-9]/, "_$&") || "element";
356
547
  }
357
- function parseDrawioToLikeC4(xml) {
358
- const cells = parseDrawioXml(xml);
359
- const byId = /* @__PURE__ */ new Map();
360
- for (const c of cells) byId.set(c.id, c);
361
- const vertices = cells.filter((c) => c.vertex && c.id !== "1");
362
- const edges = cells.filter((c) => c.edge && c.source && c.target);
363
- const rootId = "1";
364
- const idToFqn = /* @__PURE__ */ new Map();
365
- const idToCell = /* @__PURE__ */ new Map();
366
- for (const v of vertices) idToCell.set(v.id, v);
367
- const usedNames = /* @__PURE__ */ new Set();
368
- function uniqueName(base) {
548
+ /** Returns a unique name generator that avoids collisions by appending _1, _2, ... (DRY: used in parseDrawioToLikeC4 and buildDiagramState). */
549
+ function makeUniqueName(usedNames) {
550
+ return (base) => {
369
551
  let name = toId(base || "element");
370
552
  let n = name;
371
553
  let i = 0;
372
554
  while (usedNames.has(n)) n = `${name}_${++i}`;
373
555
  usedNames.add(n);
374
556
  return n;
375
- }
376
- for (const v of vertices) if (v.parent === rootId || !v.parent) {
377
- const name = uniqueName(v.value ?? v.id);
378
- idToFqn.set(v.id, name);
379
- }
557
+ };
558
+ }
559
+ /** Assign FQNs to element vertices: root first, then hierarchy by parent, then orphans (DRY). */
560
+ function assignFqnsToElementVertices(idToFqn, elementVertices, containerIdToTitle, isRootParent, uniqueName) {
561
+ const baseName = (v) => v.value ?? containerIdToTitle.get(v.id) ?? v.id;
562
+ for (const v of elementVertices) if (isRootParent(v.parent)) idToFqn.set(v.id, uniqueName(baseName(v)));
380
563
  let changed = true;
381
564
  while (changed) {
382
565
  changed = false;
383
- for (const v of vertices) {
566
+ for (const v of elementVertices) {
384
567
  if (idToFqn.has(v.id)) continue;
385
568
  const parent = v.parent ? idToFqn.get(v.parent) : null;
386
569
  if (parent != null) {
387
- const local = uniqueName(v.value ?? v.id);
388
- idToFqn.set(v.id, `${parent}.${local}`);
570
+ idToFqn.set(v.id, `${parent}.${uniqueName(baseName(v))}`);
389
571
  changed = true;
390
572
  }
391
573
  }
392
574
  }
393
- for (const v of vertices) if (!idToFqn.has(v.id)) idToFqn.set(v.id, uniqueName(v.value ?? v.id));
575
+ for (const v of elementVertices) if (!idToFqn.has(v.id)) idToFqn.set(v.id, uniqueName(baseName(v)));
576
+ }
577
+ /** Build map of hex color -> custom name from vertices and edges for specification customColors (DRY). */
578
+ function buildHexToCustomName(elementVertices, edges) {
394
579
  const hexToCustomName = /* @__PURE__ */ new Map();
395
580
  let customColorIndex = 0;
396
- for (const v of vertices) {
581
+ for (const v of elementVertices) {
397
582
  const fill = v.fillColor?.trim();
398
583
  if (fill && /^#[0-9A-Fa-f]{3,8}$/.test(fill)) {
399
- if (!hexToCustomName.has(fill)) hexToCustomName.set(fill, `drawio_color_${++customColorIndex}`);
584
+ if (v.colorName?.trim()) {
585
+ const name = v.colorName.trim().replaceAll(/\s+/g, "_");
586
+ if (!hexToCustomName.has(fill)) hexToCustomName.set(fill, name);
587
+ } else if (!hexToCustomName.has(fill)) hexToCustomName.set(fill, `drawio_color_${++customColorIndex}`);
400
588
  }
401
589
  }
402
- const lines = [];
403
- if (hexToCustomName.size > 0) {
404
- lines.push("specification {");
405
- for (const [hex, name] of hexToCustomName) lines.push(` color ${name} ${hex}`);
406
- lines.push("}");
407
- lines.push("");
590
+ let edgeColorIndex = 0;
591
+ for (const e of edges) {
592
+ const stroke = e.strokeColor?.trim();
593
+ if (stroke && /^#[0-9A-Fa-f]{3,8}$/.test(stroke) && !hexToCustomName.has(stroke)) hexToCustomName.set(stroke, `drawio_edge_color_${++edgeColorIndex}`);
408
594
  }
409
- lines.push("model {");
410
- lines.push("");
411
- const children = /* @__PURE__ */ new Map();
412
- const roots = [];
413
- for (const [cellId, fqn] of idToFqn) {
414
- const cell = idToCell.get(cellId);
415
- if (!cell) continue;
416
- if (cell.parent === rootId || !cell.parent) roots.push({
417
- cellId,
418
- fqn
419
- });
420
- else {
421
- const parentFqn = idToFqn.get(cell.parent);
422
- if (parentFqn != null) {
423
- const list = children.get(parentFqn) ?? [];
424
- list.push({
425
- cellId,
426
- fqn
427
- });
428
- children.set(parentFqn, list);
429
- } else roots.push({
430
- cellId,
431
- fqn
432
- });
595
+ return hexToCustomName;
596
+ }
597
+ /** Detect container cells and their title cells (text shape inside/near container); returns maps for FQN resolution (DRY). */
598
+ function computeContainerTitles(vertices) {
599
+ const containerIdToTitle = /* @__PURE__ */ new Map();
600
+ const titleCellIds = /* @__PURE__ */ new Set();
601
+ const containerCells = vertices.filter((v) => v.style?.toLowerCase().includes("container=1") && v.x != null && v.y != null && v.width != null && v.height != null);
602
+ for (const cont of containerCells) {
603
+ const cw = cont.width, ch = cont.height;
604
+ const titleAreaHeight = Math.min(CONTAINER_TITLE_AREA_MAX_HEIGHT_PX, ch * CONTAINER_TITLE_AREA_HEIGHT_RATIO) + CONTAINER_TITLE_AREA_TOLERANCE;
605
+ const best = vertices.find((v) => v.id !== cont.id && v.parent === cont.id && (v.style?.toLowerCase().includes("shape=text") || v.style?.toLowerCase().includes("text;")) && v.x != null && v.y != null && v.x >= -CONTAINER_TITLE_AREA_TOLERANCE && v.x <= cw + CONTAINER_TITLE_AREA_TOLERANCE && v.y >= -CONTAINER_TITLE_AREA_TOLERANCE && v.y <= titleAreaHeight);
606
+ if (best) {
607
+ const raw = (best.value ?? "").trim();
608
+ if (raw) {
609
+ containerIdToTitle.set(cont.id, stripHtml(raw));
610
+ titleCellIds.add(best.id);
611
+ }
433
612
  }
434
613
  }
435
- function emitElement(cellId, fqn, indent) {
436
- const cell = idToCell.get(cellId);
437
- if (!cell) return;
438
- const kind = inferKind(cell.style);
439
- const title = cell.value && cell.value.trim() || fqn.split(".").pop() || "Element";
440
- const name = fqn.split(".").pop();
441
- const pad = " ".repeat(indent);
442
- const desc = cell.description?.trim();
443
- const tech = cell.technology?.trim();
444
- const colorName = cell.fillColor && /^#[0-9A-Fa-f]{3,8}$/.test(cell.fillColor.trim()) ? hexToCustomName.get(cell.fillColor.trim()) : void 0;
445
- if (kind === "actor") lines.push(`${pad}${name} = actor '${title.replace(/'/g, "''")}'`);
446
- else if (kind === "system") lines.push(`${pad}${name} = system '${title.replace(/'/g, "''")}'`);
447
- else lines.push(`${pad}${name} = container '${title.replace(/'/g, "''")}'`);
448
- const childList = children.get(fqn);
449
- if (childList && childList.length > 0 || desc || tech || colorName) {
450
- lines.push(`${pad}{`);
451
- if (colorName) lines.push(`${pad} style { color ${colorName} }`);
452
- if (desc) lines.push(`${pad} description '${desc.replace(/'/g, "''")}'`);
453
- if (tech) lines.push(`${pad} technology '${tech.replace(/'/g, "''")}'`);
454
- if (childList && childList.length > 0) for (const ch of childList) emitElement(ch.cellId, ch.fqn, indent + 1);
455
- lines.push(`${pad}}`);
456
- } else {
457
- lines.push(`${pad}{`);
458
- lines.push(`${pad}}`);
614
+ return {
615
+ containerIdToTitle,
616
+ titleCellIds
617
+ };
618
+ }
619
+ /** Strip XML/HTML tags without regex to avoid S5852 (super-linear backtracking). */
620
+ function stripTags(s) {
621
+ let out = "";
622
+ let i = 0;
623
+ while (i < s.length) {
624
+ const open = s.indexOf("<", i);
625
+ if (open === -1) {
626
+ out += s.slice(i);
627
+ break;
459
628
  }
460
- lines.push("");
629
+ out += s.slice(i, open);
630
+ const close = s.indexOf(">", open);
631
+ if (close === -1) {
632
+ out += "<";
633
+ i = open + 1;
634
+ } else i = close + 1;
461
635
  }
462
- for (const { cellId, fqn } of roots) emitElement(cellId, fqn, 1);
463
- for (const e of edges) {
636
+ return out;
637
+ }
638
+ /** Decode XML entities, take first line up to <br, strip tags. Single implementation for cell value and title (DRY). */
639
+ function stripHtml(raw) {
640
+ if (!raw || raw.trim() === "") return "";
641
+ const firstLine = decodeXmlEntities(raw).split("\n")[0] ?? "";
642
+ const brIdx = firstLine.toLowerCase().indexOf("<br");
643
+ return stripTags(brIdx === -1 ? firstLine : firstLine.slice(0, brIdx)).trim() || "";
644
+ }
645
+ /** Escape single quotes for LikeC4 string literals (DRY). */
646
+ function escapeLikec4Quotes(s) {
647
+ return s.replaceAll("'", "''");
648
+ }
649
+ /** Decode optional root cell style field (likec4ViewTitle, likec4ViewDescription, etc.). Safe against malformed percent-encoding. */
650
+ function decodeRootStyleField(raw) {
651
+ if (raw == null || raw === "") return "";
652
+ try {
653
+ return decodeURIComponent(raw);
654
+ } catch {
655
+ return raw;
656
+ }
657
+ }
658
+ /** Build view block lines for model (single responsibility; DRY with parseDrawioToLikeC4). */
659
+ function buildViewBlockLines(viewId, viewTitle, viewDesc) {
660
+ return [
661
+ "views {",
662
+ ` view ${viewId} {`,
663
+ ...viewTitle ? [` title '${escapeLikec4Quotes(viewTitle)}'`] : [],
664
+ ...viewDesc ? [` description '${escapeLikec4Quotes(viewDesc)}'`] : [],
665
+ " include *",
666
+ " }",
667
+ "}",
668
+ ""
669
+ ];
670
+ }
671
+ /** Push element declaration line: name = actor|system|container 'title'. */
672
+ function pushElementHeader(ctx, pad, name, kind, title) {
673
+ const escaped = escapeLikec4Quotes(title);
674
+ switch (kind) {
675
+ case "actor":
676
+ ctx.lines.push(`${pad}${name} = actor '${escaped}'`);
677
+ break;
678
+ case "system":
679
+ ctx.lines.push(`${pad}${name} = system '${escaped}'`);
680
+ break;
681
+ default: ctx.lines.push(`${pad}${name} = container '${escaped}'`);
682
+ }
683
+ }
684
+ /** Push style { ... } block for element when any style part is present. */
685
+ function pushElementStyleBlock(ctx, pad, cell, colorName) {
686
+ const border = cell.border?.trim();
687
+ const opacityVal = cell.opacity;
688
+ const shapeOverride = inferShape(cell.style);
689
+ const sizeVal = cell.size?.trim();
690
+ const paddingVal = cell.padding?.trim();
691
+ const textSizeVal = cell.textSize?.trim();
692
+ const iconPositionVal = cell.iconPosition?.trim();
693
+ if (!colorName && !(border && [
694
+ "solid",
695
+ "dashed",
696
+ "dotted",
697
+ "none"
698
+ ].includes(border)) && !(opacityVal && /^\d+$/.test(opacityVal)) && !shapeOverride && !sizeVal && !paddingVal && !textSizeVal && !iconPositionVal) return;
699
+ const styleParts = [];
700
+ if (colorName) styleParts.push(`color ${colorName}`);
701
+ if (border && [
702
+ "solid",
703
+ "dashed",
704
+ "dotted",
705
+ "none"
706
+ ].includes(border)) styleParts.push(`border ${border}`);
707
+ if (opacityVal && /^\d+$/.test(opacityVal)) styleParts.push(`opacity ${opacityVal}`);
708
+ if (shapeOverride) styleParts.push(`shape ${shapeOverride}`);
709
+ if (sizeVal) styleParts.push(`size ${sizeVal}`);
710
+ if (paddingVal) styleParts.push(`padding ${paddingVal}`);
711
+ if (textSizeVal) styleParts.push(`textSize ${textSizeVal}`);
712
+ if (iconPositionVal) styleParts.push(`iconPosition ${iconPositionVal}`);
713
+ if (styleParts.length > 0) ctx.lines.push(`${pad} style { ${styleParts.join(", ")} }`);
714
+ }
715
+ /** Format link lines for .c4 (DRY: elements and edges). Returns lines with given prefix. */
716
+ function formatLinkLines(linksJson, nativeLink, linePrefix) {
717
+ const out = [];
718
+ try {
719
+ const linksArr = linksJson ? JSON.parse(linksJson) : null;
720
+ if (Array.isArray(linksArr)) {
721
+ for (const link of linksArr) if (link?.url) out.push(`${linePrefix}link '${escapeLikec4Quotes(String(link.url))}'${link.title ? ` '${escapeLikec4Quotes(String(link.title))}'` : ""}`);
722
+ }
723
+ if (out.length === 0 && nativeLink) out.push(`${linePrefix}link '${escapeLikec4Quotes(nativeLink)}'`);
724
+ } catch {
725
+ if (nativeLink) out.push(`${linePrefix}link '${escapeLikec4Quotes(nativeLink)}'`);
726
+ }
727
+ return out;
728
+ }
729
+ /** Push link line(s) from linksJson or nativeLink (DRY with emitEdgesToLines). */
730
+ function pushElementLinks(ctx, pad, linksJson, nativeLink) {
731
+ const prefix = `${pad} `;
732
+ for (const line of formatLinkLines(linksJson, nativeLink, prefix)) ctx.lines.push(line);
733
+ }
734
+ /** Whether element has any body content (Clean Code: single place for hasBody condition). */
735
+ function elementHasBody(cell, childList, colorName, opts) {
736
+ return (childList?.length ?? 0) > 0 || !!opts.desc || !!opts.tech || !!opts.notes || !!opts.summary || !!opts.linksJson || !!opts.nativeLink || !!opts.notation || opts.tagList.length > 0 || !!colorName || !!cell.border?.trim() || !!cell.opacity || !!inferShape(cell.style) || !!cell.size || !!cell.padding || !!cell.textSize || !!cell.iconPosition || !!opts.navigateTo || !!opts.icon;
737
+ }
738
+ /** Push element body lines (style, tags, description, links, children). */
739
+ function pushElementBody(ctx, pad, cell, childList, fqn, indent, colorName, opts) {
740
+ ctx.lines.push(`${pad}{`);
741
+ pushElementStyleBlock(ctx, pad, cell, colorName);
742
+ for (const t of opts.tagList) ctx.lines.push(`${pad} #${t.replaceAll(/\s+/g, "_")}`);
743
+ if (opts.desc) ctx.lines.push(`${pad} description '${escapeLikec4Quotes(opts.desc)}'`);
744
+ if (opts.tech) ctx.lines.push(`${pad} technology '${escapeLikec4Quotes(opts.tech)}'`);
745
+ if (opts.summary) ctx.lines.push(`${pad} summary '${escapeLikec4Quotes(opts.summary)}'`);
746
+ if (opts.notes) ctx.lines.push(`${pad} notes '${escapeLikec4Quotes(opts.notes)}'`);
747
+ if (opts.notation) ctx.lines.push(`${pad} notation '${escapeLikec4Quotes(opts.notation)}'`);
748
+ pushElementLinks(ctx, pad, opts.linksJson, opts.nativeLink);
749
+ if (opts.navigateTo) ctx.lines.push(`${pad} navigateTo ${opts.navigateTo}`);
750
+ if (opts.icon) ctx.lines.push(`${pad} icon '${escapeLikec4Quotes(opts.icon)}'`);
751
+ if (childList && childList.length > 0) for (const ch of childList) emitElement.toLines(ctx, ch.cellId, ch.fqn, indent + 1);
752
+ ctx.lines.push(`${pad}}`);
753
+ }
754
+ /** Emit one element (and its children) to LikeC4 source lines (actor/system/container + body). */
755
+ function emitElementToLines(ctx, cellId, fqn, indent) {
756
+ const cell = ctx.idToCell.get(cellId);
757
+ if (!cell) return;
758
+ const parentCell = cell.parent ? ctx.byId.get(cell.parent) : void 0;
759
+ const kind = inferKind(cell.style, parentCell);
760
+ const title = stripHtml(cell.value && cell.value.trim() || "") || (ctx.containerIdToTitle.get(cell.id) ?? ctx.containerIdToTitle.get(cellId) ?? "") || fqn.split(".").pop() || "Element";
761
+ const name = fqn.split(".").pop();
762
+ const pad = " ".repeat(indent);
763
+ const desc = cell.description?.trim();
764
+ const tech = cell.technology?.trim();
765
+ const notes = cell.notes?.trim();
766
+ const tagsStr = cell.tags?.trim();
767
+ const tagList = tagsStr ? tagsStr.split(",").map((t) => t.trim()).filter(Boolean) : [];
768
+ const navigateTo = cell.navigateTo?.trim();
769
+ const icon = cell.icon?.trim();
770
+ const summary = cell.summary?.trim();
771
+ const linksJson = cell.links?.trim();
772
+ const nativeLink = cell.link?.trim();
773
+ const notation = cell.notation?.trim();
774
+ const colorName = cell.fillColor && /^#[0-9A-Fa-f]{3,8}$/.test(cell.fillColor.trim()) ? ctx.hexToCustomName.get(cell.fillColor.trim()) : void 0;
775
+ const childList = ctx.children.get(fqn);
776
+ const opts = {
777
+ desc,
778
+ tech,
779
+ notes,
780
+ summary,
781
+ linksJson,
782
+ nativeLink,
783
+ notation,
784
+ tagList,
785
+ navigateTo,
786
+ icon
787
+ };
788
+ const hasBody = elementHasBody(cell, childList, colorName, opts);
789
+ pushElementHeader(ctx, pad, name, kind, title);
790
+ if (hasBody) pushElementBody(ctx, pad, cell, childList, fqn, indent, colorName, opts);
791
+ else {
792
+ ctx.lines.push(`${pad}{`);
793
+ ctx.lines.push(`${pad}}`);
794
+ }
795
+ ctx.lines.push("");
796
+ }
797
+ /** Group element-emit helpers for navigation (header, styleBlock, links, body, toLines). */
798
+ const emitElement = { toLines: emitElementToLines };
799
+ /** Emit relation lines (src -> tgt with optional style/body) to LikeC4 source lines. */
800
+ function emitEdgesToLines(lines, edgeEntries, hexToCustomName) {
801
+ for (const { cell: e, src, tgt } of edgeEntries) {
802
+ const title = e.value && e.value.trim() ? escapeLikec4Quotes(e.value.trim()) : "";
803
+ const desc = e.description?.trim();
804
+ const tech = e.technology?.trim();
805
+ const notes = e.notes?.trim();
806
+ const navTo = e.navigateTo?.trim();
807
+ const head = likec4Arrow(e.endArrow);
808
+ const tail = likec4Arrow(e.startArrow);
809
+ const line = likec4LineType(e.dashed, e.dashPattern);
810
+ const relKind = e.relationshipKind?.trim();
811
+ const notation = e.notation?.trim();
812
+ const linksJson = e.links?.trim();
813
+ const metadataJson = e.metadata?.trim();
814
+ const edgeStrokeHex = e.strokeColor?.trim();
815
+ const hasBody = !!notes || !!navTo || !!head || !!tail || !!line || !!notation || !!linksJson || !!metadataJson || !!edgeStrokeHex;
816
+ const relationHead = ` ${src}${relKind && /^[a-zA-Z0-9_-]+$/.test(relKind) ? ` -[${relKind}]-> ` : " -> "}${tgt}${title ? ` '${title}'` : desc || tech ? ` ''` : ""}${desc ? ` '${escapeLikec4Quotes(desc)}'` : ""}${tech ? ` '${escapeLikec4Quotes(tech)}'` : ""}`;
817
+ if (hasBody) {
818
+ const bodyLines = [];
819
+ if (notes) bodyLines.push(` notes '${escapeLikec4Quotes(notes)}'`);
820
+ if (navTo) bodyLines.push(` navigateTo ${navTo}`);
821
+ if (notation) bodyLines.push(` notation '${escapeLikec4Quotes(notation)}'`);
822
+ for (const linkLine of formatLinkLines(linksJson, void 0, " ")) bodyLines.push(linkLine);
823
+ try {
824
+ const metaObj = metadataJson ? JSON.parse(metadataJson) : null;
825
+ if (metaObj != null && typeof metaObj === "object" && !Array.isArray(metaObj)) {
826
+ const metaAttrs = [];
827
+ for (const [k, v] of Object.entries(metaObj)) {
828
+ if (k.trim() === "") continue;
829
+ const safeKey = /^[a-zA-Z_]\w*$/.test(k) ? k : `'${escapeLikec4Quotes(k)}'`;
830
+ if (Array.isArray(v)) {
831
+ const arrVals = v.map((s) => `'${escapeLikec4Quotes(String(s))}'`);
832
+ metaAttrs.push(` ${safeKey} [ ${arrVals.join(", ")} ];`);
833
+ } else if (v != null && typeof v === "string") metaAttrs.push(` ${safeKey} '${escapeLikec4Quotes(v)}';`);
834
+ }
835
+ if (metaAttrs.length > 0) bodyLines.push(" metadata {" + metaAttrs.join("") + " }");
836
+ }
837
+ } catch {}
838
+ if (head || tail || line || edgeStrokeHex) {
839
+ const parts = [];
840
+ if (line) parts.push(`line ${line}`);
841
+ if (head) parts.push(`head ${head}`);
842
+ if (tail) parts.push(`tail ${tail}`);
843
+ if (edgeStrokeHex && /^#[0-9A-Fa-f]{3,8}$/.test(edgeStrokeHex)) {
844
+ const edgeColorName = hexToCustomName.get(edgeStrokeHex);
845
+ if (edgeColorName) parts.push(`color ${edgeColorName}`);
846
+ }
847
+ if (parts.length > 0) bodyLines.push(` style { ${parts.join(", ")} }`);
848
+ }
849
+ lines.push(relationHead + " {", ...bodyLines, " }");
850
+ } else lines.push(relationHead);
851
+ }
852
+ }
853
+ /** Collect layout, stroke, customData and waypoint lines for one diagram state (shared by single and multi emit). Single pass over idToFqn. */
854
+ function collectRoundtripForState(viewId, idToFqn, byId, edges) {
855
+ const layoutNodes = {};
856
+ const strokeColorLines = [];
857
+ const strokeWidthLines = [];
858
+ const customDataLines = [];
859
+ const waypointLines = [];
860
+ for (const [cellId, fqn] of idToFqn) {
861
+ const cell = byId.get(cellId);
862
+ if (cell?.vertex && cell.x != null && cell.y != null && cell.width != null && cell.height != null) layoutNodes[fqn] = {
863
+ x: cell.x,
864
+ y: cell.y,
865
+ width: cell.width,
866
+ height: cell.height
867
+ };
868
+ if (cell?.vertex) {
869
+ if (cell.strokeColor?.trim() && /^#[0-9A-Fa-f]{3,8}$/.test(cell.strokeColor.trim())) strokeColorLines.push(`// ${fqn}=${cell.strokeColor.trim()}`);
870
+ if (cell.strokeWidth != null && cell.strokeWidth.trim() !== "") strokeWidthLines.push(`// ${fqn}=${cell.strokeWidth.trim()}`);
871
+ }
872
+ if (cell?.customData?.trim()) customDataLines.push(`// ${fqn} ${cell.customData.trim()}`);
873
+ }
874
+ const edgesWithEndpoints = edges.filter(isEdgeWithEndpoints);
875
+ for (const e of edgesWithEndpoints) {
464
876
  const src = idToFqn.get(e.source);
465
877
  const tgt = idToFqn.get(e.target);
466
- if (!src || !tgt) continue;
467
- const label = e.value && e.value.trim() ? ` '${e.value.replace(/'/g, "''")}'` : "";
468
- lines.push(` ${src} -> ${tgt}${label}`);
878
+ if (src && tgt) {
879
+ if (e.customData?.trim()) customDataLines.push(`// ${src}|${tgt} ${e.customData.trim()}`);
880
+ if (e.edgePoints?.trim()) waypointLines.push(`// ${src}|${tgt}|${e.id} ${e.edgePoints.trim()}`);
881
+ }
469
882
  }
470
- lines.push("}");
471
- lines.push("");
472
- lines.push("views {");
473
- lines.push(" view index {");
474
- lines.push(" include *");
475
- lines.push(" }");
476
- lines.push("}");
477
- lines.push("");
478
- return lines.join("\n");
883
+ return {
884
+ layoutNodes,
885
+ strokeColorLines,
886
+ strokeWidthLines,
887
+ customDataLines,
888
+ waypointLines
889
+ };
890
+ }
891
+ /** Append round-trip comment blocks for one diagram (layout, stroke, customData, waypoints) to lines. */
892
+ function emitRoundtripCommentsSingle(lines, viewId, idToFqn, byId, edges) {
893
+ const r = collectRoundtripForState(viewId, idToFqn, byId, edges);
894
+ if (Object.keys(r.layoutNodes).length > 0) lines.push("// <likec4.layout.drawio>", "// " + JSON.stringify({ [viewId]: { nodes: r.layoutNodes } }), "// </likec4.layout.drawio>");
895
+ if (r.strokeColorLines.length > 0) lines.push("// <likec4.strokeColor.vertices>", ...r.strokeColorLines, "// </likec4.strokeColor.vertices>");
896
+ if (r.strokeWidthLines.length > 0) lines.push("// <likec4.strokeWidth.vertices>", ...r.strokeWidthLines, "// </likec4.strokeWidth.vertices>");
897
+ if (r.customDataLines.length > 0) lines.push("// <likec4.customData>", ...r.customDataLines, "// </likec4.customData>");
898
+ if (r.waypointLines.length > 0) lines.push("// <likec4.edge.waypoints>", ...r.waypointLines, "// </likec4.edge.waypoints>");
899
+ }
900
+ /** Append merged round-trip comment blocks for multiple diagrams to lines. */
901
+ function emitRoundtripCommentsMulti(lines, states) {
902
+ const layoutByView = {};
903
+ const strokeColorLines = [];
904
+ const strokeWidthLines = [];
905
+ const customDataLines = [];
906
+ const waypointLines = [];
907
+ for (const st of states) {
908
+ const r = collectRoundtripForState(st.viewId, st.idToFqn, st.idToCell, st.edges);
909
+ layoutByView[st.viewId] = { nodes: r.layoutNodes };
910
+ strokeColorLines.push(...r.strokeColorLines);
911
+ strokeWidthLines.push(...r.strokeWidthLines);
912
+ customDataLines.push(...r.customDataLines);
913
+ waypointLines.push(...r.waypointLines);
914
+ }
915
+ if (Object.values(layoutByView).some((v) => v != null && Object.keys(v.nodes).length > 0)) lines.push("// <likec4.layout.drawio>", "// " + JSON.stringify(layoutByView), "// </likec4.layout.drawio>");
916
+ if (strokeColorLines.length > 0) lines.push("// <likec4.strokeColor.vertices>", ...strokeColorLines, "// </likec4.strokeColor.vertices>");
917
+ if (strokeWidthLines.length > 0) lines.push("// <likec4.strokeWidth.vertices>", ...strokeWidthLines, "// </likec4.strokeWidth.vertices>");
918
+ if (customDataLines.length > 0) lines.push("// <likec4.customData>", ...customDataLines, "// </likec4.customData>");
919
+ if (waypointLines.length > 0) lines.push("// <likec4.edge.waypoints>", ...waypointLines, "// </likec4.edge.waypoints>");
920
+ }
921
+ /**
922
+ * Decompress draw.io diagram content: base64 → inflateRaw → decodeURIComponent.
923
+ * Exported for tests (error message contract). Handles both Node (Buffer) and browser (atob).
924
+ * @param base64Content - Compressed diagram string from <diagram> inner content.
925
+ * @returns Decoded mxGraphModel XML string.
926
+ * @throws Error when base64 decode, inflate, or URI decode fails.
927
+ */
928
+ function decompressDrawioDiagram(base64Content) {
929
+ const trimmed = base64Content.trim();
930
+ let bytes;
931
+ try {
932
+ if (typeof Buffer !== "undefined") bytes = new Uint8Array(Buffer.from(trimmed, "base64"));
933
+ else {
934
+ const binary = atob(trimmed);
935
+ bytes = new Uint8Array(binary.length);
936
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i) & 255;
937
+ }
938
+ } catch (err) {
939
+ throw new Error(`DrawIO diagram decompression failed (base64 decode): ${toErrorMessage(err)}`);
940
+ }
941
+ let inflated;
942
+ try {
943
+ inflated = pako.inflateRaw(bytes, { to: "string" });
944
+ } catch (err) {
945
+ throw new Error(`DrawIO diagram decompression failed (inflate): ${toErrorMessage(err)}`);
946
+ }
947
+ try {
948
+ return decodeURIComponent(inflated);
949
+ } catch (err) {
950
+ throw new Error(`DrawIO diagram decompression failed (URI decode): ${toErrorMessage(err)}`);
951
+ }
952
+ }
953
+ /**
954
+ * Extract first diagram name, id and content from mxfile. Handles compressed (base64+deflate) and uncompressed content.
955
+ */
956
+ function getFirstDiagram(fullXml) {
957
+ return getAllDiagrams(fullXml)[0] ?? {
958
+ name: "index",
959
+ id: `${DRAWIO_DIAGRAM_ID_PREFIX}index`,
960
+ content: ""
961
+ };
962
+ }
963
+ /** Length of '<diagram' open tag for slice offset in getAllDiagrams (indexOf-based to avoid regex DoS S5852). */
964
+ const DIAGRAM_TAG_OPEN_LEN = 8;
965
+ /**
966
+ * Extract all diagram name, id and content from mxfile (for multi-tab .drawio).
967
+ * Uses indexOf-based extraction instead of regex to avoid S5852 (super-linear backtracking DoS).
968
+ * @param fullXml - Full .drawio mxfile XML string.
969
+ * @returns Array of DiagramInfo (one per <diagram>); content decompressed when needed.
970
+ */
971
+ function getAllDiagrams(fullXml) {
972
+ const results = [];
973
+ const closeDiagramLen = 10;
974
+ let start = indexOfTagStart(fullXml, "diagram", 0);
975
+ while (start !== -1) {
976
+ const endOpen = findOpenTagEnd(fullXml, start);
977
+ if (endOpen === -1) break;
978
+ const attrs = fullXml.slice(start + DIAGRAM_TAG_OPEN_LEN, endOpen).trim();
979
+ const closeStart = indexOfClosingTag(fullXml, "diagram", endOpen + 1);
980
+ if (closeStart === -1) break;
981
+ const inner = fullXml.slice(endOpen + 1, closeStart);
982
+ const name = getAttr(attrs, "name") ?? (results.length === 0 ? "index" : `diagram_${results.length + 1}`);
983
+ const id = getAttr(attrs, "id") ?? `${DRAWIO_DIAGRAM_ID_PREFIX}${name}`;
984
+ let content;
985
+ if (inner.includes("<mxGraphModel")) content = inner;
986
+ else if (inner.trim() === "") content = inner;
987
+ else try {
988
+ content = decompressDrawioDiagram(inner);
989
+ } catch {
990
+ content = inner;
991
+ }
992
+ results.push({
993
+ name,
994
+ id,
995
+ content
996
+ });
997
+ start = indexOfTagStart(fullXml, "diagram", closeStart + closeDiagramLen);
998
+ }
999
+ return results;
1000
+ }
1001
+ /** Split parsed cells into vertices and edges (single responsibility). */
1002
+ function verticesAndEdgesFromCells(cells) {
1003
+ return {
1004
+ vertices: cells.filter((c) => c.vertex && c.id !== "0" && c.id !== "1"),
1005
+ edges: cells.filter((c) => c.edge && c.source && c.target)
1006
+ };
1007
+ }
1008
+ /** Build common diagram state from cells and diagram name (byId, FQN/hierarchy, view fields from root). */
1009
+ function buildCommonDiagramStateFromCells(cells, diagramName) {
1010
+ const byId = /* @__PURE__ */ new Map();
1011
+ for (const c of cells) byId.set(c.id, c);
1012
+ const { vertices, edges } = verticesAndEdgesFromCells(cells);
1013
+ const rootIds = new Set(["0", "1"]);
1014
+ const isRootParent = (p) => !p || rootIds.has(p);
1015
+ const idToFqn = /* @__PURE__ */ new Map();
1016
+ const idToCell = /* @__PURE__ */ new Map();
1017
+ for (const v of vertices) idToCell.set(v.id, v);
1018
+ const { containerIdToTitle, titleCellIds } = computeContainerTitles(vertices);
1019
+ const elementVertices = vertices.filter((v) => !titleCellIds.has(v.id));
1020
+ assignFqnsToElementVertices(idToFqn, elementVertices, containerIdToTitle, isRootParent, makeUniqueName(/* @__PURE__ */ new Set()));
1021
+ const hexToCustomName = buildHexToCustomName(elementVertices, edges);
1022
+ const children = /* @__PURE__ */ new Map();
1023
+ const roots = [];
1024
+ for (const [cellId, fqn] of idToFqn) {
1025
+ const cell = idToCell.get(cellId);
1026
+ if (cell) if (isRootParent(cell.parent)) roots.push({
1027
+ cellId,
1028
+ fqn
1029
+ });
1030
+ else {
1031
+ const parentFqn = cell.parent != null ? idToFqn.get(cell.parent) : void 0;
1032
+ if (parentFqn != null) {
1033
+ const list = children.get(parentFqn) ?? [];
1034
+ list.push({
1035
+ cellId,
1036
+ fqn
1037
+ });
1038
+ children.set(parentFqn, list);
1039
+ } else roots.push({
1040
+ cellId,
1041
+ fqn
1042
+ });
1043
+ }
1044
+ }
1045
+ const viewId = toId(diagramName?.trim() ? diagramName : "index") || "index";
1046
+ const rootCell = byId.get("1");
1047
+ const rootStyle = rootCell?.style ? parseStyle(rootCell.style) : /* @__PURE__ */ new Map();
1048
+ return {
1049
+ idToFqn,
1050
+ idToCell,
1051
+ containerIdToTitle,
1052
+ roots,
1053
+ children,
1054
+ hexToCustomName,
1055
+ edges,
1056
+ viewId,
1057
+ viewTitle: decodeRootStyleField(rootStyle.get("likec4viewtitle")),
1058
+ viewDesc: decodeRootStyleField(rootStyle.get("likec4viewdescription")),
1059
+ viewNotation: decodeRootStyleField(rootStyle.get("likec4viewnotation")),
1060
+ byId
1061
+ };
1062
+ }
1063
+ /** Build single-diagram state from cells and diagram name (orchestrator: common state + lines buffer). */
1064
+ function buildSingleDiagramState(cells, diagramName) {
1065
+ const common = buildCommonDiagramStateFromCells(cells, diagramName);
1066
+ const { hexToCustomName } = common;
1067
+ const lines = [];
1068
+ if (hexToCustomName.size > 0) {
1069
+ lines.push("specification {");
1070
+ for (const [hex, name] of hexToCustomName) lines.push(` color ${name} ${hex}`);
1071
+ lines.push("}", "");
1072
+ }
1073
+ lines.push("model {", "");
1074
+ return {
1075
+ ...common,
1076
+ lines
1077
+ };
1078
+ }
1079
+ /** Emit LikeC4 source from single-diagram state (orchestrator: elements + edges + view + roundtrip). */
1080
+ function emitLikeC4SourceFromSingleState(state) {
1081
+ const { lines, idToCell, containerIdToTitle, children, hexToCustomName, byId } = state;
1082
+ const emitCtx = {
1083
+ lines,
1084
+ idToCell,
1085
+ containerIdToTitle,
1086
+ children,
1087
+ hexToCustomName,
1088
+ byId
1089
+ };
1090
+ for (const { cellId, fqn } of state.roots) emitElement.toLines(emitCtx, cellId, fqn, 1);
1091
+ const edgeEntries = [];
1092
+ for (const e of state.edges.filter(isEdgeWithEndpoints)) {
1093
+ const src = state.idToFqn.get(e.source);
1094
+ const tgt = state.idToFqn.get(e.target);
1095
+ if (src && tgt) edgeEntries.push({
1096
+ cell: e,
1097
+ src,
1098
+ tgt
1099
+ });
1100
+ }
1101
+ emitEdgesToLines(lines, edgeEntries, hexToCustomName);
1102
+ lines.push("}", "");
1103
+ lines.push(...buildViewBlockLines(state.viewId, state.viewTitle, state.viewDesc));
1104
+ if (state.viewNotation) lines.push(`// likec4.view.notation ${state.viewId} '${escapeLikec4Quotes(state.viewNotation)}'`);
1105
+ emitRoundtripCommentsSingle(lines, state.viewId, state.idToFqn, byId, state.edges);
1106
+ return lines.join("\n");
1107
+ }
1108
+ /**
1109
+ * Convert DrawIO XML to LikeC4 source (.c4) string.
1110
+ * Vertices → model elements (actor/container); edges → relations (->). Uses first diagram only.
1111
+ * @param xml - Full .drawio mxfile XML (single or multi-tab).
1112
+ * @returns LikeC4 .c4 source string (model + views + round-trip comments).
1113
+ */
1114
+ function parseDrawioToLikeC4(xml) {
1115
+ const { name: diagramName, content: xmlToParse } = getFirstDiagram(xml);
1116
+ return emitLikeC4SourceFromSingleState(buildSingleDiagramState(parseDrawioXml(xmlToParse), diagramName));
1117
+ }
1118
+ /** Build diagram state from XML content and name (for multi-tab); returns null if parse fails. */
1119
+ function buildDiagramState(content, diagramName) {
1120
+ const { byId: _byId, ...diagramState } = buildCommonDiagramStateFromCells(parseDrawioXml(content), diagramName);
1121
+ return diagramState;
1122
+ }
1123
+ /** Merge multiple diagram states into shared maps and view infos (single responsibility). */
1124
+ function mergeDiagramStatesIntoMaps(states) {
1125
+ const fqnToCell = /* @__PURE__ */ new Map();
1126
+ const byId = /* @__PURE__ */ new Map();
1127
+ const containerIdToTitle = /* @__PURE__ */ new Map();
1128
+ const relationKeyToEdge = /* @__PURE__ */ new Map();
1129
+ const hexToCustomName = /* @__PURE__ */ new Map();
1130
+ const viewInfos = [];
1131
+ for (const st of states) {
1132
+ for (const [cellId, fqn] of st.idToFqn) {
1133
+ const cell = st.idToCell.get(cellId);
1134
+ if (cell && !fqnToCell.has(fqn)) fqnToCell.set(fqn, cell);
1135
+ }
1136
+ for (const [id, cell] of st.idToCell) {
1137
+ if (byId.has(id)) continue;
1138
+ byId.set(id, cell);
1139
+ }
1140
+ for (const [id, title] of st.containerIdToTitle) {
1141
+ if (containerIdToTitle.has(id)) continue;
1142
+ containerIdToTitle.set(id, title);
1143
+ }
1144
+ for (const e of st.edges.filter(isEdgeWithEndpoints)) {
1145
+ const src = st.idToFqn.get(e.source);
1146
+ const tgt = st.idToFqn.get(e.target);
1147
+ if (src && tgt) {
1148
+ const key = `${src}|${tgt}`;
1149
+ if (relationKeyToEdge.has(key)) continue;
1150
+ relationKeyToEdge.set(key, {
1151
+ src,
1152
+ tgt,
1153
+ cell: e
1154
+ });
1155
+ }
1156
+ }
1157
+ for (const [hex, name] of st.hexToCustomName) {
1158
+ if (hexToCustomName.has(hex)) continue;
1159
+ hexToCustomName.set(hex, name);
1160
+ }
1161
+ viewInfos.push({
1162
+ viewId: st.viewId,
1163
+ viewTitle: st.viewTitle,
1164
+ viewDesc: st.viewDesc,
1165
+ viewNotation: st.viewNotation,
1166
+ fqnSet: new Set(st.idToFqn.values())
1167
+ });
1168
+ }
1169
+ return {
1170
+ fqnToCell,
1171
+ byId,
1172
+ containerIdToTitle,
1173
+ relationKeyToEdge,
1174
+ hexToCustomName,
1175
+ viewInfos
1176
+ };
1177
+ }
1178
+ /** Build parent → children FQN map and root FQNs (single responsibility). */
1179
+ function buildRootsFromFqnToCell(fqnToCell) {
1180
+ const rootsFromMap = /* @__PURE__ */ new Map();
1181
+ for (const fqn of fqnToCell.keys()) {
1182
+ const parent = fqn.includes(".") ? fqn.split(".").slice(0, -1).join(".") : "";
1183
+ if (parent && fqnToCell.has(parent)) {
1184
+ const list = rootsFromMap.get(parent) ?? [];
1185
+ list.push(fqn);
1186
+ rootsFromMap.set(parent, list);
1187
+ } else {
1188
+ const list = rootsFromMap.get("") ?? [];
1189
+ list.push(fqn);
1190
+ rootsFromMap.set("", list);
1191
+ }
1192
+ }
1193
+ return {
1194
+ rootsFromMap,
1195
+ rootFqns: rootsFromMap.get("") ?? []
1196
+ };
1197
+ }
1198
+ /** Emit specification + model { elements + edges } for multi-diagram (single responsibility). */
1199
+ function emitMultiDiagramModel(lines, merged, rootsFromMap, rootFqns) {
1200
+ if (merged.hexToCustomName.size > 0) {
1201
+ lines.push("specification {");
1202
+ for (const [hex, name] of merged.hexToCustomName) lines.push(` color ${name} ${hex}`);
1203
+ lines.push("}", "");
1204
+ }
1205
+ lines.push("model {", "");
1206
+ const idToCellMulti = /* @__PURE__ */ new Map();
1207
+ for (const [fqn, cell] of merged.fqnToCell) idToCellMulti.set(fqn, cell);
1208
+ const childrenMulti = /* @__PURE__ */ new Map();
1209
+ for (const [parentFqn, childFqns] of rootsFromMap) {
1210
+ if (parentFqn === "") continue;
1211
+ const list = childFqns.map((cf) => ({
1212
+ cellId: cf,
1213
+ fqn: cf
1214
+ }));
1215
+ if (list.length > 0) childrenMulti.set(parentFqn, list);
1216
+ }
1217
+ const emitCtxMulti = {
1218
+ lines,
1219
+ idToCell: idToCellMulti,
1220
+ containerIdToTitle: merged.containerIdToTitle,
1221
+ children: childrenMulti,
1222
+ hexToCustomName: merged.hexToCustomName,
1223
+ byId: merged.byId
1224
+ };
1225
+ for (const fqn of rootFqns) emitElement.toLines(emitCtxMulti, fqn, fqn, 1);
1226
+ const edgeEntriesMulti = [];
1227
+ for (const { src, tgt, cell } of merged.relationKeyToEdge.values()) edgeEntriesMulti.push({
1228
+ cell,
1229
+ src,
1230
+ tgt
1231
+ });
1232
+ emitEdgesToLines(lines, edgeEntriesMulti, merged.hexToCustomName);
1233
+ lines.push("}", "");
1234
+ }
1235
+ /**
1236
+ * Convert DrawIO XML to LikeC4 source when file has multiple diagrams (tabs).
1237
+ * Merges elements by FQN and relations by (source, target); one view per diagram.
1238
+ * @param xml - Full .drawio mxfile XML with multiple <diagram> elements.
1239
+ * @returns LikeC4 .c4 source string (model + views + round-trip comments).
1240
+ */
1241
+ function parseDrawioToLikeC4Multi(xml) {
1242
+ const diagrams = getAllDiagrams(xml);
1243
+ if (diagrams.length === 0) return `model {
1244
+
1245
+ }
1246
+ views {
1247
+ view index {
1248
+ include *
1249
+ }
1250
+ }
1251
+ `;
1252
+ if (diagrams.length === 1) {
1253
+ const d = diagrams[0];
1254
+ return emitLikeC4SourceFromSingleState(buildSingleDiagramState(parseDrawioXml(d.content), d.name));
1255
+ }
1256
+ const states = [];
1257
+ for (const d of diagrams) {
1258
+ const s = buildDiagramState(d.content, d.name);
1259
+ if (s) states.push(s);
1260
+ }
1261
+ if (states.length === 0) return parseDrawioToLikeC4(xml);
1262
+ const merged = mergeDiagramStatesIntoMaps(states);
1263
+ const { rootsFromMap, rootFqns } = buildRootsFromFqnToCell(merged.fqnToCell);
1264
+ const lines = [];
1265
+ emitMultiDiagramModel(lines, merged, rootsFromMap, rootFqns);
1266
+ lines.push("views {");
1267
+ for (const v of merged.viewInfos) {
1268
+ const includeList = [...v.fqnSet].sort((a, b) => a.localeCompare(b));
1269
+ lines.push(` view ${v.viewId} {`, ...v.viewTitle ? [` title '${escapeLikec4Quotes(v.viewTitle)}'`] : [], ...v.viewDesc ? [` description '${escapeLikec4Quotes(v.viewDesc)}'`] : [], ` include ${includeList.length > 0 ? includeList.join(", ") : "*"}`, " }");
1270
+ }
1271
+ lines.push("}", "");
1272
+ for (const v of merged.viewInfos) if (v.viewNotation) lines.push(`// likec4.view.notation ${v.viewId} '${escapeLikec4Quotes(v.viewNotation)}'`);
1273
+ emitRoundtripCommentsMulti(lines, states);
1274
+ return lines.join("\n");
1275
+ }
1276
+ const LAYOUT_START = "// <likec4.layout.drawio>";
1277
+ const LAYOUT_END = "// </likec4.layout.drawio>";
1278
+ const STROKE_COLOR_START = "// <likec4.strokeColor.vertices>";
1279
+ const STROKE_COLOR_END = "// </likec4.strokeColor.vertices>";
1280
+ const STROKE_WIDTH_START = "// <likec4.strokeWidth.vertices>";
1281
+ const STROKE_WIDTH_END = "// </likec4.strokeWidth.vertices>";
1282
+ const WAYPOINTS_START = "// <likec4.edge.waypoints>";
1283
+ const WAYPOINTS_END = "// </likec4.edge.waypoints>";
1284
+ /**
1285
+ * Parse DrawIO round-trip comment blocks from .c4 source (layout, strokeColor, strokeWidth, waypoints).
1286
+ * Used to build GenerateDrawioOptions for re-export after editing in draw.io.
1287
+ * TODO: the four section parsers (layout, strokeColor, strokeWidth, waypoints) repeat the same
1288
+ * "parse block between markers" pattern; consider a small helper to reduce duplication.
1289
+ * @param c4Source - Full .c4 source string (e.g. concatenated workspace files).
1290
+ * @returns DrawioRoundtripData or null if no likec4.* comment blocks found.
1291
+ */
1292
+ function parseDrawioRoundtripComments(c4Source) {
1293
+ const lines = c4Source.split(/\r?\n/);
1294
+ let layoutByView = {};
1295
+ let strokeColorByFqn = {};
1296
+ let strokeWidthByFqn = {};
1297
+ let edgeWaypoints = {};
1298
+ let found = false;
1299
+ let i = 0;
1300
+ while (i < lines.length) {
1301
+ const line = lines[i];
1302
+ if (line == null) {
1303
+ i += 1;
1304
+ continue;
1305
+ }
1306
+ if (line.trim() === LAYOUT_START) {
1307
+ found = true;
1308
+ i += 1;
1309
+ const layoutLines = [];
1310
+ while (i < lines.length && lines[i]?.trim() !== LAYOUT_END) {
1311
+ const ln = lines[i]?.trim();
1312
+ if (ln?.startsWith("// ")) layoutLines.push(ln.slice(3));
1313
+ i += 1;
1314
+ }
1315
+ if (layoutLines.length > 0) try {
1316
+ const json = layoutLines.join("\n");
1317
+ layoutByView = JSON.parse(json);
1318
+ } catch {}
1319
+ i += 1;
1320
+ continue;
1321
+ }
1322
+ if (line.trim() === STROKE_COLOR_START) {
1323
+ found = true;
1324
+ i += 1;
1325
+ while (i < lines.length && lines[i]?.trim() !== STROKE_COLOR_END) {
1326
+ const ln = lines[i]?.trim();
1327
+ if (ln?.startsWith("// ") && ln.includes("=")) {
1328
+ const rest = ln.slice(3).trim();
1329
+ const eq = rest.indexOf("=");
1330
+ if (eq > 0) {
1331
+ const fqn = rest.slice(0, eq).trim();
1332
+ const hex = rest.slice(eq + 1).trim();
1333
+ if (fqn && hex) strokeColorByFqn[fqn] = hex;
1334
+ }
1335
+ }
1336
+ i += 1;
1337
+ }
1338
+ i += 1;
1339
+ continue;
1340
+ }
1341
+ if (line.trim() === STROKE_WIDTH_START) {
1342
+ found = true;
1343
+ i += 1;
1344
+ while (i < lines.length && lines[i]?.trim() !== STROKE_WIDTH_END) {
1345
+ const ln = lines[i]?.trim();
1346
+ if (ln?.startsWith("// ") && ln.includes("=")) {
1347
+ const rest = ln.slice(3).trim();
1348
+ const eq = rest.indexOf("=");
1349
+ if (eq > 0) {
1350
+ const fqn = rest.slice(0, eq).trim();
1351
+ const val = rest.slice(eq + 1).trim();
1352
+ if (fqn && val !== "") strokeWidthByFqn[fqn] = val;
1353
+ }
1354
+ }
1355
+ i += 1;
1356
+ }
1357
+ i += 1;
1358
+ continue;
1359
+ }
1360
+ if (line.trim() === WAYPOINTS_START) {
1361
+ found = true;
1362
+ i += 1;
1363
+ while (i < lines.length && lines[i]?.trim() !== WAYPOINTS_END) {
1364
+ const ln = lines[i]?.trim();
1365
+ if (ln?.startsWith("// ")) {
1366
+ const rest = ln.slice(3).trim();
1367
+ const space = rest.indexOf(" ");
1368
+ if (space > 0) {
1369
+ const key = rest.slice(0, space).trim();
1370
+ const json = rest.slice(space + 1).trim();
1371
+ if (key && json) try {
1372
+ const pts = JSON.parse(json);
1373
+ if (Array.isArray(pts)) edgeWaypoints[key] = pts;
1374
+ } catch {}
1375
+ }
1376
+ }
1377
+ i += 1;
1378
+ }
1379
+ i += 1;
1380
+ continue;
1381
+ }
1382
+ i += 1;
1383
+ }
1384
+ if (found) return {
1385
+ layoutByView,
1386
+ strokeColorByFqn,
1387
+ strokeWidthByFqn,
1388
+ edgeWaypoints
1389
+ };
1390
+ return null;
1391
+ }
1392
+ /**
1393
+ * DrawIO diagram generator.
1394
+ *
1395
+ * Design system alignment: colors, spacing, and font sizes are taken from the
1396
+ * viewmodel's styles (LikeC4Styles / theme). Container padding uses
1397
+ * theme.spacing (xl, xl+md for vertical). Container title uses groupColors.stroke
1398
+ * and theme.textSizes.xs. Element and edge colors use getElementColors /
1399
+ * getEdgeLabelColors from the theme. The only value not from core theme is the
1400
+ * Font family matches LikeC4 app (--likec4-app-font / --likec4-app-font-default:
1401
+ * 'IBM Plex Sans Variable', ui-sans-serif, system-ui, sans-serif).
1402
+ */
1403
+ /**
1404
+ * DrawIO expects diagram content as base64(deflateRaw(encodeURIComponent(xml))).
1405
+ * @internal
1406
+ */
1407
+ function compressDrawioDiagramXml(xml) {
1408
+ const encoded = encodeURIComponent(xml);
1409
+ const bytes = new TextEncoder().encode(encoded);
1410
+ return uint8ArrayToBase64(pako.deflateRaw(bytes));
1411
+ }
1412
+ /** Encode bytes to base64 (Node Buffer or btoa for browser). */
1413
+ function uint8ArrayToBase64(bytes) {
1414
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
1415
+ let binary = "";
1416
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
1417
+ return btoa(binary);
1418
+ }
1419
+ /** Optional node fields (DSL allows more than base type). Single place for type/cast — Clean Code. */
1420
+ const nodeOptionalFields = {
1421
+ getNotes(node) {
1422
+ return node.notes;
1423
+ },
1424
+ getSummary(node) {
1425
+ return node.summary;
1426
+ },
1427
+ getTags(node) {
1428
+ return node.tags;
1429
+ },
1430
+ getNavigateTo(node) {
1431
+ return node.navigateTo;
1432
+ },
1433
+ getIcon(node) {
1434
+ return node.icon;
1435
+ },
1436
+ getLinks(node) {
1437
+ return node.links;
1438
+ },
1439
+ getNotation(node) {
1440
+ return node.notation;
1441
+ },
1442
+ getCustomData(node) {
1443
+ return node.customData;
1444
+ },
1445
+ getChildren(node) {
1446
+ return node.children;
1447
+ }
1448
+ };
1449
+ /** Optional edge fields. Single place for type/cast — Clean Code. */
1450
+ const edgeOptionalFields = {
1451
+ getKind(edge) {
1452
+ return edge.kind;
1453
+ },
1454
+ getNotation(edge) {
1455
+ return edge.notation;
1456
+ },
1457
+ getLinks(edge) {
1458
+ return edge.links;
1459
+ },
1460
+ getMetadata(edge) {
1461
+ return edge.metadata;
1462
+ },
1463
+ getCustomData(edge) {
1464
+ return edge.customData;
1465
+ }
1466
+ };
1467
+ /** Project styles or central default (LikeC4Styles.DEFAULT) when view has no $styles. */
1468
+ function getEffectiveStyles(viewmodel) {
1469
+ return viewmodel.$styles ?? LikeC4Styles.DEFAULT;
1470
+ }
1471
+ /** Escape for use inside HTML (e.g. cell value with html=1). */
1472
+ function escapeHtml(text) {
1473
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
1474
+ }
1475
+ /** Coerce to non-empty string for style/attribute use; empty string when null/undefined/empty or non-primitive. DRY for navTo, iconName, etc. */
1476
+ function toNonEmptyString(value) {
1477
+ if (value == null) return "";
1478
+ const t = typeof value;
1479
+ if (t === "string") {
1480
+ const s = value;
1481
+ return s.trim() === "" ? "" : s;
1482
+ }
1483
+ if (t === "number") return String(value);
1484
+ if (t === "boolean") return String(value);
1485
+ return "";
1486
+ }
1487
+ /** Container dashed style from border (KISS: single place for 3-way branch). */
1488
+ function getContainerDashedStyle(isContainer, borderVal) {
1489
+ if (isContainer && borderVal !== "none") return "dashed=1;";
1490
+ if (borderVal === "dashed") return "dashed=1;";
1491
+ return "";
1492
+ }
1493
+ /** Default stroke width for node from border and container. No stroke set ('') for leaf without border → Draw.io uses its default. */
1494
+ function getDefaultStrokeWidth(borderVal, isContainer) {
1495
+ if (borderVal === "none") return "0";
1496
+ return isContainer ? "1" : borderVal ? "1" : "";
1497
+ }
1498
+ /** Apply stroke color override to base element colors (KISS: named function instead of IIFE). */
1499
+ function applyStrokeColorOverride(base, override) {
1500
+ return {
1501
+ fill: base?.fill ?? DEFAULT_NODE_FILL_HEX,
1502
+ stroke: override,
1503
+ font: base?.font ?? override
1504
+ };
1505
+ }
1506
+ /** Theme color key valid for styles.theme.colors; falls back to primary/gray for elements/edges. */
1507
+ function resolveThemeColor(styles, color, fallback) {
1508
+ if (color && color in styles.theme.colors) return color;
1509
+ return fallback;
1510
+ }
1511
+ /** Get theme color values with fallback to DEFAULT on error (DRY + SRP for try/catch). */
1512
+ function getThemeColorValues(viewmodel, color, fallback) {
1513
+ const styles = getEffectiveStyles(viewmodel);
1514
+ const themeColor = resolveThemeColor(styles, color ?? fallback, fallback);
1515
+ try {
1516
+ return styles.colors(themeColor);
1517
+ } catch {
1518
+ return LikeC4Styles.DEFAULT.colors(fallback);
1519
+ }
1520
+ }
1521
+ /**
1522
+ * Map LikeC4 element shape to draw.io cell style string.
1523
+ * Rounded corners: arcSize as percentage integer (e.g. 12) for subtly rounded corners in Draw.io.
1524
+ */
1525
+ function drawioShape(shape) {
1526
+ const rectStyle = "shape=rectangle;rounded=1;arcSize=12;";
1527
+ switch (shape) {
1528
+ case "person": return "shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;";
1529
+ case "rectangle":
1530
+ case "browser":
1531
+ case "mobile":
1532
+ case "bucket": return rectStyle;
1533
+ case "cylinder":
1534
+ case "queue":
1535
+ case "storage": return "shape=cylinder3;whiteSpace=wrap;boundedLbl=1;backgroundOutline=1;size=15;";
1536
+ case "document": return "shape=document;whiteSpace=wrap;html=1;boundedLbl=1;";
1537
+ case "component": return "shape=component;";
1538
+ default: return rectStyle;
1539
+ }
1540
+ }
1541
+ /**
1542
+ * Resolve element fill, stroke and font colors from project styles or default theme.
1543
+ * Uses ElementColorValues (hiContrast for font when present).
1544
+ */
1545
+ function getElementColors(viewmodel, color) {
1546
+ const elementColors = getThemeColorValues(viewmodel, color, "primary").elements;
1547
+ return {
1548
+ fill: String(elementColors.fill ?? DEFAULT_NODE_FILL_HEX),
1549
+ stroke: String(elementColors.stroke ?? DEFAULT_NODE_STROKE_HEX),
1550
+ font: String(elementColors.hiContrast ?? elementColors.stroke ?? DEFAULT_NODE_FONT_HEX)
1551
+ };
1552
+ }
1553
+ /** Edge stroke (line) color from theme RelationshipColorValues.line. */
1554
+ function getEdgeStrokeColor(viewmodel, color) {
1555
+ const values = getThemeColorValues(viewmodel, color ?? "gray", "gray");
1556
+ return String(values.relationships?.line ?? DEFAULT_NODE_FONT_HEX);
1557
+ }
1558
+ /** Edge label font and background from theme (RelationshipColorValues.label, labelBg) for readable connector text. */
1559
+ function getEdgeLabelColors(viewmodel, color) {
1560
+ const rel = getThemeColorValues(viewmodel, color ?? "gray", "gray").relationships;
1561
+ return {
1562
+ font: String(rel?.label ?? rel?.line ?? DEFAULT_NODE_FONT_HEX),
1563
+ background: String(rel?.labelBg ?? "#ffffff")
1564
+ };
1565
+ }
1566
+ /**
1567
+ * Compute draw.io exit/entry anchors (0–1) from source to target bbox centers
1568
+ * so edges connect on the correct sides (LikeC4-style layout).
1569
+ */
1570
+ function edgeAnchors(sourceBbox, targetBbox) {
1571
+ const sCx = sourceBbox.x + sourceBbox.width / 2;
1572
+ const sCy = sourceBbox.y + sourceBbox.height / 2;
1573
+ const tCx = targetBbox.x + targetBbox.width / 2;
1574
+ const tCy = targetBbox.y + targetBbox.height / 2;
1575
+ const dx = tCx - sCx;
1576
+ const dy = tCy - sCy;
1577
+ const hor = Math.abs(dx) >= Math.abs(dy);
1578
+ return {
1579
+ exitX: hor ? dx >= 0 ? 1 : 0 : .5,
1580
+ exitY: hor ? .5 : dy >= 0 ? 1 : 0,
1581
+ entryX: hor ? dx >= 0 ? 0 : 1 : .5,
1582
+ entryY: hor ? .5 : dy >= 0 ? 0 : 1
1583
+ };
1584
+ }
1585
+ /** Normalize one waypoint to [x, y]; returns one element or empty. Called per element via flatMap. */
1586
+ function normalizeEdgePoint(pt) {
1587
+ if (Array.isArray(pt) && pt.length >= 2 && typeof pt[0] === "number" && typeof pt[1] === "number") return [[pt[0], pt[1]]];
1588
+ const o = pt;
1589
+ if (typeof o.x === "number" && typeof o.y === "number") return [[o.x, o.y]];
1590
+ return [];
1591
+ }
1592
+ /** Build HTML value for a vertex cell (title only or title + description). */
1593
+ function buildNodeValueHtml(title, desc, isContainer, fontHex, fontFamily, fontSizePx) {
1594
+ if (isContainer) return "";
1595
+ if (desc !== "") return `<div style="box-sizing:border-box;width:100%;min-height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:${fontHex};font-family:${fontFamily};"><b style="font-size:${fontSizePx}px;">${escapeHtml(title)}</b><br/><span style="font-weight:normal;font-size:${fontSizePx}px;">${escapeHtml(desc)}</span></div>`;
1596
+ return `<div style="box-sizing:border-box;width:100%;min-height:100%;display:flex;align-items:center;justify-content:center;text-align:center;color:${fontHex};font-family:${fontFamily};"><b style="font-size:${fontSizePx}px;">${escapeHtml(title)}</b></div>`;
1597
+ }
1598
+ /** Push "key=value" to parts when value is set; encodes value for style string (avoids repeated conditionals). */
1599
+ function pushStylePart(parts, key, value) {
1600
+ if (value != null && value !== "") parts.push(`${key}=${encodeURIComponent(value)}`);
1601
+ }
1602
+ /** Push "key=value" for numeric value (no encoding). */
1603
+ function pushStylePartNum(parts, key, value) {
1604
+ if (value != null) parts.push(`${key}=${value}`);
1605
+ }
1606
+ /** Build Draw.io link= style for navigateTo (empty string when no nav). DRY for node and container title. */
1607
+ function buildNavLinkStyle(navTo) {
1608
+ return navTo === "" ? "" : `link=${encodeURIComponent(`${DRAWIO_PAGE_LINK_PREFIX}${navTo}`)};`;
1609
+ }
1610
+ /** Flatten markdown/string and trim to single export string; empty when missing or empty-ish. DRY for node/edge fields. */
1611
+ function toExportString(raw) {
1612
+ const flat = raw != null ? flattenMarkdownOrString(raw) : null;
1613
+ return flat != null && !isEmptyish(flat) ? flat.trim() : "";
1614
+ }
1615
+ /** Serialize links array to style-safe JSON string (empty when none). DRY for node and edge links. */
1616
+ function linksToStyleJson(links) {
1617
+ if (!Array.isArray(links) || links.length === 0) return "";
1618
+ return encodeURIComponent(JSON.stringify(links.map((l) => ({
1619
+ url: l.url,
1620
+ title: l.title
1621
+ }))));
1622
+ }
1623
+ /** Serialize metadata object to style-safe JSON string (empty when none). DRY for edge metadata. */
1624
+ function metadataToStyleJson(metadata) {
1625
+ if (metadata == null || typeof metadata !== "object" || Array.isArray(metadata) || Object.keys(metadata).length === 0) return "";
1626
+ return encodeURIComponent(JSON.stringify(metadata));
1627
+ }
1628
+ const HEX_COLOR_RE = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
1629
+ /** Build LikeC4 style string (likec4Description=...; etc.) for round-trip. */
1630
+ function buildLikec4StyleForNode(params) {
1631
+ const parts = [];
1632
+ pushStylePart(parts, "likec4Description", params.desc);
1633
+ pushStylePart(parts, "likec4Technology", params.tech);
1634
+ pushStylePart(parts, "likec4Notes", params.notes);
1635
+ pushStylePart(parts, "likec4Tags", params.tagList);
1636
+ pushStylePart(parts, "likec4NavigateTo", params.navTo);
1637
+ pushStylePart(parts, "likec4Icon", params.iconName);
1638
+ pushStylePart(parts, "likec4Summary", params.summaryStr);
1639
+ if (params.linksJson !== "") parts.push(`likec4Links=${params.linksJson}`);
1640
+ pushStylePart(parts, "likec4Border", params.borderVal);
1641
+ pushStylePartNum(parts, "likec4Opacity", params.containerOpacityNum);
1642
+ pushStylePart(parts, "likec4StrokeWidth", params.strokeWidth);
1643
+ if (params.colorNameForRoundtrip !== "") parts.push(`likec4ColorName=${params.colorNameForRoundtrip}`);
1644
+ pushStylePart(parts, "likec4Size", params.nodeStyle?.size);
1645
+ pushStylePart(parts, "likec4Padding", params.nodeStyle?.padding);
1646
+ pushStylePart(parts, "likec4TextSize", params.nodeStyle?.textSize);
1647
+ pushStylePart(parts, "likec4IconPosition", params.nodeStyle?.iconPosition);
1648
+ if (params.strokeHex && HEX_COLOR_RE.test(params.strokeHex)) pushStylePart(parts, "likec4StrokeColor", params.strokeHex);
1649
+ pushStylePart(parts, "likec4Notation", params.nodeNotation ?? void 0);
1650
+ return parts.length > 0 ? parts.join(";") + ";" : "";
1651
+ }
1652
+ /** Build mxUserObject XML from customData for round-trip; returns empty string when customData is missing or empty. */
1653
+ function buildMxUserObjectXml(customData) {
1654
+ if (!customData || typeof customData !== "object" || Array.isArray(customData) || Object.keys(customData).length === 0) return "";
1655
+ return "\n <mxUserObject>" + Object.entries(customData).map(([k, v]) => {
1656
+ const safeV = typeof v === "string" ? v : v != null ? String(v) : "";
1657
+ return `<data key="${escapeXml(k)}">${escapeXml(safeV)}</data>`;
1658
+ }).join("") + "</mxUserObject>";
1659
+ }
1660
+ /** Build LikeC4 style string for an edge (likec4Description=...; etc.) for round-trip. */
1661
+ function buildLikec4StyleForEdge(params) {
1662
+ const parts = [];
1663
+ pushStylePart(parts, "likec4Description", params.edgeDesc);
1664
+ pushStylePart(parts, "likec4Technology", params.edgeTech);
1665
+ pushStylePart(parts, "likec4Notes", params.edgeNotes);
1666
+ pushStylePart(parts, "likec4NavigateTo", params.edgeNavTo);
1667
+ pushStylePart(parts, "likec4RelationshipKind", params.edgeKind ?? void 0);
1668
+ pushStylePart(parts, "likec4Notation", params.edgeNotation ?? void 0);
1669
+ if (params.edgeLinksJson !== "") parts.push(`likec4Links=${params.edgeLinksJson}`);
1670
+ if (params.edgeMetadataJson !== "") parts.push(`likec4Metadata=${params.edgeMetadataJson}`);
1671
+ return parts.length > 0 ? parts.join(";") + ";" : "";
1672
+ }
1673
+ /** Escaped edge label for mxCell value (single responsibility). */
1674
+ function buildEdgeLabelValue(edge) {
1675
+ return edge.label ? escapeXml(edge.label) : "";
1676
+ }
1677
+ /** Edge waypoints → mxGeometry XML (single responsibility). */
1678
+ function buildEdgeGeometryXml(edge, edgeWaypoints) {
1679
+ const rawEdgePoints = edgeWaypoints?.[`${edge.source}|${edge.target}|${edge.id}`] ?? edgeWaypoints?.[`${edge.source}|${edge.target}`];
1680
+ const edgePoints = Array.isArray(rawEdgePoints) ? rawEdgePoints.flatMap(normalizeEdgePoint) : [];
1681
+ if (!(edgePoints.length > 0)) return "<mxGeometry relative=\"1\" as=\"geometry\" />";
1682
+ return `<mxGeometry relative="1" as="geometry">${"<Array as=\"points\">" + edgePoints.map(([px, py]) => `<mxPoint x="${Math.round(px)}" y="${Math.round(py)}"/>`).join("") + "</Array>"}</mxGeometry>`;
1683
+ }
1684
+ /** Full edge style string for mxCell (arrows, anchors, stroke, dash, label, likec4 roundtrip). */
1685
+ function buildEdgeStyleString(edge, layout, viewmodel, label) {
1686
+ const { bboxes, fontFamily } = layout;
1687
+ const sourceBbox = bboxes.get(edge.source);
1688
+ const targetBbox = bboxes.get(edge.target);
1689
+ const anchors = sourceBbox && targetBbox ? edgeAnchors(sourceBbox, targetBbox) : {
1690
+ exitX: 1,
1691
+ exitY: .5,
1692
+ entryX: 0,
1693
+ entryY: .5
1694
+ };
1695
+ const anchorStyle = `exitX=${anchors.exitX};exitY=${anchors.exitY};entryX=${anchors.entryX};entryY=${anchors.entryY};`;
1696
+ const strokeColor = getEdgeStrokeColor(viewmodel, edge.color);
1697
+ const dashStyle = edge.line === "dashed" ? "dashed=1;" : edge.line === "dotted" ? "dashed=1;dashPattern=1 1;" : "";
1698
+ const endArrow = drawioArrow(edge.head);
1699
+ const startArrow = edge.tail == null || edge.tail === "none" ? "none" : drawioArrow(edge.tail);
1700
+ const edgeLikec4Style = buildLikec4StyleForEdge({
1701
+ edgeDesc: toExportString(edge.description),
1702
+ edgeTech: toExportString(edge.technology),
1703
+ edgeNotes: toExportString(edge.notes),
1704
+ edgeNavTo: toNonEmptyString(edge.navigateTo),
1705
+ edgeKind: edgeOptionalFields.getKind(edge),
1706
+ edgeNotation: edgeOptionalFields.getNotation(edge),
1707
+ edgeLinksJson: linksToStyleJson(edgeOptionalFields.getLinks(edge)),
1708
+ edgeMetadataJson: metadataToStyleJson(edgeOptionalFields.getMetadata(edge))
1709
+ });
1710
+ const edgeLabelColors = getEdgeLabelColors(viewmodel, edge.color);
1711
+ return `endArrow=${endArrow};startArrow=${startArrow};html=1;rounded=0;${anchorStyle}strokeColor=${strokeColor};strokeWidth=2;${dashStyle}${label === "" ? "" : `fontColor=${edgeLabelColors.font};fontSize=12;align=center;verticalAlign=middle;labelBackgroundColor=none;fontFamily=${encodeURIComponent(fontFamily)};`}${edgeLikec4Style}`;
1712
+ }
1713
+ /** Build a single edge mxCell XML (orchestrator: label + geometry + style + assembly). */
1714
+ function buildEdgeCellXml(edge, layout, options, viewmodel, getCellId, edgeCellId) {
1715
+ const { defaultParentId } = layout;
1716
+ const sourceId = getCellId(edge.source);
1717
+ const targetId = getCellId(edge.target);
1718
+ const label = buildEdgeLabelValue(edge);
1719
+ const edgeGeometryXml = buildEdgeGeometryXml(edge, options?.edgeWaypoints);
1720
+ return `<mxCell id="${edgeCellId}" value="${label}" style="${buildEdgeStyleString(edge, layout, viewmodel, label)}" edge="1" parent="${defaultParentId}" source="${sourceId}" target="${targetId}">
1721
+ ${edgeGeometryXml}${buildMxUserObjectXml(edgeOptionalFields.getCustomData(edge))}
1722
+ </mxCell>`;
1723
+ }
1724
+ /** Geometry for one node cell: id, parent, position, size (single responsibility). */
1725
+ function computeNodeGeometry(node, layout, getCellId) {
1726
+ const { bboxes, defaultParentId, nodeIdsInView } = layout;
1727
+ const id = getCellId(node.id);
1728
+ const bbox = bboxes.get(node.id);
1729
+ const { width, height } = bbox;
1730
+ const parentId = node.parent != null && nodeIdsInView.has(node.parent) ? getCellId(node.parent) : defaultParentId;
1731
+ const parentBbox = node.parent != null ? bboxes.get(node.parent) : void 0;
1732
+ return {
1733
+ id,
1734
+ parentId,
1735
+ x: parentBbox == null ? bbox.x + layout.offsetX : bbox.x - parentBbox.x,
1736
+ y: parentBbox == null ? bbox.y + layout.offsetY : bbox.y - parentBbox.y,
1737
+ width,
1738
+ height
1739
+ };
1740
+ }
1741
+ /** Style parts and value for one node (colors, stroke, likec4 roundtrip style, value HTML). */
1742
+ function computeNodeStylePartsAndValue(node, layout, options, viewmodel) {
1743
+ const { containerNodeIds, effectiveStyles, fontFamily, containerTitleFontSizePx, containerTitleColor } = layout;
1744
+ const strokeColorByNodeId = options?.strokeColorByNodeId;
1745
+ const strokeWidthByNodeId = options?.strokeWidthByNodeId;
1746
+ const isContainer = containerNodeIds.has(node.id);
1747
+ const title = node.title;
1748
+ const desc = toExportString(node.description);
1749
+ const tech = toExportString(node.technology);
1750
+ const notes = toExportString(nodeOptionalFields.getNotes(node));
1751
+ const tags = nodeOptionalFields.getTags(node);
1752
+ const tagList = Array.isArray(tags) && tags.length > 0 ? tags.join(",") : "";
1753
+ const navTo = toNonEmptyString(nodeOptionalFields.getNavigateTo(node));
1754
+ const iconName = toNonEmptyString(nodeOptionalFields.getIcon(node));
1755
+ const shapeStyle = isContainer ? "shape=rectangle;rounded=0;container=1;collapsible=0;startSize=0;" : drawioShape(node.shape);
1756
+ const strokeColorOverride = strokeColorByNodeId?.[node.id];
1757
+ const strokeWidthOverride = strokeWidthByNodeId?.[node.id];
1758
+ const elemColors = strokeColorOverride ? applyStrokeColorOverride(getElementColors(viewmodel, node.color), strokeColorOverride) : getElementColors(viewmodel, node.color);
1759
+ const fillHex = elemColors?.fill ?? DEFAULT_NODE_FILL_HEX;
1760
+ const strokeHex = elemColors?.stroke ?? DEFAULT_NODE_STROKE_HEX;
1761
+ const fontHex = elemColors?.font ?? elemColors?.stroke ?? DEFAULT_NODE_FONT_HEX;
1762
+ const colorStyle = `fillColor=${fillHex};strokeColor=${strokeHex};fontColor=${fontHex};`;
1763
+ const nodeStyle = node.style;
1764
+ const fontSizePx = effectiveStyles.fontSize(nodeStyle?.textSize);
1765
+ const value = escapeXml(buildNodeValueHtml(title, desc, isContainer, fontHex, fontFamily, fontSizePx));
1766
+ const borderVal = nodeStyle?.border;
1767
+ const strokeWidth = strokeWidthOverride ?? getDefaultStrokeWidth(borderVal, isContainer);
1768
+ const strokeWidthStyle = strokeWidth !== "" ? `strokeWidth=${strokeWidth};` : "";
1769
+ const containerDashed = getContainerDashedStyle(isContainer, borderVal);
1770
+ const containerOpacityNum = isContainer === true ? nodeStyle?.opacity ?? DEFAULT_CONTAINER_OPACITY : void 0;
1771
+ const fillOpacityStyle = containerOpacityNum != null && isContainer === true ? `fillOpacity=${Math.min(100, Math.max(0, containerOpacityNum))};` : "";
1772
+ const likec4Style = buildLikec4StyleForNode({
1773
+ desc,
1774
+ tech,
1775
+ notes,
1776
+ tagList,
1777
+ navTo,
1778
+ iconName,
1779
+ summaryStr: toExportString(nodeOptionalFields.getSummary(node)),
1780
+ linksJson: linksToStyleJson(nodeOptionalFields.getLinks(node)),
1781
+ borderVal,
1782
+ containerOpacityNum,
1783
+ strokeWidth,
1784
+ colorNameForRoundtrip: node.color ? encodeURIComponent(String(node.color)) : "",
1785
+ nodeStyle,
1786
+ strokeHex,
1787
+ nodeNotation: nodeOptionalFields.getNotation(node)
1788
+ });
1789
+ const userObjectXml = buildMxUserObjectXml(nodeOptionalFields.getCustomData(node));
1790
+ const navLinkStyle = buildNavLinkStyle(navTo);
1791
+ return {
1792
+ value,
1793
+ styleStr: `${isContainer ? "align=left;verticalAlign=top;overflow=fill;whiteSpace=wrap;html=1;" : `align=center;verticalAlign=middle;verticalLabelPosition=middle;labelPosition=center;fontSize=${fontSizePx};fontStyle=1;spacingTop=4;spacingLeft=2;spacingRight=2;spacingBottom=2;overflow=fill;whiteSpace=wrap;html=1;fontFamily=${encodeURIComponent(fontFamily)};`}${shapeStyle}${colorStyle}${strokeWidthStyle}${containerDashed}${fillOpacityStyle}${navLinkStyle}${likec4Style}`,
1794
+ userObjectXml,
1795
+ navTo,
1796
+ isContainer,
1797
+ title,
1798
+ fontFamily,
1799
+ containerTitleFontSizePx,
1800
+ containerTitleColor
1801
+ };
1802
+ }
1803
+ /** Orchestrator: compute node geometry + style/value, then merge into NodeCellExportData. */
1804
+ function computeNodeCellExportData(node, layout, options, viewmodel, getCellId, containerTitleCellId) {
1805
+ const geometry = computeNodeGeometry(node, layout, getCellId);
1806
+ const styleAndValue = computeNodeStylePartsAndValue(node, layout, options, viewmodel);
1807
+ return {
1808
+ ...geometry,
1809
+ value: styleAndValue.value,
1810
+ styleStr: styleAndValue.styleStr,
1811
+ userObjectXml: styleAndValue.userObjectXml,
1812
+ navTo: styleAndValue.navTo,
1813
+ isContainer: styleAndValue.isContainer,
1814
+ fontFamily: styleAndValue.fontFamily,
1815
+ ...styleAndValue.isContainer && {
1816
+ title: styleAndValue.title ?? "",
1817
+ titleCellId: String(containerTitleCellId),
1818
+ containerTitleFontSizePx: styleAndValue.containerTitleFontSizePx,
1819
+ containerTitleColor: styleAndValue.containerTitleColor
1820
+ }
1821
+ };
1822
+ }
1823
+ /** Build node vertex mxCell XML from precomputed data (single responsibility — XML assembly only). mxUserObject before mxGeometry for parser/roundtrip (align with draw.io expectations). */
1824
+ function buildNodeCellXml(data) {
1825
+ const geometryLine = `<mxGeometry height="${Math.round(data.height)}" width="${Math.round(data.width)}" x="${Math.round(data.x)}" y="${Math.round(data.y)}" as="geometry" />`;
1826
+ const innerContent = data.userObjectXml !== "" ? `${data.userObjectXml}\n ${geometryLine}` : `\n ${geometryLine}`;
1827
+ const userObjectLabel = data.isContainer && data.title != null ? escapeXml(data.title) : data.value;
1828
+ const cellXml = data.navTo === "" ? `<mxCell id="${data.id}" value="${data.value}" style="${data.styleStr}" vertex="1" parent="${data.parentId}">\n ${innerContent}\n</mxCell>` : `<UserObject label="${userObjectLabel}" link="${DRAWIO_PAGE_LINK_PREFIX}${escapeXml(data.navTo)}" id="${data.id}">\n <mxCell parent="${data.parentId}" style="${data.styleStr}" value="${data.value}" vertex="1">\n ${innerContent}\n</mxCell>\n</UserObject>`;
1829
+ if (!data.isContainer) return {
1830
+ vertexXml: cellXml,
1831
+ isContainer: false
1832
+ };
1833
+ return {
1834
+ vertexXml: cellXml,
1835
+ titleCellXml: buildContainerTitleCellXml(data.title ?? "", data.titleCellId ?? data.id, data.navTo, data.id, data.fontFamily, data.containerTitleFontSizePx ?? 12, data.containerTitleColor ?? CONTAINER_TITLE_COLOR),
1836
+ isContainer: true
1837
+ };
1838
+ }
1839
+ /** Build container title cell XML (child of container, relative position CONTAINER_TITLE_INSET_*). */
1840
+ function buildContainerTitleCellXml(title, titleId, navTo, containerId, fontFamily, fontSizePx, colorHex) {
1841
+ const titleValue = escapeXml(title);
1842
+ const titleWidth = Math.max(CONTAINER_TITLE_MIN_WIDTH_PX, Math.min(CONTAINER_TITLE_MAX_WIDTH_PX, title.length * CONTAINER_TITLE_CHAR_WIDTH_PX));
1843
+ const titleHeight = CONTAINER_TITLE_HEIGHT_PX;
1844
+ const titleX = CONTAINER_TITLE_INSET_X;
1845
+ const titleY = CONTAINER_TITLE_INSET_Y;
1846
+ const navLinkStyle = buildNavLinkStyle(navTo);
1847
+ const titleStyle = `shape=text;html=1;fillColor=none;strokeColor=none;align=left;verticalAlign=top;fontSize=${fontSizePx};fontStyle=1;fontColor=${colorHex};fontFamily=${encodeURIComponent(fontFamily)};${navLinkStyle}`;
1848
+ if (navTo === "") return `<mxCell id="${titleId}" value="${titleValue}" style="${titleStyle}" vertex="1" parent="${containerId}">\n <mxGeometry x="${Math.round(titleX)}" y="${Math.round(titleY)}" width="${titleWidth}" height="${titleHeight}" as="geometry" />\n</mxCell>`;
1849
+ const titleInner = `<mxCell parent="${containerId}" style="${titleStyle}" value="${titleValue}" vertex="1">\n <mxGeometry x="${Math.round(titleX)}" y="${Math.round(titleY)}" width="${titleWidth}" height="${titleHeight}" as="geometry" />\n</mxCell>`;
1850
+ return `<UserObject label="${escapeXml(title)}" link="${DRAWIO_PAGE_LINK_PREFIX}${escapeXml(navTo)}" id="${titleId}">\n ${titleInner}\n</UserObject>`;
1851
+ }
1852
+ /** View title for diagram name and root cell (single source of truth). */
1853
+ function getViewTitle(view) {
1854
+ return typeof view.title === "string" ? view.title : null;
1855
+ }
1856
+ /** Normalize view description from txt/md/string to plain string (single responsibility). */
1857
+ function getViewDescriptionString(view) {
1858
+ const raw = view.description;
1859
+ if (raw != null && typeof raw === "object" && "txt" in raw) return String(raw.txt);
1860
+ if (raw != null && typeof raw === "object" && "md" in raw) return String(raw.md);
1861
+ if (typeof raw === "string") return raw;
1862
+ return "";
1863
+ }
1864
+ /** Build root cell style string from view metadata (title, description, notation) for round-trip. */
1865
+ function buildRootCellStyle(view) {
1866
+ const viewTitle = getViewTitle(view);
1867
+ const viewDesc = getViewDescriptionString(view);
1868
+ const viewDescEnc = viewDesc.trim() !== "" ? encodeURIComponent(viewDesc.trim()) : "";
1869
+ const viewNotationRaw = view.notation;
1870
+ const viewNotation = typeof viewNotationRaw === "string" && viewNotationRaw !== "" ? viewNotationRaw : void 0;
1871
+ const viewNotationEnc = viewNotation != null ? encodeURIComponent(viewNotation) : "";
1872
+ return [
1873
+ "rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=none;",
1874
+ `likec4ViewTitle=${encodeURIComponent(viewTitle ?? view.id)};`,
1875
+ viewDescEnc !== "" ? `likec4ViewDescription=${viewDescEnc};` : "",
1876
+ viewNotationEnc !== "" ? `likec4ViewNotation=${viewNotationEnc};` : ""
1877
+ ].join("");
1878
+ }
1879
+ /**
1880
+ * Map LikeC4 RelationshipArrowType to draw.io endArrow/startArrow style value.
1881
+ */
1882
+ function drawioArrow(arrow) {
1883
+ switch (arrow) {
1884
+ case "none": return "none";
1885
+ case "open":
1886
+ case "onormal":
1887
+ case "vee": return "open";
1888
+ case "diamond":
1889
+ case "odiamond": return "diamond";
1890
+ case "dot":
1891
+ case "odot": return "oval";
1892
+ case "crow": return "block";
1893
+ default: return "block";
1894
+ }
1895
+ }
1896
+ const DEFAULT_BBOX = {
1897
+ x: 0,
1898
+ y: 0,
1899
+ width: DEFAULT_NODE_WIDTH,
1900
+ height: DEFAULT_NODE_HEIGHT
1901
+ };
1902
+ /** True when bbox equals default (unlaid) dimensions. */
1903
+ function isDefaultBbox(b) {
1904
+ return b.x === DEFAULT_BBOX.x && b.y === DEFAULT_BBOX.y && b.width === DEFAULT_BBOX.width && b.height === DEFAULT_BBOX.height;
1905
+ }
1906
+ /** Spread nodes that share the same default bbox vertically so they don't overlap (single responsibility). */
1907
+ function spreadUnlaidNodesOverVertical(bboxes, sortedNodes, containerNodeIds) {
1908
+ const bboxKey = (b) => `${b.x},${b.y},${b.width},${b.height}`;
1909
+ const nonContainerNodes = sortedNodes.filter((n) => !containerNodeIds.has(n.id));
1910
+ const byBbox = /* @__PURE__ */ new Map();
1911
+ for (const n of nonContainerNodes) {
1912
+ const b = bboxes.get(n.id);
1913
+ if (!b) continue;
1914
+ const key = bboxKey(b);
1915
+ const list = byBbox.get(key) ?? [];
1916
+ list.push(n);
1917
+ byBbox.set(key, list);
1918
+ }
1919
+ for (const bboxNodes of byBbox.values()) {
1920
+ if (bboxNodes.length <= 1) continue;
1921
+ const firstNode = bboxNodes[0];
1922
+ const firstBbox = firstNode ? bboxes.get(firstNode.id) : void 0;
1923
+ if (firstBbox && isDefaultBbox(firstBbox)) bboxNodes.forEach((node, i) => {
1924
+ bboxes.set(node.id, {
1925
+ ...firstBbox,
1926
+ x: firstBbox.x,
1927
+ y: firstBbox.y + i * (firstBbox.height + NODES_SPREAD_GAP)
1928
+ });
1929
+ });
1930
+ }
1931
+ }
1932
+ /** Wrap container bboxes around children when container has default bbox (single responsibility). */
1933
+ function computeContainerBboxesFromChildren(bboxes, containerNodeIds, sortedNodes, nodeIdsInView, containerPadding, containerPaddingVertical) {
1934
+ const containerNodesSorted = [...sortedNodes].filter((n) => containerNodeIds.has(n.id)).sort((a, b) => (b.level ?? 0) - (a.level ?? 0));
1935
+ for (const node of containerNodesSorted) {
1936
+ const inView = (nodeOptionalFields.getChildren(node) ?? []).filter((id) => nodeIdsInView.has(id));
1937
+ if (inView.length === 0) continue;
1938
+ if (!isDefaultBbox(bboxes.get(node.id))) continue;
1939
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1940
+ for (const cid of inView) {
1941
+ const b = bboxes.get(cid);
1942
+ if (!b) continue;
1943
+ minX = Math.min(minX, b.x);
1944
+ minY = Math.min(minY, b.y);
1945
+ maxX = Math.max(maxX, b.x + b.width);
1946
+ maxY = Math.max(maxY, b.y + b.height);
1947
+ }
1948
+ if (minX !== Infinity) bboxes.set(node.id, {
1949
+ x: minX - containerPadding,
1950
+ y: minY - containerPaddingVertical,
1951
+ width: maxX - minX + 2 * containerPadding,
1952
+ height: maxY - minY + 2 * containerPaddingVertical
1953
+ });
1954
+ }
1955
+ }
1956
+ /** Compute canvas offsets to center content (single responsibility). */
1957
+ function computeContentBoundsAndOffsets(bboxes) {
1958
+ let contentMinX = Infinity, contentMinY = Infinity, contentMaxX = -Infinity, contentMaxY = -Infinity;
1959
+ for (const b of bboxes.values()) {
1960
+ contentMinX = Math.min(contentMinX, b.x);
1961
+ contentMinY = Math.min(contentMinY, b.y);
1962
+ contentMaxX = Math.max(contentMaxX, b.x + b.width);
1963
+ contentMaxY = Math.max(contentMaxY, b.y + b.height);
1964
+ }
1965
+ if (contentMinX === Infinity) contentMinX = 0;
1966
+ if (contentMinY === Infinity) contentMinY = 0;
1967
+ if (contentMaxX === -Infinity) contentMaxX = contentMinX + DEFAULT_CANVAS_WIDTH;
1968
+ if (contentMaxY === -Infinity) contentMaxY = contentMinY + DEFAULT_CANVAS_HEIGHT;
1969
+ const contentCx = contentMinX + (contentMaxX - contentMinX) / 2;
1970
+ const contentCy = contentMinY + (contentMaxY - contentMinY) / 2;
1971
+ return {
1972
+ offsetX: DEFAULT_CANVAS_WIDTH / 2 - contentCx,
1973
+ offsetY: DEFAULT_CANVAS_HEIGHT / 2 - contentCy,
1974
+ canvasWidth: DEFAULT_CANVAS_WIDTH,
1975
+ canvasHeight: DEFAULT_CANVAS_HEIGHT
1976
+ };
1977
+ }
1978
+ /**
1979
+ * Layout phase: compute bboxes, container wrap, content bounds, and offsets.
1980
+ * Delegates to spreadUnlaidNodesOverVertical, computeContainerBboxesFromChildren, computeContentBoundsAndOffsets.
1981
+ */
1982
+ function computeDiagramLayout(viewmodel, options) {
1983
+ const view = viewmodel.$view;
1984
+ const { nodes } = view;
1985
+ const layoutOverride = options?.layoutOverride;
1986
+ const sortedNodes = [...nodes].sort((a, b) => {
1987
+ if (isNullish(a.parent) && isNullish(b.parent)) return 0;
1988
+ if (isNullish(a.parent)) return -1;
1989
+ if (isNullish(b.parent)) return 1;
1990
+ if (a.parent === b.parent) return 0;
1991
+ if (a.id.startsWith(b.id + ".")) return 1;
1992
+ if (b.id.startsWith(a.id + ".")) return -1;
1993
+ return 0;
1994
+ });
1995
+ const getBBox = (n) => {
1996
+ const over = layoutOverride?.[n.id];
1997
+ if (over) return over;
1998
+ const d = n;
1999
+ return {
2000
+ x: typeof d.x === "number" ? d.x : Array.isArray(d.position) ? d.position[0] : 0,
2001
+ y: typeof d.y === "number" ? d.y : Array.isArray(d.position) ? d.position[1] : 0,
2002
+ width: typeof d.width === "number" ? d.width : d.size?.width ?? DEFAULT_NODE_WIDTH,
2003
+ height: typeof d.height === "number" ? d.height : d.size?.height ?? DEFAULT_NODE_HEIGHT
2004
+ };
2005
+ };
2006
+ const bboxes = /* @__PURE__ */ new Map();
2007
+ for (const node of sortedNodes) bboxes.set(node.id, getBBox(node));
2008
+ const nodeIdsInView = new Set(nodes.map((n) => n.id));
2009
+ const containerNodeIds = new Set(nodes.filter((n) => {
2010
+ const ch = nodeOptionalFields.getChildren(n);
2011
+ return Array.isArray(ch) && ch.some((childId) => nodeIdsInView.has(childId));
2012
+ }).map((n) => n.id));
2013
+ spreadUnlaidNodesOverVertical(bboxes, sortedNodes, containerNodeIds);
2014
+ const effectiveStyles = getEffectiveStyles(viewmodel);
2015
+ const containerPadding = effectiveStyles.theme.spacing.xl;
2016
+ computeContainerBboxesFromChildren(bboxes, containerNodeIds, sortedNodes, nodeIdsInView, containerPadding, effectiveStyles.theme.spacing.xl + effectiveStyles.theme.spacing.md);
2017
+ const { offsetX, offsetY, canvasWidth, canvasHeight } = computeContentBoundsAndOffsets(bboxes);
2018
+ return {
2019
+ view,
2020
+ bboxes,
2021
+ containerNodeIds,
2022
+ sortedNodes,
2023
+ offsetX,
2024
+ offsetY,
2025
+ canvasWidth,
2026
+ canvasHeight,
2027
+ defaultParentId: "1",
2028
+ rootId: "0",
2029
+ effectiveStyles,
2030
+ fontFamily: LIKEC4_FONT_FAMILY,
2031
+ containerTitleFontSizePx: Math.round(effectiveStyles.theme.textSizes.xs),
2032
+ containerTitleColor: CONTAINER_TITLE_COLOR,
2033
+ nodeIdsInView
2034
+ };
2035
+ }
2036
+ /**
2037
+ * Generate DrawIO (mxGraph) XML from a layouted LikeC4 view.
2038
+ * Preserves positions, hierarchy, colors, descriptions and technology so the diagram
2039
+ * can be opened and edited in draw.io with full compatibility.
2040
+ *
2041
+ * @param viewmodel - Layouted LikeC4 view model (from model.view(id))
2042
+ * @param options - Optional overrides for layout/colors (round-trip from comment blocks)
2043
+ * @returns Diagram name, id and content (for single or multi composition)
2044
+ */
2045
+ function generateDiagramContent(viewmodel, options) {
2046
+ const view = viewmodel.$view;
2047
+ const { edges } = view;
2048
+ const useCompressed = options?.compressed !== false;
2049
+ const layout = computeDiagramLayout(viewmodel, options);
2050
+ const { sortedNodes, defaultParentId, rootId, canvasWidth, canvasHeight } = layout;
2051
+ const nodeIds = /* @__PURE__ */ new Map();
2052
+ let cellId = 2;
2053
+ const getCellId = (nodeId) => {
2054
+ let id = nodeIds.get(nodeId);
2055
+ if (!id) {
2056
+ if (cellId >= CONTAINER_TITLE_CELL_ID_START) throw new Error("DrawIO cell ID range exhausted");
2057
+ id = String(cellId++);
2058
+ nodeIds.set(nodeId, id);
2059
+ }
2060
+ return id;
2061
+ };
2062
+ const containerCells = [];
2063
+ const vertexCells = [];
2064
+ const edgeCells = [];
2065
+ let containerTitleCellId = CONTAINER_TITLE_CELL_ID_START;
2066
+ for (const node of sortedNodes) {
2067
+ const result = buildNodeCellXml(computeNodeCellExportData(node, layout, options, viewmodel, getCellId, containerTitleCellId));
2068
+ if (result.isContainer) {
2069
+ containerCells.push(result.vertexXml);
2070
+ if (result.titleCellXml) containerCells.push(result.titleCellXml);
2071
+ containerTitleCellId++;
2072
+ } else vertexCells.push(result.vertexXml);
2073
+ }
2074
+ for (const edge of edges) {
2075
+ if (cellId >= CONTAINER_TITLE_CELL_ID_START) throw new Error("DrawIO cell ID range exhausted");
2076
+ const edgeId = String(cellId++);
2077
+ edgeCells.push(buildEdgeCellXml(edge, layout, options, viewmodel, getCellId, edgeId));
2078
+ }
2079
+ const allCells = [
2080
+ `<mxCell id="${defaultParentId}" value="" style="${buildRootCellStyle(view)}" vertex="1" parent="${rootId}">
2081
+ <mxGeometry x="0" y="0" width="${canvasWidth}" height="${canvasHeight}" as="geometry" />
2082
+ </mxCell>`,
2083
+ ...containerCells,
2084
+ ...vertexCells,
2085
+ ...edgeCells
2086
+ ].join("\n");
2087
+ const diagramName = (getViewTitle(view) ?? view.id).trim() || view.id;
2088
+ const mxGraphModelXml = `<mxGraphModel dx="${MXGRAPH_DEFAULT_DX}" dy="${MXGRAPH_DEFAULT_DY}" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="${MXGRAPH_PAGE_WIDTH}" pageHeight="${MXGRAPH_PAGE_HEIGHT}" math="0" shadow="0">
2089
+ <root>
2090
+ <mxCell id="${rootId}" />
2091
+ ${allCells}
2092
+ </root>
2093
+ </mxGraphModel>`;
2094
+ const content = useCompressed ? compressDrawioDiagramXml(mxGraphModelXml) : mxGraphModelXml;
2095
+ return {
2096
+ name: diagramName,
2097
+ id: view.id,
2098
+ content
2099
+ };
2100
+ }
2101
+ /** Wrap one or more diagram contents in mxfile XML. */
2102
+ function wrapInMxFile(diagrams, modified) {
2103
+ const modifiedAttr = escapeXml(modified ?? (/* @__PURE__ */ new Date()).toISOString());
2104
+ if (diagrams.length === 0) return `<?xml version="1.0" encoding="UTF-8"?>
2105
+ <mxfile host="LikeC4" modified="${modifiedAttr}" agent="LikeC4" version="1.0" etag="" type="device">
2106
+ </mxfile>
2107
+ `;
2108
+ return `<?xml version="1.0" encoding="UTF-8"?>
2109
+ <mxfile host="LikeC4" modified="${modifiedAttr}" agent="LikeC4" version="1.0" etag="" type="device"${diagrams.length > 1 ? ` pages="${diagrams.length}"` : ""}>
2110
+ ${diagrams.map((d) => ` <diagram name="${escapeXml(d.name)}" id="${DRAWIO_DIAGRAM_ID_PREFIX}${escapeXml(d.id)}">${d.content}</diagram>`).join("\n")}
2111
+ </mxfile>
2112
+ `;
2113
+ }
2114
+ /**
2115
+ * Generate a single DrawIO file from one view.
2116
+ *
2117
+ * @param viewmodel - Layouted LikeC4 view model (from model.view(id))
2118
+ * @param options - Optional overrides for layout/colors (round-trip from comment blocks)
2119
+ * @returns DrawIO .drawio XML string
2120
+ */
2121
+ function generateDrawio(viewmodel, options) {
2122
+ return wrapInMxFile([generateDiagramContent(viewmodel, options)], options?.modified);
2123
+ }
2124
+ /**
2125
+ * Generate a single DrawIO file with multiple diagrams (tabs).
2126
+ * Each view becomes one tab in draw.io. Use this when exporting a project
2127
+ * so all views open in one file with one tab per view.
2128
+ *
2129
+ * @param viewmodels - Layouted view models (e.g. from model.views())
2130
+ * @param optionsByViewId - Optional per-view options (e.g. compressed: false for each tab)
2131
+ * @param modified - Optional ISO timestamp for mxfile modified attribute (for deterministic output)
2132
+ * @returns DrawIO .drawio XML string with multiple <diagram> elements
2133
+ */
2134
+ function generateDrawioMulti(viewmodels, optionsByViewId, modified) {
2135
+ return wrapInMxFile(viewmodels.map((vm) => generateDiagramContent(vm, optionsByViewId?.[vm.$view.id])), modified);
2136
+ }
2137
+ /**
2138
+ * Build export options from .c4 source round-trip comment blocks (layout, strokes, waypoints).
2139
+ * Shared by CLI and playground so options are built in one place (DRY).
2140
+ *
2141
+ * @param viewId - View id for layoutOverride lookup.
2142
+ * @param roundtrip - Parsed round-trip data or null (caller may parse once for many views).
2143
+ * @param overrides - Optional overrides (e.g. compressed: false).
2144
+ * @returns GenerateDrawioOptions for this view.
2145
+ */
2146
+ function buildOptionsFromRoundtrip(viewId, roundtrip, overrides) {
2147
+ const options = {
2148
+ compressed: false,
2149
+ ...overrides
2150
+ };
2151
+ if (!roundtrip) return options;
2152
+ const layoutForView = roundtrip.layoutByView[viewId]?.nodes;
2153
+ if (layoutForView != null) options.layoutOverride = layoutForView;
2154
+ if (Object.keys(roundtrip.strokeColorByFqn).length > 0) options.strokeColorByNodeId = roundtrip.strokeColorByFqn;
2155
+ if (Object.keys(roundtrip.strokeWidthByFqn).length > 0) options.strokeWidthByNodeId = roundtrip.strokeWidthByFqn;
2156
+ if (Object.keys(roundtrip.edgeWaypoints).length > 0) options.edgeWaypoints = roundtrip.edgeWaypoints;
2157
+ return options;
2158
+ }
2159
+ /**
2160
+ * Build export options for one view from .c4 source (parses source once).
2161
+ *
2162
+ * @param viewId - View id for layoutOverride lookup.
2163
+ * @param sourceContent - Full .c4 source (e.g. joined workspace files).
2164
+ * @param overrides - Optional overrides (e.g. compressed: false).
2165
+ * @returns GenerateDrawioOptions for this view.
2166
+ */
2167
+ function buildDrawioExportOptionsFromSource(viewId, sourceContent, overrides) {
2168
+ return buildOptionsFromRoundtrip(viewId, sourceContent ? parseDrawioRoundtripComments(sourceContent) : null, overrides);
2169
+ }
2170
+ /**
2171
+ * Build export options per view id from .c4 source (parses source once for all views).
2172
+ *
2173
+ * @param viewIds - View ids to build options for.
2174
+ * @param sourceContent - Full .c4 source (e.g. joined workspace files).
2175
+ * @param overrides - Optional overrides (e.g. compressed: false).
2176
+ * @returns Record of viewId → GenerateDrawioOptions.
2177
+ */
2178
+ function buildDrawioExportOptionsForViews(viewIds, sourceContent, overrides) {
2179
+ const roundtrip = sourceContent ? parseDrawioRoundtripComments(sourceContent) : null;
2180
+ return Object.fromEntries(viewIds.map((viewId) => [viewId, buildOptionsFromRoundtrip(viewId, roundtrip, overrides)]));
2181
+ }
2182
+ /**
2183
+ * Generate a draw.io editor URL that opens the given drawio XML pre-loaded.
2184
+ * Uses the `#create=` fragment with compressed XML data.
2185
+ *
2186
+ * @param xml - A .drawio XML string (output of generateDrawio / generateDrawioMulti).
2187
+ * @returns URL string like "https://app.diagrams.net/#create=..."
2188
+ */
2189
+ function generateDrawioEditUrl(xml) {
2190
+ const base64 = compressDrawioDiagramXml(xml);
2191
+ const createObj = JSON.stringify({
2192
+ type: "xml",
2193
+ compressed: true,
2194
+ data: base64
2195
+ });
2196
+ return "https://app.diagrams.net/#create=" + encodeURIComponent(createObj);
2197
+ }
2198
+ const capitalizeFirstLetter$1 = (value) => value.charAt(0).toLocaleUpperCase() + value.slice(1);
2199
+ const fqnName$1 = (nodeId) => nodeId.split(".").map(capitalizeFirstLetter$1).join("");
2200
+ const nodeName$1 = (node) => {
2201
+ return fqnName$1(node.parent ? node.id.slice(node.parent.length + 1) : node.id);
2202
+ };
2203
+ const toSingleQuotes = (str) => str.replace(/\\?"/g, `'`);
2204
+ const mmdshape = ({ shape, title }) => {
2205
+ const label = `label: ${JSON.stringify(title)}`;
2206
+ switch (shape) {
2207
+ case "queue": return `@{ shape: horizontal-cylinder, ${label} }`;
2208
+ case "person": return `@{ icon: "fa:user", shape: rounded, ${label} }`;
2209
+ case "storage": return `@{ shape: disk, ${label} }`;
2210
+ case "cylinder": return `@{ shape: cylinder, ${label} }`;
2211
+ case "mobile":
2212
+ case "browser": return `@{ shape: rounded, ${label} }`;
2213
+ case "bucket": return `@{ shape: trap-t, ${label} }`;
2214
+ case "rectangle": return `@{ shape: rectangle, ${label} }`;
2215
+ case "document": return `@{ shape: doc, ${label} }`;
2216
+ case "component": return `@{ shape: rectangle, ${label} }`;
2217
+ default: nonexhaustive(shape);
2218
+ }
2219
+ };
2220
+ function generateMermaid(viewmodel) {
2221
+ const view = viewmodel.$view;
2222
+ const { nodes, edges } = view;
2223
+ const names = /* @__PURE__ */ new Map();
2224
+ const printNode = (node, parentName) => {
2225
+ const name = nodeName$1(node);
2226
+ const fqnName = (parentName ? parentName + "." : "") + name;
2227
+ names.set(node.id, fqnName);
2228
+ const baseNode = new CompositeGeneratorNode();
2229
+ if (node.children.length > 0) {
2230
+ const label = toSingleQuotes(node.title);
2231
+ baseNode.append("subgraph ", fqnName, "[\"`", label, "`\"]", NL).indent({
2232
+ indentedChildren: [joinToNode(nodes.filter((n) => n.parent === node.id), (n) => printNode(n, fqnName), { appendNewLineIfNotEmpty: true })],
2233
+ indentation: 2
2234
+ }).append("end", NL);
2235
+ } else baseNode.append(fqnName, mmdshape(node));
2236
+ return baseNode;
2237
+ };
2238
+ const printEdge = (edge) => {
2239
+ return new CompositeGeneratorNode().append(names.get(edge.source), " -.", edge.label ? " \"`" + toSingleQuotes(edge.label) + "`\" .-" : "-", "> ", names.get(edge.target));
2240
+ };
2241
+ return toString(new CompositeGeneratorNode().append("---", NL, `title: ${JSON.stringify(toSingleQuotes(viewmodel.titleOrId))}`, NL, "---", NL).append("graph ", view.autoLayout.direction, NL).indent({
2242
+ indentedChildren: (indent) => {
2243
+ indent.append(joinToNode(nodes.filter((n) => isNullish(n.parent)), (n) => printNode(n), { appendNewLineIfNotEmpty: true })).appendIf(edges.length > 0, joinToNode(edges, (e) => printEdge(e), { appendNewLineIfNotEmpty: true }));
2244
+ },
2245
+ indentation: 2
2246
+ }));
479
2247
  }
480
2248
  function toUnion(elements) {
481
2249
  if (elements.length === 0) return "never";
@@ -595,7 +2363,7 @@ function generatePuml(viewmodel) {
595
2363
  const view = viewmodel.$view;
596
2364
  const colors = viewmodel.$model.$styles.theme.colors;
597
2365
  const { nodes, edges } = view;
598
- const elemntColorProvider = (key) => (colorKey) => colorKey in colors ? colors[colorKey].elements[key] : void 0;
2366
+ const elementColorProvider = (key) => (colorKey) => colorKey in colors ? colors[colorKey].elements[key] : void 0;
599
2367
  const relationshipsColorProvider = (key) => (colorKey) => colorKey in colors ? colors[colorKey].relationships[key] : void 0;
600
2368
  const names = /* @__PURE__ */ new Map();
601
2369
  const printHeader = () => {
@@ -611,7 +2379,7 @@ function generatePuml(viewmodel) {
611
2379
  const shape = pumlShape(node);
612
2380
  const fqn = fqnName(node.id);
613
2381
  return new CompositeGeneratorNode().append("skinparam ", shape, "<<", fqn, ">>", "{", NL).indent({
614
- indentedChildren: (indent) => indent.append("BackgroundColor ", pumlColor(node.color, elemntColorProvider("fill")), NL).append("FontColor ", pumlColor(node.color, elemntColorProvider("hiContrast"), "#FFFFFF"), NL).append("BorderColor ", pumlColor(node.color, elemntColorProvider("stroke")), NL),
2382
+ indentedChildren: (indent) => indent.append("BackgroundColor ", pumlColor(node.color, elementColorProvider("fill")), NL).append("FontColor ", pumlColor(node.color, elementColorProvider("hiContrast"), "#FFFFFF"), NL).append("BorderColor ", pumlColor(node.color, elementColorProvider("stroke")), NL),
615
2383
  indentation: 2
616
2384
  }).append("}", NL);
617
2385
  };
@@ -629,7 +2397,7 @@ function generatePuml(viewmodel) {
629
2397
  const fqn = fqnName(node.id);
630
2398
  names.set(node.id, fqn);
631
2399
  return new CompositeGeneratorNode().append("rectangle \"", label, "\" <<", fqn, ">> as ", fqn, " {", NL).indent({
632
- indentedChildren: (indent) => indent.append("skinparam ", "RectangleBorderColor<<", fqn, ">> ", pumlColor(node.color, elemntColorProvider("fill")), NL).append("skinparam ", "RectangleFontColor<<", fqn, ">> ", pumlColor(node.color, elemntColorProvider("fill")), NL).append("skinparam ", "RectangleBorderStyle<<", fqn, ">> ", "dashed", NL, NL).append(joinToNode(nodes.filter((n) => n.parent === node.id), (c) => c.children.length > 0 ? printBoundary(c) : printNode(c))),
2400
+ indentedChildren: (indent) => indent.append("skinparam ", "RectangleBorderColor<<", fqn, ">> ", pumlColor(node.color, elementColorProvider("fill")), NL).append("skinparam ", "RectangleFontColor<<", fqn, ">> ", pumlColor(node.color, elementColorProvider("fill")), NL).append("skinparam ", "RectangleBorderStyle<<", fqn, ">> ", "dashed", NL, NL).append(joinToNode(nodes.filter((n) => n.parent === node.id), (c) => c.children.length > 0 ? printBoundary(c) : printNode(c))),
633
2401
  indentation: 2
634
2402
  }).append("}", NL);
635
2403
  };
@@ -650,6 +2418,9 @@ function generatePuml(viewmodel) {
650
2418
  function generateViewId(views) {
651
2419
  return joinToNode(views, (view) => expandToNode`${JSON5.stringify(view.id)}`, { separator: " | " });
652
2420
  }
2421
+ /**
2422
+ * Generate *.js file with views data
2423
+ */
653
2424
  function generateViewsDataJs(diagrams) {
654
2425
  const views = Array.from(diagrams);
655
2426
  const out = new CompositeGeneratorNode();
@@ -690,6 +2461,9 @@ function generateViewsDataJs(diagrams) {
690
2461
  `.append(NL);
691
2462
  return toString(out);
692
2463
  }
2464
+ /**
2465
+ * Generate *.ts file with views data
2466
+ */
693
2467
  function generateViewsDataTs(diagrams) {
694
2468
  const views = Array.from(diagrams);
695
2469
  const out = new CompositeGeneratorNode();
@@ -735,6 +2509,9 @@ function generateViewsDataTs(diagrams) {
735
2509
  `.append(NL);
736
2510
  return toString(out);
737
2511
  }
2512
+ /**
2513
+ * Generate *.d.ts
2514
+ */
738
2515
  function generateViewsDataDTs(diagrams) {
739
2516
  const views = Array.from(diagrams);
740
2517
  const out = new CompositeGeneratorNode();
@@ -763,6 +2540,9 @@ function generateViewsDataDTs(diagrams) {
763
2540
  `.append(NL);
764
2541
  return toString(out);
765
2542
  }
2543
+ /**
2544
+ * @deprecated in favor packages/likec4/src/cli/codegen/react/index.ts
2545
+ */
766
2546
  function generateReactNext(views) {
767
2547
  return {
768
2548
  viewsData: {
@@ -925,4 +2705,4 @@ export {
925
2705
  /* prettier-ignore-end */
926
2706
  `.trimStart();
927
2707
  }
928
- export { generateD2, generateDrawio, generateLikeC4Model, generateMermaid, generatePuml, generateReactNext, generateReactTypes, generateViewsDataDTs, generateViewsDataJs, generateViewsDataTs, parseDrawioToLikeC4 };
2708
+ export { DEFAULT_DRAWIO_ALL_FILENAME, buildDrawioExportOptionsForViews, buildDrawioExportOptionsFromSource, generateD2, generateDrawio, generateDrawioEditUrl, generateDrawioMulti, generateLikeC4Model, generateMermaid, generatePuml, generateReactNext, generateReactTypes, generateViewsDataDTs, generateViewsDataJs, generateViewsDataTs, getAllDiagrams, parseDrawioRoundtripComments, parseDrawioToLikeC4, parseDrawioToLikeC4Multi };