@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.
- package/LICENSE +1 -1
- package/dist/index.d.mts +79 -0
- package/dist/index.mjs +928 -0
- package/package.json +20 -17
- package/src/d2/generate-d2.ts +1 -0
- package/src/drawio/generate-drawio.ts +219 -0
- package/src/drawio/index.ts +2 -0
- package/src/drawio/parse-drawio.ts +324 -0
- package/src/index.ts +2 -0
- package/src/mmd/generate-mmd.ts +3 -0
- package/src/puml/generate-puml.ts +3 -0
- package/src/react/generate-react-types.ts +2 -2
- package/dist/d2/generate-d2.d.ts +0 -3
- package/dist/d2/generate-d2.js +0 -87
- package/dist/d2/index.d.ts +0 -1
- package/dist/d2/index.js +0 -1
- package/dist/index.d.ts +0 -7
- package/dist/index.js +0 -7
- package/dist/mmd/generate-mmd.d.ts +0 -3
- package/dist/mmd/generate-mmd.js +0 -107
- package/dist/mmd/index.d.ts +0 -1
- package/dist/mmd/index.js +0 -1
- package/dist/model/generate-aux.d.ts +0 -4
- package/dist/model/generate-aux.js +0 -67
- package/dist/model/generate-likec4-model.d.ts +0 -4
- package/dist/model/generate-likec4-model.js +0 -22
- package/dist/model/generate-likec4.d.ts +0 -2
- package/dist/model/generate-likec4.js +0 -2
- package/dist/puml/generate-puml.d.ts +0 -3
- package/dist/puml/generate-puml.js +0 -170
- package/dist/puml/index.d.ts +0 -1
- package/dist/puml/index.js +0 -1
- package/dist/react/generate-react-types.d.ts +0 -4
- package/dist/react/generate-react-types.js +0 -64
- package/dist/react/index.d.ts +0 -1
- package/dist/react/index.js +0 -1
- package/dist/react-next/generate-react-next.d.ts +0 -20
- package/dist/react-next/generate-react-next.js +0 -102
- package/dist/react-next/index.d.ts +0 -1
- package/dist/react-next/index.js +0 -1
- package/dist/views-data-ts/generate-views-data.d.ts +0 -13
- package/dist/views-data-ts/generate-views-data.js +0 -130
- package/dist/views-data-ts/generateViewId.d.ts +0 -2
- package/dist/views-data-ts/generateViewId.js +0 -7
- package/dist/views-data-ts/index.d.ts +0 -1
- 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.
|
|
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
|
-
"
|
|
13
|
-
"!**/
|
|
14
|
-
"
|
|
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.
|
|
29
|
-
"import": "./dist/index.
|
|
30
|
-
"default": "./dist/index.
|
|
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
|
-
"
|
|
42
|
-
"@likec4/
|
|
43
|
+
"@likec4/core": "1.49.0",
|
|
44
|
+
"@likec4/log": "1.49.0"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
|
-
"@types/node": "~22.19.
|
|
47
|
+
"@types/node": "~22.19.10",
|
|
46
48
|
"typescript": "5.9.3",
|
|
47
|
-
"
|
|
48
|
-
"vitest": "4.0.
|
|
49
|
-
"@likec4/
|
|
50
|
-
"@likec4/
|
|
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": "
|
|
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.
|
|
61
|
-
"module": "dist/index.
|
|
63
|
+
"types": "dist/index.d.mts",
|
|
64
|
+
"module": "dist/index.mjs"
|
|
62
65
|
}
|
package/src/d2/generate-d2.ts
CHANGED
|
@@ -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, '&')
|
|
26
|
+
.replace(/</g, '<')
|
|
27
|
+
.replace(/>/g, '>')
|
|
28
|
+
.replace(/"/g, '"')
|
|
29
|
+
.replace(/'/g, ''')
|
|
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,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(/</g, '<')
|
|
123
|
+
.replace(/>/g, '>')
|
|
124
|
+
.replace(/"/g, '"')
|
|
125
|
+
.replace(/'/g, '\'')
|
|
126
|
+
.replace(/&/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'
|
package/src/mmd/generate-mmd.ts
CHANGED
|
@@ -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 {
|
|
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):
|
|
34
|
+
declare function useLikeC4View(viewId: $ViewId): LayoutedView<$Aux>;
|
|
35
35
|
|
|
36
36
|
declare function LikeC4ModelProvider(props: PropsWithChildren): JSX.Element;
|
|
37
37
|
|
package/dist/d2/generate-d2.d.ts
DELETED