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

331 lines
13 KiB
JavaScript

import { SVG_TO_SHAPE_MAPPER } from "../constants.js";
import { nanoid } from "nanoid";
import { createArrowSkeletonFromSVG, createContainerSkeletonFromSVG, createLineSkeletonFromSVG, createTextSkeletonFromSVG, } from "../elementSkeleton.js";
// Currently mermaid supported these 6 arrow types, the names are taken from mermaidParser.LINETYPE
const SEQUENCE_ARROW_TYPES = {
0: "SOLID",
1: "DOTTED",
3: "SOLID_CROSS",
4: "DOTTED_CROSS",
5: "SOLID_OPEN",
6: "DOTTED_OPEN",
24: "SOLID_POINT",
25: "DOTTED_POINT",
};
const MESSAGE_TYPE = {
SOLID: 0,
DOTTED: 1,
NOTE: 2,
SOLID_CROSS: 3,
DOTTED_CROSS: 4,
SOLID_OPEN: 5,
DOTTED_OPEN: 6,
LOOP_START: 10,
LOOP_END: 11,
ALT_START: 12,
ALT_ELSE: 13,
ALT_END: 14,
OPT_START: 15,
OPT_END: 16,
ACTIVE_START: 17,
ACTIVE_END: 18,
PAR_START: 19,
PAR_AND: 20,
PAR_END: 21,
RECT_START: 22,
RECT_END: 23,
SOLID_POINT: 24,
DOTTED_POINT: 25,
AUTONUMBER: 26,
CRITICAL_START: 27,
CRITICAL_OPTION: 28,
CRITICAL_END: 29,
BREAK_START: 30,
BREAK_END: 31,
PAR_OVER_START: 32,
};
const getStrokeStyle = (type) => {
let strokeStyle;
switch (type) {
case MESSAGE_TYPE.SOLID:
case MESSAGE_TYPE.SOLID_CROSS:
case MESSAGE_TYPE.SOLID_OPEN:
case MESSAGE_TYPE.SOLID_POINT:
strokeStyle = "solid";
break;
case MESSAGE_TYPE.DOTTED:
case MESSAGE_TYPE.DOTTED_CROSS:
case MESSAGE_TYPE.DOTTED_OPEN:
case MESSAGE_TYPE.DOTTED_POINT:
strokeStyle = "dotted";
break;
default:
strokeStyle = "solid";
break;
}
return strokeStyle;
};
const attachSequenceNumberToArrow = (node, arrow) => {
const showSequenceNumber = !!node.nextElementSibling?.classList.contains("sequenceNumber");
if (showSequenceNumber) {
const text = node.nextElementSibling?.textContent;
if (!text) {
throw new Error("sequence number not present");
}
const height = 30;
const yOffset = height / 2;
const xOffset = 10;
const sequenceNumber = {
type: "rectangle",
x: arrow.startX - xOffset,
y: arrow.startY - yOffset,
label: { text, fontSize: 14 },
bgColor: "#e9ecef",
height,
subtype: "sequence",
};
Object.assign(arrow, { sequenceNumber });
}
};
const createActorSymbol = (rootNode, text, opts) => {
if (!rootNode) {
throw "root node not found";
}
const groupId = nanoid();
const children = Array.from(rootNode.children);
const nodeElements = [];
children.forEach((child, index) => {
const id = `${opts?.id}-${index}`;
let ele;
switch (child.tagName) {
case "line":
const startX = Number(child.getAttribute("x1"));
const startY = Number(child.getAttribute("y1"));
const endX = Number(child.getAttribute("x2"));
const endY = Number(child.getAttribute("y2"));
ele = createLineSkeletonFromSVG(child, startX, startY, endX, endY, { groupId, id });
break;
case "text":
ele = createTextSkeletonFromSVG(child, text, {
groupId,
id,
});
break;
case "circle":
ele = createContainerSkeletonFromSVG(child, "ellipse", {
label: child.textContent ? { text: child.textContent } : undefined,
groupId,
id,
});
default:
ele = createContainerSkeletonFromSVG(child, SVG_TO_SHAPE_MAPPER[child.tagName], {
label: child.textContent ? { text: child.textContent } : undefined,
groupId,
id,
});
}
nodeElements.push(ele);
});
return nodeElements;
};
const parseActor = (actors, containerEl) => {
const actorTopNodes = Array.from(containerEl.querySelectorAll(".actor-top"));
const actorBottomNodes = Array.from(containerEl.querySelectorAll(".actor-bottom"));
const nodes = [];
const lines = [];
Object.values(actors).forEach((actor, index) => {
const topRootNode = actorTopNodes.find((actorNode) => actorNode.getAttribute("name") === actor.name);
const bottomRootNode = actorBottomNodes.find((actorNode) => actorNode.getAttribute("name") === actor.name);
if (!topRootNode || !bottomRootNode) {
throw "root not found";
}
const text = actor.description;
if (actor.type === "participant") {
// creating top actor node element
const topNodeElement = createContainerSkeletonFromSVG(topRootNode, "rectangle", { id: `${actor.name}-top`, label: { text }, subtype: "actor" });
if (!topNodeElement) {
throw "Top Node element not found!";
}
nodes.push([topNodeElement]);
// creating bottom actor node element
const bottomNodeElement = createContainerSkeletonFromSVG(bottomRootNode, "rectangle", { id: `${actor.name}-bottom`, label: { text }, subtype: "actor" });
nodes.push([bottomNodeElement]);
// Get the line connecting the top and bottom nodes. As per the DOM, the line is rendered as sibling parent of top root node
const lineNode = topRootNode?.parentElement
?.previousElementSibling;
if (lineNode?.tagName !== "line") {
throw "Line not found";
}
const startX = Number(lineNode.getAttribute("x1"));
if (!topNodeElement.height) {
throw "Top node element height is null";
}
const startY = topNodeElement.y + topNodeElement.height;
// Make sure lines don't overlap with the nodes, in mermaid it overlaps but isn't visible as its pushed back and containers are non transparent
const endY = bottomNodeElement.y;
const endX = Number(lineNode.getAttribute("x2"));
const line = createLineSkeletonFromSVG(lineNode, startX, startY, endX, endY);
lines.push(line);
}
else if (actor.type === "actor") {
const topNodeElement = createActorSymbol(topRootNode, text, {
id: `${actor.name}-top`,
});
nodes.push(topNodeElement);
const bottomNodeElement = createActorSymbol(bottomRootNode, text, {
id: `${actor.name}-bottom`,
});
nodes.push(bottomNodeElement);
// Get the line connecting the top and bottom nodes. As per the DOM, the line is rendered as sibling of the actor root element
const lineNode = topRootNode.previousElementSibling;
if (lineNode?.tagName !== "line") {
throw "Line not found";
}
const startX = Number(lineNode.getAttribute("x1"));
const startY = Number(lineNode.getAttribute("y1"));
const endX = Number(lineNode.getAttribute("x2"));
// Make sure lines don't overlap with the nodes, in mermaid it overlaps but isn't visible as its pushed back and containers are non transparent
const bottomEllipseNode = bottomNodeElement.find((node) => node.type === "ellipse");
if (bottomEllipseNode) {
const endY = bottomEllipseNode.y;
const line = createLineSkeletonFromSVG(lineNode, startX, startY, endX, endY);
lines.push(line);
}
}
});
return { nodes, lines };
};
const computeArrows = (messages, containerEl) => {
const arrows = [];
const arrowNodes = Array.from(containerEl.querySelectorAll('[class*="messageLine"]'));
const supportedMessageTypes = Object.keys(SEQUENCE_ARROW_TYPES);
const arrowMessages = messages.filter((message) => supportedMessageTypes.includes(message.type.toString()));
arrowNodes.forEach((arrowNode, index) => {
const message = arrowMessages[index];
const messageType = SEQUENCE_ARROW_TYPES[message.type];
const arrow = createArrowSkeletonFromSVG(arrowNode, {
label: message?.message,
strokeStyle: getStrokeStyle(message.type),
endArrowhead: messageType === "SOLID_OPEN" || messageType === "DOTTED_OPEN"
? null
: "arrow",
});
attachSequenceNumberToArrow(arrowNode, arrow);
arrows.push(arrow);
});
return arrows;
};
const computeNotes = (messages, containerEl) => {
const noteNodes = Array.from(containerEl.querySelectorAll(".note")).map((node) => node.parentElement);
const noteText = messages.filter((message) => message.type === MESSAGE_TYPE.NOTE);
const notes = [];
noteNodes.forEach((node, index) => {
if (!node) {
return;
}
const rect = node.firstChild;
const text = noteText[index].message;
const note = createContainerSkeletonFromSVG(rect, "rectangle", {
label: { text },
subtype: "note",
});
notes.push(note);
});
return notes;
};
const parseActivations = (containerEl) => {
const activationNodes = Array.from(containerEl.querySelectorAll(`[class*=activation]`));
const activations = [];
activationNodes.forEach((node) => {
const rect = createContainerSkeletonFromSVG(node, "rectangle", {
label: { text: "" },
subtype: "activation",
});
activations.push(rect);
});
return activations;
};
const parseLoops = (messages, containerEl) => {
const lineNodes = Array.from(containerEl.querySelectorAll(".loopLine"));
const lines = [];
const texts = [];
const nodes = [];
lineNodes.forEach((node) => {
const startX = Number(node.getAttribute("x1"));
const startY = Number(node.getAttribute("y1"));
const endX = Number(node.getAttribute("x2"));
const endY = Number(node.getAttribute("y2"));
const line = createLineSkeletonFromSVG(node, startX, startY, endX, endY);
line.strokeStyle = "dotted";
line.strokeColor = "#adb5bd";
line.strokeWidth = 2;
lines.push(line);
});
const loopTextNodes = Array.from(containerEl.querySelectorAll(".loopText"));
const criticalMessages = messages
.filter((message) => message.type === MESSAGE_TYPE.CRITICAL_START)
.map((message) => message.message);
loopTextNodes.forEach((node) => {
const text = node.textContent || "";
const textElement = createTextSkeletonFromSVG(node, text);
// The text is rendered between [ ] in DOM hence getting the text excluding the [ ]
const rawText = text.match(/\[(.*?)\]/)?.[1] || "";
const isCritical = criticalMessages.includes(rawText);
// For critical label the coordinates are not accurate in mermaid as there is
// no padding left hence shifting the text next to critical label by 16px (font size)
if (isCritical) {
textElement.x += 16;
}
texts.push(textElement);
});
const labelBoxes = Array.from(containerEl?.querySelectorAll(".labelBox"));
const labelTextNode = Array.from(containerEl?.querySelectorAll(".labelText"));
labelBoxes.forEach((labelBox, index) => {
const text = labelTextNode[index]?.textContent || "";
const container = createContainerSkeletonFromSVG(labelBox, "rectangle", {
label: { text },
});
container.strokeColor = "#adb5bd";
container.bgColor = "#e9ecef";
// So width is calculated based on label
container.width = undefined;
nodes.push(container);
});
return { lines, texts, nodes };
};
const computeHighlights = (containerEl) => {
const rects = Array.from(containerEl.querySelectorAll(".rect"))
// Only drawing specifically for highlights as the same selector is for grouping as well. For grouping we
// draw it ourselves
.filter((node) => node.parentElement?.tagName !== "g");
const nodes = [];
rects.forEach((rect) => {
const node = createContainerSkeletonFromSVG(rect, "rectangle", {
label: { text: "" },
subtype: "highlight",
});
nodes.push(node);
});
return nodes;
};
export const parseMermaidSequenceDiagram = (diagram, containerEl) => {
diagram.parse();
// Get mermaid parsed data from parser shared variable `yy`
//@ts-ignore
const mermaidParser = diagram.parser.yy;
const nodes = [];
const groups = mermaidParser.getBoxes();
const bgHightlights = computeHighlights(containerEl);
const actorData = mermaidParser.getActors();
const { nodes: actors, lines } = parseActor(actorData, containerEl);
const messages = mermaidParser.getMessages();
const arrows = computeArrows(messages, containerEl);
const notes = computeNotes(messages, containerEl);
const activations = parseActivations(containerEl);
const loops = parseLoops(messages, containerEl);
nodes.push(bgHightlights);
nodes.push(...actors);
nodes.push(notes);
nodes.push(activations);
return { type: "sequence", lines, arrows, nodes, loops, groups };
};