From 59a85a7dfd4ce29bdc7943299b64b9865a08e48b Mon Sep 17 00:00:00 2001 From: Justin Greywolf Date: Sun, 16 Apr 2023 19:42:51 -0700 Subject: [PATCH] Multiple Fixes to classes --- docs/syntax/classDiagram.md | 10 +- .../mermaid/src/diagrams/class/classDb.ts | 5 +- .../src/diagrams/class/classDiagram.spec.ts | 1095 +++++++++-------- .../diagrams/class/parser/classDiagram.jison | 6 +- .../mermaid/src/diagrams/class/svgDraw.js | 67 +- .../src/diagrams/class/svgDraw.spec.js | 339 +++-- .../mermaid/src/diagrams/common/common.ts | 4 +- .../mermaid/src/docs/syntax/classDiagram.md | 10 +- 8 files changed, 884 insertions(+), 652 deletions(-) diff --git a/docs/syntax/classDiagram.md b/docs/syntax/classDiagram.md index 69144ef39..7dd9bd6b3 100644 --- a/docs/syntax/classDiagram.md +++ b/docs/syntax/classDiagram.md @@ -128,7 +128,7 @@ classDiagram Vehicle <|-- Car ``` -Naming convention: a class name should be composed only of alphanumeric characters (including unicode), and underscores. +Naming convention: a class name should be composed only of alphanumeric characters (including unicode), underscores, and dashes (-). ### Class labels @@ -283,12 +283,12 @@ To describe the visibility (or encapsulation) of an attribute or method/function - `#` Protected - `~` Package/Internal -> _note_ you can also include additional _classifiers_ to a method definition by adding the following notation to the _end_ of the method, i.e.: after the `()`: +> _note_ you can also include additional _classifiers_ to a method definition by adding the following notation to the _end_ of the method, i.e.: after the `()` or after the return type: > -> - `*` Abstract e.g.: `someAbstractMethod()*` -> - `$` Static e.g.: `someStaticMethod()$` +> - `*` Abstract e.g.: `someAbstractMethod()*` or `someAbstractMethod() int*` +> - `$` Static e.g.: `someStaticMethod()$` or `someStaticMethod() String$` -> _note_ you can also include additional _classifiers_ to a field definition by adding the following notation to the end of its name: +> _note_ you can also include additional _classifiers_ to a field definition by adding the following notation to the very end: > > - `$` Static e.g.: `String someField$` diff --git a/packages/mermaid/src/diagrams/class/classDb.ts b/packages/mermaid/src/diagrams/class/classDb.ts index 8fa1eeb26..a2121f69e 100644 --- a/packages/mermaid/src/diagrams/class/classDb.ts +++ b/packages/mermaid/src/diagrams/class/classDb.ts @@ -106,6 +106,7 @@ export const clear = function () { export const getClass = function (id: string) { return classes[id]; }; + export const getClasses = function () { return classes; }; @@ -170,9 +171,10 @@ export const addMember = function (className: string, member: string) { const memberString = member.trim(); if (memberString.startsWith('<<') && memberString.endsWith('>>')) { - // Remove leading and trailing brackets + // its an annotation theClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2))); } else if (memberString.indexOf(')') > 0) { + //its a method theClass.methods.push(sanitizeText(memberString)); } else if (memberString) { theClass.members.push(sanitizeText(memberString)); @@ -234,6 +236,7 @@ const setTooltip = function (ids: string, tooltip?: string) { } }); }; + export const getTooltip = function (id: string) { return classes[id].tooltip; }; diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts index 060ba6911..8d8eca0f1 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts @@ -4,11 +4,33 @@ import classDb from './classDb.js'; import { vi, describe, it, expect } from 'vitest'; const spyOn = vi.spyOn; -describe('given a class diagram, ', function () { +describe('given a basic class diagram, ', function () { describe('when parsing class definition', function () { beforeEach(function () { parser.yy = classDb; }); + it('should handle accTitle and accDescr', function () { + const str = `classDiagram + accTitle: My Title + accDescr: My Description`; + + parser.parse(str); + expect(parser.yy.getAccTitle()).toBe('My Title'); + expect(parser.yy.getAccDescription()).toBe('My Description'); + }); + + it('should handle accTitle and multiline accDescr', function () { + const str = `classDiagram + accTitle: My Title + accDescr { + This is my multi + line description + }`; + + parser.parse(str); + expect(parser.yy.getAccTitle()).toBe('My Title'); + expect(parser.yy.getAccDescription()).toBe('This is my multi\nline description'); + }); it.skip('should handle a leading newline', function () { const str = '\nclassDiagram\n' + 'class Car'; @@ -27,113 +49,20 @@ describe('given a class diagram, ', function () { parser.parse(str); }); + it('should handle class names with dash', function () { + const str = 'classDiagram\n' + 'class Ca-r'; + + parser.parse(str); + const actual = classDb.getClass('Ca-r'); + expect(actual.label).toBe('Ca-r'); + }); + it('should handle class names with underscore', function () { const str = 'classDiagram\n' + 'class `A_Car`'; parser.parse(str); }); - it('should handle basic class definitions', function () { - const str = - 'classDiagram\n' + - 'class Car\n' + - 'Driver -- Car : drives >\n' + - 'Car *-- Wheel : have 4 >\n' + - 'Car -- Person : < owns'; - - parser.parse(str); - }); - - it('should handle member definitions in brackets', function () { - const str = 'classDiagram\n' + 'class Car{\n' + '+int wheels\n' + '}'; - - parser.parse(str); - }); - - it('should handle method declaration in brackets', function () { - const str = 'classDiagram\n' + 'class Car{\n' + '+size()\n' + '}'; - - parser.parse(str); - }); - - it('should handle properties in brackets, and some outside', function () { - const str = - 'classDiagram\n' + 'class Car{\n' + '+int wheels\n' + '}\n' + 'Car : +ArrayList size()\n'; - - parser.parse(str); - }); - - it('should handle method statements', function () { - const str = - 'classDiagram\n' + - 'Object <|-- ArrayList\n' + - 'Object : equals()\n' + - 'ArrayList : Object[] elementData\n' + - 'ArrayList : size()'; - - parser.parse(str); - }); - - it('should handle visibility for methods and members', function () { - const str = - 'classDiagram\n' + - 'class actual\n' + - 'actual : -int privateMember\n' + - 'actual : +int publicMember\n' + - 'actual : #int protectedMember\n' + - 'actual : -privateMethod()\n' + - 'actual : +publicMethod()\n' + - 'actual : #protectedMethod()\n'; - - parser.parse(str); - }); - - it('should handle parsing of method statements grouped by brackets', function () { - const str = - 'classDiagram\n' + - 'class Dummy_Class {\n' + - 'String data\n' + - ' void methods()\n' + - '}\n' + - '\n' + - 'class Flight {\n' + - ' flightNumber : Integer\n' + - ' departureTime : Date\n' + - '}'; - - parser.parse(str); - }); - - it('should handle return types on methods', function () { - const str = - 'classDiagram\n' + - 'Object <|-- ArrayList\n' + - 'Object : equals()\n' + - 'Object : -Object[] objects\n' + - 'Object : +getObjects() Object[]\n' + - 'ArrayList : Dummy elementData\n' + - 'ArrayList : getDummy() Dummy'; - - parser.parse(str); - }); - - it('should handle return types on methods grouped by brackets', function () { - const str = - 'classDiagram\n' + - 'class Dummy_Class {\n' + - 'string data\n' + - 'getDummy() Dummy\n' + - '}\n' + - '\n' + - 'class Flight {\n' + - ' int flightNumber\n' + - ' datetime departureTime\n' + - ' getDepartureTime() datetime\n' + - '}'; - - parser.parse(str); - }); - it('should handle parsing of separators', function () { const str = 'classDiagram\n' + @@ -167,122 +96,90 @@ describe('given a class diagram, ', function () { parser.parse(str); }); - it('should add bracket members in right order', function () { - const str = - 'classDiagram\n' + - 'class Class1 {\n' + - 'int : test\n' + - 'string : foo\n' + - 'test()\n' + - 'foo()\n' + - '}'; + it('should parse a class with a text label', () => { + const str = 'classDiagram\n' + 'class C1["Class 1 with text label"]'; + parser.parse(str); - const actual = parser.yy.getClass('Class1'); - expect(actual.members.length).toBe(2); - expect(actual.methods.length).toBe(2); - expect(actual.members[0]).toBe('int : test'); - expect(actual.members[1]).toBe('string : foo'); - expect(actual.methods[0]).toBe('test()'); - expect(actual.methods[1]).toBe('foo()'); - }); - - it('should parse a class with a text label', () => { - parser.parse(`classDiagram - class C1["Class 1 with text label"] - C1 --> C2 - `); const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); - const c2 = classDb.getClass('C2'); - expect(c2.label).toBe('C2'); }); - it('should parse two classes with text labels', () => { - parser.parse(`classDiagram - class C1["Class 1 with text label"] - class C2["Class 2 with chars @?"] - C1 --> C2 - `); + it('should parse two classes with text labels', function () { + const str = + 'classDiagram\n' + + 'class C1["Class 1 with text label"]\n' + + 'class C2["Class 2 with chars @?"]\n'; + + parser.parse(str); + const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); const c2 = classDb.getClass('C2'); expect(c2.label).toBe('Class 2 with chars @?'); }); - it('should parse a class with a text label and members', () => { - parser.parse(`classDiagram - class C1["Class 1 with text label"] { - +member1 - } - C1 --> C2 - `); + it('should parse a class with a text label and member', () => { + const str = 'classDiagram\n' + 'class C1["Class 1 with text label"]\n' + 'C1: member1'; + + parser.parse(str); const c1 = classDb.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'); - expect(c2.label).toBe('C2'); + expect(c1.members[0]).toBe('member1'); }); - it('should parse a class with a text label, members and annotation', () => { - parser.parse(`classDiagram - class C1["Class 1 with text label"] { - <> - +member1 - } - C1 --> C2 - `); + it('should parse a class with a text label, member and annotation', () => { + const str = + 'classDiagram\n' + + 'class C1["Class 1 with text label"]\n' + + '<> C1\n' + + 'C1 : int member1'; + + parser.parse(str); + const c1 = classDb.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.members[0]).toBe('int member1'); expect(c1.annotations.length).toBe(1); expect(c1.annotations[0]).toBe('interface'); - - const c2 = classDb.getClass('C2'); - expect(c2.label).toBe('C2'); }); it('should parse a class with text label and css class shorthand', () => { - parser.parse(`classDiagram -class C1["Class 1 with text label"]:::styleClass { - +member1 -} -C1 --> C2 - `); + const str = 'classDiagram\n' + 'class C1["Class 1 with text label"]:::styleClass'; + + parser.parse(str); const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); - expect(c1.members[0]).toBe('+member1'); expect(c1.cssClasses[0]).toBe('styleClass'); }); it('should parse a class with text label and css class', () => { - parser.parse(`classDiagram -class C1["Class 1 with text label"] { - +member1 -} -C1 --> C2 -cssClass "C1" styleClass - `); + const str = + 'classDiagram\n' + + 'class C1["Class 1 with text label"]\n' + + 'C1 : int member1\n' + + 'cssClass "C1" styleClass'; + + parser.parse(str); const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); - expect(c1.members[0]).toBe('+member1'); + expect(c1.members[0]).toBe('int member1'); expect(c1.cssClasses[0]).toBe('styleClass'); }); it('should parse two classes with text labels and css classes', () => { - parser.parse(`classDiagram -class C1["Class 1 with text label"] { - +member1 -} -class C2["Long long long long long long long long long long label"] -C1 --> C2 -cssClass "C1,C2" styleClass - `); + const str = + 'classDiagram\n' + + 'class C1["Class 1 with text label"]\n' + + 'C1 : int member1\n' + + 'class C2["Long long long long long long long long long long label"]\n' + + 'cssClass "C1,C2" styleClass'; + + parser.parse(str); const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); @@ -294,13 +191,12 @@ cssClass "C1,C2" styleClass }); it('should parse two classes with text labels and css class shorthands', () => { - parser.parse(`classDiagram -class C1["Class 1 with text label"]:::styleClass1 { - +member1 -} -class C2["Class 2 !@#$%^&*() label"]:::styleClass2 -C1 --> C2 - `); + const str = + 'classDiagram\n' + + 'class C1["Class 1 with text label"]:::styleClass1\n' + + 'class C2["Class 2 !@#$%^&*() label"]:::styleClass2'; + + parser.parse(str); const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); @@ -315,10 +211,7 @@ C1 --> C2 parser.parse(`classDiagram class C1["Class with text label"] class C2["Class with text label"] -class C3["Class with text label"] -C1 --> C2 -C3 ..> C2 - `); +class C3["Class with text label"]`); const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class with text label'); @@ -370,185 +263,96 @@ class C13["With Città foreign language"] const str = 'classDiagram\n' + 'note "test"\n'; parser.parse(str); }); - - it('should handle accTitle and accDescr', function () { - const str = `classDiagram - accTitle: My Title - accDescr: My Description - - Class1 <|-- Class02 - `; - - parser.parse(str); - expect(parser.yy.getAccTitle()).toBe('My Title'); - expect(parser.yy.getAccDescription()).toBe('My Description'); - }); - - it('should handle accTitle and multiline accDescr', function () { - const str = `classDiagram - accTitle: My Title - accDescr { - This is mu multi - line description - } - - Class1 <|-- Class02 - `; - - parser.parse(str); - expect(parser.yy.getAccTitle()).toBe('My Title'); - expect(parser.yy.getAccDescription()).toBe('This is mu multi\nline description'); - }); }); - describe('when parsing method definition', function () { + describe('when parsing class defined in brackets', function () { beforeEach(function () { parser.yy = classDb; }); - it('should handle abstract methods', function () { - const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()*'; + it('should handle member definitions', function () { + const str = 'classDiagram\n' + 'class Car{\n' + '+int wheels\n' + '}'; + + parser.parse(str); + }); + + it('should handle method definitions', function () { + const str = 'classDiagram\n' + 'class Car{\n' + '+size()\n' + '}'; + + parser.parse(str); + }); + + it('should handle a mix of members defined in and outside of brackets', function () { + const str = + 'classDiagram\n' + 'class Car{\n' + '+int wheels\n' + '}\n' + 'Car : +ArrayList size()\n'; + + parser.parse(str); + }); + + it('should handle member and method definitions', () => { + const str = + 'classDiagram\n' + 'class Dummy_Class {\n' + 'String data\n' + 'void methods()\n' + '}'; + + parser.parse(str); + }); + + it('should handle return types on methods', () => { + const str = + 'classDiagram\n' + + 'class Flight {\n' + + 'int flightNumber\n' + + 'datetime departureTime\n' + + 'getDepartureTime() datetime\n' + + '}'; + + parser.parse(str); + }); + + it('should add bracket members in right order', () => { + const str = + 'classDiagram\n' + + 'class Class1 {\n' + + 'int testMember\n' + + 'string fooMember\n' + + 'test()\n' + + 'foo()\n' + + '}'; parser.parse(str); const actual = parser.yy.getClass('Class1'); - expect(actual.annotations.length).toBe(0); - expect(actual.members.length).toBe(0); - expect(actual.methods.length).toBe(1); - expect(actual.methods[0]).toBe('someMethod()*'); + expect(actual.members.length).toBe(2); + expect(actual.methods.length).toBe(2); + expect(actual.members[0]).toBe('int testMember'); + expect(actual.members[1]).toBe('string fooMember'); + expect(actual.methods[0]).toBe('test()'); + expect(actual.methods[1]).toBe('foo()'); }); - it('should handle static methods', function () { - const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()$'; - parser.parse(str); - - const actual = parser.yy.getClass('Class1'); - expect(actual.annotations.length).toBe(0); - expect(actual.members.length).toBe(0); - expect(actual.methods.length).toBe(1); - expect(actual.methods[0]).toBe('someMethod()$'); - }); - - it('should handle generic types in members', function () { - const str = - 'classDiagram\n' + - 'class Car~T~\n' + - 'Car : -List~Wheel~ wheels\n' + - 'Car : +setWheels(List~Wheel~ wheels)\n' + - 'Car : +getWheels() List~Wheel~'; + it('should parse a class with a text label and members', () => { + const str = 'classDiagram\n' + 'class C1["Class 1 with text label"] {\n' + '+member1\n' + '}'; parser.parse(str); + const c1 = classDb.getClass('C1'); + expect(c1.label).toBe('Class 1 with text label'); + expect(c1.members.length).toBe(1); + expect(c1.members[0]).toBe('+member1'); }); - it('should handle generic types in members in class with brackets', function () { + it('should parse a class with a text label, members and annotation', () => { const str = 'classDiagram\n' + - 'class Car {\n' + - 'List~Wheel~ wheels\n' + - 'setWheels(List~Wheel~ wheels)\n' + - '+getWheels() List~Wheel~\n' + + 'class C1["Class 1 with text label"] {\n' + + '<>\n' + + '+member1\n' + '}'; parser.parse(str); - }); - }); - - describe('when parsing generics', function () { - beforeEach(function () { - parser.yy = classDb; - }); - - it('should handle generic class', function () { - const str = - 'classDiagram\n' + - 'class Car~T~\n' + - 'Driver -- Car : drives >\n' + - 'Car *-- Wheel : have 4 >\n' + - 'Car -- Person : < owns'; - - parser.parse(str); - }); - - it('should handle generic class with a literal name', function () { - const str = - 'classDiagram\n' + - 'class `Car`~T~\n' + - 'Driver -- `Car` : drives >\n' + - '`Car` *-- Wheel : have 4 >\n' + - '`Car` -- Person : < owns'; - - parser.parse(str); - }); - - it('should handle generic class with brackets', function () { - const str = - 'classDiagram\n' + - 'class Dummy_Class~T~ {\n' + - 'String data\n' + - ' void methods()\n' + - '}\n' + - '\n' + - 'class Flight {\n' + - ' flightNumber : Integer\n' + - ' departureTime : Date\n' + - '}'; - - parser.parse(str); - }); - - it('should handle generic class with brackets and a literal name', function () { - const str = - 'classDiagram\n' + - 'class `Dummy_Class`~T~ {\n' + - 'String data\n' + - ' void methods()\n' + - '}\n' + - '\n' + - 'class Flight {\n' + - ' flightNumber : Integer\n' + - ' departureTime : Date\n' + - '}'; - - parser.parse(str); - }); - - it('should break when another `{`is encountered before closing the first one while defining generic class with brackets', function () { - const str = - 'classDiagram\n' + - 'class Dummy_Class~T~ {\n' + - 'String data\n' + - ' void methods()\n' + - '}\n' + - '\n' + - 'class Dummy_Class {\n' + - 'class Flight {\n' + - ' flightNumber : Integer\n' + - ' departureTime : Date\n' + - '}'; - let testPassed = false; - try { - parser.parse(str); - } catch (error) { - testPassed = true; - } - expect(testPassed).toBe(true); - }); - - it('should break when EOF is encountered before closing the first `{` while defining generic class with brackets', function () { - const str = - 'classDiagram\n' + - 'class Dummy_Class~T~ {\n' + - 'String data\n' + - ' void methods()\n' + - '}\n' + - '\n' + - 'class Dummy_Class {\n'; - let testPassed = false; - try { - parser.parse(str); - } catch (error) { - testPassed = true; - } - expect(testPassed).toBe(true); + const c1 = classDb.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'); }); }); @@ -641,180 +445,6 @@ foo() }); }); - describe('when parsing relationships', function () { - beforeEach(function () { - parser.yy = classDb; - }); - - it('should handle relation definitions', function () { - const str = - 'classDiagram\n' + - 'Class1 <|-- Class02\n' + - 'Class03 *-- Class04\n' + - 'Class05 o-- Class06\n' + - 'Class07 .. Class08\n' + - 'Class09 -- Class1'; - - parser.parse(str); - }); - - it('should handle backquoted relation definitions', function () { - const str = - 'classDiagram\n' + - '`Class1` <|-- Class02\n' + - 'Class03 *-- Class04\n' + - 'Class05 o-- Class06\n' + - 'Class07 .. Class08\n' + - 'Class09 -- Class1'; - - parser.parse(str); - }); - - it('should handle relation definitions EXTENSION', function () { - const str = 'classDiagram\n' + 'Class1 <|-- Class02'; - - parser.parse(str); - - const relations = parser.yy.getRelations(); - - 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.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); - }); - - it('should handle relation definition of different types and directions', function () { - const str = - 'classDiagram\n' + - 'Class11 <|.. Class12\n' + - 'Class13 --> Class14\n' + - 'Class15 ..> Class16\n' + - 'Class17 ..|> Class18\n' + - 'Class19 <--* Class20'; - - parser.parse(str); - }); - - it('should handle cardinality and labels', function () { - const str = - 'classDiagram\n' + - 'Class1 "1" *-- "many" Class02 : contains\n' + - 'Class03 o-- Class04 : aggregation\n' + - 'Class05 --> "1" Class06'; - - parser.parse(str); - }); - - it('should handle dashed relation definition of different types and directions', function () { - const str = - 'classDiagram\n' + - 'Class11 <|.. Class12\n' + - 'Class13 <.. Class14\n' + - 'Class15 ..|> Class16\n' + - 'Class17 ..> Class18\n' + - 'Class19 .. Class20'; - parser.parse(str); - }); - - it('should handle relation definitions AGGREGATION and dotted line', function () { - const str = 'classDiagram\n' + 'Class1 o.. Class02'; - - parser.parse(str); - - const relations = parser.yy.getRelations(); - - 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.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.DOTTED_LINE); - }); - - it('should handle relation definitions COMPOSITION on both sides', function () { - const str = 'classDiagram\n' + 'Class1 *--* Class02'; - - parser.parse(str); - - const relations = parser.yy.getRelations(); - - 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); - }); - - it('should handle relation definitions no types', function () { - const str = 'classDiagram\n' + 'Class1 -- Class02'; - - parser.parse(str); - - const relations = parser.yy.getRelations(); - - 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('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); - }); - - it('should handle relation definitions with type only on right side', function () { - const str = 'classDiagram\n' + 'Class1 --|> Class02'; - - parser.parse(str); - - const relations = parser.yy.getRelations(); - - 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); - }); - - it('should handle multiple classes and relation definitions', function () { - const str = - 'classDiagram\n' + - 'Class1 <|-- Class02\n' + - 'Class03 *-- Class04\n' + - 'Class05 o-- Class06\n' + - 'Class07 .. Class08\n' + - 'Class09 -- Class10'; - - parser.parse(str); - - const relations = parser.yy.getRelations(); - - expect(parser.yy.getClass('Class1').id).toBe('Class1'); - expect(parser.yy.getClass('Class10').id).toBe('Class10'); - - expect(relations.length).toBe(5); - - expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION); - expect(relations[0].relation.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.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); - }); - - it('should handle generic class with relation definitions', function () { - const str = 'classDiagram\n' + 'Class1~T~ <|-- Class02'; - - parser.parse(str); - - const relations = parser.yy.getRelations(); - - 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.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); - }); - }); - describe('when parsing click statements', function () { beforeEach(function () { parser.yy = classDb; @@ -987,3 +617,422 @@ foo() }); }); }); + +describe('given a class diagram with members and methods ', function () { + describe('when parsing members', function () { + beforeEach(function () { + parser.yy = classDb; + parser.yy.clear(); + }); + + it('should handle simple member declaration', function () { + const str = 'classDiagram\n' + 'class Car\n' + 'Car : wheels'; + + parser.parse(str); + }); + + it('should handle simple member declaration with type', function () { + const str = 'classDiagram\n' + 'class Car\n' + 'Car : int wheels'; + + parser.parse(str); + }); + + it('should handle visibility', function () { + const str = + 'classDiagram\n' + + 'class actual\n' + + 'actual : -int privateMember\n' + + 'actual : +int publicMember\n' + + 'actual : #int protectedMember'; + + parser.parse(str); + }); + + it('should handle generic types', function () { + const str = 'classDiagram\n' + 'class Car\n' + 'Car : -List~Wheel~ wheels'; + + parser.parse(str); + }); + }); + + describe('when parsing method definition', function () { + beforeEach(function () { + parser.yy = classDb; + }); + + it('should handle method definition', function () { + const str = 'classDiagram\n' + 'class Car\n' + 'Car : GetSize()'; + + parser.parse(str); + }); + + it('should handle simple return types', function () { + const str = 'classDiagram\n' + 'class Object\n' + 'Object : getObject() Object'; + + parser.parse(str); + }); + + it('should handle return types as array', function () { + const str = 'classDiagram\n' + 'class Object\n' + 'Object : getObjects() Object[]'; + + parser.parse(str); + }); + + it('should handle visibility', function () { + const str = + 'classDiagram\n' + + 'class actual\n' + + 'actual : -privateMethod()\n' + + 'actual : +publicMethod()\n' + + 'actual : #protectedMethod()\n'; + + parser.parse(str); + }); + + it('should handle abstract methods', function () { + const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()*'; + parser.parse(str); + + const actual = parser.yy.getClass('Class1'); + expect(actual.annotations.length).toBe(0); + expect(actual.members.length).toBe(0); + expect(actual.methods.length).toBe(1); + expect(actual.methods[0]).toBe('someMethod()*'); + }); + + it('should handle static methods', function () { + const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()$'; + parser.parse(str); + + const actual = parser.yy.getClass('Class1'); + expect(actual.annotations.length).toBe(0); + expect(actual.members.length).toBe(0); + expect(actual.methods.length).toBe(1); + expect(actual.methods[0]).toBe('someMethod()$'); + }); + + it('should handle generic types in arguments', function () { + const str = 'classDiagram\n' + 'class Car\n' + 'Car : +setWheels(List~Wheel~ wheels)'; + parser.parse(str); + }); + + it('should handle generic return types', function () { + const str = 'classDiagram\n' + 'class Car\n' + 'Car : +getWheels() List~Wheel~'; + + parser.parse(str); + }); + + it('should handle generic types in members in class with brackets', function () { + const str = + 'classDiagram\n' + + 'class Car {\n' + + 'List~Wheel~ wheels\n' + + 'setWheels(List~Wheel~ wheels)\n' + + '+getWheels() List~Wheel~\n' + + '}'; + + parser.parse(str); + }); + }); +}); + +describe('given a class diagram with generics, ', function () { + describe('when parsing valid generic classes', function () { + beforeEach(function () { + parser.yy = classDb; + }); + + it('should handle generic class', function () { + const str = 'classDiagram\n' + 'class Car~T~'; + + parser.parse(str); + }); + + it('should handle generic class with relationships', function () { + const str = + 'classDiagram\n' + + 'class Car~T~\n' + + 'Driver -- Car : drives >\n' + + 'Car *-- Wheel : have 4 >\n' + + 'Car -- Person : < owns'; + + parser.parse(str); + }); + + it('should handle generic class with a literal name', function () { + const str = + 'classDiagram\n' + + 'class `Car`~T~\n' + + 'Driver -- `Car` : drives >\n' + + '`Car` *-- Wheel : have 4 >\n' + + '`Car` -- Person : < owns'; + + parser.parse(str); + }); + + it('should handle generic class with brackets', function () { + const str = + 'classDiagram\n' + + 'class Dummy_Class~T~ {\n' + + 'String data\n' + + 'void methods()\n' + + '}\n' + + '\n' + + 'class Flight {\n' + + 'Integer flightNumber\n' + + 'Date departureTime\n' + + '}'; + + parser.parse(str); + }); + + it('should handle generic class with brackets and a literal name', function () { + const str = + 'classDiagram\n' + + 'class `Dummy_Class`~T~ {\n' + + 'String data\n' + + ' void methods()\n' + + '}\n' + + '\n' + + 'class Flight {\n' + + ' flightNumber : Integer\n' + + ' departureTime : Date\n' + + '}'; + + parser.parse(str); + }); + }); + + describe('when parsing invalid generic classes', function () { + beforeEach(function () { + parser.yy = classDb; + }); + + it('should break when another `{`is encountered before closing the first one while defining generic class with brackets', function () { + const str = + 'classDiagram\n' + + 'class Dummy_Class~T~ {\n' + + 'String data\n' + + ' void methods()\n' + + '}\n' + + '\n' + + 'class Dummy_Class {\n' + + 'class Flight {\n' + + ' flightNumber : Integer\n' + + ' departureTime : Date\n' + + '}'; + let testPassed = false; + try { + parser.parse(str); + } catch (error) { + testPassed = true; + } + expect(testPassed).toBe(true); + }); + + it('should break when EOF is encountered before closing the first `{` while defining generic class with brackets', function () { + const str = + 'classDiagram\n' + + 'class Dummy_Class~T~ {\n' + + 'String data\n' + + ' void methods()\n' + + '}\n' + + '\n' + + 'class Dummy_Class {\n'; + let testPassed = false; + try { + parser.parse(str); + } catch (error) { + testPassed = true; + } + expect(testPassed).toBe(true); + }); + }); +}); + +describe('given a class diagram with relationships, ', function () { + describe('when parsing basic relationships', function () { + beforeEach(function () { + parser.yy = classDb; + }); + + it('should handle all basic relationships', function () { + const str = + 'classDiagram\n' + + 'Class1 <|-- Class02\n' + + 'Class03 *-- Class04\n' + + 'Class05 o-- Class06\n' + + 'Class07 .. Class08\n' + + 'Class09 -- Class1'; + + parser.parse(str); + }); + + it('should handle backquoted class name', function () { + const str = + 'classDiagram\n' + + '`Class1` <|-- Class02\n' + + 'Class03 *-- Class04\n' + + 'Class05 o-- Class06\n' + + 'Class07 .. Class08\n' + + 'Class09 -- Class1'; + + parser.parse(str); + }); + + it('should handle generics', function () { + const str = 'classDiagram\n' + 'Class1~T~ <|-- Class02'; + + parser.parse(str); + + const relations = parser.yy.getRelations(); + + 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.type2).toBe('none'); + expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + }); + + it('should handle relationships with labels', function () { + const str = + 'classDiagram\n' + + 'class Car\n' + + 'Driver -- Car : drives >\n' + + 'Car *-- Wheel : have 4 >\n' + + 'Car -- Person : < owns'; + + parser.parse(str); + }); + + it('should handle relation definitions EXTENSION', function () { + const str = 'classDiagram\n' + 'Class1 <|-- Class02'; + + parser.parse(str); + + const relations = parser.yy.getRelations(); + + 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.type2).toBe('none'); + expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + }); + + it('should handle relation definition of different types and directions', function () { + const str = + 'classDiagram\n' + + 'Class11 <|.. Class12\n' + + 'Class13 --> Class14\n' + + 'Class15 ..> Class16\n' + + 'Class17 ..|> Class18\n' + + 'Class19 <--* Class20'; + + parser.parse(str); + }); + + it('should handle cardinality and labels', function () { + const str = + 'classDiagram\n' + + 'Class1 "1" *-- "many" Class02 : contains\n' + + 'Class03 o-- Class04 : aggregation\n' + + 'Class05 --> "1" Class06'; + + parser.parse(str); + }); + + it('should handle dashed relation definition of different types and directions', function () { + const str = + 'classDiagram\n' + + 'Class11 <|.. Class12\n' + + 'Class13 <.. Class14\n' + + 'Class15 ..|> Class16\n' + + 'Class17 ..> Class18\n' + + 'Class19 .. Class20'; + parser.parse(str); + }); + + it('should handle relation definitions AGGREGATION and dotted line', function () { + const str = 'classDiagram\n' + 'Class1 o.. Class02'; + + parser.parse(str); + + const relations = parser.yy.getRelations(); + + 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.type2).toBe('none'); + expect(relations[0].relation.lineType).toBe(classDb.lineType.DOTTED_LINE); + }); + + it('should handle relation definitions COMPOSITION on both sides', function () { + const str = 'classDiagram\n' + 'Class1 *--* Class02'; + + parser.parse(str); + + const relations = parser.yy.getRelations(); + + 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); + }); + + it('should handle relation definitions with no types', function () { + const str = 'classDiagram\n' + 'Class1 -- Class02'; + + parser.parse(str); + + const relations = parser.yy.getRelations(); + + 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('none'); + expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + }); + + it('should handle relation definitions with type only on right side', function () { + const str = 'classDiagram\n' + 'Class1 --|> Class02'; + + parser.parse(str); + + const relations = parser.yy.getRelations(); + + 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); + }); + + it('should handle multiple classes and relation definitions', function () { + const str = + 'classDiagram\n' + + 'Class1 <|-- Class02\n' + + 'Class03 *-- Class04\n' + + 'Class05 o-- Class06\n' + + 'Class07 .. Class08\n' + + 'Class09 -- Class10'; + + parser.parse(str); + + const relations = parser.yy.getRelations(); + + expect(parser.yy.getClass('Class1').id).toBe('Class1'); + expect(parser.yy.getClass('Class10').id).toBe('Class10'); + + expect(relations.length).toBe(5); + + expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION); + expect(relations[0].relation.type2).toBe('none'); + expect(relations[0].relation.lineType).toBe(classDb.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); + }); + }); +}); diff --git a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison index 4354c54be..6a7834eca 100644 --- a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison +++ b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison @@ -259,8 +259,8 @@ className : alphaNumToken { $$=$1; } | classLiteralName { $$=$1; } | alphaNumToken className { $$=$1+$2; } - | alphaNumToken GENERICTYPE { $$=$1+'~'+$2; } - | classLiteralName GENERICTYPE { $$=$1+'~'+$2; } + | alphaNumToken GENERICTYPE { $$=$1+'~'+$2+'~'; } + | classLiteralName GENERICTYPE { $$=$1+'~'+$2+'~'; } ; statement @@ -366,7 +366,7 @@ textToken : textNoTagsToken | TAGSTART | TAGEND | '==' | '--' | PCT | DEFA textNoTagsToken: alphaNumToken | SPACE | MINUS | keywords ; -alphaNumToken : UNICODE_TEXT | NUM | ALPHA; +alphaNumToken : UNICODE_TEXT | NUM | ALPHA | MINUS; classLiteralName : BQUOTE_STR; diff --git a/packages/mermaid/src/diagrams/class/svgDraw.js b/packages/mermaid/src/diagrams/class/svgDraw.js index 3ce8e980b..7206506a2 100644 --- a/packages/mermaid/src/diagrams/class/svgDraw.js +++ b/packages/mermaid/src/diagrams/class/svgDraw.js @@ -199,11 +199,7 @@ export const drawClass = function (elem, classDef, conf, diagObj) { isFirst = false; }); - let classTitleString = classDef.id; - - if (classDef.type !== undefined && classDef.type !== '') { - classTitleString += '<' + classDef.type + '>'; - } + let classTitleString = getClassTitleString(classDef); const classTitle = title.append('tspan').text(classTitleString).attr('class', 'title'); @@ -291,6 +287,16 @@ export const drawClass = function (elem, classDef, conf, diagObj) { return classInfo; }; +export const getClassTitleString = function (classDef) { + let classTitleString = classDef.id; + + if (classDef.type) { + classTitleString += '<' + classDef.type + '>'; + } + + return classTitleString; +}; + /** * Renders a note diagram * @@ -355,6 +361,9 @@ export const drawNote = function (elem, note, conf, diagObj) { }; export const parseMember = function (text) { + // Note: these two regular expressions don't parse the official UML syntax for attributes + // and methods. They parse a Java-style syntax of the form + // "String name" (for attributes) and "String name(int x)" for methods const fieldRegEx = /^([#+~-])?(\w+)(~\w+~|\[])?\s+(\w+) *([$*])?$/; const methodRegEx = /^([#+|~-])?(\w+) *\( *(.*)\) *([$*])? *(\w*[[\]|~]*\s*\w*~?)$/; @@ -421,33 +430,48 @@ const buildLegacyDisplay = function (text) { let displayText = ''; let cssStyle = ''; let returnType = ''; + + let visibility = ''; + let firstChar = text.substring(0, 1); + let lastChar = text.substring(text.length - 1, text.length); + + if (firstChar.match(/[#+~-]/)) { + visibility = firstChar; + } + + let noClassifierRe = /[\s\w)~]/; + if (!lastChar.match(noClassifierRe)) { + cssStyle = parseClassifier(lastChar); + } + + let startIndex = visibility === '' ? 0 : 1; + let endIndex = cssStyle === '' ? text.length : text.length - 1; + text = text.substring(startIndex, endIndex); + let methodStart = text.indexOf('('); let methodEnd = text.indexOf(')'); if (methodStart > 1 && methodEnd > methodStart && methodEnd <= text.length) { - let visibility = ''; - let methodName = ''; - - let firstChar = text.substring(0, 1); - if (firstChar.match(/\w/)) { - methodName = text.substring(0, methodStart).trim(); - } else { - if (firstChar.match(/[#+~-]/)) { - visibility = firstChar; - } - - methodName = text.substring(1, methodStart).trim(); - } + let methodName = text.substring(0, methodStart).trim(); const parameters = text.substring(methodStart + 1, methodEnd); - const classifier = text.substring(methodEnd + 1, 1); - cssStyle = parseClassifier(text.substring(methodEnd + 1, methodEnd + 2)); displayText = visibility + methodName + '(' + parseGenericTypes(parameters.trim()) + ')'; if (methodEnd < text.length) { - returnType = text.substring(methodEnd + 2).trim(); + // special case: classifier after the closing parenthesis + let potentialClassifier = text.substring(methodEnd + 1, methodEnd + 2); + if (cssStyle === '' && !potentialClassifier.match(noClassifierRe)) { + cssStyle = parseClassifier(potentialClassifier); + returnType = text.substring(methodEnd + 2).trim(); + } else { + returnType = text.substring(methodEnd + 1).trim(); + } + if (returnType !== '') { + if (returnType.charAt(0) === ':') { + returnType = returnType.substring(1).trim(); + } returnType = ' : ' + parseGenericTypes(returnType); displayText += returnType; } @@ -502,6 +526,7 @@ const parseClassifier = function (classifier) { }; export default { + getClassTitleString, drawClass, drawEdge, drawNote, diff --git a/packages/mermaid/src/diagrams/class/svgDraw.spec.js b/packages/mermaid/src/diagrams/class/svgDraw.spec.js index 2e7c64fa0..e8ba9f7e1 100644 --- a/packages/mermaid/src/diagrams/class/svgDraw.spec.js +++ b/packages/mermaid/src/diagrams/class/svgDraw.spec.js @@ -1,8 +1,19 @@ import svgDraw from './svgDraw.js'; -describe('class member Renderer, ', function () { - describe('when parsing text to build method display string', function () { - it('should handle simple method declaration', function () { +describe('given a string representing class method, ', function () { + it('should handle class names with generics', function () { + const classDef = { + id: 'Car', + type: 'T', + label: 'Car', + }; + + let actual = svgDraw.getClassTitleString(classDef); + expect(actual).toBe('Car'); + }); + + describe('when parsing base method declaration', function () { + it('should handle simple declaration', function () { const str = 'foo()'; let actual = svgDraw.parseMember(str); @@ -10,71 +21,7 @@ describe('class member Renderer, ', function () { expect(actual.cssStyle).toBe(''); }); - it('should handle public visibility', function () { - const str = '+foo()'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('+foo()'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle private visibility', function () { - const str = '-foo()'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('-foo()'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle protected visibility', function () { - const str = '#foo()'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('#foo()'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle package/internal visibility', function () { - const str = '~foo()'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('~foo()'); - expect(actual.cssStyle).toBe(''); - }); - - it('should ignore unknown character for visibility', function () { - const str = '!foo()'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo()'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle abstract method classifier', function () { - const str = 'foo()*'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo()'); - expect(actual.cssStyle).toBe('font-style:italic;'); - }); - - it('should handle static method classifier', function () { - const str = 'foo()$'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo()'); - expect(actual.cssStyle).toBe('text-decoration:underline;'); - }); - - it('should ignore unknown character for classifier', function () { - const str = 'foo()!'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo()'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle simple method declaration with parameters', function () { + it('should handle declaration with parameters', function () { const str = 'foo(int id)'; let actual = svgDraw.parseMember(str); @@ -82,7 +29,7 @@ describe('class member Renderer, ', function () { expect(actual.cssStyle).toBe(''); }); - it('should handle simple method declaration with multiple parameters', function () { + it('should handle declaration with multiple parameters', function () { const str = 'foo(int id, object thing)'; let actual = svgDraw.parseMember(str); @@ -90,7 +37,7 @@ describe('class member Renderer, ', function () { expect(actual.cssStyle).toBe(''); }); - it('should handle simple method declaration with single item in parameters', function () { + it('should handle declaration with single item in parameters', function () { const str = 'foo(id)'; let actual = svgDraw.parseMember(str); @@ -98,7 +45,7 @@ describe('class member Renderer, ', function () { expect(actual.cssStyle).toBe(''); }); - it('should handle simple method declaration with single item in parameters with extra spaces', function () { + it('should handle declaration with single item in parameters with extra spaces', function () { const str = ' foo ( id) '; let actual = svgDraw.parseMember(str); @@ -106,22 +53,6 @@ describe('class member Renderer, ', function () { expect(actual.cssStyle).toBe(''); }); - it('should handle method declaration with return value', function () { - const str = 'foo(id) int'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(id) : int'); - expect(actual.cssStyle).toBe(''); - }); - - it('should handle method declaration with generic return value', function () { - const str = 'foo(id) List~int~'; - let actual = svgDraw.parseMember(str); - - expect(actual.displayText).toBe('foo(id) : List'); - expect(actual.cssStyle).toBe(''); - }); - it('should handle method declaration with generic parameter', function () { const str = 'foo(List~int~)'; let actual = svgDraw.parseMember(str); @@ -130,6 +61,46 @@ describe('class member Renderer, ', function () { expect(actual.cssStyle).toBe(''); }); + it('should handle method declaration with normal and generic parameter', function () { + const str = 'foo(int, List~int~)'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('foo(int, List)'); + expect(actual.cssStyle).toBe(''); + }); + + it('should handle declaration with return value', function () { + const str = 'foo(id) int'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('foo(id) : int'); + expect(actual.cssStyle).toBe(''); + }); + + it('should handle declaration with colon return value', function () { + const str = 'foo(id) : int'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('foo(id) : int'); + expect(actual.cssStyle).toBe(''); + }); + + it('should handle declaration with generic return value', function () { + const str = 'foo(id) List~int~'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('foo(id) : List'); + expect(actual.cssStyle).toBe(''); + }); + + it('should handle declaration with colon generic return value', function () { + const str = 'foo(id) : List~int~'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('foo(id) : List'); + expect(actual.cssStyle).toBe(''); + }); + it('should handle method declaration with all possible markup', function () { const str = '+foo ( List~int~ ids )* List~Item~'; let actual = svgDraw.parseMember(str); @@ -138,7 +109,7 @@ describe('class member Renderer, ', function () { expect(actual.cssStyle).toBe('font-style:italic;'); }); - it('should handle method declaration with nested markup', function () { + it('should handle method declaration with nested generics', function () { const str = '+foo ( List~List~int~~ ids )* List~List~Item~~'; let actual = svgDraw.parseMember(str); @@ -147,8 +118,134 @@ describe('class member Renderer, ', function () { }); }); - describe('when parsing text to build field display string', function () { - it('should handle simple field declaration', function () { + describe('when parsing method visibility', function () { + it('should correctly handle public', function () { + const str = '+foo()'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('+foo()'); + expect(actual.cssStyle).toBe(''); + }); + + it('should correctly handle private', function () { + const str = '-foo()'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('-foo()'); + expect(actual.cssStyle).toBe(''); + }); + + it('should correctly handle protected', function () { + const str = '#foo()'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('#foo()'); + expect(actual.cssStyle).toBe(''); + }); + + it('should correctly handle package/internal', 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); + + expect(actual.displayText).toBe('foo()'); + expect(actual.cssStyle).toBe('font-style:italic;'); + }); + + it('should handle abstract method with return type', function () { + const str = 'foo(name: String) int*'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('foo(name: String) : int'); + expect(actual.cssStyle).toBe('font-style:italic;'); + }); + + it('should handle abstract method classifier after parenthesis with return type', function () { + const str = 'foo(name: String)* int'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('foo(name: String) : int'); + expect(actual.cssStyle).toBe('font-style:italic;'); + }); + + it('should handle static method classifier', function () { + const str = 'foo()$'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('foo()'); + expect(actual.cssStyle).toBe('text-decoration:underline;'); + }); + + it('should handle static method classifier with return type', function () { + const str = 'foo(name: String) int$'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('foo(name: String) : int'); + expect(actual.cssStyle).toBe('text-decoration:underline;'); + }); + + it('should handle static method classifier with colon and return type', function () { + const str = 'foo(name: String): int$'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('foo(name: String) : int'); + expect(actual.cssStyle).toBe('text-decoration:underline;'); + }); + + it('should handle static method classifier after parenthesis with return type', function () { + const str = 'foo(name: String)$ int'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('foo(name: String) : int'); + expect(actual.cssStyle).toBe('text-decoration:underline;'); + }); + + it('should ignore unknown character for classifier', function () { + const str = 'foo()!'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('foo()'); + expect(actual.cssStyle).toBe(''); + }); + }); +}); + +describe('given a string representing class member, ', function () { + describe('when parsing member declaration', function () { + it('should handle simple field', function () { + const str = 'id'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('id'); + expect(actual.cssStyle).toBe(''); + }); + + it('should handle field with type', function () { + const str = 'int id'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('int id'); + expect(actual.cssStyle).toBe(''); + }); + + it('should handle field with type (name first)', function () { + const str = 'id: int'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('id: int'); + expect(actual.cssStyle).toBe(''); + }); + + it('should handle array field', function () { const str = 'int[] ids'; let actual = svgDraw.parseMember(str); @@ -156,7 +253,15 @@ describe('class member Renderer, ', function () { expect(actual.cssStyle).toBe(''); }); - it('should handle field declaration with generic type', function () { + it('should handle array field (name first)', function () { + const str = 'ids: int[]'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('ids: int[]'); + expect(actual.cssStyle).toBe(''); + }); + + it('should handle field with generic type', function () { const str = 'List~int~ ids'; let actual = svgDraw.parseMember(str); @@ -164,12 +269,62 @@ describe('class member Renderer, ', function () { expect(actual.cssStyle).toBe(''); }); - it('should handle static field classifier', function () { + it('should handle field with generic type (name first)', function () { + const str = 'ids: List~int~'; + let actual = svgDraw.parseMember(str); + + 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); expect(actual.displayText).toBe('String foo'); expect(actual.cssStyle).toBe('text-decoration:underline;'); }); + + it('should handle static field (name first)', function () { + const str = 'foo: String$'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('foo: String'); + expect(actual.cssStyle).toBe('text-decoration:underline;'); + }); + + it('should handle static field with generic type', function () { + const str = 'List~String~ foo$'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('List foo'); + expect(actual.cssStyle).toBe('text-decoration:underline;'); + }); + + it('should handle static field with generic type (name first)', function () { + const str = 'foo: List~String~$'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('foo: List'); + expect(actual.cssStyle).toBe('text-decoration:underline;'); + }); + + it('should handle field with nested generic type', function () { + const str = 'List~List~int~~ idLists'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('List> idLists'); + expect(actual.cssStyle).toBe(''); + }); + + it('should handle field with nested generic type (name first)', function () { + const str = 'idLists: List~List~int~~'; + let actual = svgDraw.parseMember(str); + + expect(actual.displayText).toBe('idLists: List>'); + expect(actual.cssStyle).toBe(''); + }); }); }); diff --git a/packages/mermaid/src/diagrams/common/common.ts b/packages/mermaid/src/diagrams/common/common.ts index 3b72e8718..369d84f21 100644 --- a/packages/mermaid/src/diagrams/common/common.ts +++ b/packages/mermaid/src/diagrams/common/common.ts @@ -1,6 +1,8 @@ import DOMPurify from 'dompurify'; import { MermaidConfig } from '../../config.type.js'; +export const lineBreakRegex = //gi; + /** * Gets the rows of lines in a string * @@ -65,8 +67,6 @@ export const sanitizeTextOrArray = ( return a.flat().map((x: string) => sanitizeText(x, config)); }; -export const lineBreakRegex = //gi; - /** * Whether or not a text has any line breaks * diff --git a/packages/mermaid/src/docs/syntax/classDiagram.md b/packages/mermaid/src/docs/syntax/classDiagram.md index 9d3766590..5dee13918 100644 --- a/packages/mermaid/src/docs/syntax/classDiagram.md +++ b/packages/mermaid/src/docs/syntax/classDiagram.md @@ -74,7 +74,7 @@ classDiagram Vehicle <|-- Car ``` -Naming convention: a class name should be composed only of alphanumeric characters (including unicode), and underscores. +Naming convention: a class name should be composed only of alphanumeric characters (including unicode), underscores, and dashes (-). ### Class labels @@ -171,12 +171,12 @@ To describe the visibility (or encapsulation) of an attribute or method/function - `#` Protected - `~` Package/Internal -> _note_ you can also include additional _classifiers_ to a method definition by adding the following notation to the _end_ of the method, i.e.: after the `()`: +> _note_ you can also include additional _classifiers_ to a method definition by adding the following notation to the _end_ of the method, i.e.: after the `()` or after the return type: > -> - `*` Abstract e.g.: `someAbstractMethod()*` -> - `$` Static e.g.: `someStaticMethod()$` +> - `*` Abstract e.g.: `someAbstractMethod()*` or `someAbstractMethod() int*` +> - `$` Static e.g.: `someStaticMethod()$` or `someStaticMethod() String$` -> _note_ you can also include additional _classifiers_ to a field definition by adding the following notation to the end of its name: +> _note_ you can also include additional _classifiers_ to a field definition by adding the following notation to the very end: > > - `$` Static e.g.: `String someField$`