@likec4/generators 1.47.0 → 1.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +1 -1
  2. package/dist/index.d.mts +79 -0
  3. package/dist/index.mjs +928 -0
  4. package/package.json +20 -17
  5. package/src/d2/generate-d2.ts +1 -0
  6. package/src/drawio/generate-drawio.ts +219 -0
  7. package/src/drawio/index.ts +2 -0
  8. package/src/drawio/parse-drawio.ts +324 -0
  9. package/src/index.ts +2 -0
  10. package/src/mmd/generate-mmd.ts +3 -0
  11. package/src/puml/generate-puml.ts +3 -0
  12. package/src/react/generate-react-types.ts +2 -2
  13. package/dist/d2/generate-d2.d.ts +0 -3
  14. package/dist/d2/generate-d2.js +0 -87
  15. package/dist/d2/index.d.ts +0 -1
  16. package/dist/d2/index.js +0 -1
  17. package/dist/index.d.ts +0 -7
  18. package/dist/index.js +0 -7
  19. package/dist/mmd/generate-mmd.d.ts +0 -3
  20. package/dist/mmd/generate-mmd.js +0 -107
  21. package/dist/mmd/index.d.ts +0 -1
  22. package/dist/mmd/index.js +0 -1
  23. package/dist/model/generate-aux.d.ts +0 -4
  24. package/dist/model/generate-aux.js +0 -67
  25. package/dist/model/generate-likec4-model.d.ts +0 -4
  26. package/dist/model/generate-likec4-model.js +0 -22
  27. package/dist/model/generate-likec4.d.ts +0 -2
  28. package/dist/model/generate-likec4.js +0 -2
  29. package/dist/puml/generate-puml.d.ts +0 -3
  30. package/dist/puml/generate-puml.js +0 -170
  31. package/dist/puml/index.d.ts +0 -1
  32. package/dist/puml/index.js +0 -1
  33. package/dist/react/generate-react-types.d.ts +0 -4
  34. package/dist/react/generate-react-types.js +0 -64
  35. package/dist/react/index.d.ts +0 -1
  36. package/dist/react/index.js +0 -1
  37. package/dist/react-next/generate-react-next.d.ts +0 -20
  38. package/dist/react-next/generate-react-next.js +0 -102
  39. package/dist/react-next/index.d.ts +0 -1
  40. package/dist/react-next/index.js +0 -1
  41. package/dist/views-data-ts/generate-views-data.d.ts +0 -13
  42. package/dist/views-data-ts/generate-views-data.js +0 -130
  43. package/dist/views-data-ts/generateViewId.d.ts +0 -2
  44. package/dist/views-data-ts/generateViewId.js +0 -7
  45. package/dist/views-data-ts/index.d.ts +0 -1
  46. package/dist/views-data-ts/index.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@likec4/generators",
3
- "version": "1.47.0",
3
+ "version": "1.49.0",
4
4
  "license": "MIT",
5
5
  "bugs": "https://github.com/likec4/likec4/issues",
6
6
  "homepage": "https://likec4.dev",
@@ -9,9 +9,10 @@
9
9
  "files": [
10
10
  "dist",
11
11
  "src",
12
- "!**/*.spec.ts",
13
- "!**/__mocks__/*",
14
- "!**/__snapshots__/*",
12
+ "**/package.json",
13
+ "!**/__*/**",
14
+ "!**/*.spec.*",
15
+ "!**/*.snap",
15
16
  "!**/*.map"
16
17
  ],
17
18
  "repository": {
@@ -25,9 +26,9 @@
25
26
  ".": {
26
27
  "sources": "./src/index.ts",
27
28
  "default": {
28
- "types": "./dist/index.d.ts",
29
- "import": "./dist/index.js",
30
- "default": "./dist/index.js"
29
+ "types": "./dist/index.d.mts",
30
+ "import": "./dist/index.mjs",
31
+ "default": "./dist/index.mjs"
31
32
  }
32
33
  }
33
34
  },
@@ -36,27 +37,29 @@
36
37
  "access": "public"
37
38
  },
38
39
  "dependencies": {
40
+ "remeda": "^2.33.5",
39
41
  "json5": "^2.2.3",
40
42
  "langium": "3.5.0",
41
- "remeda": "^2.32.0",
42
- "@likec4/core": "1.47.0"
43
+ "@likec4/core": "1.49.0",
44
+ "@likec4/log": "1.49.0"
43
45
  },
44
46
  "devDependencies": {
45
- "@types/node": "~22.19.3",
47
+ "@types/node": "~22.19.10",
46
48
  "typescript": "5.9.3",
47
- "unbuild": "3.5.0",
48
- "vitest": "4.0.16",
49
- "@likec4/devops": "1.42.0",
50
- "@likec4/tsconfig": "1.46.1"
49
+ "obuild": "^0.4.27",
50
+ "vitest": "4.0.18",
51
+ "@likec4/tsconfig": "1.49.0",
52
+ "@likec4/devops": "1.42.0"
51
53
  },
52
54
  "scripts": {
53
55
  "typecheck": "tsc -b --verbose",
54
- "build": "unbuild",
56
+ "build": "obuild",
55
57
  "lint": "run -T eslint src/ --fix",
56
58
  "clean": "likec4ops clean",
59
+ "pack": "pnpm pack",
57
60
  "test": "vitest run --no-isolate",
58
61
  "test:watch": "vitest"
59
62
  },
60
- "types": "dist/index.d.ts",
61
- "module": "dist/index.js"
63
+ "types": "dist/index.d.mts",
64
+ "module": "dist/index.mjs"
62
65
  }
@@ -45,6 +45,7 @@ const d2shape = ({ shape }: Node) => {
45
45
  case 'storage': {
46
46
  return 'stored_data' as const
47
47
  }
48
+ case 'component':
48
49
  case 'bucket':
49
50
  case 'mobile':
50
51
  case 'browser': {
@@ -0,0 +1,219 @@
1
+ import type { LikeC4ViewModel } from '@likec4/core/model'
2
+ import type { aux, DiagramNode, NodeId, ProcessedView } from '@likec4/core/types'
3
+ import { flattenMarkdownOrString } from '@likec4/core/types'
4
+ import { isEmptyish, isNullish as isNil } from 'remeda'
5
+
6
+ type View = ProcessedView<aux.Unknown>
7
+ type Node = View['nodes'][number]
8
+ type Edge = View['edges'][number]
9
+
10
+ /** Default hex colors when view model has no $styles (e.g. in tests). Aligns with common theme colors. */
11
+ const DEFAULT_ELEMENT_COLORS: Record<string, { fill: string; stroke: string }> = {
12
+ primary: { fill: '#3b82f6', stroke: '#2563eb' },
13
+ gray: { fill: '#6b7280', stroke: '#4b5563' },
14
+ green: { fill: '#22c55e', stroke: '#16a34a' },
15
+ red: { fill: '#ef4444', stroke: '#dc2626' },
16
+ blue: { fill: '#3b82f6', stroke: '#2563eb' },
17
+ indigo: { fill: '#6366f1', stroke: '#4f46e5' },
18
+ muted: { fill: '#9ca3af', stroke: '#6b7280' },
19
+ }
20
+
21
+ const DEFAULT_EDGE_COLOR = '#6b7280'
22
+
23
+ function escapeXml(unsafe: string): string {
24
+ return unsafe
25
+ .replace(/&/g, '&amp;')
26
+ .replace(/</g, '&lt;')
27
+ .replace(/>/g, '&gt;')
28
+ .replace(/"/g, '&quot;')
29
+ .replace(/'/g, '&apos;')
30
+ }
31
+
32
+ /**
33
+ * Map LikeC4 shape to DrawIO style. DrawIO uses shape=rectangle, ellipse, cylinder, etc.
34
+ */
35
+ function drawioShape(shape: Node['shape']): string {
36
+ switch (shape) {
37
+ case 'person':
38
+ return 'shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;'
39
+ case 'rectangle':
40
+ return 'shape=rectangle;'
41
+ case 'browser':
42
+ return 'shape=rectangle;rounded=1;'
43
+ case 'mobile':
44
+ return 'shape=rectangle;rounded=1;'
45
+ case 'cylinder':
46
+ return 'shape=cylinder3;whiteSpace=wrap;boundedLbl=1;backgroundOutline=1;size=15;'
47
+ case 'queue':
48
+ return 'shape=cylinder3;whiteSpace=wrap;boundedLbl=1;backgroundOutline=1;size=15;'
49
+ case 'storage':
50
+ return 'shape=cylinder3;whiteSpace=wrap;boundedLbl=1;backgroundOutline=1;size=15;'
51
+ case 'bucket':
52
+ return 'shape=rectangle;rounded=1;'
53
+ case 'document':
54
+ return 'shape=document;whiteSpace=wrap;html=1;boundedLbl=1;'
55
+ case 'component':
56
+ return 'shape=component;'
57
+ default:
58
+ return 'shape=rectangle;'
59
+ }
60
+ }
61
+
62
+ function getElementColors(
63
+ viewmodel: LikeC4ViewModel<aux.Unknown>,
64
+ color: string,
65
+ ): { fill: string; stroke: string } | undefined {
66
+ const styles = '$styles' in viewmodel && viewmodel.$styles ? viewmodel.$styles : null
67
+ if (styles) {
68
+ try {
69
+ const values = styles.colors(color)
70
+ return {
71
+ fill: values.elements.fill as string,
72
+ stroke: values.elements.stroke as string,
73
+ }
74
+ } catch {
75
+ // custom color or missing
76
+ }
77
+ }
78
+ return DEFAULT_ELEMENT_COLORS[color] ?? DEFAULT_ELEMENT_COLORS['primary']
79
+ }
80
+
81
+ function getEdgeStrokeColor(viewmodel: LikeC4ViewModel<aux.Unknown>, color: string): string {
82
+ const styles = '$styles' in viewmodel && viewmodel.$styles ? viewmodel.$styles : null
83
+ if (styles) {
84
+ try {
85
+ const values = styles.colors(color)
86
+ return values.relationships.line as string
87
+ } catch {
88
+ // custom color or missing
89
+ }
90
+ }
91
+ return DEFAULT_EDGE_COLOR
92
+ }
93
+
94
+ /**
95
+ * Generate DrawIO (mxGraph) XML from a layouted LikeC4 view.
96
+ * Preserves positions, hierarchy, colors, descriptions and technology so the diagram
97
+ * can be opened and edited in draw.io with full compatibility.
98
+ *
99
+ * @param viewmodel - Layouted LikeC4 view model (from model.view(id))
100
+ * @returns DrawIO .drawio XML string
101
+ */
102
+ export function generateDrawio(viewmodel: LikeC4ViewModel<aux.Unknown>): string {
103
+ const view = viewmodel.$view
104
+ const { nodes, edges } = view
105
+
106
+ const rootId = '0'
107
+ const defaultParentId = '1'
108
+
109
+ const nodeIds = new Map<NodeId, string>()
110
+ let cellId = 2
111
+
112
+ const getCellId = (nodeId: NodeId): string => {
113
+ let id = nodeIds.get(nodeId)
114
+ if (!id) {
115
+ id = String(cellId++)
116
+ nodeIds.set(nodeId, id)
117
+ }
118
+ return id
119
+ }
120
+
121
+ const vertexCells: string[] = []
122
+ const edgeCells: string[] = []
123
+
124
+ const sortedNodes = [...nodes].sort((a, b) => {
125
+ if (isNil(a.parent) && isNil(b.parent)) return 0
126
+ if (isNil(a.parent)) return -1
127
+ if (isNil(b.parent)) return 1
128
+ if (a.parent === b.parent) return 0
129
+ if (a.id.startsWith(b.id + '.')) return 1
130
+ if (b.id.startsWith(a.id + '.')) return -1
131
+ return 0
132
+ })
133
+
134
+ /** Support both BBox (x,y,width,height) and legacy position/size used in some mocks */
135
+ const getBBox = (n: View['nodes'][number]) => {
136
+ const d = n as DiagramNode & { position?: [number, number]; size?: { width: number; height: number } }
137
+ const x = typeof d.x === 'number' ? d.x : (Array.isArray(d.position) ? d.position[0] : 0)
138
+ const y = typeof d.y === 'number' ? d.y : (Array.isArray(d.position) ? d.position[1] : 0)
139
+ const width = typeof d.width === 'number' ? d.width : (d.size?.width ?? 120)
140
+ const height = typeof d.height === 'number' ? d.height : (d.size?.height ?? 60)
141
+ return { x, y, width, height }
142
+ }
143
+
144
+ for (const node of sortedNodes) {
145
+ const id = getCellId(node.id)
146
+ const parentId = node.parent ? getCellId(node.parent) : defaultParentId
147
+ const label = escapeXml(node.title)
148
+ const shapeStyle = drawioShape(node.shape)
149
+ const { x, y, width, height } = getBBox(node)
150
+
151
+ const elemColors = getElementColors(viewmodel, node.color)
152
+ const colorStyle = elemColors != null
153
+ ? `fillColor=${elemColors.fill};strokeColor=${elemColors.stroke};fontColor=${elemColors.stroke};`
154
+ : ''
155
+
156
+ const description = node.description && flattenMarkdownOrString(node.description)
157
+ const desc = isEmptyish(description) ? '' : escapeXml(description)
158
+ const technology = node.technology && flattenMarkdownOrString(node.technology)
159
+ const tech = isEmptyish(technology) ? '' : escapeXml(technology)
160
+ const userData = desc !== '' || tech !== ''
161
+ ? `<mxUserObject><data key="likec4Description">${desc}</data><data key="likec4Technology">${tech}</data></mxUserObject>\n `
162
+ : ''
163
+
164
+ vertexCells.push(
165
+ `<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}">
166
+ ${userData}<mxGeometry x="${Math.round(x)}" y="${Math.round(y)}" width="${Math.round(width)}" height="${
167
+ Math.round(height)
168
+ }" as="geometry" />
169
+ </mxCell>`,
170
+ )
171
+ }
172
+
173
+ for (const edge of edges as Edge[]) {
174
+ const id = String(cellId++)
175
+ const sourceId = getCellId(edge.source)
176
+ const targetId = getCellId(edge.target)
177
+ const label = edge.label ? escapeXml(edge.label) : ''
178
+ const strokeColor = getEdgeStrokeColor(viewmodel, edge.color)
179
+ const dashStyle = edge.line === 'dashed' ? 'dashed=1;' : edge.line === 'dotted' ? 'dashed=1;dashPattern=1 1;' : ''
180
+ edgeCells.push(
181
+ `<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}">
182
+ <mxGeometry relative="1" as="geometry" />
183
+ </mxCell>`,
184
+ )
185
+ }
186
+
187
+ let bounds: { x: number; y: number; width: number; height: number } = {
188
+ x: 0,
189
+ y: 0,
190
+ width: 800,
191
+ height: 600,
192
+ }
193
+ try {
194
+ const b = viewmodel.bounds
195
+ if (b != null && typeof b.x === 'number') bounds = b
196
+ } catch {
197
+ // View not layouted (e.g. in tests); use default canvas size
198
+ }
199
+ const allCells = [
200
+ `<mxCell id="${defaultParentId}" vertex="1" parent="${rootId}">
201
+ <mxGeometry x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" as="geometry" />
202
+ </mxCell>`,
203
+ ...vertexCells,
204
+ ...edgeCells,
205
+ ].join('\n')
206
+
207
+ return `<?xml version="1.0" encoding="UTF-8"?>
208
+ <mxfile host="LikeC4" modified="${new Date().toISOString()}" agent="LikeC4" version="1.0" etag="" type="device">
209
+ <diagram name="${escapeXml(view.id)}" id="likec4-${escapeXml(view.id)}">
210
+ <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">
211
+ <root>
212
+ <mxCell id="${rootId}" />
213
+ ${allCells}
214
+ </root>
215
+ </mxGraphModel>
216
+ </diagram>
217
+ </mxfile>
218
+ `
219
+ }
@@ -0,0 +1,2 @@
1
+ export { generateDrawio } from './generate-drawio'
2
+ export { parseDrawioToLikeC4, type DrawioCell } from './parse-drawio'
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Parse DrawIO (mxGraph) XML and generate LikeC4 source code.
3
+ * Extracts vertices as elements and edges as relations; preserves colors, descriptions,
4
+ * technology and other compatible attributes for full bidirectional compatibility.
5
+ */
6
+
7
+ export interface DrawioCell {
8
+ id: string
9
+ value?: string
10
+ parent?: string
11
+ source?: string
12
+ target?: string
13
+ vertex?: boolean
14
+ edge?: boolean
15
+ style?: string
16
+ x?: number
17
+ y?: number
18
+ width?: number
19
+ height?: number
20
+ /** From style fillColor= or mxUserObject */
21
+ fillColor?: string
22
+ strokeColor?: string
23
+ /** From mxUserObject data likec4Description / likec4Technology (exported by LikeC4) */
24
+ description?: string
25
+ technology?: string
26
+ }
27
+
28
+ function getAttr(attrs: string, name: string): string | undefined {
29
+ const re = new RegExp(`${name}="([^"]*)"`, 'i')
30
+ const m = attrs.match(re)
31
+ return m ? m[1] : undefined
32
+ }
33
+
34
+ function parseNum(s: string | undefined): number | undefined {
35
+ if (s === undefined || s === '') return undefined
36
+ const n = Number.parseFloat(s)
37
+ return Number.isNaN(n) ? undefined : n
38
+ }
39
+
40
+ /** Parse DrawIO style string (semicolon-separated key=value) into a map. */
41
+ function parseStyle(style: string | undefined): Map<string, string> {
42
+ const map = new Map<string, string>()
43
+ if (!style) return map
44
+ for (const part of style.split(';')) {
45
+ const eq = part.indexOf('=')
46
+ if (eq > 0) {
47
+ const k = part.slice(0, eq).trim()
48
+ const v = part.slice(eq + 1).trim()
49
+ if (k && v) map.set(k.toLowerCase(), v)
50
+ }
51
+ }
52
+ return map
53
+ }
54
+
55
+ /** Extract LikeC4 custom data from mxUserObject/data inside cell XML. */
56
+ function parseUserData(fullTag: string): { description?: string; technology?: string } {
57
+ const out: { description?: string; technology?: string } = {}
58
+ const descMatch = fullTag.match(/<data\s+key="likec4Description"[^>]*>([\s\S]*?)<\/data>/i)
59
+ if (descMatch?.[1]) out.description = decodeXmlEntities(descMatch[1].trim())
60
+ const techMatch = fullTag.match(/<data\s+key="likec4Technology"[^>]*>([\s\S]*?)<\/data>/i)
61
+ if (techMatch?.[1]) out.technology = decodeXmlEntities(techMatch[1].trim())
62
+ return out
63
+ }
64
+
65
+ /**
66
+ * Simple XML parser for DrawIO mxCell elements. Extracts cells with id, value, parent,
67
+ * source, target, vertex, edge, geometry, style colors and LikeC4 user data.
68
+ */
69
+ function parseDrawioXml(xml: string): DrawioCell[] {
70
+ const cells: DrawioCell[] = []
71
+ const mxCellRe = /<mxCell\s+([^>]+?)(?:\s*\/>|>([\s\S]*?)<\/mxCell>)/gi
72
+ const geomAttr = (tag: string, name: string) => getAttr(tag, name)
73
+ let m
74
+ while ((m = mxCellRe.exec(xml)) !== null) {
75
+ const attrs = m[1] ?? ''
76
+ const inner = m[2] ?? ''
77
+ const id = getAttr(attrs, 'id')
78
+ if (!id) continue
79
+ const valueRaw = getAttr(attrs, 'value')
80
+ const parent = getAttr(attrs, 'parent')
81
+ const source = getAttr(attrs, 'source')
82
+ const target = getAttr(attrs, 'target')
83
+ const vertex = getAttr(attrs, 'vertex') === '1'
84
+ const edge = getAttr(attrs, 'edge') === '1'
85
+ const style = getAttr(attrs, 'style')
86
+ const fullTag = m[0]
87
+ const geomMatch = fullTag.match(/<mxGeometry[^>]*>/i)
88
+ const geomStr = geomMatch ? geomMatch[0] : ''
89
+ const styleMap = parseStyle(style ?? undefined)
90
+ const userData = parseUserData(inner)
91
+ const x = parseNum(geomAttr(geomStr, 'x'))
92
+ const y = parseNum(geomAttr(geomStr, 'y'))
93
+ const width = parseNum(geomAttr(geomStr, 'width'))
94
+ const height = parseNum(geomAttr(geomStr, 'height'))
95
+ const fillColor = styleMap.get('fillcolor') ?? styleMap.get('fillColor')
96
+ const strokeColor = styleMap.get('strokecolor') ?? styleMap.get('strokeColor')
97
+ const cell: DrawioCell = {
98
+ id,
99
+ ...(valueRaw != null && valueRaw !== '' ? { value: decodeXmlEntities(valueRaw) } : {}),
100
+ ...(parent != null && parent !== '' ? { parent } : {}),
101
+ ...(source != null && source !== '' ? { source } : {}),
102
+ ...(target != null && target !== '' ? { target } : {}),
103
+ vertex,
104
+ edge,
105
+ ...(style != null && style !== '' ? { style } : {}),
106
+ ...(x !== undefined ? { x } : {}),
107
+ ...(y !== undefined ? { y } : {}),
108
+ ...(width !== undefined ? { width } : {}),
109
+ ...(height !== undefined ? { height } : {}),
110
+ ...(fillColor !== undefined ? { fillColor } : {}),
111
+ ...(strokeColor !== undefined ? { strokeColor } : {}),
112
+ ...(userData.description != null ? { description: userData.description } : {}),
113
+ ...(userData.technology != null ? { technology: userData.technology } : {}),
114
+ }
115
+ cells.push(cell)
116
+ }
117
+ return cells
118
+ }
119
+
120
+ function decodeXmlEntities(s: string): string {
121
+ return s
122
+ .replace(/&lt;/g, '<')
123
+ .replace(/&gt;/g, '>')
124
+ .replace(/&quot;/g, '"')
125
+ .replace(/&apos;/g, '\'')
126
+ .replace(/&amp;/g, '&')
127
+ }
128
+
129
+ /**
130
+ * Infer LikeC4 element kind from DrawIO shape style.
131
+ */
132
+ function inferKind(style: string | undefined): 'actor' | 'system' | 'container' | 'component' {
133
+ if (!style) return 'container'
134
+ const s = style.toLowerCase()
135
+ if (s.includes('umlactor') || s.includes('shape=person')) return 'actor'
136
+ if (s.includes('swimlane') || s.includes('shape=rectangle') && s.includes('rounded')) return 'system'
137
+ return 'container'
138
+ }
139
+
140
+ /**
141
+ * Sanitize a string for use as LikeC4 identifier (element name).
142
+ */
143
+ function toId(name: string): string {
144
+ return name
145
+ .trim()
146
+ .replace(/\s+/g, '_')
147
+ .replace(/[^a-zA-Z0-9_.-]/g, '')
148
+ .replace(/^[0-9]/, '_$&') || 'element'
149
+ }
150
+
151
+ /**
152
+ * Convert DrawIO XML to LikeC4 source (.c4) string.
153
+ * - Vertices become model elements (actor/container); hierarchy from parent refs.
154
+ * - Edges become relations (->).
155
+ * - Root diagram cells (parent "1") are top-level; others are nested by parent.
156
+ */
157
+ export function parseDrawioToLikeC4(xml: string): string {
158
+ const cells = parseDrawioXml(xml)
159
+ const byId = new Map<string, DrawioCell>()
160
+ for (const c of cells) {
161
+ byId.set(c.id, c)
162
+ }
163
+
164
+ const vertices = cells.filter(c => c.vertex && c.id !== '1')
165
+ const edges = cells.filter(c => c.edge && c.source && c.target)
166
+
167
+ // Build hierarchy: root is parent "1". Assign FQN by traversing parent chain.
168
+ const rootId = '1'
169
+ const idToFqn = new Map<string, string>()
170
+ const idToCell = new Map<string, DrawioCell>()
171
+ for (const v of vertices) {
172
+ idToCell.set(v.id, v)
173
+ }
174
+
175
+ // Assign FQNs: use value as base name, ensure uniqueness. Flatten for simplicity if no clear hierarchy.
176
+ const usedNames = new Set<string>()
177
+ function uniqueName(base: string): string {
178
+ let name = toId(base || 'element')
179
+ let n = name
180
+ let i = 0
181
+ while (usedNames.has(n)) {
182
+ n = `${name}_${++i}`
183
+ }
184
+ usedNames.add(n)
185
+ return n
186
+ }
187
+
188
+ for (const v of vertices) {
189
+ if (v.parent === rootId || !v.parent) {
190
+ const name = uniqueName(v.value ?? v.id)
191
+ idToFqn.set(v.id, name)
192
+ }
193
+ }
194
+
195
+ // If we have parent refs that are not root, build hierarchy (e.g. parent is another vertex)
196
+ let changed = true
197
+ while (changed) {
198
+ changed = false
199
+ for (const v of vertices) {
200
+ if (idToFqn.has(v.id)) continue
201
+ const parent = v.parent ? idToFqn.get(v.parent) : null
202
+ if (parent != null) {
203
+ const local = uniqueName(v.value ?? v.id)
204
+ idToFqn.set(v.id, `${parent}.${local}`)
205
+ changed = true
206
+ }
207
+ }
208
+ }
209
+
210
+ // Any remaining vertices (orphans) get top-level names
211
+ for (const v of vertices) {
212
+ if (!idToFqn.has(v.id)) {
213
+ idToFqn.set(v.id, uniqueName(v.value ?? v.id))
214
+ }
215
+ }
216
+
217
+ // Collect unique hex colors from vertices for specification customColors
218
+ const hexToCustomName = new Map<string, string>()
219
+ let customColorIndex = 0
220
+ for (const v of vertices) {
221
+ const fill = v.fillColor?.trim()
222
+ if (fill && /^#[0-9A-Fa-f]{3,8}$/.test(fill)) {
223
+ if (!hexToCustomName.has(fill)) {
224
+ hexToCustomName.set(fill, `drawio_color_${++customColorIndex}`)
225
+ }
226
+ }
227
+ }
228
+
229
+ const lines: string[] = []
230
+
231
+ if (hexToCustomName.size > 0) {
232
+ lines.push('specification {')
233
+ for (const [hex, name] of hexToCustomName) {
234
+ lines.push(` color ${name} ${hex}`)
235
+ }
236
+ lines.push('}')
237
+ lines.push('')
238
+ }
239
+
240
+ lines.push('model {')
241
+ lines.push('')
242
+
243
+ const children = new Map<string, Array<{ cellId: string; fqn: string }>>()
244
+ const roots: Array<{ cellId: string; fqn: string }> = []
245
+ for (const [cellId, fqn] of idToFqn) {
246
+ const cell = idToCell.get(cellId)
247
+ if (!cell) continue
248
+ if (cell.parent === rootId || !cell.parent) {
249
+ roots.push({ cellId, fqn })
250
+ } else {
251
+ const parentFqn = idToFqn.get(cell.parent)
252
+ if (parentFqn != null) {
253
+ const list = children.get(parentFqn) ?? []
254
+ list.push({ cellId, fqn })
255
+ children.set(parentFqn, list)
256
+ } else {
257
+ roots.push({ cellId, fqn })
258
+ }
259
+ }
260
+ }
261
+
262
+ function emitElement(cellId: string, fqn: string, indent: number): void {
263
+ const cell = idToCell.get(cellId)
264
+ if (!cell) return
265
+ const kind = inferKind(cell.style)
266
+ const title = (cell.value && cell.value.trim()) || fqn.split('.').pop() || 'Element'
267
+ const name = fqn.split('.').pop()!
268
+ const pad = ' '.repeat(indent)
269
+ const desc = cell.description?.trim()
270
+ const tech = cell.technology?.trim()
271
+ const colorName = cell.fillColor && /^#[0-9A-Fa-f]{3,8}$/.test(cell.fillColor.trim())
272
+ ? hexToCustomName.get(cell.fillColor.trim())
273
+ : undefined
274
+
275
+ if (kind === 'actor') {
276
+ lines.push(`${pad}${name} = actor '${title.replace(/'/g, '\'\'')}'`)
277
+ } else if (kind === 'system') {
278
+ lines.push(`${pad}${name} = system '${title.replace(/'/g, '\'\'')}'`)
279
+ } else {
280
+ lines.push(`${pad}${name} = container '${title.replace(/'/g, '\'\'')}'`)
281
+ }
282
+ const childList = children.get(fqn)
283
+ const hasBody = (childList && childList.length > 0) || desc || tech || colorName
284
+ if (hasBody) {
285
+ lines.push(`${pad}{`)
286
+ if (colorName) lines.push(`${pad} style { color ${colorName} }`)
287
+ if (desc) lines.push(`${pad} description '${desc.replace(/'/g, '\'\'')}'`)
288
+ if (tech) lines.push(`${pad} technology '${tech.replace(/'/g, '\'\'')}'`)
289
+ if (childList && childList.length > 0) {
290
+ for (const ch of childList) {
291
+ emitElement(ch.cellId, ch.fqn, indent + 1)
292
+ }
293
+ }
294
+ lines.push(`${pad}}`)
295
+ } else {
296
+ lines.push(`${pad}{`)
297
+ lines.push(`${pad}}`)
298
+ }
299
+ lines.push('')
300
+ }
301
+
302
+ for (const { cellId, fqn } of roots) {
303
+ emitElement(cellId, fqn, 1)
304
+ }
305
+
306
+ for (const e of edges) {
307
+ const src = idToFqn.get(e.source!)
308
+ const tgt = idToFqn.get(e.target!)
309
+ if (!src || !tgt) continue
310
+ const label = (e.value && e.value.trim()) ? ` '${e.value.replace(/'/g, '\'\'')}'` : ''
311
+ lines.push(` ${src} -> ${tgt}${label}`)
312
+ }
313
+
314
+ lines.push('}')
315
+ lines.push('')
316
+ lines.push('views {')
317
+ lines.push(' view index {')
318
+ lines.push(' include *')
319
+ lines.push(' }')
320
+ lines.push('}')
321
+ lines.push('')
322
+
323
+ return lines.join('\n')
324
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
+ export { generateDrawio } from './drawio/generate-drawio'
1
2
  export { generateD2 } from './d2/generate-d2'
2
3
  export { generateMermaid } from './mmd/generate-mmd'
4
+ export { parseDrawioToLikeC4 } from './drawio/parse-drawio'
3
5
  export { generateLikeC4Model } from './model/generate-likec4-model'
4
6
  export { generatePuml } from './puml/generate-puml'
5
7
  export { generateReactNext } from './react-next/generate-react-next'
@@ -45,6 +45,9 @@ const mmdshape = ({ shape, title }: Node): string => {
45
45
  case 'document': {
46
46
  return `@{ shape: doc, ${label} }`
47
47
  }
48
+ case 'component': {
49
+ return `@{ shape: rectangle, ${label} }`
50
+ }
48
51
  default:
49
52
  nonexhaustive(shape)
50
53
  }
@@ -68,6 +68,9 @@ const pumlShape = ({ shape }: ComputedNode) => {
68
68
  case 'cylinder': {
69
69
  return 'database' as const
70
70
  }
71
+ case 'component': {
72
+ return 'component' as const
73
+ }
71
74
  case 'document':
72
75
  case 'mobile':
73
76
  case 'bucket':
@@ -19,7 +19,7 @@ export function generateReactTypes(model: AnyLikeC4Model, options: { useCorePack
19
19
  import type { PropsWithChildren } from 'react'
20
20
  import type { JSX } from 'react/jsx-runtime'
21
21
  import type { LikeC4Model } from '${useCorePackage ? '@likec4/core' : 'likec4'}/model'
22
- import type { DiagramView } from '${useCorePackage ? '@likec4/core/types' : 'likec4/model'}'
22
+ import type { LayoutedView } from '${useCorePackage ? '@likec4/core/types' : 'likec4/model'}'
23
23
  import type {
24
24
  LikeC4ViewProps as GenericLikeC4ViewProps,
25
25
  ReactLikeC4Props as GenericReactLikeC4Props
@@ -31,7 +31,7 @@ declare function isLikeC4ViewId(value: unknown): value is $ViewId;
31
31
 
32
32
  declare const likec4model: LikeC4Model<$Aux>;
33
33
  declare function useLikeC4Model(): LikeC4Model<$Aux>;
34
- declare function useLikeC4View(viewId: $ViewId): DiagramView<$Aux>;
34
+ declare function useLikeC4View(viewId: $ViewId): LayoutedView<$Aux>;
35
35
 
36
36
  declare function LikeC4ModelProvider(props: PropsWithChildren): JSX.Element;
37
37
 
@@ -1,3 +0,0 @@
1
- import type { LikeC4ViewModel } from '@likec4/core/model';
2
- import type { aux } from '@likec4/core/types';
3
- export declare function generateD2(viewmodel: LikeC4ViewModel<aux.Unknown>): any;