@diagrammo/dgmo 0.2.28 → 0.3.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.
@@ -441,7 +441,10 @@ Animal
441
441
  + speak(): void
442
442
 
443
443
  Dog extends Animal
444
+ + breed: string
445
+
444
446
  Cat extends Animal
447
+ + indoor: boolean
445
448
  ```
446
449
 
447
450
  Full example:
@@ -453,38 +456,37 @@ title: Type Hierarchy
453
456
  Printable [interface]
454
457
  + print(): void
455
458
 
456
- Shape [abstract]
459
+ Shape implements Printable [abstract]
457
460
  # x: number
458
461
  # y: number
459
462
  + area(): number
460
- {static} count: number
463
+ count: number {static}
461
464
 
462
- Circle
465
+ Circle extends Shape
463
466
  - radius: number
464
467
  + area(): number
465
468
 
466
- Rectangle
469
+ Rectangle extends Shape
467
470
  - width: number
468
471
  - height: number
469
472
 
470
- Circle extends Shape
471
- Rectangle extends Shape
472
- Shape implements Printable
473
473
  Shape *-- Circle : contains
474
474
  ```
475
475
 
476
476
  **Class modifiers**: `[abstract]`, `[interface]`, `[enum]`
477
477
 
478
+ **Inheritance**: `ClassName extends Parent` or `ClassName implements Interface` — declared inline in the class header. Members are indented below.
479
+
478
480
  **Member visibility**: `+` public, `#` protected, `-` private. Static: `{static}`.
479
481
 
480
- **Relationships** (keyword or arrow):
481
- - Inheritance: `A extends B` or `A --|> B`
482
- - Implementation: `A implements B` or `A ..|> B`
483
- - Composition: `A contains B` or `A *-- B`
484
- - Aggregation: `A has B` or `A o-- B`
485
- - Dependency: `A uses B` or `A ..> B`
482
+ **Relationships** (arrow syntax):
483
+ - Inheritance: `A --|> B`
484
+ - Implementation: `A ..|> B`
485
+ - Composition: `A *-- B`
486
+ - Aggregation: `A o-- B`
487
+ - Dependency: `A ..> B`
486
488
  - Association: `A -> B`
487
- - Optional label: `A extends B : description`
489
+ - Optional label: `A *-- B : description`
488
490
 
489
491
  ### er
490
492
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.2.28",
3
+ "version": "0.3.0",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -50,11 +50,11 @@
50
50
  "d3-scale": "^4.0.2",
51
51
  "d3-selection": "^3.0.0",
52
52
  "d3-shape": "^3.2.0",
53
- "echarts": "^5.6.0",
53
+ "echarts": "^6.0.0",
54
54
  "lz-string": "^1.5.0"
55
55
  },
56
56
  "optionalDependencies": {
57
- "jsdom": "^26.0.0"
57
+ "jsdom": "^28.1.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/d3-array": "^3.2.1",
@@ -63,11 +63,11 @@
63
63
  "@types/d3-scale": "^4.0.8",
64
64
  "@types/d3-selection": "^3.0.11",
65
65
  "@types/d3-shape": "^3.1.7",
66
- "@types/dagre": "^0.7.53",
67
- "@types/jsdom": "^21.1.7",
66
+ "@types/dagre": "^0.7.54",
67
+ "@types/jsdom": "^28.0.0",
68
68
  "jscpd": "^4.0.8",
69
69
  "tsup": "^8.5.1",
70
70
  "typescript": "^5.7.3",
71
- "vitest": "^3.0.0"
71
+ "vitest": "^4.0.18"
72
72
  }
73
73
  }
@@ -87,7 +87,7 @@ function computeNodeDimensions(node: ClassNode): {
87
87
  const headerHeight = HEADER_BASE + (node.modifier ? MODIFIER_BADGE : 0);
88
88
 
89
89
  // Fields compartment
90
- let fieldsHeight = 0;
90
+ let fieldsHeight: number;
91
91
  if (isEnum) {
92
92
  // Enum values go in fields compartment
93
93
  const enumValues = node.members; // all members are enum values
@@ -96,6 +96,8 @@ function computeNodeDimensions(node: ClassNode): {
96
96
  COMPARTMENT_PADDING_Y * 2 +
97
97
  enumValues.length * MEMBER_LINE_HEIGHT +
98
98
  SEPARATOR_HEIGHT;
99
+ } else {
100
+ fieldsHeight = SEPARATOR_HEIGHT + COMPARTMENT_PADDING_Y;
99
101
  }
100
102
  } else {
101
103
  if (fields.length > 0) {
@@ -103,24 +105,27 @@ function computeNodeDimensions(node: ClassNode): {
103
105
  COMPARTMENT_PADDING_Y * 2 +
104
106
  fields.length * MEMBER_LINE_HEIGHT +
105
107
  SEPARATOR_HEIGHT;
108
+ } else {
109
+ // UML: always show attributes compartment
110
+ fieldsHeight = SEPARATOR_HEIGHT + COMPARTMENT_PADDING_Y;
106
111
  }
107
112
  }
108
113
 
109
114
  // Methods compartment (not for enums)
110
115
  let methodsHeight = 0;
111
- if (!isEnum && methods.length > 0) {
112
- methodsHeight =
113
- COMPARTMENT_PADDING_Y * 2 +
114
- methods.length * MEMBER_LINE_HEIGHT +
115
- SEPARATOR_HEIGHT;
116
+ if (!isEnum) {
117
+ if (methods.length > 0) {
118
+ methodsHeight =
119
+ COMPARTMENT_PADDING_Y * 2 +
120
+ methods.length * MEMBER_LINE_HEIGHT +
121
+ SEPARATOR_HEIGHT;
122
+ } else {
123
+ // UML: always show methods compartment
124
+ methodsHeight = SEPARATOR_HEIGHT + COMPARTMENT_PADDING_Y;
125
+ }
116
126
  }
117
127
 
118
- // If no members at all, add minimal padding
119
- const height =
120
- headerHeight +
121
- fieldsHeight +
122
- methodsHeight +
123
- (fieldsHeight === 0 && methodsHeight === 0 ? 4 : 0);
128
+ const height = headerHeight + fieldsHeight + methodsHeight;
124
129
 
125
130
  return { width, height, headerHeight, fieldsHeight, methodsHeight };
126
131
  }
@@ -6,7 +6,6 @@ import type {
6
6
  ParsedClassDiagram,
7
7
  ClassNode,
8
8
  ClassMember,
9
- ClassRelationship,
10
9
  ClassModifier,
11
10
  MemberVisibility,
12
11
  RelationshipType,
@@ -24,14 +23,9 @@ function classId(name: string): string {
24
23
  // Regex patterns
25
24
  // ============================================================
26
25
 
27
- // Class declaration: ClassName [modifier] (color)
26
+ // Class declaration: ClassName [extends|implements ParentClass] [modifier] (color)
28
27
  const CLASS_DECL_RE =
29
- /^([A-Z][A-Za-z0-9_]*)(?:\s+\[(abstract|interface|enum)\])?(?:\s+\(([^)]+)\))?\s*$/;
30
-
31
- // Relationship — keyword syntax:
32
- // ClassName extends|implements|contains|has|uses TargetClass : label
33
- const REL_KEYWORD_RE =
34
- /^([A-Z][A-Za-z0-9_]*)\s+(extends|implements|contains|has|uses)\s+([A-Z][A-Za-z0-9_]*)(?:\s*:\s*(.+))?$/;
28
+ /^([A-Z][A-Za-z0-9_]*)(?:\s+(extends|implements)\s+([A-Z][A-Za-z0-9_]*))?(?:\s+\[(abstract|interface|enum)\])?(?:\s+\(([^)]+)\))?\s*$/;
35
29
 
36
30
  // Relationship — arrow syntax:
37
31
  // ClassName --|> TargetClass : label
@@ -45,14 +39,6 @@ const STATIC_SUFFIX_RE = /\{static\}\s*$/;
45
39
  const METHOD_RE = /^(.+?)\(([^)]*)\)(?:\s*:\s*(.+))?$/;
46
40
  const FIELD_RE = /^(.+?)\s*:\s*(.+)$/;
47
41
 
48
- const KEYWORD_TO_TYPE: Record<string, RelationshipType> = {
49
- extends: 'extends',
50
- implements: 'implements',
51
- contains: 'composes',
52
- has: 'aggregates',
53
- uses: 'depends',
54
- };
55
-
56
42
  const ARROW_TO_TYPE: Record<string, RelationshipType> = {
57
43
  '--|>': 'extends',
58
44
  '..|>': 'implements',
@@ -257,28 +243,6 @@ export function parseClassDiagram(
257
243
  currentClass = null;
258
244
  contentStarted = true;
259
245
 
260
- // Try relationship — keyword syntax
261
- const relKeyword = trimmed.match(REL_KEYWORD_RE);
262
- if (relKeyword) {
263
- const sourceName = relKeyword[1];
264
- const keyword = relKeyword[2].toLowerCase();
265
- const targetName = relKeyword[3];
266
- const label = relKeyword[4]?.trim();
267
-
268
- // Ensure both classes exist
269
- getOrCreateClass(sourceName, lineNumber);
270
- getOrCreateClass(targetName, lineNumber);
271
-
272
- result.relationships.push({
273
- source: classId(sourceName),
274
- target: classId(targetName),
275
- type: KEYWORD_TO_TYPE[keyword],
276
- ...(label && { label }),
277
- lineNumber,
278
- });
279
- continue;
280
- }
281
-
282
246
  // Try relationship — arrow syntax
283
247
  const relArrow = trimmed.match(REL_ARROW_RE);
284
248
  if (relArrow) {
@@ -305,8 +269,10 @@ export function parseClassDiagram(
305
269
  const classDecl = trimmed.match(CLASS_DECL_RE);
306
270
  if (classDecl) {
307
271
  const name = classDecl[1];
308
- const modifier = classDecl[2] as ClassModifier | undefined;
309
- const colorName = classDecl[3]?.trim();
272
+ const relKeyword = classDecl[2] as 'extends' | 'implements' | undefined;
273
+ const parentName = classDecl[3];
274
+ const modifier = classDecl[4] as ClassModifier | undefined;
275
+ const colorName = classDecl[5]?.trim();
310
276
  const color = colorName ? resolveColor(colorName, palette) : undefined;
311
277
 
312
278
  const node = getOrCreateClass(name, lineNumber);
@@ -315,6 +281,17 @@ export function parseClassDiagram(
315
281
  // Update line number to the declaration line (may have been created by relationship)
316
282
  node.lineNumber = lineNumber;
317
283
 
284
+ // Inline extends/implements creates a relationship
285
+ if (relKeyword && parentName) {
286
+ getOrCreateClass(parentName, lineNumber);
287
+ result.relationships.push({
288
+ source: classId(name),
289
+ target: classId(parentName),
290
+ type: relKeyword as RelationshipType,
291
+ lineNumber,
292
+ });
293
+ }
294
+
318
295
  currentClass = node;
319
296
  continue;
320
297
  }
@@ -376,9 +353,10 @@ export function looksLikeClassDiagram(content: string): boolean {
376
353
  hasModifier = true;
377
354
  hasClassDecl = true;
378
355
  }
379
- // Check for relationship keywords
380
- if (REL_KEYWORD_RE.test(trimmed)) {
356
+ // Check for inline extends/implements in class declaration
357
+ if (/^[A-Z][A-Za-z0-9_]*\s+(extends|implements)\s+[A-Z]/.test(trimmed)) {
381
358
  hasRelationship = true;
359
+ hasClassDecl = true;
382
360
  }
383
361
  // Check for relationship arrows
384
362
  if (REL_ARROW_RE.test(trimmed)) {
@@ -406,43 +406,42 @@ export function renderClassDiagram(
406
406
  const methods = node.members.filter((m) => m.isMethod);
407
407
 
408
408
  if (isEnum) {
409
- // Enum: all members as values
410
- if (node.members.length > 0) {
411
- // Separator
412
- nodeG.append('line')
413
- .attr('x1', -w / 2)
414
- .attr('y1', yPos)
415
- .attr('x2', w / 2)
416
- .attr('y2', yPos)
417
- .attr('stroke', stroke)
418
- .attr('stroke-width', 0.5)
419
- .attr('stroke-opacity', 0.5);
420
-
421
- let memberY = yPos + COMPARTMENT_PADDING_Y;
422
- for (const member of node.members) {
423
- nodeG.append('text')
424
- .attr('x', -w / 2 + MEMBER_PADDING_X)
425
- .attr('y', memberY + MEMBER_LINE_HEIGHT / 2)
426
- .attr('dominant-baseline', 'central')
427
- .attr('fill', palette.text)
428
- .attr('font-size', MEMBER_FONT_SIZE)
429
- .text(member.name);
430
- memberY += MEMBER_LINE_HEIGHT;
431
- }
409
+ // Enum: single values compartment
410
+ // Separator
411
+ nodeG.append('line')
412
+ .attr('x1', -w / 2)
413
+ .attr('y1', yPos)
414
+ .attr('x2', w / 2)
415
+ .attr('y2', yPos)
416
+ .attr('stroke', stroke)
417
+ .attr('stroke-width', 0.5)
418
+ .attr('stroke-opacity', 0.5);
419
+
420
+ let memberY = yPos + COMPARTMENT_PADDING_Y;
421
+ for (const member of node.members) {
422
+ nodeG.append('text')
423
+ .attr('x', -w / 2 + MEMBER_PADDING_X)
424
+ .attr('y', memberY + MEMBER_LINE_HEIGHT / 2)
425
+ .attr('dominant-baseline', 'central')
426
+ .attr('fill', palette.text)
427
+ .attr('font-size', MEMBER_FONT_SIZE)
428
+ .text(member.name);
429
+ memberY += MEMBER_LINE_HEIGHT;
432
430
  }
433
431
  } else {
434
- // Fields compartment
435
- if (fields.length > 0) {
436
- // Separator
437
- nodeG.append('line')
438
- .attr('x1', -w / 2)
439
- .attr('y1', yPos)
440
- .attr('x2', w / 2)
441
- .attr('y2', yPos)
442
- .attr('stroke', stroke)
443
- .attr('stroke-width', 0.5)
444
- .attr('stroke-opacity', 0.5);
432
+ // UML 3-compartment layout: always show both separators
433
+
434
+ // Fields separator
435
+ nodeG.append('line')
436
+ .attr('x1', -w / 2)
437
+ .attr('y1', yPos)
438
+ .attr('x2', w / 2)
439
+ .attr('y2', yPos)
440
+ .attr('stroke', stroke)
441
+ .attr('stroke-width', 0.5)
442
+ .attr('stroke-opacity', 0.5);
445
443
 
444
+ if (fields.length > 0) {
446
445
  let memberY = yPos + COMPARTMENT_PADDING_Y;
447
446
  for (const field of fields) {
448
447
  const vis = visibilitySymbol(field.visibility);
@@ -463,21 +462,20 @@ export function renderClassDiagram(
463
462
 
464
463
  memberY += MEMBER_LINE_HEIGHT;
465
464
  }
466
- yPos += node.fieldsHeight;
467
465
  }
466
+ yPos += node.fieldsHeight;
467
+
468
+ // Methods separator
469
+ nodeG.append('line')
470
+ .attr('x1', -w / 2)
471
+ .attr('y1', yPos)
472
+ .attr('x2', w / 2)
473
+ .attr('y2', yPos)
474
+ .attr('stroke', stroke)
475
+ .attr('stroke-width', 0.5)
476
+ .attr('stroke-opacity', 0.5);
468
477
 
469
- // Methods compartment
470
478
  if (methods.length > 0) {
471
- // Separator
472
- nodeG.append('line')
473
- .attr('x1', -w / 2)
474
- .attr('y1', yPos)
475
- .attr('x2', w / 2)
476
- .attr('y2', yPos)
477
- .attr('stroke', stroke)
478
- .attr('stroke-width', 0.5)
479
- .attr('stroke-opacity', 0.5);
480
-
481
479
  let memberY = yPos + COMPARTMENT_PADDING_Y;
482
480
  for (const method of methods) {
483
481
  const vis = visibilitySymbol(method.visibility);