diff --git a/.changeset/silver-olives-marry.md b/.changeset/silver-olives-marry.md new file mode 100644 index 000000000..d709b17ba --- /dev/null +++ b/.changeset/silver-olives-marry.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each sequence diagram. Added unique IDs for messages. diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDb.ts b/packages/mermaid/src/diagrams/sequence/sequenceDb.ts index a353fab98..c6b44dac0 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDb.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceDb.ts @@ -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(() => ({ - 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,342 +62,597 @@ 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 ( - 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(), - }; +export class SequenceDB implements DiagramDB { + private readonly state = new ImperativeState(() => ({ + prevActor: undefined, + actors: new Map(), + createdActors: new Map(), + destroyedActors: new Map(), + boxes: [], + messages: [], + notes: [], + sequenceNumbersEnabled: false, + wrapEnabled: undefined, + currentBox: undefined, + lastCreated: undefined, + lastDestroyed: undefined, + })); - //@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({ - from: actors[0], - to: actors[1], - message: message.text, - wrap: message.wrap ?? autoWrap(), - type: LINETYPE.NOTE, - placement: placement, - }); -}; + 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); -export const addLinks = function (actorId: string, text: { text: string }) { - // find the actor - const actor = getActor(actorId); - // JSON.parse the text - try { - let sanitizedText = sanitizeText(text.text, getConfig()); - sanitizedText = sanitizedText.replace(/=/g, '='); - sanitizedText = sanitizedText.replace(/&/g, '&'); - const links = JSON.parse(sanitizedText); - // add the deserialized text to the actor's links field. - insertLinks(actor, links); - } catch (e) { - log.error('error while parsing actor link text', e); - } -}; + this.clear(); -export const addALink = function (actorId: string, text: { text: string }) { - // find the actor - const actor = getActor(actorId); - try { - const links: Record = {}; - let sanitizedText = sanitizeText(text.text, getConfig()); - const sep = sanitizedText.indexOf('@'); - sanitizedText = sanitizedText.replace(/=/g, '='); - sanitizedText = sanitizedText.replace(/&/g, '&'); - const label = sanitizedText.slice(0, sep - 1).trim(); - const link = sanitizedText.slice(sep + 1).trim(); - - links[label] = link; - // add the deserialized text to the actor's links field. - 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) { - if (actor.links == null) { - actor.links = links; - } else { - for (const key in links) { - actor.links[key] = links[key]; - } - } -} - -export const addProperties = function (actorId: string, text: { text: string }) { - // find the actor - const actor = getActor(actorId); - // JSON.parse the text - try { - const sanitizedText = sanitizeText(text.text, getConfig()); - const properties: Record = JSON.parse(sanitizedText); - // add the deserialized text to the actor's property field. - 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) { - if (actor.properties == null) { - actor.properties = properties; - } else { - for (const key in properties) { - actor.properties[key] = properties[key]; - } - } -} - -function boxEnd() { - state.records.currentBox = undefined; -} - -export const addDetails = function (actorId: string, text: { text: string }) { - // find the actor - const actor = getActor(actorId); - const elem = document.getElementById(text.text)!; - - // JSON.parse the text - try { - const text = elem.innerHTML; - const details = JSON.parse(text); - // add the deserialized text to the actor's property field. - if (details.properties) { - insertProperties(actor, details.properties); - } - - if (details.links) { - insertLinks(actor, details.links); - } - } catch (e) { - log.error('error while parsing actor details text', e); - } -}; - -export const getActorProperty = function (actor: Actor, key: string) { - if (actor?.properties !== undefined) { - return actor.properties[key]; + this.setWrap(getConfig().wrap); + this.LINETYPE = LINETYPE; + this.ARROWTYPE = ARROWTYPE; + this.PLACEMENT = PLACEMENT; } - 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[]) { - if (Array.isArray(param)) { - param.forEach(function (item) { - apply(item); + 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: [], }); - } else { - switch (param.type) { - case 'sequenceIndex': - state.records.messages.push({ - from: undefined, - to: undefined, - message: { - start: param.sequenceIndex, - step: param.sequenceIndexStep, - visible: param.sequenceVisible, - }, - wrap: false, - type: param.signalType, - }); - break; - case 'addParticipant': - addActor(param.actor, param.actor, param.description, param.draw); - break; - case 'createParticipant': - if (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); - break; - case 'destroyParticipant': - state.records.lastDestroyed = param.actor; - state.records.destroyedActors.set(param.actor, state.records.messages.length); - break; - case 'activeStart': - addSignal(param.actor, undefined, undefined, param.signalType); - break; - case 'activeEnd': - addSignal(param.actor, undefined, undefined, param.signalType); - break; - case 'addNote': - addNote(param.actor, param.placement, param.text); - break; - case 'addLinks': - addLinks(param.actor, param.text); - break; - case 'addALink': - addALink(param.actor, param.text); - break; - case 'addProperties': - addProperties(param.actor, param.text); - break; - case 'addDetails': - addDetails(param.actor, param.text); - break; - case 'addMessage': - if (state.records.lastCreated) { - if (param.to !== state.records.lastCreated) { - throw new Error( - 'The created participant ' + - state.records.lastCreated.name + - ' does not have an associated creating message after its declaration. Please check the sequence diagram.' - ); - } else { - state.records.lastCreated = undefined; - } - } else if (state.records.lastDestroyed) { - if ( - param.to !== state.records.lastDestroyed && - param.from !== state.records.lastDestroyed - ) { - throw new Error( - 'The destroyed participant ' + - state.records.lastDestroyed.name + - ' does not have an associated destroying message after its declaration. Please check the sequence diagram.' - ); - } else { - state.records.lastDestroyed = undefined; - } - } - addSignal(param.from, param.to, param.msg, param.signalType, param.activate); - break; - case 'boxStart': - addBox(param.boxData); - break; - case 'boxEnd': - boxEnd(); - break; - case 'loopStart': - addSignal(undefined, undefined, param.loopText, param.signalType); - break; - case 'loopEnd': - addSignal(undefined, undefined, undefined, param.signalType); - break; - case 'rectStart': - addSignal(undefined, undefined, param.color, param.signalType); - break; - case 'rectEnd': - addSignal(undefined, undefined, undefined, param.signalType); - break; - case 'optStart': - addSignal(undefined, undefined, param.optText, param.signalType); - break; - case 'optEnd': - addSignal(undefined, undefined, undefined, param.signalType); - break; - case 'altStart': - addSignal(undefined, undefined, param.altText, param.signalType); - break; - case 'else': - addSignal(undefined, undefined, param.altText, param.signalType); - break; - case 'altEnd': - addSignal(undefined, undefined, undefined, param.signalType); - break; - case 'setAccTitle': - setAccTitle(param.text); - break; - case 'parStart': - addSignal(undefined, undefined, param.parText, param.signalType); - break; - case 'and': - addSignal(undefined, undefined, param.parText, param.signalType); - break; - case 'parEnd': - addSignal(undefined, undefined, undefined, param.signalType); - break; - case 'criticalStart': - addSignal(undefined, undefined, param.criticalText, param.signalType); - break; - case 'option': - addSignal(undefined, undefined, param.optionText, param.signalType); - break; - case 'criticalEnd': - addSignal(undefined, undefined, undefined, param.signalType); - break; - case 'breakStart': - addSignal(undefined, undefined, param.breakText, param.signalType); - break; - case 'breakEnd': - addSignal(undefined, undefined, undefined, param.signalType); - break; + 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 ?? this.autoWrap(), + }; + + //@ts-ignore: Coerce actor into a [to, from, ...] array + // eslint-disable-next-line unicorn/prefer-spread + const actors = [].concat(actor, actor); + 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 ?? this.autoWrap(), + type: this.LINETYPE.NOTE, + placement: placement, + }); + } + + public addLinks(actorId: string, text: { text: string }) { + // find the actor + const actor = this.getActor(actorId); + // JSON.parse the text + try { + let sanitizedText = sanitizeText(text.text, getConfig()); + sanitizedText = sanitizedText.replace(/=/g, '='); + sanitizedText = sanitizedText.replace(/&/g, '&'); + const links = JSON.parse(sanitizedText); + // add the deserialized text to the actor's links field. + this.insertLinks(actor, links); + } catch (e) { + log.error('error while parsing actor link text', e); } } -}; -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 addALink(actorId: string, text: { text: string }) { + // find the actor + const actor = this.getActor(actorId); + try { + const links: Record = {}; + let sanitizedText = sanitizeText(text.text, getConfig()); + const sep = sanitizedText.indexOf('@'); + sanitizedText = sanitizedText.replace(/=/g, '='); + sanitizedText = sanitizedText.replace(/&/g, '&'); + const label = sanitizedText.slice(0, sep - 1).trim(); + const link = sanitizedText.slice(sep + 1).trim(); + + links[label] = link; + // add the deserialized text to the actor's links field. + this.insertLinks(actor, links); + } catch (e) { + log.error('error while parsing actor link text', e); + } + } + + private insertLinks(actor: Actor, links: Record) { + if (actor.links == null) { + actor.links = links; + } else { + for (const key in links) { + actor.links[key] = links[key]; + } + } + } + + public addProperties(actorId: string, text: { text: string }) { + // find the actor + const actor = this.getActor(actorId); + // JSON.parse the text + try { + const sanitizedText = sanitizeText(text.text, getConfig()); + const properties: Record = JSON.parse(sanitizedText); + // add the deserialized text to the actor's property field. + this.insertProperties(actor, properties); + } catch (e) { + log.error('error while parsing actor properties text', e); + } + } + + private insertProperties(actor: Actor, properties: Record) { + if (actor.properties == null) { + actor.properties = properties; + } else { + for (const key in properties) { + actor.properties[key] = properties[key]; + } + } + } + + private boxEnd() { + this.state.records.currentBox = undefined; + } + + public addDetails(actorId: string, text: { text: string }) { + // find the actor + const actor = this.getActor(actorId); + const elem = document.getElementById(text.text)!; + + // JSON.parse the text + try { + const text = elem.innerHTML; + const details = JSON.parse(text); + // add the deserialized text to the actor's property field. + if (details.properties) { + this.insertProperties(actor, details.properties); + } + + if (details.links) { + this.insertLinks(actor, details.links); + } + } catch (e) { + log.error('error while parsing actor details text', e); + } + } + + 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 + public apply(param: any | AddMessageParams | AddMessageParams[]) { + if (Array.isArray(param)) { + param.forEach((item) => { + this.apply(item); + }); + } else { + switch (param.type) { + case 'sequenceIndex': + this.state.records.messages.push({ + id: this.state.records.messages.length.toString(), + from: undefined, + to: undefined, + message: { + start: param.sequenceIndex, + step: param.sequenceIndexStep, + visible: param.sequenceVisible, + }, + wrap: false, + type: param.signalType, + }); + break; + case 'addParticipant': + this.addActor(param.actor, param.actor, param.description, param.draw); + break; + case 'createParticipant': + 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" + ); + } + 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': + this.state.records.lastDestroyed = param.actor; + this.state.records.destroyedActors.set(param.actor, this.state.records.messages.length); + break; + case 'activeStart': + this.addSignal(param.actor, undefined, undefined, param.signalType); + break; + case 'activeEnd': + this.addSignal(param.actor, undefined, undefined, param.signalType); + break; + case 'addNote': + this.addNote(param.actor, param.placement, param.text); + break; + case 'addLinks': + this.addLinks(param.actor, param.text); + break; + case 'addALink': + this.addALink(param.actor, param.text); + break; + case 'addProperties': + this.addProperties(param.actor, param.text); + break; + case 'addDetails': + this.addDetails(param.actor, param.text); + break; + case 'addMessage': + if (this.state.records.lastCreated) { + if (param.to !== this.state.records.lastCreated) { + throw new Error( + 'The created participant ' + + this.state.records.lastCreated.name + + ' does not have an associated creating message after its declaration. Please check the sequence diagram.' + ); + } else { + this.state.records.lastCreated = undefined; + } + } else if (this.state.records.lastDestroyed) { + if ( + param.to !== this.state.records.lastDestroyed && + param.from !== this.state.records.lastDestroyed + ) { + throw new Error( + 'The destroyed participant ' + + this.state.records.lastDestroyed.name + + ' does not have an associated destroying message after its declaration. Please check the sequence diagram.' + ); + } else { + this.state.records.lastDestroyed = undefined; + } + } + this.addSignal(param.from, param.to, param.msg, param.signalType, param.activate); + break; + case 'boxStart': + this.addBox(param.boxData); + break; + case 'boxEnd': + this.boxEnd(); + break; + case 'loopStart': + this.addSignal(undefined, undefined, param.loopText, param.signalType); + break; + case 'loopEnd': + this.addSignal(undefined, undefined, undefined, param.signalType); + break; + case 'rectStart': + this.addSignal(undefined, undefined, param.color, param.signalType); + break; + case 'rectEnd': + this.addSignal(undefined, undefined, undefined, param.signalType); + break; + case 'optStart': + this.addSignal(undefined, undefined, param.optText, param.signalType); + break; + case 'optEnd': + this.addSignal(undefined, undefined, undefined, param.signalType); + break; + case 'altStart': + this.addSignal(undefined, undefined, param.altText, param.signalType); + break; + case 'else': + this.addSignal(undefined, undefined, param.altText, param.signalType); + break; + case 'altEnd': + this.addSignal(undefined, undefined, undefined, param.signalType); + break; + case 'setAccTitle': + setAccTitle(param.text); + break; + case 'parStart': + this.addSignal(undefined, undefined, param.parText, param.signalType); + break; + case 'and': + this.addSignal(undefined, undefined, param.parText, param.signalType); + break; + case 'parEnd': + this.addSignal(undefined, undefined, undefined, param.signalType); + break; + case 'criticalStart': + this.addSignal(undefined, undefined, param.criticalText, param.signalType); + break; + case 'option': + this.addSignal(undefined, undefined, param.optionText, param.signalType); + break; + case 'criticalEnd': + this.addSignal(undefined, undefined, undefined, param.signalType); + break; + case 'breakStart': + this.addSignal(undefined, undefined, param.breakText, param.signalType); + break; + case 'breakEnd': + this.addSignal(undefined, undefined, undefined, param.signalType); + break; + } + } + } + + public setAccTitle = setAccTitle; + public setAccDescription = setAccDescription; + public setDiagramTitle = setDiagramTitle; + public getAccTitle = getAccTitle; + public getAccDescription = getAccDescription; + public getDiagramTitle = getDiagramTitle; + public getConfig() { + return getConfig().sequence; + } +} diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js index fde813cef..1fb35bce6 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js +++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js @@ -3,6 +3,7 @@ import { setSiteConfig } from '../../diagram-api/diagramAPI.js'; import mermaidAPI from '../../mermaidAPI.js'; import { Diagram } from '../../Diagram.js'; import { addDiagrams } from '../../diagram-api/diagram-orchestration.js'; +import { SequenceDB } from './sequenceDb.js'; beforeAll(async () => { // Is required to load the sequence diagram @@ -92,19 +93,19 @@ function addConf(conf, key, value) { } // const parser = sequence.parser; -let diagram; describe('more than one sequence diagram', () => { it('should not have duplicated messages', async () => { - const diagram1 = await Diagram.fromText(` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob:Hello Bob, how are you? Bob-->Alice: I am good thanks!`); - expect(diagram1.db.getMessages()).toMatchInlineSnapshot(` + expect(diagram.db.getMessages()).toMatchInlineSnapshot(` [ { "activate": false, "from": "Alice", + "id": "0", "message": "Hello Bob, how are you?", "to": "Bob", "type": 5, @@ -113,6 +114,7 @@ describe('more than one sequence diagram', () => { { "activate": false, "from": "Bob", + "id": "1", "message": "I am good thanks!", "to": "Alice", "type": 6, @@ -130,6 +132,7 @@ describe('more than one sequence diagram', () => { { "activate": false, "from": "Alice", + "id": "0", "message": "Hello Bob, how are you?", "to": "Bob", "type": 5, @@ -138,6 +141,7 @@ describe('more than one sequence diagram', () => { { "activate": false, "from": "Bob", + "id": "1", "message": "I am good thanks!", "to": "Alice", "type": 6, @@ -157,6 +161,7 @@ describe('more than one sequence diagram', () => { { "activate": false, "from": "Alice", + "id": "0", "message": "Hello John, how are you?", "to": "John", "type": 5, @@ -165,6 +170,7 @@ describe('more than one sequence diagram', () => { { "activate": false, "from": "John", + "id": "1", "message": "I am good thanks!", "to": "Alice", "type": 6, @@ -176,6 +182,7 @@ describe('more than one sequence diagram', () => { }); describe('when parsing a sequenceDiagram', function () { + let diagram; beforeEach(async function () { diagram = await Diagram.fromText(` sequenceDiagram @@ -183,14 +190,7 @@ Alice->Bob:Hello Bob, how are you? Note right of Bob: Bob thinks Bob-->Alice: I am good thanks!`); }); - it('should handle a sequenceDiagram definition', async function () { - const str = ` -sequenceDiagram -Alice->Bob:Hello Bob, how are you? -Note right of Bob: Bob thinks -Bob-->Alice: I am good thanks!`; - - await mermaidAPI.parse(str); + it('should handle a sequenceDiagram definition', function () { const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); actors.get('Bob').description = 'Bob'; @@ -208,7 +208,6 @@ Alice->Bob:Hello Bob, how are you? Note right of Bob: Bob thinks Bob-->Alice: I am good thanks!`; - await mermaidAPI.parse(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); // needs to be rendered for the correct value of visibility auto numbers expect(diagram.db.showSequenceNumbers()).toBe(false); }); @@ -220,20 +219,20 @@ Alice->Bob:Hello Bob, how are you? Note right of Bob: Bob thinks Bob-->Alice: I am good thanks!`; - await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); // needs to be rendered for the correct value of visibility auto numbers expect(diagram.db.showSequenceNumbers()).toBe(true); }); it('should handle a sequenceDiagram definition with a title:', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram title: Diagram Title Alice->Bob:Hello Bob, how are you? Note right of Bob: Bob thinks -Bob-->Alice: I am good thanks!`; +Bob-->Alice: I am good thanks!`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); actors.get('Bob').description = 'Bob'; @@ -249,14 +248,13 @@ Bob-->Alice: I am good thanks!`; }); it('should handle a sequenceDiagram definition with a title without a :', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram title Diagram Title Alice->Bob:Hello Bob, how are you? Note right of Bob: Bob thinks -Bob-->Alice: I am good thanks!`; +Bob-->Alice: I am good thanks!`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); actors.get('Bob').description = 'Bob'; @@ -272,22 +270,20 @@ Bob-->Alice: I am good thanks!`; }); it('should handle a sequenceDiagram definition with a accessibility title and description (accDescr)', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram title: Diagram Title accTitle: This is the title accDescr: Accessibility Description Alice->Bob:Hello Bob, how are you? -`; +`); - await mermaidAPI.parse(str); expect(diagram.db.getDiagramTitle()).toBe('Diagram Title'); expect(diagram.db.getAccTitle()).toBe('This is the title'); expect(diagram.db.getAccDescription()).toBe('Accessibility Description'); - const messages = diagram.db.getMessages(); }); it('should handle a sequenceDiagram definition with a accessibility title and multiline description (accDescr)', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram accTitle: This is the title accDescr { @@ -295,21 +291,18 @@ Accessibility Description } Alice->Bob:Hello Bob, how are you? -`; +`); - await mermaidAPI.parse(str); expect(diagram.db.getAccTitle()).toBe('This is the title'); expect(diagram.db.getAccDescription()).toBe('Accessibility\nDescription'); - const messages = diagram.db.getMessages(); }); it('should space in actor names', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob:Hello Bob, how are - you? -Bob-->Alice: I am good thanks!`; +Bob-->Alice: I am good thanks!`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); actors.get('Bob').description = 'Bob'; @@ -321,12 +314,11 @@ Bob-->Alice: I am good thanks!`; expect(messages[1].from).toBe('Bob'); }); it('should handle dashes in actor names', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice-in-Wonderland->Bob:Hello Bob, how are - you? -Bob-->Alice-in-Wonderland:I am good thanks!`; +Bob-->Alice-in-Wonderland:I am good thanks!`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice-in-Wonderland').description).toBe('Alice-in-Wonderland'); expect(actors.get('Bob').description).toBe('Bob'); @@ -339,14 +331,13 @@ Bob-->Alice-in-Wonderland:I am good thanks!`; }); it('should handle dashes in participant names', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram participant Alice-in-Wonderland participant Bob Alice-in-Wonderland->Bob:Hello Bob, how are - you? -Bob-->Alice-in-Wonderland:I am good thanks!`; +Bob-->Alice-in-Wonderland:I am good thanks!`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect([...actors.keys()]).toEqual(['Alice-in-Wonderland', 'Bob']); expect(actors.get('Alice-in-Wonderland').description).toBe('Alice-in-Wonderland'); @@ -360,14 +351,12 @@ Bob-->Alice-in-Wonderland:I am good thanks!`; }); it('should alias participants', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram participant A as Alice participant B as Bob A->B:Hello Bob, how are you? -B-->A: I am good thanks!`; - - await mermaidAPI.parse(str); +B-->A: I am good thanks!`); const actors = diagram.db.getActors(); @@ -381,7 +370,7 @@ B-->A: I am good thanks!`; expect(messages[1].from).toBe('B'); }); it('should alias a mix of actors and participants apa12', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram actor Alice as Alice2 actor Bob @@ -391,9 +380,7 @@ sequenceDiagram Bob->>Alice: Hi Alice Alice->>John: Hi John John->>Mandy: Hi Mandy - Mandy ->>Joan: Hi Joan`; - - await mermaidAPI.parse(str); + Mandy ->>Joan: Hi Joan`); const actors = diagram.db.getActors(); expect([...actors.keys()]).toEqual(['Alice', 'Bob', 'John', 'Mandy', 'Joan']); @@ -409,14 +396,12 @@ sequenceDiagram expect(messages[4].to).toBe('Joan'); }); it('should alias actors apa13', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram actor A as Alice actor B as Bob A->B:Hello Bob, how are you? -B-->A: I am good thanks!`; - - await mermaidAPI.parse(str); +B-->A: I am good thanks!`); const actors = diagram.db.getActors(); expect([...actors.keys()]).toEqual(['A', 'B']); @@ -429,11 +414,10 @@ B-->A: I am good thanks!`; expect(messages[1].from).toBe('B'); }); it('should handle in async messages', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram -Alice-xBob:Hello Bob, how are you?`; +Alice-xBob:Hello Bob, how are you?`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); expect(actors.get('Bob').description).toBe('Bob'); @@ -444,11 +428,10 @@ Alice-xBob:Hello Bob, how are you?`; expect(messages[0].type).toBe(diagram.db.LINETYPE.SOLID_CROSS); }); it('should handle in async dotted messages', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram -Alice--xBob:Hello Bob, how are you?`; +Alice--xBob:Hello Bob, how are you?`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); expect(actors.get('Bob').description).toBe('Bob'); @@ -459,11 +442,10 @@ Alice--xBob:Hello Bob, how are you?`; expect(messages[0].type).toBe(diagram.db.LINETYPE.DOTTED_CROSS); }); it('should handle in sync messages', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram -Alice-)Bob:Hello Bob, how are you?`; +Alice-)Bob:Hello Bob, how are you?`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); expect(actors.get('Bob').description).toBe('Bob'); @@ -474,11 +456,10 @@ Alice-)Bob:Hello Bob, how are you?`; expect(messages[0].type).toBe(diagram.db.LINETYPE.SOLID_POINT); }); it('should handle in sync dotted messages', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram -Alice--)Bob:Hello Bob, how are you?`; +Alice--)Bob:Hello Bob, how are you?`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); expect(actors.get('Bob').description).toBe('Bob'); @@ -489,11 +470,10 @@ Alice--)Bob:Hello Bob, how are you?`; expect(messages[0].type).toBe(diagram.db.LINETYPE.DOTTED_POINT); }); it('should handle in arrow messages', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram -Alice->>Bob:Hello Bob, how are you?`; +Alice->>Bob:Hello Bob, how are you?`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); expect(actors.get('Bob').description).toBe('Bob'); @@ -504,9 +484,10 @@ Alice->>Bob:Hello Bob, how are you?`; expect(messages[0].type).toBe(diagram.db.LINETYPE.SOLID); }); it('should handle in arrow messages', async () => { - const str = 'sequenceDiagram\n' + 'Alice-->>Bob:Hello Bob, how are you?'; + const diagram = await Diagram.fromText( + 'sequenceDiagram\n' + 'Alice-->>Bob:Hello Bob, how are you?' + ); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); expect(actors.get('Bob').description).toBe('Bob'); @@ -517,11 +498,12 @@ Alice->>Bob:Hello Bob, how are you?`; expect(messages[0].type).toBe(diagram.db.LINETYPE.DOTTED); }); it('should handle bidirectional arrow messages', async () => { - const str = ` + const diagram = await Diagram.fromText( + ` sequenceDiagram -Alice<<->>Bob:Hello Bob, how are you?`; +Alice<<->>Bob:Hello Bob, how are you?` + ); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); expect(actors.get('Bob').description).toBe('Bob'); @@ -532,11 +514,12 @@ Alice<<->>Bob:Hello Bob, how are you?`; expect(messages[0].type).toBe(diagram.db.LINETYPE.BIDIRECTIONAL_SOLID); }); it('should handle bidirectional dotted arrow messages', async () => { - const str = ` + const diagram = await Diagram.fromText( + ` sequenceDiagram - Alice<<-->>Bob:Hello Bob, how are you?`; + Alice<<-->>Bob:Hello Bob, how are you?` + ); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); expect(actors.get('Bob').description).toBe('Bob'); @@ -547,14 +530,13 @@ Alice<<->>Bob:Hello Bob, how are you?`; expect(messages[0].type).toBe(diagram.db.LINETYPE.BIDIRECTIONAL_DOTTED); }); it('should handle actor activation', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice-->>Bob:Hello Bob, how are you? activate Bob Bob-->>Alice:Hello Alice, I'm fine and you? -deactivate Bob`; +deactivate Bob`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); expect(actors.get('Bob').description).toBe('Bob'); @@ -570,12 +552,11 @@ deactivate Bob`; expect(messages[3].from).toBe('Bob'); }); it('should handle actor one line notation activation', async () => { - const str = ` - sequenceDiagram - Alice-->>+Bob:Hello Bob, how are you? - Bob-->>- Alice:Hello Alice, I'm fine and you?`; + const diagram = await Diagram.fromText(` + sequenceDiagram + Alice-->>+Bob:Hello Bob, how are you? + Bob-->>- Alice:Hello Alice, I'm fine and you?`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); expect(actors.get('Bob').description).toBe('Bob'); @@ -592,14 +573,13 @@ deactivate Bob`; expect(messages[3].from).toBe('Bob'); }); it('should handle stacked activations', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice-->>+Bob:Hello Bob, how are you? Bob-->>+Carol:Carol, let me introduce Alice? Bob-->>- Alice:Hello Alice, please meet Carol? - Carol->>- Bob:Oh Bob, I'm so happy to be here!`; + Carol->>- Bob:Oh Bob, I'm so happy to be here!`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); expect(actors.get('Bob').description).toBe('Bob'); @@ -645,14 +625,13 @@ deactivate Bob`; }); it('should handle comments in a sequenceDiagram', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? %% Comment Note right of Bob: Bob thinks - Bob-->Alice: I am good thanks!`; + Bob-->Alice: I am good thanks!`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); actors.get('Bob').description = 'Bob'; @@ -664,16 +643,15 @@ deactivate Bob`; expect(messages[2].from).toBe('Bob'); }); it('should handle new lines in a sequenceDiagram', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? %% Comment Note right of Bob: Bob thinks Bob-->Alice: I am good thanks! - `; + `); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); actors.get('Bob').description = 'Bob'; @@ -685,10 +663,9 @@ deactivate Bob`; expect(messages[2].from).toBe('Bob'); }); it('should handle semicolons', async () => { - const str = ` -sequenceDiagram;Alice->Bob: Hello Bob, how are you?;Note right of Bob: Bob thinks;Bob-->Alice: I am good thanks!;`; + const diagram = await Diagram.fromText(` +sequenceDiagram;Alice->Bob: Hello Bob, how are you?;Note right of Bob: Bob thinks;Bob-->Alice: I am good thanks!;`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); actors.get('Bob').description = 'Bob'; @@ -700,15 +677,14 @@ sequenceDiagram;Alice->Bob: Hello Bob, how are you?;Note right of Bob: Bob think expect(messages[2].from).toBe('Bob'); }); it('should handle one leading space in lines in a sequenceDiagram', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? %% Comment Note right of Bob: Bob thinks -Bob-->Alice: I am good thanks!`; +Bob-->Alice: I am good thanks!`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); actors.get('Bob').description = 'Bob'; @@ -720,15 +696,14 @@ Bob-->Alice: I am good thanks!`; expect(messages[2].from).toBe('Bob'); }); it('should handle several leading spaces in lines in a sequenceDiagram', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? %% Comment Note right of Bob: Bob thinks -Bob-->Alice: I am good thanks!`; +Bob-->Alice: I am good thanks!`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); actors.get('Bob').description = 'Bob'; @@ -740,7 +715,7 @@ Bob-->Alice: I am good thanks!`; expect(messages[2].from).toBe('Bob'); }); it('should handle several leading spaces in lines in a sequenceDiagram', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram participant Alice participant Bob @@ -751,9 +726,8 @@ John->John: Fight against hypochondria Note right of John: Rational thoughts
prevail... John-->Alice: Great! John->Bob: How about you? -Bob-->John: Jolly good!`; +Bob-->John: Jolly good!`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); actors.get('Bob').description = 'Bob'; @@ -765,7 +739,7 @@ Bob-->John: Jolly good!`; expect(messages[2].from).toBe('John'); }); it('should handle different line breaks', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram participant 1 as multiline
text participant 2 as multiline
text @@ -779,9 +753,7 @@ note right of 3: multiline
text note right of 4: multiline
text 4->>1: multiline
text note right of 1: multiline
text -`; - - await mermaidAPI.parse(str); +`); const actors = diagram.db.getActors(); expect(actors.get('1').description).toBe('multiline
text'); @@ -800,7 +772,7 @@ note right of 1: multiline
text expect(messages[7].message).toBe('multiline
text'); }); it('should handle notes and messages without wrap specified', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram participant 1 participant 2 @@ -814,9 +786,7 @@ note right of 3:nowrap: single-line text note right of 4: multiline
text 4->>1:nowrap: multiline
text note right of 1:nowrap: multiline
text -`; - - await mermaidAPI.parse(str); +`); const messages = diagram.db.getMessages(); expect(messages[0].message).toBe('single-line text'); @@ -839,7 +809,7 @@ note right of 1:nowrap: multiline
text expect(messages[7].wrap).toBe(false); }); it('should handle notes and messages with wrap specified', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram participant 1 participant 2 @@ -849,9 +819,7 @@ participant 4 note right of 2:wrap: single-line text 2->>3:wrap: multiline
text note right of 3:wrap: multiline
text -`; - - await mermaidAPI.parse(str); +`); const messages = diagram.db.getMessages(); expect(messages[0].message).toBe('single-line text'); @@ -864,15 +832,13 @@ note right of 3:wrap: multiline
text expect(messages[3].wrap).toBe(true); }); it('should handle notes and messages with nowrap or line breaks', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram participant 1 participant 2 1->>2: single-line text note right of 2: single-line text -`; - - await mermaidAPI.parse(str); +`); const messages = diagram.db.getMessages(); expect(messages[0].message).toBe('single-line text'); @@ -881,27 +847,23 @@ note right of 2: single-line text expect(messages[1].wrap).toBe(false); }); it('should handle notes over a single actor', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? Note over Bob: Bob thinks -`; - - await mermaidAPI.parse(str); +`); const messages = diagram.db.getMessages(); expect(messages[1].from).toBe('Bob'); expect(messages[1].to).toBe('Bob'); }); it('should handle notes over multiple actors', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? Note over Alice,Bob: confusion Note over Bob,Alice: resolution -`; - - await mermaidAPI.parse(str); +`); const messages = diagram.db.getMessages(); expect(messages[1].from).toBe('Alice'); @@ -910,7 +872,7 @@ Note over Bob,Alice: resolution expect(messages[2].to).toBe('Alice'); }); it('should handle loop statements', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? @@ -919,9 +881,8 @@ Note right of Bob: Bob thinks loop Multiple happy responses Bob-->Alice: I am good thanks! -end`; +end`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); actors.get('Bob').description = 'Bob'; @@ -933,7 +894,7 @@ end`; expect(messages[1].from).toBe('Bob'); }); it('should add a rect around sequence', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? %% Comment @@ -941,9 +902,8 @@ end`; Note right of Bob: Bob thinks Bob-->Alice: I am good thanks end - `; + `); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); actors.get('Bob').description = 'Bob'; @@ -957,7 +917,7 @@ end`; }); it('should allow for nested rects', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? %% Comment @@ -967,8 +927,8 @@ end`; end Bob-->Alice: I am good thanks end - `; - await mermaidAPI.parse(str); + `); + const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); actors.get('Bob').description = 'Bob'; @@ -984,7 +944,7 @@ end`; expect(messages[6].type).toEqual(diagram.db.LINETYPE.RECT_END); }); it('should handle opt statements', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? @@ -993,9 +953,8 @@ Note right of Bob: Bob thinks opt Perhaps a happy response Bob-->Alice: I am good thanks! -end`; +end`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); actors.get('Bob').description = 'Bob'; @@ -1007,7 +966,7 @@ end`; expect(messages[1].from).toBe('Bob'); }); it('should handle alt statements', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? @@ -1018,9 +977,8 @@ alt isWell Bob-->Alice: I am good thanks! else isSick Bob-->Alice: Feel sick... -end`; +end`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); @@ -1033,7 +991,7 @@ end`; expect(messages[1].from).toBe('Bob'); }); it('should handle alt statements with multiple elses', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? @@ -1046,8 +1004,8 @@ else isSick Bob-->Alice: Feel sick... else default Bob-->Alice: :-) -end`; - await mermaidAPI.parse(str); +end`); + const messages = diagram.db.getMessages(); expect(messages.length).toBe(9); expect(messages[1].from).toBe('Bob'); @@ -1060,13 +1018,12 @@ end`; expect(messages[8].type).toBe(diagram.db.LINETYPE.ALT_END); }); it('should handle critical statements without options', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram critical Establish a connection to the DB Service-->DB: connect - end`; + end`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Service').description).toBe('Service'); @@ -1080,7 +1037,7 @@ sequenceDiagram expect(messages[2].type).toBe(diagram.db.LINETYPE.CRITICAL_END); }); it('should handle critical statements with options', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram critical Establish a connection to the DB Service-->DB: connect @@ -1088,9 +1045,8 @@ sequenceDiagram Service-->Service: Log error option Credentials rejected Service-->Service: Log different error - end`; + end`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Service').description).toBe('Service'); @@ -1108,16 +1064,15 @@ sequenceDiagram expect(messages[6].type).toBe(diagram.db.LINETYPE.CRITICAL_END); }); it('should handle break statements', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Consumer-->API: Book something API-->BookingService: Start booking process break when the booking process fails API-->Consumer: show failure end - API-->BillingService: Start billing process`; + API-->BillingService: Start billing process`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Consumer').description).toBe('Consumer'); @@ -1134,7 +1089,7 @@ sequenceDiagram expect(messages[5].from).toBe('API'); }); it('should handle par statements a sequenceDiagram', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram par Parallel one Alice->>Bob: Hello Bob, how are you? @@ -1145,9 +1100,8 @@ Bob-->>Alice: Fine! and Parallel three Alice->>Bob: What do you think about it? Bob-->>Alice: It's good! -end`; +end`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); @@ -1161,15 +1115,14 @@ end`; expect(messages[2].from).toBe('Bob'); }); it('it should handle par_over statements', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram par_over Parallel overlap Alice ->> Bob: Message Note left of Alice: Alice note Note right of Bob: Bob note -end`; +end`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('Alice').description).toBe('Alice'); @@ -1184,120 +1137,102 @@ end`; expect(messages[3].from).toBe('Bob'); }); it('should handle special characters in signals', async () => { - const str = 'sequenceDiagram\n' + 'Alice->Bob: -:<>,;# comment'; - - await mermaidAPI.parse(str); + const diagram = await Diagram.fromText('sequenceDiagram\n' + 'Alice->Bob: -:<>,;# comment'); const messages = diagram.db.getMessages(); expect(messages[0].message).toBe('-:<>,'); }); it('should handle special characters in notes', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? -Note right of Bob: -:<>,;# comment`; - - await mermaidAPI.parse(str); +Note right of Bob: -:<>,;# comment`); const messages = diagram.db.getMessages(); expect(messages[1].message).toBe('-:<>,'); }); it('should handle special characters in loop', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? loop -:<>,;# comment Bob-->Alice: I am good thanks! -end`; - - await mermaidAPI.parse(str); +end`); const messages = diagram.db.getMessages(); expect(messages[1].message).toBe('-:<>,'); }); it('should handle special characters in opt', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? opt -:<>,;# comment Bob-->Alice: I am good thanks! -end`; - - await mermaidAPI.parse(str); +end`); const messages = diagram.db.getMessages(); expect(messages[1].message).toBe('-:<>,'); }); it('should handle special characters in alt', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? alt -:<>,;# comment Bob-->Alice: I am good thanks! else ,<>:-#; comment Bob-->Alice: I am good thanks! -end`; - - await mermaidAPI.parse(str); +end`); const messages = diagram.db.getMessages(); expect(messages[1].message).toBe('-:<>,'); expect(messages[3].message).toBe(',<>:-'); }); it('should handle special characters in par', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? par -:<>,;# comment Bob-->Alice: I am good thanks! and ,<>:-#; comment Bob-->Alice: I am good thanks! -end`; - - await mermaidAPI.parse(str); +end`); const messages = diagram.db.getMessages(); expect(messages[1].message).toBe('-:<>,'); expect(messages[3].message).toBe(',<>:-'); }); it('should handle no-label loop', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? loop Bob-->Alice: I am good thanks! -end`; - - await mermaidAPI.parse(str); +end`); const messages = diagram.db.getMessages(); expect(messages[1].message).toBe(''); expect(messages[2].message).toBe('I am good thanks!'); }); it('should handle no-label opt', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? opt # comment Bob-->Alice: I am good thanks! -end`; - - await mermaidAPI.parse(str); +end`); const messages = diagram.db.getMessages(); expect(messages[1].message).toBe(''); expect(messages[2].message).toBe('I am good thanks!'); }); it('should handle no-label alt', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? alt;Bob-->Alice: I am good thanks! else # comment Bob-->Alice: I am good thanks! -end`; - - await mermaidAPI.parse(str); +end`); const messages = diagram.db.getMessages(); expect(messages[1].message).toBe(''); @@ -1306,15 +1241,13 @@ end`; expect(messages[4].message).toBe('I am good thanks!'); }); it('should handle no-label par', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob: Hello Bob, how are you? par;Bob-->Alice: I am good thanks! and # comment Bob-->Alice: I am good thanks! -end`; - - await mermaidAPI.parse(str); +end`); const messages = diagram.db.getMessages(); expect(messages[1].message).toBe(''); @@ -1324,7 +1257,7 @@ end`; }); it('should handle links', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram participant a as Alice participant b as Bob @@ -1335,9 +1268,8 @@ links a: { "On-Call": "https://oncall.contoso.com/?svc=alice" } link a: Endpoint @ https://alice.contoso.com link a: Swagger @ https://swagger.contoso.com link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com -`; +`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('a').links.Repo).toBe('https://repo.contoso.com/'); expect(actors.get('b').links.Repo).toBe(undefined); @@ -1352,16 +1284,15 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com it('should handle properties EXPERIMENTAL: USE WITH CAUTION', async () => { //Be aware that the syntax for "properties" is likely to be changed. - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram participant a as Alice participant b as Bob participant c as Charlie properties a: {"class": "internal-service-actor", "icon": "@clock"} properties b: {"class": "external-service-actor", "icon": "@computer"} -`; +`); - await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.get('a').properties.class).toBe('internal-service-actor'); expect(actors.get('b').properties.class).toBe('external-service-actor'); @@ -1371,7 +1302,7 @@ properties b: {"class": "external-service-actor", "icon": "@computer"} }); it('should handle box', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram box green Group 1 participant a as Alice @@ -1384,9 +1315,8 @@ links a: { "On-Call": "https://oncall.contoso.com/?svc=alice" } link a: Endpoint @ https://alice.contoso.com link a: Swagger @ https://swagger.contoso.com link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com -`; +`); - await mermaidAPI.parse(str); const boxes = diagram.db.getBoxes(); expect(boxes[0].name).toEqual('Group 1'); expect(boxes[0].actorKeys).toEqual(['a', 'b']); @@ -1394,7 +1324,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com }); it('should handle box without color', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram box Group 1 participant a as Alice @@ -1407,9 +1337,8 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com link a: Endpoint @ https://alice.contoso.com link a: Swagger @ https://swagger.contoso.com link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com - `; + `); - await mermaidAPI.parse(str); const boxes = diagram.db.getBoxes(); expect(boxes[0].name).toEqual('Group 1'); expect(boxes[0].actorKeys).toEqual(['a', 'b']); @@ -1417,7 +1346,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com }); it('should handle box without description', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram box Aqua participant a as Alice @@ -1430,9 +1359,8 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com link a: Endpoint @ https://alice.contoso.com link a: Swagger @ https://swagger.contoso.com link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com - `; + `); - await mermaidAPI.parse(str); const boxes = diagram.db.getBoxes(); expect(boxes[0].name).toBeFalsy(); expect(boxes[0].actorKeys).toEqual(['a', 'b']); @@ -1440,7 +1368,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com }); it('should handle simple actor creation', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram participant a as Alice a ->>b: Hello Bob? @@ -1449,8 +1377,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com c ->> b: Hello b? create actor d as Donald a ->> d: Hello Donald? - `; - await mermaidAPI.parse(str); + `); const actors = diagram.db.getActors(); const createdActors = diagram.db.getCreatedActors(); expect(actors.get('c').name).toEqual('c'); @@ -1463,7 +1390,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com expect(createdActors.get('d')).toEqual(3); }); it('should handle simple actor destruction', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram participant a as Alice a ->>b: Hello Bob? @@ -1472,14 +1399,13 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com b ->> c: Where is Alice? destroy c b ->> c: Where are you? - `; - await mermaidAPI.parse(str); + `); const destroyedActors = diagram.db.getDestroyedActors(); expect(destroyedActors.get('a')).toEqual(1); expect(destroyedActors.get('c')).toEqual(3); }); it('should handle the creation and destruction of the same actor', async () => { - const str = ` + const diagram2 = await Diagram.fromText(` sequenceDiagram a ->>b: Hello Bob? create participant c @@ -1487,10 +1413,9 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com c ->> b: Hello b? destroy c b ->> c : Bye c ! - `; - await mermaidAPI.parse(str); - const createdActors = diagram.db.getCreatedActors(); - const destroyedActors = diagram.db.getDestroyedActors(); + `); + const createdActors = diagram2.db.getCreatedActors(); + const destroyedActors = diagram2.db.getDestroyedActors(); expect(createdActors.get('c')).toEqual(1); expect(destroyedActors.get('c')).toEqual(3); }); @@ -1514,7 +1439,12 @@ describe('when checking the bounds in a sequenceDiagram', function () { }); let conf; - beforeEach(function () { + let diagram; + beforeEach(async function () { + diagram = await Diagram.fromText(` + sequenceDiagram + Alice->Bob:Hello Bob, how are you? + Bob-->Alice: I am good thanks!`); mermaidAPI.reset(); diagram.renderer.bounds.init(); conf = diagram.db.getConfig(); @@ -1570,7 +1500,7 @@ describe('when checking the bounds in a sequenceDiagram', function () { expect(bounds.stopx).toBe(300); expect(bounds.stopy).toBe(400); }); - it('should handle multiple loops withtout expanding the bounds', () => { + it('should handle multiple loops without expanding the bounds', () => { diagram.renderer.bounds.insert(100, 100, 1000, 1000); diagram.renderer.bounds.verticalPos = 200; diagram.renderer.bounds.newLoop(); @@ -1643,7 +1573,7 @@ describe('when rendering a sequenceDiagram APA', function () { setSiteConfig({ logLevel: 5, sequence: conf }); }); let conf; - beforeEach(async function () { + beforeEach(function () { mermaidAPI.reset(); // }); @@ -1662,11 +1592,6 @@ describe('when rendering a sequenceDiagram APA', function () { mirrorActors: false, }; setSiteConfig({ logLevel: 5, sequence: conf }); - diagram = await Diagram.fromText(` -sequenceDiagram -Alice->Bob:Hello Bob, how are you? -Note right of Bob: Bob thinks -Bob-->Alice: I am good thanks!`); }); ['tspan', 'fo', 'old', undefined].forEach(function (textPlacement) { it(` @@ -1675,7 +1600,7 @@ it should handle one actor, when textPlacement is ${textPlacement}`, async () => sequenceDiagram participant Alice`; - await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); // diagram.renderer.setConf(mermaidAPI.getConfig().sequence); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); @@ -1687,14 +1612,12 @@ participant Alice`; }); }); it('should handle same actor with different whitespace properly', async () => { - const str = ` + const diagram = await Diagram.fromText(` sequenceDiagram participant Alice participant Alice participant Alice -`; - - await mermaidAPI.parse(str); +`); const actors = diagram.db.getActors(); expect([...actors.keys()]).toEqual(['Alice']); @@ -1705,9 +1628,9 @@ sequenceDiagram participant Alice Note over Alice: Alice thinks `; + const diagram = await Diagram.fromText(str); expect(mermaidAPI.getConfig().sequence.mirrorActors).toBeFalsy(); - await mermaidAPI.parse(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1723,7 +1646,7 @@ sequenceDiagram participant Alice Note left of Alice: Alice thinks`; - await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1739,7 +1662,7 @@ sequenceDiagram participant Alice Note right of Alice: Alice thinks`; - await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1754,7 +1677,7 @@ Note right of Alice: Alice thinks`; sequenceDiagram Alice->Bob: Hello Bob, how are you?`; - await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1772,7 +1695,7 @@ participant Bob end Alice->Bob: Hello Bob, how are you?`; - await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1786,8 +1709,8 @@ Alice->Bob: Hello Bob, how are you?`; %%{init: {'logLevel': 0}}%% sequenceDiagram Alice->Bob: Hello Bob, how are you?`; - await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1802,12 +1725,11 @@ Alice->Bob: Hello Bob, how are you?`; const str = ` %%{init: { 'logLevel': 0}}%% sequenceDiagram -%%{ -wrap -}%% +%%{wrap}%% Alice->Bob: Hello Bob, how are you?`; await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const msgs = diagram.db.getMessages(); @@ -1827,8 +1749,8 @@ Alice->Bob: Hello Bob, how are you? Note over Alice,Bob: Looks Note over Bob,Alice: Looks back `; + const diagram = await Diagram.fromText(str); // mermaidAPI.initialize({logLevel:0}) - await mermaidAPI.parse(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1843,7 +1765,7 @@ sequenceDiagram Alice->Bob: Hello Bob, how are you? Bob->Alice: Fine!`; - await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1859,7 +1781,7 @@ Alice->Bob: Hello Bob, how are you? Note right of Bob: Bob thinks Bob->Alice: Fine!`; - await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1878,7 +1800,7 @@ Alice->Bob: Hello Bob, how are you? Note left of Alice: Bob thinks Bob->Alice: Fine!`; - await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1895,7 +1817,7 @@ Alice->>Bob:wrap: Hello Bob, how are you? If you are not available right now, I Note left of Alice: Bob thinks Bob->>Alice: Fine!`; - await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1917,6 +1839,7 @@ Note left of Alice: Bob thinks Bob->>Alice: Fine!`; await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1940,6 +1863,8 @@ Note left of Alice: Bob thinks Bob->>Alice: Fine!`; await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1962,6 +1887,7 @@ Note left of Alice: Bob thinks Bob->>Alice: Fine!`; // mermaidAPI.initialize({ logLevel: 0 }); await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1985,9 +1911,9 @@ Alice->Bob: Hello Bob, how are you? loop Cheers Bob->Alice: Fine! end`; - await mermaidAPI.parse(str); - await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + const diagram = await Diagram.fromText(str); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(0); expect(bounds.starty).toBe(0); @@ -2003,7 +1929,7 @@ end`; Bob->Alice: I feel surrounded by darkness end `; - await mermaidAPI.parse(str); + const diagram = await Diagram.fromText(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(0); @@ -2037,20 +1963,23 @@ describe('when rendering a sequenceDiagram with actor mirror activated', () => { }); let conf; - beforeEach(function () { + let diagram; + beforeEach(async function () { + diagram = await Diagram.fromText(` + sequenceDiagram + Alice->Bob:Hello Bob, how are you? + Bob-->Alice: I am good thanks!`); mermaidAPI.reset(); conf = diagram.db.getConfig(); diagram.renderer.bounds.init(); }); ['tspan', 'fo', 'old', undefined].forEach(function (textPlacement) { it('should handle one actor, when textPlacement is' + textPlacement, async () => { - mermaidAPI.initialize(addConf(conf, 'textPlacement', textPlacement)); - diagram.renderer.bounds.init(); const str = ` sequenceDiagram participant Alice`; + const diagram = await Diagram.fromText(str); diagram.renderer.bounds.init(); - await mermaidAPI.parse(str); await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -2071,3 +2000,27 @@ ${prop}-->>A: Hello, how are you?`) ).resolves.toBeDefined(); }); }); + +describe('sequence db class', () => { + let sequenceDb; + beforeEach(() => { + sequenceDb = new SequenceDB(); + }); + // This is to ensure that functions used in sequence JISON are exposed as function from SequenceDB + it('should have functions used in sequence JISON as own property', () => { + const functionsUsedInParser = [ + 'apply', + 'parseBoxData', + 'LINETYPE', + 'setDiagramTitle', + 'setAccTitle', + 'setAccDescription', + 'parseMessage', + 'PLACEMENT', + ]; + + for (const fun of functionsUsedInParser) { + expect(Object.hasOwn(sequenceDb, fun)).toBe(true); + } + }); +}); diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.ts b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.ts index f8d71c95e..f2b701712 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.ts @@ -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 } }); + } }, }; diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index 951d84b86..389171d3c 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -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: diff --git a/packages/mermaid/src/diagrams/sequence/types.ts b/packages/mermaid/src/diagrams/sequence/types.ts index 10c1c8ed3..7cf2ead9c 100644 --- a/packages/mermaid/src/diagrams/sequence/types.ts +++ b/packages/mermaid/src/diagrams/sequence/types.ts @@ -20,6 +20,7 @@ export interface Actor { } export interface Message { + id: string; from?: string; to?: string; message: diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index e1b13dbd7..c3480d203 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -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); + + // 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()); }); }); - - // 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?` - ); - expect(sequenceDiagram1.db).not.toBe(sequenceDiagram2.db); - }); });