Compare commits

..

7 Commits

Author SHA1 Message Date
Justin Greywolf
391a1f5827 resolving lint errors 2025-10-21 14:36:55 -07:00
Justin Greywolf
b6b666d705 feat: support multiple stereotypes in class diagrams 2025-10-21 14:31:14 -07:00
Knut Sveidqvist
fed8a523a4 Merge pull request #7076 from mermaid-js/6919-fix-incorrect-viewBox-casing
6919: correct viewBox casing in Radar & Packet
2025-10-21 12:20:04 +00:00
Knut Sveidqvist
33b4946e21 Merge branch 'develop' into 6919-fix-incorrect-viewBox-casing 2025-10-21 10:33:21 +02:00
darshanr0107
76e17ffd20 fix: add validation to ensure correct casing of 'viewBox' in all rendering tests
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-15 18:56:49 +05:30
darshanr0107
60f633101c chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-13 19:14:22 +05:30
darshanr0107
7def6eecbf fix: correct viewBox casing in radar and packet diagram
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-13 18:15:38 +05:30
19 changed files with 211 additions and 59 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Correct viewBox casing and make SVGs responsive

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Handle backslash parsing in math formulas within new flowchart shape syntax

View File

@@ -1 +1 @@
./packages/mermaid/CHANGELOG.md
./packages/mermaid/CHANGELOG.md

View File

@@ -1 +1 @@
./packages/mermaid/src/docs/community/contributing.md
./packages/mermaid/src/docs/community/contributing.md

View File

@@ -99,6 +99,7 @@ export const openURLAndVerifyRendering = (
cy.visit(url);
cy.window().should('have.property', 'rendered', true);
cy.get('svg').should('be.visible');
cy.get('svg').should('not.have.attr', 'viewbox');
if (validation) {
cy.get('svg').should(validation);

View File

@@ -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"] {
&lt;&lt;interface&gt;&gt; &lt;&lt;injected&gt;&gt;
+member1
}
C1 --> C2`,
{ logLevel: 1, htmlLabels: true, layout: 'elk' }
);
});
it('ELK: should render multiple classes with same text labels', () => {
imgSnapshotTest(
`classDiagram

View File

@@ -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"] {
&lt;&lt;interface&gt;&gt; &lt;&lt;injected&gt;&gt;
+member1
}
C1 --> C2`,
{ logLevel: 1, htmlLabels: true, look: 'handDrawn' }
);
});
it('HD: should render multiple classes with same text labels', () => {
imgSnapshotTest(
`classDiagram

View File

@@ -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"] {
&lt;&lt;interface&gt;&gt; &lt;&lt;injected&gt;&gt;
+member1
}
C1 --> C2`
);
});
it('should render multiple classes with same text labels', () => {
imgSnapshotTest(
`classDiagram

View File

@@ -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"] {
&lt;&lt;interface&gt;&gt; &lt;&lt;injected&gt;&gt;
+member1
}
C1 --> C2`
);
});
it('should render multiple classes with same text labels', () => {
imgSnapshotTest(
`classDiagram

View File

@@ -452,6 +452,20 @@ describe('Class diagram', () => {
&lt;&lt;Interface&gt;&gt; \`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()
}
&lt;&lt;Interface&gt;&gt; &lt;&lt;Service&gt;&gt; \`This\nTitle\nHas\nMany\nNewlines\`
`);
});
it('should handle newline title in namespace', () => {
imgSnapshotTest(`

View File

@@ -0,0 +1,93 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Class Diagram - Multiple Stereotypes Test</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
<style>
div.mermaid {
font-family: 'Courier New', Courier, monospace !important;
}
</style>
</head>
<body>
<h1>Multiple Stereotypes Test</h1>
<h2>Test 1: Inline with class definition (single)</h2>
<pre class="mermaid">
classDiagram
class Shape &lt;&lt;interface&gt;&gt;
</pre>
<h2>Test 2: Inline with class definition (multiple)</h2>
<pre class="mermaid">
classDiagram
class Shape &lt;&lt;interface&gt;&gt; &lt;&lt;injected&gt;&gt;
</pre>
<h2>Test 3: Separate line (single)</h2>
<pre class="mermaid">
classDiagram
class Shape
&lt;&lt;interface&gt;&gt; Shape
</pre>
<h2>Test 4: Separate line (multiple)</h2>
<pre class="mermaid">
classDiagram
class Shape
&lt;&lt;interface&gt;&gt; &lt;&lt;injected&gt;&gt; Shape
</pre>
<h2>Test 5: Inside class body (single)</h2>
<pre class="mermaid">
classDiagram
class Shape{
&lt;&lt;interface&gt;&gt;
noOfVertices
draw()
}
</pre>
<h2>Test 6: Inside class body (multiple on same line)</h2>
<pre class="mermaid">
classDiagram
class Shape{
&lt;&lt;interface&gt;&gt; &lt;&lt;injected&gt;&gt;
noOfVertices
draw()
}
</pre>
<h2>Test 7: Combined example</h2>
<pre class="mermaid">
classDiagram
class Shape{
&lt;&lt;interface&gt;&gt; &lt;&lt;injected&gt;&gt;
noOfVertices
draw()
}
class Color{
&lt;&lt;enumeration&gt;&gt;
RED
BLUE
GREEN
WHITE
BLACK
}
Shape &lt;|-- Color
</pre>
<hr />
<script type="module">
import mermaid from './mermaid.esm.mjs';
mermaid.initialize({
theme: 'default',
logLevel: 3,
securityLevel: 'loose',
});
</script>
</body>
</html>

View File

@@ -38,7 +38,7 @@
+quack()
}
class Fish{
-Listint sizeInFeet
-int sizeInFeet
-canEat()
}
class Zebra{
@@ -143,21 +143,7 @@
Pineapple : -int leafCount()
Pineapple : -int spikeCount()
</pre>
<hr />
<pre class="mermaid">
classDiagram
class Person {
+ID : Guid
+FirstName : string
+LastName : string
-privateProperty : string
#ProtectedProperty : string
~InternalProperty : string
~AnotherInternalProperty : List~List~string~~
}
class People List~List~Person~~
</pre>
<hr />
<pre class="mermaid">
classDiagram

View File

@@ -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 <callback_name>()' simply executes 'callb
<class-body><<EOF>> return "EOF_IN_STRUCT";
<class-body>"[*]" { return 'EDGE_STATE';}
<class-body>[{] return "OPEN_IN_STRUCT";
<class-body>"<<" { this.begin("class-body-annotation"); return 'ANNOTATION_START';}
<class-body>[\n] /* nothing */
<class-body>[ \t]+ /* skip whitespace in class body */
<class-body>[^{}\n]* { return "MEMBER";}
<class-body-annotation>">>" { this.popState(); return 'ANNOTATION_END';}
<class-body-annotation>[0-9]+ return 'NUM';
<class-body-annotation>\w+ return 'ALPHA';
<class-body-annotation>[\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

View File

@@ -36,8 +36,8 @@ export async function textHelper<T extends SVGGraphicsElement>(
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;

View File

@@ -144,16 +144,6 @@ describe('when parsing directions', function () {
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('DD');
});
it('should handle mathematical formulas with backslashes in quoted strings', function () {
const res = flow.parser.parse(`flowchart TB
A@{ shape: rect, label: "$$\\sin x$$"}`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('rect');
expect(data4Layout.nodes[0].label).toEqual('$$\\sin x$$');
});
it('should be possible to link to a node with more data', function () {
const res = flow.parser.parse(`flowchart TB
A --> D@{

View File

@@ -53,7 +53,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multilin
// console.log('shapeData', yytext);
const re = /\n\s*/g;
yytext = yytext.replace(re,"<br/>");
yytext = yytext.replace(/\\/g, "\\\\");
return 'SHAPE_DATA'}
<shapeData>[^}^"]+ {
// console.log('shapeData', yytext);

View File

@@ -16,7 +16,7 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => {
const svgWidth = bitWidth * bitsPerRow + 2;
const svg: SVG = selectSvgElement(id);
svg.attr('viewbox', `0 0 ${svgWidth} ${svgHeight}`);
svg.attr('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
configureSvgSize(svg, svgHeight, svgWidth, config.useMaxWidth);
for (const [word, packet] of words.entries()) {

View File

@@ -2,6 +2,7 @@ import type { Diagram } from '../../Diagram.js';
import type { RadarDiagramConfig } from '../../config.type.js';
import type { DiagramRenderer, DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import type { RadarDB, RadarAxis, RadarCurve } from './types.js';
const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => {
@@ -53,11 +54,9 @@ const drawFrame = (svg: SVG, config: Required<RadarDiagramConfig>): SVGGroup =>
x: config.marginLeft + config.width / 2,
y: config.marginTop + config.height / 2,
};
// Initialize the SVG
svg
.attr('viewbox', `0 0 ${totalWidth} ${totalHeight}`)
.attr('width', totalWidth)
.attr('height', totalHeight);
configureSvgSize(svg, totalHeight, totalWidth, config.useMaxWidth ?? true);
svg.attr('viewBox', `0 0 ${totalWidth} ${totalHeight}`);
// g element to center the radar chart
return svg.append('g').attr('transform', `translate(${center.x}, ${center.y})`);
};

View File

@@ -358,17 +358,19 @@ It is possible to annotate classes with markers to provide additional metadata a
- `<<Service>>` To represent a service class
- `<<Enumeration>>` 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 `<<interface>>` can be attached in two ways:
>
> - **Inline with the class definition** (Recommended for consistency):
>
> ```mermaid-example
> classDiagram
> class Shape <<interface>>
> ```
> ```mermaid-example
> classDiagram
> class Shape <<interface>> <<injected>>
> ```
>
> - **Separate line after the class definition**:
>
@@ -378,25 +380,18 @@ Annotations are defined within the opening `<<` and closing `>>`. There are two
> <<interface>> 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
> <<interface>> <<injected>> Shape
> ```
- In a **_separate line_** after a class is defined:
```mermaid-example
classDiagram
class Shape
<<interface>> 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{
<<interface>>
<<interface>> <<injected>>
noOfVertices
draw()
}