@likec4/generators 1.52.0 → 1.53.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/_chunks/chunk.mjs +11 -0
- package/dist/index.d.mts +18 -1
- package/dist/index.mjs +189 -47
- package/dist/likec4/index.d.mts +277169 -0
- package/dist/likec4/index.mjs +1799 -0
- package/likec4/package.json +4 -0
- package/package.json +22 -8
- package/src/drawio/generate-drawio.ts +73 -8
- package/src/drawio/index.ts +1 -0
- package/src/drawio/parse-drawio.ts +172 -18
- package/src/index.ts +2 -0
- package/src/likec4/generate-likec4.ts +72 -0
- package/src/likec4/index.ts +12 -0
- package/src/likec4/operators/base.ts +938 -0
- package/src/likec4/operators/deployment.ts +263 -0
- package/src/likec4/operators/expressions.ts +422 -0
- package/src/likec4/operators/index.ts +13 -0
- package/src/likec4/operators/likec4data.ts +33 -0
- package/src/likec4/operators/model.ts +222 -0
- package/src/likec4/operators/properties.ts +244 -0
- package/src/likec4/operators/specification.ts +119 -0
- package/src/likec4/operators/views.ts +390 -0
- package/src/likec4/schemas/common.ts +123 -0
- package/src/likec4/schemas/deployment.ts +113 -0
- package/src/likec4/schemas/expression.ts +218 -0
- package/src/likec4/schemas/index.ts +83 -0
- package/src/likec4/schemas/likec4data.ts +76 -0
- package/src/likec4/schemas/model.ts +127 -0
- package/src/likec4/schemas/specification.ts +83 -0
- package/src/likec4/schemas/views.ts +321 -0
- package/src/model/generate-likec4.ts +0 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@likec4/generators",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.53.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bugs": "https://github.com/likec4/likec4/issues",
|
|
6
6
|
"homepage": "https://likec4.dev",
|
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
"import": "./dist/index.mjs",
|
|
31
31
|
"default": "./dist/index.mjs"
|
|
32
32
|
}
|
|
33
|
+
},
|
|
34
|
+
"./likec4": {
|
|
35
|
+
"sources": "./src/likec4/index.ts",
|
|
36
|
+
"default": {
|
|
37
|
+
"types": "./dist/likec4/index.d.mts",
|
|
38
|
+
"import": "./dist/likec4/index.mjs",
|
|
39
|
+
"default": "./dist/likec4/index.mjs"
|
|
40
|
+
}
|
|
33
41
|
}
|
|
34
42
|
},
|
|
35
43
|
"publishConfig": {
|
|
@@ -40,18 +48,24 @@
|
|
|
40
48
|
"langium": "3.5.0",
|
|
41
49
|
"pako": "^2.1.0",
|
|
42
50
|
"remeda": "^2.33.6",
|
|
51
|
+
"immer": "^11.1.4",
|
|
43
52
|
"json5": "^2.2.3",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
53
|
+
"type-fest": "^4.41.0",
|
|
54
|
+
"indent-string": "^5.0.0",
|
|
55
|
+
"strip-indent": "^4.1.1",
|
|
56
|
+
"zod": "^3.25.76",
|
|
57
|
+
"@likec4/core": "1.53.0",
|
|
58
|
+
"@likec4/log": "1.53.0",
|
|
59
|
+
"@likec4/config": "1.53.0"
|
|
46
60
|
},
|
|
47
61
|
"devDependencies": {
|
|
48
|
-
"@types/node": "~22.19.
|
|
62
|
+
"@types/node": "~22.19.15",
|
|
49
63
|
"@types/pako": "^2.0.4",
|
|
50
64
|
"typescript": "5.9.3",
|
|
51
|
-
"obuild": "
|
|
52
|
-
"vitest": "4.0
|
|
53
|
-
"@likec4/
|
|
54
|
-
"@likec4/
|
|
65
|
+
"obuild": "0.4.31",
|
|
66
|
+
"vitest": "4.1.0",
|
|
67
|
+
"@likec4/devops": "1.42.0",
|
|
68
|
+
"@likec4/tsconfig": "1.53.0"
|
|
55
69
|
},
|
|
56
70
|
"scripts": {
|
|
57
71
|
"typecheck": "tsc -b --verbose",
|
|
@@ -438,6 +438,39 @@ function buildLikec4StyleForNode(params: NodeLikec4StyleParams): string {
|
|
|
438
438
|
return parts.length > 0 ? parts.join(';') + ';' : ''
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
/** Bridge-managed style parts for profile 'leanix': likec4Id, likec4Kind, likec4ViewId, likec4ProjectId, bridgeManaged, optional leanixFactSheetType. */
|
|
442
|
+
function buildBridgeManagedStyleForNode(
|
|
443
|
+
nodeId: string,
|
|
444
|
+
nodeKind: string,
|
|
445
|
+
viewId: string,
|
|
446
|
+
options: GenerateDrawioOptions | undefined,
|
|
447
|
+
): string {
|
|
448
|
+
if (options?.profile !== 'leanix') return ''
|
|
449
|
+
const parts = [
|
|
450
|
+
'bridgeManaged=true',
|
|
451
|
+
`likec4Id=${encodeURIComponent(nodeId)}`,
|
|
452
|
+
`likec4Kind=${encodeURIComponent(nodeKind)}`,
|
|
453
|
+
`likec4ViewId=${encodeURIComponent(viewId)}`,
|
|
454
|
+
]
|
|
455
|
+
if (options.projectId != null && options.projectId !== '') {
|
|
456
|
+
parts.push(`likec4ProjectId=${encodeURIComponent(options.projectId)}`)
|
|
457
|
+
}
|
|
458
|
+
const factSheetType = options.leanixFactSheetTypeByKind?.[nodeKind]
|
|
459
|
+
if (factSheetType != null && factSheetType !== '') {
|
|
460
|
+
parts.push(`leanixFactSheetType=${encodeURIComponent(factSheetType)}`)
|
|
461
|
+
}
|
|
462
|
+
return parts.join(';') + ';'
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Bridge-managed style parts for edge when profile is 'leanix': likec4RelationId, bridgeManaged. */
|
|
466
|
+
function buildBridgeManagedStyleForEdge(
|
|
467
|
+
relationId: string,
|
|
468
|
+
options: GenerateDrawioOptions | undefined,
|
|
469
|
+
): string {
|
|
470
|
+
if (options?.profile !== 'leanix') return ''
|
|
471
|
+
return `bridgeManaged=true;likec4RelationId=${encodeURIComponent(relationId)};`
|
|
472
|
+
}
|
|
473
|
+
|
|
441
474
|
/** Build mxUserObject XML from customData for round-trip; returns empty string when customData is missing or empty. */
|
|
442
475
|
function buildMxUserObjectXml(customData: Record<string, string> | undefined): string {
|
|
443
476
|
if (
|
|
@@ -508,12 +541,13 @@ function buildEdgeGeometryXml(
|
|
|
508
541
|
return `<mxGeometry relative="1" as="geometry">${pointsXml}</mxGeometry>`
|
|
509
542
|
}
|
|
510
543
|
|
|
511
|
-
/** Full edge style string for mxCell (arrows, anchors, stroke, dash, label, likec4 roundtrip). */
|
|
544
|
+
/** Full edge style string for mxCell (arrows, anchors, stroke, dash, label, likec4 roundtrip, optional bridge-managed). */
|
|
512
545
|
function buildEdgeStyleString(
|
|
513
546
|
edge: Edge,
|
|
514
547
|
layout: DiagramLayoutState,
|
|
515
548
|
viewmodel: DrawioViewModelLike,
|
|
516
549
|
label: string,
|
|
550
|
+
options: GenerateDrawioOptions | undefined,
|
|
517
551
|
): string {
|
|
518
552
|
const { bboxes, fontFamily } = layout
|
|
519
553
|
const sourceBbox = bboxes.get(edge.source)
|
|
@@ -550,13 +584,14 @@ function buildEdgeStyleString(
|
|
|
550
584
|
edgeLinksJson,
|
|
551
585
|
edgeMetadataJson,
|
|
552
586
|
})
|
|
587
|
+
const edgeBridgeStyle = buildBridgeManagedStyleForEdge(edge.id, options)
|
|
553
588
|
const edgeLabelColors = getEdgeLabelColors(viewmodel, edge.color)
|
|
554
589
|
const edgeLabelStyle = label === ''
|
|
555
590
|
? ''
|
|
556
591
|
: `fontColor=${edgeLabelColors.font};fontSize=12;align=center;verticalAlign=middle;labelBackgroundColor=none;fontFamily=${
|
|
557
592
|
encodeURIComponent(fontFamily)
|
|
558
593
|
};`
|
|
559
|
-
return `endArrow=${endArrow};startArrow=${startArrow};html=1;rounded=0;${anchorStyle}strokeColor=${strokeColor};strokeWidth=2;${dashStyle}${edgeLabelStyle}${edgeLikec4Style}`
|
|
594
|
+
return `endArrow=${endArrow};startArrow=${startArrow};html=1;rounded=0;${anchorStyle}strokeColor=${strokeColor};strokeWidth=2;${dashStyle}${edgeLabelStyle}${edgeLikec4Style}${edgeBridgeStyle}`
|
|
560
595
|
}
|
|
561
596
|
|
|
562
597
|
/** Build a single edge mxCell XML (orchestrator: label + geometry + style + assembly). */
|
|
@@ -573,7 +608,7 @@ function buildEdgeCellXml(
|
|
|
573
608
|
const targetId = getCellId(edge.target)
|
|
574
609
|
const label = buildEdgeLabelValue(edge)
|
|
575
610
|
const edgeGeometryXml = buildEdgeGeometryXml(edge, options?.edgeWaypoints)
|
|
576
|
-
const styleStr = buildEdgeStyleString(edge, layout, viewmodel, label)
|
|
611
|
+
const styleStr = buildEdgeStyleString(edge, layout, viewmodel, label, options)
|
|
577
612
|
const edgeCustomData = edgeOptionalFields.getCustomData(edge)
|
|
578
613
|
const edgeUserObjectXml = buildMxUserObjectXml(edgeCustomData)
|
|
579
614
|
return `<mxCell id="${edgeCellId}" value="${label}" style="${styleStr}" edge="1" parent="${defaultParentId}" source="${sourceId}" target="${targetId}">
|
|
@@ -650,6 +685,7 @@ function computeNodeStylePartsAndValue(
|
|
|
650
685
|
const strokeColorByNodeId = options?.strokeColorByNodeId
|
|
651
686
|
const strokeWidthByNodeId = options?.strokeWidthByNodeId
|
|
652
687
|
const isContainer = containerNodeIds.has(node.id)
|
|
688
|
+
const nodeKind = (node as Node & { kind?: string }).kind ?? ''
|
|
653
689
|
const title = node.title
|
|
654
690
|
const desc = toExportString(node.description)
|
|
655
691
|
const tech = toExportString(node.technology)
|
|
@@ -659,8 +695,11 @@ function computeNodeStylePartsAndValue(
|
|
|
659
695
|
const navTo = toNonEmptyString(nodeOptionalFields.getNavigateTo(node))
|
|
660
696
|
const iconName = toNonEmptyString(nodeOptionalFields.getIcon(node))
|
|
661
697
|
|
|
698
|
+
const isActor = nodeKind === 'actor' || node.shape === 'person'
|
|
662
699
|
const shapeStyle = isContainer
|
|
663
700
|
? 'shape=rectangle;rounded=0;container=1;collapsible=0;startSize=0;'
|
|
701
|
+
: isActor
|
|
702
|
+
? 'shape=actor;'
|
|
664
703
|
: drawioShape(node.shape)
|
|
665
704
|
const strokeColorOverride = strokeColorByNodeId?.[node.id]
|
|
666
705
|
const strokeWidthOverride = strokeWidthByNodeId?.[node.id]
|
|
@@ -713,6 +752,8 @@ function computeNodeStylePartsAndValue(
|
|
|
713
752
|
strokeHex,
|
|
714
753
|
nodeNotation,
|
|
715
754
|
})
|
|
755
|
+
const bridgeStyle = buildBridgeManagedStyleForNode(node.id, nodeKind, layout.view.id, options)
|
|
756
|
+
const likec4StyleWithBridge = likec4Style + bridgeStyle
|
|
716
757
|
|
|
717
758
|
const nodeCustomData = nodeOptionalFields.getCustomData(node)
|
|
718
759
|
const userObjectXml = buildMxUserObjectXml(nodeCustomData)
|
|
@@ -726,7 +767,7 @@ function computeNodeStylePartsAndValue(
|
|
|
726
767
|
|
|
727
768
|
// vertexTextStyle already includes html=1; for both container and non-container
|
|
728
769
|
const styleStr =
|
|
729
|
-
`${vertexTextStyle}${shapeStyle}${colorStyle}${strokeWidthStyle}${containerDashed}${fillOpacityStyle}${navLinkStyle}${
|
|
770
|
+
`${vertexTextStyle}${shapeStyle}${colorStyle}${strokeWidthStyle}${containerDashed}${fillOpacityStyle}${navLinkStyle}${likec4StyleWithBridge}`
|
|
730
771
|
|
|
731
772
|
return {
|
|
732
773
|
value,
|
|
@@ -850,20 +891,32 @@ function getViewDescriptionString(view: View): string {
|
|
|
850
891
|
return ''
|
|
851
892
|
}
|
|
852
893
|
|
|
853
|
-
/**
|
|
854
|
-
function
|
|
894
|
+
/** Returns draw.io style tokens for the leanix profile (bridgeManaged, likec4ViewId, likec4ProjectId). Each token ends with ";". */
|
|
895
|
+
function getLeanixRootStyleParts(view: View, options: GenerateDrawioOptions): string[] {
|
|
896
|
+
const parts = ['bridgeManaged=true;', `likec4ViewId=${encodeURIComponent(view.id)};`]
|
|
897
|
+
if (options.projectId != null && options.projectId !== '') {
|
|
898
|
+
parts.push(`likec4ProjectId=${encodeURIComponent(options.projectId)};`)
|
|
899
|
+
}
|
|
900
|
+
return parts
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** Build root cell style string from view metadata (title, description, notation) for round-trip; when profile is 'leanix' adds likec4ViewId, likec4ProjectId, bridgeManaged. */
|
|
904
|
+
function buildRootCellStyle(view: View, options: GenerateDrawioOptions | undefined): string {
|
|
855
905
|
const viewTitle = getViewTitle(view)
|
|
856
906
|
const viewDesc = getViewDescriptionString(view)
|
|
857
907
|
const viewDescEnc = viewDesc.trim() !== '' ? encodeURIComponent(viewDesc.trim()) : ''
|
|
858
908
|
const viewNotationRaw = (view as unknown as { notation?: unknown }).notation
|
|
859
909
|
const viewNotation = typeof viewNotationRaw === 'string' && viewNotationRaw !== '' ? viewNotationRaw : undefined
|
|
860
910
|
const viewNotationEnc = viewNotation != null ? encodeURIComponent(viewNotation) : ''
|
|
861
|
-
const rootParts = [
|
|
911
|
+
const rootParts: string[] = [
|
|
862
912
|
'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=none;',
|
|
863
913
|
`likec4ViewTitle=${encodeURIComponent(viewTitle ?? view.id)};`,
|
|
864
914
|
viewDescEnc !== '' ? `likec4ViewDescription=${viewDescEnc};` : '',
|
|
865
915
|
viewNotationEnc !== '' ? `likec4ViewNotation=${viewNotationEnc};` : '',
|
|
866
916
|
]
|
|
917
|
+
if (options?.profile === 'leanix') {
|
|
918
|
+
rootParts.push(...getLeanixRootStyleParts(view, options))
|
|
919
|
+
}
|
|
867
920
|
return rootParts.join('')
|
|
868
921
|
}
|
|
869
922
|
|
|
@@ -892,6 +945,9 @@ function drawioArrow(arrow: string | undefined | null): string {
|
|
|
892
945
|
}
|
|
893
946
|
}
|
|
894
947
|
|
|
948
|
+
/** Draw.io export profile: default (round-trip) or leanix (bridge-managed metadata for LeanIX interoperability). */
|
|
949
|
+
export type DrawioExportProfile = 'default' | 'leanix'
|
|
950
|
+
|
|
895
951
|
/** Optional overrides for round-trip (e.g. from parsed comment blocks). Keys are node/edge ids from the view. */
|
|
896
952
|
export type GenerateDrawioOptions = {
|
|
897
953
|
/** Node id -> bbox to use instead of viewmodel layout */
|
|
@@ -912,6 +968,15 @@ export type GenerateDrawioOptions = {
|
|
|
912
968
|
* Set for deterministic output (e.g. tests, content-addressable storage).
|
|
913
969
|
*/
|
|
914
970
|
modified?: string
|
|
971
|
+
/**
|
|
972
|
+
* Export profile. When 'leanix', adds bridge-managed metadata (likec4Id, likec4Kind, likec4ViewId,
|
|
973
|
+
* likec4ProjectId, likec4RelationId, bridgeManaged) for round-trip and LeanIX interoperability.
|
|
974
|
+
*/
|
|
975
|
+
profile?: DrawioExportProfile
|
|
976
|
+
/** Project id (included when profile is 'leanix' as likec4ProjectId). */
|
|
977
|
+
projectId?: string
|
|
978
|
+
/** Optional mapping of element kind -> LeanIX fact sheet type (included when profile is 'leanix' as leanixFactSheetType on vertices). */
|
|
979
|
+
leanixFactSheetTypeByKind?: Record<string, string>
|
|
915
980
|
}
|
|
916
981
|
|
|
917
982
|
/** Result of layout phase: bboxes, offsets, and shared styling so cell-building phase stays readable. */
|
|
@@ -1202,7 +1267,7 @@ function generateDiagramContent(
|
|
|
1202
1267
|
)
|
|
1203
1268
|
}
|
|
1204
1269
|
|
|
1205
|
-
const rootCellStyle = buildRootCellStyle(view)
|
|
1270
|
+
const rootCellStyle = buildRootCellStyle(view, options)
|
|
1206
1271
|
|
|
1207
1272
|
const allCells = [
|
|
1208
1273
|
`<mxCell id="${defaultParentId}" value="" style="${rootCellStyle}" vertex="1" parent="${rootId}">
|
package/src/drawio/index.ts
CHANGED
|
@@ -89,10 +89,16 @@ export interface DrawioCell {
|
|
|
89
89
|
notation?: string
|
|
90
90
|
/** From style likec4Metadata (edge; JSON object for relation metadata block) */
|
|
91
91
|
metadata?: string
|
|
92
|
+
/** From style likec4Id (vertex; bridge-managed element id for round-trip) */
|
|
93
|
+
likec4Id?: string
|
|
94
|
+
/** From style likec4RelationId (edge; bridge-managed relation id for round-trip) */
|
|
95
|
+
likec4RelationId?: string
|
|
92
96
|
/** From mxUserObject/data keys not mapped to fields above (JSON object for round-trip comment) */
|
|
93
97
|
customData?: string
|
|
94
98
|
/** From mxGeometry Array/mxPoint (edge waypoints; JSON array of [x,y][]) */
|
|
95
99
|
edgePoints?: string
|
|
100
|
+
/** From style key "shape" (e.g. actor, person) for kind/shape inference when raw style string is unavailable */
|
|
101
|
+
shapeFromStyle?: string
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
/** Edge cell with required source and target; use after filtering with isEdgeWithEndpoints. */
|
|
@@ -132,6 +138,77 @@ function getAttr(attrs: string, name: string): string | undefined {
|
|
|
132
138
|
return undefined
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Fallback to extract style value from attrs when getAttr(attrs, 'style') returns undefined (e.g. spacing like "style =\"").
|
|
143
|
+
* Looks for style="..." (case-insensitive, optional spaces before '=') and returns the quoted value so the cell gets style when present in XML.
|
|
144
|
+
*/
|
|
145
|
+
function extractStyleFromAttrsFallback(attrs: string): string | undefined {
|
|
146
|
+
const lower = attrs.toLowerCase()
|
|
147
|
+
let i = lower.indexOf('style')
|
|
148
|
+
while (i !== -1) {
|
|
149
|
+
const prev = i === 0 ? ' ' : (attrs[i - 1] ?? ' ')
|
|
150
|
+
if (!isAttrBoundaryChar(prev)) {
|
|
151
|
+
i = lower.indexOf('style', i + 1)
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
let j = i + 5 // after "style"
|
|
155
|
+
while (j < attrs.length && isAttrBoundaryChar(attrs[j] ?? '')) j += 1
|
|
156
|
+
if (j < attrs.length && attrs[j] === '=') {
|
|
157
|
+
j += 1
|
|
158
|
+
while (j < attrs.length && isAttrBoundaryChar(attrs[j] ?? '')) j += 1
|
|
159
|
+
if (j < attrs.length && attrs[j] === '"') {
|
|
160
|
+
const valueStart = j + 1
|
|
161
|
+
const valueEnd = attrs.indexOf('"', valueStart)
|
|
162
|
+
if (valueEnd !== -1) return attrs.slice(valueStart, valueEnd)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
i = lower.indexOf('style', i + 1)
|
|
166
|
+
}
|
|
167
|
+
return undefined
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Extract style value from full open tag (e.g. from fullTag.slice(0, fullTag.indexOf('>'))). Use when attrs-based extraction missed it. */
|
|
171
|
+
function extractStyleFromOpenTag(fullTag: string): string | undefined {
|
|
172
|
+
const gt = fullTag.indexOf('>')
|
|
173
|
+
if (gt === -1) return undefined
|
|
174
|
+
return getAttr(fullTag.slice(7, gt), 'style') ?? extractStyleFromAttrsFallback(fullTag.slice(7, gt))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Max fullTag length to run style re-extraction (avoids scanning huge strings). */
|
|
178
|
+
const MAX_FULLTAG_LENGTH_FOR_STYLE_SCAN = 10_000
|
|
179
|
+
/** Characters to scan for style=" or style=' when re-extracting from tag (covers open tag). */
|
|
180
|
+
const STYLE_VALUE_SCAN_CHARS = 1500
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Re-extract style attribute value from tag content when getAttr missed it.
|
|
184
|
+
* Scans the first maxScan chars for style="..." or style='...'.
|
|
185
|
+
*/
|
|
186
|
+
function extractStyleFromTagContent(fullTag: string, maxScan = STYLE_VALUE_SCAN_CHARS): string | undefined {
|
|
187
|
+
const scan = fullTag.slice(0, maxScan)
|
|
188
|
+
const styleDq = scan.toLowerCase().indexOf('style="')
|
|
189
|
+
const styleSq = scan.toLowerCase().indexOf('style=\'')
|
|
190
|
+
const useDq = styleDq !== -1 && (styleSq === -1 || styleDq <= styleSq)
|
|
191
|
+
const styleIdx = useDq ? styleDq : styleSq
|
|
192
|
+
const quote = styleIdx !== -1 ? (useDq ? '"' : '\'') : ''
|
|
193
|
+
if (styleIdx === -1 || !quote) return undefined
|
|
194
|
+
const valueStart = styleIdx + 7
|
|
195
|
+
const valueEnd = scan.indexOf(quote, valueStart)
|
|
196
|
+
return valueEnd !== -1 ? scan.slice(valueStart, valueEnd) : undefined
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** True when style or fullTag (lowercased) indicates actor/person shape. */
|
|
200
|
+
function styleOrTagIndicatesActor(style: string | undefined, fullTagLower: string): boolean {
|
|
201
|
+
const s = style?.toLowerCase() ?? ''
|
|
202
|
+
return (
|
|
203
|
+
s.includes('shape=actor') ||
|
|
204
|
+
s.includes('shape=person') ||
|
|
205
|
+
s.includes('umlactor') ||
|
|
206
|
+
fullTagLower.includes('shape=actor') ||
|
|
207
|
+
fullTagLower.includes('shape=person') ||
|
|
208
|
+
fullTagLower.includes('umlactor')
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
135
212
|
/** Find end of XML open tag (first unquoted '>'). Handles both single- and double-quoted attributes. Avoids regex for S5852. */
|
|
136
213
|
function findOpenTagEnd(xml: string, start: number): number {
|
|
137
214
|
let quoteChar = ''
|
|
@@ -338,6 +415,8 @@ function buildCellOptionalFields(params: {
|
|
|
338
415
|
const relationshipKind = getDecodedStyle(styleMap, 'likec4relationshipkind')
|
|
339
416
|
const notation = getDecodedStyle(styleMap, 'likec4notation')
|
|
340
417
|
const metadata = getDecodedStyle(styleMap, 'likec4metadata')
|
|
418
|
+
const likec4Id = getDecodedStyle(styleMap, 'likec4id')
|
|
419
|
+
const likec4RelationId = getDecodedStyle(styleMap, 'likec4relationid')
|
|
341
420
|
const optional: Partial<DrawioCell> = {}
|
|
342
421
|
if (params.valueRaw != null && params.valueRaw !== '') {
|
|
343
422
|
optional.value = decodeXmlEntities(params.valueRaw)
|
|
@@ -346,6 +425,10 @@ function buildCellOptionalFields(params: {
|
|
|
346
425
|
if (params.source != null && params.source !== '') optional.source = params.source
|
|
347
426
|
if (params.target != null && params.target !== '') optional.target = params.target
|
|
348
427
|
if (params.style != null && params.style !== '') optional.style = params.style
|
|
428
|
+
else if (params.styleMap.has('shape')) {
|
|
429
|
+
// Fallback: raw style string may be missing from attrs (e.g. parsing path); ensure kind/shape inference can run
|
|
430
|
+
optional.style = `shape=${params.styleMap.get('shape')};`
|
|
431
|
+
}
|
|
349
432
|
if (x !== undefined) optional.x = x
|
|
350
433
|
if (y !== undefined) optional.y = y
|
|
351
434
|
if (width !== undefined) optional.width = width
|
|
@@ -376,6 +459,16 @@ function buildCellOptionalFields(params: {
|
|
|
376
459
|
if (relationshipKind != null) optional.relationshipKind = relationshipKind
|
|
377
460
|
if (notation != null) optional.notation = notation
|
|
378
461
|
if (metadata != null && edge) optional.metadata = metadata
|
|
462
|
+
if (likec4Id != null && vertex) optional.likec4Id = likec4Id
|
|
463
|
+
if (likec4RelationId != null && edge) optional.likec4RelationId = likec4RelationId
|
|
464
|
+
let shapeFromStyle = params.styleMap.get('shape')?.trim()
|
|
465
|
+
const fullTagLower = params.fullTag.toLowerCase()
|
|
466
|
+
if (
|
|
467
|
+
(shapeFromStyle == null || shapeFromStyle === '') && vertex && styleOrTagIndicatesActor(params.style, fullTagLower)
|
|
468
|
+
) {
|
|
469
|
+
shapeFromStyle = 'actor'
|
|
470
|
+
}
|
|
471
|
+
if (shapeFromStyle != null && shapeFromStyle !== '' && vertex) optional.shapeFromStyle = shapeFromStyle
|
|
379
472
|
if (userData.customData != null) optional.customData = userData.customData
|
|
380
473
|
if (edge) {
|
|
381
474
|
const pts = parseEdgePoints(fullTag)
|
|
@@ -399,9 +492,19 @@ function buildCellFromMxCell(
|
|
|
399
492
|
if (!id) return null
|
|
400
493
|
const vertex = getAttr(attrs, 'vertex') === '1'
|
|
401
494
|
const edge = getAttr(attrs, 'edge') === '1'
|
|
402
|
-
|
|
495
|
+
let style = getAttr(attrs, 'style') ??
|
|
496
|
+
extractStyleFromAttrsFallback(attrs) ??
|
|
497
|
+
extractStyleFromOpenTag(fullTag)
|
|
498
|
+
const styleMissing = !style || style.trim() === ''
|
|
499
|
+
const tagHasActor = fullTag.length < MAX_FULLTAG_LENGTH_FOR_STYLE_SCAN &&
|
|
500
|
+
fullTag.toLowerCase().includes('shape=actor')
|
|
501
|
+
const needLastResort = styleMissing || (tagHasActor && !style?.toLowerCase().includes('shape=actor'))
|
|
502
|
+
if (needLastResort && fullTag.length < MAX_FULLTAG_LENGTH_FOR_STYLE_SCAN) {
|
|
503
|
+
const reExtracted = extractStyleFromTagContent(fullTag)
|
|
504
|
+
if (reExtracted != null) style = reExtracted
|
|
505
|
+
}
|
|
403
506
|
const geomStr = extractMxGeometryOpenTag(fullTag)
|
|
404
|
-
const styleMap = parseStyle(style
|
|
507
|
+
const styleMap = parseStyle(style?.trim() || undefined)
|
|
405
508
|
const userData = parseUserData(inner)
|
|
406
509
|
const navigateTo = overrides?.navigateTo ?? getDecodedStyle(styleMap, 'likec4navigateto')
|
|
407
510
|
const optional = buildCellOptionalFields({
|
|
@@ -409,7 +512,7 @@ function buildCellFromMxCell(
|
|
|
409
512
|
parent: getAttr(attrs, 'parent'),
|
|
410
513
|
source: getAttr(attrs, 'source'),
|
|
411
514
|
target: getAttr(attrs, 'target'),
|
|
412
|
-
style
|
|
515
|
+
style,
|
|
413
516
|
styleMap,
|
|
414
517
|
userData,
|
|
415
518
|
geomStr,
|
|
@@ -418,7 +521,7 @@ function buildCellFromMxCell(
|
|
|
418
521
|
edge,
|
|
419
522
|
navigateTo,
|
|
420
523
|
})
|
|
421
|
-
return { id, vertex, edge, ...optional } as DrawioCell
|
|
524
|
+
return { id, vertex, edge, ...(style != null && style !== '' ? { style } : {}), ...optional } as DrawioCell
|
|
422
525
|
}
|
|
423
526
|
|
|
424
527
|
/** Extract one mxCell from xml starting at tagStart. Returns attrs, inner, fullTag and next search index, or null. */
|
|
@@ -549,20 +652,29 @@ function likec4LineType(
|
|
|
549
652
|
return undefined
|
|
550
653
|
}
|
|
551
654
|
|
|
655
|
+
/** True when style or shapeFromStyle indicates actor/person (DrawIO shape=actor, shape=person, umlActor). */
|
|
656
|
+
function isActorShapeInStyle(style: string | undefined, shapeFromStyle?: string): boolean {
|
|
657
|
+
const s = style?.toLowerCase() ?? ''
|
|
658
|
+
const shape = shapeFromStyle?.toLowerCase().trim()
|
|
659
|
+
return shape === 'actor' || shape === 'person' || s.includes('shape=actor') || s.includes('shape=person') ||
|
|
660
|
+
s.includes('umlactor')
|
|
661
|
+
}
|
|
662
|
+
|
|
552
663
|
/**
|
|
553
664
|
* Infer LikeC4 element kind from DrawIO shape style. When parent is a container (container=1), child is component.
|
|
554
665
|
* Explicit container=1 in style → system (context box); others default to container unless actor/swimlane.
|
|
666
|
+
* Uses shapeFromStyle when raw style is missing so actor/person is still inferred.
|
|
555
667
|
*/
|
|
556
668
|
function inferKind(
|
|
557
669
|
style: string | undefined,
|
|
558
670
|
parentCell?: DrawioCell,
|
|
671
|
+
shapeFromStyle?: string,
|
|
559
672
|
): 'actor' | 'system' | 'container' | 'component' {
|
|
560
673
|
const s = style?.toLowerCase() ?? ''
|
|
674
|
+
if (isActorShapeInStyle(style, shapeFromStyle)) return 'actor'
|
|
561
675
|
switch (true) {
|
|
562
|
-
case !style:
|
|
676
|
+
case !style && !shapeFromStyle:
|
|
563
677
|
return parentCell?.style?.toLowerCase().includes('container=1') ? 'component' : 'container'
|
|
564
|
-
case s.includes('umlactor') || s.includes('shape=person') || s.includes('shape=actor'):
|
|
565
|
-
return 'actor'
|
|
566
678
|
case s.includes('swimlane'):
|
|
567
679
|
case s.includes('container=1'):
|
|
568
680
|
return 'system'
|
|
@@ -573,12 +685,10 @@ function inferKind(
|
|
|
573
685
|
}
|
|
574
686
|
}
|
|
575
687
|
|
|
576
|
-
/** Infer LikeC4 shape from DrawIO style when possible (person, cylinder, document, etc.). */
|
|
577
|
-
function inferShape(style: string | undefined): string | undefined {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
// Actor/person shape (export may use shape=actor or shape=umlActor; legacy may have shape=person)
|
|
581
|
-
if (s.includes('shape=actor') || s.includes('shape=person') || s.includes('umlactor')) return 'person'
|
|
688
|
+
/** Infer LikeC4 shape from DrawIO style (or shapeFromStyle) when possible (person, cylinder, document, etc.). */
|
|
689
|
+
function inferShape(style: string | undefined, shapeFromStyle?: string): string | undefined {
|
|
690
|
+
const s = style?.toLowerCase() ?? ''
|
|
691
|
+
if (isActorShapeInStyle(style, shapeFromStyle)) return 'person'
|
|
582
692
|
if (s.includes('shape=cylinder') || s.includes('cylinder3')) return 'cylinder'
|
|
583
693
|
if (s.includes('shape=document')) return 'document'
|
|
584
694
|
if (s.includes('shape=rectangle') && s.includes('rounded')) return 'rectangle'
|
|
@@ -610,16 +720,60 @@ function makeUniqueName(usedNames: Set<string>): (base: string) => string {
|
|
|
610
720
|
}
|
|
611
721
|
}
|
|
612
722
|
|
|
613
|
-
/**
|
|
723
|
+
/** True when s is a syntactically valid dot-separated FQN (each segment non-empty, identifier-like). */
|
|
724
|
+
function isValidFqn(s: string): boolean {
|
|
725
|
+
if (s.length === 0) return false
|
|
726
|
+
const segments = s.split('.')
|
|
727
|
+
return segments.every(seg => /^[a-zA-Z0-9_-]+$/.test(seg))
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** Depth of vertex from root (0 = root or parent not in diagram). Cycle-safe: cycles in parent graph return 0. */
|
|
731
|
+
function vertexDepth(
|
|
732
|
+
v: DrawioCell,
|
|
733
|
+
idToVertex: Map<string, DrawioCell>,
|
|
734
|
+
visited: Set<string> = new Set(),
|
|
735
|
+
): number {
|
|
736
|
+
if (visited.has(v.id)) return 0
|
|
737
|
+
visited.add(v.id)
|
|
738
|
+
if (v.parent == null || !idToVertex.has(v.parent)) return 0
|
|
739
|
+
return 1 + vertexDepth(idToVertex.get(v.parent)!, idToVertex, visited)
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** True when bridgeId is valid FQN and matches parent chain (root or prefix). */
|
|
743
|
+
function isUsableBridgeId(
|
|
744
|
+
bridgeId: string,
|
|
745
|
+
v: DrawioCell,
|
|
746
|
+
idToFqn: Map<string, string>,
|
|
747
|
+
_idToVertex: Map<string, DrawioCell>,
|
|
748
|
+
isRootParent: (parent: string | undefined) => boolean,
|
|
749
|
+
): boolean {
|
|
750
|
+
if (!isValidFqn(bridgeId)) return false
|
|
751
|
+
const parentFqn = v.parent ? idToFqn.get(v.parent) : undefined
|
|
752
|
+
if (parentFqn === undefined) return isRootParent(v.parent)
|
|
753
|
+
return bridgeId.startsWith(parentFqn + '.') && bridgeId.length > parentFqn.length + 1
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Assign FQNs to element vertices: bridge-managed likec4Id first (when valid), then root, hierarchy, orphans (DRY). */
|
|
614
757
|
function assignFqnsToElementVertices(
|
|
615
758
|
idToFqn: Map<string, string>,
|
|
616
759
|
elementVertices: DrawioCell[],
|
|
617
760
|
containerIdToTitle: Map<string, string>,
|
|
618
761
|
isRootParent: (parent: string | undefined) => boolean,
|
|
619
762
|
uniqueName: (base: string) => string,
|
|
763
|
+
usedNames: Set<string>,
|
|
620
764
|
): void {
|
|
621
765
|
const baseName = (v: DrawioCell) => v.value ?? containerIdToTitle.get(v.id) ?? v.id
|
|
766
|
+
const idToVertex = new Map(elementVertices.map(v => [v.id, v]))
|
|
767
|
+
const byDepth = [...elementVertices].sort((a, b) => vertexDepth(a, idToVertex) - vertexDepth(b, idToVertex))
|
|
768
|
+
for (const v of byDepth) {
|
|
769
|
+
const bridgeId = v.likec4Id?.trim()
|
|
770
|
+
if (bridgeId && isUsableBridgeId(bridgeId, v, idToFqn, idToVertex, isRootParent)) {
|
|
771
|
+
idToFqn.set(v.id, bridgeId)
|
|
772
|
+
for (const segment of bridgeId.split('.')) usedNames.add(segment)
|
|
773
|
+
}
|
|
774
|
+
}
|
|
622
775
|
for (const v of elementVertices) {
|
|
776
|
+
if (idToFqn.has(v.id)) continue
|
|
623
777
|
if (isRootParent(v.parent)) idToFqn.set(v.id, uniqueName(baseName(v)))
|
|
624
778
|
}
|
|
625
779
|
let changed = true
|
|
@@ -804,7 +958,7 @@ function pushElementStyleBlock(
|
|
|
804
958
|
): void {
|
|
805
959
|
const border = cell.border?.trim()
|
|
806
960
|
const opacityVal = cell.opacity
|
|
807
|
-
const shapeOverride = inferShape(cell.style)
|
|
961
|
+
const shapeOverride = inferShape(cell.style, cell.shapeFromStyle)
|
|
808
962
|
const sizeVal = cell.size?.trim()
|
|
809
963
|
const paddingVal = cell.padding?.trim()
|
|
810
964
|
const textSizeVal = cell.textSize?.trim()
|
|
@@ -918,7 +1072,7 @@ function elementHasBody(
|
|
|
918
1072
|
!!colorName ||
|
|
919
1073
|
!!cell.border?.trim() ||
|
|
920
1074
|
!!cell.opacity ||
|
|
921
|
-
!!inferShape(cell.style) ||
|
|
1075
|
+
!!inferShape(cell.style, cell.shapeFromStyle) ||
|
|
922
1076
|
!!cell.size ||
|
|
923
1077
|
!!cell.padding ||
|
|
924
1078
|
!!cell.textSize ||
|
|
@@ -974,7 +1128,7 @@ function emitElementToLines(ctx: ElementEmitContext, cellId: string, fqn: string
|
|
|
974
1128
|
const cell = ctx.idToCell.get(cellId)
|
|
975
1129
|
if (!cell) return
|
|
976
1130
|
const parentCell = cell.parent ? ctx.byId.get(cell.parent) : undefined
|
|
977
|
-
const kind = inferKind(cell.style, parentCell)
|
|
1131
|
+
const kind = inferKind(cell.style, parentCell, cell.shapeFromStyle)
|
|
978
1132
|
const rawTitle = (cell.value && cell.value.trim()) || ''
|
|
979
1133
|
const title = stripHtml(rawTitle) ||
|
|
980
1134
|
(ctx.containerIdToTitle.get(cell.id) ?? ctx.containerIdToTitle.get(cellId) ?? '') ||
|
|
@@ -1363,7 +1517,7 @@ function buildCommonDiagramStateFromCells(
|
|
|
1363
1517
|
const elementVertices = vertices.filter(v => !titleCellIds.has(v.id))
|
|
1364
1518
|
const usedNames = new Set<string>()
|
|
1365
1519
|
const uniqueName = makeUniqueName(usedNames)
|
|
1366
|
-
assignFqnsToElementVertices(idToFqn, elementVertices, containerIdToTitle, isRootParent, uniqueName)
|
|
1520
|
+
assignFqnsToElementVertices(idToFqn, elementVertices, containerIdToTitle, isRootParent, uniqueName, usedNames)
|
|
1367
1521
|
const hexToCustomName = buildHexToCustomName(elementVertices, edges)
|
|
1368
1522
|
const children = new Map<string, Array<{ cellId: string; fqn: string }>>()
|
|
1369
1523
|
const roots: Array<{ cellId: string; fqn: string }> = []
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { DEFAULT_DRAWIO_ALL_FILENAME } from './drawio/constants'
|
|
|
3
3
|
export {
|
|
4
4
|
buildDrawioExportOptionsForViews,
|
|
5
5
|
buildDrawioExportOptionsFromSource,
|
|
6
|
+
type DrawioExportProfile,
|
|
6
7
|
type DrawioViewModelLike,
|
|
7
8
|
generateDrawio,
|
|
8
9
|
generateDrawioEditUrl,
|
|
@@ -10,6 +11,7 @@ export {
|
|
|
10
11
|
type GenerateDrawioOptions,
|
|
11
12
|
} from './drawio/generate-drawio'
|
|
12
13
|
export {
|
|
14
|
+
decompressDrawioDiagram,
|
|
13
15
|
getAllDiagrams,
|
|
14
16
|
parseDrawioRoundtripComments,
|
|
15
17
|
parseDrawioToLikeC4,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as operators from './operators'
|
|
2
|
+
import { type AnyOp, type ctxOf, materialize, withctx } from './operators/base'
|
|
3
|
+
import { schemas } from './schemas'
|
|
4
|
+
|
|
5
|
+
type Params = {
|
|
6
|
+
indentation?: string | number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function generateLikeC4(input: schemas.likec4data.Input, params?: Params): string {
|
|
10
|
+
params = {
|
|
11
|
+
indentation: 2,
|
|
12
|
+
...params,
|
|
13
|
+
}
|
|
14
|
+
return materialize(withctx(input, operators.likec4data()), params.indentation)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Prints the result of an operation with the data
|
|
19
|
+
*
|
|
20
|
+
* @see operators
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* print(operators.expression, {
|
|
25
|
+
* ref: {
|
|
26
|
+
* model: 'some.el',
|
|
27
|
+
* },
|
|
28
|
+
* selector: 'descendants',
|
|
29
|
+
* })
|
|
30
|
+
* // "some.el.**"
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* print(operators.model, {
|
|
36
|
+
* elements: [
|
|
37
|
+
* {
|
|
38
|
+
* id: 'cloud',
|
|
39
|
+
* kind: 'system',
|
|
40
|
+
* },
|
|
41
|
+
* {
|
|
42
|
+
* id: 'cloud.mobile',
|
|
43
|
+
* kind: 'mobileapp',
|
|
44
|
+
* shape: 'mobile',
|
|
45
|
+
* color: 'amber',
|
|
46
|
+
* }
|
|
47
|
+
* ],
|
|
48
|
+
* })
|
|
49
|
+
* // model {
|
|
50
|
+
* // cloud = system {
|
|
51
|
+
* // mobile = mobileapp {
|
|
52
|
+
* // style {
|
|
53
|
+
* // shape mobile
|
|
54
|
+
* // color amber
|
|
55
|
+
* // }
|
|
56
|
+
* // }
|
|
57
|
+
* // }
|
|
58
|
+
* // }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function print<O extends () => AnyOp>(operator: O, data: ctxOf<O>, params?: Params): string {
|
|
62
|
+
return materialize(withctx(data, operator()), params?.indentation)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Same as {@link print} but uses tab indentation
|
|
67
|
+
*/
|
|
68
|
+
export function printTabIndent<O extends () => AnyOp>(operator: O, data: ctxOf<O>): string {
|
|
69
|
+
return materialize(withctx(data, operator()), '\t')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { operators }
|