From cc476d59d1440d919f5a32ab4a393fd93138a926 Mon Sep 17 00:00:00 2001 From: omkarht Date: Fri, 27 Jun 2025 13:42:19 +0530 Subject: [PATCH 01/89] 6637-Add new participant types to Sequence Diagrams --- .../sequence/parser/sequenceDiagram.jison | 25 + .../src/diagrams/sequence/sequenceDb.ts | 11 + .../src/diagrams/sequence/sequenceRenderer.ts | 12 +- .../mermaid/src/diagrams/sequence/styles.js | 6 + .../mermaid/src/diagrams/sequence/svgDraw.js | 615 ++++++++++++++++++ 5 files changed, 666 insertions(+), 3 deletions(-) diff --git a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison index d2e81df5f..4b5a82851 100644 --- a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison +++ b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison @@ -31,6 +31,12 @@ "box" { this.begin('LINE'); return 'box'; } "participant" { this.begin('ID'); return 'participant'; } "actor" { this.begin('ID'); return 'participant_actor'; } +"boundary" { this.begin('ID'); return 'participant_boundary'; } +"control" { this.begin('ID'); return 'participant_control'; } +"entity" { this.begin('ID'); return 'participant_entity'; } +"database" { this.begin('ID'); return 'participant_database'; } +"collections" { this.begin('ID'); return 'participant_collections'; } +"queue" { this.begin('ID'); return 'participant_queue'; } "create" return 'create'; "destroy" { this.begin('ID'); return 'destroy'; } [^\<->\->:\n,;]+?([\-]*[^\<->\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; } @@ -231,6 +237,25 @@ participant_statement | 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;} | 'participant_actor' actor 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;} | 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;} + + | 'participant_boundary' actor 'AS' restOfLine 'NEWLINE' {$2.draw='boundary'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;} + | 'participant_boundary' actor 'NEWLINE' {$2.draw='boundary'; $2.type='addParticipant'; $$=$2;} + + | 'participant_control' actor 'AS' restOfLine 'NEWLINE' {$2.draw='control'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;} + | 'participant_control' actor 'NEWLINE' {$2.draw='control'; $2.type='addParticipant'; $$=$2;} + + | 'participant_entity' actor 'AS' restOfLine 'NEWLINE' {$2.draw='entity'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;} + | 'participant_entity' actor 'NEWLINE' {$2.draw='entity'; $2.type='addParticipant'; $$=$2;} + + | 'participant_database' actor 'AS' restOfLine 'NEWLINE' {$2.draw='database'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;} + | 'participant_database' actor 'NEWLINE' {$2.draw='database'; $2.type='addParticipant'; $$=$2;} + + | 'participant_collections' actor 'AS' restOfLine 'NEWLINE' {$2.draw='collections'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;} + | 'participant_collections' actor 'NEWLINE' {$2.draw='collections'; $2.type='addParticipant'; $$=$2;} + + | 'participant_queue' actor 'AS' restOfLine 'NEWLINE' {$2.draw='queue'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;} + | 'participant_queue' actor 'NEWLINE' {$2.draw='queue'; $2.type='addParticipant'; $$=$2;} + ; note_statement diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDb.ts b/packages/mermaid/src/diagrams/sequence/sequenceDb.ts index c6b44dac0..d8421b84d 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDb.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceDb.ts @@ -75,6 +75,17 @@ const PLACEMENT = { OVER: 2, } as const; +export const PARTICIPANT_TYPE = { + ACTOR: 'actor', + BOUNDARY: 'boundary', + COLLECTIONS: 'collections', + CONTROL: 'control', + DATABASE: 'database', + ENTITY: 'entity', + PARTICIPANT: 'participant', + QUEUE: 'queue', +} as const; + export class SequenceDB implements DiagramDB { private readonly state = new ImperativeState(() => ({ prevActor: undefined, diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index cfba92b79..8b4842fc1 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -10,6 +10,7 @@ import assignWithDepth from '../../assignWithDepth.js'; import utils from '../../utils.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; import type { Diagram } from '../../Diagram.js'; +import { PARTICIPANT_TYPE } from './sequenceDb.js'; let conf = {}; @@ -724,11 +725,14 @@ function adjustCreatedDestroyedData( msgModel.startx = msgModel.startx - adjustment; } } + const actorArray = [PARTICIPANT_TYPE.ACTOR, PARTICIPANT_TYPE.CONTROL, PARTICIPANT_TYPE.ENTITY]; // if it is a create message if (createdActors.get(msg.to) == index) { const actor = actors.get(msg.to); - const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3; + const adjustment = actorArray.includes(actor.type) + ? ACTOR_TYPE_WIDTH / 2 + 3 + : actor.width / 2 + 3; receiverAdjustment(actor, adjustment); actor.starty = lineStartY - actor.height / 2; bounds.bumpVerticalPos(actor.height / 2); @@ -737,7 +741,7 @@ function adjustCreatedDestroyedData( else if (destroyedActors.get(msg.from) == index) { const actor = actors.get(msg.from); if (conf.mirrorActors) { - const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2; + const adjustment = actorArray.includes(actor.type) ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2; senderAdjustment(actor, adjustment); } actor.stopy = lineStartY - actor.height / 2; @@ -747,7 +751,9 @@ function adjustCreatedDestroyedData( else if (destroyedActors.get(msg.to) == index) { const actor = actors.get(msg.to); if (conf.mirrorActors) { - const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3; + const adjustment = actorArray.includes(actor.type) + ? ACTOR_TYPE_WIDTH / 2 + 3 + : actor.width / 2 + 3; receiverAdjustment(actor, adjustment); } actor.stopy = lineStartY - actor.height / 2; diff --git a/packages/mermaid/src/diagrams/sequence/styles.js b/packages/mermaid/src/diagrams/sequence/styles.js index 5c36b4ed1..3cee9d3dc 100644 --- a/packages/mermaid/src/diagrams/sequence/styles.js +++ b/packages/mermaid/src/diagrams/sequence/styles.js @@ -12,6 +12,11 @@ const getStyles = (options) => .actor-line { stroke: ${options.actorLineColor}; } + + .innerArc { + stroke-width: 1.5; + stroke-dasharray: none; + } .messageLine0 { stroke-width: 1.5; @@ -115,6 +120,7 @@ const getStyles = (options) => fill: ${options.actorBkg}; stroke-width: 2px; } + `; export default getStyles; diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js index c681c9491..3af3d3cf0 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js @@ -3,6 +3,8 @@ import * as svgDrawCommon from '../common/svgDrawCommon.js'; import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js'; import { sanitizeUrl } from '@braintree/sanitize-url'; import * as configApi from '../../config.js'; +import { createInnerCylinderPathD } from '../../rendering-util/rendering-elements/shapes/tiltedCylinder.js'; +import { createCylinderPathD } from '../../rendering-util/rendering-elements/shapes/cylinder.js'; export const ACTOR_TYPE_WIDTH = 18 * 2; const TOP_ACTOR_CLASS = 'actor-top'; @@ -411,6 +413,607 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) { return height; }; +/** + * Draws an actor in the diagram with the attached line + * + * @param {any} elem - The diagram we'll draw to. + * @param {any} actor - The actor to draw. + * @param {any} conf - DrawText implementation discriminator object + * @param {boolean} isFooter - If the actor is the footer one + */ +const drawActorTypeCollections = function (elem, actor, conf, isFooter) { + const actorY = isFooter ? actor.stopy : actor.starty; + const center = actor.x + actor.width / 2; + const centerY = actorY + actor.height; + + const boxplusLineGroup = elem.append('g').lower(); + var g = boxplusLineGroup; + + if (!isFooter) { + actorCnt++; + if (Object.keys(actor.links || {}).length && !conf.forceMenus) { + g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer'); + } + g.append('line') + .attr('id', 'actor' + actorCnt) + .attr('x1', center) + .attr('y1', centerY) + .attr('x2', center) + .attr('y2', 2000) + .attr('class', 'actor-line 200') + .attr('stroke-width', '0.5px') + .attr('stroke', '#999') + .attr('name', actor.name); + + g = boxplusLineGroup.append('g'); + actor.actorCnt = actorCnt; + + if (actor.links != null) { + g.attr('id', 'root-' + actorCnt); + } + } + + const rect = svgDrawCommon.getNoteRect(); + var cssclass = 'actor'; + if (actor.properties?.class) { + cssclass = actor.properties.class; + } else { + rect.fill = '#eaeaea'; + } + if (isFooter) { + cssclass += ` ${BOTTOM_ACTOR_CLASS}`; + } else { + cssclass += ` ${TOP_ACTOR_CLASS}`; + } + rect.x = actor.x; + rect.y = actorY; + rect.width = actor.width; + rect.height = actor.height; + rect.class = cssclass; + // rect.rx = 3; + // rect.ry = 3; + rect.name = actor.name; + + // 🔹 DRAW STACKED RECTANGLES + const offset = 6; + const shadowRect = { + ...rect, + x: rect.x + offset, + y: rect.y + (isFooter ? +offset : -offset), + class: 'actor', + }; + drawRect(g, shadowRect); + const rectElem = drawRect(g, rect); // draw main rectangle on top + actor.rectData = rect; + + if (actor.properties?.icon) { + const iconSrc = actor.properties.icon.trim(); + if (iconSrc.charAt(0) === '@') { + svgDrawCommon.drawEmbeddedImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc.substr(1)); + } else { + svgDrawCommon.drawImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc); + } + } + + _drawTextCandidateFunc(conf, hasKatex(actor.description))( + actor.description, + g, + rect.x, + rect.y, + rect.width, + rect.height, + { class: `actor ${ACTOR_BOX_CLASS}` }, + conf + ); + + let height = actor.height; + if (rectElem.node) { + const bounds = rectElem.node().getBBox(); + actor.height = bounds.height; + height = bounds.height; + } + + return height; +}; + +const drawActorTypeQueue = function (elem, actor, conf, isFooter) { + const actorY = isFooter ? actor.stopy : actor.starty; + const center = actor.x + actor.width / 2; + const centerY = actorY + actor.height; + + const boxplusLineGroup = elem.append('g').lower(); + let g = boxplusLineGroup; + + if (!isFooter) { + actorCnt++; + if (Object.keys(actor.links || {}).length && !conf.forceMenus) { + g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer'); + } + g.append('line') + .attr('id', 'actor' + actorCnt) + .attr('x1', center) + .attr('y1', centerY) + .attr('x2', center) + .attr('y2', 2000) + .attr('class', 'actor-line 200') + .attr('stroke-width', '0.5px') + .attr('stroke', '#999') + .attr('name', actor.name); + + g = boxplusLineGroup.append('g'); + actor.actorCnt = actorCnt; + + if (actor.links != null) { + g.attr('id', 'root-' + actorCnt); + } + } + + const rect = svgDrawCommon.getNoteRect(); + let cssclass = 'actor'; + if (actor.properties?.class) { + cssclass = actor.properties.class; + } else { + rect.fill = '#eaeaea'; + } + + if (isFooter) { + cssclass += ` ${BOTTOM_ACTOR_CLASS}`; + } else { + cssclass += ` ${TOP_ACTOR_CLASS}`; + } + + rect.x = actor.x; + rect.y = actorY; + rect.width = actor.width; + rect.height = actor.height; + rect.class = cssclass; + rect.name = actor.name; + + // Cylinder dimensions + const ry = rect.height / 2; + const rx = ry / (2.5 + rect.height / 50); + + // Cylinder base group + const cylinderGroup = g.append('g'); + const cylinderArc = g.append('g'); + + // Main cylinder body + cylinderGroup + .append('path') + .attr( + 'd', + ` + M ${rect.x},${rect.y + ry} + a ${rx},${ry} 0 0 0 0,${rect.height} + h ${rect.width - 2 * rx} + a ${rx},${ry} 0 0 0 0,-${rect.height} + Z + ` + ) + .attr('class', cssclass); + cylinderArc + .append('path') + .attr('d', createInnerCylinderPathD(rect.x - rx, rect.y + ry, rect.width, rect.height, rx, ry)) + .attr('stroke', '#666') + .attr('stroke-width', '1px') + .attr('class', cssclass); + + if (!isFooter) { + cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2) - rect.y})`); + cylinderArc.attr('transform', `translate(${rect.width / 2}, ${rect.height / 2})`); + } else { + cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2)})`); + cylinderArc.attr('transform', `translate(${rect.width / 2}, ${actorY + rect.height / 2})`); + } + + actor.rectData = rect; + + if (actor.properties?.icon) { + const iconSrc = actor.properties.icon.trim(); + const iconX = rect.x + rect.width - 20; + const iconY = rect.y + 10; + if (iconSrc.charAt(0) === '@') { + svgDrawCommon.drawEmbeddedImage(g, iconX, iconY, iconSrc.substr(1)); + } else { + svgDrawCommon.drawImage(g, iconX, iconY, iconSrc); + } + } + + _drawTextCandidateFunc(conf, hasKatex(actor.description))( + actor.description, + g, + rect.x, + rect.y, + rect.width, + rect.height, + { class: `actor ${ACTOR_BOX_CLASS}` }, + conf + ); + + let height = actor.height; + const lastPath = cylinderGroup.select('path:last-child'); + if (lastPath.node()) { + const bounds = lastPath.node().getBBox(); + actor.height = bounds.height; + height = bounds.height; + } + + return height; +}; + +const drawActorTypeControl = function (elem, actor, conf, isFooter) { + const actorY = isFooter ? actor.stopy : actor.starty; + const center = actor.x + actor.width / 2; + const centerY = actorY + 75; + + const line = elem.append('g').lower(); + + if (!isFooter) { + actorCnt++; + line + .append('line') + .attr('id', 'actor' + actorCnt) + .attr('x1', center) + .attr('y1', centerY) + .attr('x2', center) + .attr('y2', 2000) + .attr('class', 'actor-line 200') + .attr('stroke-width', '0.5px') + .attr('stroke', '#999') + .attr('name', actor.name); + + actor.actorCnt = actorCnt; + } + const actElem = elem.append('g'); + let cssClass = ACTOR_MAN_FIGURE_CLASS; + if (isFooter) { + cssClass += ` ${BOTTOM_ACTOR_CLASS}`; + } else { + cssClass += ` ${TOP_ACTOR_CLASS}`; + } + actElem.attr('class', cssClass); + actElem.attr('name', actor.name); + + const rect = svgDrawCommon.getNoteRect(); + rect.x = actor.x; + rect.y = actorY; + rect.fill = '#eaeaea'; + rect.width = actor.width; + rect.height = actor.height; + rect.class = 'actor'; + + const cx = actor.x + actor.width / 2; + const cy = actorY + 30; + const r = 18; + + actElem + .append('defs') + .append('marker') + .attr('id', 'filled-head-control') + .attr('refX', 15.5) + .attr('refY', 7) + .attr('markerWidth', 20) + .attr('markerHeight', 28) + .attr('orient', '180') + .append('path') + .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z'); + + // Draw the base circle + actElem + .append('circle') + .attr('cx', cx) + .attr('cy', cy) + .attr('r', r) + .attr('fill', '#eaeaf7') + .attr('stroke', '#666') + .attr('stroke-width', 1.2); + + // Draw looping arrow as arc path + actElem + .append('path') + .attr('d', `M ${cx},${cy - r}`) + .attr('stroke-width', 1.5) + .attr('marker-end', 'url(#filled-head-control)'); + + const bounds = actElem.node().getBBox(); + actor.height = bounds.height + conf.boxTextMargin; + + _drawTextCandidateFunc(conf, hasKatex(actor.description))( + actor.description, + actElem, + rect.x, + rect.y + (!isFooter ? 30 : 40), + rect.width, + rect.height, + { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` }, + conf + ); + + return actor.height; +}; + +const drawActorTypeEntity = function (elem, actor, conf, isFooter) { + const actorY = isFooter ? actor.stopy : actor.starty; + const center = actor.x + actor.width / 2; + const centerY = actorY + 75; + + const line = elem.append('g').lower(); + + const actElem = elem.append('g'); + let cssClass = ACTOR_MAN_FIGURE_CLASS; + if (isFooter) { + cssClass += ` ${BOTTOM_ACTOR_CLASS}`; + } else { + cssClass += ` ${TOP_ACTOR_CLASS}`; + } + actElem.attr('class', cssClass); + actElem.attr('name', actor.name); + + const rect = svgDrawCommon.getNoteRect(); + rect.x = actor.x; + rect.y = actorY; + rect.fill = '#eaeaea'; + rect.width = actor.width; + rect.height = actor.height; + rect.class = 'actor'; + + const cx = actor.x + actor.width / 2; + const cy = actorY + 25; + const r = 18; + + actElem + .append('circle') + .attr('cx', cx) + .attr('cy', cy) + .attr('r', r) + .attr('width', actor.width) + .attr('height', actor.height); + + actElem + .append('line') + .attr('x1', cx - r) + .attr('x2', cx + r) + .attr('y1', cy + r) + .attr('y2', cy + r) + .attr('stroke', '#333') + .attr('stroke-width', 2); + + const boundBox = actElem.node().getBBox(); + actor.height = boundBox.height + conf.boxTextMargin; + + if (!isFooter) { + actorCnt++; + line + .append('line') + .attr('id', 'actor' + actorCnt) + .attr('x1', center) + .attr('y1', centerY) + .attr('x2', center) + .attr('y2', 2000) + .attr('class', 'actor-line 200') + .attr('stroke-width', '0.5px') + .attr('stroke', '#999') + .attr('name', actor.name); + + actor.actorCnt = actorCnt; + } + + _drawTextCandidateFunc(conf, hasKatex(actor.description))( + actor.description, + actElem, + rect.x, + rect.y + (!isFooter ? (cy + r - actorY) / 2 : (cy - actorY) / 2 + r + 2), + rect.width, + rect.height, + { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` }, + conf + ); + + if (!isFooter) { + actElem.attr('transform', `translate(${0}, ${r / 2})`); + } else { + actElem.attr('transform', `translate(${0}, ${r / 2})`); + } + + return actor.height; +}; + +const drawActorTypeDatabase = function (elem, actor, conf, isFooter) { + const actorY = isFooter ? actor.stopy : actor.starty; + const center = actor.x + actor.width / 2; + const centerY = actorY + actor.height; + + const boxplusLineGroup = elem.append('g').lower(); + let g = boxplusLineGroup; + + if (!isFooter) { + actorCnt++; + if (Object.keys(actor.links || {}).length && !conf.forceMenus) { + g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer'); + } + g.append('line') + .attr('id', 'actor' + actorCnt) + .attr('x1', center) + .attr('y1', centerY) + .attr('x2', center) + .attr('y2', 2000) + .attr('class', 'actor-line 200') + .attr('stroke-width', '0.5px') + .attr('stroke', '#999') + .attr('name', actor.name); + + g = boxplusLineGroup.append('g'); + actor.actorCnt = actorCnt; + + if (actor.links != null) { + g.attr('id', 'root-' + actorCnt); + } + } + + const rect = svgDrawCommon.getNoteRect(); + + let cssclass = 'actor'; + if (actor.properties?.class) { + cssclass = actor.properties.class; + } else { + rect.fill = '#eaeaea'; + } + + if (isFooter) { + cssclass += ` ${BOTTOM_ACTOR_CLASS}`; + } else { + cssclass += ` ${TOP_ACTOR_CLASS}`; + } + + rect.x = actor.x; + rect.y = actorY; + rect.width = actor.width; + rect.height = actor.height; + rect.class = cssclass; + rect.name = actor.name; + + // Cylinder dimensions + rect.x = actor.x; + rect.y = actorY; + const w = rect.width; + const h = rect.height; + const rx = w / 2; + const ry = rx / (2.5 + w / 50); + + // Cylinder base group + const cylinderGroup = g.append('g'); + + const pathData = createCylinderPathD( + rect.x, + rect.y, + w / 2, + isFooter ? h - ry : h / 2, + rx / 4, + ry / 4 + ); + cylinderGroup.append('path').attr('d', pathData).attr('class', cssclass); + + if (!isFooter) { + cylinderGroup.attr('transform', `translate(${w / 4}, ${-centerY / 2 + 2.75 * ry})`); + } else { + cylinderGroup.attr('transform', `translate(${w / 4}, ${ry / 2})`); + } + + actor.rectData = rect; + + _drawTextCandidateFunc(conf, hasKatex(actor.description))( + actor.description, + g, + rect.x, + rect.y + (!isFooter ? ry * 1.75 : h / 2 + ry), + rect.width, + rect.height, + { class: `actor ${ACTOR_BOX_CLASS}` }, + conf + ); + + let height = actor.height; + const lastPath = cylinderGroup.select('path:last-child'); + if (lastPath.node()) { + const bounds = lastPath.node().getBBox(); + actor.height = bounds.height; + height = bounds.height; + } + + return height; +}; + +const drawActorTypeBoundary = function (elem, actor, conf, isFooter) { + const actorY = isFooter ? actor.stopy : actor.starty; + const center = actor.x + actor.width / 2; + const centerY = actorY + 80; + const radius = 30; + const line = elem.append('g').lower(); + + if (!isFooter) { + actorCnt++; + line + .append('line') + .attr('id', 'actor' + actorCnt) + .attr('x1', center) + .attr('y1', centerY) + .attr('x2', center) + .attr('y2', 2000) + .attr('class', 'actor-line 200') + .attr('stroke-width', '0.5px') + .attr('stroke', '#999') + .attr('name', actor.name); + + actor.actorCnt = actorCnt; + } + const actElem = elem.append('g'); + let cssClass = ACTOR_MAN_FIGURE_CLASS; + if (isFooter) { + cssClass += ` ${BOTTOM_ACTOR_CLASS}`; + } else { + cssClass += ` ${TOP_ACTOR_CLASS}`; + } + actElem.attr('class', cssClass); + actElem.attr('name', actor.name); + + const rect = svgDrawCommon.getNoteRect(); + rect.x = actor.x; + rect.y = actorY; + rect.fill = '#eaeaea'; + rect.width = actor.width; + rect.height = actor.height; + rect.class = 'actor'; + rect.rx = 3; + rect.ry = 3; + + actElem + .append('line') + .attr('id', 'actor-man-torso' + actorCnt) + .attr('x1', actor.x + actor.width / 2 - radius * 2.5) + .attr('y1', actorY + 10) + .attr('x2', actor.x + actor.width / 2 - 15) + .attr('y2', actorY + 10); + + actElem + .append('line') + .attr('id', 'actor-man-arms' + actorCnt) + .attr('x1', actor.x + actor.width / 2 - radius * 2.5) + .attr('y1', actorY + 0) // starting Y + .attr('x2', actor.x + actor.width / 2 - radius * 2.5) + .attr('y2', actorY + 20); // ending Y (26px long, adjust as needed) + + actElem + .append('circle') + .attr('cx', actor.x + actor.width / 2) + .attr('cy', actorY + 10) + .attr('r', radius) + .attr('width', actor.width); + + const bounds = actElem.node().getBBox(); + actor.height = bounds.height + conf.boxTextMargin; + + _drawTextCandidateFunc(conf, hasKatex(actor.description))( + actor.description, + actElem, + rect.x, + rect.y + (!isFooter ? radius / 2 : radius / 2), + rect.width, + rect.height, + { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` }, + conf + ); + + if (!isFooter) { + actElem.attr('transform', `translate(0,${radius / 2 + 7})`); + } else { + actElem.attr('transform', `translate(0,${radius / 2 + 7})`); + } + + // actElem.attr('transform', `translate(${rect.width / 2}, ${actorY + rect.height / 2})`); + + return actor.height; +}; + const drawActorTypeActor = function (elem, actor, conf, isFooter) { const actorY = isFooter ? actor.stopy : actor.starty; const center = actor.x + actor.width / 2; @@ -512,6 +1115,18 @@ export const drawActor = async function (elem, actor, conf, isFooter) { return await drawActorTypeActor(elem, actor, conf, isFooter); case 'participant': return await drawActorTypeParticipant(elem, actor, conf, isFooter); + case 'boundary': + return await drawActorTypeBoundary(elem, actor, conf, isFooter); + case 'control': + return await drawActorTypeControl(elem, actor, conf, isFooter); + case 'entity': + return await drawActorTypeEntity(elem, actor, conf, isFooter); + case 'database': + return await drawActorTypeDatabase(elem, actor, conf, isFooter); + case 'collections': + return await drawActorTypeCollections(elem, actor, conf, isFooter); + case 'queue': + return await drawActorTypeQueue(elem, actor, conf, isFooter); } }; From 8693be56ee78382b491387f77c6a2bed9599fcdd Mon Sep 17 00:00:00 2001 From: rajat-ht Date: Fri, 27 Jun 2025 17:26:54 +0530 Subject: [PATCH 02/89] feat: Add start pro trial button. --- .../.vitepress/components/EditorSelectionModal.vue | 11 +++++++++++ packages/mermaid/src/docs/.vitepress/config.ts | 6 ++---- packages/mermaid/src/docs/.vitepress/theme/index.ts | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue b/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue index e41a7096d..52b635392 100644 --- a/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue +++ b/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue @@ -11,6 +11,7 @@ interface EditorColumn { description: string; redirectUrl: string; highlighted?: boolean; + proTrialUrl?: string; features: Feature[]; } @@ -32,6 +33,8 @@ const editorColumns: EditorColumn[] = [ redirectUrl: 'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection&utm_campaign=mermaid_chart&redirect=%2Fapp%2Fdiagrams%2Fnew', highlighted: true, + proTrialUrl: + 'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection&utm_campaign=mermaid_chart_trial&redirect=%2Fapp%2Fuser%2Fbilling%2Fcheckout', features: [ { iconUrl: '/icons/folder.svg', featureName: 'Storage' }, { iconUrl: '/icons/terminal.svg', featureName: 'Code editor' }, @@ -121,6 +124,14 @@ onUnmounted(() => { > Start free + + Start Pro trial +
  • h(TopBar), 'doc-before': () => h(TopBar), 'layout-bottom': () => h(Tooltip), - 'home-hero-after': () => h(EditorSelectionModal), + 'layout-top': () => h(EditorSelectionModal), }); }, enhanceApp({ app, router }: EnhanceAppContext) { From 76d073b0272d5b3694cb754860717b874195ac49 Mon Sep 17 00:00:00 2001 From: omkarht Date: Fri, 27 Jun 2025 17:56:03 +0530 Subject: [PATCH 03/89] chore: adjusted database and queue shape --- .../src/diagrams/sequence/sequenceRenderer.ts | 7 +- .../mermaid/src/diagrams/sequence/svgDraw.js | 65 +++++++++---------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index 8b4842fc1..e44431928 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -725,7 +725,12 @@ function adjustCreatedDestroyedData( msgModel.startx = msgModel.startx - adjustment; } } - const actorArray = [PARTICIPANT_TYPE.ACTOR, PARTICIPANT_TYPE.CONTROL, PARTICIPANT_TYPE.ENTITY]; + const actorArray = [ + PARTICIPANT_TYPE.ACTOR, + PARTICIPANT_TYPE.CONTROL, + PARTICIPANT_TYPE.ENTITY, + PARTICIPANT_TYPE.DATABASE, + ]; // if it is a create message if (createdActors.get(msg.to) == index) { diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js index 3af3d3cf0..5304f57f5 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js @@ -3,8 +3,6 @@ import * as svgDrawCommon from '../common/svgDrawCommon.js'; import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js'; import { sanitizeUrl } from '@braintree/sanitize-url'; import * as configApi from '../../config.js'; -import { createInnerCylinderPathD } from '../../rendering-util/rendering-elements/shapes/tiltedCylinder.js'; -import { createCylinderPathD } from '../../rendering-util/rendering-elements/shapes/cylinder.js'; export const ACTOR_TYPE_WIDTH = 18 * 2; const TOP_ACTOR_CLASS = 'actor-top'; @@ -582,8 +580,7 @@ const drawActorTypeQueue = function (elem, actor, conf, isFooter) { .append('path') .attr( 'd', - ` - M ${rect.x},${rect.y + ry} + `M ${rect.x},${rect.y + ry} a ${rx},${ry} 0 0 0 0,${rect.height} h ${rect.width - 2 * rx} a ${rx},${ry} 0 0 0 0,-${rect.height} @@ -593,18 +590,17 @@ const drawActorTypeQueue = function (elem, actor, conf, isFooter) { .attr('class', cssclass); cylinderArc .append('path') - .attr('d', createInnerCylinderPathD(rect.x - rx, rect.y + ry, rect.width, rect.height, rx, ry)) + .attr( + 'd', + `M ${rect.x},${rect.y + ry} + a ${rx},${ry} 0 0 0 0,${rect.height}` + ) .attr('stroke', '#666') .attr('stroke-width', '1px') .attr('class', cssclass); - if (!isFooter) { - cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2) - rect.y})`); - cylinderArc.attr('transform', `translate(${rect.width / 2}, ${rect.height / 2})`); - } else { - cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2)})`); - cylinderArc.attr('transform', `translate(${rect.width / 2}, ${actorY + rect.height / 2})`); - } + cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2)})`); + cylinderArc.attr('transform', `translate(${rect.width - rx}, ${-rect.height / 2})`); actor.rectData = rect; @@ -821,7 +817,7 @@ const drawActorTypeEntity = function (elem, actor, conf, isFooter) { const drawActorTypeDatabase = function (elem, actor, conf, isFooter) { const actorY = isFooter ? actor.stopy : actor.starty; const center = actor.x + actor.width / 2; - const centerY = actorY + actor.height; + const centerY = actorY + actor.height + 2 * conf.boxTextMargin; const boxplusLineGroup = elem.append('g').lower(); let g = boxplusLineGroup; @@ -875,52 +871,55 @@ const drawActorTypeDatabase = function (elem, actor, conf, isFooter) { // Cylinder dimensions rect.x = actor.x; rect.y = actorY; - const w = rect.width; - const h = rect.height; + const w = rect.width / 4; + const h = rect.height * 0.8; const rx = w / 2; const ry = rx / (2.5 + w / 50); // Cylinder base group const cylinderGroup = g.append('g'); - const pathData = createCylinderPathD( - rect.x, - rect.y, - w / 2, - isFooter ? h - ry : h / 2, - rx / 4, - ry / 4 - ); - cylinderGroup.append('path').attr('d', pathData).attr('class', cssclass); + const d = ` + M ${rect.x},${rect.y + ry} + a ${rx},${ry} 0 0 0 ${w},0 + a ${rx},${ry} 0 0 0 -${w},0 + l 0,${h - 2 * ry} + a ${rx},${ry} 0 0 0 ${w},0 + l 0,-${h - 2 * ry} +`; + // Draw the main cylinder body + cylinderGroup + .append('path') + .attr('d', d) + .attr('fill', '#eaeaea') + .attr('stroke', '#000') + .attr('stroke-width', 1) + .attr('class', cssclass); if (!isFooter) { - cylinderGroup.attr('transform', `translate(${w / 4}, ${-centerY / 2 + 2.75 * ry})`); + cylinderGroup.attr('transform', `translate(${w * 1.5}, ${rect.height / 4 - 2 * ry})`); } else { - cylinderGroup.attr('transform', `translate(${w / 4}, ${ry / 2})`); + cylinderGroup.attr('transform', `translate(${w * 1.5}, ${rect.height / 4 - 2 * ry})`); } - actor.rectData = rect; - _drawTextCandidateFunc(conf, hasKatex(actor.description))( actor.description, g, rect.x, - rect.y + (!isFooter ? ry * 1.75 : h / 2 + ry), + rect.y + (!isFooter ? rect.height / 2 : h / 2 + ry), rect.width, rect.height, { class: `actor ${ACTOR_BOX_CLASS}` }, conf ); - let height = actor.height; const lastPath = cylinderGroup.select('path:last-child'); if (lastPath.node()) { const bounds = lastPath.node().getBBox(); - actor.height = bounds.height; - height = bounds.height; + actor.height = bounds.height + 2 * conf.boxTextMargin; } - return height; + return actor.height; }; const drawActorTypeBoundary = function (elem, actor, conf, isFooter) { From b61bec8fafee523cc545493023151fcd0c13a45d Mon Sep 17 00:00:00 2001 From: omkarht Date: Tue, 1 Jul 2025 12:58:04 +0530 Subject: [PATCH 04/89] added test cases for new participant types of sequence diagram --- .../rendering/sequencediagram-v2.spec.js | 644 ++++++++++++++++++ 1 file changed, 644 insertions(+) create mode 100644 cypress/integration/rendering/sequencediagram-v2.spec.js diff --git a/cypress/integration/rendering/sequencediagram-v2.spec.js b/cypress/integration/rendering/sequencediagram-v2.spec.js new file mode 100644 index 000000000..4b7fe88d5 --- /dev/null +++ b/cypress/integration/rendering/sequencediagram-v2.spec.js @@ -0,0 +1,644 @@ +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; + +const looks = ['classic']; +const participantTypes = [ + 'participant', + 'actor', + 'boundary', + 'control', + 'entity', + 'database', + 'collections', + 'queue', +]; + +const interactionTypes = [ + '->>', // Solid arrow with arrowhead + '-->>', // Dotted arrow with arrowhead + '->', // Solid arrow without arrowhead + '-->', // Dotted arrow without arrowhead + '-x', // Solid arrow with cross + '--x', // Dotted arrow with cross + '->>+', // Solid arrow with arrowhead (activate) + '-->>+', // Dotted arrow with arrowhead (activate) +]; + +const notePositions = ['left of', 'right of', 'over']; + +looks.forEach((look) => { + describe(`Sequence Diagram Tests - ${look} look`, () => { + it('should render all participant types', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.forEach((type, index) => { + diagramCode += ` ${type} ${type}${index} as ${type} ${index}\n`; + }); + // Add some basic interactions + for (let i = 0; i < participantTypes.length - 1; i++) { + diagramCode += ` ${participantTypes[i]}${i} ->> ${participantTypes[i + 1]}${i + 1}: Message ${i}\n`; + } + imgSnapshotTest(diagramCode, { look, sequence: { diagramMarginX: 50, diagramMarginY: 10 } }); + }); + + it('should render all interaction types', () => { + let diagramCode = `sequenceDiagram\n`; + // Create two participants + // Add all interaction types + diagramCode += ` participant A\n`; + diagramCode += ` participant B\n`; + interactionTypes.forEach((interaction, index) => { + diagramCode += ` A ${interaction} B: ${interaction} message ${index}\n`; + }); + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render participant creation and destruction', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.forEach((type, index) => { + diagramCode += ` ${type} A\n`; + diagramCode += ` ${type} B\n`; + diagramCode += ` create ${type} ${type}${index}\n`; + diagramCode += ` A ->> ${type}${index}: Hello ${type}\n`; + if (index % 2 === 0) { + diagramCode += ` destroy ${type}${index}\n`; + } + }); + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render notes in all positions', () => { + let diagramCode = `sequenceDiagram\n`; + diagramCode += ` participant A\n`; + diagramCode += ` participant B\n`; + notePositions.forEach((position, index) => { + diagramCode += ` Note ${position} A: Note ${position} ${index}\n`; + }); + diagramCode += ` A ->> B: Message with notes\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render parallel interactions', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.slice(0, 4).forEach((type, index) => { + diagramCode += ` ${type} ${type}${index}\n`; + }); + diagramCode += ` par Parallel actions\n`; + for (let i = 0; i < participantTypes.length - 1; i += 2) { + diagramCode += ` ${participantTypes[i]}${i} ->> ${participantTypes[i + 1]}${i + 1}: Message ${i}\n`; + if (i < participantTypes.length - 2) { + diagramCode += ` and\n`; + } + } + diagramCode += ` end\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render alternative flows', () => { + let diagramCode = `sequenceDiagram\n`; + diagramCode += ` participant A\n`; + diagramCode += ` participant B\n`; + diagramCode += ` alt Successful case\n`; + diagramCode += ` A ->> B: Request\n`; + diagramCode += ` B -->> A: Success\n`; + diagramCode += ` else Failure case\n`; + diagramCode += ` A ->> B: Request\n`; + diagramCode += ` B --x A: Failure\n`; + diagramCode += ` end\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render loops', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.slice(0, 3).forEach((type, index) => { + diagramCode += ` ${type} ${type}${index}\n`; + }); + diagramCode += ` loop For each participant\n`; + for (let i = 0; i < 3; i++) { + diagramCode += ` ${participantTypes[0]}0 ->> ${participantTypes[1]}1: Message ${i}\n`; + } + diagramCode += ` end\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render boxes around groups', () => { + let diagramCode = `sequenceDiagram\n`; + diagramCode += ` box Group 1\n`; + participantTypes.slice(0, 3).forEach((type, index) => { + diagramCode += ` ${type} ${type}${index}\n`; + }); + diagramCode += ` end\n`; + diagramCode += ` box rgb(200,220,255) Group 2\n`; + participantTypes.slice(3, 6).forEach((type, index) => { + diagramCode += ` ${type} ${type}${index}\n`; + }); + diagramCode += ` end\n`; + // Add some interactions + diagramCode += ` ${participantTypes[0]}0 ->> ${participantTypes[3]}0: Cross-group message\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render with different font settings', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.slice(0, 3).forEach((type, index) => { + diagramCode += ` ${type} ${type}${index}\n`; + }); + diagramCode += ` ${participantTypes[0]}0 ->> ${participantTypes[1]}1: Regular message\n`; + diagramCode += ` Note right of ${participantTypes[1]}1: Regular note\n`; + imgSnapshotTest(diagramCode, { + look, + sequence: { + actorFontFamily: 'courier', + actorFontSize: 14, + messageFontFamily: 'Arial', + messageFontSize: 12, + noteFontFamily: 'times', + noteFontSize: 16, + noteAlign: 'left', + }, + }); + }); + }); +}); + +// Additional tests for specific combinations +describe('Sequence Diagram Special Cases', () => { + it('should render complex sequence with all features', () => { + const diagramCode = ` + sequenceDiagram + box rgb(200,220,255) Authentication + actor User + boundary LoginUI + control AuthService + database UserDB + end + + box rgb(200,255,220) Order Processing + entity Order + queue OrderQueue + collections AuditLogs + end + + User ->> LoginUI: Enter credentials + LoginUI ->> AuthService: Validate + AuthService ->> UserDB: Query user + UserDB -->> AuthService: User data + alt Valid credentials + AuthService -->> LoginUI: Success + LoginUI -->> User: Welcome + + par Place order + User ->> Order: New order + Order ->> OrderQueue: Process + and + Order ->> AuditLogs: Record + end + + loop Until confirmed + OrderQueue ->> Order: Update status + Order -->> User: Notification + end + else Invalid credentials + AuthService --x LoginUI: Failure + LoginUI --x User: Retry + end + `; + imgSnapshotTest(diagramCode, {}); + }); + + it('should render with wrapped messages and notes', () => { + const diagramCode = ` + sequenceDiagram + participant A + participant B + + A ->> B: This is a very long message that should wrap properly in the diagram rendering + Note over A,B: This is a very long note that should also wrap properly when rendered in the diagram + + par Wrapped parallel + A ->> B: Parallel message 1
    with explicit line break + and + B ->> A: Parallel message 2
    with explicit line break + end + + loop Wrapped loop + Note right of B: This is a long note
    in a loop + A ->> B: Message in loop + end + `; + imgSnapshotTest(diagramCode, { sequence: { wrap: true } }); + }); + describe('Sequence Diagram Rendering with Different Participant Types', () => { + it('should render a sequence diagram with various participant types', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor User + participant AuthService as Authentication Service + boundary UI + control OrderController + entity Product + database MongoDB + collections Products + queue OrderQueue + User ->> UI: Login request + UI ->> AuthService: Validate credentials + AuthService -->> UI: Authentication token + UI ->> OrderController: Place order + OrderController ->> Product: Check availability + Product -->> OrderController: Available + OrderController ->> MongoDB: Save order + MongoDB -->> OrderController: Order saved + OrderController ->> OrderQueue: Process payment + OrderQueue -->> User: Order confirmation + ` + ); + }); + + it('should render participant creation and destruction with different types', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Customer + participant Frontend + boundary PaymentGateway + Customer ->> Frontend: Place order + Frontend ->> OrderProcessor: Process order + create database OrderDB + OrderProcessor ->> OrderDB: Save order + ` + ); + }); + + it('should handle complex interactions between different participant types', () => { + imgSnapshotTest( + ` + sequenceDiagram + box rgba(200,220,255,0.5) System Components + actor User + boundary WebUI + control API + entity BusinessLogic + database MainDB + end + box rgba(200,255,220,0.5) External Services + queue MessageQueue + database AuditLogs + end + + User ->> WebUI: Submit request + WebUI ->> API: Process request + API ->> BusinessLogic: Execute business rules + BusinessLogic ->> MainDB: Query data + MainDB -->> BusinessLogic: Return results + BusinessLogic ->> MessageQueue: Publish event + MessageQueue -->> AuditLogs: Store audit trail + AuditLogs -->> API: Audit complete + API -->> WebUI: Return response + WebUI -->> User: Show results + `, + { sequence: { useMaxWidth: false } } + ); + }); + + it('should render parallel processes with different participant types', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Customer + participant Frontend + boundary PaymentService + control InventoryManager + entity Order + database OrdersDB + queue NotificationQueue + + Customer ->> Frontend: Place order + Frontend ->> Order: Create order + par Parallel Processing + Order ->> PaymentService: Process payment + and + Order ->> InventoryManager: Reserve items + end + PaymentService -->> Order: Payment confirmed + InventoryManager -->> Order: Items reserved + Order ->> OrdersDB: Save finalized order + OrdersDB -->> Order: Order saved + Order ->> NotificationQueue: Send confirmation + NotificationQueue -->> Customer: Order confirmation + ` + ); + }); + + it('should render different participant types with notes and loops', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Admin + participant Dashboard + boundary AuthService + control UserManager + entity UserProfile + database UserDB + database Logs + + Admin ->> Dashboard: Open user management + loop Authentication check + Dashboard ->> AuthService: Verify admin rights + AuthService ->> Dashboard: Access granted + end + Dashboard ->> UserManager: List users + UserManager ->> UserDB: Query users + UserDB ->> UserManager: Return user data + Note right of UserDB: Encrypted data
    requires decryption + UserManager ->> UserProfile: Format profiles + UserProfile ->> UserManager: Formatted data + UserManager ->> Dashboard: Display users + Dashboard ->> Logs: Record access + Logs ->> Admin: Audit trail + ` + ); + }); + + it('should render different participant types with alternative flows', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Client + participant MobileApp + boundary CloudService + control DataProcessor + entity Transaction + database TransactionsDB + queue EventBus + + Client ->> MobileApp: Initiate transaction + MobileApp ->> CloudService: Authenticate + alt Authentication successful + CloudService -->> MobileApp: Auth token + MobileApp ->> DataProcessor: Process data + DataProcessor ->> Transaction: Create transaction + Transaction ->> TransactionsDB: Save record + TransactionsDB -->> Transaction: Confirmation + Transaction ->> EventBus: Publish event + EventBus -->> Client: Notification + else Authentication failed + CloudService -->> MobileApp: Error + MobileApp -->> Client: Show error + end + ` + ); + }); + + it('should render different participant types with wrapping text', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor LongNameUser as User With A Very
    Long Name + participant FE as Frontend Service
    With Long Name + boundary B as Boundary With
    Multiline Name + control C as Control With
    Multiline Name + entity E as Entity With
    Multiline Name + database DB as Database With
    Multiline Name + collections COL as Collections With
    Multiline Name + queue Q as Queue With
    Multiline Name + + LongNameUser ->> FE: This is a very long message that should wrap properly in the diagram + FE ->> B: Another long message
    with explicit
    line breaks + B -->> FE: Response message that is also quite long and needs to wrap + FE ->> C: Process data + C ->> E: Validate + E -->> C: Validation result + C ->> DB: Save + DB -->> C: Save result + C ->> COL: Log + COL -->> Q: Forward + Q -->> LongNameUser: Final response with confirmation of all actions taken + `, + { sequence: { wrap: true } } + ); + }); + describe('Sequence Diagram - New Participant Types with Long Notes and Messages', () => { + it('should render long notes left of boundary', () => { + imgSnapshotTest( + ` + sequenceDiagram + boundary Alice + actor Bob + Alice->>Bob: Hola + Note left of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render wrapped long notes left of control', () => { + imgSnapshotTest( + ` + sequenceDiagram + control Alice + actor Bob + Alice->>Bob: Hola + Note left of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render long notes right of entity', () => { + imgSnapshotTest( + ` + sequenceDiagram + entity Alice + actor Bob + Alice->>Bob: Hola + Note right of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render wrapped long notes right of database', () => { + imgSnapshotTest( + ` + sequenceDiagram + database Alice + actor Bob + Alice->>Bob: Hola + Note right of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render long notes over collections', () => { + imgSnapshotTest( + ` + sequenceDiagram + collections Alice + actor Bob + Alice->>Bob: Hola + Note over Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render wrapped long notes over queue', () => { + imgSnapshotTest( + ` + sequenceDiagram + queue Alice + actor Bob + Alice->>Bob: Hola + Note over Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render notes over actor and boundary', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Alice + boundary Charlie + note over Alice: Some note + note over Charlie: Other note + `, + {} + ); + }); + + it('should render long messages from database to collections', () => { + imgSnapshotTest( + ` + sequenceDiagram + database Alice + collections Bob + Alice->>Bob: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render wrapped long messages from control to entity', () => { + imgSnapshotTest( + ` + sequenceDiagram + control Alice + entity Bob + Alice->>Bob:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render long messages from queue to boundary', () => { + imgSnapshotTest( + ` + sequenceDiagram + queue Alice + boundary Bob + Alice->>Bob: I'm short + Bob->>Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + `, + {} + ); + }); + + it('should render wrapped long messages from actor to database', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Alice + database Bob + Alice->>Bob: I'm short + Bob->>Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + `, + {} + ); + }); + }); + }); + + describe('svg size', () => { + it('should render a sequence diagram when useMaxWidth is true (default)', () => { + renderGraph( + ` + sequenceDiagram + actor Alice + boundary Bob + control John as John
    Second Line + Alice ->> Bob: Hello Bob, how are you? + Bob-->>John: How about you John? + Bob--x Alice: I am good thanks! + Bob-x John: I am good thanks! + Note right of John: Bob thinks a long
    long time, so long
    that the text does
    not fit on a row. + Bob-->Alice: Checking with John... + alt either this + Alice->>John: Yes + else or this + Alice->>John: No + else or this will happen + Alice->John: Maybe + end + par this happens in parallel + Alice -->> Bob: Parallel message 1 + and + Alice -->> John: Parallel message 2 + end + `, + { sequence: { useMaxWidth: true } } + ); + cy.get('svg').should((svg) => { + expect(svg).to.have.attr('width', '100%'); + const style = svg.attr('style'); + expect(style).to.match(/^max-width: [\d.]+px;$/); + const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join('')); + expect(maxWidthValue).to.be.within(820 * 0.95, 820 * 1.05); + }); + }); + + it('should render a sequence diagram when useMaxWidth is false', () => { + renderGraph( + ` + sequenceDiagram + actor Alice + boundary Bob + control John as John
    Second Line + Alice ->> Bob: Hello Bob, how are you? + Bob-->>John: How about you John? + Bob--x Alice: I am good thanks! + Bob-x John: I am good thanks! + Note right of John: Bob thinks a long
    long time, so long
    that the text does
    not fit on a row. + Bob-->Alice: Checking with John... + alt either this + Alice->>John: Yes + else or this + Alice->>John: No + else or this will happen + Alice->John: Maybe + end + par this happens in parallel + Alice -->> Bob: Parallel message 1 + and + Alice -->> John: Parallel message 2 + end + `, + { sequence: { useMaxWidth: false } } + ); + cy.get('svg').should((svg) => { + const width = parseFloat(svg.attr('width')); + expect(width).to.be.within(820 * 0.95, 820 * 1.05); + expect(svg).to.not.have.attr('style'); + }); + }); + }); +}); From a4a27611dd94ecbb6ebb85ee49fd12c83984596e Mon Sep 17 00:00:00 2001 From: omkarht Date: Tue, 1 Jul 2025 18:11:41 +0530 Subject: [PATCH 05/89] fix: minor refinement --- .../src/diagrams/sequence/sequenceRenderer.ts | 12 ++++++++---- packages/mermaid/src/diagrams/sequence/svgDraw.js | 12 ++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index e44431928..d390ebada 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -1076,10 +1076,11 @@ export const draw = async function (_text: string, id: string, _version: string, for (const box of bounds.models.boxes) { box.height = bounds.getVerticalPos() - box.y; bounds.insert(box.x, box.y, box.x + box.width, box.height); - box.startx = box.x; - box.starty = box.y; - box.stopx = box.startx + box.width; - box.stopy = box.starty + box.height; + const boxPadding = conf.boxMargin * 2; + box.startx = box.x - boxPadding; + box.starty = box.y - boxPadding * 0.25; + box.stopx = box.startx + box.width + 2 * boxPadding; + box.stopy = box.starty + box.height + boxPadding * 0.75; box.stroke = 'rgb(0,0,0, 0.5)'; svgDraw.drawBox(diagram, box, conf); } @@ -1344,6 +1345,9 @@ async function calculateActorMargins( return (total += actors.get(aKey).width + (actors.get(aKey).margin || 0)); }, 0); + const standardBoxPadding = conf.boxMargin * 8; + totalWidth += standardBoxPadding; + totalWidth -= 2 * conf.boxTextMargin; if (box.wrap) { box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont); diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js index 5304f57f5..6b8fa0208 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js @@ -712,13 +712,13 @@ const drawActorTypeControl = function (elem, actor, conf, isFooter) { .attr('marker-end', 'url(#filled-head-control)'); const bounds = actElem.node().getBBox(); - actor.height = bounds.height + conf.boxTextMargin; + actor.height = bounds.height + 2 * (conf?.sequence?.labelBoxHeight ?? 0); _drawTextCandidateFunc(conf, hasKatex(actor.description))( actor.description, actElem, rect.x, - rect.y + (!isFooter ? 30 : 40), + rect.y + 30, rect.width, rect.height, { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` }, @@ -774,8 +774,8 @@ const drawActorTypeEntity = function (elem, actor, conf, isFooter) { .attr('stroke', '#333') .attr('stroke-width', 2); - const boundBox = actElem.node().getBBox(); - actor.height = boundBox.height + conf.boxTextMargin; + const bounds = actElem.node().getBBox(); + actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0); if (!isFooter) { actorCnt++; @@ -916,7 +916,7 @@ const drawActorTypeDatabase = function (elem, actor, conf, isFooter) { const lastPath = cylinderGroup.select('path:last-child'); if (lastPath.node()) { const bounds = lastPath.node().getBBox(); - actor.height = bounds.height + 2 * conf.boxTextMargin; + actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0); } return actor.height; @@ -989,7 +989,7 @@ const drawActorTypeBoundary = function (elem, actor, conf, isFooter) { .attr('width', actor.width); const bounds = actElem.node().getBBox(); - actor.height = bounds.height + conf.boxTextMargin; + actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0); _drawTextCandidateFunc(conf, hasKatex(actor.description))( actor.description, From 012530e98e9b8b80962ab270b6bb3b6d9f6ada05 Mon Sep 17 00:00:00 2001 From: omkarht Date: Tue, 1 Jul 2025 19:02:57 +0530 Subject: [PATCH 06/89] added changeset --- .changeset/hungry-baths-glow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hungry-baths-glow.md diff --git a/.changeset/hungry-baths-glow.md b/.changeset/hungry-baths-glow.md new file mode 100644 index 000000000..b3084bcab --- /dev/null +++ b/.changeset/hungry-baths-glow.md @@ -0,0 +1,5 @@ +--- +'mermaid': minor +--- + +feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`. From 4a86b68e01d1ba4acaa9c755beed6f356f076c2e Mon Sep 17 00:00:00 2001 From: rajat-ht Date: Wed, 2 Jul 2025 18:51:06 +0530 Subject: [PATCH 07/89] chore: update UI as per suggestion --- .../components/EditorSelectionModal.vue | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue b/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue index 52b635392..c78b10de1 100644 --- a/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue +++ b/packages/mermaid/src/docs/.vitepress/components/EditorSelectionModal.vue @@ -10,6 +10,7 @@ interface EditorColumn { title: string; description: string; redirectUrl: string; + buttonText: string; highlighted?: boolean; proTrialUrl?: string; features: Feature[]; @@ -21,6 +22,7 @@ const editorColumns: EditorColumn[] = [ description: 'Basic features, no login', redirectUrl: 'https://www.mermaidchart.com/play?utm_source=mermaid_js&utm_medium=editor_selection&utm_campaign=playground', + buttonText: 'Start free', features: [ { iconUrl: '/icons/public.svg', featureName: 'Diagram stored in URL' }, { iconUrl: '/icons/terminal.svg', featureName: 'Code editor' }, @@ -28,10 +30,11 @@ const editorColumns: EditorColumn[] = [ ], }, { - title: 'Free', - description: 'Advanced features, free account', + title: 'Free or Pro', + description: 'Advanced features, Free or Pro account', redirectUrl: 'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection&utm_campaign=mermaid_chart&redirect=%2Fapp%2Fdiagrams%2Fnew', + buttonText: 'Start Free', highlighted: true, proTrialUrl: 'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection&utm_campaign=mermaid_chart_trial&redirect=%2Fapp%2Fuser%2Fbilling%2Fcheckout', @@ -53,6 +56,7 @@ const editorColumns: EditorColumn[] = [ title: 'Open Source', description: 'Code only, no login', redirectUrl: 'https://mermaid.live/edit', + buttonText: 'Start free', features: [ { iconUrl: '/icons/public.svg', featureName: 'Diagram stored in URL' }, { iconUrl: '/icons/terminal.svg', featureName: 'Code editor' }, @@ -87,15 +91,17 @@ onUnmounted(() => {