@oh-my-pi/pi-utils 16.0.6 → 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.
Files changed (87) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/types/mermaid-ascii.d.ts +1 -1
  3. package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +41 -0
  4. package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +89 -0
  5. package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +7 -0
  6. package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +12 -0
  7. package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +66 -0
  8. package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +48 -0
  9. package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +43 -0
  10. package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +7 -0
  11. package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +56 -0
  12. package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +65 -0
  13. package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +27 -0
  14. package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +17 -0
  15. package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +7 -0
  16. package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +11 -0
  17. package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +34 -0
  18. package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +11 -0
  19. package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +11 -0
  20. package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +26 -0
  21. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +31 -0
  22. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +11 -0
  23. package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +59 -0
  24. package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +17 -0
  25. package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +30 -0
  26. package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +55 -0
  27. package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +206 -0
  28. package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +51 -0
  29. package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +2 -0
  30. package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +6 -0
  31. package/dist/types/vendor/mermaid-ascii/class/types.d.ts +102 -0
  32. package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +6 -0
  33. package/dist/types/vendor/mermaid-ascii/er/types.d.ts +76 -0
  34. package/dist/types/vendor/mermaid-ascii/index.d.ts +1 -0
  35. package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +9 -0
  36. package/dist/types/vendor/mermaid-ascii/parser.d.ts +7 -0
  37. package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +6 -0
  38. package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +130 -0
  39. package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +21 -0
  40. package/dist/types/vendor/mermaid-ascii/types.d.ts +114 -0
  41. package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +25 -0
  42. package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +6 -0
  43. package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +145 -0
  44. package/package.json +2 -3
  45. package/src/mermaid-ascii.ts +1 -1
  46. package/src/vendor/mermaid-ascii/NOTICE +33 -0
  47. package/src/vendor/mermaid-ascii/ascii/ansi.ts +409 -0
  48. package/src/vendor/mermaid-ascii/ascii/canvas.ts +476 -0
  49. package/src/vendor/mermaid-ascii/ascii/class-diagram.ts +699 -0
  50. package/src/vendor/mermaid-ascii/ascii/converter.ts +271 -0
  51. package/src/vendor/mermaid-ascii/ascii/draw.ts +1382 -0
  52. package/src/vendor/mermaid-ascii/ascii/edge-bundling.ts +328 -0
  53. package/src/vendor/mermaid-ascii/ascii/edge-routing.ts +297 -0
  54. package/src/vendor/mermaid-ascii/ascii/er-diagram.ts +441 -0
  55. package/src/vendor/mermaid-ascii/ascii/grid.ts +578 -0
  56. package/src/vendor/mermaid-ascii/ascii/index.ts +187 -0
  57. package/src/vendor/mermaid-ascii/ascii/multiline-utils.ts +78 -0
  58. package/src/vendor/mermaid-ascii/ascii/pathfinder.ts +215 -0
  59. package/src/vendor/mermaid-ascii/ascii/sequence.ts +460 -0
  60. package/src/vendor/mermaid-ascii/ascii/shapes/circle.ts +27 -0
  61. package/src/vendor/mermaid-ascii/ascii/shapes/corners.ts +127 -0
  62. package/src/vendor/mermaid-ascii/ascii/shapes/diamond.ts +27 -0
  63. package/src/vendor/mermaid-ascii/ascii/shapes/hexagon.ts +27 -0
  64. package/src/vendor/mermaid-ascii/ascii/shapes/index.ts +101 -0
  65. package/src/vendor/mermaid-ascii/ascii/shapes/rectangle.ts +175 -0
  66. package/src/vendor/mermaid-ascii/ascii/shapes/rounded.ts +27 -0
  67. package/src/vendor/mermaid-ascii/ascii/shapes/special.ts +296 -0
  68. package/src/vendor/mermaid-ascii/ascii/shapes/stadium.ts +114 -0
  69. package/src/vendor/mermaid-ascii/ascii/shapes/state.ts +192 -0
  70. package/src/vendor/mermaid-ascii/ascii/shapes/types.ts +73 -0
  71. package/src/vendor/mermaid-ascii/ascii/types.ts +273 -0
  72. package/src/vendor/mermaid-ascii/ascii/validate.ts +120 -0
  73. package/src/vendor/mermaid-ascii/ascii/xychart.ts +875 -0
  74. package/src/vendor/mermaid-ascii/class/parser.ts +290 -0
  75. package/src/vendor/mermaid-ascii/class/types.ts +121 -0
  76. package/src/vendor/mermaid-ascii/er/parser.ts +181 -0
  77. package/src/vendor/mermaid-ascii/er/types.ts +91 -0
  78. package/src/vendor/mermaid-ascii/index.ts +14 -0
  79. package/src/vendor/mermaid-ascii/multiline-utils.ts +30 -0
  80. package/src/vendor/mermaid-ascii/parser.ts +645 -0
  81. package/src/vendor/mermaid-ascii/sequence/parser.ts +207 -0
  82. package/src/vendor/mermaid-ascii/sequence/types.ts +146 -0
  83. package/src/vendor/mermaid-ascii/text-metrics.ts +71 -0
  84. package/src/vendor/mermaid-ascii/types.ts +164 -0
  85. package/src/vendor/mermaid-ascii/xychart/colors.ts +140 -0
  86. package/src/vendor/mermaid-ascii/xychart/parser.ts +115 -0
  87. 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'