Compare commits

..

2 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
13 changed files with 207 additions and 337 deletions

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

View File

@@ -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(`

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

@@ -266,156 +266,4 @@ describe('[Arrows] when parsing', () => {
});
});
});
describe('Issue #2492: Node names starting with o/x should not be consumed by arrow markers', () => {
it('should handle node names starting with "o" after plain arrows', function () {
const res = flow.parser.parse('graph TD;\ndev---ops;');
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('dev').id).toBe('dev');
expect(vert.get('ops').id).toBe('ops');
expect(edges.length).toBe(1);
expect(edges[0].start).toBe('dev');
expect(edges[0].end).toBe('ops');
expect(edges[0].type).toBe('arrow_open');
expect(edges[0].stroke).toBe('normal');
});
it('should handle node names starting with "x" after plain arrows', function () {
const res = flow.parser.parse('graph TD;\ndev---xerxes;');
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('dev').id).toBe('dev');
expect(vert.get('xerxes').id).toBe('xerxes');
expect(edges.length).toBe(1);
expect(edges[0].start).toBe('dev');
expect(edges[0].end).toBe('xerxes');
expect(edges[0].type).toBe('arrow_open');
expect(edges[0].stroke).toBe('normal');
});
it('should still support circle arrows with spaces', function () {
const res = flow.parser.parse('graph TD;\nA --o B;');
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('A').id).toBe('A');
expect(vert.get('B').id).toBe('B');
expect(edges.length).toBe(1);
expect(edges[0].start).toBe('A');
expect(edges[0].end).toBe('B');
expect(edges[0].type).toBe('arrow_circle');
expect(edges[0].stroke).toBe('normal');
});
it('should still support cross arrows with spaces', function () {
const res = flow.parser.parse('graph TD;\nC --x D;');
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('C').id).toBe('C');
expect(vert.get('D').id).toBe('D');
expect(edges.length).toBe(1);
expect(edges[0].start).toBe('C');
expect(edges[0].end).toBe('D');
expect(edges[0].type).toBe('arrow_cross');
expect(edges[0].stroke).toBe('normal');
});
it('should support circle arrows to uppercase nodes without spaces', function () {
const res = flow.parser.parse('graph TD;\nA--oB;');
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('A').id).toBe('A');
expect(vert.get('B').id).toBe('B');
expect(edges.length).toBe(1);
expect(edges[0].start).toBe('A');
expect(edges[0].end).toBe('B');
expect(edges[0].type).toBe('arrow_circle');
expect(edges[0].stroke).toBe('normal');
});
it('should support cross arrows to uppercase nodes without spaces', function () {
const res = flow.parser.parse('graph TD;\nA--xBar;');
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('A').id).toBe('A');
expect(vert.get('Bar').id).toBe('Bar');
expect(edges.length).toBe(1);
expect(edges[0].start).toBe('A');
expect(edges[0].end).toBe('Bar');
expect(edges[0].type).toBe('arrow_cross');
expect(edges[0].stroke).toBe('normal');
});
it('should handle thick arrows with lowercase node names starting with "o"', function () {
const res = flow.parser.parse('graph TD;\nalpha===omega;');
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('alpha').id).toBe('alpha');
expect(vert.get('omega').id).toBe('omega');
expect(edges.length).toBe(1);
expect(edges[0].start).toBe('alpha');
expect(edges[0].end).toBe('omega');
expect(edges[0].type).toBe('arrow_open');
expect(edges[0].stroke).toBe('thick');
});
it('should handle dotted arrows with lowercase node names starting with "o"', function () {
const res = flow.parser.parse('graph TD;\nfoo-.-opus;');
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('foo').id).toBe('foo');
expect(vert.get('opus').id).toBe('opus');
expect(edges.length).toBe(1);
expect(edges[0].start).toBe('foo');
expect(edges[0].end).toBe('opus');
expect(edges[0].type).toBe('arrow_open');
expect(edges[0].stroke).toBe('dotted');
});
it('should still support dotted circle arrows with spaces', function () {
const res = flow.parser.parse('graph TD;\nB -.-o C;');
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('B').id).toBe('B');
expect(vert.get('C').id).toBe('C');
expect(edges.length).toBe(1);
expect(edges[0].start).toBe('B');
expect(edges[0].end).toBe('C');
expect(edges[0].type).toBe('arrow_circle');
expect(edges[0].stroke).toBe('dotted');
});
it('should still support thick cross arrows with spaces', function () {
const res = flow.parser.parse('graph TD;\nC ==x D;');
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('C').id).toBe('C');
expect(vert.get('D').id).toBe('D');
expect(edges.length).toBe(1);
expect(edges[0].start).toBe('C');
expect(edges[0].end).toBe('D');
expect(edges[0].type).toBe('arrow_cross');
expect(edges[0].stroke).toBe('thick');
});
});
});

View File

@@ -1,129 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import flow from './flowParser.js';
import { FlowDB } from '../flowDb.js';
describe('Flowchart arrow parsing - Issue #2492', () => {
let flowDb: FlowDB;
beforeEach(() => {
flowDb = new FlowDB();
flow.parser.yy = flowDb;
flowDb.clear();
});
describe('Solid arrows with markers', () => {
it('should parse --> followed by uppercase node', () => {
const diagram = 'graph TD\nA-->B';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse --> followed by lowercase node', () => {
const diagram = 'graph TD\nA-->b';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse --> followed by space', () => {
const diagram = 'graph TD\nA--> B';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse --- followed by uppercase node (issue #2492)', () => {
const diagram = 'graph TD\ndev---Ops';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse --- followed by lowercase node (issue #2492)', () => {
const diagram = 'graph TD\ndev---ops';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse --o followed by uppercase node', () => {
const diagram = 'graph TD\nA--oB';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse --o followed by lowercase node', () => {
const diagram = 'graph TD\nA--ob';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse --x followed by uppercase node', () => {
const diagram = 'graph TD\nA--xBar';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse --x followed by lowercase node', () => {
const diagram = 'graph TD\nA--xbar';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
});
describe('Thick arrows with markers', () => {
it('should parse ==> followed by uppercase node', () => {
const diagram = 'graph TD\nA==>B';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse ==> followed by lowercase node', () => {
const diagram = 'graph TD\nA==>b';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse === followed by lowercase node', () => {
const diagram = 'graph TD\nA===b';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
});
describe('Dotted arrows with markers', () => {
it('should parse -.-> followed by uppercase node', () => {
const diagram = 'graph TD\nA-.->B';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse -.-> followed by lowercase node', () => {
const diagram = 'graph TD\nA-.->b';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse -.- followed by lowercase node', () => {
const diagram = 'graph TD\nA-.-b';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
});
describe('Arrows with edge text', () => {
it('should parse arrow with edge text followed by uppercase node', () => {
const diagram = 'graph TD\nA-->|text|B';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse arrow with edge text followed by lowercase node', () => {
const diagram = 'graph TD\nA-->|text|b';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse multiple arrows with edge text (regression test)', () => {
const diagram = 'graph TD\nA-->|Get money|B\nB-->C\nC-->|One|D\nC-->|Two|E';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
});
describe('Arrows followed by digits', () => {
it('should parse --> followed by digit', () => {
const diagram = 'graph LR\n47-->48';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse --> followed by node starting with digit', () => {
const diagram = 'graph LR\nA-->48(Node)';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
it('should parse complex diagram with digit node IDs (Sample 4)', () => {
const diagram =
'graph LR\n47(SAM.CommonFA.FMESummary)-->48(SAM.CommonFA.CommonFAFinanceBudget)\n37(SAM.CommonFA.BudgetSubserviceLineVolume)-->48(SAM.CommonFA.CommonFAFinanceBudget)';
expect(() => flow.parser.parse(diagram)).not.toThrow();
});
});
});

View File

@@ -152,29 +152,17 @@ that id.
"," return 'COMMA';
"*" return 'MULT';
<INITIAL,edgeText>\s*[xo<]?\-\-+[-xo>]\s+ { this.popState(); return 'LINK'; }
<INITIAL>\s*[xo<]?\-\-+[-xo>](?=[A-Z]) { return 'LINK'; }
<INITIAL>\s*[xo<]?\-\-+[-xo>](?=[a-z]) { return 'LINK'; }
<INITIAL>\s*[xo<]?\-\-+[-xo>](?=[0-9]) { return 'LINK'; }
<INITIAL,edgeText>\s*[xo<]?\-\-+[-xo>](?=\s*\|) { this.popState(); return 'LINK'; }
<INITIAL>\s*[xo<]?\-\-\s* { this.pushState("edgeText"); return 'START_LINK'; }
<edgeText>[^-]|\-(?!\-)+ return 'EDGE_TEXT';
<INITIAL,edgeText>\s*[xo<]?\-\-+[-xo>]\s* { this.popState(); return 'LINK'; }
<INITIAL>\s*[xo<]?\-\-\s* { this.pushState("edgeText"); return 'START_LINK'; }
<edgeText>[^-]|\-(?!\-)+ return 'EDGE_TEXT';
<INITIAL,thickEdgeText>\s*[xo<]?\=\=+[=xo>]\s+ { this.popState(); return 'LINK'; }
<INITIAL>\s*[xo<]?\=\=+[=xo>](?=[A-Z]) { return 'LINK'; }
<INITIAL>\s*[xo<]?\=\=+[=xo>](?=[a-z]) { return 'LINK'; }
<INITIAL>\s*[xo<]?\=\=+[=xo>](?=[0-9]) { return 'LINK'; }
<INITIAL,thickEdgeText>\s*[xo<]?\=\=+[=xo>](?=\s*\|) { this.popState(); return 'LINK'; }
<INITIAL>\s*[xo<]?\=\=\s* { this.pushState("thickEdgeText"); return 'START_LINK'; }
<thickEdgeText>[^=]|\=(?!=) return 'EDGE_TEXT';
<INITIAL,thickEdgeText>\s*[xo<]?\=\=+[=xo>]\s* { this.popState(); return 'LINK'; }
<INITIAL>\s*[xo<]?\=\=\s* { this.pushState("thickEdgeText"); return 'START_LINK'; }
<thickEdgeText>[^=]|\=(?!=) return 'EDGE_TEXT';
<INITIAL,dottedEdgeText>\s*[xo<]?\-?\.+\-[xo>]?\s+ { this.popState(); return 'LINK'; }
<INITIAL>\s*[xo<]?\-?\.+\-[xo>]?(?=[A-Z]) { return 'LINK'; }
<INITIAL>\s*[xo<]?\-?\.+\-[xo>]?(?=[a-z]) { return 'LINK'; }
<INITIAL>\s*[xo<]?\-?\.+\-[xo>]?(?=[0-9]) { return 'LINK'; }
<INITIAL,dottedEdgeText>\s*[xo<]?\-?\.+\-[xo>]?(?=\s*\|) { this.popState(); return 'LINK'; }
<INITIAL>\s*[xo<]?\-\.\s* { this.pushState("dottedEdgeText"); return 'START_LINK'; }
<dottedEdgeText>[^\.]|\.(?!-) return 'EDGE_TEXT';
<INITIAL,dottedEdgeText>\s*[xo<]?\-?\.+\-[xo>]?\s* { this.popState(); return 'LINK'; }
<INITIAL>\s*[xo<]?\-\.\s* { this.pushState("dottedEdgeText"); return 'START_LINK'; }
<dottedEdgeText>[^\.]|\.(?!-) return 'EDGE_TEXT';
<*>\s*\~\~[\~]+\s* return 'LINK';

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()
}