From 447564ab0c88dbc97f09d6e58470a64d0761487e Mon Sep 17 00:00:00 2001 From: Justin Greywolf Date: Mon, 22 Jan 2024 11:21:25 -0800 Subject: [PATCH 1/9] Update classId generation to use generic type --- docs/syntax/classDiagram.md | 40 +++++++++++++- .../mermaid/src/diagrams/class/classDb.ts | 55 ++++++++++--------- .../src/diagrams/class/classDiagram.spec.ts | 18 ++++++ .../mermaid/src/diagrams/class/classTypes.ts | 1 + .../mermaid/src/docs/syntax/classDiagram.md | 23 +++++++- 5 files changed, 104 insertions(+), 33 deletions(-) diff --git a/docs/syntax/classDiagram.md b/docs/syntax/classDiagram.md index 28ae37f9e9..1f3c0e88ac 100644 --- a/docs/syntax/classDiagram.md +++ b/docs/syntax/classDiagram.md @@ -240,9 +240,7 @@ class BankAccount{ #### Generic Types -Generics can be representated as part of a class definition, and for class members/return types. In order to denote an item as generic, you enclose that type within `~` (**tilde**). **Nested** type declarations such as `List>` are supported, though generics that include a comma are currently not supported. (such as `List>`) - -> _note_ when a generic is used within a class definition, the generic type is NOT considered part of the class name. i.e.: for any syntax which required you to reference the class name, you need to drop the type part of the definition. This also means that mermaid does not currently support having two classes with the same name, but different generic types. +Generics can be representated as part of a class definition, and for class members/return types. In order to denote an item as generic, you enclose that type within `~` (**tilde**). **Nested** type declarations such as `List>` are supported, though generics that include a comma are currently not supported. (such as `List>`, however there is a workaround that you can use, which is to substitue the `,` with the HTML Entity code #44; (e.g.: Dictionary ~~decimal#44; Queue~~Order\~\~ BuyQueue) ```mermaid-example classDiagram @@ -274,6 +272,42 @@ Square : +getMessages() List~string~ Square : +getDistanceMatrix() List~List~int~~ ``` +> _note_ `(v+)` classes defined with a generic type (e.g.: ThisClass~~T~~) will have the type information added to the classname to createa unique classId. This means that you can have multiple classes defined with the same name, but different types. For any syntax where you are required to add the **class name** you should now use the same syntax as adding a class to reference this object (e.g.: ThisClass~~T~~), or just add a `-` in between the classname and type so that the parser associates items correctly. + +```mermaid-example +classDiagram +class Thing~T~{ + int id +} +class Thing~L~ + + List~int~ position + setPoints(List~int~ points) + getPoints() List~int~ + +Thing~T~: int id +Thing~L~: int lId + +Thing-T --> Thing-L +``` + +```mermaid +classDiagram +class Thing~T~{ + int id +} +class Thing~L~ + + List~int~ position + setPoints(List~int~ points) + getPoints() List~int~ + +Thing~T~: int id +Thing~L~: int lId + +Thing-T --> Thing-L +``` + #### Visibility To describe the visibility (or encapsulation) of an attribute or method/function that is a part of a class (i.e. a class member), optional notation may be placed before that members' name: diff --git a/packages/mermaid/src/diagrams/class/classDb.ts b/packages/mermaid/src/diagrams/class/classDb.ts index 0e18d1e0fa..8bce49ec3b 100644 --- a/packages/mermaid/src/diagrams/class/classDb.ts +++ b/packages/mermaid/src/diagrams/class/classDb.ts @@ -39,15 +39,17 @@ const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig()); const splitClassNameAndType = function (_id: string) { const id = common.sanitizeText(_id, getConfig()); let genericType = ''; + let classId = id; let className = id; if (id.indexOf('~') > 0) { const split = id.split('~'); className = sanitizeText(split[0]); genericType = sanitizeText(split[1]); + classId = `${className}-${genericType}`; } - return { className: className, type: genericType }; + return { classId: classId, className: className, type: genericType }; }; export const setClassLabel = function (_id: string, label: string) { @@ -56,28 +58,29 @@ export const setClassLabel = function (_id: string, label: string) { label = sanitizeText(label); } - const { className } = splitClassNameAndType(id); - classes[className].label = label; + const { classId } = splitClassNameAndType(id); + classes[classId].label = label; }; /** * Function called by parser when a node definition has been found. * - * @param id - Id of the class to add + * @param id - Id of the class to add, which can include generic type info * @public */ export const addClass = function (_id: string) { const id = common.sanitizeText(_id, getConfig()); - const { className, type } = splitClassNameAndType(id); + const { classId, className, type } = splitClassNameAndType(id); // Only add class if not exists - if (Object.hasOwn(classes, className)) { - return; + if (Object.hasOwn(classes, classId)) { + return classes[classId]; } // alert('Adding class: ' + className); const name = common.sanitizeText(className, getConfig()); // alert('Adding class after: ' + name); - classes[name] = { - id: name, + const newClass = { + id: classId, + name: name, type: type, label: name, cssClasses: [], @@ -88,7 +91,10 @@ export const addClass = function (_id: string) { domId: MERMAID_DOM_ID_PREFIX + name + '-' + classCounter, } as ClassNode; + classes[classId] = newClass; classCounter++; + + return newClass; }; /** @@ -134,14 +140,13 @@ export const getNotes = function () { export const addRelation = function (relation: ClassRelation) { log.debug('Adding relation: ' + JSON.stringify(relation)); - addClass(relation.id1); - addClass(relation.id2); + const relation1 = addClass(relation.id1); + const relation2 = addClass(relation.id2); - relation.id1 = splitClassNameAndType(relation.id1).className; - relation.id2 = splitClassNameAndType(relation.id2).className; + relation.id1 = relation1.id; + relation.id2 = relation2.id; relation.relationTitle1 = common.sanitizeText(relation.relationTitle1.trim(), getConfig()); - relation.relationTitle2 = common.sanitizeText(relation.relationTitle2.trim(), getConfig()); relations.push(relation); @@ -156,8 +161,8 @@ export const addRelation = function (relation: ClassRelation) { * @public */ export const addAnnotation = function (className: string, annotation: string) { - const validatedClassName = splitClassNameAndType(className).className; - classes[validatedClassName].annotations.push(annotation); + const { classId } = splitClassNameAndType(className); + classes[classId].annotations.push(annotation); }; /** @@ -170,23 +175,19 @@ export const addAnnotation = function (className: string, annotation: string) { * @public */ export const addMember = function (className: string, member: string) { - addClass(className); - - const validatedClassName = splitClassNameAndType(className).className; - const theClass = classes[validatedClassName]; + const newClass = addClass(className); if (typeof member === 'string') { - // Member can contain white spaces, we trim them out const memberString = member.trim(); if (memberString.startsWith('<<') && memberString.endsWith('>>')) { // its an annotation - theClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2))); + newClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2))); } else if (memberString.indexOf(')') > 0) { //its a method - theClass.methods.push(new ClassMember(memberString, 'method')); + newClass.methods.push(new ClassMember(memberString, 'method')); } else if (memberString) { - theClass.members.push(new ClassMember(memberString, 'attribute')); + newClass.members.push(new ClassMember(memberString, 'attribute')); } } }; @@ -218,16 +219,16 @@ export const cleanupLabel = function (label: string) { * Called by parser when assigning cssClass to a class * * @param ids - Comma separated list of ids - * @param className - Class to add + * @param cssClassName - Class to add */ -export const setCssClass = function (ids: string, className: string) { +export const setCssClass = function (ids: string, cssClassName: string) { ids.split(',').forEach(function (_id) { let id = _id; if (_id[0].match(/\d/)) { id = MERMAID_DOM_ID_PREFIX + id; } if (classes[id] !== undefined) { - classes[id].cssClasses.push(className); + classes[id].cssClasses.push(cssClassName); } }); }; diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts index e3dbb17f14..5a2b3c373c 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts @@ -859,6 +859,24 @@ describe('given a class diagram with members and methods ', function () { parser.parse(str); }); + + it('should handle multiple classes with same name, and different types types', function () { + const str = `classDiagram + class Car~T~ + Car-T : int numSeats' + class Car~L~`; + + parser.parse(str); + + const classes = parser.yy.getClasses(); + expect(Object.keys(classes).length).toBe(2); + expect(classes['Car-T'].id).toBe('Car-T'); + expect(classes['Car-T'].name).toBe('Car'); + expect(classes['Car-T'].type).toBe('T'); + expect(classes['Car-L'].id).toBe('Car-L'); + expect(classes['Car-L'].name).toBe('Car'); + expect(classes['Car-L'].type).toBe('L'); + }); }); describe('when parsing method definition', function () { diff --git a/packages/mermaid/src/diagrams/class/classTypes.ts b/packages/mermaid/src/diagrams/class/classTypes.ts index 85be3a4e87..a16df1c167 100644 --- a/packages/mermaid/src/diagrams/class/classTypes.ts +++ b/packages/mermaid/src/diagrams/class/classTypes.ts @@ -3,6 +3,7 @@ import { parseGenericTypes, sanitizeText } from '../common/common.js'; export interface ClassNode { id: string; + name: string; type: string; label: string; cssClasses: string[]; diff --git a/packages/mermaid/src/docs/syntax/classDiagram.md b/packages/mermaid/src/docs/syntax/classDiagram.md index bb7b31678d..faf4035984 100644 --- a/packages/mermaid/src/docs/syntax/classDiagram.md +++ b/packages/mermaid/src/docs/syntax/classDiagram.md @@ -143,9 +143,7 @@ class BankAccount{ #### Generic Types -Generics can be representated as part of a class definition, and for class members/return types. In order to denote an item as generic, you enclose that type within `~` (**tilde**). **Nested** type declarations such as `List>` are supported, though generics that include a comma are currently not supported. (such as `List>`) - -> _note_ when a generic is used within a class definition, the generic type is NOT considered part of the class name. i.e.: for any syntax which required you to reference the class name, you need to drop the type part of the definition. This also means that mermaid does not currently support having two classes with the same name, but different generic types. +Generics can be representated as part of a class definition, and for class members/return types. In order to denote an item as generic, you enclose that type within `~` (**tilde**). **Nested** type declarations such as `List>` are supported, though generics that include a comma are currently not supported. (such as `List>`, however there is a workaround that you can use, which is to substitue the `,` with the HTML Entity code #44; (e.g.: Dictionary ~decimal#44; Queue~Order~~ BuyQueue) ```mermaid-example classDiagram @@ -162,6 +160,25 @@ Square : +getMessages() List~string~ Square : +getDistanceMatrix() List~List~int~~ ``` +> _note_ `(v+)` classes defined with a generic type (e.g.: ThisClass~T~) will have the type information added to the classname to createa unique classId. This means that you can have multiple classes defined with the same name, but different types. For any syntax where you are required to add the **class name** you should now use the same syntax as adding a class to reference this object (e.g.: ThisClass~T~), or just add a `-` in between the classname and type so that the parser associates items correctly. + +```mermaid-example +classDiagram +class Thing~T~{ + int id +} +class Thing~L~ + + List~int~ position + setPoints(List~int~ points) + getPoints() List~int~ + +Thing~T~: int id +Thing~L~: int lId + +Thing-T --> Thing-L +``` + #### Visibility To describe the visibility (or encapsulation) of an attribute or method/function that is a part of a class (i.e. a class member), optional notation may be placed before that members' name: From 5ac13a4d0e541b79ee0cbef83b4c82e205584123 Mon Sep 17 00:00:00 2001 From: Justin Greywolf Date: Sat, 27 Jan 2024 12:03:32 -0800 Subject: [PATCH 2/9] Update packages/mermaid/src/diagrams/class/classDb.ts Co-authored-by: Sidharth Vinod --- packages/mermaid/src/diagrams/class/classDb.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/class/classDb.ts b/packages/mermaid/src/diagrams/class/classDb.ts index 8bce49ec3b..795018f59b 100644 --- a/packages/mermaid/src/diagrams/class/classDb.ts +++ b/packages/mermaid/src/diagrams/class/classDb.ts @@ -49,7 +49,7 @@ const splitClassNameAndType = function (_id: string) { classId = `${className}-${genericType}`; } - return { classId: classId, className: className, type: genericType }; + return { classId, className, type: genericType }; }; export const setClassLabel = function (_id: string, label: string) { From 5f089706408910122404407cfb775e47c8de1a22 Mon Sep 17 00:00:00 2001 From: Justin Greywolf Date: Sat, 27 Jan 2024 12:20:42 -0800 Subject: [PATCH 3/9] Fixed documentation --- docs/syntax/classDiagram.md | 8 ++++---- packages/mermaid/src/docs/syntax/classDiagram.md | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/syntax/classDiagram.md b/docs/syntax/classDiagram.md index 1f3c0e88ac..0a6ddea622 100644 --- a/docs/syntax/classDiagram.md +++ b/docs/syntax/classDiagram.md @@ -279,11 +279,11 @@ classDiagram class Thing~T~{ int id } -class Thing~L~ - +class Thing~L~{ List~int~ position setPoints(List~int~ points) getPoints() List~int~ +} Thing~T~: int id Thing~L~: int lId @@ -296,11 +296,11 @@ classDiagram class Thing~T~{ int id } -class Thing~L~ - +class Thing~L~{ List~int~ position setPoints(List~int~ points) getPoints() List~int~ +} Thing~T~: int id Thing~L~: int lId diff --git a/packages/mermaid/src/docs/syntax/classDiagram.md b/packages/mermaid/src/docs/syntax/classDiagram.md index faf4035984..3543add265 100644 --- a/packages/mermaid/src/docs/syntax/classDiagram.md +++ b/packages/mermaid/src/docs/syntax/classDiagram.md @@ -167,11 +167,11 @@ classDiagram class Thing~T~{ int id } -class Thing~L~ - +class Thing~L~{ List~int~ position setPoints(List~int~ points) getPoints() List~int~ +} Thing~T~: int id Thing~L~: int lId From d772f78661246d5b8cbb084210d5ad67a3b3fbdc Mon Sep 17 00:00:00 2001 From: Justin Greywolf Date: Sat, 27 Jan 2024 17:39:57 -0800 Subject: [PATCH 4/9] Update how generic classes inside of a namespace are handled. Fix tests --- .../mermaid/src/diagrams/class/classDb.ts | 52 ++++++++++--------- .../src/diagrams/class/classDiagram.spec.ts | 39 ++++++++------ 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/packages/mermaid/src/diagrams/class/classDb.ts b/packages/mermaid/src/diagrams/class/classDb.ts index 795018f59b..e232f0a0fe 100644 --- a/packages/mermaid/src/diagrams/class/classDb.ts +++ b/packages/mermaid/src/diagrams/class/classDb.ts @@ -68,16 +68,16 @@ export const setClassLabel = function (_id: string, label: string) { * @param id - Id of the class to add, which can include generic type info * @public */ -export const addClass = function (_id: string) { +export const getOrAddClass = function (_id: string) { const id = common.sanitizeText(_id, getConfig()); const { classId, className, type } = splitClassNameAndType(id); + // Only add class if not exists if (Object.hasOwn(classes, classId)) { return classes[classId]; } - // alert('Adding class: ' + className); + const name = common.sanitizeText(className, getConfig()); - // alert('Adding class after: ' + name); const newClass = { id: classId, name: name, @@ -140,8 +140,8 @@ export const getNotes = function () { export const addRelation = function (relation: ClassRelation) { log.debug('Adding relation: ' + JSON.stringify(relation)); - const relation1 = addClass(relation.id1); - const relation2 = addClass(relation.id2); + const relation1 = getOrAddClass(relation.id1); + const relation2 = getOrAddClass(relation.id2); relation.id1 = relation1.id; relation.id2 = relation2.id; @@ -175,19 +175,19 @@ export const addAnnotation = function (className: string, annotation: string) { * @public */ export const addMember = function (className: string, member: string) { - const newClass = addClass(className); + const theClass = getOrAddClass(className); if (typeof member === 'string') { const memberString = member.trim(); if (memberString.startsWith('<<') && memberString.endsWith('>>')) { // its an annotation - newClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2))); + theClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2))); } else if (memberString.indexOf(')') > 0) { //its a method - newClass.methods.push(new ClassMember(memberString, 'method')); + theClass.methods.push(new ClassMember(memberString, 'method')); } else if (memberString) { - newClass.members.push(new ClassMember(memberString, 'attribute')); + theClass.members.push(new ClassMember(memberString, 'attribute')); } } }; @@ -418,18 +418,19 @@ const setDirection = (dir: string) => { * @public */ export const addNamespace = function (id: string) { - if (namespaces[id] !== undefined) { - return; + // Only add namespace if it does not exist + if (!Object.hasOwn(namespaces, id)) { + namespaces[id] = { + id: id, + classes: {}, + children: {}, + domId: MERMAID_DOM_ID_PREFIX + id + '-' + namespaceCounter, + } as NamespaceNode; + + namespaceCounter++; } - namespaces[id] = { - id: id, - classes: {}, - children: {}, - domId: MERMAID_DOM_ID_PREFIX + id + '-' + namespaceCounter, - } as NamespaceNode; - - namespaceCounter++; + return namespaces[id]; }; const getNamespace = function (name: string): NamespaceNode { @@ -448,13 +449,16 @@ const getNamespaces = function (): NamespaceMap { * @public */ export const addClassesToNamespace = function (id: string, classNames: string[]) { - if (namespaces[id] === undefined) { + const namespace = addNamespace(id); + + if (namespace === undefined) { return; } + for (const name of classNames) { - const { className } = splitClassNameAndType(name); - classes[className].parent = id; - namespaces[id].classes[className] = classes[className]; + const theClass = getOrAddClass(name); + theClass.parent = id; + namespace.classes[theClass.id] = theClass; } }; @@ -478,7 +482,7 @@ export default { getAccDescription, setAccDescription, getConfig: () => getConfig().class, - addClass, + addClass: getOrAddClass, bindFunctions, clear, getClass, diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts index 5a2b3c373c..3fb092446d 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts @@ -2,6 +2,8 @@ import { parser } from './parser/classDiagram.jison'; import classDb from './classDb.js'; import { vi, describe, it, expect } from 'vitest'; +import { getClasses } from './classDb'; +import type { ClassNode } from './classTypes.js'; const spyOn = vi.spyOn; const staticCssStyle = 'text-decoration:underline;'; @@ -397,7 +399,7 @@ class C13["With Città foreign language"] { "annotations": [], "cssClasses": [], - "domId": "classId-Student-134", + "domId": "classId-Student-0", "id": "Student", "label": "Student", "members": [ @@ -409,6 +411,7 @@ class C13["With Città foreign language"] }, ], "methods": [], + "name": "Student", "styles": [], "type": "", } @@ -1064,16 +1067,18 @@ foo() }); it('should handle namespace with generic types', () => { - parser.parse(`classDiagram + const str = `classDiagram + namespace space { + class Square~Shape~{ + int : id + List~int~ position + setPoints(List~int~ points) + getPoints() List~int~ + } + } + `; -namespace space { - class Square~Shape~{ - int id - List~int~ position - setPoints(List~int~ points) - getPoints() List~int~ - } -}`); + parser.parse(str); }); }); }); @@ -1115,9 +1120,11 @@ describe('given a class diagram with relationships, ', function () { parser.parse(str); const relations = parser.yy.getRelations(); + const classNode1 = parser.yy.getClass('Class1-T') as ClassNode; - expect(parser.yy.getClass('Class1').id).toBe('Class1'); - expect(parser.yy.getClass('Class1').type).toBe('T'); + expect(classNode1.id).toBe('Class1-T'); + expect(classNode1.type).toBe('T'); + expect(classNode1.name).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'); @@ -1265,14 +1272,16 @@ describe('given a class diagram with relationships, ', function () { }); it('should handle generic class with relation definitions', function () { - const str = 'classDiagram\n' + 'Class01~T~ <|-- Class02'; + const str = 'classDiagram\n' + 'Class1~T~ <|-- Class02'; parser.parse(str); const relations = parser.yy.getRelations(); + const classNode1 = parser.yy.getClass('Class1-T') as ClassNode; - expect(parser.yy.getClass('Class01').id).toBe('Class01'); - expect(parser.yy.getClass('Class01').type).toBe('T'); + expect(classNode1.id).toBe('Class1-T'); + expect(classNode1.type).toBe('T'); + expect(classNode1.name).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'); From 80f344944ce8066a52cf7d33a37c68e8bdeec5b3 Mon Sep 17 00:00:00 2001 From: Justin Greywolf Date: Sat, 27 Jan 2024 18:19:45 -0800 Subject: [PATCH 5/9] Remove unneeded import --- packages/mermaid/src/diagrams/class/classDiagram.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts index 3fb092446d..8770eacc29 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts @@ -2,7 +2,6 @@ import { parser } from './parser/classDiagram.jison'; import classDb from './classDb.js'; import { vi, describe, it, expect } from 'vitest'; -import { getClasses } from './classDb'; import type { ClassNode } from './classTypes.js'; const spyOn = vi.spyOn; From c104ffa26dded9714a08579e974a06d5ad112a87 Mon Sep 17 00:00:00 2001 From: Justin Greywolf Date: Sat, 27 Jan 2024 19:54:01 -0800 Subject: [PATCH 6/9] fix test expectation --- packages/mermaid/src/diagrams/class/classDiagram.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts index 8770eacc29..b3a9824def 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts @@ -398,7 +398,7 @@ class C13["With Città foreign language"] { "annotations": [], "cssClasses": [], - "domId": "classId-Student-0", + "domId": "classId-Student-134", "id": "Student", "label": "Student", "members": [ From ef28e6f77ddab793b2d73623ab19e6110ffe5ef0 Mon Sep 17 00:00:00 2001 From: Justin Greywolf Date: Sat, 27 Jan 2024 20:17:28 -0800 Subject: [PATCH 7/9] Fix cypress class diagram generic tests --- .../rendering/classDiagram-v2.spec.js | 24 +++++++++---------- .../rendering/classDiagram.spec.js | 24 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/cypress/integration/rendering/classDiagram-v2.spec.js b/cypress/integration/rendering/classDiagram-v2.spec.js index 20a1aea0ab..919d7ad5b0 100644 --- a/cypress/integration/rendering/classDiagram-v2.spec.js +++ b/cypress/integration/rendering/classDiagram-v2.spec.js @@ -201,9 +201,9 @@ describe('Class diagram V2', () => { ` classDiagram-v2 class Class01~T~ - Class01 : size() - Class01 : int chimp - Class01 : int gorilla + Class01-T : size() + Class01-T : int chimp + Class01-T : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> @@ -221,9 +221,9 @@ describe('Class diagram V2', () => { classDiagram-v2 Class01~T~ <|-- AveryLongClass : Cool Class03~T~ *-- Class04~T~ - Class01 : size() - Class01 : int chimp - Class01 : int gorilla + Class01-T : size() + Class01-T : int chimp + Class01-T : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> @@ -241,9 +241,9 @@ describe('Class diagram V2', () => { classDiagram-v2 Class01~T~ <|-- AveryLongClass : Cool Class03~T~ *-- Class04~T~ - Class01 : size() - Class01 : int chimp - Class01 : int gorilla + Class01-T : size() + Class01-T : int chimp + Class01-T : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> @@ -262,9 +262,9 @@ describe('Class diagram V2', () => { classDiagram-v2 Class01~T~ <|-- AveryLongClass : Cool Class03~T~ *-- Class04~T~ - Class01 : size() - Class01 : int chimp - Class01 : int gorilla + Class01-T : size() + Class01-T : int chimp + Class01-T : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> diff --git a/cypress/integration/rendering/classDiagram.spec.js b/cypress/integration/rendering/classDiagram.spec.js index cab3649df4..6c0b5846d2 100644 --- a/cypress/integration/rendering/classDiagram.spec.js +++ b/cypress/integration/rendering/classDiagram.spec.js @@ -141,9 +141,9 @@ describe('Class diagram', () => { ` classDiagram class Class01~T~ - Class01 : size() - Class01 : int chimp - Class01 : int gorilla + Class01-T : size() + Class01-T : int chimp + Class01~T~ : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> @@ -162,9 +162,9 @@ describe('Class diagram', () => { classDiagram Class01~T~ <|-- AveryLongClass : Cool Class03~T~ *-- Class04~T~ - Class01 : size() - Class01 : int chimp - Class01 : int gorilla + Class01-T : size() + Class01-T : int chimp + Class01-T : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> @@ -183,9 +183,9 @@ describe('Class diagram', () => { classDiagram Class01~T~ <|-- AveryLongClass : Cool Class03~T~ *-- Class04~T~ - Class01 : size() - Class01 : int chimp - Class01 : int gorilla + Class01-T : size() + Class01-T : int chimp + Class01-T : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> @@ -205,9 +205,9 @@ describe('Class diagram', () => { classDiagram Class01~T~ <|-- AveryLongClass : Cool Class03~T~ *-- Class04~T~ - Class01 : size() - Class01 : int chimp - Class01 : int gorilla + Class01-T : size() + Class01-T : int chimp + Class01-T : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> From 4feb8181ae4b3c612faacc32e871f89773e77d2e Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Sat, 23 Mar 2024 18:30:26 +0530 Subject: [PATCH 8/9] Use proper note format --- docs/syntax/classDiagram.md | 2 +- packages/mermaid/src/docs/syntax/classDiagram.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/syntax/classDiagram.md b/docs/syntax/classDiagram.md index 779eece4f2..2f07a868c4 100644 --- a/docs/syntax/classDiagram.md +++ b/docs/syntax/classDiagram.md @@ -272,7 +272,7 @@ Square : +getMessages() List~string~ Square : +getDistanceMatrix() List~List~int~~ ``` -> _note_ `(v+)` classes defined with a generic type (e.g.: ThisClass~~T~~) will have the type information added to the classname to create a unique classID. This means that you can have multiple classes defined with the same name, but different types. For any syntax where you are required to add the **class name** you should now use the same syntax as adding a class to reference this object (e.g.: ThisClass~~T~~), or just add a `-` in between the classname and type so that the parser associates items correctly. +> **Note** > `(v+)` classes defined with a generic type (e.g.: ThisClass\~T\~) will have the type information added to the classname to create a unique classID. This means that you can have multiple classes defined with the same name, but different types. For any syntax where you are required to add the **class name** you should now use the same syntax as adding a class to reference this object (e.g.: ThisClass\~T\~), or just add a `-` in between the classname and type so that the parser associates items correctly. ```mermaid-example classDiagram diff --git a/packages/mermaid/src/docs/syntax/classDiagram.md b/packages/mermaid/src/docs/syntax/classDiagram.md index 6bf4d055ec..c3fd40a412 100644 --- a/packages/mermaid/src/docs/syntax/classDiagram.md +++ b/packages/mermaid/src/docs/syntax/classDiagram.md @@ -160,7 +160,9 @@ Square : +getMessages() List~string~ Square : +getDistanceMatrix() List~List~int~~ ``` -> _note_ `(v+)` classes defined with a generic type (e.g.: ThisClass~T~) will have the type information added to the classname to create a unique classID. This means that you can have multiple classes defined with the same name, but different types. For any syntax where you are required to add the **class name** you should now use the same syntax as adding a class to reference this object (e.g.: ThisClass~T~), or just add a `-` in between the classname and type so that the parser associates items correctly. +```note +`(v+)` classes defined with a generic type (e.g.: ThisClass~T~) will have the type information added to the classname to create a unique classID. This means that you can have multiple classes defined with the same name, but different types. For any syntax where you are required to add the **class name** you should now use the same syntax as adding a class to reference this object (e.g.: ThisClass~T~), or just add a `-` in between the classname and type so that the parser associates items correctly. +``` ```mermaid-example classDiagram From 234a2cc819c656aba67888024949b50d912476eb Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Sun, 24 Mar 2024 12:29:40 +0530 Subject: [PATCH 9/9] chore: Revert changes to existing tests, add new tests --- .cspell/contributors.txt | 1 + .../rendering/classDiagram-v2.spec.js | 142 +++++++++++++++-- .../rendering/classDiagram.spec.js | 143 ++++++++++++++++-- 3 files changed, 262 insertions(+), 24 deletions(-) diff --git a/.cspell/contributors.txt b/.cspell/contributors.txt index bd3ad9da24..f90c064998 100644 --- a/.cspell/contributors.txt +++ b/.cspell/contributors.txt @@ -2,6 +2,7 @@ Ashish Jain cpettitt Dong Cai +jgreywolf Nikolay Rozhkov Peng Xiao subhash-halder diff --git a/cypress/integration/rendering/classDiagram-v2.spec.js b/cypress/integration/rendering/classDiagram-v2.spec.js index 919d7ad5b0..8da82f98c6 100644 --- a/cypress/integration/rendering/classDiagram-v2.spec.js +++ b/cypress/integration/rendering/classDiagram-v2.spec.js @@ -201,9 +201,9 @@ describe('Class diagram V2', () => { ` classDiagram-v2 class Class01~T~ - Class01-T : size() - Class01-T : int chimp - Class01-T : int gorilla + Class01 : size() + Class01 : int chimp + Class01 : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> @@ -221,9 +221,9 @@ describe('Class diagram V2', () => { classDiagram-v2 Class01~T~ <|-- AveryLongClass : Cool Class03~T~ *-- Class04~T~ - Class01-T : size() - Class01-T : int chimp - Class01-T : int gorilla + Class01 : size() + Class01 : int chimp + Class01 : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> @@ -241,9 +241,9 @@ describe('Class diagram V2', () => { classDiagram-v2 Class01~T~ <|-- AveryLongClass : Cool Class03~T~ *-- Class04~T~ - Class01-T : size() - Class01-T : int chimp - Class01-T : int gorilla + Class01 : size() + Class01 : int chimp + Class01 : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> @@ -262,9 +262,9 @@ describe('Class diagram V2', () => { classDiagram-v2 Class01~T~ <|-- AveryLongClass : Cool Class03~T~ *-- Class04~T~ - Class01-T : size() - Class01-T : int chimp - Class01-T : int gorilla + Class01 : size() + Class01 : int chimp + Class01 : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> @@ -581,4 +581,122 @@ class C13["With Città foreign language"] { logLevel: 1, flowchart: { htmlLabels: false } } ); }); + + describe('when adding generic types', () => { + it('should add properties when type is mentioned in classID', () => { + imgSnapshotTest( + ` + classDiagram-v2 + class Class01~T~ + Class01-T : size() + Class01-T : int chimp + Class01-T : int gorilla + ` + ); + }); + + it('should fallback to matching class name when type is not mentioned in property', () => { + imgSnapshotTest( + ` + classDiagram-v2 + class Class01~T~ + Class01-T : size() + Class01-T : int chimp + Class01 : int gorilla + ` + ); + }); + + it('should fallback to the first matching class name when type is not mentioned in property', () => { + imgSnapshotTest( + ` + classDiagram-v2 + class Class01~T~ + class Class01~X~ + Class01-T : int inClassT + Class01-X : int inClassX + Class01 : int alsoInClassT + ` + ); + }); + + it('should detect generic classes correctly when using different classIDs', () => { + imgSnapshotTest( + ` + classDiagram-v2 + class Class01~T~ + Class01-T : size() + Class01-T : int chimp + Class01 : int gorillaInClassT + class Class01~X~ + Class01-X : size() + Class01-X : int chimp + Class01-X : int gorilla + ` + ); + }); + + it('should render with Generic class and relations', () => { + imgSnapshotTest( + ` + classDiagram-v2 + Class01~T~ <|-- AveryLongClass : Cool + Class03~T~ *-- Class04~T~ + Class01-T : size() + Class01-T : int chimp + Class01-T : int gorilla + Class08 <--> C2: Cool label + class Class10~T~ { + <<service>> + int id + test() + } + ` + ); + }); + + it('should render with clickable link when type is not mentioned', () => { + imgSnapshotTest( + ` + classDiagram-v2 + Class01~T~ <|-- AveryLongClass : Cool + Class01-T : size() + link Class01 "google.com" "A Tooltip" + ` + ); + }); + + it('should render with clickable callback when type is not mentioned', () => { + imgSnapshotTest( + ` + classDiagram-v2 + Class01~T~ <|-- AveryLongClass : Cool + Class01-T : size() + callback Class01 "functionCall" "A Tooltip" + ` + ); + }); + + it('should render with clickable link when type is mentioned', () => { + imgSnapshotTest( + ` + classDiagram-v2 + Class01~T~ <|-- AveryLongClass : Cool + Class01-T : size() + link Class01-T "google.com" "A Tooltip" + ` + ); + }); + + it('should render with clickable callback when type is mentioned', () => { + imgSnapshotTest( + ` + classDiagram-v2 + Class01~T~ <|-- AveryLongClass : Cool + Class01-T : size() + callback Class01-T "functionCall" "A Tooltip" + ` + ); + }); + }); }); diff --git a/cypress/integration/rendering/classDiagram.spec.js b/cypress/integration/rendering/classDiagram.spec.js index 6c0b5846d2..95f7d9723a 100644 --- a/cypress/integration/rendering/classDiagram.spec.js +++ b/cypress/integration/rendering/classDiagram.spec.js @@ -141,9 +141,9 @@ describe('Class diagram', () => { ` classDiagram class Class01~T~ - Class01-T : size() - Class01-T : int chimp - Class01~T~ : int gorilla + Class01 : size() + Class01 : int chimp + Class01 : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> @@ -162,9 +162,9 @@ describe('Class diagram', () => { classDiagram Class01~T~ <|-- AveryLongClass : Cool Class03~T~ *-- Class04~T~ - Class01-T : size() - Class01-T : int chimp - Class01-T : int gorilla + Class01 : size() + Class01 : int chimp + Class01 : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> @@ -183,9 +183,9 @@ describe('Class diagram', () => { classDiagram Class01~T~ <|-- AveryLongClass : Cool Class03~T~ *-- Class04~T~ - Class01-T : size() - Class01-T : int chimp - Class01-T : int gorilla + Class01 : size() + Class01 : int chimp + Class01 : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> @@ -205,9 +205,9 @@ describe('Class diagram', () => { classDiagram Class01~T~ <|-- AveryLongClass : Cool Class03~T~ *-- Class04~T~ - Class01-T : size() - Class01-T : int chimp - Class01-T : int gorilla + Class01 : size() + Class01 : int chimp + Class01 : int gorilla Class08 <--> C2: Cool label class Class10~T~ { <<service>> @@ -513,4 +513,123 @@ describe('Class diagram', () => { cy.get('a').should('have.attr', 'target', '_blank').should('have.attr', 'rel', 'noopener'); }); }); + + describe('when adding generic types', () => { + it('should add properties when type is mentioned in classID', () => { + imgSnapshotTest( + ` + classDiagram + class Class01~T~ + Class01-T : size() + Class01-T : int chimp + Class01-T : int gorilla + ` + ); + }); + + it('should fallback to matching class name when type is not mentioned in property', () => { + imgSnapshotTest( + ` + classDiagram + class Class01~T~ + Class01-T : size() + Class01-T : int chimp + Class01 : int gorilla + ` + ); + }); + + it('should fallback to the first matching class name when type is not mentioned in property', () => { + imgSnapshotTest( + ` + classDiagram + class Class01~T~ + class Class01~X~ + Class01-T : int inClassT + Class01-X : int inClassX + Class01 : int alsoInClassT + ` + ); + }); + + it('should detect generic classes correctly when using different classIDs', () => { + imgSnapshotTest( + ` + classDiagram + class Class01~T~ + Class01-T : size() + Class01-T : int chimp + Class01 : int gorillaInClassT + class Class01~X~ + Class01-X : size() + Class01-X : int chimp + Class01-X : int gorilla + ` + ); + }); + + it('should render with Generic class and relations', () => { + imgSnapshotTest( + ` + classDiagram + Class01~T~ <|-- AveryLongClass : Cool + Class03~T~ *-- Class04~T~ + Class01-T : size() + Class01-T : int chimp + Class01-T : int gorilla + Class08 <--> C2: Cool label + class Class10~T~ { + <<service>> + int id + test() + } + ` + ); + }); + + // TODO: @jgreywolf These tests should ideally be unit tests, as links cannot be verified visually. + it('should render with clickable link when type is not mentioned', () => { + imgSnapshotTest( + ` + classDiagram + Class01~T~ <|-- AveryLongClass : Cool + Class01-T : size() + link Class01 "google.com" "A Tooltip" + ` + ); + }); + + it('should render with clickable callback when type is not mentioned', () => { + imgSnapshotTest( + ` + classDiagram + Class01~T~ <|-- AveryLongClass : Cool + Class01-T : size() + callback Class01 "functionCall" "A Tooltip" + ` + ); + }); + + it('should render with clickable link when type is mentioned', () => { + imgSnapshotTest( + ` + classDiagram + Class01~T~ <|-- AveryLongClass : Cool + Class01-T : size() + link Class01-T "google.com" "A Tooltip" + ` + ); + }); + + it('should render with clickable callback when type is mentioned', () => { + imgSnapshotTest( + ` + classDiagram + Class01~T~ <|-- AveryLongClass : Cool + Class01-T : size() + callback Class01-T "functionCall" "A Tooltip" + ` + ); + }); + }); });