Test/node_modules/@excalidraw/mermaid-to-excalidraw/dist/parser/class.js
2026-04-09 22:54:00 +07:00

335 lines
13 KiB
JavaScript

import { nanoid } from "nanoid";
import { computeEdgePositions, getTransformAttr } from "../utils.js";
import { createArrowSkeletion, createContainerSkeletonFromSVG, createLineSkeletonFromSVG, createTextSkeleton, } from "../elementSkeleton.js";
// Taken from mermaidParser.relationType
const RELATION_TYPE = {
AGGREGATION: 0,
EXTENSION: 1,
COMPOSITION: 2,
DEPENDENCY: 3,
LOLLIPOP: 4,
};
// Taken from mermaidParser.lineType
const LINE_TYPE = {
LINE: 0,
DOTTED_LINE: 1,
};
// This is the offset to update the arrow head postition for rendering in excalidraw as mermaid calculates the position until the start of arrowhead
const MERMAID_ARROW_HEAD_OFFSET = 16;
const getStrokeStyle = (type) => {
let lineType;
switch (type) {
case LINE_TYPE.LINE:
lineType = "solid";
break;
case LINE_TYPE.DOTTED_LINE:
lineType = "dotted";
break;
default:
lineType = "solid";
}
return lineType;
};
const getArrowhead = (type) => {
let arrowhead;
switch (type) {
case RELATION_TYPE.AGGREGATION:
arrowhead = "diamond_outline";
break;
case RELATION_TYPE.COMPOSITION:
arrowhead = "diamond";
break;
case RELATION_TYPE.EXTENSION:
arrowhead = "triangle_outline";
break;
case "none":
arrowhead = null;
break;
case RELATION_TYPE.DEPENDENCY:
default:
arrowhead = "arrow";
break;
}
return arrowhead;
};
const parseClasses = (classes, containerEl) => {
const nodes = [];
const lines = [];
const text = [];
Object.values(classes).forEach((classNode) => {
const { domId, id: classId } = classNode;
const groupId = nanoid();
const domNode = containerEl.querySelector(`[data-id=${classId}]`);
if (!domNode) {
throw Error(`DOM Node with id ${domId} not found`);
}
const { transformX, transformY } = getTransformAttr(domNode);
const container = createContainerSkeletonFromSVG(domNode.firstChild, "rectangle", { id: classId, groupId });
container.x += transformX;
container.y += transformY;
container.metadata = { classId };
nodes.push(container);
const lineNodes = Array.from(domNode.querySelectorAll(".divider"));
lineNodes.forEach((lineNode) => {
const startX = Number(lineNode.getAttribute("x1"));
const startY = Number(lineNode.getAttribute("y1"));
const endX = Number(lineNode.getAttribute("x2"));
const endY = Number(lineNode.getAttribute("y2"));
const line = createLineSkeletonFromSVG(lineNode, startX, startY, endX, endY, {
groupId,
id: nanoid(),
});
line.startX += transformX;
line.startY += transformY;
line.endX += transformX;
line.endY += transformY;
line.metadata = { classId };
lines.push(line);
});
const labelNodes = domNode.querySelector(".label")?.children;
if (!labelNodes) {
throw "label nodes not found";
}
Array.from(labelNodes).forEach((node) => {
const label = node.textContent;
if (!label) {
return;
}
const id = nanoid();
const { transformX: textTransformX, transformY: textTransformY } = getTransformAttr(node);
const boundingBox = node.getBBox();
const offsetY = 10;
const textElement = createTextSkeleton(transformX + textTransformX, transformY + textTransformY + offsetY, label, {
width: boundingBox.width,
height: boundingBox.height,
id,
groupId,
metadata: { classId },
});
text.push(textElement);
});
});
return { nodes, lines, text };
};
// update arrow position by certain offset for triangle and diamond arrow head types
// as mermaid calculates the position until the start of arrowhead
// for reference - https://github.com/mermaid-js/mermaid/issues/5417
const adjustArrowPosition = (direction, arrow) => {
// The arrowhead shapes where we need to update the position by a 16px offset
const arrowHeadShapes = ["triangle_outline", "diamond", "diamond_outline"];
const shouldUpdateStartArrowhead = arrow.startArrowhead && arrowHeadShapes.includes(arrow.startArrowhead);
const shouldUpdateEndArrowhead = arrow.endArrowhead && arrowHeadShapes.includes(arrow.endArrowhead);
if (!shouldUpdateEndArrowhead && !shouldUpdateStartArrowhead) {
return arrow;
}
if (shouldUpdateStartArrowhead) {
if (direction === "LR") {
arrow.startX -= MERMAID_ARROW_HEAD_OFFSET;
}
else if (direction === "RL") {
arrow.startX += MERMAID_ARROW_HEAD_OFFSET;
}
else if (direction === "TB") {
arrow.startY -= MERMAID_ARROW_HEAD_OFFSET;
}
else if (direction === "BT") {
arrow.startY += MERMAID_ARROW_HEAD_OFFSET;
}
}
if (shouldUpdateEndArrowhead) {
if (direction === "LR") {
arrow.endX += MERMAID_ARROW_HEAD_OFFSET;
}
else if (direction === "RL") {
arrow.endX -= MERMAID_ARROW_HEAD_OFFSET;
}
else if (direction === "TB") {
arrow.endY += MERMAID_ARROW_HEAD_OFFSET;
}
else if (direction === "BT") {
arrow.endY -= MERMAID_ARROW_HEAD_OFFSET;
}
}
return arrow;
};
const parseRelations = (relations, classNodes, containerEl, direction) => {
const edges = containerEl.querySelector(".edgePaths")?.children;
if (!edges) {
throw new Error("No Edges found!");
}
const arrows = [];
const text = [];
relations.forEach((relationNode, index) => {
const { id1, id2, relation } = relationNode;
const node1 = classNodes.find((node) => node.id === id1);
const node2 = classNodes.find((node) => node.id === id2);
const strokeStyle = getStrokeStyle(relation.lineType);
const startArrowhead = getArrowhead(relation.type1);
const endArrowhead = getArrowhead(relation.type2);
const edgePositionData = computeEdgePositions(edges[index]);
const arrowSkeletion = createArrowSkeletion(edgePositionData.startX, edgePositionData.startY, edgePositionData.endX, edgePositionData.endY, {
strokeStyle,
startArrowhead,
endArrowhead,
label: relationNode.title ? { text: relationNode.title } : undefined,
start: { type: "rectangle", id: node1.id },
end: { type: "rectangle", id: node2.id },
});
const arrow = adjustArrowPosition(direction, arrowSkeletion);
arrows.push(arrow);
// Add cardianlities and Multiplicities
const { relationTitle1, relationTitle2 } = relationNode;
const offsetX = 20;
const offsetY = 15;
const directionOffset = 15;
let x;
let y;
if (relationTitle1 && relationTitle1 !== "none") {
switch (direction) {
case "TB":
x = arrow.startX - offsetX;
if (arrow.endX < arrow.startX) {
x -= directionOffset;
}
y = arrow.startY + offsetY;
break;
case "BT":
x = arrow.startX + offsetX;
if (arrow.endX > arrow.startX) {
x += directionOffset;
}
y = arrow.startY - offsetY;
break;
case "LR":
x = arrow.startX + offsetX;
y = arrow.startY + offsetY;
if (arrow.endY > arrow.startY) {
y += directionOffset;
}
break;
case "RL":
x = arrow.startX - offsetX;
y = arrow.startY - offsetY;
if (arrow.startY > arrow.endY) {
y -= directionOffset;
}
break;
default:
x = arrow.startX - offsetX;
y = arrow.startY + offsetY;
}
const relationTitleElement = createTextSkeleton(x, y, relationTitle1, {
fontSize: 16,
});
text.push(relationTitleElement);
}
if (relationTitle2 && relationTitle2 !== "none") {
switch (direction) {
case "TB":
x = arrow.endX + offsetX;
if (arrow.endX < arrow.startX) {
x += directionOffset;
}
y = arrow.endY - offsetY;
break;
case "BT":
x = arrow.endX - offsetX;
if (arrow.endX > arrow.startX) {
x -= directionOffset;
}
y = arrow.endY + offsetY;
break;
case "LR":
x = arrow.endX - offsetX;
y = arrow.endY - offsetY;
if (arrow.endY > arrow.startY) {
y -= directionOffset;
}
break;
case "RL":
x = arrow.endX + offsetX;
y = arrow.endY + offsetY;
if (arrow.startY > arrow.endY) {
y += directionOffset;
}
break;
default:
x = arrow.endX + offsetX;
y = arrow.endY - offsetY;
}
const relationTitleElement = createTextSkeleton(x, y, relationTitle2, {
fontSize: 16,
});
text.push(relationTitleElement);
}
});
return { arrows, text };
};
const parseNotes = (notes, containerEl, classNodes) => {
const noteContainers = [];
const connectors = [];
notes.forEach((note) => {
const { id, text, class: classId } = note;
const node = containerEl.querySelector(`#${id}`);
if (!node) {
throw new Error(`Node with id ${id} not found!`);
}
const { transformX, transformY } = getTransformAttr(node);
const rect = node.firstChild;
const container = createContainerSkeletonFromSVG(rect, "rectangle", {
id,
subtype: "note",
label: { text },
});
Object.assign(container, {
x: container.x + transformX,
y: container.y + transformY,
});
noteContainers.push(container);
if (classId) {
const classNode = classNodes.find((node) => node.id === classId);
if (!classNode) {
throw new Error(`class node with id ${classId} not found!`);
}
const startX = container.x + (container.width || 0) / 2;
const startY = container.y + (container.height || 0);
const endX = startX;
const endY = classNode.y;
const connector = createArrowSkeletion(startX, startY, endX, endY, {
strokeStyle: "dotted",
startArrowhead: null,
endArrowhead: null,
start: { id: container.id, type: "rectangle" },
end: { id: classNode.id, type: "rectangle" },
});
connectors.push(connector);
}
});
return { notes: noteContainers, connectors };
};
export const parseMermaidClassDiagram = (diagram, containerEl) => {
diagram.parse();
//@ts-ignore
const mermaidParser = diagram.parser.yy;
const direction = mermaidParser.getDirection();
const nodes = [];
const lines = [];
const text = [];
const classNodes = [];
const namespaces = mermaidParser.getNamespaces();
const classes = mermaidParser.getClasses();
if (Object.keys(classes).length) {
const classData = parseClasses(classes, containerEl);
nodes.push(classData.nodes);
lines.push(...classData.lines);
text.push(...classData.text);
classNodes.push(...classData.nodes);
}
const relations = mermaidParser.getRelations();
const { arrows, text: relationTitles } = parseRelations(relations, classNodes, containerEl, direction);
const { notes, connectors } = parseNotes(mermaidParser.getNotes(), containerEl, classNodes);
nodes.push(notes);
arrows.push(...connectors);
text.push(...relationTitles);
return { type: "class", nodes, lines, arrows, text, namespaces };
};