mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-15 22:39:26 +02:00
Merge branch 'develop' into sidv/stateDB-ts
This commit is contained in:
5
.changeset/silver-olives-marry.md
Normal file
5
.changeset/silver-olives-marry.md
Normal 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.
|
@@ -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(/&/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;
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -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 } });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@@ -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:
|
||||
|
@@ -20,6 +20,7 @@ export interface Actor {
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
message:
|
||||
|
@@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user