diff --git a/cypress/platform/yari.html b/cypress/platform/yari.html
new file mode 100644
index 000000000..5031a1ed2
--- /dev/null
+++ b/cypress/platform/yari.html
@@ -0,0 +1,32 @@
+
+
+
+ classDiagram
+ class Square~Shape~{
+ int id
+ List~int~ position
+ setPoints(List~int~ points)
+ getPoints() List~int~
+ }
+
+Square : -List~string~ messages
+Square : +setMessages(List~string~ messages)
+Square : +getMessages() List~string~
+Square : +getDistanceMatrix() List~List~int~~
+
+
+ classDiagram class Duck{ +String beakColor +swim() +quack() }
+
+
+
+
diff --git a/packages/mermaid/src/diagrams/class/classDb.ts b/packages/mermaid/src/diagrams/class/classDb.ts
index 1fec5c2dc..06046f882 100644
--- a/packages/mermaid/src/diagrams/class/classDb.ts
+++ b/packages/mermaid/src/diagrams/class/classDb.ts
@@ -80,6 +80,8 @@ export const addClass = function (_id: string) {
id: name,
type: type,
label: name,
+ text: `${name}${type ? `<${type}>` : ''}`,
+ shape: 'classBox',
cssClasses: [],
methods: [],
members: [],
@@ -472,6 +474,11 @@ export const setCssStyle = function (id: string, styles: string[]) {
}
};
+export const getData = () => {
+ const config = getConfig();
+ return { nodes: classes, edges: relations, other: {}, config, direction: getDirection() };
+};
+
export default {
setAccTitle,
getAccTitle,
@@ -509,4 +516,5 @@ export default {
getNamespace,
getNamespaces,
setCssStyle,
+ getData,
};
diff --git a/packages/mermaid/src/diagrams/class/classDiagram-v2.ts b/packages/mermaid/src/diagrams/class/classDiagram-v2.ts
index ec5398d29..6a3747e41 100644
--- a/packages/mermaid/src/diagrams/class/classDiagram-v2.ts
+++ b/packages/mermaid/src/diagrams/class/classDiagram-v2.ts
@@ -3,7 +3,7 @@ import type { DiagramDefinition } from '../../diagram-api/types.js';
import parser from './parser/classDiagram.jison';
import db from './classDb.js';
import styles from './styles.js';
-import renderer from './classRenderer-v2.js';
+import renderer from './classRenderer-v3-unified.js';
export const diagram: DiagramDefinition = {
parser,
diff --git a/packages/mermaid/src/diagrams/class/classRenderer-v3-unified.ts b/packages/mermaid/src/diagrams/class/classRenderer-v3-unified.ts
new file mode 100644
index 000000000..404a53c6c
--- /dev/null
+++ b/packages/mermaid/src/diagrams/class/classRenderer-v3-unified.ts
@@ -0,0 +1,87 @@
+import { getConfig } from '../../diagram-api/diagramAPI.js';
+import type { DiagramStyleClassDef } from '../../diagram-api/types.js';
+import { log } from '../../logger.js';
+import { getDiagramElements } from '../../rendering-util/insertElementsForSize.js';
+import { render } from '../../rendering-util/render.js';
+import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
+import type { LayoutData } from '../../rendering-util/types.js';
+import utils from '../../utils.js';
+// import { CSS_DIAGRAM, DEFAULT_NESTED_DOC_DIR } from './stateCommon.js';
+
+/**
+ * Get the direction from the statement items.
+ * Look through all of the documents (docs) in the parsedItems
+ * Because is a _document_ direction, the default direction is not necessarily the same as the overall default _diagram_ direction.
+ * @param parsedItem - the parsed statement item to look through
+ * @param defaultDir - the direction to use if none is found
+ * @returns The direction to use
+ */
+export const getDir = (parsedItem: any, defaultDir = DEFAULT_NESTED_DOC_DIR) => {
+ if (!parsedItem.doc) {
+ return defaultDir;
+ }
+
+ let dir = defaultDir;
+
+ for (const parsedItemDoc of parsedItem.doc) {
+ if (parsedItemDoc.stmt === 'dir') {
+ dir = parsedItemDoc.value;
+ }
+ }
+
+ return dir;
+};
+
+export const getClasses = function (
+ text: string,
+ diagramObj: any
+): Map {
+ // diagramObj.db.extract(diagramObj.db.getRootDocV2());
+ return diagramObj.db.getClasses();
+};
+
+export const draw = async function (text: string, id: string, _version: string, diag: any) {
+ log.info('REF0:');
+ log.info('Drawing class diagram (v3)', id);
+ const { securityLevel, state: conf, layout } = getConfig();
+ // Extracting the data from the parsed structure into a more usable form
+ // Not related to the refactoring, but this is the first step in the rendering process
+ // diag.db.extract(diag.db.getRootDocV2());
+
+ //const DIR = getDir(diag.db.getRootDocV2());
+
+ // The getData method provided in all supported diagrams is used to extract the data from the parsed structure
+ // into the Layout data format
+ const data4Layout = diag.db.getData() as LayoutData;
+
+ // Create the root SVG - the element is the div containing the SVG element
+ const { element, svg } = getDiagramElements(id, securityLevel);
+
+ data4Layout.type = diag.type;
+ data4Layout.layoutAlgorithm = layout;
+
+ // TODO: Should we move these two to baseConfig? These types are not there in StateConfig.
+
+ data4Layout.nodeSpacing = conf?.nodeSpacing || 50;
+ data4Layout.rankSpacing = conf?.rankSpacing || 50;
+ data4Layout.markers = ['barb'];
+ data4Layout.diagramId = id;
+ await render(data4Layout, svg, element);
+ const padding = 8;
+ utils.insertTitle(
+ element,
+ 'statediagramTitleText',
+ conf?.titleTopMargin ?? 25,
+ diag.db.getDiagramTitle()
+ );
+
+ const CSS_DIAGRAM = 'classDiagram';
+
+ setupViewPortForSVG(svg, padding, CSS_DIAGRAM, conf?.useMaxWidth ?? true);
+};
+
+export default {
+ getClasses,
+ draw,
+ getDir,
+};
diff --git a/packages/mermaid/src/diagrams/class/classTypes.ts b/packages/mermaid/src/diagrams/class/classTypes.ts
index f1955a224..cb3ba8325 100644
--- a/packages/mermaid/src/diagrams/class/classTypes.ts
+++ b/packages/mermaid/src/diagrams/class/classTypes.ts
@@ -5,6 +5,7 @@ export interface ClassNode {
id: string;
type: string;
label: string;
+ text: string;
cssClasses: string[];
methods: ClassMember[];
members: ClassMember[];
@@ -30,6 +31,7 @@ export class ClassMember {
cssStyle!: string;
memberType!: 'method' | 'attribute';
visibility!: Visibility;
+ text: string;
/**
* denote if static or to determine which css class to apply to the node
* @defaultValue ''
@@ -50,6 +52,7 @@ export class ClassMember {
this.memberType = memberType;
this.visibility = '';
this.classifier = '';
+ this.text = '';
const sanitizedInput = sanitizeText(input, getConfig());
this.parseMember(sanitizedInput);
}
@@ -118,6 +121,21 @@ export class ClassMember {
}
this.classifier = potentialClassifier;
+ this.text = `${this.visibility}${this.id}${this.memberType === 'method' ? `(${this.parameters})${this.returnType ? ' : ' + this.returnType : ''}` : ''}`; //.replaceAll('~', '<');
+ const combinedText = `${this.visibility}${this.id}${this.memberType === 'method' ? `(${this.parameters})${this.returnType ? ' : ' + this.returnType : ''}` : ''}`;
+ if (combinedText.includes('~')) {
+ let count = (combinedText.match(/~/g) ?? []).length;
+
+ // Replace all '~' with '>'
+ let replacedRaw = combinedText.replaceAll('~', '>');
+
+ // Replace the first half of '>' with '<'
+ while (count > 0) {
+ replacedRaw = replacedRaw.replace('>', '<');
+ count -= 2; // Each iteration replaces one '>' with '<', so reduce count by 2
+ }
+ this.text = replacedRaw;
+ }
}
parseClassifier() {
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/nodes.js b/packages/mermaid/src/rendering-util/rendering-elements/nodes.js
index 2f69a36bc..5b6ce08d5 100644
--- a/packages/mermaid/src/rendering-util/rendering-elements/nodes.js
+++ b/packages/mermaid/src/rendering-util/rendering-elements/nodes.js
@@ -22,6 +22,7 @@ import { lean_left } from './shapes/leanLeft.js';
import { trapezoid } from './shapes/trapezoid.js';
import { inv_trapezoid } from './shapes/invertedTrapezoid.js';
import { labelRect } from './shapes/labelRect.js';
+import { classBox } from './shapes/classBox.js';
const shapes = {
state,
@@ -47,6 +48,7 @@ const shapes = {
trapezoid,
inv_trapezoid,
labelRect,
+ classBox,
};
const nodeElems = new Map();
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts
new file mode 100644
index 000000000..86fb40a62
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts
@@ -0,0 +1,139 @@
+import { getNodeClasses } from './util.js';
+import { getConfig } from '$root/diagram-api/diagramAPI.js';
+import { createText } from '../../createText.js';
+import { select } from 'd3';
+import type { Node } from '$root/rendering-util/types.d.ts';
+import { evaluate } from '$root/diagrams/common/common.js';
+import { calculateTextDimensions, calculateTextWidth } from '$root/utils.js';
+import { styles2String } from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
+
+export const classBox = async (parent: SVGAElement, node: Node): Promise => {
+ const { labelStyles, nodeStyles } = styles2String(node);
+ node.labelStyle = labelStyles;
+
+ const mainGroup = parent
+ .insert('g')
+ .attr('class', getNodeClasses(node))
+ .attr('style', nodeStyles)
+ .attr('id', node.domId || node.id);
+ let labelGroup = null;
+ let membersGroup = null;
+ let methodsGroup = null;
+
+ let labelGroupHeight = 0;
+ let membersGroupHeight = 0;
+
+ const config = getConfig();
+
+ const PADDING = config.class.padding;
+ const GAP = PADDING;
+
+ if (node.label) {
+ labelGroup = mainGroup.insert('g').attr('class', 'label-group');
+ // TODO: Add Padding
+ await helper(labelGroup, node, 0);
+ const labelGroupBBox = labelGroup.node().getBBox();
+ labelGroupHeight = labelGroupBBox.height;
+ }
+
+ if (node.members) {
+ membersGroup = mainGroup.insert('g').attr('class', 'members-group');
+ let yOffset = 0;
+ for (const member of node.members) {
+ await helper(membersGroup, member, yOffset);
+ yOffset += calculateTextDimensions(member.text, config).height;
+ }
+ membersGroupHeight = membersGroup.node().getBBox().height;
+ membersGroup.attr('transform', `translate(0, ${labelGroupHeight + GAP * 3})`);
+ }
+
+ if (node.methods) {
+ methodsGroup = mainGroup.insert('g').attr('class', 'methods-group');
+ let methodsYOffset = 0;
+ for (const method of node.methods) {
+ await helper(methodsGroup, method, methodsYOffset);
+ methodsYOffset += calculateTextDimensions(method.text, config).height;
+ }
+ // TODO: Update transform
+ methodsGroup.attr(
+ 'transform',
+ `translate(0, ${labelGroupHeight + (membersGroupHeight ? membersGroupHeight + GAP * 5 : GAP * 3)})`
+ );
+ }
+
+ const mainGroupBBox = mainGroup.node().getBBox();
+ const labelGroupBBox = labelGroup.node().getBBox();
+ // Center label
+ labelGroup.attr(
+ 'transform',
+ `translate(${mainGroupBBox.width / 2 - labelGroupBBox.width / 2}, 0)`
+ );
+
+ // Insert the rectangle around the main group
+ mainGroup
+ .insert('rect', ':first-child')
+ .attr('class', 'basic label-container')
+ .attr('style', nodeStyles)
+ .attr('data-id', 'abc')
+ .attr('data-et', 'node')
+ .attr('x', mainGroupBBox.x - PADDING)
+ .attr('y', mainGroupBBox.y - PADDING)
+ .attr('width', mainGroupBBox.width + 2 * PADDING)
+ .attr('height', mainGroupBBox.height + 2 * PADDING);
+
+ // Render separating lines.
+ if (node.label) {
+ mainGroup
+ .insert('line')
+ .attr('x1', 0 - PADDING)
+ .attr('y1', labelGroupHeight - GAP)
+ .attr('x2', mainGroupBBox.width + PADDING)
+ .attr('y2', labelGroupHeight - GAP)
+ .attr('class', 'divider');
+ }
+
+ if (node.members.length > 0 && node.methods.length > 0) {
+ mainGroup
+ .insert('line')
+ .attr('x1', 0 - PADDING)
+ .attr('y1', labelGroupHeight + membersGroupHeight + GAP * 2)
+ .attr('x2', mainGroupBBox.width + PADDING)
+ .attr('y2', labelGroupHeight + membersGroupHeight + GAP * 2)
+ .attr('class', 'divider');
+ }
+
+ return mainGroup;
+};
+
+const helper = async (parentGroup, node, yOffset) => {
+ const textEl = parentGroup.insert('g').attr('class', 'label').attr('style', node.labelStyle);
+ const textContent = node.text;
+ const config = getConfig();
+ const text = await createText(
+ textEl,
+ textContent,
+ {
+ width: calculateTextWidth(textContent, config),
+ classes: 'markdown-node-label',
+ style: node.labelStyle,
+ useHtmlLabels: true,
+ },
+ config
+ );
+
+ let bbox = text.getBBox();
+
+ if (evaluate(config.flowchart.htmlLabels)) {
+ const div = text.children[0];
+ const dv = select(text);
+
+ bbox = div.getBoundingClientRect();
+ dv.attr('width', bbox.width);
+ dv.attr('height', bbox.height);
+ }
+
+ textEl.attr('transform', 'translate(' + 0 + ', ' + (-bbox.height / 2 + yOffset) + ')');
+ if (node.centerLabel) {
+ textEl.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');
+ }
+};