Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update classId generation to use generic type #5223

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions docs/syntax/classDiagram.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<int>>` are supported, though generics that include a comma are currently not supported. (such as `List<List<K, V>>`)

> _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<List<int>>` are supported, though generics that include a comma are currently not supported. (such as `List<List<K, V>>`, 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
Expand Down Expand Up @@ -274,6 +272,42 @@ Square : +getMessages() List~string~
Square : +getDistanceMatrix() List~List~int~~
```

> _note_ `(v<MERMAID_RELEASE_VERSION>+)` 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:
Expand Down
55 changes: 28 additions & 27 deletions packages/mermaid/src/diagrams/class/classDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
jgreywolf marked this conversation as resolved.
Show resolved Hide resolved
};

export const setClassLabel = function (_id: string, label: string) {
Expand All @@ -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: [],
Expand All @@ -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;
};

/**
Expand Down Expand Up @@ -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);
Expand All @@ -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);
};

/**
Expand All @@ -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'));
}
}
};
Expand Down Expand Up @@ -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);
}
});
};
Expand Down
18 changes: 18 additions & 0 deletions packages/mermaid/src/diagrams/class/classDiagram.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
1 change: 1 addition & 0 deletions packages/mermaid/src/diagrams/class/classTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { parseGenericTypes, sanitizeText } from '../common/common.js';

export interface ClassNode {
id: string;
name: string;
type: string;
label: string;
cssClasses: string[];
Expand Down
23 changes: 20 additions & 3 deletions packages/mermaid/src/docs/syntax/classDiagram.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<int>>` are supported, though generics that include a comma are currently not supported. (such as `List<List<K, V>>`)

> _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<List<int>>` are supported, though generics that include a comma are currently not supported. (such as `List<List<K, V>>`, 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
Expand All @@ -162,6 +160,25 @@ Square : +getMessages() List~string~
Square : +getDistanceMatrix() List~List~int~~
```

> _note_ `(v<MERMAID_RELEASE_VERSION>+)` 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
```
jgreywolf marked this conversation as resolved.
Show resolved Hide resolved

#### 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:
Expand Down
Loading