Compare commits

..

4 Commits

Author SHA1 Message Date
Justin Greywolf
4693aa2749 Merge branch 'develop' into 5824-diff-between-markdown-and-strings 2024-12-02 14:45:11 -08:00
Justin Greywolf
468b6a55b6 Update cypress/integration/rendering/flowchart.spec.js
Co-authored-by: Sidharth Vinod <github@sidharth.dev>
2024-12-02 14:43:55 -08:00
Knut Sveidqvist
c7880d7281 Adding changeset 2024-11-27 13:02:36 +01:00
Knut Sveidqvist
f27ca07371 Adding back create label for the cases where you do not want markdown text 2024-11-27 12:38:08 +01:00
22 changed files with 293 additions and 241 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Proper separation between strings and markdown strings

View File

@@ -1,5 +0,0 @@
---
'mermaid': minor
---
Rename internal fields for class diagrams to match documentation and usage

View File

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

View File

@@ -650,12 +650,12 @@ class Class10
{ logLevel: 1, htmlLabels: true, layout: 'elk' } { logLevel: 1, htmlLabels: true, layout: 'elk' }
); );
}); });
it('ELK: should render a class with a text label, attribute and annotation', () => { it('ELK: should render a class with a text label, members and annotation', () => {
imgSnapshotTest( imgSnapshotTest(
`classDiagram `classDiagram
class C1["Class 1 with text label"] { class C1["Class 1 with text label"] {
&lt;&lt;interface&gt;&gt; &lt;&lt;interface&gt;&gt;
+attribute1 +member1
} }
C1 --> C2`, C1 --> C2`,
{ logLevel: 1, htmlLabels: true, layout: 'elk' } { logLevel: 1, htmlLabels: true, layout: 'elk' }

View File

@@ -650,12 +650,12 @@ class Class10
{ logLevel: 1, htmlLabels: true, look: 'handDrawn' } { logLevel: 1, htmlLabels: true, look: 'handDrawn' }
); );
}); });
it('HD: should render a class with a text label, attribute and annotation', () => { it('HD: should render a class with a text label, members and annotation', () => {
imgSnapshotTest( imgSnapshotTest(
`classDiagram `classDiagram
class C1["Class 1 with text label"] { class C1["Class 1 with text label"] {
&lt;&lt;interface&gt;&gt; &lt;&lt;interface&gt;&gt;
+attribute1 +member1
} }
C1 --> C2`, C1 --> C2`,
{ logLevel: 1, htmlLabels: true, look: 'handDrawn' } { logLevel: 1, htmlLabels: true, look: 'handDrawn' }

View File

@@ -500,12 +500,12 @@ class Class10
C1 --> C2` C1 --> C2`
); );
}); });
it('should render a class with a text label, attribute and annotation', () => { it('should render a class with a text label, members and annotation', () => {
imgSnapshotTest( imgSnapshotTest(
`classDiagram `classDiagram
class C1["Class 1 with text label"] { class C1["Class 1 with text label"] {
&lt;&lt;interface&gt;&gt; &lt;&lt;interface&gt;&gt;
+attribute1 +member1
} }
C1 --> C2` C1 --> C2`
); );

View File

@@ -647,12 +647,12 @@ class Class10
C1 --> C2` C1 --> C2`
); );
}); });
it('should render a class with a text label, attribute and annotation', () => { it('should render a class with a text label, members and annotation', () => {
imgSnapshotTest( imgSnapshotTest(
`classDiagram `classDiagram
class C1["Class 1 with text label"] { class C1["Class 1 with text label"] {
&lt;&lt;interface&gt;&gt; &lt;&lt;interface&gt;&gt;
+attribute1 +member1
} }
C1 --> C2` C1 --> C2`
); );

View File

@@ -430,7 +430,7 @@ describe('Class diagram', () => {
class \`This\nTitle\nHas\nMany\nNewlines\` { class \`This\nTitle\nHas\nMany\nNewlines\` {
+String Also +String Also
-Stirng Many -Stirng Many
#int attribute #int Members
+And() +And()
-Many() -Many()
#Methods() #Methods()
@@ -444,7 +444,7 @@ describe('Class diagram', () => {
class \`This\nTitle\nHas\nMany\nNewlines\` { class \`This\nTitle\nHas\nMany\nNewlines\` {
+String Also +String Also
-Stirng Many -Stirng Many
#int attribute #int Members
+And() +And()
-Many() -Many()
#Methods() #Methods()
@@ -460,7 +460,7 @@ describe('Class diagram', () => {
class \`This\nTitle\nHas\nMany\nNewlines\` { class \`This\nTitle\nHas\nMany\nNewlines\` {
+String Also +String Also
-Stirng Many -Stirng Many
#int attribute #int Members
+And() +And()
-Many() -Many()
#Methods() #Methods()

View File

@@ -895,7 +895,7 @@ graph TD
imgSnapshotTest( imgSnapshotTest(
` `
graph TD graph TD
classDef default fill:#a34,stroke:#000,stroke-width:4px,color:#fff classDef default fill:#a34,stroke:#000,stroke-width:4px,color:#fff
hello --> default hello --> default
`, `,
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' } { htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
@@ -917,4 +917,24 @@ graph TD
} }
); );
}); });
it('68: should be able to render string and markdown labels (#5824)', () => {
imgSnapshotTest(
`
flowchart TB
mermaid{"What is\nyourmermaid version?"} --> v10["<11"] --"\`<**1**1\`"--> fine["No bug"]
mermaid --> v11[">= v11"] -- ">= v11" --> broken["Affected by https://github.com/mermaid-js/mermaid/issues/5824"]
subgraph subgraph1["\`How to fix **fix**\`"]
broken --> B["B"]
end
githost["Github, Gitlab, BitBucket, etc."]
githost2["\`Github, Gitlab, BitBucket, etc.\`"]
a["1."]
b["- x"]
`,
{
flowchart: { htmlLabels: true },
securityLevel: 'loose',
}
);
});
}); });

View File

@@ -144,7 +144,7 @@ config:
D-->I D-->I
D-->I D-->I
</pre> </pre>
<pre id="diagram4" class="mermaid"> <pre id="diagram4" class="mermaid2">
--- ---
config: config:
layout: elk layout: elk
@@ -185,19 +185,19 @@ flowchart LR
</pre> </pre>
<pre id="diagram4" class="mermaid"> <pre id="diagram4" class="mermaid">
--- ---
config: title: https://github.com/mermaid-js/mermaid/issues/5824
layout: elk
--- ---
flowchart LR %% 6048, 5824
subgraph s1["Untitled subgraph"] flowchart TB
n1["Evaluate"] mermaid{"What is\nyourmermaid version?"} --> v10["<11"] --"`<**1**1`"--> fine["No bug"]
n2["Option 1"] mermaid --> v11[">= v11"] -- ">= v11" --> broken["Affected by https://github.com/mermaid-js/mermaid/issues/5824"]
end subgraph subgraph1["`How to fix **fix**`"]
n1 -- One --> n2 broken --> B["B"]
end
githost["Github, Gitlab, BitBucket, etc."]
githost2["`Github, Gitlab, BitBucket, etc.`"]
a["1."]
b["- x"]
</pre> </pre>
<pre id="diagram4" class="mermaid"> <pre id="diagram4" class="mermaid">
--- ---

View File

@@ -15,7 +15,6 @@ interface LabelData {
interface NodeWithVertex extends Omit<Node, 'domId'> { interface NodeWithVertex extends Omit<Node, 'domId'> {
children?: unknown[]; children?: unknown[];
labelData?: LabelData; labelData?: LabelData;
domId?: Node['domId'] | SVGGroup | d3.Selection<SVGAElement, unknown, Element | null, unknown>; domId?: Node['domId'] | SVGGroup | d3.Selection<SVGAElement, unknown, Element | null, unknown>;
} }

View File

@@ -950,8 +950,8 @@ const class_box = (parent, node) => {
maxWidth = classTitleBBox.width; maxWidth = classTitleBBox.width;
} }
const classAttributes = []; const classAttributes = [];
node.classData.attributes.forEach((attribute) => { node.classData.members.forEach((member) => {
const parsedInfo = attribute.getDisplayDetails(); const parsedInfo = member.getDisplayDetails();
let parsedText = parsedInfo.displayText; let parsedText = parsedInfo.displayText;
if (getConfig().flowchart.htmlLabels) { if (getConfig().flowchart.htmlLabels) {
parsedText = parsedText.replace(/</g, '&lt;').replace(/>/g, '&gt;'); parsedText = parsedText.replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -984,8 +984,8 @@ const class_box = (parent, node) => {
maxHeight += lineHeight; maxHeight += lineHeight;
const classMethods = []; const classMethods = [];
node.classData.methods.forEach((method) => { node.classData.methods.forEach((member) => {
const parsedInfo = method.getDisplayDetails(); const parsedInfo = member.getDisplayDetails();
let displayText = parsedInfo.displayText; let displayText = parsedInfo.displayText;
if (getConfig().flowchart.htmlLabels) { if (getConfig().flowchart.htmlLabels) {
displayText = displayText.replace(/</g, '&lt;').replace(/>/g, '&gt;'); displayText = displayText.replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -1059,9 +1059,9 @@ const class_box = (parent, node) => {
((-1 * maxHeight) / 2 + verticalPos + lineHeight / 2) + ((-1 * maxHeight) / 2 + verticalPos + lineHeight / 2) +
')' ')'
); );
//get the height of the bounding box of each attribute if exists //get the height of the bounding box of each member if exists
const fieldBBox = lbl?.getBBox(); const memberBBox = lbl?.getBBox();
verticalPos += (fieldBBox?.height ?? 0) + rowPadding; verticalPos += (memberBBox?.height ?? 0) + rowPadding;
}); });
verticalPos += lineHeight; verticalPos += lineHeight;
@@ -1079,8 +1079,8 @@ const class_box = (parent, node) => {
'transform', 'transform',
'translate( ' + -maxWidth / 2 + ', ' + ((-1 * maxHeight) / 2 + verticalPos) + ')' 'translate( ' + -maxWidth / 2 + ', ' + ((-1 * maxHeight) / 2 + verticalPos) + ')'
); );
const methodBBox = lbl?.getBBox(); const memberBBox = lbl?.getBBox();
verticalPos += (methodBBox?.height ?? 0) + rowPadding; verticalPos += (memberBBox?.height ?? 0) + rowPadding;
}); });
rect rect

View File

@@ -40,18 +40,30 @@ let functions: any[] = [];
const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig()); const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig());
const splitClassIdAndType = function (_id: string) { const splitClassNameAndType = function (_id: string) {
const id = sanitizeText(_id); const id = common.sanitizeText(_id, getConfig());
let genericType = ''; let genericType = '';
let classId = id; let className = id;
if (id.indexOf('~') > 0) { if (id.indexOf('~') > 0) {
const split = id.split('~'); const split = id.split('~');
classId = sanitizeText(split[0]); className = sanitizeText(split[0]);
genericType = sanitizeText(split[1]); genericType = sanitizeText(split[1]);
} }
return { classId, type: genericType }; return { className: className, type: genericType };
};
export const setClassLabel = function (_id: string, label: string) {
const id = common.sanitizeText(_id, getConfig());
if (label) {
label = sanitizeText(label);
}
const { className } = splitClassNameAndType(id);
classes.get(className)!.label = label;
classes.get(className)!.text =
`${label}${classes.get(className)!.type ? `<${classes.get(className)!.type}>` : ''}`;
}; };
/** /**
@@ -60,33 +72,28 @@ const splitClassIdAndType = function (_id: string) {
* @param id - Id of the class to add * @param id - Id of the class to add
* @public * @public
*/ */
export const addClass = function (_id: string, label?: string) { export const addClass = function (_id: string) {
const id = sanitizeText(_id); const id = common.sanitizeText(_id, getConfig());
const { classId, type } = splitClassIdAndType(id); const { className, type } = splitClassNameAndType(id);
let newLabel = classId; // Only add class if not exists
if (classes.has(className)) {
if (classes.has(classId)) {
return; return;
} }
// alert('Adding class: ' + className);
if (label) { const name = common.sanitizeText(className, getConfig());
newLabel = sanitizeText(label); // alert('Adding class after: ' + name);
} classes.set(name, {
id: name,
const text = `${newLabel}${type ? `&lt;${type}&gt;` : ''}`;
classes.set(classId, {
id: classId,
type: type, type: type,
label: newLabel, label: name,
text: text, text: `${name}${type ? `&lt;${type}&gt;` : ''}`,
shape: 'classBox', shape: 'classBox',
cssClasses: 'default', cssClasses: 'default',
methods: [], methods: [],
attributes: [], members: [],
annotations: [], annotations: [],
styles: [], styles: [],
domId: `${MERMAID_DOM_ID_PREFIX}${classId}-${classCounter}`, domId: MERMAID_DOM_ID_PREFIX + name + '-' + classCounter,
} as ClassNode); } as ClassNode);
classCounter++; classCounter++;
@@ -109,7 +116,7 @@ const addInterface = function (label: string, classId: string) {
* @public * @public
*/ */
export const lookUpDomId = function (_id: string): string { export const lookUpDomId = function (_id: string): string {
const id = sanitizeText(_id); const id = common.sanitizeText(_id, getConfig());
if (classes.has(id)) { if (classes.has(id)) {
return classes.get(id)!.domId; return classes.get(id)!.domId;
} }
@@ -175,12 +182,18 @@ export const addRelation = function (classRelation: ClassRelation) {
addClass(classRelation.id2); addClass(classRelation.id2);
} }
classRelation.id1 = splitClassIdAndType(classRelation.id1).classId; classRelation.id1 = splitClassNameAndType(classRelation.id1).className;
classRelation.id2 = splitClassIdAndType(classRelation.id2).classId; classRelation.id2 = splitClassNameAndType(classRelation.id2).className;
classRelation.relationTitle1 = sanitizeText(classRelation.relationTitle1.trim()); classRelation.relationTitle1 = common.sanitizeText(
classRelation.relationTitle1.trim(),
getConfig()
);
classRelation.relationTitle2 = sanitizeText(classRelation.relationTitle2.trim()); classRelation.relationTitle2 = common.sanitizeText(
classRelation.relationTitle2.trim(),
getConfig()
);
relations.push(classRelation); relations.push(classRelation);
}; };
@@ -189,29 +202,29 @@ export const addRelation = function (classRelation: ClassRelation) {
* Adds an annotation to the specified class Annotations mark special properties of the given type * Adds an annotation to the specified class Annotations mark special properties of the given type
* (like 'interface' or 'service') * (like 'interface' or 'service')
* *
* @param classId - The class name * @param className - The class name
* @param annotation - The name of the annotation without any brackets * @param annotation - The name of the annotation without any brackets
* @public * @public
*/ */
export const addAnnotation = function (className: string, annotation: string) { export const addAnnotation = function (className: string, annotation: string) {
const validatedClassName = splitClassIdAndType(className).classId; const validatedClassName = splitClassNameAndType(className).className;
classes.get(validatedClassName)!.annotations.push(annotation); classes.get(validatedClassName)!.annotations.push(annotation);
}; };
/** /**
* Adds a member to the specified class * Adds a member to the specified class
* *
* @param classId - The class name * @param className - The class name
* @param member - The full name of the member. If the member is enclosed in `<<brackets>>` it is * @param member - The full name of the member. If the member is enclosed in `<<brackets>>` it is
* treated as an annotation If the member is ending with a closing bracket ) it is treated as a * treated as an annotation If the member is ending with a closing bracket ) it is treated as a
* method Otherwise the member will be treated as a normal property * method Otherwise the member will be treated as a normal property
* @public * @public
*/ */
export const addMember = function (classId: string, member: string) { export const addMember = function (className: string, member: string) {
addClass(classId); addClass(className);
const validatedClassId = splitClassIdAndType(classId).classId; const validatedClassName = splitClassNameAndType(className).className;
const theClass = classes.get(validatedClassId)!; const theClass = classes.get(validatedClassName)!;
if (typeof member === 'string') { if (typeof member === 'string') {
// Member can contain white spaces, we trim them out // Member can contain white spaces, we trim them out
@@ -224,7 +237,7 @@ export const addMember = function (classId: string, member: string) {
//its a method //its a method
theClass.methods.push(new ClassMember(memberString, 'method')); theClass.methods.push(new ClassMember(memberString, 'method'));
} else if (memberString) { } else if (memberString) {
theClass.attributes.push(new ClassMember(memberString, 'attribute')); theClass.members.push(new ClassMember(memberString, 'attribute'));
} }
} }
}; };
@@ -364,7 +377,7 @@ export const setClickEvent = function (ids: string, functionName: string, functi
}; };
const setClickFunc = function (_domId: string, functionName: string, functionArgs: string) { const setClickFunc = function (_domId: string, functionName: string, functionArgs: string) {
const domId = sanitizeText(_domId); const domId = common.sanitizeText(_domId, getConfig());
const config = getConfig(); const config = getConfig();
if (config.securityLevel !== 'loose') { if (config.securityLevel !== 'loose') {
return; return;
@@ -507,15 +520,17 @@ const getNamespaces = function (): NamespaceMap {
* Function called by parser when a namespace definition has been found. * Function called by parser when a namespace definition has been found.
* *
* @param id - Id of the namespace to add * @param id - Id of the namespace to add
* @param classIds - Ids of the class to add * @param classNames - Ids of the class to add
* @public * @public
*/ */
export const addClassesToNamespace = function (_id: string, classIds: string[]) { export const addClassesToNamespace = function (id: string, classNames: string[]) {
addNamespace(_id); if (!namespaces.has(id)) {
for (const id of classIds) { return;
const { classId } = splitClassIdAndType(id); }
classes.get(classId)!.parent = _id; for (const name of classNames) {
namespaces.get(_id)!.classes.set(classId, classes.get(classId)!); const { className } = splitClassNameAndType(name);
classes.get(className)!.parent = id;
namespaces.get(id)!.classes.set(className, classes.get(className)!);
} }
}; };
@@ -711,6 +726,7 @@ export default {
lookUpDomId, lookUpDomId,
setDiagramTitle, setDiagramTitle,
getDiagramTitle, getDiagramTitle,
setClassLabel,
addNamespace, addNamespace,
addClassesToNamespace, addClassesToNamespace,
getNamespace, getNamespace,

View File

@@ -212,29 +212,29 @@ describe('given a basic class diagram, ', function () {
expect(c2.label).toBe('Class 2 with chars @?'); expect(c2.label).toBe('Class 2 with chars @?');
}); });
it('should parse a class with a text label and attribute', () => { it('should parse a class with a text label and member', () => {
const str = 'classDiagram\n' + 'class C1["Class 1 with text label"]\n' + 'C1: attribute1'; const str = 'classDiagram\n' + 'class C1["Class 1 with text label"]\n' + 'C1: member1';
parser.parse(str); parser.parse(str);
const c1 = classDb.getClass('C1'); const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label'); expect(c1.label).toBe('Class 1 with text label');
expect(c1.attributes.length).toBe(1); expect(c1.members.length).toBe(1);
expect(c1.attributes[0].getDisplayDetails().displayText).toBe('attribute1'); expect(c1.members[0].getDisplayDetails().displayText).toBe('member1');
}); });
it('should parse a class with a text label, attribute and annotation', () => { it('should parse a class with a text label, member and annotation', () => {
const str = const str =
'classDiagram\n' + 'classDiagram\n' +
'class C1["Class 1 with text label"]\n' + 'class C1["Class 1 with text label"]\n' +
'<<interface>> C1\n' + '<<interface>> C1\n' +
'C1 : int attribute1'; 'C1 : int member1';
parser.parse(str); parser.parse(str);
const c1 = classDb.getClass('C1'); const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label'); expect(c1.label).toBe('Class 1 with text label');
expect(c1.attributes.length).toBe(1); expect(c1.members.length).toBe(1);
expect(c1.attributes[0].getDisplayDetails().displayText).toBe('int attribute1'); expect(c1.members[0].getDisplayDetails().displayText).toBe('int member1');
expect(c1.annotations.length).toBe(1); expect(c1.annotations.length).toBe(1);
expect(c1.annotations[0]).toBe('interface'); expect(c1.annotations[0]).toBe('interface');
}); });
@@ -253,14 +253,14 @@ describe('given a basic class diagram, ', function () {
const str = const str =
'classDiagram\n' + 'classDiagram\n' +
'class C1["Class 1 with text label"]\n' + 'class C1["Class 1 with text label"]\n' +
'C1 : int attribute1\n' + 'C1 : int member1\n' +
'cssClass "C1" styleClass'; 'cssClass "C1" styleClass';
parser.parse(str); parser.parse(str);
const c1 = classDb.getClass('C1'); const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label'); expect(c1.label).toBe('Class 1 with text label');
expect(c1.attributes[0].getDisplayDetails().displayText).toBe('int attribute1'); expect(c1.members[0].getDisplayDetails().displayText).toBe('int member1');
expect(c1.cssClasses).toBe('default styleClass'); expect(c1.cssClasses).toBe('default styleClass');
}); });
@@ -268,7 +268,7 @@ describe('given a basic class diagram, ', function () {
const str = const str =
'classDiagram\n' + 'classDiagram\n' +
'class C1["Class 1 with text label"]\n' + 'class C1["Class 1 with text label"]\n' +
'C1 : int attribute1\n' + 'C1 : int member1\n' +
'class C2["Long long long long long long long long long long label"]\n' + 'class C2["Long long long long long long long long long long label"]\n' +
'cssClass "C1,C2" styleClass'; 'cssClass "C1,C2" styleClass';
@@ -486,7 +486,7 @@ class C13["With Città foreign language"]
expect(studentClass).toMatchObject({ expect(studentClass).toMatchObject({
id: 'Student', id: 'Student',
label: 'Student', label: 'Student',
attributes: [ members: [
expect.objectContaining({ expect.objectContaining({
id: 'idCard : IdCard', id: 'idCard : IdCard',
visibility: '-', visibility: '-',
@@ -500,7 +500,11 @@ class C13["With Città foreign language"]
expect(classDb.getClasses().get('Student')).toMatchInlineSnapshot(` expect(classDb.getClasses().get('Student')).toMatchInlineSnapshot(`
{ {
"annotations": [], "annotations": [],
"attributes": [ "cssClasses": "default",
"domId": "classId-Student-141",
"id": "Student",
"label": "Student",
"members": [
ClassMember { ClassMember {
"classifier": "", "classifier": "",
"id": "idCard : IdCard", "id": "idCard : IdCard",
@@ -509,10 +513,6 @@ class C13["With Città foreign language"]
"visibility": "-", "visibility": "-",
}, },
], ],
"cssClasses": "default",
"domId": "classId-Student-141",
"id": "Student",
"label": "Student",
"methods": [], "methods": [],
"shape": "classBox", "shape": "classBox",
"styles": [], "styles": [],
@@ -569,7 +569,7 @@ class C13["With Città foreign language"]
parser.yy = classDb; parser.yy = classDb;
}); });
it('should handle attribute definitions', function () { it('should handle member definitions', function () {
const str = 'classDiagram\n' + 'class Car{\n' + '+int wheels\n' + '}'; const str = 'classDiagram\n' + 'class Car{\n' + '+int wheels\n' + '}';
parser.parse(str); parser.parse(str);
@@ -588,7 +588,7 @@ class C13["With Città foreign language"]
parser.parse(str); parser.parse(str);
}); });
it('should handle attribute and method definitions', () => { it('should handle member and method definitions', () => {
const str = const str =
'classDiagram\n' + 'class Dummy_Class {\n' + 'String data\n' + 'void methods()\n' + '}'; 'classDiagram\n' + 'class Dummy_Class {\n' + 'String data\n' + 'void methods()\n' + '}';
@@ -611,46 +611,45 @@ class C13["With Città foreign language"]
const str = const str =
'classDiagram\n' + 'classDiagram\n' +
'class Class1 {\n' + 'class Class1 {\n' +
'int testAttribute\n' + 'int testMember\n' +
'test()\n' + 'test()\n' +
'string fooAttribute\n' + 'string fooMember\n' +
'foo()\n' + 'foo()\n' +
'}'; '}';
parser.parse(str); parser.parse(str);
const actual = parser.yy.getClass('Class1'); const actual = parser.yy.getClass('Class1');
expect(actual.attributes.length).toBe(2); expect(actual.members.length).toBe(2);
expect(actual.methods.length).toBe(2); expect(actual.methods.length).toBe(2);
expect(actual.attributes[0].getDisplayDetails().displayText).toBe('int testAttribute'); expect(actual.members[0].getDisplayDetails().displayText).toBe('int testMember');
expect(actual.attributes[1].getDisplayDetails().displayText).toBe('string fooAttribute'); expect(actual.members[1].getDisplayDetails().displayText).toBe('string fooMember');
expect(actual.methods[0].getDisplayDetails().displayText).toBe('test()'); expect(actual.methods[0].getDisplayDetails().displayText).toBe('test()');
expect(actual.methods[1].getDisplayDetails().displayText).toBe('foo()'); expect(actual.methods[1].getDisplayDetails().displayText).toBe('foo()');
}); });
it('should parse a class with a text label and attribute', () => { it('should parse a class with a text label and members', () => {
const str = const str = 'classDiagram\n' + 'class C1["Class 1 with text label"] {\n' + '+member1\n' + '}';
'classDiagram\n' + 'class C1["Class 1 with text label"] {\n' + '+attributes1\n' + '}';
parser.parse(str); parser.parse(str);
const c1 = classDb.getClass('C1'); const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label'); expect(c1.label).toBe('Class 1 with text label');
expect(c1.attributes.length).toBe(1); expect(c1.members.length).toBe(1);
expect(c1.attributes[0].getDisplayDetails().displayText).toBe('+attributes1'); expect(c1.members[0].getDisplayDetails().displayText).toBe('+member1');
}); });
it('should parse a class with a text label, attribute and annotation', () => { it('should parse a class with a text label, members and annotation', () => {
const str = const str =
'classDiagram\n' + 'classDiagram\n' +
'class C1["Class 1 with text label"] {\n' + 'class C1["Class 1 with text label"] {\n' +
'<<interface>>\n' + '<<interface>>\n' +
'+attribute1\n' + '+member1\n' +
'}'; '}';
parser.parse(str); parser.parse(str);
const c1 = classDb.getClass('C1'); const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label'); expect(c1.label).toBe('Class 1 with text label');
expect(c1.attributes.length).toBe(1); expect(c1.members.length).toBe(1);
expect(c1.attributes[0].getDisplayDetails().displayText).toBe('+attribute1'); expect(c1.members[0].getDisplayDetails().displayText).toBe('+member1');
expect(c1.annotations.length).toBe(1); expect(c1.annotations.length).toBe(1);
expect(c1.annotations[0]).toBe('interface'); expect(c1.annotations[0]).toBe('interface');
}); });
@@ -869,12 +868,12 @@ foo()
const actual = parser.yy.getClass('Class1'); const actual = parser.yy.getClass('Class1');
expect(actual.annotations.length).toBe(1); expect(actual.annotations.length).toBe(1);
expect(actual.attributes.length).toBe(0); expect(actual.members.length).toBe(0);
expect(actual.attributes.length).toBe(0); expect(actual.methods.length).toBe(0);
expect(actual.annotations[0]).toBe('interface'); expect(actual.annotations[0]).toBe('interface');
}); });
it('should handle class annotations with attributes and methods', function () { it('should handle class annotations with members and methods', function () {
const str = const str =
'classDiagram\n' + 'classDiagram\n' +
'class Class1\n' + 'class Class1\n' +
@@ -885,7 +884,7 @@ foo()
const actual = parser.yy.getClass('Class1'); const actual = parser.yy.getClass('Class1');
expect(actual.annotations.length).toBe(1); expect(actual.annotations.length).toBe(1);
expect(actual.attributes.length).toBe(1); expect(actual.members.length).toBe(1);
expect(actual.methods.length).toBe(1); expect(actual.methods.length).toBe(1);
expect(actual.annotations[0]).toBe('interface'); expect(actual.annotations[0]).toBe('interface');
}); });
@@ -896,12 +895,12 @@ foo()
const actual = parser.yy.getClass('Class1'); const actual = parser.yy.getClass('Class1');
expect(actual.annotations.length).toBe(1); expect(actual.annotations.length).toBe(1);
expect(actual.attributes.length).toBe(0); expect(actual.members.length).toBe(0);
expect(actual.methods.length).toBe(0); expect(actual.methods.length).toBe(0);
expect(actual.annotations[0]).toBe('interface'); expect(actual.annotations[0]).toBe('interface');
}); });
it('should handle class annotations in brackets with attributes and methods', function () { it('should handle class annotations in brackets with members and methods', function () {
const str = const str =
'classDiagram\n' + 'classDiagram\n' +
'class Class1 {\n' + 'class Class1 {\n' +
@@ -913,41 +912,41 @@ foo()
const actual = parser.yy.getClass('Class1'); const actual = parser.yy.getClass('Class1');
expect(actual.annotations.length).toBe(1); expect(actual.annotations.length).toBe(1);
expect(actual.attributes.length).toBe(1); expect(actual.members.length).toBe(1);
expect(actual.methods.length).toBe(1); expect(actual.methods.length).toBe(1);
expect(actual.annotations[0]).toBe('interface'); expect(actual.annotations[0]).toBe('interface');
}); });
}); });
}); });
describe('given a class diagram with attributes and methods ', function () { describe('given a class diagram with members and methods ', function () {
describe('when parsing attributes', function () { describe('when parsing members', function () {
beforeEach(function () { beforeEach(function () {
classDb.clear(); classDb.clear();
parser.yy = classDb; parser.yy = classDb;
}); });
it('should handle simple attribute declaration', function () { it('should handle simple member declaration', function () {
const str = 'classDiagram\n' + 'class Car\n' + 'Car : wheels'; const str = 'classDiagram\n' + 'class Car\n' + 'Car : wheels';
parser.parse(str); parser.parse(str);
}); });
it('should handle direct attribute declaration', function () { it('should handle direct member declaration', function () {
parser.parse('classDiagram\n' + 'Car : wheels'); parser.parse('classDiagram\n' + 'Car : wheels');
const car = classDb.getClass('Car'); const car = classDb.getClass('Car');
expect(car.attributes.length).toBe(1); expect(car.members.length).toBe(1);
expect(car.attributes[0].id).toBe('wheels'); expect(car.members[0].id).toBe('wheels');
}); });
it('should handle direct attribute declaration with type', function () { it('should handle direct member declaration with type', function () {
parser.parse('classDiagram\n' + 'Car : int wheels'); parser.parse('classDiagram\n' + 'Car : int wheels');
const car = classDb.getClass('Car'); const car = classDb.getClass('Car');
expect(car.attributes.length).toBe(1); expect(car.members.length).toBe(1);
expect(car.attributes[0].id).toBe('int wheels'); expect(car.members[0].id).toBe('int wheels');
}); });
it('should handle simple attribute declaration with type', function () { it('should handle simple member declaration with type', function () {
const str = 'classDiagram\n' + 'class Car\n' + 'Car : int wheels'; const str = 'classDiagram\n' + 'class Car\n' + 'Car : int wheels';
parser.parse(str); parser.parse(str);
@@ -957,20 +956,20 @@ describe('given a class diagram with attributes and methods ', function () {
const str = const str =
'classDiagram\n' + 'classDiagram\n' +
'class actual\n' + 'class actual\n' +
'actual : -int privateAttribute\n' + 'actual : -int privateMember\n' +
'actual : +int publicAttribute\n' + 'actual : +int publicMember\n' +
'actual : #int protectedAttribute\n' + 'actual : #int protectedMember\n' +
'actual : ~int privatePackage'; 'actual : ~int privatePackage';
parser.parse(str); parser.parse(str);
const actual = parser.yy.getClass('actual'); const actual = parser.yy.getClass('actual');
expect(actual.attributes.length).toBe(4); expect(actual.members.length).toBe(4);
expect(actual.methods.length).toBe(0); expect(actual.methods.length).toBe(0);
expect(actual.attributes[0].getDisplayDetails().displayText).toBe('-int privateAttribute'); expect(actual.members[0].getDisplayDetails().displayText).toBe('-int privateMember');
expect(actual.attributes[1].getDisplayDetails().displayText).toBe('+int publicAttribute'); expect(actual.members[1].getDisplayDetails().displayText).toBe('+int publicMember');
expect(actual.attributes[2].getDisplayDetails().displayText).toBe('#int protectedAttribute'); expect(actual.members[2].getDisplayDetails().displayText).toBe('#int protectedMember');
expect(actual.attributes[3].getDisplayDetails().displayText).toBe('~int privatePackage'); expect(actual.members[3].getDisplayDetails().displayText).toBe('~int privatePackage');
}); });
it('should handle generic types', function () { it('should handle generic types', function () {
@@ -1021,7 +1020,7 @@ describe('given a class diagram with attributes and methods ', function () {
const actual = parser.yy.getClass('Class1'); const actual = parser.yy.getClass('Class1');
expect(actual.annotations.length).toBe(0); expect(actual.annotations.length).toBe(0);
expect(actual.attributes.length).toBe(0); expect(actual.members.length).toBe(0);
expect(actual.methods.length).toBe(1); expect(actual.methods.length).toBe(1);
const method = actual.methods[0]; const method = actual.methods[0];
expect(method.getDisplayDetails().displayText).toBe('someMethod()'); expect(method.getDisplayDetails().displayText).toBe('someMethod()');
@@ -1034,7 +1033,7 @@ describe('given a class diagram with attributes and methods ', function () {
const actual = parser.yy.getClass('Class1'); const actual = parser.yy.getClass('Class1');
expect(actual.annotations.length).toBe(0); expect(actual.annotations.length).toBe(0);
expect(actual.attributes.length).toBe(0); expect(actual.members.length).toBe(0);
expect(actual.methods.length).toBe(1); expect(actual.methods.length).toBe(1);
const method = actual.methods[0]; const method = actual.methods[0];
expect(method.getDisplayDetails().displayText).toBe('someMethod()'); expect(method.getDisplayDetails().displayText).toBe('someMethod()');
@@ -1052,7 +1051,7 @@ describe('given a class diagram with attributes and methods ', function () {
parser.parse(str); parser.parse(str);
}); });
it('should handle generic types in attributes in class with brackets', function () { it('should handle generic types in members in class with brackets', function () {
const str = const str =
'classDiagram\n' + 'classDiagram\n' +
'class Car {\n' + 'class Car {\n' +
@@ -1386,12 +1385,12 @@ describe('given a class diagram with relationships, ', function () {
const testClass = parser.yy.getClass('Class1'); const testClass = parser.yy.getClass('Class1');
expect(testClass.annotations.length).toBe(1); expect(testClass.annotations.length).toBe(1);
expect(testClass.attributes.length).toBe(0); expect(testClass.members.length).toBe(0);
expect(testClass.methods.length).toBe(0); expect(testClass.methods.length).toBe(0);
expect(testClass.annotations[0]).toBe('interface'); expect(testClass.annotations[0]).toBe('interface');
}); });
it('should handle class annotations with attributes and methods', function () { it('should handle class annotations with members and methods', function () {
const str = const str =
'classDiagram\n' + 'classDiagram\n' +
'class Class1\n' + 'class Class1\n' +
@@ -1402,7 +1401,7 @@ describe('given a class diagram with relationships, ', function () {
const testClass = parser.yy.getClass('Class1'); const testClass = parser.yy.getClass('Class1');
expect(testClass.annotations.length).toBe(1); expect(testClass.annotations.length).toBe(1);
expect(testClass.attributes.length).toBe(1); expect(testClass.members.length).toBe(1);
expect(testClass.methods.length).toBe(1); expect(testClass.methods.length).toBe(1);
expect(testClass.annotations[0]).toBe('interface'); expect(testClass.annotations[0]).toBe('interface');
}); });
@@ -1413,12 +1412,12 @@ describe('given a class diagram with relationships, ', function () {
const testClass = parser.yy.getClass('Class1'); const testClass = parser.yy.getClass('Class1');
expect(testClass.annotations.length).toBe(1); expect(testClass.annotations.length).toBe(1);
expect(testClass.attributes.length).toBe(0); expect(testClass.members.length).toBe(0);
expect(testClass.methods.length).toBe(0); expect(testClass.methods.length).toBe(0);
expect(testClass.annotations[0]).toBe('interface'); expect(testClass.annotations[0]).toBe('interface');
}); });
it('should handle class annotations in brackets with attributes and methods', function () { it('should handle class annotations in brackets with members and methods', function () {
const str = const str =
'classDiagram\n' + 'classDiagram\n' +
'class Class1 {\n' + 'class Class1 {\n' +
@@ -1430,7 +1429,7 @@ describe('given a class diagram with relationships, ', function () {
const testClass = parser.yy.getClass('Class1'); const testClass = parser.yy.getClass('Class1');
expect(testClass.annotations.length).toBe(1); expect(testClass.annotations.length).toBe(1);
expect(testClass.attributes.length).toBe(1); expect(testClass.members.length).toBe(1);
expect(testClass.methods.length).toBe(1); expect(testClass.methods.length).toBe(1);
expect(testClass.annotations[0]).toBe('interface'); expect(testClass.annotations[0]).toBe('interface');
}); });
@@ -1447,10 +1446,10 @@ describe('given a class diagram with relationships, ', function () {
parser.parse(str); parser.parse(str);
const testClass = parser.yy.getClass('Class1'); const testClass = parser.yy.getClass('Class1');
expect(testClass.attributes.length).toBe(2); expect(testClass.members.length).toBe(2);
expect(testClass.methods.length).toBe(2); expect(testClass.methods.length).toBe(2);
expect(testClass.attributes[0].getDisplayDetails().displayText).toBe('int : test'); expect(testClass.members[0].getDisplayDetails().displayText).toBe('int : test');
expect(testClass.attributes[1].getDisplayDetails().displayText).toBe('string : foo'); expect(testClass.members[1].getDisplayDetails().displayText).toBe('string : foo');
expect(testClass.methods[0].getDisplayDetails().displayText).toBe('test()'); expect(testClass.methods[0].getDisplayDetails().displayText).toBe('test()');
expect(testClass.methods[1].getDisplayDetails().displayText).toBe('foo()'); expect(testClass.methods[1].getDisplayDetails().displayText).toBe('foo()');
}); });
@@ -1461,7 +1460,7 @@ describe('given a class diagram with relationships, ', function () {
const testClass = parser.yy.getClass('Class1'); const testClass = parser.yy.getClass('Class1');
expect(testClass.annotations.length).toBe(0); expect(testClass.annotations.length).toBe(0);
expect(testClass.attributes.length).toBe(0); expect(testClass.members.length).toBe(0);
expect(testClass.methods.length).toBe(1); expect(testClass.methods.length).toBe(1);
const method = testClass.methods[0]; const method = testClass.methods[0];
expect(method.getDisplayDetails().displayText).toBe('someMethod()'); expect(method.getDisplayDetails().displayText).toBe('someMethod()');
@@ -1474,7 +1473,7 @@ describe('given a class diagram with relationships, ', function () {
const testClass = parser.yy.getClass('Class1'); const testClass = parser.yy.getClass('Class1');
expect(testClass.annotations.length).toBe(0); expect(testClass.annotations.length).toBe(0);
expect(testClass.attributes.length).toBe(0); expect(testClass.members.length).toBe(0);
expect(testClass.methods.length).toBe(1); expect(testClass.methods.length).toBe(1);
const method = testClass.methods[0]; const method = testClass.methods[0];
expect(method.getDisplayDetails().displayText).toBe('someMethod()'); expect(method.getDisplayDetails().displayText).toBe('someMethod()');
@@ -1689,17 +1688,17 @@ class Class2
const testClasses = parser.yy.getClasses(); const testClasses = parser.yy.getClasses();
const testRelations = parser.yy.getRelations(); const testRelations = parser.yy.getRelations();
expect(testNamespaceA.classes.size).toBe(2); expect(testNamespaceA.classes.size).toBe(2);
expect(testNamespaceA.classes.get('A1').attributes[0].getDisplayDetails().displayText).toBe( expect(testNamespaceA.classes.get('A1').members[0].getDisplayDetails().displayText).toBe(
'+foo : string' '+foo : string'
); );
expect(testNamespaceA.classes.get('A2').attributes[0].getDisplayDetails().displayText).toBe( expect(testNamespaceA.classes.get('A2').members[0].getDisplayDetails().displayText).toBe(
'+bar : int' '+bar : int'
); );
expect(testNamespaceB.classes.size).toBe(2); expect(testNamespaceB.classes.size).toBe(2);
expect(testNamespaceB.classes.get('B1').attributes[0].getDisplayDetails().displayText).toBe( expect(testNamespaceB.classes.get('B1').members[0].getDisplayDetails().displayText).toBe(
'+foo : bool' '+foo : bool'
); );
expect(testNamespaceB.classes.get('B2').attributes[0].getDisplayDetails().displayText).toBe( expect(testNamespaceB.classes.get('B2').members[0].getDisplayDetails().displayText).toBe(
'+bar : float' '+bar : float'
); );
expect(testClasses.size).toBe(4); expect(testClasses.size).toBe(4);
@@ -1743,37 +1742,37 @@ class Class2
expect(c2.label).toBe('Class 2 with chars @?'); expect(c2.label).toBe('Class 2 with chars @?');
}); });
it('should parse a class with a text label and attributes', () => { it('should parse a class with a text label and members', () => {
parser.parse(`classDiagram parser.parse(`classDiagram
class C1["Class 1 with text label"] { class C1["Class 1 with text label"] {
+attribute1 +member1
} }
C1 --> C2 C1 --> C2
`); `);
const c1 = classDb.getClass('C1'); const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label'); expect(c1.label).toBe('Class 1 with text label');
expect(c1.attributes.length).toBe(1); expect(c1.members.length).toBe(1);
const attribute = c1.attributes[0]; const member = c1.members[0];
expect(attribute.getDisplayDetails().displayText).toBe('+attribute1'); expect(member.getDisplayDetails().displayText).toBe('+member1');
const c2 = classDb.getClass('C2'); const c2 = classDb.getClass('C2');
expect(c2.label).toBe('C2'); expect(c2.label).toBe('C2');
}); });
it('should parse a class with a text label, attributes and annotation', () => { it('should parse a class with a text label, members and annotation', () => {
parser.parse(`classDiagram parser.parse(`classDiagram
class C1["Class 1 with text label"] { class C1["Class 1 with text label"] {
<<interface>> <<interface>>
+attribute1 +member1
} }
C1 --> C2 C1 --> C2
`); `);
const c1 = classDb.getClass('C1'); const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label'); expect(c1.label).toBe('Class 1 with text label');
expect(c1.attributes.length).toBe(1); expect(c1.members.length).toBe(1);
expect(c1.annotations.length).toBe(1); expect(c1.annotations.length).toBe(1);
expect(c1.annotations[0]).toBe('interface'); expect(c1.annotations[0]).toBe('interface');
const attribute = c1.attributes[0]; const member = c1.members[0];
expect(attribute.getDisplayDetails().displayText).toBe('+attribute1'); expect(member.getDisplayDetails().displayText).toBe('+member1');
const c2 = classDb.getClass('C2'); const c2 = classDb.getClass('C2');
expect(c2.label).toBe('C2'); expect(c2.label).toBe('C2');
@@ -1782,7 +1781,7 @@ class Class2
it('should parse a class with text label and css class shorthand', () => { it('should parse a class with text label and css class shorthand', () => {
parser.parse(`classDiagram parser.parse(`classDiagram
class C1["Class 1 with text label"]:::styleClass { class C1["Class 1 with text label"]:::styleClass {
+attribute1 +member1
} }
C1 --> C2 C1 --> C2
`); `);
@@ -1790,14 +1789,14 @@ C1 --> C2
const c1 = classDb.getClass('C1'); const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label'); expect(c1.label).toBe('Class 1 with text label');
expect(c1.cssClasses).toBe('default styleClass'); expect(c1.cssClasses).toBe('default styleClass');
const attribute = c1.attributes[0]; const member = c1.members[0];
expect(attribute.getDisplayDetails().displayText).toBe('+attribute1'); expect(member.getDisplayDetails().displayText).toBe('+member1');
}); });
it('should parse a class with text label and css class', () => { it('should parse a class with text label and css class', () => {
parser.parse(`classDiagram parser.parse(`classDiagram
class C1["Class 1 with text label"] { class C1["Class 1 with text label"] {
+attribute1 +member1
} }
C1 --> C2 C1 --> C2
cssClass "C1" styleClass cssClass "C1" styleClass
@@ -1806,14 +1805,14 @@ cssClass "C1" styleClass
const c1 = classDb.getClass('C1'); const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label'); expect(c1.label).toBe('Class 1 with text label');
expect(c1.cssClasses).toBe('default styleClass'); expect(c1.cssClasses).toBe('default styleClass');
const attribute = c1.attributes[0]; const member = c1.members[0];
expect(attribute.getDisplayDetails().displayText).toBe('+attribute1'); expect(member.getDisplayDetails().displayText).toBe('+member1');
}); });
it('should parse two classes with text labels and css classes', () => { it('should parse two classes with text labels and css classes', () => {
parser.parse(`classDiagram parser.parse(`classDiagram
class C1["Class 1 with text label"] { class C1["Class 1 with text label"] {
+attribute1 +member1
} }
class C2["Long long long long long long long long long long label"] class C2["Long long long long long long long long long long label"]
C1 --> C2 C1 --> C2
@@ -1832,7 +1831,7 @@ cssClass "C1,C2" styleClass
it('should parse two classes with text labels and css class shorthands', () => { it('should parse two classes with text labels and css class shorthands', () => {
parser.parse(`classDiagram parser.parse(`classDiagram
class C1["Class 1 with text label"]:::styleClass1 { class C1["Class 1 with text label"]:::styleClass1 {
+attribute1 +member1
} }
class C2["Class 2 !@#$%^&*() label"]:::styleClass2 class C2["Class 2 !@#$%^&*() label"]:::styleClass2
C1 --> C2 C1 --> C2

View File

@@ -3,13 +3,13 @@ import { parseGenericTypes, sanitizeText } from '../common/common.js';
export interface ClassNode { export interface ClassNode {
id: string; id: string;
type?: string; type: string;
label?: string; label: string;
shape: string; shape: string;
text?: string; text: string;
cssClasses: string; cssClasses: string;
methods: ClassMember[]; methods: ClassMember[];
attributes: ClassMember[]; members: ClassMember[];
annotations: string[]; annotations: string[];
domId: string; domId: string;
styles: string[]; styles: string[];

View File

@@ -297,7 +297,7 @@ classStatement
classIdentifier classIdentifier
: CLASS className {$$=$2; yy.addClass($2);} : CLASS className {$$=$2; yy.addClass($2);}
| CLASS className classLabel {$$=$2; yy.addClass($2, $3);} | CLASS className classLabel {$$=$2; yy.addClass($2);yy.setClassLabel($2, $3);}
; ;
annotationStatement annotationStatement

View File

@@ -27,12 +27,12 @@ export async function textHelper<T extends SVGGraphicsElement>(
let annotationGroup = null; let annotationGroup = null;
let labelGroup = null; let labelGroup = null;
let attributeGroup = null; let membersGroup = null;
let methodsGroup = null; let methodsGroup = null;
let annotationGroupHeight = 0; let annotationGroupHeight = 0;
let labelGroupHeight = 0; let labelGroupHeight = 0;
let attributeGroupHeight = 0; let membersGroupHeight = 0;
annotationGroup = shapeSvg.insert('g').attr('class', 'annotation-group text'); annotationGroup = shapeSvg.insert('g').attr('class', 'annotation-group text');
if (node.annotations.length > 0) { if (node.annotations.length > 0) {
@@ -48,15 +48,15 @@ export async function textHelper<T extends SVGGraphicsElement>(
const labelGroupBBox = labelGroup.node()!.getBBox(); const labelGroupBBox = labelGroup.node()!.getBBox();
labelGroupHeight = labelGroupBBox.height; labelGroupHeight = labelGroupBBox.height;
attributeGroup = shapeSvg.insert('g').attr('class', 'attribute-group text'); membersGroup = shapeSvg.insert('g').attr('class', 'members-group text');
let yOffset = 0; let yOffset = 0;
for (const attribute of node.attributes) { for (const member of node.members) {
const height = await addText(attributeGroup, attribute, yOffset, [attribute.parseClassifier()]); const height = await addText(membersGroup, member, yOffset, [member.parseClassifier()]);
yOffset += height + TEXT_PADDING; yOffset += height + TEXT_PADDING;
} }
attributeGroupHeight = attributeGroup.node()!.getBBox().height; membersGroupHeight = membersGroup.node()!.getBBox().height;
if (attributeGroupHeight <= 0) { if (membersGroupHeight <= 0) {
attributeGroupHeight = GAP / 2; membersGroupHeight = GAP / 2;
} }
methodsGroup = shapeSvg.insert('g').attr('class', 'methods-group text'); methodsGroup = shapeSvg.insert('g').attr('class', 'methods-group text');
@@ -79,14 +79,14 @@ export async function textHelper<T extends SVGGraphicsElement>(
bbox = shapeSvg.node()!.getBBox(); bbox = shapeSvg.node()!.getBBox();
attributeGroup.attr( membersGroup.attr(
'transform', 'transform',
`translate(${0}, ${annotationGroupHeight + labelGroupHeight + GAP * 2})` `translate(${0}, ${annotationGroupHeight + labelGroupHeight + GAP * 2})`
); );
bbox = shapeSvg.node()!.getBBox(); bbox = shapeSvg.node()!.getBBox();
methodsGroup.attr( methodsGroup.attr(
'transform', 'transform',
`translate(${0}, ${annotationGroupHeight + labelGroupHeight + (attributeGroupHeight ? attributeGroupHeight + GAP * 4 : GAP * 2)})` `translate(${0}, ${annotationGroupHeight + labelGroupHeight + (membersGroupHeight ? membersGroupHeight + GAP * 4 : GAP * 2)})`
); );
bbox = shapeSvg.node()!.getBBox(); bbox = shapeSvg.node()!.getBBox();
@@ -107,10 +107,9 @@ async function addText<T extends SVGGraphicsElement>(
'useHtmlLabels' in node ? node.useHtmlLabels : (evaluate(config.htmlLabels) ?? true); 'useHtmlLabels' in node ? node.useHtmlLabels : (evaluate(config.htmlLabels) ?? true);
let textContent = ''; let textContent = '';
// Support regular node type (.label) and classNodes (.text) // Support regular node type (.label) and classNodes (.text)
if ('text' in node) { if ('text' in node) {
textContent = node.text ?? ''; textContent = node.text;
} else { } else {
textContent = node.label!; textContent = node.label!;
} }

View File

@@ -190,8 +190,8 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
// add annotations // add annotations
let isFirst = true; let isFirst = true;
classDef.annotations.forEach(function (annotation) { classDef.annotations.forEach(function (member) {
const titleText2 = title.append('tspan').text('«' + annotation + '»'); const titleText2 = title.append('tspan').text('«' + member + '»');
if (!isFirst) { if (!isFirst) {
titleText2.attr('dy', conf.textHeight); titleText2.attr('dy', conf.textHeight);
} }
@@ -208,19 +208,19 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
} }
const titleHeight = title.node().getBBox().height; const titleHeight = title.node().getBBox().height;
let attributesLine; let membersLine;
let attributesBox; let membersBox;
let methodsLine; let methodsLine;
// don't draw box if no attributes // don't draw box if no members
if (classDef.attributes.length > 0) { if (classDef.members.length > 0) {
attributesLine = g membersLine = g
.append('line') // text label for the x axis .append('line') // text label for the x axis
.attr('x1', 0) .attr('x1', 0)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2) .attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2); .attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2);
const attributes = g const members = g
.append('text') // text label for the x axis .append('text') // text label for the x axis
.attr('x', conf.padding) .attr('x', conf.padding)
.attr('y', titleHeight + conf.dividerMargin + conf.textHeight) .attr('y', titleHeight + conf.dividerMargin + conf.textHeight)
@@ -228,12 +228,12 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
.attr('class', 'classText'); .attr('class', 'classText');
isFirst = true; isFirst = true;
classDef.attributes.forEach(function (attribute) { classDef.members.forEach(function (member) {
addTspan(attributes, attribute, isFirst, conf); addTspan(members, member, isFirst, conf);
isFirst = false; isFirst = false;
}); });
attributesBox = attributes.node().getBBox(); membersBox = members.node().getBBox();
} }
// don't draw box if no methods // don't draw box if no methods
@@ -241,13 +241,13 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
methodsLine = g methodsLine = g
.append('line') // text label for the x axis .append('line') // text label for the x axis
.attr('x1', 0) .attr('x1', 0)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin + attributesBox.height) .attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin + attributesBox.height); .attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height);
const methods = g const methods = g
.append('text') // text label for the x axis .append('text') // text label for the x axis
.attr('x', conf.padding) .attr('x', conf.padding)
.attr('y', titleHeight + 2 * conf.dividerMargin + attributesBox.height + conf.textHeight) .attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight)
.attr('fill', 'white') .attr('fill', 'white')
.attr('class', 'classText'); .attr('class', 'classText');
@@ -286,8 +286,8 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
title.insert('title').text(classDef.tooltip); title.insert('title').text(classDef.tooltip);
} }
if (attributesLine) { if (membersLine) {
attributesLine.attr('x2', rectWidth); membersLine.attr('x2', rectWidth);
} }
if (methodsLine) { if (methodsLine) {
methodsLine.attr('x2', rectWidth); methodsLine.attr('x2', rectWidth);

View File

@@ -896,6 +896,7 @@ const addNodeFromVertex = (
const baseNode = { const baseNode = {
id: vertex.id, id: vertex.id,
label: vertex.text, label: vertex.text,
labelType: vertex.labelType,
labelStyle: '', labelStyle: '',
parentId, parentId,
padding: config.flowchart?.padding || 8, padding: config.flowchart?.padding || 8,
@@ -1002,6 +1003,7 @@ export const getData = () => {
end: rawEdge.end, end: rawEdge.end,
type: rawEdge.type ?? 'normal', type: rawEdge.type ?? 'normal',
label: rawEdge.text, label: rawEdge.text,
labelType: rawEdge.labelType,
labelpos: 'c', labelpos: 'c',
thickness: rawEdge.stroke, thickness: rawEdge.stroke,
minlen: rawEdge.length, minlen: rawEdge.length,

View File

@@ -26,12 +26,16 @@ export const getLabelStyles = (styleArray) => {
export const insertEdgeLabel = async (elem, edge) => { export const insertEdgeLabel = async (elem, edge) => {
let useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels); let useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
const labelElement = await createText(elem, edge.label, { const labelElement =
style: getLabelStyles(edge.labelStyle), edge.labelType === 'markdown'
useHtmlLabels, ? await createText(elem, edge.label, {
addSvgBackground: true, style: getLabelStyles(edge.labelStyle),
isNode: false, useHtmlLabels,
}); addSvgBackground: true,
isNode: false,
})
: await createLabel(edge.label, getLabelStyles(edge.labelStyle), undefined, false);
log.info('abc82', edge, edge.labelType); log.info('abc82', edge, edge.labelType);
// Create outer g, edgeLabel, this will be positioned after graph layout // Create outer g, edgeLabel, this will be positioned after graph layout

View File

@@ -18,7 +18,7 @@ export async function classBox<T extends SVGGraphicsElement>(parent: D3Selection
// Treat node as classNode // Treat node as classNode
const classNode = node as unknown as ClassNode; const classNode = node as unknown as ClassNode;
classNode.annotations = classNode.annotations ?? []; classNode.annotations = classNode.annotations ?? [];
classNode.attributes = classNode.attributes ?? []; classNode.members = classNode.members ?? [];
classNode.methods = classNode.methods ?? []; classNode.methods = classNode.methods ?? [];
const { shapeSvg, bbox } = await textHelper(parent, node, config, useHtmlLabels, GAP); const { shapeSvg, bbox } = await textHelper(parent, node, config, useHtmlLabels, GAP);
@@ -35,7 +35,7 @@ export async function classBox<T extends SVGGraphicsElement>(parent: D3Selection
} }
const renderExtraBox = const renderExtraBox =
classNode.attributes.length === 0 && classNode.members.length === 0 &&
classNode.methods.length === 0 && classNode.methods.length === 0 &&
!config.class?.hideEmptyMembersBox; !config.class?.hideEmptyMembersBox;
@@ -51,9 +51,9 @@ export async function classBox<T extends SVGGraphicsElement>(parent: D3Selection
const w = bbox.width; const w = bbox.width;
let h = bbox.height; let h = bbox.height;
if (classNode.attributes.length === 0 && classNode.methods.length === 0) { if (classNode.members.length === 0 && classNode.methods.length === 0) {
h += GAP; h += GAP;
} else if (classNode.attributes.length > 0 && classNode.methods.length === 0) { } else if (classNode.members.length > 0 && classNode.methods.length === 0) {
h += GAP * 2; h += GAP * 2;
} }
const x = -w / 2; const x = -w / 2;
@@ -66,7 +66,7 @@ export async function classBox<T extends SVGGraphicsElement>(parent: D3Selection
PADDING - PADDING -
(renderExtraBox (renderExtraBox
? PADDING ? PADDING
: classNode.attributes.length === 0 && classNode.methods.length === 0 : classNode.members.length === 0 && classNode.methods.length === 0
? -PADDING / 2 ? -PADDING / 2
: 0), : 0),
w + 2 * PADDING, w + 2 * PADDING,
@@ -74,7 +74,7 @@ export async function classBox<T extends SVGGraphicsElement>(parent: D3Selection
2 * PADDING + 2 * PADDING +
(renderExtraBox (renderExtraBox
? PADDING * 2 ? PADDING * 2
: classNode.attributes.length === 0 && classNode.methods.length === 0 : classNode.members.length === 0 && classNode.methods.length === 0
? -PADDING ? -PADDING
: 0), : 0),
options options
@@ -107,7 +107,7 @@ export async function classBox<T extends SVGGraphicsElement>(parent: D3Selection
PADDING - PADDING -
(renderExtraBox (renderExtraBox
? PADDING ? PADDING
: classNode.attributes.length === 0 && classNode.methods.length === 0 : classNode.members.length === 0 && classNode.methods.length === 0
? -PADDING / 2 ? -PADDING / 2
: 0); : 0);
if (!useHtmlLabels) { if (!useHtmlLabels) {
@@ -138,11 +138,11 @@ export async function classBox<T extends SVGGraphicsElement>(parent: D3Selection
const labelGroupHeight = const labelGroupHeight =
(shapeSvg.select('.label-group').node() as SVGGraphicsElement).getBBox().height - (shapeSvg.select('.label-group').node() as SVGGraphicsElement).getBBox().height -
(renderExtraBox ? PADDING / 2 : 0) || 0; (renderExtraBox ? PADDING / 2 : 0) || 0;
const attributeGroupHeight = const membersGroupHeight =
(shapeSvg.select('.attribute-group').node() as SVGGraphicsElement).getBBox().height - (shapeSvg.select('.members-group').node() as SVGGraphicsElement).getBBox().height -
(renderExtraBox ? PADDING / 2 : 0) || 0; (renderExtraBox ? PADDING / 2 : 0) || 0;
// First line (under label) // First line (under label)
if (classNode.attributes.length > 0 || classNode.methods.length > 0 || renderExtraBox) { if (classNode.members.length > 0 || classNode.methods.length > 0 || renderExtraBox) {
const roughLine = rc.line( const roughLine = rc.line(
rectBBox.x, rectBBox.x,
annotationGroupHeight + labelGroupHeight + y + PADDING, annotationGroupHeight + labelGroupHeight + y + PADDING,
@@ -154,13 +154,13 @@ export async function classBox<T extends SVGGraphicsElement>(parent: D3Selection
line.attr('class', 'divider').attr('style', styles); line.attr('class', 'divider').attr('style', styles);
} }
// Second line (under attributes) // Second line (under members)
if (renderExtraBox || classNode.attributes.length > 0 || classNode.methods.length > 0) { if (renderExtraBox || classNode.members.length > 0 || classNode.methods.length > 0) {
const roughLine = rc.line( const roughLine = rc.line(
rectBBox.x, rectBBox.x,
annotationGroupHeight + labelGroupHeight + attributeGroupHeight + y + GAP * 2 + PADDING, annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + GAP * 2 + PADDING,
rectBBox.x + rectBBox.width, rectBBox.x + rectBBox.width,
annotationGroupHeight + labelGroupHeight + attributeGroupHeight + y + PADDING + GAP * 2, annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + PADDING + GAP * 2,
options options
); );
const line = shapeSvg.insert(() => roughLine); const line = shapeSvg.insert(() => roughLine);

View File

@@ -1,3 +1,4 @@
import createLabel from '../createLabel.js';
import { createText } from '../../createText.js'; import { createText } from '../../createText.js';
import type { Node } from '../../types.js'; import type { Node } from '../../types.js';
import { getConfig } from '../../../diagram-api/diagramAPI.js'; import { getConfig } from '../../../diagram-api/diagramAPI.js';
@@ -40,14 +41,26 @@ export const labelHelper = async <T extends SVGGraphicsElement>(
label = typeof node.label === 'string' ? node.label : node.label[0]; label = typeof node.label === 'string' ? node.label : node.label[0];
} }
const text = await createText(labelEl, sanitizeText(decodeEntities(label), getConfig()), { let text;
useHtmlLabels, if (node.labelType !== 'string') {
width: node.width || getConfig().flowchart?.wrappingWidth, text = await createText(labelEl, sanitizeText(decodeEntities(label), getConfig()), {
// @ts-expect-error -- This is currently not used. Should this be `classes` instead? useHtmlLabels,
cssClasses: 'markdown-node-label', width: node.width || getConfig().flowchart?.wrappingWidth,
style: node.labelStyle, // @ts-expect-error -- This is currently not used. Should this be `classes` instead?
addSvgBackground: !!node.icon || !!node.img, cssClasses: 'markdown-node-label',
}); style: node.labelStyle,
addSvgBackground: !!node.icon || !!node.img,
});
} else {
const labelElement = await createLabel(
sanitizeText(decodeEntities(label), getConfig()),
node.labelStyle,
false,
true
);
text = labelEl.node()?.appendChild(labelElement);
}
// Get the size of the label // Get the size of the label
let bbox = text.getBBox(); let bbox = text.getBBox();
const halfPadding = (node?.padding ?? 0) / 2; const halfPadding = (node?.padding ?? 0) / 2;