@oh-my-pi/pi-utils 16.0.7 → 16.0.8
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/CHANGELOG.md +10 -0
- package/dist/types/mermaid-ascii.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +41 -0
- package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +89 -0
- package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +12 -0
- package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +66 -0
- package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +48 -0
- package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +43 -0
- package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +56 -0
- package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +65 -0
- package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +27 -0
- package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +17 -0
- package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +34 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +26 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +31 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +59 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +17 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +30 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +55 -0
- package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +206 -0
- package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +51 -0
- package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +2 -0
- package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/class/types.d.ts +102 -0
- package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/er/types.d.ts +76 -0
- package/dist/types/vendor/mermaid-ascii/index.d.ts +1 -0
- package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +9 -0
- package/dist/types/vendor/mermaid-ascii/parser.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +130 -0
- package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +21 -0
- package/dist/types/vendor/mermaid-ascii/types.d.ts +114 -0
- package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +25 -0
- package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +145 -0
- package/package.json +2 -3
- package/src/mermaid-ascii.ts +1 -1
- package/src/vendor/mermaid-ascii/NOTICE +33 -0
- package/src/vendor/mermaid-ascii/ascii/ansi.ts +409 -0
- package/src/vendor/mermaid-ascii/ascii/canvas.ts +476 -0
- package/src/vendor/mermaid-ascii/ascii/class-diagram.ts +699 -0
- package/src/vendor/mermaid-ascii/ascii/converter.ts +271 -0
- package/src/vendor/mermaid-ascii/ascii/draw.ts +1382 -0
- package/src/vendor/mermaid-ascii/ascii/edge-bundling.ts +328 -0
- package/src/vendor/mermaid-ascii/ascii/edge-routing.ts +297 -0
- package/src/vendor/mermaid-ascii/ascii/er-diagram.ts +441 -0
- package/src/vendor/mermaid-ascii/ascii/grid.ts +578 -0
- package/src/vendor/mermaid-ascii/ascii/index.ts +187 -0
- package/src/vendor/mermaid-ascii/ascii/multiline-utils.ts +78 -0
- package/src/vendor/mermaid-ascii/ascii/pathfinder.ts +215 -0
- package/src/vendor/mermaid-ascii/ascii/sequence.ts +460 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/circle.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/corners.ts +127 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/diamond.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/hexagon.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/index.ts +101 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/rectangle.ts +175 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/rounded.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/special.ts +296 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/stadium.ts +114 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/state.ts +192 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/types.ts +73 -0
- package/src/vendor/mermaid-ascii/ascii/types.ts +273 -0
- package/src/vendor/mermaid-ascii/ascii/validate.ts +120 -0
- package/src/vendor/mermaid-ascii/ascii/xychart.ts +875 -0
- package/src/vendor/mermaid-ascii/class/parser.ts +290 -0
- package/src/vendor/mermaid-ascii/class/types.ts +121 -0
- package/src/vendor/mermaid-ascii/er/parser.ts +181 -0
- package/src/vendor/mermaid-ascii/er/types.ts +91 -0
- package/src/vendor/mermaid-ascii/index.ts +14 -0
- package/src/vendor/mermaid-ascii/multiline-utils.ts +30 -0
- package/src/vendor/mermaid-ascii/parser.ts +645 -0
- package/src/vendor/mermaid-ascii/sequence/parser.ts +207 -0
- package/src/vendor/mermaid-ascii/sequence/types.ts +146 -0
- package/src/vendor/mermaid-ascii/text-metrics.ts +71 -0
- package/src/vendor/mermaid-ascii/types.ts +164 -0
- package/src/vendor/mermaid-ascii/xychart/colors.ts +140 -0
- package/src/vendor/mermaid-ascii/xychart/parser.ts +115 -0
- package/src/vendor/mermaid-ascii/xychart/types.ts +150 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type { ClassDiagram, ClassNode, ClassRelationship, ClassMember, RelationshipType, ClassNamespace } from './types'
|
|
2
|
+
import { normalizeBrTags } from '../multiline-utils'
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Class diagram parser
|
|
6
|
+
//
|
|
7
|
+
// Parses Mermaid classDiagram syntax into a ClassDiagram structure.
|
|
8
|
+
//
|
|
9
|
+
// Supported syntax:
|
|
10
|
+
// class Animal { +String name; +eat() void }
|
|
11
|
+
// class Shape { <<abstract>> }
|
|
12
|
+
// Animal <|-- Dog (inheritance)
|
|
13
|
+
// Car *-- Engine (composition)
|
|
14
|
+
// Car o-- Wheel (aggregation)
|
|
15
|
+
// A --> B (association)
|
|
16
|
+
// A ..> B (dependency)
|
|
17
|
+
// A ..|> B (realization)
|
|
18
|
+
// A "1" --> "*" B : label (with cardinality + label)
|
|
19
|
+
// Animal : +String name (inline attribute)
|
|
20
|
+
// namespace MyNamespace { class A { } }
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a Mermaid class diagram.
|
|
25
|
+
* Expects the first line to be "classDiagram".
|
|
26
|
+
*/
|
|
27
|
+
export function parseClassDiagram(lines: string[]): ClassDiagram {
|
|
28
|
+
const diagram: ClassDiagram = {
|
|
29
|
+
classes: [],
|
|
30
|
+
relationships: [],
|
|
31
|
+
namespaces: [],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Track classes by ID for deduplication
|
|
35
|
+
const classMap = new Map<string, ClassNode>()
|
|
36
|
+
// Track namespace nesting
|
|
37
|
+
let currentNamespace: ClassNamespace | null = null
|
|
38
|
+
// Track class body parsing
|
|
39
|
+
let currentClass: ClassNode | null = null
|
|
40
|
+
let braceDepth = 0
|
|
41
|
+
|
|
42
|
+
for (let i = 1; i < lines.length; i++) {
|
|
43
|
+
const line = lines[i]!
|
|
44
|
+
|
|
45
|
+
// --- Inside a class body block ---
|
|
46
|
+
if (currentClass && braceDepth > 0) {
|
|
47
|
+
if (line === '}') {
|
|
48
|
+
braceDepth--
|
|
49
|
+
if (braceDepth === 0) {
|
|
50
|
+
currentClass = null
|
|
51
|
+
}
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check for annotation like <<interface>>
|
|
56
|
+
const annotMatch = line.match(/^<<(\w+)>>$/)
|
|
57
|
+
if (annotMatch) {
|
|
58
|
+
currentClass.annotation = annotMatch[1]!
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Parse member: visibility, name, type, optional parens for method
|
|
63
|
+
const member = parseMember(line)
|
|
64
|
+
if (member) {
|
|
65
|
+
if (member.isMethod) {
|
|
66
|
+
currentClass.methods.push(member.member)
|
|
67
|
+
} else {
|
|
68
|
+
currentClass.attributes.push(member.member)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Namespace block start ---
|
|
75
|
+
const nsMatch = line.match(/^namespace\s+(\S+)\s*\{$/)
|
|
76
|
+
if (nsMatch) {
|
|
77
|
+
currentNamespace = { name: nsMatch[1]!, classIds: [] }
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- Namespace end ---
|
|
82
|
+
if (line === '}' && currentNamespace) {
|
|
83
|
+
diagram.namespaces.push(currentNamespace)
|
|
84
|
+
currentNamespace = null
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Class block start: `class ClassName {` or `class ClassName` ---
|
|
89
|
+
const classBlockMatch = line.match(/^class\s+(\S+?)(?:\s*~(\w+)~)?\s*\{$/)
|
|
90
|
+
if (classBlockMatch) {
|
|
91
|
+
const id = classBlockMatch[1]!
|
|
92
|
+
const generic = classBlockMatch[2]
|
|
93
|
+
const cls = ensureClass(classMap, id)
|
|
94
|
+
if (generic) {
|
|
95
|
+
cls.label = `${id}<${generic}>`
|
|
96
|
+
}
|
|
97
|
+
currentClass = cls
|
|
98
|
+
braceDepth = 1
|
|
99
|
+
if (currentNamespace) {
|
|
100
|
+
currentNamespace.classIds.push(id)
|
|
101
|
+
}
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Standalone class declaration (no body): `class ClassName` ---
|
|
106
|
+
const classOnlyMatch = line.match(/^class\s+(\S+?)(?:\s*~(\w+)~)?\s*$/)
|
|
107
|
+
if (classOnlyMatch) {
|
|
108
|
+
const id = classOnlyMatch[1]!
|
|
109
|
+
const generic = classOnlyMatch[2]
|
|
110
|
+
const cls = ensureClass(classMap, id)
|
|
111
|
+
if (generic) {
|
|
112
|
+
cls.label = `${id}<${generic}>`
|
|
113
|
+
}
|
|
114
|
+
if (currentNamespace) {
|
|
115
|
+
currentNamespace.classIds.push(id)
|
|
116
|
+
}
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- Inline annotation: `class ClassName { <<interface>> }` (single line) ---
|
|
121
|
+
const inlineAnnotMatch = line.match(/^class\s+(\S+?)\s*\{\s*<<(\w+)>>\s*\}$/)
|
|
122
|
+
if (inlineAnnotMatch) {
|
|
123
|
+
const cls = ensureClass(classMap, inlineAnnotMatch[1]!)
|
|
124
|
+
cls.annotation = inlineAnnotMatch[2]!
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- Inline attribute: `ClassName : +String name` ---
|
|
129
|
+
const inlineAttrMatch = line.match(/^(\S+?)\s*:\s*(.+)$/)
|
|
130
|
+
if (inlineAttrMatch) {
|
|
131
|
+
// Make sure this isn't a relationship line (those have arrows)
|
|
132
|
+
const rest = inlineAttrMatch[2]!
|
|
133
|
+
if (!rest.match(/<\|--|--|\*--|o--|-->|\.\.>|\.\.\|>/)) {
|
|
134
|
+
const cls = ensureClass(classMap, inlineAttrMatch[1]!)
|
|
135
|
+
const member = parseMember(rest)
|
|
136
|
+
if (member) {
|
|
137
|
+
if (member.isMethod) {
|
|
138
|
+
cls.methods.push(member.member)
|
|
139
|
+
} else {
|
|
140
|
+
cls.attributes.push(member.member)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- Relationship ---
|
|
148
|
+
// Pattern: [FROM] ["card"] ARROW ["card"] [TO] [: label]
|
|
149
|
+
// Arrows: <|--, *--, o--, -->, ..|>, ..>
|
|
150
|
+
// Can also be reversed: --o, --*, --|>
|
|
151
|
+
const rel = parseRelationship(line)
|
|
152
|
+
if (rel) {
|
|
153
|
+
// Ensure both classes exist
|
|
154
|
+
ensureClass(classMap, rel.from)
|
|
155
|
+
ensureClass(classMap, rel.to)
|
|
156
|
+
diagram.relationships.push(rel)
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
diagram.classes = [...classMap.values()]
|
|
162
|
+
return diagram
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Ensure a class exists in the map, creating a default if needed */
|
|
166
|
+
function ensureClass(classMap: Map<string, ClassNode>, id: string): ClassNode {
|
|
167
|
+
let cls = classMap.get(id)
|
|
168
|
+
if (!cls) {
|
|
169
|
+
cls = { id, label: id, attributes: [], methods: [] }
|
|
170
|
+
classMap.set(id, cls)
|
|
171
|
+
}
|
|
172
|
+
return cls
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Parse a class member line (attribute or method) */
|
|
176
|
+
function parseMember(line: string): { member: ClassMember; isMethod: boolean } | null {
|
|
177
|
+
const trimmed = line.trim().replace(/;$/, '')
|
|
178
|
+
if (!trimmed) return null
|
|
179
|
+
|
|
180
|
+
// Extract visibility prefix
|
|
181
|
+
let visibility: ClassMember['visibility'] = ''
|
|
182
|
+
let rest = trimmed
|
|
183
|
+
if (/^[+\-#~]/.test(rest)) {
|
|
184
|
+
visibility = rest[0] as ClassMember['visibility']
|
|
185
|
+
rest = rest.slice(1).trim()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check if it's a method (has parentheses)
|
|
189
|
+
const methodMatch = rest.match(/^(.+?)\(([^)]*)\)(?:\s*(.+))?$/)
|
|
190
|
+
if (methodMatch) {
|
|
191
|
+
const name = methodMatch[1]!.trim()
|
|
192
|
+
const params = methodMatch[2]?.trim() || undefined // Store the parameter string
|
|
193
|
+
const type = methodMatch[3]?.trim()
|
|
194
|
+
// Check for static ($) or abstract (*) markers
|
|
195
|
+
const isStatic = name.endsWith('$') || rest.includes('$')
|
|
196
|
+
const isAbstract = name.endsWith('*') || rest.includes('*')
|
|
197
|
+
return {
|
|
198
|
+
member: {
|
|
199
|
+
visibility,
|
|
200
|
+
name: name.replace(/[$*]$/, ''),
|
|
201
|
+
type: type || undefined,
|
|
202
|
+
isStatic,
|
|
203
|
+
isAbstract,
|
|
204
|
+
isMethod: true,
|
|
205
|
+
params,
|
|
206
|
+
},
|
|
207
|
+
isMethod: true,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// It's an attribute: [Type] name or name Type
|
|
212
|
+
// Common patterns: "String name", "+int age", "name"
|
|
213
|
+
const parts = rest.split(/\s+/)
|
|
214
|
+
let name: string
|
|
215
|
+
let type: string | undefined
|
|
216
|
+
|
|
217
|
+
if (parts.length >= 2) {
|
|
218
|
+
// "Type name" pattern
|
|
219
|
+
type = parts[0]
|
|
220
|
+
name = parts.slice(1).join(' ')
|
|
221
|
+
} else {
|
|
222
|
+
name = parts[0] ?? rest
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const isStatic = name.endsWith('$')
|
|
226
|
+
const isAbstract = name.endsWith('*')
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
member: {
|
|
230
|
+
visibility,
|
|
231
|
+
name: name.replace(/[$*]$/, ''),
|
|
232
|
+
type: type || undefined,
|
|
233
|
+
isStatic,
|
|
234
|
+
isAbstract,
|
|
235
|
+
isMethod: false,
|
|
236
|
+
},
|
|
237
|
+
isMethod: false,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Parse a relationship line into a ClassRelationship */
|
|
242
|
+
function parseRelationship(line: string): ClassRelationship | null {
|
|
243
|
+
// Relationship regex — handles all arrow types with optional cardinality and labels
|
|
244
|
+
// Pattern: FROM ["card"] ARROW ["card"] TO [: label]
|
|
245
|
+
const match = line.match(
|
|
246
|
+
/^(\S+?)\s+(?:"([^"]*?)"\s+)?(<\|--|<\|\.\.|\*--|o--|-->|--\*|--o|--\|>|\.\.>|\.\.\|>|<--|<\.\.?|--)\s+(?:"([^"]*?)"\s+)?(\S+?)(?:\s*:\s*(.+))?$/
|
|
247
|
+
)
|
|
248
|
+
if (!match) return null
|
|
249
|
+
|
|
250
|
+
const from = match[1]!
|
|
251
|
+
const rawFromCardinality = match[2]
|
|
252
|
+
const fromCardinality = rawFromCardinality ? normalizeBrTags(rawFromCardinality) : undefined
|
|
253
|
+
const arrow = match[3]!.trim()
|
|
254
|
+
const rawToCardinality = match[4]
|
|
255
|
+
const toCardinality = rawToCardinality ? normalizeBrTags(rawToCardinality) : undefined
|
|
256
|
+
const to = match[5]!
|
|
257
|
+
const rawLabel = match[6]?.trim()
|
|
258
|
+
const label = rawLabel ? normalizeBrTags(rawLabel) : undefined
|
|
259
|
+
|
|
260
|
+
const parsed = parseArrow(arrow)
|
|
261
|
+
if (!parsed) return null
|
|
262
|
+
|
|
263
|
+
return { from, to, type: parsed.type, markerAt: parsed.markerAt, label, fromCardinality, toCardinality }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Map arrow syntax to relationship type and marker placement side.
|
|
268
|
+
* Prefix markers (`<|--`, `*--`, `o--`) place the UML shape at the 'from' end.
|
|
269
|
+
* Suffix markers (`..|>`, `-->`, `..>`, `--*`, `--o`) place it at the 'to' end.
|
|
270
|
+
*/
|
|
271
|
+
function parseArrow(arrow: string): { type: RelationshipType; markerAt: 'from' | 'to' } | null {
|
|
272
|
+
// Trim whitespace that might be captured by the regex
|
|
273
|
+
const a = arrow.trim()
|
|
274
|
+
switch (a) {
|
|
275
|
+
case '<|--': return { type: 'inheritance', markerAt: 'from' }
|
|
276
|
+
case '--|>': return { type: 'inheritance', markerAt: 'to' }
|
|
277
|
+
case '<|..': return { type: 'realization', markerAt: 'from' }
|
|
278
|
+
case '..|>': return { type: 'realization', markerAt: 'to' }
|
|
279
|
+
case '*--': return { type: 'composition', markerAt: 'from' }
|
|
280
|
+
case '--*': return { type: 'composition', markerAt: 'to' }
|
|
281
|
+
case 'o--': return { type: 'aggregation', markerAt: 'from' }
|
|
282
|
+
case '--o': return { type: 'aggregation', markerAt: 'to' }
|
|
283
|
+
case '-->': return { type: 'association', markerAt: 'to' }
|
|
284
|
+
case '<--': return { type: 'association', markerAt: 'from' }
|
|
285
|
+
case '..>': return { type: 'dependency', markerAt: 'to' }
|
|
286
|
+
case '<..': return { type: 'dependency', markerAt: 'from' }
|
|
287
|
+
case '--': return { type: 'association', markerAt: 'to' }
|
|
288
|
+
default: return null
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Class diagram types
|
|
3
|
+
//
|
|
4
|
+
// Models the parsed and positioned representations of a Mermaid class diagram.
|
|
5
|
+
// Class diagrams show UML class relationships, inheritance, composition, etc.
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
/** Parsed class diagram — logical structure from mermaid text */
|
|
9
|
+
export interface ClassDiagram {
|
|
10
|
+
/** All class definitions */
|
|
11
|
+
classes: ClassNode[]
|
|
12
|
+
/** Relationships between classes */
|
|
13
|
+
relationships: ClassRelationship[]
|
|
14
|
+
/** Optional namespace groupings */
|
|
15
|
+
namespaces: ClassNamespace[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ClassNode {
|
|
19
|
+
id: string
|
|
20
|
+
label: string
|
|
21
|
+
/** Annotation like <<interface>>, <<abstract>>, <<service>>, <<enumeration>> */
|
|
22
|
+
annotation?: string
|
|
23
|
+
/** Class attributes (fields/properties) */
|
|
24
|
+
attributes: ClassMember[]
|
|
25
|
+
/** Class methods (functions) */
|
|
26
|
+
methods: ClassMember[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ClassMember {
|
|
30
|
+
/** Visibility: + public, - private, # protected, ~ package */
|
|
31
|
+
visibility: '+' | '-' | '#' | '~' | ''
|
|
32
|
+
/** Member name */
|
|
33
|
+
name: string
|
|
34
|
+
/** Type annotation (e.g., "String", "int", "void") */
|
|
35
|
+
type?: string
|
|
36
|
+
/** Whether the member is static (underlined in UML) */
|
|
37
|
+
isStatic?: boolean
|
|
38
|
+
/** Whether the member is abstract (italic in UML) */
|
|
39
|
+
isAbstract?: boolean
|
|
40
|
+
/** Whether the member is a method (renders with parentheses) */
|
|
41
|
+
isMethod?: boolean
|
|
42
|
+
/** Method parameters (e.g., "data", "key, val") — only for methods */
|
|
43
|
+
params?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Relationship types following UML conventions */
|
|
47
|
+
export type RelationshipType =
|
|
48
|
+
| 'inheritance' // A <|-- B (solid line, hollow triangle)
|
|
49
|
+
| 'composition' // A *-- B (solid line, filled diamond)
|
|
50
|
+
| 'aggregation' // A o-- B (solid line, hollow diamond)
|
|
51
|
+
| 'association' // A --> B (solid line, open arrow)
|
|
52
|
+
| 'dependency' // A ..> B (dashed line, open arrow)
|
|
53
|
+
| 'realization' // A ..|> B (dashed line, hollow triangle)
|
|
54
|
+
|
|
55
|
+
export interface ClassRelationship {
|
|
56
|
+
from: string
|
|
57
|
+
to: string
|
|
58
|
+
type: RelationshipType
|
|
59
|
+
/**
|
|
60
|
+
* Which end of the relationship line has the UML marker (triangle, diamond, arrow).
|
|
61
|
+
* Determined by the arrow syntax direction:
|
|
62
|
+
* - Prefix markers like `<|--`, `*--`, `o--` → 'from' (marker on left/from side)
|
|
63
|
+
* - Suffix markers like `..|>`, `-->`, `..>`, `--*`, `--o` → 'to' (marker on right/to side)
|
|
64
|
+
*/
|
|
65
|
+
markerAt: 'from' | 'to'
|
|
66
|
+
/** Label on the relationship line */
|
|
67
|
+
label?: string
|
|
68
|
+
/** Cardinality at the "from" end (e.g., "1", "*", "0..1") */
|
|
69
|
+
fromCardinality?: string
|
|
70
|
+
/** Cardinality at the "to" end */
|
|
71
|
+
toCardinality?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ClassNamespace {
|
|
75
|
+
name: string
|
|
76
|
+
classIds: string[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Positioned class diagram — ready for SVG rendering
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
export interface PositionedClassDiagram {
|
|
84
|
+
width: number
|
|
85
|
+
height: number
|
|
86
|
+
classes: PositionedClassNode[]
|
|
87
|
+
relationships: PositionedClassRelationship[]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface PositionedClassNode {
|
|
91
|
+
id: string
|
|
92
|
+
label: string
|
|
93
|
+
annotation?: string
|
|
94
|
+
attributes: ClassMember[]
|
|
95
|
+
methods: ClassMember[]
|
|
96
|
+
x: number
|
|
97
|
+
y: number
|
|
98
|
+
width: number
|
|
99
|
+
height: number
|
|
100
|
+
/** Height of the header section (name + annotation) */
|
|
101
|
+
headerHeight: number
|
|
102
|
+
/** Height of the attributes section */
|
|
103
|
+
attrHeight: number
|
|
104
|
+
/** Height of the methods section */
|
|
105
|
+
methodHeight: number
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface PositionedClassRelationship {
|
|
109
|
+
from: string
|
|
110
|
+
to: string
|
|
111
|
+
type: RelationshipType
|
|
112
|
+
/** Which end of the line has the UML marker — propagated from ClassRelationship */
|
|
113
|
+
markerAt: 'from' | 'to'
|
|
114
|
+
label?: string
|
|
115
|
+
fromCardinality?: string
|
|
116
|
+
toCardinality?: string
|
|
117
|
+
/** Path points from source to target */
|
|
118
|
+
points: Array<{ x: number; y: number }>
|
|
119
|
+
/** Dagre-computed label center position (avoids overlaps between nearby edges) */
|
|
120
|
+
labelPosition?: { x: number; y: number }
|
|
121
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { ErDiagram, ErEntity, ErAttribute, ErRelationship, Cardinality } from './types'
|
|
2
|
+
import { normalizeBrTags } from '../multiline-utils'
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// ER diagram parser
|
|
6
|
+
//
|
|
7
|
+
// Parses Mermaid erDiagram syntax into an ErDiagram structure.
|
|
8
|
+
//
|
|
9
|
+
// Supported syntax:
|
|
10
|
+
// CUSTOMER ||--o{ ORDER : places
|
|
11
|
+
// CUSTOMER {
|
|
12
|
+
// string name PK
|
|
13
|
+
// int age
|
|
14
|
+
// string email UK "user email"
|
|
15
|
+
// }
|
|
16
|
+
//
|
|
17
|
+
// Cardinality notation:
|
|
18
|
+
// || exactly one
|
|
19
|
+
// o| zero or one (also |o)
|
|
20
|
+
// }| one or more (also |{)
|
|
21
|
+
// o{ zero or more (also {o)
|
|
22
|
+
//
|
|
23
|
+
// Line style:
|
|
24
|
+
// -- identifying (solid line)
|
|
25
|
+
// .. non-identifying (dashed line)
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse a Mermaid ER diagram.
|
|
30
|
+
* Expects the first line to be "erDiagram".
|
|
31
|
+
*/
|
|
32
|
+
export function parseErDiagram(lines: string[]): ErDiagram {
|
|
33
|
+
const diagram: ErDiagram = {
|
|
34
|
+
entities: [],
|
|
35
|
+
relationships: [],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Track entities by ID for deduplication
|
|
39
|
+
const entityMap = new Map<string, ErEntity>()
|
|
40
|
+
// Track entity body parsing
|
|
41
|
+
let currentEntity: ErEntity | null = null
|
|
42
|
+
|
|
43
|
+
for (let i = 1; i < lines.length; i++) {
|
|
44
|
+
const line = lines[i]!
|
|
45
|
+
|
|
46
|
+
// --- Inside entity body ---
|
|
47
|
+
if (currentEntity) {
|
|
48
|
+
if (line === '}') {
|
|
49
|
+
currentEntity = null
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Attribute line: type name [PK|FK|UK] ["comment"]
|
|
54
|
+
const attr = parseAttribute(line)
|
|
55
|
+
if (attr) {
|
|
56
|
+
currentEntity.attributes.push(attr)
|
|
57
|
+
}
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- Entity block start: `ENTITY_NAME {` ---
|
|
62
|
+
const entityBlockMatch = line.match(/^(\S+)\s*\{$/)
|
|
63
|
+
if (entityBlockMatch) {
|
|
64
|
+
const id = entityBlockMatch[1]!
|
|
65
|
+
const entity = ensureEntity(entityMap, id)
|
|
66
|
+
currentEntity = entity
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- Relationship: `ENTITY1 cardinality1--cardinality2 ENTITY2 : label` ---
|
|
71
|
+
const rel = parseRelationshipLine(line)
|
|
72
|
+
if (rel) {
|
|
73
|
+
// Ensure both entities exist
|
|
74
|
+
ensureEntity(entityMap, rel.entity1)
|
|
75
|
+
ensureEntity(entityMap, rel.entity2)
|
|
76
|
+
diagram.relationships.push(rel)
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
diagram.entities = [...entityMap.values()]
|
|
82
|
+
return diagram
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Ensure an entity exists in the map */
|
|
86
|
+
function ensureEntity(entityMap: Map<string, ErEntity>, id: string): ErEntity {
|
|
87
|
+
let entity = entityMap.get(id)
|
|
88
|
+
if (!entity) {
|
|
89
|
+
entity = { id, label: id, attributes: [] }
|
|
90
|
+
entityMap.set(id, entity)
|
|
91
|
+
}
|
|
92
|
+
return entity
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Parse an attribute line inside an entity block */
|
|
96
|
+
function parseAttribute(line: string): ErAttribute | null {
|
|
97
|
+
// Format: type name [PK|FK|UK [...]] ["comment"]
|
|
98
|
+
const match = line.match(/^(\S+)\s+(\S+)(?:\s+(.+))?$/)
|
|
99
|
+
if (!match) return null
|
|
100
|
+
|
|
101
|
+
const type = match[1]!
|
|
102
|
+
const name = match[2]!
|
|
103
|
+
const rest = match[3]?.trim() ?? ''
|
|
104
|
+
|
|
105
|
+
// Extract key constraints (PK, FK, UK) and optional comment
|
|
106
|
+
const keys: ErAttribute['keys'] = []
|
|
107
|
+
let comment: string | undefined
|
|
108
|
+
|
|
109
|
+
// Extract quoted comment first (supports <br> tags)
|
|
110
|
+
const commentMatch = rest.match(/"([^"]*)"/)
|
|
111
|
+
if (commentMatch) {
|
|
112
|
+
comment = normalizeBrTags(commentMatch[1]!)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Extract key constraints
|
|
116
|
+
const restWithoutComment = rest.replace(/"[^"]*"/, '').trim()
|
|
117
|
+
for (const part of restWithoutComment.split(/\s+/)) {
|
|
118
|
+
const upper = part.toUpperCase()
|
|
119
|
+
if (upper === 'PK' || upper === 'FK' || upper === 'UK') {
|
|
120
|
+
keys.push(upper as 'PK' | 'FK' | 'UK')
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { type, name, keys, comment }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse a relationship line.
|
|
129
|
+
*
|
|
130
|
+
* Cardinality symbols on each side of the line style:
|
|
131
|
+
* Left side (entity1): || |o o| }| |{ o{ {o
|
|
132
|
+
* Line: -- (identifying) or .. (non-identifying)
|
|
133
|
+
* Right side (entity2): || o| |o |{ }| {o o{
|
|
134
|
+
*
|
|
135
|
+
* Full pattern example: CUSTOMER ||--o{ ORDER : places
|
|
136
|
+
*/
|
|
137
|
+
function parseRelationshipLine(line: string): ErRelationship | null {
|
|
138
|
+
// Match: ENTITY1 <cardinality_and_line> ENTITY2 : label
|
|
139
|
+
const match = line.match(/^(\S+)\s+([|o}{]+(?:--|\.\.)[|o}{]+)\s+(\S+)\s*:\s*(.+)$/)
|
|
140
|
+
if (!match) return null
|
|
141
|
+
|
|
142
|
+
const entity1 = match[1]!
|
|
143
|
+
const cardinalityStr = match[2]!
|
|
144
|
+
const entity2 = match[3]!
|
|
145
|
+
// Strip surrounding quotes if present, then normalize br tags
|
|
146
|
+
const rawLabel = match[4]!.trim().replace(/^["']|["']$/g, '')
|
|
147
|
+
const label = normalizeBrTags(rawLabel)
|
|
148
|
+
|
|
149
|
+
// Split the cardinality string into left side, line style, right side
|
|
150
|
+
const lineMatch = cardinalityStr.match(/^([|o}{]+)(--|\.\.?)([|o}{]+)$/)
|
|
151
|
+
if (!lineMatch) return null
|
|
152
|
+
|
|
153
|
+
const leftStr = lineMatch[1]!
|
|
154
|
+
const lineStyle = lineMatch[2]!
|
|
155
|
+
const rightStr = lineMatch[3]!
|
|
156
|
+
|
|
157
|
+
const cardinality1 = parseCardinality(leftStr)
|
|
158
|
+
const cardinality2 = parseCardinality(rightStr)
|
|
159
|
+
const identifying = lineStyle === '--'
|
|
160
|
+
|
|
161
|
+
if (!cardinality1 || !cardinality2) return null
|
|
162
|
+
|
|
163
|
+
return { entity1, entity2, cardinality1, cardinality2, label, identifying }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Parse a cardinality notation string into a Cardinality type */
|
|
167
|
+
function parseCardinality(str: string): Cardinality | null {
|
|
168
|
+
// Normalize: sort the characters to handle both orders (e.g., |o and o|)
|
|
169
|
+
const sorted = str.split('').sort().join('')
|
|
170
|
+
|
|
171
|
+
// Exact one: || → sorted "||"
|
|
172
|
+
if (sorted === '||') return 'one'
|
|
173
|
+
// Zero or one: o| or |o → sorted "o|" (o=111 < |=124 in char codes)
|
|
174
|
+
if (sorted === 'o|') return 'zero-one'
|
|
175
|
+
// One or more: }| or |{ → sorted "|}" or "{|"
|
|
176
|
+
if (sorted === '|}' || sorted === '{|') return 'many'
|
|
177
|
+
// Zero or more: o{ or {o → sorted "{o" or "o{"
|
|
178
|
+
if (sorted === '{o' || sorted === 'o{') return 'zero-many'
|
|
179
|
+
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ER diagram types
|
|
3
|
+
//
|
|
4
|
+
// Models the parsed and positioned representations of a Mermaid ER diagram.
|
|
5
|
+
// ER diagrams show database entities, their attributes, and relationships.
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
/** Parsed ER diagram — logical structure from mermaid text */
|
|
9
|
+
export interface ErDiagram {
|
|
10
|
+
/** All entity definitions */
|
|
11
|
+
entities: ErEntity[]
|
|
12
|
+
/** Relationships between entities */
|
|
13
|
+
relationships: ErRelationship[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ErEntity {
|
|
17
|
+
id: string
|
|
18
|
+
/** Display name (same as id unless aliased) */
|
|
19
|
+
label: string
|
|
20
|
+
/** Entity attributes (columns) */
|
|
21
|
+
attributes: ErAttribute[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ErAttribute {
|
|
25
|
+
/** Data type (string, int, varchar, etc.) */
|
|
26
|
+
type: string
|
|
27
|
+
/** Attribute name */
|
|
28
|
+
name: string
|
|
29
|
+
/** Key constraints: PK, FK, UK */
|
|
30
|
+
keys: Array<'PK' | 'FK' | 'UK'>
|
|
31
|
+
/** Optional comment */
|
|
32
|
+
comment?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Cardinality notation (crow's foot):
|
|
37
|
+
* 'one' || exactly one
|
|
38
|
+
* 'zero-one' |o zero or one
|
|
39
|
+
* 'many' }| one or more
|
|
40
|
+
* 'zero-many' o{ zero or more
|
|
41
|
+
*/
|
|
42
|
+
export type Cardinality = 'one' | 'zero-one' | 'many' | 'zero-many'
|
|
43
|
+
|
|
44
|
+
export interface ErRelationship {
|
|
45
|
+
entity1: string
|
|
46
|
+
entity2: string
|
|
47
|
+
/** Cardinality at entity1's end */
|
|
48
|
+
cardinality1: Cardinality
|
|
49
|
+
/** Cardinality at entity2's end */
|
|
50
|
+
cardinality2: Cardinality
|
|
51
|
+
/** Relationship verb/label (e.g., "places", "contains") */
|
|
52
|
+
label: string
|
|
53
|
+
/** Whether the relationship is identifying (solid line) or non-identifying (dashed) */
|
|
54
|
+
identifying: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Positioned ER diagram — ready for SVG rendering
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
export interface PositionedErDiagram {
|
|
62
|
+
width: number
|
|
63
|
+
height: number
|
|
64
|
+
entities: PositionedErEntity[]
|
|
65
|
+
relationships: PositionedErRelationship[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface PositionedErEntity {
|
|
69
|
+
id: string
|
|
70
|
+
label: string
|
|
71
|
+
attributes: ErAttribute[]
|
|
72
|
+
x: number
|
|
73
|
+
y: number
|
|
74
|
+
width: number
|
|
75
|
+
height: number
|
|
76
|
+
/** Height of the header row */
|
|
77
|
+
headerHeight: number
|
|
78
|
+
/** Height per attribute row */
|
|
79
|
+
rowHeight: number
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface PositionedErRelationship {
|
|
83
|
+
entity1: string
|
|
84
|
+
entity2: string
|
|
85
|
+
cardinality1: Cardinality
|
|
86
|
+
cardinality2: Cardinality
|
|
87
|
+
label: string
|
|
88
|
+
identifying: boolean
|
|
89
|
+
/** Path points from entity1 to entity2 */
|
|
90
|
+
points: Array<{ x: number; y: number }>
|
|
91
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Mermaid → ASCII renderer (vendored)
|
|
3
|
+
//
|
|
4
|
+
// First-party copy of the ASCII rendering pipeline from `beautiful-mermaid`
|
|
5
|
+
// (MIT, Copyright (c) 2026 Craft Docs — see ./NOTICE). The SVG pipeline and
|
|
6
|
+
// its `elkjs` graph-layout dependency are omitted; the ASCII renderers use
|
|
7
|
+
// their own grid layout + A* edge routing and need no external deps. Terminal
|
|
8
|
+
// display-width math is delegated to `Bun.stringWidth` (see ./text-metrics).
|
|
9
|
+
//
|
|
10
|
+
// Public surface: renderMermaidASCII + AsciiRenderOptions (incl. the
|
|
11
|
+
// `direction` override) and the theme/color-mode types.
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export * from './ascii/index'
|