diff --git a/cypress/integration/rendering/classDiagram-elk-v3.spec.js b/cypress/integration/rendering/classDiagram-elk-v3.spec.js index ee6ca0b2b..292d4b2fe 100644 --- a/cypress/integration/rendering/classDiagram-elk-v3.spec.js +++ b/cypress/integration/rendering/classDiagram-elk-v3.spec.js @@ -661,6 +661,19 @@ class Class10 { logLevel: 1, htmlLabels: true, layout: 'elk' } ); }); + + it('ELK: should render a class with a text label, members and multiple annotations', () => { + imgSnapshotTest( + `classDiagram + class C1["Class 1 with text label"] { + <<interface>> <<injected>> + +member1 + } + C1 --> C2`, + { logLevel: 1, htmlLabels: true, layout: 'elk' } + ); + }); + it('ELK: should render multiple classes with same text labels', () => { imgSnapshotTest( `classDiagram diff --git a/cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js b/cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js index 32a82c089..a6890379f 100644 --- a/cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js +++ b/cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js @@ -661,6 +661,19 @@ class Class10 { logLevel: 1, htmlLabels: true, look: 'handDrawn' } ); }); + + it('HD: should render a class with a text label, membersand multiple annotations', () => { + imgSnapshotTest( + `classDiagram + class C1["Class 1 with text label"] { + <<interface>> <<injected>> + +member1 + } + C1 --> C2`, + { logLevel: 1, htmlLabels: true, look: 'handDrawn' } + ); + }); + it('HD: should render multiple classes with same text labels', () => { imgSnapshotTest( `classDiagram diff --git a/cypress/integration/rendering/classDiagram-v2.spec.js b/cypress/integration/rendering/classDiagram-v2.spec.js index 0c5dbc04b..d905ae4c7 100644 --- a/cypress/integration/rendering/classDiagram-v2.spec.js +++ b/cypress/integration/rendering/classDiagram-v2.spec.js @@ -510,6 +510,16 @@ class Class10 C1 --> C2` ); }); + it('should render a class with a text label, members and multiple annotations', () => { + imgSnapshotTest( + `classDiagram + class C1["Class 1 with text label"] { + <<interface>> <<injected>> + +member1 + } + C1 --> C2` + ); + }); it('should render multiple classes with same text labels', () => { imgSnapshotTest( `classDiagram diff --git a/cypress/integration/rendering/classDiagram-v3.spec.js b/cypress/integration/rendering/classDiagram-v3.spec.js index 626d6fcea..68412fe81 100644 --- a/cypress/integration/rendering/classDiagram-v3.spec.js +++ b/cypress/integration/rendering/classDiagram-v3.spec.js @@ -657,6 +657,17 @@ class Class10 C1 --> C2` ); }); + it('should render a class with a text label, members and multiple annotations', () => { + imgSnapshotTest( + `classDiagram + class C1["Class 1 with text label"] { + <<interface>> <<injected>> + +member1 + } + C1 --> C2` + ); + }); + it('should render multiple classes with same text labels', () => { imgSnapshotTest( `classDiagram diff --git a/cypress/integration/rendering/classDiagram.spec.js b/cypress/integration/rendering/classDiagram.spec.js index 6cea402f8..a629ab982 100644 --- a/cypress/integration/rendering/classDiagram.spec.js +++ b/cypress/integration/rendering/classDiagram.spec.js @@ -452,6 +452,20 @@ describe('Class diagram', () => { <<Interface>> \`This\nTitle\nHas\nMany\nNewlines\` `); }); + it('should render with newlines in title and multiple annotations', () => { + imgSnapshotTest(` + classDiagram + class \`This\nTitle\nHas\nMany\nNewlines\` { + +String Also + -String Many + #int Members + +And() + -Many() + #Methods() + } + <<Interface>> <<Service>> \`This\nTitle\nHas\nMany\nNewlines\` + `); + }); it('should handle newline title in namespace', () => { imgSnapshotTest(` diff --git a/demos/class-multiple-stereotypes.html b/demos/class-multiple-stereotypes.html new file mode 100644 index 000000000..86eb91ac4 --- /dev/null +++ b/demos/class-multiple-stereotypes.html @@ -0,0 +1,93 @@ + + +
+ + ++ classDiagram + class Shape <<interface>> ++ +
+ classDiagram + class Shape <<interface>> <<injected>> ++ +
+ classDiagram + class Shape + <<interface>> Shape ++ +
+ classDiagram + class Shape + <<interface>> <<injected>> Shape ++ +
+ classDiagram + class Shape{ + <<interface>> + noOfVertices + draw() + } ++ +
+ classDiagram + class Shape{ + <<interface>> <<injected>> + noOfVertices + draw() + } ++ +
+ classDiagram + class Shape{ + <<interface>> <<injected>> + noOfVertices + draw() + } + class Color{ + <<enumeration>> + RED + BLUE + GREEN + WHITE + BLACK + } + Shape <|-- Color ++ +
- classDiagram - class Person { - +ID : Guid - +FirstName : string - +LastName : string - -privateProperty : string - #ProtectedProperty : string - ~InternalProperty : string - ~AnotherInternalProperty : List~List~string~~ - } - class People List~List~Person~~ -
classDiagram diff --git a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison index 9a1f991a7..15daa3c85 100644 --- a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison +++ b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison @@ -18,6 +18,7 @@ %x acc_descr_multiline %x class %x class-body +%x class-body-annotation %x namespace %x namespace-body %% @@ -82,9 +83,16 @@ Function arguments are optional: 'call()' simply executes 'callb < > return "EOF_IN_STRUCT"; "[*]" { return 'EDGE_STATE';} [{] return "OPEN_IN_STRUCT"; + "<<" { this.begin("class-body-annotation"); return 'ANNOTATION_START';} [\n] /* nothing */ + [ \t]+ /* skip whitespace in class body */ [^{}\n]* { return "MEMBER";} + ">>" { this.popState(); return 'ANNOTATION_END';} + [0-9]+ return 'NUM'; + \w+ return 'ALPHA'; + [\s]+ /* ignore whitespace */ + <*>"cssClass" return 'CSSCLASS'; <*>"callback" return 'CALLBACK'; <*>"link" return 'LINK'; @@ -294,12 +302,26 @@ classStatement | classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);} | classIdentifier STRUCT_START members STRUCT_STOP {yy.addMembers($1,$3);} | classIdentifier STRUCT_START STRUCT_STOP {} + | classIdentifier STRUCT_START NEWLINE members STRUCT_STOP {yy.addMembers($1,$4);} + | classIdentifier STRUCT_START annotationList members STRUCT_STOP {for(const annotation of $3) { yy.addAnnotation($1, annotation); } yy.addMembers($1,$4);} + | classIdentifier STRUCT_START annotationList STRUCT_STOP {for(const annotation of $3) { yy.addAnnotation($1, annotation); }} + | classIdentifier STRUCT_START NEWLINE annotationList members STRUCT_STOP {for(const annotation of $4) { yy.addAnnotation($1, annotation); } yy.addMembers($1,$5);} + | classIdentifier STRUCT_START NEWLINE annotationList STRUCT_STOP {for(const annotation of $4) { yy.addAnnotation($1, annotation); }} | classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.setCssClass($1, $3);yy.addMembers($1,$5);} + | classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START NEWLINE members STRUCT_STOP {yy.setCssClass($1, $3);yy.addMembers($1,$6);} + | classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START annotationList members STRUCT_STOP {yy.setCssClass($1, $3); for(const annotation of $5) { yy.addAnnotation($1, annotation); } yy.addMembers($1,$6);} + | classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START annotationList STRUCT_STOP {yy.setCssClass($1, $3); for(const annotation of $5) { yy.addAnnotation($1, annotation); }} + | classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START NEWLINE annotationList members STRUCT_STOP {yy.setCssClass($1, $3); for(const annotation of $6) { yy.addAnnotation($1, annotation); } yy.addMembers($1,$7);} + | classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START NEWLINE annotationList STRUCT_STOP {yy.setCssClass($1, $3); for(const annotation of $6) { yy.addAnnotation($1, annotation); }} ; classIdentifier : CLASS className {$$=$2; yy.addClass($2);} + | CLASS className ANNOTATION_START alphaNumToken ANNOTATION_END {$$=$2; yy.addClass($2); yy.addAnnotation($2,$4);} + | CLASS className annotationList {$$=$2; yy.addClass($2); for(const annotation of $3) { yy.addAnnotation($2, annotation); }} | CLASS className classLabel {$$=$2; yy.addClass($2);yy.setClassLabel($2, $3);} + | CLASS className classLabel ANNOTATION_START alphaNumToken ANNOTATION_END {$$=$2; yy.addClass($2);yy.setClassLabel($2, $3); yy.addAnnotation($2,$5);} + | CLASS className classLabel annotationList {$$=$2; yy.addClass($2);yy.setClassLabel($2, $3); for(const annotation of $4) { yy.addAnnotation($2, annotation); }} ; @@ -311,6 +333,12 @@ emptyBody annotationStatement : ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); } + | annotationList className { for(const annotation of $1) { yy.addAnnotation($2, annotation); } } + ; + +annotationList + : ANNOTATION_START alphaNumToken ANNOTATION_END { $$ = [$2]; } + | annotationList ANNOTATION_START alphaNumToken ANNOTATION_END { $1.push($3); $$ = $1; } ; members diff --git a/packages/mermaid/src/diagrams/class/shapeUtil.ts b/packages/mermaid/src/diagrams/class/shapeUtil.ts index 94c8f817a..cdf9e8a31 100644 --- a/packages/mermaid/src/diagrams/class/shapeUtil.ts +++ b/packages/mermaid/src/diagrams/class/shapeUtil.ts @@ -36,8 +36,8 @@ export async function textHelper ( annotationGroup = shapeSvg.insert('g').attr('class', 'annotation-group text'); if (node.annotations.length > 0) { - const annotation = node.annotations[0]; - await addText(annotationGroup, { text: `«${annotation}»` } as unknown as ClassMember, 0); + const annotationText = node.annotations.map((a: string) => `«${a}»`).join('\n'); + await addText(annotationGroup, { text: annotationText } as unknown as ClassMember, 0); const annotationGroupBBox = annotationGroup.node()!.getBBox(); annotationGroupHeight = annotationGroupBBox.height; diff --git a/packages/mermaid/src/docs/syntax/classDiagram.md b/packages/mermaid/src/docs/syntax/classDiagram.md index ed69bd223..a98b3cd41 100644 --- a/packages/mermaid/src/docs/syntax/classDiagram.md +++ b/packages/mermaid/src/docs/syntax/classDiagram.md @@ -358,17 +358,19 @@ It is possible to annotate classes with markers to provide additional metadata a - `< >` To represent a service class - `< >` To represent an enum -Annotations are defined within the opening `<<` and closing `>>`. There are two ways to add an annotation to a class, and either way the output will be same: +Annotations are defined within the opening `<<` and closing `>>`. There are multiple ways to add an annotation to a class, which all result in the same output, and you can add multiple annotations by adding others on the same line: -> **Tip:** -> In Mermaid class diagrams, annotations like `< >` can be attached in two ways: -> > - **Inline with the class definition** (Recommended for consistency): > > ```mermaid-example > classDiagram > class Shape < > > ``` + +> ```mermaid-example +> classDiagram +> class Shape < > < > +> ``` > > - **Separate line after the class definition**: > @@ -378,25 +380,18 @@ Annotations are defined within the opening `<<` and closing `>>`. There are two > < > Shape > ``` > -> Both methods are fully supported and produce identical diagrams. -> However, it is recommended to use the **inline style** for better readability and consistent formatting across diagrams. +> ```mermaid-example +> classDiagram +> class Shape +> < > < > Shape +> ``` -- In a **_separate line_** after a class is defined: - -```mermaid-example -classDiagram -class Shape -< > Shape -Shape : noOfVertices -Shape : draw() -``` - -- In a **_nested structure_** along with the class definition: +> In a **_nested structure_** along with the class definition: ```mermaid-example classDiagram class Shape{ - < > + < > < > noOfVertices draw() }