diff --git a/demos/classchart.html b/demos/classchart.html
index 10d8e6b70..d46fc8004 100644
--- a/demos/classchart.html
+++ b/demos/classchart.html
@@ -33,12 +33,15 @@
Animal: +mate()
class Duck{
+ <<injected>>
+ <<interface>>
+
+String beakColor
+swim()
+quack()
}
class Fish{
- -Listint sizeInFeet
+ -List~int~ sizeInFeet
-canEat()
}
class Zebra{
@@ -56,6 +59,8 @@
Class01 <|-- AveryLongClass : Cool
<<interface>> Class01
+ <<injected>> Class01
+
Class03 "0" *-- "0..n" Class04
Class05 "1" o-- "many" Class06
Class07 .. Class08
@@ -156,7 +161,7 @@
~InternalProperty : string
~AnotherInternalProperty : List~List~string~~
}
- class People List~List~Person~~
+ class People List~Person~
diff --git a/demos/multiple-annotations-test.html b/demos/multiple-annotations-test.html
new file mode 100644
index 000000000..0c1385680
--- /dev/null
+++ b/demos/multiple-annotations-test.html
@@ -0,0 +1,118 @@
+
+
+
+
+
+ Multiple Annotations Test
+
+
+
+ Multiple Annotations Test - Issue #6680 Fix
+
+
+ Testing: Multiple stereotypes/annotations should now render correctly using
+ external and inline annotation methods.
+
+
+ Baseline - no annotations
+
+classDiagram
+ class Shape
+ class Circle
+ class Triangle
+
+
+ #0 Baseline - single annotation
+
+classDiagram
+ class Shape
+ class Circle
+ class Triangle
+
+ <<injected>> Shape
+
+
+ Method 1: External/Next-line Annotations
+
+ External annotations defined on the line after the class definition:
+
+
+ classDiagram
+ class Shape
+ <<interface>> <<injected>> Shape
+ class Circle
+ <<abstract>> <<serializable>> Circle
+ class Triangle
+ <<interface>> <<cached>> <<singleton>> Triangle
+
+
+ Method 2: Inline Annotations
+ Inline annotations defined directly with the class definition:
+
+ classDiagram
+ class Shape <<interface>> <<injected>>
+ class Circle <<abstract>> <<serializable>>
+ class Square <<service>> <<singleton>> <<cached>>
+ class Triangle <<interface>> <<component>> <<transient>>
+
+
+ Method 3: Mixed Methods Test
+ Combination of both external and inline annotation methods:
+
+ classDiagram
+ class Component <<interface>> <<injected>>
+ class Service
+ <<abstract>> <<singleton>> Service
+ class Repository <<dao>> <<cached>>
+ class Controller
+ <<rest>> <<secured>> Controller
+
+
+ Real-world Example
+ A practical example with relationships and multiple annotations:
+
+ classDiagram
+ class BaseService <<abstract>> <<injectable>>
+ class UserService <<service>> <<singleton>>
+ class UserRepository <<repository>> <<cached>>
+ class UserController
+ <<controller>> <<secured>> UserController
+
+ BaseService <|-- UserService
+ UserService --> UserRepository
+ UserController --> UserService
+
+
+
+
+
diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts
index 544a25a46..46e9963d5 100644
--- a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts
+++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts
@@ -241,6 +241,31 @@ describe('given a basic class diagram, ', function () {
expect(c1.annotations[0]).toBe('interface');
});
+ it('should handle multiple external annotations', () => {
+ const str = `classDiagram
+ <><> Car
+ Car : int size
+ Car : int age`;
+
+ parser.parse(str);
+ const carClass = classDb.getClass('Car');
+ expect(carClass.annotations).toHaveLength(2);
+ expect(carClass.annotations).toContain('interface');
+ expect(carClass.annotations).toContain('service');
+ });
+
+ it('should handle multiple annotations inline with class definition', () => {
+ const str = `classDiagram
+ class Car <> <>
+ Car : int size`;
+
+ parser.parse(str);
+ const carClass = classDb.getClass('Car');
+ expect(carClass.annotations).toHaveLength(2);
+ expect(carClass.annotations).toContain('interface');
+ expect(carClass.annotations).toContain('service');
+ });
+
it('should parse a class with text label and css class shorthand', () => {
const str = 'classDiagram\n' + 'class C1["Class 1 with text label"]:::styleClass';
diff --git a/packages/mermaid/src/diagrams/class/classTypes.attribute.spec.ts b/packages/mermaid/src/diagrams/class/classTypes.attribute.spec.ts
new file mode 100644
index 000000000..73942b344
--- /dev/null
+++ b/packages/mermaid/src/diagrams/class/classTypes.attribute.spec.ts
@@ -0,0 +1,317 @@
+import { ClassMember } from './classTypes.js';
+import { vi, describe, it, expect } from 'vitest';
+const spyOn = vi.spyOn;
+
+const staticCssStyle = 'text-decoration:underline;';
+const abstractCssStyle = 'font-style:italic;';
+const abstractStaticCssStyle = 'text-decoration:underline;font-style:italic;';
+
+describe('ClassTypes - Attribute Tests', () => {
+ describe('Basic attribute parsing without classifiers', () => {
+ it('should parse attribute with no modifiers', () => {
+ const str = 'name String';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('name String');
+ expect(displayDetails.cssStyle).toBe('');
+ });
+
+ it('should parse attribute with public "+" visibility', () => {
+ const str = '+name String';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('+name String');
+ expect(displayDetails.cssStyle).toBe('');
+ });
+
+ it('should parse attribute with protected "#" visibility', () => {
+ const str = '#name String';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('#name String');
+ expect(displayDetails.cssStyle).toBe('');
+ });
+
+ it('should parse attribute with private "-" visibility', () => {
+ const str = '-name String';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('-name String');
+ expect(displayDetails.cssStyle).toBe('');
+ });
+
+ it('should parse attribute with internal "~" visibility', () => {
+ const str = '~name String';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('~name String');
+ expect(displayDetails.cssStyle).toBe('');
+ });
+
+ it('should parse simple attribute name only', () => {
+ const str = 'id';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('id');
+ expect(displayDetails.cssStyle).toBe('');
+ });
+
+ it('should parse attribute with visibility and name only', () => {
+ const str = '+id';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('+id');
+ expect(displayDetails.cssStyle).toBe('');
+ });
+ });
+
+ describe('Static classifier ($) attributes', () => {
+ it('should parse static attribute without visibility', () => {
+ const str = 'count int$';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('count int');
+ expect(displayDetails.cssStyle).toBe(staticCssStyle);
+ });
+
+ it('should parse static attribute with public visibility', () => {
+ const str = '+count int$';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('+count int');
+ expect(displayDetails.cssStyle).toBe(staticCssStyle);
+ });
+
+ it('should parse static attribute with protected visibility', () => {
+ const str = '#count int$';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('#count int');
+ expect(displayDetails.cssStyle).toBe(staticCssStyle);
+ });
+
+ it('should parse static attribute with private visibility', () => {
+ const str = '-count int$';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('-count int');
+ expect(displayDetails.cssStyle).toBe(staticCssStyle);
+ });
+
+ it('should parse static attribute with internal visibility', () => {
+ const str = '~count int$';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('~count int');
+ expect(displayDetails.cssStyle).toBe(staticCssStyle);
+ });
+
+ it('should parse static attribute name only', () => {
+ const str = 'MAX_SIZE$';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('MAX_SIZE');
+ expect(displayDetails.cssStyle).toBe(staticCssStyle);
+ });
+ });
+
+ describe('Abstract classifier (*) attributes', () => {
+ it('should parse abstract attribute without visibility', () => {
+ const str = 'data String*';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('data String');
+ expect(displayDetails.cssStyle).toBe(abstractCssStyle);
+ });
+
+ it('should parse abstract attribute with public visibility', () => {
+ const str = '+data String*';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('+data String');
+ expect(displayDetails.cssStyle).toBe(abstractCssStyle);
+ });
+
+ it('should parse abstract attribute with protected visibility', () => {
+ const str = '#data String*';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('#data String');
+ expect(displayDetails.cssStyle).toBe(abstractCssStyle);
+ });
+
+ it('should parse abstract attribute with private visibility', () => {
+ const str = '-data String*';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('-data String');
+ expect(displayDetails.cssStyle).toBe(abstractCssStyle);
+ });
+
+ it('should parse abstract attribute with internal visibility', () => {
+ const str = '~data String*';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('~data String');
+ expect(displayDetails.cssStyle).toBe(abstractCssStyle);
+ });
+
+ it('should parse abstract attribute name only', () => {
+ const str = 'value*';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('value');
+ expect(displayDetails.cssStyle).toBe(abstractCssStyle);
+ });
+ });
+
+ describe('Abstract and Static combined classifiers', () => {
+ it('should parse abstract+static ($*) attribute without visibility', () => {
+ const str = 'config Map$*';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('config Map');
+ expect(displayDetails.cssStyle).toBe(abstractStaticCssStyle);
+ });
+
+ it('should parse static+abstract (*$) attribute without visibility', () => {
+ const str = 'config Map*$';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('config Map');
+ expect(displayDetails.cssStyle).toBe(abstractStaticCssStyle);
+ });
+
+ it('should parse abstract+static ($*) attribute with public visibility', () => {
+ const str = '+config Map$*';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('+config Map');
+ expect(displayDetails.cssStyle).toBe(abstractStaticCssStyle);
+ });
+
+ it('should parse static+abstract (*$) attribute with public visibility', () => {
+ const str = '+config Map*$';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('+config Map');
+ expect(displayDetails.cssStyle).toBe(abstractStaticCssStyle);
+ });
+
+ it('should parse abstract+static ($*) attribute with protected visibility', () => {
+ const str = '#registry HashMap$*';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('#registry HashMap');
+ expect(displayDetails.cssStyle).toBe(abstractStaticCssStyle);
+ });
+
+ it('should parse static+abstract (*$) attribute with protected visibility', () => {
+ const str = '#registry HashMap*$';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('#registry HashMap');
+ expect(displayDetails.cssStyle).toBe(abstractStaticCssStyle);
+ });
+
+ it('should parse abstract+static ($*) attribute with private visibility', () => {
+ const str = '-cache LRUCache$*';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('-cache LRUCache');
+ expect(displayDetails.cssStyle).toBe(abstractStaticCssStyle);
+ });
+
+ it('should parse static+abstract (*$) attribute with private visibility', () => {
+ const str = '-cache LRUCache*$';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('-cache LRUCache');
+ expect(displayDetails.cssStyle).toBe(abstractStaticCssStyle);
+ });
+
+ it('should parse abstract+static ($*) attribute with internal visibility', () => {
+ const str = '~pool ThreadPool$*';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('~pool ThreadPool');
+ expect(displayDetails.cssStyle).toBe(abstractStaticCssStyle);
+ });
+
+ it('should parse static+abstract (*$) attribute with internal visibility', () => {
+ const str = '~pool ThreadPool*$';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('~pool ThreadPool');
+ expect(displayDetails.cssStyle).toBe(abstractStaticCssStyle);
+ });
+
+ it('should parse abstract+static ($*) attribute name only', () => {
+ const str = 'INSTANCE$*';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('INSTANCE');
+ expect(displayDetails.cssStyle).toBe(abstractStaticCssStyle);
+ });
+
+ it('should parse static+abstract (*$) attribute name only', () => {
+ const str = 'INSTANCE*$';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('INSTANCE');
+ expect(displayDetails.cssStyle).toBe(abstractStaticCssStyle);
+ });
+ });
+
+ describe('Complex attribute type scenarios', () => {
+ it('should parse generic type attribute with static classifier', () => {
+ const str = '+items List~String~$';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('+items List');
+ expect(displayDetails.cssStyle).toBe(staticCssStyle);
+ });
+
+ it('should parse nested generic type attribute with abstract classifier', () => {
+ const str = '#mapping Map~String, List~Integer~~*';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe('#mapping Map~String, List');
+ expect(displayDetails.cssStyle).toBe(abstractCssStyle);
+ });
+
+ it('should parse complex generic type with abstract+static classifiers', () => {
+ const str = '+factory Function~Map~String, Object~, Promise~Result~~$*';
+ const displayDetails = new ClassMember(str, 'attribute').getDisplayDetails();
+
+ expect(displayDetails.displayText).toBe(
+ '+factory Function