335 lines
13 KiB
JavaScript
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 };
|
|
};
|