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 }; };