@zombie-mermaid/mermaid-parser 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +879 -0
- package/dist/index.d.ts +879 -0
- package/dist/index.js +994 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -0
- package/src/__tests__/box-color.test.ts +80 -0
- package/src/__tests__/sequence-activation-fix.test.ts +91 -0
- package/src/__tests__/xychart-colors.test.ts +149 -0
- package/src/class/format.ts +15 -0
- package/src/class/parser.ts +593 -0
- package/src/class/types.ts +166 -0
- package/src/er/parser.ts +286 -0
- package/src/er/types.ts +99 -0
- package/src/expanded-shapes.ts +376 -0
- package/src/index.ts +48 -0
- package/src/sequence/activation-check.ts +149 -0
- package/src/sequence/activation-fix.ts +112 -0
- package/src/sequence/box-color.ts +85 -0
- package/src/sequence/parser.ts +613 -0
- package/src/sequence/types.ts +242 -0
- package/src/xychart/colors.ts +177 -0
- package/src/xychart/parser.ts +246 -0
- package/src/xychart/types.ts +150 -0
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
import type { NodeInteraction, StyleDirectives } from '@zombie-mermaid/core'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parsed class diagram — logical structure from mermaid text.
|
|
12
|
+
*
|
|
13
|
+
* Extends {@link StyleDirectives} so `classDef` / `cssClass` / `style` /
|
|
14
|
+
* `:::` resolve through the same cascade flowcharts use (see
|
|
15
|
+
* packages/core/src/style-directives.ts).
|
|
16
|
+
*/
|
|
17
|
+
export interface ClassDiagram extends StyleDirectives {
|
|
18
|
+
/** All class definitions */
|
|
19
|
+
classes: ClassNode[]
|
|
20
|
+
/** Relationships between classes */
|
|
21
|
+
relationships: ClassRelationship[]
|
|
22
|
+
/** Optional namespace groupings */
|
|
23
|
+
namespaces: ClassNamespace[]
|
|
24
|
+
/** Maps class IDs to interactions declared by `click` statements */
|
|
25
|
+
interactions: Map<string, NodeInteraction>
|
|
26
|
+
/** Notes, in source order — `note "text"` and `note for X "text"` */
|
|
27
|
+
notes: ClassNote[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A class-diagram note. Mermaid lays an attached note out as its own node
|
|
32
|
+
* joined to the class by a dotted, arrowless link (classDb.getData); a free
|
|
33
|
+
* note is a lone node.
|
|
34
|
+
*/
|
|
35
|
+
export interface ClassNote {
|
|
36
|
+
/** Note text; `\n` / `<br/>` in the source are already normalized to newlines */
|
|
37
|
+
text: string
|
|
38
|
+
/** Class this note is attached to (`note for X`); absent for a free note */
|
|
39
|
+
forClass?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ClassNode {
|
|
43
|
+
id: string
|
|
44
|
+
label: string
|
|
45
|
+
/** Annotation like <<interface>>, <<abstract>>, <<service>>, <<enumeration>> */
|
|
46
|
+
annotation?: string
|
|
47
|
+
/** Class attributes (fields/properties) */
|
|
48
|
+
attributes: ClassMember[]
|
|
49
|
+
/** Class methods (functions) */
|
|
50
|
+
methods: ClassMember[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ClassMember {
|
|
54
|
+
/** Visibility: + public, - private, # protected, ~ package */
|
|
55
|
+
visibility: '+' | '-' | '#' | '~' | ''
|
|
56
|
+
/** Member name */
|
|
57
|
+
name: string
|
|
58
|
+
/** Type annotation (e.g., "String", "int", "void") */
|
|
59
|
+
type?: string
|
|
60
|
+
/** Whether the member is static (underlined in UML) */
|
|
61
|
+
isStatic?: boolean
|
|
62
|
+
/** Whether the member is abstract (italic in UML) */
|
|
63
|
+
isAbstract?: boolean
|
|
64
|
+
/** Whether the member is a method (renders with parentheses) */
|
|
65
|
+
isMethod?: boolean
|
|
66
|
+
/** Method parameters (e.g., "data", "key, val") — only for methods */
|
|
67
|
+
params?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Relationship types following UML conventions */
|
|
71
|
+
export type RelationshipType =
|
|
72
|
+
| 'inheritance' // A <|-- B (solid line, hollow triangle)
|
|
73
|
+
| 'composition' // A *-- B (solid line, filled diamond)
|
|
74
|
+
| 'aggregation' // A o-- B (solid line, hollow diamond)
|
|
75
|
+
| 'association' // A --> B (solid line, open arrow)
|
|
76
|
+
| 'dependency' // A ..> B (dashed line, open arrow)
|
|
77
|
+
| 'realization' // A ..|> B (dashed line, hollow triangle)
|
|
78
|
+
|
|
79
|
+
export interface ClassRelationship {
|
|
80
|
+
from: string
|
|
81
|
+
to: string
|
|
82
|
+
type: RelationshipType
|
|
83
|
+
/**
|
|
84
|
+
* Which end of the relationship line has the UML marker (triangle, diamond, arrow).
|
|
85
|
+
* Determined by the arrow syntax direction:
|
|
86
|
+
* - Prefix markers like `<|--`, `*--`, `o--` → 'from' (marker on left/from side)
|
|
87
|
+
* - Suffix markers like `..|>`, `-->`, `..>`, `--*`, `--o` → 'to' (marker on right/to side)
|
|
88
|
+
*/
|
|
89
|
+
markerAt: 'from' | 'to'
|
|
90
|
+
/** Label on the relationship line */
|
|
91
|
+
label?: string
|
|
92
|
+
/** Cardinality at the "from" end (e.g., "1", "*", "0..1") */
|
|
93
|
+
fromCardinality?: string
|
|
94
|
+
/** Cardinality at the "to" end */
|
|
95
|
+
toCardinality?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ClassNamespace {
|
|
99
|
+
name: string
|
|
100
|
+
classIds: string[]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Positioned class diagram — ready for SVG rendering
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
export interface PositionedClassDiagram {
|
|
108
|
+
width: number
|
|
109
|
+
height: number
|
|
110
|
+
classes: PositionedClassNode[]
|
|
111
|
+
relationships: PositionedClassRelationship[]
|
|
112
|
+
notes: PositionedClassNote[]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface PositionedClassNote {
|
|
116
|
+
/** Layout id — never collides with a class id (class ids contain no spaces) */
|
|
117
|
+
id: string
|
|
118
|
+
text: string
|
|
119
|
+
/** Class this note is attached to, when that class exists in the diagram */
|
|
120
|
+
forClass?: string
|
|
121
|
+
x: number
|
|
122
|
+
y: number
|
|
123
|
+
width: number
|
|
124
|
+
height: number
|
|
125
|
+
/** Routed path of the dotted note→class link; absent for a free note */
|
|
126
|
+
linkPoints?: Array<{ x: number; y: number }>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface PositionedClassNode {
|
|
130
|
+
id: string
|
|
131
|
+
label: string
|
|
132
|
+
annotation?: string
|
|
133
|
+
attributes: ClassMember[]
|
|
134
|
+
methods: ClassMember[]
|
|
135
|
+
x: number
|
|
136
|
+
y: number
|
|
137
|
+
width: number
|
|
138
|
+
height: number
|
|
139
|
+
/** Height of the header section (name + annotation) */
|
|
140
|
+
headerHeight: number
|
|
141
|
+
/** Height of the attributes section */
|
|
142
|
+
attrHeight: number
|
|
143
|
+
/** Height of the methods section */
|
|
144
|
+
methodHeight: number
|
|
145
|
+
/** Interaction from a `click` statement — an href wraps the class box in an <a> */
|
|
146
|
+
interaction?: NodeInteraction
|
|
147
|
+
/** Inline styles resolved from classDef + `style` statements — override theme defaults */
|
|
148
|
+
inlineStyle?: Record<string, string>
|
|
149
|
+
/** Style class assigned via `cssClass`, `class A name`, or `:::name` — emitted onto the group's `class` attribute so external CSS can target it */
|
|
150
|
+
className?: string
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface PositionedClassRelationship {
|
|
154
|
+
from: string
|
|
155
|
+
to: string
|
|
156
|
+
type: RelationshipType
|
|
157
|
+
/** Which end of the line has the UML marker — propagated from ClassRelationship */
|
|
158
|
+
markerAt: 'from' | 'to'
|
|
159
|
+
label?: string
|
|
160
|
+
fromCardinality?: string
|
|
161
|
+
toCardinality?: string
|
|
162
|
+
/** Path points from source to target */
|
|
163
|
+
points: Array<{ x: number; y: number }>
|
|
164
|
+
/** ELK-computed label center position (avoids overlaps between nearby edges) */
|
|
165
|
+
labelPosition?: { x: number; y: number }
|
|
166
|
+
}
|
package/src/er/parser.ts
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ErDiagram,
|
|
3
|
+
ErEntity,
|
|
4
|
+
ErAttribute,
|
|
5
|
+
ErRelationship,
|
|
6
|
+
Cardinality,
|
|
7
|
+
} from './types.ts'
|
|
8
|
+
import {
|
|
9
|
+
normalizeBrTags,
|
|
10
|
+
toDirection,
|
|
11
|
+
type Statement,
|
|
12
|
+
} from '@zombie-mermaid/core'
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// ER diagram parser
|
|
16
|
+
//
|
|
17
|
+
// Parses Mermaid erDiagram syntax into an ErDiagram structure.
|
|
18
|
+
//
|
|
19
|
+
// Supported syntax:
|
|
20
|
+
// CUSTOMER ||--o{ ORDER : places
|
|
21
|
+
// CUSTOMER {
|
|
22
|
+
// string name PK
|
|
23
|
+
// int age
|
|
24
|
+
// string email UK "user email"
|
|
25
|
+
// }
|
|
26
|
+
//
|
|
27
|
+
// Cardinality notation:
|
|
28
|
+
// || || exactly one
|
|
29
|
+
// |o o| zero or one
|
|
30
|
+
// }| |{ one or more
|
|
31
|
+
// }o o{ zero or more
|
|
32
|
+
//
|
|
33
|
+
// Line style:
|
|
34
|
+
// -- identifying (solid line)
|
|
35
|
+
// .. non-identifying (dashed line)
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse a Mermaid ER diagram.
|
|
40
|
+
* Expects the first line to be "erDiagram".
|
|
41
|
+
*/
|
|
42
|
+
// Audited for issue #100 (non-null assertions): every `!` in this file is
|
|
43
|
+
// either a bounds-checked loop-index array access (`lines[i]!` inside a
|
|
44
|
+
// `for (let i = 1; i < lines.length; ...)` loop) or a regex-mandatory-
|
|
45
|
+
// capture-group access after `.match()` (a group that isn't wrapped in an
|
|
46
|
+
// optional `(?:...)?`, so it always participates when the overall match
|
|
47
|
+
// succeeds). Both are the same idioms already accepted as justified
|
|
48
|
+
// elsewhere in this codebase (see src/parser.ts, PR #158, and this
|
|
49
|
+
// subsystem's layout.ts/renderer.ts audit, PR #148, which fixed the
|
|
50
|
+
// genuinely risky assertions there but didn't reach this file — PR #146
|
|
51
|
+
// separately reviewed this file's `as` casts, which are a different
|
|
52
|
+
// concern from these `!`s) — `noUncheckedIndexedAccess` can't see either
|
|
53
|
+
// guarantee, but removing the `!` would only replace a proven-safe
|
|
54
|
+
// assertion with an unreachable guard. Left as-is; no behavior change.
|
|
55
|
+
export function parseErDiagram(lines: Statement[]): ErDiagram {
|
|
56
|
+
const diagram: ErDiagram = {
|
|
57
|
+
entities: [],
|
|
58
|
+
relationships: [],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Track entities by ID for deduplication
|
|
62
|
+
const entityMap = new Map<string, ErEntity>()
|
|
63
|
+
// Track entity body parsing
|
|
64
|
+
let currentEntity: ErEntity | null = null
|
|
65
|
+
|
|
66
|
+
for (let i = 1; i < lines.length; i++) {
|
|
67
|
+
const stmt = lines[i]!
|
|
68
|
+
const line = stmt.text
|
|
69
|
+
|
|
70
|
+
// --- Inside entity body ---
|
|
71
|
+
if (currentEntity) {
|
|
72
|
+
if (line === '}') {
|
|
73
|
+
currentEntity = null
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Attribute line: type name [PK|FK|UK] ["comment"]
|
|
78
|
+
const attr = parseAttribute(line)
|
|
79
|
+
if (attr) {
|
|
80
|
+
currentEntity.attributes.push(attr)
|
|
81
|
+
}
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- direction directive: `direction TB` / `direction LR` / etc. ---
|
|
86
|
+
// ER diagrams have no subgraph nesting, so a single top-level direction
|
|
87
|
+
// applies to the whole diagram (unlike flowcharts, where `direction` can
|
|
88
|
+
// also appear per-subgraph).
|
|
89
|
+
const dirMatch = line.match(/^direction\s+(TD|TB|LR|BT|RL)\s*$/i)
|
|
90
|
+
if (dirMatch) {
|
|
91
|
+
diagram.direction = toDirection(dirMatch[1]!)
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- Entity block start: `ENTITY_NAME {`, `id[Alias Label] {`, or
|
|
96
|
+
// `id["Quoted Alias"] {` — optionally with the body (and even the
|
|
97
|
+
// closing `}`) inline on the same line, e.g. `p[Person] { string name }`.
|
|
98
|
+
const entityBlockMatch = line.match(/^(\S+?)(?:\[(.+)\])?\s*\{(.*)$/)
|
|
99
|
+
if (entityBlockMatch) {
|
|
100
|
+
const id = entityBlockMatch[1]!
|
|
101
|
+
const aliasRaw = entityBlockMatch[2]
|
|
102
|
+
const alias =
|
|
103
|
+
aliasRaw !== undefined
|
|
104
|
+
? normalizeBrTags(aliasRaw.trim().replace(/^["']|["']$/g, ''))
|
|
105
|
+
: undefined
|
|
106
|
+
const entity = ensureEntity(entityMap, id, alias)
|
|
107
|
+
|
|
108
|
+
const trailing = entityBlockMatch[3]!.trim()
|
|
109
|
+
const closesInline = trailing.endsWith('}')
|
|
110
|
+
const body = closesInline ? trailing.slice(0, -1).trim() : trailing
|
|
111
|
+
for (const part of body
|
|
112
|
+
.split(';')
|
|
113
|
+
.map((s) => s.trim())
|
|
114
|
+
.filter(Boolean)) {
|
|
115
|
+
const attr = parseAttribute(part)
|
|
116
|
+
if (attr) entity.attributes.push(attr)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Only keep the block open for subsequent attribute lines when this
|
|
120
|
+
// line didn't already close it.
|
|
121
|
+
if (!closesInline) {
|
|
122
|
+
currentEntity = entity
|
|
123
|
+
}
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- Relationship: `ENTITY1 cardinality1--cardinality2 ENTITY2 : label` ---
|
|
128
|
+
const rel = parseRelationshipLine(line, stmt.line)
|
|
129
|
+
if (rel) {
|
|
130
|
+
// Ensure both entities exist
|
|
131
|
+
ensureEntity(entityMap, rel.entity1)
|
|
132
|
+
ensureEntity(entityMap, rel.entity2)
|
|
133
|
+
diagram.relationships.push(rel)
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
diagram.entities = [...entityMap.values()]
|
|
139
|
+
return diagram
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Ensure an entity exists in the map. When `alias` is provided (from an
|
|
144
|
+
* `id[Alias]` entity-block header), it becomes the display label while the
|
|
145
|
+
* map key — and every relationship reference — stays keyed off the raw `id`.
|
|
146
|
+
*/
|
|
147
|
+
function ensureEntity(
|
|
148
|
+
entityMap: Map<string, ErEntity>,
|
|
149
|
+
id: string,
|
|
150
|
+
alias?: string,
|
|
151
|
+
): ErEntity {
|
|
152
|
+
let entity = entityMap.get(id)
|
|
153
|
+
if (!entity) {
|
|
154
|
+
entity = { id, label: alias ?? id, attributes: [] }
|
|
155
|
+
entityMap.set(id, entity)
|
|
156
|
+
} else if (alias !== undefined) {
|
|
157
|
+
entity.label = alias
|
|
158
|
+
}
|
|
159
|
+
return entity
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Parse an attribute line inside an entity block */
|
|
163
|
+
function parseAttribute(line: string): ErAttribute | null {
|
|
164
|
+
// Format: type name [PK|FK|UK [...]] ["comment"]
|
|
165
|
+
const match = line.match(/^(\S+)\s+(\S+)(?:\s+(.+))?$/)
|
|
166
|
+
if (!match) return null
|
|
167
|
+
|
|
168
|
+
const type = match[1]!
|
|
169
|
+
const name = match[2]!
|
|
170
|
+
const rest = match[3]?.trim() ?? ''
|
|
171
|
+
|
|
172
|
+
// Extract key constraints (PK, FK, UK) and optional comment
|
|
173
|
+
const keys: ErAttribute['keys'] = []
|
|
174
|
+
let comment: string | undefined
|
|
175
|
+
|
|
176
|
+
// Extract quoted comment first (supports <br> tags)
|
|
177
|
+
const commentMatch = rest.match(/"([^"]*)"/)
|
|
178
|
+
if (commentMatch) {
|
|
179
|
+
comment = normalizeBrTags(commentMatch[1]!)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Extract key constraints
|
|
183
|
+
const restWithoutComment = rest.replace(/"[^"]*"/, '').trim()
|
|
184
|
+
for (const part of restWithoutComment.split(/\s+/)) {
|
|
185
|
+
const upper = part.toUpperCase()
|
|
186
|
+
if (upper === 'PK' || upper === 'FK' || upper === 'UK') {
|
|
187
|
+
keys.push(upper)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { type, name, keys, comment }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Parse a relationship line.
|
|
196
|
+
*
|
|
197
|
+
* Cardinality symbols on each side of the line style:
|
|
198
|
+
* Left side (entity1): || |o }| }o
|
|
199
|
+
* Line: -- (identifying) or .. (non-identifying)
|
|
200
|
+
* Right side (entity2): || o| |{ o{
|
|
201
|
+
*
|
|
202
|
+
* Full pattern example: CUSTOMER ||--o{ ORDER : places
|
|
203
|
+
*/
|
|
204
|
+
function parseRelationshipLine(
|
|
205
|
+
line: string,
|
|
206
|
+
lineNumber: number,
|
|
207
|
+
): ErRelationship | null {
|
|
208
|
+
// Loosely match a line shaped like a relationship attempt: two bare
|
|
209
|
+
// tokens flanking a `--`/`..`-based marker, with an optional `: label`.
|
|
210
|
+
// A line with no such marker at all doesn't look like a relationship
|
|
211
|
+
// attempt and falls through silently (returns null), same as before —
|
|
212
|
+
// that part of the parser's leniency is unchanged.
|
|
213
|
+
//
|
|
214
|
+
// Everything that *does* match this shape is validated below and either
|
|
215
|
+
// returns a fully-parsed relationship or throws an actionable error.
|
|
216
|
+
// Before this pass, an invalid cardinality token or a missing/empty
|
|
217
|
+
// label silently dropped the *entire* line (both entities included)
|
|
218
|
+
// with zero indication anything was wrong — see issue #541 (parser
|
|
219
|
+
// error-message quality audit — this parser had zero throw sites before
|
|
220
|
+
// this pass).
|
|
221
|
+
const attempt = line.match(
|
|
222
|
+
/^(\S+)\s+(\S*(?:--|\.\.)\S*)\s+(\S+)\s*(?::\s*(.*))?$/,
|
|
223
|
+
)
|
|
224
|
+
if (!attempt) return null
|
|
225
|
+
|
|
226
|
+
const entity1 = attempt[1]!
|
|
227
|
+
const cardinalityStr = attempt[2]!
|
|
228
|
+
const entity2 = attempt[3]!
|
|
229
|
+
const hasLabel = attempt[4] !== undefined
|
|
230
|
+
const rawLabel = attempt[4]?.trim() ?? ''
|
|
231
|
+
|
|
232
|
+
// Cardinality symbols on each side of the line style:
|
|
233
|
+
// Left side (entity1): || |o }| }o
|
|
234
|
+
// Line: -- (identifying) or .. (non-identifying)
|
|
235
|
+
// Right side (entity2): || o| |{ o{
|
|
236
|
+
// `shapeMatch` only confirms the token is built from crow's-foot
|
|
237
|
+
// characters around a line-style marker — `parseLeftCardinality`/
|
|
238
|
+
// `parseRightCardinality` still reject a shape match that isn't one of
|
|
239
|
+
// the four valid two-character combinations per side (e.g. a lone "|").
|
|
240
|
+
const shapeMatch = cardinalityStr.match(/^([|o}{]*)(--|\.\.?)([|o}{]*)$/)
|
|
241
|
+
const cardinality1 = shapeMatch ? parseLeftCardinality(shapeMatch[1]!) : null
|
|
242
|
+
const cardinality2 = shapeMatch ? parseRightCardinality(shapeMatch[3]!) : null
|
|
243
|
+
|
|
244
|
+
if (!cardinality1 || !cardinality2) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Line ${lineNumber}: Invalid ER relationship cardinality "${cardinalityStr}" in "${line}". ` +
|
|
247
|
+
'Left side must be one of ||, |o, }|, }o; right side must be one of ||, o|, |{, o{ (e.g. "||--o{").',
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!hasLabel || rawLabel.length === 0) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Line ${lineNumber}: ER relationship "${entity1} ${cardinalityStr} ${entity2}" is missing a ": label" — expected e.g. "${entity1} ${cardinalityStr} ${entity2} : label".`,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Strip surrounding quotes if present, then normalize br tags.
|
|
258
|
+
const label = normalizeBrTags(rawLabel.replace(/^["']|["']$/g, ''))
|
|
259
|
+
const identifying = shapeMatch![2] === '--'
|
|
260
|
+
|
|
261
|
+
return { entity1, entity2, cardinality1, cardinality2, label, identifying }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Parse a left-side (entity1) cardinality notation string.
|
|
266
|
+
* The crow's-foot character sits nearer the entity, so left- and
|
|
267
|
+
* right-side notations are mirror images of each other and must be
|
|
268
|
+
* matched exactly rather than order-normalized (sorting `}o` and `o{`
|
|
269
|
+
* to the same key conflates "zero or more" with malformed input).
|
|
270
|
+
*/
|
|
271
|
+
function parseLeftCardinality(str: string): Cardinality | null {
|
|
272
|
+
if (str === '||') return 'one'
|
|
273
|
+
if (str === '|o') return 'zero-one'
|
|
274
|
+
if (str === '}|') return 'many'
|
|
275
|
+
if (str === '}o') return 'zero-many'
|
|
276
|
+
return null
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Parse a right-side (entity2) cardinality notation string. */
|
|
280
|
+
function parseRightCardinality(str: string): Cardinality | null {
|
|
281
|
+
if (str === '||') return 'one'
|
|
282
|
+
if (str === 'o|') return 'zero-one'
|
|
283
|
+
if (str === '|{') return 'many'
|
|
284
|
+
if (str === 'o{') return 'zero-many'
|
|
285
|
+
return null
|
|
286
|
+
}
|
package/src/er/types.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
import type { Direction } from '@zombie-mermaid/core'
|
|
9
|
+
|
|
10
|
+
/** Parsed ER diagram — logical structure from mermaid text */
|
|
11
|
+
export interface ErDiagram {
|
|
12
|
+
/** All entity definitions */
|
|
13
|
+
entities: ErEntity[]
|
|
14
|
+
/** Relationships between entities */
|
|
15
|
+
relationships: ErRelationship[]
|
|
16
|
+
/**
|
|
17
|
+
* Overall layout direction, from a top-level `direction TB` / `direction LR`
|
|
18
|
+
* / `direction BT` / `direction RL` statement. `undefined` when the diagram
|
|
19
|
+
* doesn't specify one, in which case the layout falls back to its default.
|
|
20
|
+
*/
|
|
21
|
+
direction?: Direction
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ErEntity {
|
|
25
|
+
id: string
|
|
26
|
+
/** Display name (same as id unless aliased) */
|
|
27
|
+
label: string
|
|
28
|
+
/** Entity attributes (columns) */
|
|
29
|
+
attributes: ErAttribute[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ErAttribute {
|
|
33
|
+
/** Data type (string, int, varchar, etc.) */
|
|
34
|
+
type: string
|
|
35
|
+
/** Attribute name */
|
|
36
|
+
name: string
|
|
37
|
+
/** Key constraints: PK, FK, UK */
|
|
38
|
+
keys: Array<'PK' | 'FK' | 'UK'>
|
|
39
|
+
/** Optional comment */
|
|
40
|
+
comment?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Cardinality notation (crow's foot):
|
|
45
|
+
* 'one' || || exactly one
|
|
46
|
+
* 'zero-one' |o o| zero or one
|
|
47
|
+
* 'many' }| |{ one or more
|
|
48
|
+
* 'zero-many' }o o{ zero or more
|
|
49
|
+
*/
|
|
50
|
+
export type Cardinality = 'one' | 'zero-one' | 'many' | 'zero-many'
|
|
51
|
+
|
|
52
|
+
export interface ErRelationship {
|
|
53
|
+
entity1: string
|
|
54
|
+
entity2: string
|
|
55
|
+
/** Cardinality at entity1's end */
|
|
56
|
+
cardinality1: Cardinality
|
|
57
|
+
/** Cardinality at entity2's end */
|
|
58
|
+
cardinality2: Cardinality
|
|
59
|
+
/** Relationship verb/label (e.g., "places", "contains") */
|
|
60
|
+
label: string
|
|
61
|
+
/** Whether the relationship is identifying (solid line) or non-identifying (dashed) */
|
|
62
|
+
identifying: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Positioned ER diagram — ready for SVG rendering
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
export interface PositionedErDiagram {
|
|
70
|
+
width: number
|
|
71
|
+
height: number
|
|
72
|
+
entities: PositionedErEntity[]
|
|
73
|
+
relationships: PositionedErRelationship[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface PositionedErEntity {
|
|
77
|
+
id: string
|
|
78
|
+
label: string
|
|
79
|
+
attributes: ErAttribute[]
|
|
80
|
+
x: number
|
|
81
|
+
y: number
|
|
82
|
+
width: number
|
|
83
|
+
height: number
|
|
84
|
+
/** Height of the header row */
|
|
85
|
+
headerHeight: number
|
|
86
|
+
/** Height per attribute row */
|
|
87
|
+
rowHeight: number
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface PositionedErRelationship {
|
|
91
|
+
entity1: string
|
|
92
|
+
entity2: string
|
|
93
|
+
cardinality1: Cardinality
|
|
94
|
+
cardinality2: Cardinality
|
|
95
|
+
label: string
|
|
96
|
+
identifying: boolean
|
|
97
|
+
/** Path points from entity1 to entity2 */
|
|
98
|
+
points: Array<{ x: number; y: number }>
|
|
99
|
+
}
|