mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-10-28 01:14:11 +01:00
Compare commits
1 Commits
feature/66
...
fix-edge-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7d7fe42aa |
5
.changeset/lucky-cases-switch.md
Normal file
5
.changeset/lucky-cases-switch.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Allow IDs starting with L, R, T, or B in parser
|
||||
@@ -1 +1 @@
|
||||
./packages/mermaid/CHANGELOG.md
|
||||
./packages/mermaid/CHANGELOG.md
|
||||
@@ -1 +1 @@
|
||||
./packages/mermaid/src/docs/community/contributing.md
|
||||
./packages/mermaid/src/docs/community/contributing.md
|
||||
@@ -661,19 +661,6 @@ 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
|
||||
|
||||
@@ -661,19 +661,6 @@ 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
|
||||
|
||||
@@ -510,16 +510,6 @@ 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
|
||||
|
||||
@@ -657,17 +657,6 @@ 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
|
||||
|
||||
@@ -452,20 +452,6 @@ 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(`
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<!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 <<interface>>
|
||||
</pre>
|
||||
|
||||
<h2>Test 2: Inline with class definition (multiple)</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Shape <<interface>> <<injected>>
|
||||
</pre>
|
||||
|
||||
<h2>Test 3: Separate line (single)</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Shape
|
||||
<<interface>> Shape
|
||||
</pre>
|
||||
|
||||
<h2>Test 4: Separate line (multiple)</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Shape
|
||||
<<interface>> <<injected>> Shape
|
||||
</pre>
|
||||
|
||||
<h2>Test 5: Inside class body (single)</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Shape{
|
||||
<<interface>>
|
||||
noOfVertices
|
||||
draw()
|
||||
}
|
||||
</pre>
|
||||
|
||||
<h2>Test 6: Inside class body (multiple on same line)</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Shape{
|
||||
<<interface>> <<injected>>
|
||||
noOfVertices
|
||||
draw()
|
||||
}
|
||||
</pre>
|
||||
|
||||
<h2>Test 7: Combined example</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Shape{
|
||||
<<interface>> <<injected>>
|
||||
noOfVertices
|
||||
draw()
|
||||
}
|
||||
class Color{
|
||||
<<enumeration>>
|
||||
RED
|
||||
BLUE
|
||||
GREEN
|
||||
WHITE
|
||||
BLACK
|
||||
}
|
||||
Shape <|-- Color
|
||||
</pre>
|
||||
|
||||
<hr />
|
||||
<script type="module">
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
mermaid.initialize({
|
||||
theme: 'default',
|
||||
logLevel: 3,
|
||||
securityLevel: 'loose',
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -38,7 +38,7 @@
|
||||
+quack()
|
||||
}
|
||||
class Fish{
|
||||
-int sizeInFeet
|
||||
-Listint sizeInFeet
|
||||
-canEat()
|
||||
}
|
||||
class Zebra{
|
||||
@@ -143,7 +143,21 @@
|
||||
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
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
%x acc_descr_multiline
|
||||
%x class
|
||||
%x class-body
|
||||
%x class-body-annotation
|
||||
%x namespace
|
||||
%x namespace-body
|
||||
%%
|
||||
@@ -83,16 +82,9 @@ 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';
|
||||
@@ -302,26 +294,12 @@ 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); }}
|
||||
;
|
||||
|
||||
|
||||
@@ -333,12 +311,6 @@ 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
|
||||
|
||||
@@ -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 annotationText = node.annotations.map((a: string) => `«${a}»`).join('\n');
|
||||
await addText(annotationGroup, { text: annotationText } as unknown as ClassMember, 0);
|
||||
const annotation = node.annotations[0];
|
||||
await addText(annotationGroup, { text: `«${annotation}»` } as unknown as ClassMember, 0);
|
||||
|
||||
const annotationGroupBBox = annotationGroup.node()!.getBBox();
|
||||
annotationGroupHeight = annotationGroupBBox.height;
|
||||
|
||||
@@ -358,19 +358,17 @@ 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 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:
|
||||
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:
|
||||
|
||||
> **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**:
|
||||
>
|
||||
@@ -380,18 +378,25 @@ Annotations are defined within the opening `<<` and closing `>>`. There are mult
|
||||
> <<interface>> Shape
|
||||
> ```
|
||||
>
|
||||
> ```mermaid-example
|
||||
> classDiagram
|
||||
> class Shape
|
||||
> <<interface>> <<injected>> 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.
|
||||
|
||||
> In a **_nested structure_** along with the class definition:
|
||||
- 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:
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
class Shape{
|
||||
<<interface>> <<injected>>
|
||||
<<interface>>
|
||||
noOfVertices
|
||||
draw()
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ fragment Statement:
|
||||
;
|
||||
|
||||
fragment LeftPort:
|
||||
':'lhsDir=ARROW_DIRECTION
|
||||
':' lhsDir=ID
|
||||
;
|
||||
|
||||
fragment RightPort:
|
||||
rhsDir=ARROW_DIRECTION':'
|
||||
rhsDir=ID ':'
|
||||
;
|
||||
|
||||
fragment Arrow:
|
||||
@@ -47,6 +47,5 @@ Edge:
|
||||
lhsId=ID lhsGroup?=ARROW_GROUP? Arrow rhsId=ID rhsGroup?=ARROW_GROUP? EOL
|
||||
;
|
||||
|
||||
terminal ARROW_DIRECTION: 'L' | 'R' | 'T' | 'B';
|
||||
terminal ARROW_GROUP: /\{group\}/;
|
||||
terminal ARROW_INTO: /<|>/;
|
||||
|
||||
@@ -19,6 +19,64 @@ describe('architecture', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle services', () => {
|
||||
it('should handle service with icon', () => {
|
||||
const context = `architecture-beta
|
||||
service TH(disk)
|
||||
`;
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Architecture);
|
||||
expect(result.value.services).toHaveLength(1);
|
||||
expect(result.value.services?.[0].id).toBe('TH');
|
||||
expect(result.value.services?.[0].icon).toBe('disk');
|
||||
});
|
||||
|
||||
it('should handle service with icon starting with arrow direction letters', () => {
|
||||
const context = `architecture-beta
|
||||
service T(disk)
|
||||
service TH(database)
|
||||
service L(server)
|
||||
service R(cloud)
|
||||
service B(internet)
|
||||
service TOP(disk)
|
||||
service LEFT(disk)
|
||||
service RIGHT(disk)
|
||||
service BOTTOM(disk)
|
||||
`;
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Architecture);
|
||||
expect(result.value.services).toHaveLength(9);
|
||||
});
|
||||
|
||||
it('should handle service with icon and title', () => {
|
||||
const context = `architecture-beta
|
||||
service db(database)[Database]
|
||||
`;
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Architecture);
|
||||
expect(result.value.services).toHaveLength(1);
|
||||
expect(result.value.services?.[0].id).toBe('db');
|
||||
expect(result.value.services?.[0].icon).toBe('database');
|
||||
expect(result.value.services?.[0].title).toBe('Database');
|
||||
});
|
||||
|
||||
it('should handle service in a group', () => {
|
||||
const context = `architecture-beta
|
||||
group api(cloud)[API]
|
||||
service db(database)[Database] in api
|
||||
`;
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Architecture);
|
||||
expect(result.value.services).toHaveLength(1);
|
||||
expect(result.value.services?.[0].id).toBe('db');
|
||||
expect(result.value.services?.[0].in).toBe('api');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle TitleAndAccessibilities', () => {
|
||||
it.each([
|
||||
`architecture-beta title sample title`,
|
||||
|
||||
Reference in New Issue
Block a user