Merge branch 'develop' into sidv/stateDB-ts

This commit is contained in:
Sidharth Vinod
2025-02-20 16:31:59 +05:30
committed by GitHub
7 changed files with 818 additions and 869 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each sequence diagram. Added unique IDs for messages.

View File

@@ -1,4 +1,5 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
import type { DiagramDB } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { ImperativeState } from '../../utils/imperativeState.js';
import { sanitizeText } from '../common/common.js';
@@ -28,273 +29,7 @@ interface SequenceState {
lastDestroyed?: Actor;
}
const state = new ImperativeState<SequenceState>(() => ({
prevActor: undefined,
actors: new Map(),
createdActors: new Map(),
destroyedActors: new Map(),
boxes: [],
messages: [],
notes: [],
sequenceNumbersEnabled: false,
wrapEnabled: undefined,
currentBox: undefined,
lastCreated: undefined,
lastDestroyed: undefined,
}));
export const addBox = function (data: { text: string; color: string; wrap: boolean }) {
state.records.boxes.push({
name: data.text,
wrap: data.wrap ?? autoWrap(),
fill: data.color,
actorKeys: [],
});
state.records.currentBox = state.records.boxes.slice(-1)[0];
};
export const addActor = function (
id: string,
name: string,
description: { text: string; wrap?: boolean | null; type: string },
type: string
) {
let assignedBox = state.records.currentBox;
const old = state.records.actors.get(id);
if (old) {
// If already set and trying to set to a new one throw error
if (state.records.currentBox && old.box && state.records.currentBox !== old.box) {
throw new Error(
`A same participant should only be defined in one Box: ${old.name} can't be in '${old.box.name}' and in '${state.records.currentBox.name}' at the same time.`
);
}
// Don't change the box if already
assignedBox = old.box ? old.box : state.records.currentBox;
old.box = assignedBox;
// Don't allow description nulling
if (old && name === old.name && description == null) {
return;
}
}
// Don't allow null descriptions, either
if (description?.text == null) {
description = { text: name, type };
}
if (type == null || description.text == null) {
description = { text: name, type };
}
state.records.actors.set(id, {
box: assignedBox,
name: name,
description: description.text,
wrap: description.wrap ?? autoWrap(),
prevActor: state.records.prevActor,
links: {},
properties: {},
actorCnt: null,
rectData: null,
type: type ?? 'participant',
});
if (state.records.prevActor) {
const prevActorInRecords = state.records.actors.get(state.records.prevActor);
if (prevActorInRecords) {
prevActorInRecords.nextActor = id;
}
}
if (state.records.currentBox) {
state.records.currentBox.actorKeys.push(id);
}
state.records.prevActor = id;
};
const activationCount = (part: string) => {
let i;
let count = 0;
if (!part) {
return 0;
}
for (i = 0; i < state.records.messages.length; i++) {
if (
state.records.messages[i].type === LINETYPE.ACTIVE_START &&
state.records.messages[i].from === part
) {
count++;
}
if (
state.records.messages[i].type === LINETYPE.ACTIVE_END &&
state.records.messages[i].from === part
) {
count--;
}
}
return count;
};
export const addMessage = function (
idFrom: Message['from'],
idTo: Message['to'],
message: { text: string; wrap?: boolean },
answer: Message['answer']
) {
state.records.messages.push({
from: idFrom,
to: idTo,
message: message.text,
wrap: message.wrap ?? autoWrap(),
answer: answer,
});
};
export const addSignal = function (
idFrom?: Message['from'],
idTo?: Message['to'],
message?: { text: string; wrap: boolean },
messageType?: number,
activate = false
) {
if (messageType === LINETYPE.ACTIVE_END) {
const cnt = activationCount(idFrom ?? '');
if (cnt < 1) {
// Bail out as there is an activation signal from an inactive participant
const error = new Error('Trying to inactivate an inactive participant (' + idFrom + ')');
// @ts-ignore: we are passing hash param to the error object, however we should define our own custom error class to make it type safe
error.hash = {
text: '->>-',
token: '->>-',
line: '1',
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
expected: ["'ACTIVE_PARTICIPANT'"],
};
throw error;
}
}
state.records.messages.push({
from: idFrom,
to: idTo,
message: message?.text ?? '',
wrap: message?.wrap ?? autoWrap(),
type: messageType,
activate,
});
return true;
};
export const hasAtLeastOneBox = function () {
return state.records.boxes.length > 0;
};
export const hasAtLeastOneBoxWithTitle = function () {
return state.records.boxes.some((b) => b.name);
};
export const getMessages = function () {
return state.records.messages;
};
export const getBoxes = function () {
return state.records.boxes;
};
export const getActors = function () {
return state.records.actors;
};
export const getCreatedActors = function () {
return state.records.createdActors;
};
export const getDestroyedActors = function () {
return state.records.destroyedActors;
};
export const getActor = function (id: string) {
// TODO: do we ever use this function in a way that it might return undefined?
return state.records.actors.get(id)!;
};
export const getActorKeys = function () {
return [...state.records.actors.keys()];
};
export const enableSequenceNumbers = function () {
state.records.sequenceNumbersEnabled = true;
};
export const disableSequenceNumbers = function () {
state.records.sequenceNumbersEnabled = false;
};
export const showSequenceNumbers = () => state.records.sequenceNumbersEnabled;
export const setWrap = function (wrapSetting?: boolean) {
state.records.wrapEnabled = wrapSetting;
};
const extractWrap = (text?: string): { cleanedText?: string; wrap?: boolean } => {
if (text === undefined) {
return {};
}
text = text.trim();
const wrap =
/^:?wrap:/.exec(text) !== null ? true : /^:?nowrap:/.exec(text) !== null ? false : undefined;
const cleanedText = (wrap === undefined ? text : text.replace(/^:?(?:no)?wrap:/, '')).trim();
return { cleanedText, wrap };
};
export const autoWrap = () => {
// if setWrap has been called, use that value, otherwise use the value from the config
// TODO: refactor, always use the config value let setWrap update the config value
if (state.records.wrapEnabled !== undefined) {
return state.records.wrapEnabled;
}
return getConfig().sequence?.wrap ?? false;
};
export const clear = function () {
state.reset();
commonClear();
};
export const parseMessage = function (str: string) {
const trimmedStr = str.trim();
const { wrap, cleanedText } = extractWrap(trimmedStr);
const message = {
text: cleanedText,
wrap,
};
log.debug(`parseMessage: ${JSON.stringify(message)}`);
return message;
};
// We expect the box statement to be color first then description
// The color can be rgb,rgba,hsl,hsla, or css code names #hex codes are not supported for now because of the way the char # is handled
// We extract first segment as color, the rest of the line is considered as text
export const parseBoxData = function (str: string) {
const match = /^((?:rgba?|hsla?)\s*\(.*\)|\w*)(.*)$/.exec(str);
let color = match?.[1] ? match[1].trim() : 'transparent';
let title = match?.[2] ? match[2].trim() : undefined;
// check that the string is a color
if (window?.CSS) {
if (!window.CSS.supports('color', color)) {
color = 'transparent';
title = str.trim();
}
} else {
const style = new Option().style;
style.color = color;
if (style.color !== color) {
color = 'transparent';
title = str.trim();
}
}
const { wrap, cleanedText } = extractWrap(title);
return {
text: cleanedText ? sanitizeText(cleanedText, getConfig()) : undefined,
color,
wrap,
};
};
export const LINETYPE = {
const LINETYPE = {
SOLID: 0,
DOTTED: 1,
NOTE: 2,
@@ -327,48 +62,338 @@ export const LINETYPE = {
PAR_OVER_START: 32,
BIDIRECTIONAL_SOLID: 33,
BIDIRECTIONAL_DOTTED: 34,
};
} as const;
export const ARROWTYPE = {
const ARROWTYPE = {
FILLED: 0,
OPEN: 1,
};
} as const;
export const PLACEMENT = {
const PLACEMENT = {
LEFTOF: 0,
RIGHTOF: 1,
OVER: 2,
};
} as const;
export const addNote = function (
export class SequenceDB implements DiagramDB {
private readonly state = new ImperativeState<SequenceState>(() => ({
prevActor: undefined,
actors: new Map(),
createdActors: new Map(),
destroyedActors: new Map(),
boxes: [],
messages: [],
notes: [],
sequenceNumbersEnabled: false,
wrapEnabled: undefined,
currentBox: undefined,
lastCreated: undefined,
lastDestroyed: undefined,
}));
constructor() {
// Needed for JISON since it only supports direct properties
this.apply = this.apply.bind(this);
this.parseBoxData = this.parseBoxData.bind(this);
this.parseMessage = this.parseMessage.bind(this);
this.clear();
this.setWrap(getConfig().wrap);
this.LINETYPE = LINETYPE;
this.ARROWTYPE = ARROWTYPE;
this.PLACEMENT = PLACEMENT;
}
public addBox(data: { text: string; color: string; wrap: boolean }) {
this.state.records.boxes.push({
name: data.text,
wrap: data.wrap ?? this.autoWrap(),
fill: data.color,
actorKeys: [],
});
this.state.records.currentBox = this.state.records.boxes.slice(-1)[0];
}
public addActor(
id: string,
name: string,
description: { text: string; wrap?: boolean | null; type: string },
type: string
) {
let assignedBox = this.state.records.currentBox;
const old = this.state.records.actors.get(id);
if (old) {
// If already set and trying to set to a new one throw error
if (this.state.records.currentBox && old.box && this.state.records.currentBox !== old.box) {
throw new Error(
`A same participant should only be defined in one Box: ${old.name} can't be in '${old.box.name}' and in '${this.state.records.currentBox.name}' at the same time.`
);
}
// Don't change the box if already
assignedBox = old.box ? old.box : this.state.records.currentBox;
old.box = assignedBox;
// Don't allow description nulling
if (old && name === old.name && description == null) {
return;
}
}
// Don't allow null descriptions, either
if (description?.text == null) {
description = { text: name, type };
}
if (type == null || description.text == null) {
description = { text: name, type };
}
this.state.records.actors.set(id, {
box: assignedBox,
name: name,
description: description.text,
wrap: description.wrap ?? this.autoWrap(),
prevActor: this.state.records.prevActor,
links: {},
properties: {},
actorCnt: null,
rectData: null,
type: type ?? 'participant',
});
if (this.state.records.prevActor) {
const prevActorInRecords = this.state.records.actors.get(this.state.records.prevActor);
if (prevActorInRecords) {
prevActorInRecords.nextActor = id;
}
}
if (this.state.records.currentBox) {
this.state.records.currentBox.actorKeys.push(id);
}
this.state.records.prevActor = id;
}
private activationCount(part: string) {
let i;
let count = 0;
if (!part) {
return 0;
}
for (i = 0; i < this.state.records.messages.length; i++) {
if (
this.state.records.messages[i].type === this.LINETYPE.ACTIVE_START &&
this.state.records.messages[i].from === part
) {
count++;
}
if (
this.state.records.messages[i].type === this.LINETYPE.ACTIVE_END &&
this.state.records.messages[i].from === part
) {
count--;
}
}
return count;
}
public addMessage(
idFrom: Message['from'],
idTo: Message['to'],
message: { text: string; wrap?: boolean },
answer: Message['answer']
) {
this.state.records.messages.push({
id: this.state.records.messages.length.toString(),
from: idFrom,
to: idTo,
message: message.text,
wrap: message.wrap ?? this.autoWrap(),
answer: answer,
});
}
public addSignal(
idFrom?: Message['from'],
idTo?: Message['to'],
message?: { text: string; wrap: boolean },
messageType?: number,
activate = false
) {
if (messageType === this.LINETYPE.ACTIVE_END) {
const cnt = this.activationCount(idFrom ?? '');
if (cnt < 1) {
// Bail out as there is an activation signal from an inactive participant
const error = new Error('Trying to inactivate an inactive participant (' + idFrom + ')');
// @ts-ignore: we are passing hash param to the error object, however we should define our own custom error class to make it type safe
error.hash = {
text: '->>-',
token: '->>-',
line: '1',
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
expected: ["'ACTIVE_PARTICIPANT'"],
};
throw error;
}
}
this.state.records.messages.push({
id: this.state.records.messages.length.toString(),
from: idFrom,
to: idTo,
message: message?.text ?? '',
wrap: message?.wrap ?? this.autoWrap(),
type: messageType,
activate,
});
return true;
}
public hasAtLeastOneBox() {
return this.state.records.boxes.length > 0;
}
public hasAtLeastOneBoxWithTitle() {
return this.state.records.boxes.some((b) => b.name);
}
public getMessages() {
return this.state.records.messages;
}
public getBoxes() {
return this.state.records.boxes;
}
public getActors() {
return this.state.records.actors;
}
public getCreatedActors() {
return this.state.records.createdActors;
}
public getDestroyedActors() {
return this.state.records.destroyedActors;
}
public getActor(id: string) {
// TODO: do we ever use this function in a way that it might return undefined?
return this.state.records.actors.get(id)!;
}
public getActorKeys() {
return [...this.state.records.actors.keys()];
}
public enableSequenceNumbers() {
this.state.records.sequenceNumbersEnabled = true;
}
public disableSequenceNumbers() {
this.state.records.sequenceNumbersEnabled = false;
}
public showSequenceNumbers() {
return this.state.records.sequenceNumbersEnabled;
}
public setWrap(wrapSetting?: boolean) {
this.state.records.wrapEnabled = wrapSetting;
}
private extractWrap(text?: string): { cleanedText?: string; wrap?: boolean } {
if (text === undefined) {
return {};
}
text = text.trim();
const wrap =
/^:?wrap:/.exec(text) !== null ? true : /^:?nowrap:/.exec(text) !== null ? false : undefined;
const cleanedText = (wrap === undefined ? text : text.replace(/^:?(?:no)?wrap:/, '')).trim();
return { cleanedText, wrap };
}
public autoWrap() {
// if setWrap has been called, use that value, otherwise use the value from the config
// TODO: refactor, always use the config value let setWrap update the config value
if (this.state.records.wrapEnabled !== undefined) {
return this.state.records.wrapEnabled;
}
return getConfig().sequence?.wrap ?? false;
}
public clear() {
this.state.reset();
commonClear();
}
public parseMessage(str: string) {
const trimmedStr = str.trim();
const { wrap, cleanedText } = this.extractWrap(trimmedStr);
const message = {
text: cleanedText,
wrap,
};
log.debug(`parseMessage: ${JSON.stringify(message)}`);
return message;
}
// We expect the box statement to be color first then description
// The color can be rgb,rgba,hsl,hsla, or css code names #hex codes are not supported for now because of the way the char # is handled
// We extract first segment as color, the rest of the line is considered as text
public parseBoxData(str: string) {
const match = /^((?:rgba?|hsla?)\s*\(.*\)|\w*)(.*)$/.exec(str);
let color = match?.[1] ? match[1].trim() : 'transparent';
let title = match?.[2] ? match[2].trim() : undefined;
// check that the string is a color
if (window?.CSS) {
if (!window.CSS.supports('color', color)) {
color = 'transparent';
title = str.trim();
}
} else {
const style = new Option().style;
style.color = color;
if (style.color !== color) {
color = 'transparent';
title = str.trim();
}
}
const { wrap, cleanedText } = this.extractWrap(title);
return {
text: cleanedText ? sanitizeText(cleanedText, getConfig()) : undefined,
color,
wrap,
};
}
public readonly LINETYPE: typeof LINETYPE;
public readonly ARROWTYPE: typeof ARROWTYPE;
public readonly PLACEMENT: typeof PLACEMENT;
public addNote(
actor: { actor: string },
placement: Message['placement'],
message: { text: string; wrap?: boolean }
) {
) {
const note: Note = {
actor: actor,
placement: placement,
message: message.text,
wrap: message.wrap ?? autoWrap(),
wrap: message.wrap ?? this.autoWrap(),
};
//@ts-ignore: Coerce actor into a [to, from, ...] array
// eslint-disable-next-line unicorn/prefer-spread
const actors = [].concat(actor, actor);
state.records.notes.push(note);
state.records.messages.push({
this.state.records.notes.push(note);
this.state.records.messages.push({
id: this.state.records.messages.length.toString(),
from: actors[0],
to: actors[1],
message: message.text,
wrap: message.wrap ?? autoWrap(),
type: LINETYPE.NOTE,
wrap: message.wrap ?? this.autoWrap(),
type: this.LINETYPE.NOTE,
placement: placement,
});
};
}
export const addLinks = function (actorId: string, text: { text: string }) {
public addLinks(actorId: string, text: { text: string }) {
// find the actor
const actor = getActor(actorId);
const actor = this.getActor(actorId);
// JSON.parse the text
try {
let sanitizedText = sanitizeText(text.text, getConfig());
@@ -376,15 +401,15 @@ export const addLinks = function (actorId: string, text: { text: string }) {
sanitizedText = sanitizedText.replace(/&amp;/g, '&');
const links = JSON.parse(sanitizedText);
// add the deserialized text to the actor's links field.
insertLinks(actor, links);
this.insertLinks(actor, links);
} catch (e) {
log.error('error while parsing actor link text', e);
}
};
}
export const addALink = function (actorId: string, text: { text: string }) {
public addALink(actorId: string, text: { text: string }) {
// find the actor
const actor = getActor(actorId);
const actor = this.getActor(actorId);
try {
const links: Record<string, string> = {};
let sanitizedText = sanitizeText(text.text, getConfig());
@@ -396,17 +421,13 @@ export const addALink = function (actorId: string, text: { text: string }) {
links[label] = link;
// add the deserialized text to the actor's links field.
insertLinks(actor, links);
this.insertLinks(actor, links);
} catch (e) {
log.error('error while parsing actor link text', e);
}
};
}
/**
* @param actor - the actor to add the links to
* @param links - the links to add to the actor
*/
function insertLinks(actor: Actor, links: Record<string, string>) {
private insertLinks(actor: Actor, links: Record<string, string>) {
if (actor.links == null) {
actor.links = links;
} else {
@@ -414,27 +435,23 @@ function insertLinks(actor: Actor, links: Record<string, string>) {
actor.links[key] = links[key];
}
}
}
}
export const addProperties = function (actorId: string, text: { text: string }) {
public addProperties(actorId: string, text: { text: string }) {
// find the actor
const actor = getActor(actorId);
const actor = this.getActor(actorId);
// JSON.parse the text
try {
const sanitizedText = sanitizeText(text.text, getConfig());
const properties: Record<string, unknown> = JSON.parse(sanitizedText);
// add the deserialized text to the actor's property field.
insertProperties(actor, properties);
this.insertProperties(actor, properties);
} catch (e) {
log.error('error while parsing actor properties text', e);
}
};
}
/**
* @param actor - the actor to add the properties to
* @param properties - the properties to add to the actor's properties
*/
function insertProperties(actor: Actor, properties: Record<string, unknown>) {
private insertProperties(actor: Actor, properties: Record<string, unknown>) {
if (actor.properties == null) {
actor.properties = properties;
} else {
@@ -442,15 +459,15 @@ function insertProperties(actor: Actor, properties: Record<string, unknown>) {
actor.properties[key] = properties[key];
}
}
}
}
function boxEnd() {
state.records.currentBox = undefined;
}
private boxEnd() {
this.state.records.currentBox = undefined;
}
export const addDetails = function (actorId: string, text: { text: string }) {
public addDetails(actorId: string, text: { text: string }) {
// find the actor
const actor = getActor(actorId);
const actor = this.getActor(actorId);
const elem = document.getElementById(text.text)!;
// JSON.parse the text
@@ -459,35 +476,36 @@ export const addDetails = function (actorId: string, text: { text: string }) {
const details = JSON.parse(text);
// add the deserialized text to the actor's property field.
if (details.properties) {
insertProperties(actor, details.properties);
this.insertProperties(actor, details.properties);
}
if (details.links) {
insertLinks(actor, details.links);
this.insertLinks(actor, details.links);
}
} catch (e) {
log.error('error while parsing actor details text', e);
}
};
}
export const getActorProperty = function (actor: Actor, key: string) {
public getActorProperty(actor: Actor, key: string) {
if (actor?.properties !== undefined) {
return actor.properties[key];
}
return undefined;
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-redundant-type-constituents
export const apply = function (param: any | AddMessageParams | AddMessageParams[]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-redundant-type-constituents
public apply(param: any | AddMessageParams | AddMessageParams[]) {
if (Array.isArray(param)) {
param.forEach(function (item) {
apply(item);
param.forEach((item) => {
this.apply(item);
});
} else {
switch (param.type) {
case 'sequenceIndex':
state.records.messages.push({
this.state.records.messages.push({
id: this.state.records.messages.length.toString(),
from: undefined,
to: undefined,
message: {
@@ -500,169 +518,141 @@ export const apply = function (param: any | AddMessageParams | AddMessageParams[
});
break;
case 'addParticipant':
addActor(param.actor, param.actor, param.description, param.draw);
this.addActor(param.actor, param.actor, param.description, param.draw);
break;
case 'createParticipant':
if (state.records.actors.has(param.actor)) {
if (this.state.records.actors.has(param.actor)) {
throw new Error(
"It is not possible to have actors with the same id, even if one is destroyed before the next is created. Use 'AS' aliases to simulate the behavior"
);
}
state.records.lastCreated = param.actor;
addActor(param.actor, param.actor, param.description, param.draw);
state.records.createdActors.set(param.actor, state.records.messages.length);
this.state.records.lastCreated = param.actor;
this.addActor(param.actor, param.actor, param.description, param.draw);
this.state.records.createdActors.set(param.actor, this.state.records.messages.length);
break;
case 'destroyParticipant':
state.records.lastDestroyed = param.actor;
state.records.destroyedActors.set(param.actor, state.records.messages.length);
this.state.records.lastDestroyed = param.actor;
this.state.records.destroyedActors.set(param.actor, this.state.records.messages.length);
break;
case 'activeStart':
addSignal(param.actor, undefined, undefined, param.signalType);
this.addSignal(param.actor, undefined, undefined, param.signalType);
break;
case 'activeEnd':
addSignal(param.actor, undefined, undefined, param.signalType);
this.addSignal(param.actor, undefined, undefined, param.signalType);
break;
case 'addNote':
addNote(param.actor, param.placement, param.text);
this.addNote(param.actor, param.placement, param.text);
break;
case 'addLinks':
addLinks(param.actor, param.text);
this.addLinks(param.actor, param.text);
break;
case 'addALink':
addALink(param.actor, param.text);
this.addALink(param.actor, param.text);
break;
case 'addProperties':
addProperties(param.actor, param.text);
this.addProperties(param.actor, param.text);
break;
case 'addDetails':
addDetails(param.actor, param.text);
this.addDetails(param.actor, param.text);
break;
case 'addMessage':
if (state.records.lastCreated) {
if (param.to !== state.records.lastCreated) {
if (this.state.records.lastCreated) {
if (param.to !== this.state.records.lastCreated) {
throw new Error(
'The created participant ' +
state.records.lastCreated.name +
this.state.records.lastCreated.name +
' does not have an associated creating message after its declaration. Please check the sequence diagram.'
);
} else {
state.records.lastCreated = undefined;
this.state.records.lastCreated = undefined;
}
} else if (state.records.lastDestroyed) {
} else if (this.state.records.lastDestroyed) {
if (
param.to !== state.records.lastDestroyed &&
param.from !== state.records.lastDestroyed
param.to !== this.state.records.lastDestroyed &&
param.from !== this.state.records.lastDestroyed
) {
throw new Error(
'The destroyed participant ' +
state.records.lastDestroyed.name +
this.state.records.lastDestroyed.name +
' does not have an associated destroying message after its declaration. Please check the sequence diagram.'
);
} else {
state.records.lastDestroyed = undefined;
this.state.records.lastDestroyed = undefined;
}
}
addSignal(param.from, param.to, param.msg, param.signalType, param.activate);
this.addSignal(param.from, param.to, param.msg, param.signalType, param.activate);
break;
case 'boxStart':
addBox(param.boxData);
this.addBox(param.boxData);
break;
case 'boxEnd':
boxEnd();
this.boxEnd();
break;
case 'loopStart':
addSignal(undefined, undefined, param.loopText, param.signalType);
this.addSignal(undefined, undefined, param.loopText, param.signalType);
break;
case 'loopEnd':
addSignal(undefined, undefined, undefined, param.signalType);
this.addSignal(undefined, undefined, undefined, param.signalType);
break;
case 'rectStart':
addSignal(undefined, undefined, param.color, param.signalType);
this.addSignal(undefined, undefined, param.color, param.signalType);
break;
case 'rectEnd':
addSignal(undefined, undefined, undefined, param.signalType);
this.addSignal(undefined, undefined, undefined, param.signalType);
break;
case 'optStart':
addSignal(undefined, undefined, param.optText, param.signalType);
this.addSignal(undefined, undefined, param.optText, param.signalType);
break;
case 'optEnd':
addSignal(undefined, undefined, undefined, param.signalType);
this.addSignal(undefined, undefined, undefined, param.signalType);
break;
case 'altStart':
addSignal(undefined, undefined, param.altText, param.signalType);
this.addSignal(undefined, undefined, param.altText, param.signalType);
break;
case 'else':
addSignal(undefined, undefined, param.altText, param.signalType);
this.addSignal(undefined, undefined, param.altText, param.signalType);
break;
case 'altEnd':
addSignal(undefined, undefined, undefined, param.signalType);
this.addSignal(undefined, undefined, undefined, param.signalType);
break;
case 'setAccTitle':
setAccTitle(param.text);
break;
case 'parStart':
addSignal(undefined, undefined, param.parText, param.signalType);
this.addSignal(undefined, undefined, param.parText, param.signalType);
break;
case 'and':
addSignal(undefined, undefined, param.parText, param.signalType);
this.addSignal(undefined, undefined, param.parText, param.signalType);
break;
case 'parEnd':
addSignal(undefined, undefined, undefined, param.signalType);
this.addSignal(undefined, undefined, undefined, param.signalType);
break;
case 'criticalStart':
addSignal(undefined, undefined, param.criticalText, param.signalType);
this.addSignal(undefined, undefined, param.criticalText, param.signalType);
break;
case 'option':
addSignal(undefined, undefined, param.optionText, param.signalType);
this.addSignal(undefined, undefined, param.optionText, param.signalType);
break;
case 'criticalEnd':
addSignal(undefined, undefined, undefined, param.signalType);
this.addSignal(undefined, undefined, undefined, param.signalType);
break;
case 'breakStart':
addSignal(undefined, undefined, param.breakText, param.signalType);
this.addSignal(undefined, undefined, param.breakText, param.signalType);
break;
case 'breakEnd':
addSignal(undefined, undefined, undefined, param.signalType);
this.addSignal(undefined, undefined, undefined, param.signalType);
break;
}
}
};
}
export default {
addActor,
addMessage,
addSignal,
addLinks,
addDetails,
addProperties,
autoWrap,
setWrap,
enableSequenceNumbers,
disableSequenceNumbers,
showSequenceNumbers,
getMessages,
getActors,
getCreatedActors,
getDestroyedActors,
getActor,
getActorKeys,
getActorProperty,
getAccTitle,
getBoxes,
getDiagramTitle,
setDiagramTitle,
getConfig: () => getConfig().sequence,
clear,
parseMessage,
parseBoxData,
LINETYPE,
ARROWTYPE,
PLACEMENT,
addNote,
setAccTitle,
apply,
setAccDescription,
getAccDescription,
hasAtLeastOneBox,
hasAtLeastOneBoxWithTitle,
};
public setAccTitle = setAccTitle;
public setAccDescription = setAccDescription;
public setDiagramTitle = setDiagramTitle;
public getAccTitle = getAccTitle;
public getAccDescription = getAccDescription;
public getDiagramTitle = getDiagramTitle;
public getConfig() {
return getConfig().sequence;
}
}

View File

@@ -1,16 +1,26 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types
import parser from './parser/sequenceDiagram.jison';
import db from './sequenceDb.js';
import { SequenceDB } from './sequenceDb.js';
import styles from './styles.js';
import { setConfig } from '../../diagram-api/diagramAPI.js';
import renderer from './sequenceRenderer.js';
import type { MermaidConfig } from '../../config.type.js';
export const diagram: DiagramDefinition = {
parser,
db,
get db() {
return new SequenceDB();
},
renderer,
styles,
init: ({ wrap }) => {
db.setWrap(wrap);
init: (cnf: MermaidConfig) => {
if (!cnf.sequence) {
cnf.sequence = {};
}
if (cnf.wrap) {
cnf.sequence.wrap = cnf.wrap;
setConfig({ sequence: { wrap: cnf.wrap } });
}
},
};

View File

@@ -1538,7 +1538,6 @@ const calculateLoopBounds = async function (messages, actors, _maxWidthPerActor,
let current, noteModel, msgModel;
for (const msg of messages) {
msg.id = utils.random({ length: 10 });
switch (msg.type) {
case diagObj.db.LINETYPE.LOOP_START:
case diagObj.db.LINETYPE.ALT_START:

View File

@@ -20,6 +20,7 @@ export interface Actor {
}
export interface Message {
id: string;
from?: string;
to?: string;
message:

View File

@@ -67,10 +67,11 @@ vi.mock('stylis', () => {
import { compile, serialize } from 'stylis';
import { Diagram } from './Diagram.js';
import { decodeEntities, encodeEntities } from './utils.js';
import { toBase64 } from './utils/base64.js';
import { ClassDB } from './diagrams/class/classDb.js';
import { FlowDB } from './diagrams/flowchart/flowDb.js';
import { SequenceDB } from './diagrams/sequence/sequenceDb.js';
import { decodeEntities, encodeEntities } from './utils.js';
import { toBase64 } from './utils/base64.js';
import { StateDB } from './diagrams/state/stateDb.js';
/**
@@ -925,28 +926,18 @@ graph TD;A--x|text including URL space|B;`)
);
const sequenceDiagram2 = await mermaidAPI.getDiagramFromText(
`sequenceDiagram
actor A1
Alice->>+John: Hello John, how are you?
Alice->>+John: John, can you hear me?
John-->>-Alice: Hi Alice, I can hear you!
John-->>-Alice: I feel great!`
);
// Since sequenceDiagram will return same Db object each time, we can compare the db to be same.
expect(sequenceDiagram1.db).toBe(sequenceDiagram2.db);
});
});
// Sequence Diagram currently uses a singleton DB, so this test will fail
it.fails('should not modify db when rendering different sequence diagrams', async () => {
const sequenceDiagram1 = await mermaidAPI.getDiagramFromText(
`sequenceDiagram
Alice->>Bob: Hello Bob, how are you?
Bob-->>John: How about you John?`
);
const sequenceDiagram2 = await mermaidAPI.getDiagramFromText(
`sequenceDiagram
Alice->>Bob: Hello Bob, how are you?
Bob-->>John: How about you John?`
);
// Since sequenceDiagram will return new Db object each time, we can compare the db to be different.
expect(sequenceDiagram1.db).not.toBe(sequenceDiagram2.db);
assert(sequenceDiagram1.db instanceof SequenceDB);
assert(sequenceDiagram2.db instanceof SequenceDB);
expect(sequenceDiagram1.db.getActors()).not.toEqual(sequenceDiagram2.db.getActors());
});
});
});