From 0aa09bfca597a5302f2b35a9ba91fe0403da928d Mon Sep 17 00:00:00 2001 From: Justin Greywolf Date: Tue, 30 May 2023 10:43:44 -0700 Subject: [PATCH] Create new type for member handling --- cSpell.json | 3 + .../rendering/classDiagram-v2.spec.js | 59 ++-- .../class/classDiagram-styles.spec.js | 4 +- .../src/diagrams/class/classDiagram-v2.ts | 2 +- .../src/diagrams/class/classDiagram.spec.ts | 284 +++++++++--------- .../src/diagrams/class/classDiagram.ts | 2 +- .../src/diagrams/class/classParser.spec.ts | 12 +- .../class/{classDb.ts => classParser.ts} | 1 + .../src/diagrams/class/classRenderer-v2.ts | 14 +- .../src/diagrams/class/classTypes.spec.ts | 211 +++++++++++++ .../mermaid/src/diagrams/class/classTypes.ts | 86 ++++++ .../diagrams/class/parser/classDiagram.jison | 34 +-- .../mermaid/src/diagrams/class/svgDraw.js | 95 +++--- .../src/diagrams/class/svgDraw.spec.js | 41 ++- .../mermaid/src/diagrams/common/common.ts | 6 +- 15 files changed, 589 insertions(+), 265 deletions(-) rename packages/mermaid/src/diagrams/class/{classDb.ts => classParser.ts} (99%) create mode 100644 packages/mermaid/src/diagrams/class/classTypes.spec.ts diff --git a/cSpell.json b/cSpell.json index 154d01a99..e72a7bb2b 100644 --- a/cSpell.json +++ b/cSpell.json @@ -37,7 +37,10 @@ "docsy", "doku", "dompurify", + "dont", + "doublecircle", "edgechromium", + "elems", "elkjs", "faber", "flatmap", diff --git a/cypress/integration/rendering/classDiagram-v2.spec.js b/cypress/integration/rendering/classDiagram-v2.spec.js index 2e7a1cbd7..40d2f5cd7 100644 --- a/cypress/integration/rendering/classDiagram-v2.spec.js +++ b/cypress/integration/rendering/classDiagram-v2.spec.js @@ -386,30 +386,6 @@ describe('Class diagram V2', () => { { logLevel: 1, flowchart: { htmlLabels: false } } ); }); - - it('18: should handle the direction statement with LR', () => { - imgSnapshotTest( - ` - classDiagram - direction LR - class Student { - -idCard : IdCard - } - class IdCard{ - -id : int - -name : string - } - class Bike{ - -id : int - -name : string - } - Student "1" --o "1" IdCard : carries - Student "1" --o "1" Bike : rides - - `, - { logLevel: 1, flowchart: { htmlLabels: false } } - ); - }); it('17a: should handle the direction statement with BT', () => { imgSnapshotTest( ` @@ -457,7 +433,31 @@ describe('Class diagram V2', () => { ); }); - it('18: should render a simple class diagram with notes', () => { + it('18a: should handle the direction statement with LR', () => { + imgSnapshotTest( + ` + classDiagram + direction LR + class Student { + -idCard : IdCard + } + class IdCard{ + -id : int + -name : string + } + class Bike{ + -id : int + -name : string + } + Student "1" --o "1" IdCard : carries + Student "1" --o "1" Bike : rides + + `, + { logLevel: 1, flowchart: { htmlLabels: false } } + ); + }); + + it('18b: should render a simple class diagram with notes', () => { imgSnapshotTest( ` classDiagram-v2 @@ -562,4 +562,13 @@ class C13["With Città foreign language"] ` ); }); + it('should render a simple class diagram with no members', () => { + imgSnapshotTest( + ` + classDiagram-v2 + class Class10 + `, + { logLevel: 1, flowchart: { htmlLabels: false } } + ); + }); }); diff --git a/packages/mermaid/src/diagrams/class/classDiagram-styles.spec.js b/packages/mermaid/src/diagrams/class/classDiagram-styles.spec.js index a693fbbea..fe01854b0 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram-styles.spec.js +++ b/packages/mermaid/src/diagrams/class/classDiagram-styles.spec.js @@ -1,10 +1,10 @@ import { parser } from './parser/classDiagram.jison'; -import classDb from './classDb.js'; +import classParser from './classParser.js'; describe('class diagram, ', function () { describe('when parsing data from a classDiagram it', function () { beforeEach(function () { - parser.yy = classDb; + parser.yy = classParser; parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/class/classDiagram-v2.ts b/packages/mermaid/src/diagrams/class/classDiagram-v2.ts index 5b952627c..c40d36c53 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram-v2.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram-v2.ts @@ -1,7 +1,7 @@ import { DiagramDefinition } from '../../diagram-api/types.js'; // @ts-ignore: TODO Fix ts errors import parser from './parser/classDiagram.jison'; -import db from './classDb.js'; +import db from './classParser.js'; import styles from './styles.js'; import renderer from './classRenderer-v2.js'; diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts index a43ed2fcd..9da413b6a 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts @@ -1,14 +1,14 @@ // @ts-expect-error Jison doesn't export types import { parser } from './parser/classDiagram.jison'; -import classDb from './classDb.js'; +import classParser from './classParser.js'; import { vi, describe, it, expect } from 'vitest'; const spyOn = vi.spyOn; describe('given a basic class diagram, ', function () { describe('when parsing class definition', function () { beforeEach(function () { - classDb.clear(); - parser.yy = classDb; + classParser.clear(); + parser.yy = classParser; }); it('should handle accTitle and accDescr', function () { const str = `classDiagram @@ -54,7 +54,7 @@ describe('given a basic class diagram, ', function () { const str = 'classDiagram\n' + 'class Ca-r'; parser.parse(str); - const actual = classDb.getClass('Ca-r'); + const actual = classParser.getClass('Ca-r'); expect(actual.label).toBe('Ca-r'); }); @@ -102,7 +102,7 @@ describe('given a basic class diagram, ', function () { parser.parse(str); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); }); @@ -114,9 +114,9 @@ describe('given a basic class diagram, ', function () { parser.parse(str); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); - const c2 = classDb.getClass('C2'); + const c2 = classParser.getClass('C2'); expect(c2.label).toBe('Class 2 with chars @?'); }); @@ -124,7 +124,7 @@ describe('given a basic class diagram, ', function () { const str = 'classDiagram\n' + 'class C1["Class 1 with text label"]\n' + 'C1: member1'; parser.parse(str); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.members.length).toBe(1); expect(c1.members[0]).toBe('member1'); @@ -139,7 +139,7 @@ describe('given a basic class diagram, ', function () { parser.parse(str); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.members.length).toBe(1); expect(c1.members[0]).toBe('int member1'); @@ -152,7 +152,7 @@ describe('given a basic class diagram, ', function () { parser.parse(str); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.cssClasses[0]).toBe('styleClass'); }); @@ -166,7 +166,7 @@ describe('given a basic class diagram, ', function () { parser.parse(str); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.members[0]).toBe('int member1'); expect(c1.cssClasses[0]).toBe('styleClass'); @@ -182,11 +182,11 @@ describe('given a basic class diagram, ', function () { parser.parse(str); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.cssClasses[0]).toBe('styleClass'); - const c2 = classDb.getClass('C2'); + const c2 = classParser.getClass('C2'); expect(c2.label).toBe('Long long long long long long long long long long label'); expect(c2.cssClasses[0]).toBe('styleClass'); }); @@ -199,11 +199,11 @@ describe('given a basic class diagram, ', function () { parser.parse(str); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.cssClasses[0]).toBe('styleClass1'); - const c2 = classDb.getClass('C2'); + const c2 = classParser.getClass('C2'); expect(c2.label).toBe('Class 2 !@#$%^&*() label'); expect(c2.cssClasses[0]).toBe('styleClass2'); }); @@ -214,13 +214,13 @@ class C1["Class with text label"] class C2["Class with text label"] class C3["Class with text label"]`); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class with text label'); - const c2 = classDb.getClass('C2'); + const c2 = classParser.getClass('C2'); expect(c2.label).toBe('Class with text label'); - const c3 = classDb.getClass('C3'); + const c3 = classParser.getClass('C3'); expect(c3.label).toBe('Class with text label'); }); @@ -240,19 +240,19 @@ class C11["With ' single quote"] class C12["With ~!@#$%^&*()_+=-/?"] class C13["With Città foreign language"] `); - expect(classDb.getClass('C1').label).toBe('OneWord'); - expect(classDb.getClass('C2').label).toBe('With, Comma'); - expect(classDb.getClass('C3').label).toBe('With (Brackets)'); - expect(classDb.getClass('C4').label).toBe('With [Brackets]'); - expect(classDb.getClass('C5').label).toBe('With {Brackets}'); - expect(classDb.getClass('C6').label).toBe(' '); - expect(classDb.getClass('C7').label).toBe('With 1 number'); - expect(classDb.getClass('C8').label).toBe('With . period...'); - expect(classDb.getClass('C9').label).toBe('With - dash'); - expect(classDb.getClass('C10').label).toBe('With _ underscore'); - expect(classDb.getClass('C11').label).toBe("With ' single quote"); - expect(classDb.getClass('C12').label).toBe('With ~!@#$%^&*()_+=-/?'); - expect(classDb.getClass('C13').label).toBe('With Città foreign language'); + expect(classParser.getClass('C1').label).toBe('OneWord'); + expect(classParser.getClass('C2').label).toBe('With, Comma'); + expect(classParser.getClass('C3').label).toBe('With (Brackets)'); + expect(classParser.getClass('C4').label).toBe('With [Brackets]'); + expect(classParser.getClass('C5').label).toBe('With {Brackets}'); + expect(classParser.getClass('C6').label).toBe(' '); + expect(classParser.getClass('C7').label).toBe('With 1 number'); + expect(classParser.getClass('C8').label).toBe('With . period...'); + expect(classParser.getClass('C9').label).toBe('With - dash'); + expect(classParser.getClass('C10').label).toBe('With _ underscore'); + expect(classParser.getClass('C11').label).toBe("With ' single quote"); + expect(classParser.getClass('C12').label).toBe('With ~!@#$%^&*()_+=-/?'); + expect(classParser.getClass('C13').label).toBe('With Città foreign language'); }); it('should handle "note for"', function () { @@ -268,8 +268,8 @@ class C13["With Città foreign language"] describe('when parsing class defined in brackets', function () { beforeEach(function () { - classDb.clear(); - parser.yy = classDb; + classParser.clear(); + parser.yy = classParser; }); it('should handle member definitions', function () { @@ -334,7 +334,7 @@ class C13["With Città foreign language"] const str = 'classDiagram\n' + 'class C1["Class 1 with text label"] {\n' + '+member1\n' + '}'; parser.parse(str); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.members.length).toBe(1); expect(c1.members[0]).toBe('+member1'); @@ -349,7 +349,7 @@ class C13["With Città foreign language"] '}'; parser.parse(str); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.members.length).toBe(1); expect(c1.members[0]).toBe('+member1'); @@ -360,8 +360,8 @@ class C13["With Città foreign language"] describe('when parsing comments', function () { beforeEach(function () { - classDb.clear(); - parser.yy = classDb; + classParser.clear(); + parser.yy = classParser; }); it('should handle comments at the start', function () { @@ -450,16 +450,16 @@ foo() describe('when parsing click statements', function () { beforeEach(function () { - classDb.clear(); - parser.yy = classDb; + classParser.clear(); + parser.yy = classParser; }); it('should handle href link', function () { - spyOn(classDb, 'setLink'); + spyOn(classParser, 'setLink'); const str = 'classDiagram\n' + 'class Class1 \n' + 'click Class1 href "google.com" '; parser.parse(str); - expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com'); + expect(classParser.setLink).toHaveBeenCalledWith('Class1', 'google.com'); const actual = parser.yy.getClass('Class1'); expect(actual.link).toBe('google.com'); @@ -467,14 +467,14 @@ foo() }); it('should handle href link with tooltip', function () { - spyOn(classDb, 'setLink'); - spyOn(classDb, 'setTooltip'); + spyOn(classParser, 'setLink'); + spyOn(classParser, 'setTooltip'); const str = 'classDiagram\n' + 'class Class1 \n' + 'click Class1 href "google.com" "A Tooltip" '; parser.parse(str); - expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com'); + expect(classParser.setLink).toHaveBeenCalledWith('Class1', 'google.com'); const actual = parser.yy.getClass('Class1'); expect(actual.link).toBe('google.com'); @@ -483,8 +483,8 @@ foo() }); it('should handle href link with tooltip and target', function () { - spyOn(classDb, 'setLink'); - spyOn(classDb, 'setTooltip'); + spyOn(classParser, 'setLink'); + spyOn(classParser, 'setTooltip'); const str = 'classDiagram\n' + 'class Class1\n' + @@ -492,8 +492,8 @@ foo() 'click Class1 href "google.com" "A tooltip" _self'; parser.parse(str); - expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self'); - expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip'); + expect(classParser.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self'); + expect(classParser.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip'); const actual = parser.yy.getClass('Class1'); expect(actual.link).toBe('google.com'); @@ -502,30 +502,30 @@ foo() }); it('should handle function call', function () { - spyOn(classDb, 'setClickEvent'); + spyOn(classParser, 'setClickEvent'); const str = 'classDiagram\n' + 'class Class1 \n' + 'click Class1 call functionCall() '; parser.parse(str); - expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); + expect(classParser.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); }); it('should handle function call with tooltip', function () { - spyOn(classDb, 'setClickEvent'); - spyOn(classDb, 'setTooltip'); + spyOn(classParser, 'setClickEvent'); + spyOn(classParser, 'setTooltip'); const str = 'classDiagram\n' + 'class Class1 \n' + 'click Class1 call functionCall() "A Tooltip" '; parser.parse(str); - expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); - expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A Tooltip'); + expect(classParser.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); + expect(classParser.setTooltip).toHaveBeenCalledWith('Class1', 'A Tooltip'); }); it('should handle function call with an arbitrary number of args', function () { - spyOn(classDb, 'setClickEvent'); + spyOn(classParser, 'setClickEvent'); const str = 'classDiagram\n' + 'class Class1\n' + @@ -533,7 +533,7 @@ foo() 'click Class1 call functionCall(test, test1, test2)'; parser.parse(str); - expect(classDb.setClickEvent).toHaveBeenCalledWith( + expect(classParser.setClickEvent).toHaveBeenCalledWith( 'Class1', 'functionCall', 'test, test1, test2' @@ -541,8 +541,8 @@ foo() }); it('should handle function call with an arbitrary number of args and tooltip', function () { - spyOn(classDb, 'setClickEvent'); - spyOn(classDb, 'setTooltip'); + spyOn(classParser, 'setClickEvent'); + spyOn(classParser, 'setTooltip'); const str = 'classDiagram\n' + 'class Class1\n' + @@ -550,19 +550,19 @@ foo() 'click Class1 call functionCall("test0", test1, test2) "A Tooltip"'; parser.parse(str); - expect(classDb.setClickEvent).toHaveBeenCalledWith( + expect(classParser.setClickEvent).toHaveBeenCalledWith( 'Class1', 'functionCall', '"test0", test1, test2' ); - expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A Tooltip'); + expect(classParser.setTooltip).toHaveBeenCalledWith('Class1', 'A Tooltip'); }); }); describe('when parsing annotations', function () { beforeEach(function () { - classDb.clear(); - parser.yy = classDb; + classParser.clear(); + parser.yy = classParser; }); it('should handle class annotations', function () { @@ -625,8 +625,8 @@ foo() describe('given a class diagram with members and methods ', function () { describe('when parsing members', function () { beforeEach(function () { - classDb.clear(); - parser.yy = classDb; + classParser.clear(); + parser.yy = classParser; }); it('should handle simple member declaration', function () { @@ -670,8 +670,8 @@ describe('given a class diagram with members and methods ', function () { describe('when parsing method definition', function () { beforeEach(function () { - classDb.clear(); - parser.yy = classDb; + classParser.clear(); + parser.yy = classParser; }); it('should handle method definition', function () { @@ -753,8 +753,8 @@ describe('given a class diagram with members and methods ', function () { describe('given a class diagram with generics, ', function () { describe('when parsing valid generic classes', function () { beforeEach(function () { - classDb.clear(); - parser.yy = classDb; + classParser.clear(); + parser.yy = classParser; }); it('should handle generic class', function () { @@ -851,8 +851,8 @@ foo() describe('when parsing invalid generic classes', function () { beforeEach(function () { - classDb.clear(); - parser.yy = classDb; + classParser.clear(); + parser.yy = classParser; }); it('should break when another `{`is encountered before closing the first one while defining generic class with brackets', function () { @@ -900,8 +900,8 @@ foo() describe('given a class diagram with relationships, ', function () { describe('when parsing basic relationships', function () { beforeEach(function () { - classDb.clear(); - parser.yy = classDb; + classParser.clear(); + parser.yy = classParser; }); it('should handle all basic relationships', function () { @@ -938,9 +938,9 @@ describe('given a class diagram with relationships, ', function () { expect(parser.yy.getClass('Class1').id).toBe('Class1'); expect(parser.yy.getClass('Class1').type).toBe('T'); expect(parser.yy.getClass('Class02').id).toBe('Class02'); - expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION); + expect(relations[0].relation.type1).toBe(classParser.relationType.EXTENSION); expect(relations[0].relation.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + expect(relations[0].relation.lineType).toBe(classParser.lineType.LINE); }); it('should handle relationships with labels', function () { @@ -963,9 +963,9 @@ describe('given a class diagram with relationships, ', function () { expect(parser.yy.getClass('Class1').id).toBe('Class1'); expect(parser.yy.getClass('Class02').id).toBe('Class02'); - expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION); + expect(relations[0].relation.type1).toBe(classParser.relationType.EXTENSION); expect(relations[0].relation.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + expect(relations[0].relation.lineType).toBe(classParser.lineType.LINE); }); it('should handle relation definition of different types and directions', function () { @@ -1010,9 +1010,9 @@ describe('given a class diagram with relationships, ', function () { expect(parser.yy.getClass('Class1').id).toBe('Class1'); expect(parser.yy.getClass('Class02').id).toBe('Class02'); - expect(relations[0].relation.type1).toBe(classDb.relationType.AGGREGATION); + expect(relations[0].relation.type1).toBe(classParser.relationType.AGGREGATION); expect(relations[0].relation.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.DOTTED_LINE); + expect(relations[0].relation.lineType).toBe(classParser.lineType.DOTTED_LINE); }); it('should handle relation definitions COMPOSITION on both sides', function () { @@ -1024,9 +1024,9 @@ describe('given a class diagram with relationships, ', function () { expect(parser.yy.getClass('Class1').id).toBe('Class1'); expect(parser.yy.getClass('Class02').id).toBe('Class02'); - expect(relations[0].relation.type1).toBe(classDb.relationType.COMPOSITION); - expect(relations[0].relation.type2).toBe(classDb.relationType.COMPOSITION); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + expect(relations[0].relation.type1).toBe(classParser.relationType.COMPOSITION); + expect(relations[0].relation.type2).toBe(classParser.relationType.COMPOSITION); + expect(relations[0].relation.lineType).toBe(classParser.lineType.LINE); }); it('should handle relation definitions with no types', function () { @@ -1040,7 +1040,7 @@ describe('given a class diagram with relationships, ', function () { expect(parser.yy.getClass('Class02').id).toBe('Class02'); expect(relations[0].relation.type1).toBe('none'); expect(relations[0].relation.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + expect(relations[0].relation.lineType).toBe(classParser.lineType.LINE); }); it('should handle relation definitions with type only on right side', function () { @@ -1053,8 +1053,8 @@ describe('given a class diagram with relationships, ', function () { expect(parser.yy.getClass('Class1').id).toBe('Class1'); expect(parser.yy.getClass('Class02').id).toBe('Class02'); expect(relations[0].relation.type1).toBe('none'); - expect(relations[0].relation.type2).toBe(classDb.relationType.EXTENSION); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + expect(relations[0].relation.type2).toBe(classParser.relationType.EXTENSION); + expect(relations[0].relation.lineType).toBe(classParser.lineType.LINE); }); it('should handle multiple classes and relation definitions', function () { @@ -1075,12 +1075,12 @@ describe('given a class diagram with relationships, ', function () { expect(relations.length).toBe(5); - expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION); + expect(relations[0].relation.type1).toBe(classParser.relationType.EXTENSION); expect(relations[0].relation.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + expect(relations[0].relation.lineType).toBe(classParser.lineType.LINE); expect(relations[3].relation.type1).toBe('none'); expect(relations[3].relation.type2).toBe('none'); - expect(relations[3].relation.lineType).toBe(classDb.lineType.DOTTED_LINE); + expect(relations[3].relation.lineType).toBe(classParser.lineType.DOTTED_LINE); }); it('should handle generic class with relation definitions', function () { @@ -1093,9 +1093,9 @@ describe('given a class diagram with relationships, ', function () { expect(parser.yy.getClass('Class01').id).toBe('Class01'); expect(parser.yy.getClass('Class01').type).toBe('T'); expect(parser.yy.getClass('Class02').id).toBe('Class02'); - expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION); + expect(relations[0].relation.type1).toBe(classParser.relationType.EXTENSION); expect(relations[0].relation.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + expect(relations[0].relation.lineType).toBe(classParser.lineType.LINE); }); it('should handle class annotations', function () { @@ -1254,8 +1254,8 @@ describe('given a class diagram with relationships, ', function () { }); it('should associate click and href link with tooltip and target appropriately', function () { - spyOn(classDb, 'setLink'); - spyOn(classDb, 'setTooltip'); + spyOn(classParser, 'setLink'); + spyOn(classParser, 'setTooltip'); const str = 'classDiagram\n' + 'class Class1\n' + @@ -1263,12 +1263,12 @@ describe('given a class diagram with relationships, ', function () { 'click Class1 href "google.com" "A tooltip" _self'; parser.parse(str); - expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self'); - expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip'); + expect(classParser.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self'); + expect(classParser.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip'); }); it('should associate click and href link appropriately', function () { - spyOn(classDb, 'setLink'); + spyOn(classParser, 'setLink'); const str = 'classDiagram\n' + 'class Class1\n' + @@ -1276,11 +1276,11 @@ describe('given a class diagram with relationships, ', function () { 'click Class1 href "google.com"'; parser.parse(str); - expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com'); + expect(classParser.setLink).toHaveBeenCalledWith('Class1', 'google.com'); }); it('should associate click and href link with target appropriately', function () { - spyOn(classDb, 'setLink'); + spyOn(classParser, 'setLink'); const str = 'classDiagram\n' + 'class Class1\n' + @@ -1288,12 +1288,12 @@ describe('given a class diagram with relationships, ', function () { 'click Class1 href "google.com" _self'; parser.parse(str); - expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self'); + expect(classParser.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self'); }); it('should associate link appropriately', function () { - spyOn(classDb, 'setLink'); - spyOn(classDb, 'setTooltip'); + spyOn(classParser, 'setLink'); + spyOn(classParser, 'setTooltip'); const str = 'classDiagram\n' + 'class Class1\n' + @@ -1301,12 +1301,12 @@ describe('given a class diagram with relationships, ', function () { 'link Class1 "google.com" "A tooltip" _self'; parser.parse(str); - expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self'); - expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip'); + expect(classParser.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self'); + expect(classParser.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip'); }); it('should associate callback appropriately', function () { - spyOn(classDb, 'setClickEvent'); + spyOn(classParser, 'setClickEvent'); const str = 'classDiagram\n' + 'class Class1\n' + @@ -1314,11 +1314,11 @@ describe('given a class diagram with relationships, ', function () { 'callback Class1 "functionCall"'; parser.parse(str); - expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); + expect(classParser.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); }); it('should associate click and call callback appropriately', function () { - spyOn(classDb, 'setClickEvent'); + spyOn(classParser, 'setClickEvent'); const str = 'classDiagram\n' + 'class Class1\n' + @@ -1326,11 +1326,11 @@ describe('given a class diagram with relationships, ', function () { 'click Class1 call functionCall()'; parser.parse(str); - expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); + expect(classParser.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); }); it('should associate callback appropriately with an arbitrary number of args', function () { - spyOn(classDb, 'setClickEvent'); + spyOn(classParser, 'setClickEvent'); const str = 'classDiagram\n' + 'class Class1\n' + @@ -1338,7 +1338,7 @@ describe('given a class diagram with relationships, ', function () { 'click Class1 call functionCall("test0", test1, test2)'; parser.parse(str); - expect(classDb.setClickEvent).toHaveBeenCalledWith( + expect(classParser.setClickEvent).toHaveBeenCalledWith( 'Class1', 'functionCall', '"test0", test1, test2' @@ -1346,8 +1346,8 @@ describe('given a class diagram with relationships, ', function () { }); it('should associate callback with tooltip', function () { - spyOn(classDb, 'setClickEvent'); - spyOn(classDb, 'setTooltip'); + spyOn(classParser, 'setClickEvent'); + spyOn(classParser, 'setTooltip'); const str = 'classDiagram\n' + 'class Class1\n' + @@ -1355,8 +1355,8 @@ describe('given a class diagram with relationships, ', function () { 'click Class1 call functionCall() "A tooltip"'; parser.parse(str); - expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); - expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip'); + expect(classParser.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); + expect(classParser.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip'); }); it('should add classes namespaces', function () { @@ -1381,7 +1381,7 @@ class Class2 describe('when parsing classDiagram with text labels', () => { beforeEach(function () { - parser.yy = classDb; + parser.yy = classParser; parser.yy.clear(); }); @@ -1390,9 +1390,9 @@ class Class2 class C1["Class 1 with text label"] C1 --> C2 `); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); - const c2 = classDb.getClass('C2'); + const c2 = classParser.getClass('C2'); expect(c2.label).toBe('C2'); }); @@ -1402,9 +1402,9 @@ class Class2 class C2["Class 2 with chars @?"] C1 --> C2 `); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); - const c2 = classDb.getClass('C2'); + const c2 = classParser.getClass('C2'); expect(c2.label).toBe('Class 2 with chars @?'); }); @@ -1415,12 +1415,12 @@ class Class2 } C1 --> C2 `); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.members.length).toBe(1); expect(c1.members[0]).toBe('+member1'); - const c2 = classDb.getClass('C2'); + const c2 = classParser.getClass('C2'); expect(c2.label).toBe('C2'); }); @@ -1432,14 +1432,14 @@ class Class2 } C1 --> C2 `); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.members.length).toBe(1); expect(c1.members[0]).toBe('+member1'); expect(c1.annotations.length).toBe(1); expect(c1.annotations[0]).toBe('interface'); - const c2 = classDb.getClass('C2'); + const c2 = classParser.getClass('C2'); expect(c2.label).toBe('C2'); }); @@ -1451,7 +1451,7 @@ class C1["Class 1 with text label"]:::styleClass { C1 --> C2 `); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.cssClasses.length).toBe(1); expect(c1.members[0]).toBe('+member1'); @@ -1467,7 +1467,7 @@ C1 --> C2 cssClass "C1" styleClass `); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.cssClasses.length).toBe(1); expect(c1.members[0]).toBe('+member1'); @@ -1484,12 +1484,12 @@ C1 --> C2 cssClass "C1,C2" styleClass `); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.cssClasses.length).toBe(1); expect(c1.cssClasses[0]).toBe('styleClass'); - const c2 = classDb.getClass('C2'); + const c2 = classParser.getClass('C2'); expect(c2.label).toBe('Long long long long long long long long long long label'); expect(c2.cssClasses.length).toBe(1); expect(c2.cssClasses[0]).toBe('styleClass'); @@ -1504,12 +1504,12 @@ class C2["Class 2 !@#$%^&*() label"]:::styleClass2 C1 --> C2 `); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); expect(c1.cssClasses.length).toBe(1); expect(c1.cssClasses[0]).toBe('styleClass1'); - const c2 = classDb.getClass('C2'); + const c2 = classParser.getClass('C2'); expect(c2.label).toBe('Class 2 !@#$%^&*() label'); expect(c2.cssClasses.length).toBe(1); expect(c2.cssClasses[0]).toBe('styleClass2'); @@ -1524,13 +1524,13 @@ C1 --> C2 C3 ..> C2 `); - const c1 = classDb.getClass('C1'); + const c1 = classParser.getClass('C1'); expect(c1.label).toBe('Class with text label'); - const c2 = classDb.getClass('C2'); + const c2 = classParser.getClass('C2'); expect(c2.label).toBe('Class with text label'); - const c3 = classDb.getClass('C3'); + const c3 = classParser.getClass('C3'); expect(c3.label).toBe('Class with text label'); }); @@ -1550,19 +1550,19 @@ class C11["With ' single quote"] class C12["With ~!@#$%^&*()_+=-/?"] class C13["With Città foreign language"] `); - expect(classDb.getClass('C1').label).toBe('OneWord'); - expect(classDb.getClass('C2').label).toBe('With, Comma'); - expect(classDb.getClass('C3').label).toBe('With (Brackets)'); - expect(classDb.getClass('C4').label).toBe('With [Brackets]'); - expect(classDb.getClass('C5').label).toBe('With {Brackets}'); - expect(classDb.getClass('C6').label).toBe(' '); - expect(classDb.getClass('C7').label).toBe('With 1 number'); - expect(classDb.getClass('C8').label).toBe('With . period...'); - expect(classDb.getClass('C9').label).toBe('With - dash'); - expect(classDb.getClass('C10').label).toBe('With _ underscore'); - expect(classDb.getClass('C11').label).toBe("With ' single quote"); - expect(classDb.getClass('C12').label).toBe('With ~!@#$%^&*()_+=-/?'); - expect(classDb.getClass('C13').label).toBe('With Città foreign language'); + expect(classParser.getClass('C1').label).toBe('OneWord'); + expect(classParser.getClass('C2').label).toBe('With, Comma'); + expect(classParser.getClass('C3').label).toBe('With (Brackets)'); + expect(classParser.getClass('C4').label).toBe('With [Brackets]'); + expect(classParser.getClass('C5').label).toBe('With {Brackets}'); + expect(classParser.getClass('C6').label).toBe(' '); + expect(classParser.getClass('C7').label).toBe('With 1 number'); + expect(classParser.getClass('C8').label).toBe('With . period...'); + expect(classParser.getClass('C9').label).toBe('With - dash'); + expect(classParser.getClass('C10').label).toBe('With _ underscore'); + expect(classParser.getClass('C11').label).toBe("With ' single quote"); + expect(classParser.getClass('C12').label).toBe('With ~!@#$%^&*()_+=-/?'); + expect(classParser.getClass('C13').label).toBe('With Città foreign language'); }); }); }); diff --git a/packages/mermaid/src/diagrams/class/classDiagram.ts b/packages/mermaid/src/diagrams/class/classDiagram.ts index 0d2a246b4..f9ae8c709 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.ts @@ -1,7 +1,7 @@ import { DiagramDefinition } from '../../diagram-api/types.js'; // @ts-ignore: TODO Fix ts errors import parser from './parser/classDiagram.jison'; -import db from './classDb.js'; +import db from './classParser.js'; import styles from './styles.js'; import renderer from './classRenderer.js'; diff --git a/packages/mermaid/src/diagrams/class/classParser.spec.ts b/packages/mermaid/src/diagrams/class/classParser.spec.ts index c479b8272..ed402c28f 100644 --- a/packages/mermaid/src/diagrams/class/classParser.spec.ts +++ b/packages/mermaid/src/diagrams/class/classParser.spec.ts @@ -1,5 +1,5 @@ import { setConfig } from '../../config.js'; -import classDB from './classDb.js'; +import classParser from './classParser.js'; // @ts-ignore - no types in jison import classDiagram from './parser/classDiagram.jison'; @@ -9,7 +9,7 @@ setConfig({ describe('when parsing class diagram', function () { beforeEach(function () { - classDiagram.parser.yy = classDB; + classDiagram.parser.yy = classParser; classDiagram.parser.yy.clear(); }); @@ -30,8 +30,8 @@ describe('when parsing class diagram', function () { Student "1" --o "1" IdCard : carries Student "1" --o "1" Bike : rides`); - expect(Object.keys(classDB.getClasses()).length).toBe(3); - expect(classDB.getClasses().Student).toMatchInlineSnapshot(` + expect(Object.keys(classParser.getClasses()).length).toBe(3); + expect(classParser.getClasses().Student).toMatchInlineSnapshot(` { "annotations": [], "cssClasses": [], @@ -45,8 +45,8 @@ describe('when parsing class diagram', function () { "type": "", } `); - expect(classDB.getRelations().length).toBe(2); - expect(classDB.getRelations()).toMatchInlineSnapshot(` + expect(classParser.getRelations().length).toBe(2); + expect(classParser.getRelations()).toMatchInlineSnapshot(` [ { "id1": "Student", diff --git a/packages/mermaid/src/diagrams/class/classDb.ts b/packages/mermaid/src/diagrams/class/classParser.ts similarity index 99% rename from packages/mermaid/src/diagrams/class/classDb.ts rename to packages/mermaid/src/diagrams/class/classParser.ts index d9e17db54..aa2d02975 100644 --- a/packages/mermaid/src/diagrams/class/classDb.ts +++ b/packages/mermaid/src/diagrams/class/classParser.ts @@ -255,6 +255,7 @@ export const getTooltip = function (id: string, namespace?: string) { return classes[id].tooltip; }; + /** * Called by parser when a link is found. Adds the URL to the vertex data. * diff --git a/packages/mermaid/src/diagrams/class/classRenderer-v2.ts b/packages/mermaid/src/diagrams/class/classRenderer-v2.ts index 352002242..e648854d3 100644 --- a/packages/mermaid/src/diagrams/class/classRenderer-v2.ts +++ b/packages/mermaid/src/diagrams/class/classRenderer-v2.ts @@ -92,7 +92,6 @@ export const addClasses = function ( log.info('keys:', keys); log.info(classes); - // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition keys.forEach(function (id) { const vertex = classes[id]; @@ -157,24 +156,17 @@ export const addNotes = function ( ) { log.info(notes); - // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition notes.forEach(function (note, i) { const vertex = note; - /** - * Variable for storing the classes for the vertex - * - */ const cssNoteStr = ''; const styles = { labelStyle: '', style: '' }; - // Use vertex id as text in the box if no text is provided by the graph definition const vertexText = vertex.text; const radius = 0; const shape = 'note'; - // Add the node const node = { labelStyle: styles.labelStyle, shape: shape, @@ -302,7 +294,7 @@ export const setConf = function (cnf: any) { }; /** - * Draws a flowchart in the tag with id: id based on the graph definition in text. + * Draws a class diagram in the tag with id: id based on the definition in text. * * @param text - * @param id - @@ -353,9 +345,7 @@ export const draw = async function (text: string, id: string, _version: string, } const root = securityLevel === 'sandbox' - ? // @ts-ignore Ignore type error for now - - select(sandboxElement.nodes()[0].contentDocument.body) + ? select(sandboxElement.nodes()[0].contentDocument.body) : select('body'); // @ts-ignore Ignore type error for now const svg = root.select(`[id="${id}"]`); diff --git a/packages/mermaid/src/diagrams/class/classTypes.spec.ts b/packages/mermaid/src/diagrams/class/classTypes.spec.ts new file mode 100644 index 000000000..b5ff157c7 --- /dev/null +++ b/packages/mermaid/src/diagrams/class/classTypes.spec.ts @@ -0,0 +1,211 @@ +import { ClassMember } from './classTypes.js'; +import { vi, describe, it, expect } from 'vitest'; +const spyOn = vi.spyOn; + +describe('given text representing member declaration, ', function () { + describe('when text is a method with no parameters', function () { + it('should parse simple method', function () { + const str = `getTime()`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should parse public visibiity', function () { + const str = `+getTime()`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('+getTime()'); + }); + + it('should parse private visibiity', function () { + const str = `-getTime()`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('-getTime()'); + }); + + it('should parse protected visibiity', function () { + const str = `#getTime()`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('#getTime()'); + }); + + it('should parse internal visibiity', function () { + const str = `~getTime()`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('~getTime()'); + }); + + it('should return correct css for static classifier', function () { + const str = `getTime()$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime()'); + expect(classMember.getDisplayDetails().cssStyle).toBe('text-decoration:underline;'); + }); + + it('should return correct css for abstrtact', function () { + const str = `getTime()*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime()'); + expect(classMember.getDisplayDetails().cssStyle).toBe('font-style:italic;'); + }); + }); + + describe('when text is a method with parameters', function () { + it('should parse method with parameter type, as provided', function () { + const str = `getTime(String)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should parse method with parameter type and name, as provided', function () { + const str = `getTime(String time)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should parse method with parameters, as provided', function () { + const str = `getTime(String time, date Date)`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should return correct css for static method with parameter type, as provided', function () { + const str = `getTime(String)$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(String)'); + expect(classMember.getDisplayDetails().cssStyle).toBe('text-decoration:underline;'); + }); + + it('should return correct css for static method with parameter type and name, as provided', function () { + const str = `getTime(String time)$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(String time)'); + expect(classMember.getDisplayDetails().cssStyle).toBe('text-decoration:underline;'); + }); + + it('should return correct css for static method with parameters, as provided', function () { + const str = `getTime(String time, date Date)$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(String time, date Date)'); + expect(classMember.getDisplayDetails().cssStyle).toBe('text-decoration:underline;'); + }); + + it('should return correct css for abstract method with parameter type, as provided', function () { + const str = `getTime(String)*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(String)'); + expect(classMember.getDisplayDetails().cssStyle).toBe('font-style:italic;'); + }); + + it('should return correct css for abstract method with parameter type and name, as provided', function () { + const str = `getTime(String time)*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(String time)'); + expect(classMember.getDisplayDetails().cssStyle).toBe('font-style:italic;'); + }); + + it('should return correct css for abstract method with parameters, as provided', function () { + const str = `getTime(String time, date Date)*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(String time, date Date)'); + expect(classMember.getDisplayDetails().cssStyle).toBe('font-style:italic;'); + }); + }); + + describe('when text is a method with return type', function () { + it('should parse simple method with no parameter', function () { + const str = `getTime() String`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should parse method with parameter type, as provided', function () { + const str = `getTime(String) String`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should parse method with parameter type and name, as provided', function () { + const str = `getTime(String time) String`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should parse method with parameters, as provided', function () { + const str = `getTime(String time, date Date) String`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe(str); + }); + + it('should return correct css for static method with no parameter', function () { + const str = `getTime() String$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime() String'); + expect(classMember.getDisplayDetails().cssStyle).toBe('text-decoration:underline;'); + }); + + it('should return correct css for static method with parameter type and name, as provided', function () { + const str = `getTime(String time) String$`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(String time) String'); + expect(classMember.getDisplayDetails().cssStyle).toBe('text-decoration:underline;'); + }); + + it('should return correct css for static method with parameters, as provided', function () { + const str = `getTime(String time, date Date)$ String`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe( + 'getTime(String time, date Date) String' + ); + expect(classMember.getDisplayDetails().cssStyle).toBe('text-decoration:underline;'); + }); + + it('should return correct css for abstract method with parameter type, as provided', function () { + const str = `getTime(String) String*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(String) String'); + expect(classMember.getDisplayDetails().cssStyle).toBe('font-style:italic;'); + }); + + it('should return correct css for abstract method with parameter type and name, as provided', function () { + const str = `getTime(String time) String*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe('getTime(String time) String'); + expect(classMember.getDisplayDetails().cssStyle).toBe('font-style:italic;'); + }); + + it('should return correct css for abstract method with parameters, as provided', function () { + const str = `getTime(String time, date Date) String*`; + + const classMember = new ClassMember(str, 'method'); + expect(classMember.getDisplayDetails().displayText).toBe( + 'getTime(String time, date Date) String' + ); + expect(classMember.getDisplayDetails().cssStyle).toBe('font-style:italic;'); + }); + }); +}); diff --git a/packages/mermaid/src/diagrams/class/classTypes.ts b/packages/mermaid/src/diagrams/class/classTypes.ts index cf6f20f0b..becf6fe0d 100644 --- a/packages/mermaid/src/diagrams/class/classTypes.ts +++ b/packages/mermaid/src/diagrams/class/classTypes.ts @@ -1,3 +1,5 @@ +import { parseGenericTypes } from '../common/common.js'; + export interface ClassNode { id: string; type: string; @@ -13,6 +15,90 @@ export interface ClassNode { tooltip?: string; } +export class ClassMember { + id!: string; + cssStyle!: string; + memberType!: string; + visibility!: string; + classifier!: string; + parameters!: string; + returnType!: string; + + constructor(input: string, memberType: string) { + this.memberType = memberType; + this.parseMember(input); + } + + getDisplayDetails() { + let displayText = this.visibility + parseGenericTypes(this.id); + if (this.memberType === 'method') { + displayText += '(' + parseGenericTypes(this.parameters.trim()) + ')' + ' ' + this.returnType; + } + + displayText = displayText.trim(); + const cssStyle = this.parseClassifier(); + + return { + displayText, + cssStyle, + }; + } + + parseMember(input: string) { + let potentialClassifier = ''; + + if (this.memberType === 'method') { + const methodRegEx = /([#+~-])?(.+)\((.*)\)([\s$*])?(.*)([$*])?/; + const match = input.match(methodRegEx); + if (match) { + this.visibility = match[1] ? match[1].trim() : ''; + this.id = match[2].trim(); + this.parameters = match[3] ? match[3].trim() : ''; + potentialClassifier = match[4] ? match[4].trim() : ''; + this.returnType = match[5] ? match[5].trim() : ''; + + if (potentialClassifier === '') { + const lastChar = this.returnType.substring(this.returnType.length - 1); + if (lastChar.match(/[$*]/)) { + potentialClassifier = lastChar; + this.returnType = this.returnType.substring(0, this.returnType.length - 1); + } + } + } + } else { + const length = input.length; + const firstChar = input.substring(0, 1); + const lastChar = input.substring(length - 1); + + if (firstChar.match(/[#+~-]/)) { + this.visibility = firstChar; + } + + if (lastChar.match(/[*?]/)) { + potentialClassifier = lastChar; + } + + this.id = input.substring( + this.visibility === '' ? 0 : 1, + potentialClassifier === '' ? length : length - 1 + ); + } + + this.classifier = potentialClassifier; + } + + parseClassifier() { + switch (this.classifier) { + case '*': + return 'font-style:italic;'; + case '$': + return 'text-decoration:underline;'; + default: + return ''; + } + } +} + export interface ClassNote { id: string; class: string; diff --git a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison index 7788fcc0c..c5130194f 100644 --- a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison +++ b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison @@ -226,21 +226,14 @@ start | statements ; -direction - : direction_tb - { yy.setDirection('TB');} - | direction_bt - { yy.setDirection('BT');} - | direction_rl - { yy.setDirection('RL');} - | direction_lr - { yy.setDirection('LR');} - ; - mermaidDoc : graphConfig ; +graphConfig + : CLASS_DIAGRAM NEWLINE statements EOF + ; + directive : openDirective typeDirective closeDirective NEWLINE | openDirective typeDirective ':' argDirective closeDirective NEWLINE @@ -262,10 +255,6 @@ closeDirective : close_directive { yy.parseDirective('}%%', 'close_directive', 'class'); } ; -graphConfig - : CLASS_DIAGRAM NEWLINE statements EOF - ; - statements : statement | statement NEWLINE @@ -294,7 +283,7 @@ statement | relationStatement LABEL { $1.title = yy.cleanupLabel($2); yy.addRelation($1); } | namespaceStatement | classStatement - | methodStatement + | memberStatement | annotationStatement | clickStatement | cssClassStatement @@ -341,7 +330,7 @@ members | MEMBER members { $2.push($1);$$=$2;} ; -methodStatement +memberStatement : className {/*console.log('Rel found',$1);*/} | className LABEL {yy.addMember($1,yy.cleanupLabel($2));} | MEMBER {/*console.warn('Member',$1);*/} @@ -360,6 +349,17 @@ noteStatement | NOTE noteText { yy.addNote($2); } ; +direction + : direction_tb + { yy.setDirection('TB');} + | direction_bt + { yy.setDirection('BT');} + | direction_rl + { yy.setDirection('RL');} + | direction_lr + { yy.setDirection('LR');} + ; + relation : relationType lineType relationType { $$={type1:$1,type2:$3,lineType:$2}; } | lineType relationType { $$={type1:'none',type2:$2,lineType:$1}; } diff --git a/packages/mermaid/src/diagrams/class/svgDraw.js b/packages/mermaid/src/diagrams/class/svgDraw.js index e4afe2136..37c8319d2 100644 --- a/packages/mermaid/src/diagrams/class/svgDraw.js +++ b/packages/mermaid/src/diagrams/class/svgDraw.js @@ -172,7 +172,6 @@ export const drawClass = function (elem, classDef, conf, diagObj) { // add class group const g = elem.append('g').attr('id', diagObj.db.lookUpDomId(id)).attr('class', 'classGroup'); - // add title let title; if (classDef.link) { title = g @@ -209,47 +208,56 @@ export const drawClass = function (elem, classDef, conf, diagObj) { } const titleHeight = title.node().getBBox().height; + let membersLine; + let membersBox; + let methodsLine; - const membersLine = g - .append('line') // text label for the x axis - .attr('x1', 0) - .attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2) - .attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2); + // don't draw box if no members + if (classDef.members.length > 0) { + membersLine = g + .append('line') // text label for the x axis + .attr('x1', 0) + .attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2) + .attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2); - const members = g - .append('text') // text label for the x axis - .attr('x', conf.padding) - .attr('y', titleHeight + conf.dividerMargin + conf.textHeight) - .attr('fill', 'white') - .attr('class', 'classText'); + const members = g + .append('text') // text label for the x axis + .attr('x', conf.padding) + .attr('y', titleHeight + conf.dividerMargin + conf.textHeight) + .attr('fill', 'white') + .attr('class', 'classText'); - isFirst = true; - classDef.members.forEach(function (member) { - addTspan(members, member, isFirst, conf); - isFirst = false; - }); + isFirst = true; + classDef.members.forEach(function (member) { + addTspan(members, member, isFirst, conf); + isFirst = false; + }); - const membersBox = members.node().getBBox(); + membersBox = members.node().getBBox(); + } - const methodsLine = g - .append('line') // text label for the x axis - .attr('x1', 0) - .attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height) - .attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height); + // don't draw box if no methods + if (classDef.methods.length > 0) { + methodsLine = g + .append('line') // text label for the x axis + .attr('x1', 0) + .attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height) + .attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height); - const methods = g - .append('text') // text label for the x axis - .attr('x', conf.padding) - .attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight) - .attr('fill', 'white') - .attr('class', 'classText'); + const methods = g + .append('text') // text label for the x axis + .attr('x', conf.padding) + .attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight) + .attr('fill', 'white') + .attr('class', 'classText'); - isFirst = true; + isFirst = true; - classDef.methods.forEach(function (method) { - addTspan(methods, method, isFirst, conf); - isFirst = false; - }); + classDef.methods.forEach(function (method) { + addTspan(methods, method, isFirst, conf); + isFirst = false; + }); + } const classBox = g.node().getBBox(); var cssClassStr = ' '; @@ -278,8 +286,12 @@ export const drawClass = function (elem, classDef, conf, diagObj) { title.insert('title').text(classDef.tooltip); } - membersLine.attr('x2', rectWidth); - methodsLine.attr('x2', rectWidth); + if (membersLine) { + membersLine.attr('x2', rectWidth); + } + if (methodsLine) { + methodsLine.attr('x2', rectWidth); + } classInfo.width = rectWidth; classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin; @@ -366,20 +378,20 @@ export const parseMember = function (text) { let returnType = ''; let visibility = ''; - let firstChar = text.substring(0, 1); - let lastChar = text.substring(text.length - 1, text.length); + const firstChar = text.substring(0, 1); + const lastChar = text.substring(text.length - 1, text.length); if (firstChar.match(/[#+~-]/)) { visibility = firstChar; } - let noClassifierRe = /[\s\w)~]/; + const noClassifierRe = /[\s\w)~]/; if (!lastChar.match(noClassifierRe)) { cssStyle = parseClassifier(lastChar); } const startIndex = visibility === '' ? 0 : 1; - let endIndex = cssStyle === '' ? text.length : text.length - 1; + const endIndex = cssStyle === '' ? text.length : text.length - 1; text = text.substring(startIndex, endIndex); const methodStart = text.indexOf('('); @@ -387,8 +399,7 @@ export const parseMember = function (text) { const isMethod = methodStart > 1 && methodEnd > methodStart && methodEnd <= text.length; if (isMethod) { - let methodName = text.substring(0, methodStart).trim(); - + const methodName = text.substring(0, methodStart).trim(); const parameters = text.substring(methodStart + 1, methodEnd); displayText = visibility + methodName + '(' + parseGenericTypes(parameters.trim()) + ')'; diff --git a/packages/mermaid/src/diagrams/class/svgDraw.spec.js b/packages/mermaid/src/diagrams/class/svgDraw.spec.js index e8ba9f7e1..6f9b78d27 100644 --- a/packages/mermaid/src/diagrams/class/svgDraw.spec.js +++ b/packages/mermaid/src/diagrams/class/svgDraw.spec.js @@ -1,6 +1,6 @@ import svgDraw from './svgDraw.js'; -describe('given a string representing class method, ', function () { +describe('given a string representing a class, ', function () { it('should handle class names with generics', function () { const classDef = { id: 'Car', @@ -11,8 +11,18 @@ describe('given a string representing class method, ', function () { let actual = svgDraw.getClassTitleString(classDef); expect(actual).toBe('Car'); }); + it('should handle class names with nested generics', function () { + const classDef = { + id: 'Car', + type: 'T~TT~', + label: 'Car', + }; - describe('when parsing base method declaration', function () { + let actual = svgDraw.getClassTitleString(classDef); + expect(actual).toBe('Car>'); + }); + + describe('when parsing member method', function () { it('should handle simple declaration', function () { const str = 'foo()'; let actual = svgDraw.parseMember(str); @@ -116,10 +126,8 @@ describe('given a string representing class method, ', function () { expect(actual.displayText).toBe('+foo(List> ids) : List>'); expect(actual.cssStyle).toBe('font-style:italic;'); }); - }); - describe('when parsing method visibility', function () { - it('should correctly handle public', function () { + it('should correctly handle public visibility', function () { const str = '+foo()'; let actual = svgDraw.parseMember(str); @@ -127,7 +135,7 @@ describe('given a string representing class method, ', function () { expect(actual.cssStyle).toBe(''); }); - it('should correctly handle private', function () { + it('should correctly handle private visibility', function () { const str = '-foo()'; let actual = svgDraw.parseMember(str); @@ -135,7 +143,7 @@ describe('given a string representing class method, ', function () { expect(actual.cssStyle).toBe(''); }); - it('should correctly handle protected', function () { + it('should correctly handle protected visibility', function () { const str = '#foo()'; let actual = svgDraw.parseMember(str); @@ -143,16 +151,14 @@ describe('given a string representing class method, ', function () { expect(actual.cssStyle).toBe(''); }); - it('should correctly handle package/internal', function () { + it('should correctly handle package/internal visibility', function () { const str = '~foo()'; let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('~foo()'); expect(actual.cssStyle).toBe(''); }); - }); - describe('when parsing method classifier', function () { it('should handle abstract method', function () { const str = 'foo()*'; let actual = svgDraw.parseMember(str); @@ -217,10 +223,8 @@ describe('given a string representing class method, ', function () { expect(actual.cssStyle).toBe(''); }); }); -}); -describe('given a string representing class member, ', function () { - describe('when parsing member declaration', function () { + describe('when parsing member field', function () { it('should handle simple field', function () { const str = 'id'; let actual = svgDraw.parseMember(str); @@ -276,9 +280,7 @@ describe('given a string representing class member, ', function () { expect(actual.displayText).toBe('ids: List'); expect(actual.cssStyle).toBe(''); }); - }); - describe('when parsing classifiers', function () { it('should handle static field', function () { const str = 'String foo$'; let actual = svgDraw.parseMember(str); @@ -328,3 +330,12 @@ describe('given a string representing class member, ', function () { }); }); }); + +describe('given a string representing class with no members, ', function () { + it('should have no members', function () { + const str = 'class Class10'; + let actual = svgDraw.drawClass(str); + + expect(actual.displayText).toBe(''); + }); +}); diff --git a/packages/mermaid/src/diagrams/common/common.ts b/packages/mermaid/src/diagrams/common/common.ts index 243c0cbf2..275e16a8e 100644 --- a/packages/mermaid/src/diagrams/common/common.ts +++ b/packages/mermaid/src/diagrams/common/common.ts @@ -178,13 +178,15 @@ export const getMin = function (...values: number[]): number { * @returns The converted string */ export const parseGenericTypes = function (text: string): string { + if (!text) { + return ''; + } + let cleanedText = text; if (text.split('~').length - 1 >= 2) { let newCleanedText = cleanedText; - // use a do...while loop instead of replaceAll to detect recursion - // e.g. Array~Array~T~~ do { cleanedText = newCleanedText; newCleanedText = cleanedText.replace(/~([^\s,:;]+)~/, '<$1>');