From fcb1de915bad30d011a9ea95387652ea268c1408 Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Mon, 13 Jan 2025 20:30:38 +0530 Subject: [PATCH 1/7] convert stateDb to class, added test case. --- .../state/parser/state-parser.spec.js | 4 +- .../diagrams/state/parser/state-style.spec.js | 4 +- packages/mermaid/src/diagrams/state/shapes.js | 3 +- .../mermaid/src/diagrams/state/stateDb.js | 1128 ++++++++--------- .../src/diagrams/state/stateDb.spec.js | 30 +- .../diagrams/state/stateDiagram-v2.spec.js | 4 +- .../src/diagrams/state/stateDiagram-v2.ts | 7 +- .../src/diagrams/state/stateDiagram.spec.js | 4 +- .../src/diagrams/state/stateDiagram.ts | 7 +- packages/mermaid/src/mermaidAPI.spec.ts | 43 + 10 files changed, 654 insertions(+), 580 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js index 9fa8acab8..6d278d3db 100644 --- a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js +++ b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js @@ -1,4 +1,4 @@ -import stateDb from '../stateDb.js'; +import { StateDb } from '../stateDb.js'; import stateDiagram from './stateDiagram.jison'; import { setConfig } from '../../../config.js'; @@ -7,7 +7,9 @@ setConfig({ }); describe('state parser can parse...', () => { + let stateDb; beforeEach(function () { + stateDb = new StateDb(); stateDiagram.parser.yy = stateDb; stateDiagram.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/state/parser/state-style.spec.js b/packages/mermaid/src/diagrams/state/parser/state-style.spec.js index fed63c444..012a56a16 100644 --- a/packages/mermaid/src/diagrams/state/parser/state-style.spec.js +++ b/packages/mermaid/src/diagrams/state/parser/state-style.spec.js @@ -1,4 +1,4 @@ -import stateDb from '../stateDb.js'; +import { StateDb } from '../stateDb.js'; import stateDiagram from './stateDiagram.jison'; import { setConfig } from '../../../config.js'; @@ -7,7 +7,9 @@ setConfig({ }); describe('ClassDefs and classes when parsing a State diagram', () => { + let stateDb; beforeEach(function () { + stateDb = new StateDb(); stateDiagram.parser.yy = stateDb; stateDiagram.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/state/shapes.js b/packages/mermaid/src/diagrams/state/shapes.js index f0ab4136b..f096f0cbc 100644 --- a/packages/mermaid/src/diagrams/state/shapes.js +++ b/packages/mermaid/src/diagrams/state/shapes.js @@ -1,6 +1,6 @@ import { line, curveBasis } from 'd3'; import idCache from './id-cache.js'; -import stateDb from './stateDb.js'; +import { StateDb } from './stateDb.js'; import utils from '../../utils.js'; import common from '../common/common.js'; import { getConfig } from '../../diagram-api/diagramAPI.js'; @@ -412,6 +412,7 @@ export const drawState = function (elem, stateDef) { let edgeCount = 0; export const drawEdge = function (elem, path, relation) { + const stateDb = new StateDb(); const getRelationType = function (type) { switch (type) { case stateDb.relationType.AGGREGATION: diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.js index 1f12425e6..5250eff3d 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.js +++ b/packages/mermaid/src/diagrams/state/stateDb.js @@ -1,626 +1,622 @@ +import { getConfig } from '../../diagram-api/diagramAPI.js'; import { log } from '../../logger.js'; import { generateId } from '../../utils.js'; import common from '../common/common.js'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; import { - setAccTitle, - getAccTitle, - getAccDescription, - setAccDescription, clear as commonClear, - setDiagramTitle, + getAccDescription, + getAccTitle, getDiagramTitle, + setAccDescription, + setAccTitle, + setDiagramTitle, } from '../common/commonDb.js'; import { dataFetcher, reset as resetDataFetching } from './dataFetcher.js'; import { getDir } from './stateRenderer-v3-unified.js'; import { DEFAULT_DIAGRAM_DIRECTION, - STMT_STATE, - STMT_RELATION, - STMT_CLASSDEF, - STMT_STYLEDEF, - STMT_APPLYCLASS, DEFAULT_STATE_TYPE, DIVIDER_TYPE, + STMT_APPLYCLASS, + STMT_CLASSDEF, + STMT_RELATION, + STMT_STATE, + STMT_STYLEDEF, } from './stateCommon.js'; -const START_NODE = '[*]'; -const START_TYPE = 'start'; -const END_NODE = START_NODE; -const END_TYPE = 'end'; +export class StateDb { + #START_NODE = '[*]'; + #START_TYPE = 'start'; + #END_NODE = this.#START_NODE; + #END_TYPE = 'end'; -const COLOR_KEYWORD = 'color'; -const FILL_KEYWORD = 'fill'; -const BG_FILL = 'bgFill'; -const STYLECLASS_SEP = ','; + #COLOR_KEYWORD = 'color'; + #FILL_KEYWORD = 'fill'; + #BG_FILL = 'bgFill'; + #STYLECLASS_SEP = ','; -/** - * Returns a new list of classes. - * In the future, this can be replaced with a class common to all diagrams. - * ClassDef information = { id: id, styles: [], textStyles: [] } - * - * @returns {Map} - */ -function newClassesList() { - return new Map(); -} - -let nodes = []; -let edges = []; - -let direction = DEFAULT_DIAGRAM_DIRECTION; -let rootDoc = []; -let classes = newClassesList(); // style classes defined by a classDef - -// -------------------------------------- - -const newDoc = () => { - return { - /** @type {{ id1: string, id2: string, relationTitle: string }[]} */ - relations: [], - states: new Map(), - documents: {}, + /** + * Returns a new list of classes. + * In the future, this can be replaced with a class common to all diagrams. + * ClassDef information = { id: id, styles: [], textStyles: [] } + * + * @returns {Map} + */ + #newClassesList = () => { + return new Map(); }; -}; -let documents = { - root: newDoc(), -}; -let currentDocument = documents.root; -let startEndCount = 0; -let dividerCnt = 0; + #nodes = []; + #edges = []; -export const lineType = { - LINE: 0, - DOTTED_LINE: 1, -}; + #direction = DEFAULT_DIAGRAM_DIRECTION; + #rootDoc = []; + #classes = this.#newClassesList(); // style classes defined by a classDef -export const relationType = { - AGGREGATION: 0, - EXTENSION: 1, - COMPOSITION: 2, - DEPENDENCY: 3, -}; + constructor() { + this.clear(); + } -const clone = (o) => JSON.parse(JSON.stringify(o)); + // -------------------------------------- -const setRootDoc = (o) => { - log.info('Setting root doc', o); - // rootDoc = { id: 'root', doc: o }; - rootDoc = o; -}; + #newDoc = () => { + return { + /** @type {{ id1: string, id2: string, relationTitle: string }[]} */ + relations: [], + states: new Map(), + documents: {}, + }; + }; -const getRootDoc = () => rootDoc; + #documents = { + root: this.#newDoc(), + }; -const docTranslator = (parent, node, first) => { - if (node.stmt === STMT_RELATION) { - docTranslator(parent, node.state1, true); - docTranslator(parent, node.state2, false); - } else { - if (node.stmt === STMT_STATE) { - if (node.id === '[*]') { - node.id = first ? parent.id + '_start' : parent.id + '_end'; - node.start = first; - } else { - // This is just a plain state, not a start or end - node.id = node.id.trim(); - } - } + #currentDocument = this.#documents.root; + #startEndCount = 0; + #dividerCnt = 0; - if (node.doc) { - const doc = []; - // Check for concurrency - let currentDoc = []; - let i; - for (i = 0; i < node.doc.length; i++) { - if (node.doc[i].type === DIVIDER_TYPE) { - // debugger; - const newNode = clone(node.doc[i]); - newNode.doc = clone(currentDoc); - doc.push(newNode); - currentDoc = []; + lineType = { + LINE: 0, + DOTTED_LINE: 1, + }; + + relationType = { + AGGREGATION: 0, + EXTENSION: 1, + COMPOSITION: 2, + DEPENDENCY: 3, + }; + + #clone = (o) => JSON.parse(JSON.stringify(o)); + + setRootDoc = (o) => { + log.info('Setting root doc', o); + // rootDoc = { id: 'root', doc: o }; + this.#rootDoc = o; + }; + + getRootDoc = () => this.#rootDoc; + + #docTranslator = (parent, node, first) => { + if (node.stmt === STMT_RELATION) { + this.#docTranslator(parent, node.state1, true); + this.#docTranslator(parent, node.state2, false); + } else { + if (node.stmt === STMT_STATE) { + if (node.id === '[*]') { + node.id = first ? parent.id + '_start' : parent.id + '_end'; + node.start = first; } else { - currentDoc.push(node.doc[i]); + // This is just a plain state, not a start or end + node.id = node.id.trim(); } } - // If any divider was encountered - if (doc.length > 0 && currentDoc.length > 0) { - const newNode = { - stmt: STMT_STATE, - id: generateId(), - type: 'divider', - doc: clone(currentDoc), - }; - doc.push(clone(newNode)); - node.doc = doc; - } - - node.doc.forEach((docNode) => docTranslator(node, docNode, true)); - } - } -}; -const getRootDocV2 = () => { - docTranslator({ id: 'root' }, { id: 'root', doc: rootDoc }, true); - return { id: 'root', doc: rootDoc }; - // Here -}; - -/** - * Convert all of the statements (stmts) that were parsed into states and relationships. - * This is done because a state diagram may have nested sections, - * where each section is a 'document' and has its own set of statements. - * Ex: the section within a fork has its own statements, and incoming and outgoing statements - * refer to the fork as a whole (document). - * See the parser grammar: the definition of a document is a document then a 'line', where a line can be a statement. - * This will push the statement into the list of statements for the current document. - * - * @param _doc - */ -const extract = (_doc) => { - // const res = { states: [], relations: [] }; - let doc; - if (_doc.doc) { - doc = _doc.doc; - } else { - doc = _doc; - } - // let doc = root.doc; - // if (!doc) { - // doc = root; - // } - log.info(doc); - clear(true); - - log.info('Extract initial document:', doc); - - doc.forEach((item) => { - log.warn('Statement', item.stmt); - switch (item.stmt) { - case STMT_STATE: - addState( - item.id.trim(), - item.type, - item.doc, - item.description, - item.note, - item.classes, - item.styles, - item.textStyles - ); - break; - case STMT_RELATION: - addRelation(item.state1, item.state2, item.description); - break; - case STMT_CLASSDEF: - addStyleClass(item.id.trim(), item.classes); - break; - case STMT_STYLEDEF: - { - const ids = item.id.trim().split(','); - const styles = item.styleClass.split(','); - ids.forEach((id) => { - let foundState = getState(id); - if (foundState === undefined) { - const trimmedId = id.trim(); - addState(trimmedId); - foundState = getState(trimmedId); - } - foundState.styles = styles.map((s) => s.replace(/;/g, '')?.trim()); - }); + if (node.doc) { + const doc = []; + // Check for concurrency + let currentDoc = []; + let i; + for (i = 0; i < node.doc.length; i++) { + if (node.doc[i].type === DIVIDER_TYPE) { + const newNode = this.#clone(node.doc[i]); + newNode.doc = this.#clone(currentDoc); + doc.push(newNode); + currentDoc = []; + } else { + currentDoc.push(node.doc[i]); + } } - break; - case STMT_APPLYCLASS: - setCssClass(item.id.trim(), item.styleClass); - break; - } - }); - const diagramStates = getStates(); - const config = getConfig(); - const look = config.look; - resetDataFetching(); - dataFetcher(undefined, getRootDocV2(), diagramStates, nodes, edges, true, look, classes); - nodes.forEach((node) => { - if (Array.isArray(node.label)) { - // add the rest as description - node.description = node.label.slice(1); - if (node.isGroup && node.description.length > 0) { - throw new Error( - 'Group nodes can only have label. Remove the additional description for node [' + - node.id + - ']' - ); + // If any divider was encountered + if (doc.length > 0 && currentDoc.length > 0) { + const newNode = { + stmt: STMT_STATE, + id: generateId(), + type: 'divider', + doc: this.#clone(currentDoc), + }; + doc.push(this.#clone(newNode)); + node.doc = doc; + } + + node.doc.forEach((docNode) => this.#docTranslator(node, docNode, true)); } - // add first description as label - node.label = node.label[0]; } - }); -}; - -/** - * Function called by parser when a node definition has been found. - * - * @param {null | string} id - * @param {null | string} type - * @param {null | string} doc - * @param {null | string | string[]} descr - description for the state. Can be a string or a list or strings - * @param {null | string} note - * @param {null | string | string[]} classes - class styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 class, convert it to an array of that 1 class. - * @param {null | string | string[]} styles - styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 style, convert it to an array of that 1 style. - * @param {null | string | string[]} textStyles - text styles to apply to this state. Can be a string (1 text test) or an array of text styles. If it's just 1 text style, convert it to an array of that 1 text style. - */ -export const addState = function ( - id, - type = DEFAULT_STATE_TYPE, - doc = null, - descr = null, - note = null, - classes = null, - styles = null, - textStyles = null -) { - const trimmedId = id?.trim(); - // add the state if needed - if (!currentDocument.states.has(trimmedId)) { - log.info('Adding state ', trimmedId, descr); - currentDocument.states.set(trimmedId, { - id: trimmedId, - descriptions: [], - type, - doc, - note, - classes: [], - styles: [], - textStyles: [], - }); - } else { - if (!currentDocument.states.get(trimmedId).doc) { - currentDocument.states.get(trimmedId).doc = doc; - } - if (!currentDocument.states.get(trimmedId).type) { - currentDocument.states.get(trimmedId).type = type; - } - } - - if (descr) { - log.info('Setting state description', trimmedId, descr); - if (typeof descr === 'string') { - addDescription(trimmedId, descr.trim()); - } - - if (typeof descr === 'object') { - descr.forEach((des) => addDescription(trimmedId, des.trim())); - } - } - - if (note) { - const doc2 = currentDocument.states.get(trimmedId); - doc2.note = note; - doc2.note.text = common.sanitizeText(doc2.note.text, getConfig()); - } - - if (classes) { - log.info('Setting state classes', trimmedId, classes); - const classesList = typeof classes === 'string' ? [classes] : classes; - classesList.forEach((cssClass) => setCssClass(trimmedId, cssClass.trim())); - } - - if (styles) { - log.info('Setting state styles', trimmedId, styles); - const stylesList = typeof styles === 'string' ? [styles] : styles; - stylesList.forEach((style) => setStyle(trimmedId, style.trim())); - } - - if (textStyles) { - log.info('Setting state styles', trimmedId, styles); - const textStylesList = typeof textStyles === 'string' ? [textStyles] : textStyles; - textStylesList.forEach((textStyle) => setTextStyle(trimmedId, textStyle.trim())); - } -}; - -export const clear = function (saveCommon) { - nodes = []; - edges = []; - documents = { - root: newDoc(), }; - currentDocument = documents.root; + getRootDocV2 = () => { + this.#docTranslator({ id: 'root' }, { id: 'root', doc: this.#rootDoc }, true); + return { id: 'root', doc: this.#rootDoc }; + // Here + }; - // number of start and end nodes; used to construct ids - startEndCount = 0; - classes = newClassesList(); - if (!saveCommon) { - commonClear(); - } -}; + /** + * Convert all of the statements (stmts) that were parsed into states and relationships. + * This is done because a state diagram may have nested sections, + * where each section is a 'document' and has its own set of statements. + * Ex: the section within a fork has its own statements, and incoming and outgoing statements + * refer to the fork as a whole (document). + * See the parser grammar: the definition of a document is a document then a 'line', where a line can be a statement. + * This will push the statement into the list of statements for the current document. + * + * @param _doc + */ + extract = (_doc) => { + // const res = { states: [], relations: [] }; + let doc; + if (_doc.doc) { + doc = _doc.doc; + } else { + doc = _doc; + } + // let doc = root.doc; + // if (!doc) { + // doc = root; + // } + log.info(doc); + this.clear(true); -export const getState = function (id) { - return currentDocument.states.get(id); -}; + log.info('Extract initial document:', doc); -export const getStates = function () { - return currentDocument.states; -}; -export const logDocuments = function () { - log.info('Documents = ', documents); -}; -export const getRelations = function () { - return currentDocument.relations; -}; + doc.forEach((item) => { + log.warn('Statement', item.stmt); + switch (item.stmt) { + case STMT_STATE: + this.addState( + item.id.trim(), + item.type, + item.doc, + item.description, + item.note, + item.classes, + item.styles, + item.textStyles + ); + break; + case STMT_RELATION: + this.addRelation(item.state1, item.state2, item.description); + break; + case STMT_CLASSDEF: + this.addStyleClass(item.id.trim(), item.classes); + break; + case STMT_STYLEDEF: + { + const ids = item.id.trim().split(','); + const styles = item.styleClass.split(','); + ids.forEach((id) => { + let foundState = this.getState(id); + if (foundState === undefined) { + const trimmedId = id.trim(); + this.addState(trimmedId); + foundState = this.getState(trimmedId); + } + foundState.styles = styles.map((s) => s.replace(/;/g, '')?.trim()); + }); + } + break; + case STMT_APPLYCLASS: + this.setCssClass(item.id.trim(), item.styleClass); + break; + } + }); -/** - * If the id is a start node ( [*] ), then return a new id constructed from - * the start node name and the current start node count. - * else return the given id - * - * @param {string} id - * @returns {string} - the id (original or constructed) - */ -function startIdIfNeeded(id = '') { - let fixedId = id; - if (id === START_NODE) { - startEndCount++; - fixedId = `${START_TYPE}${startEndCount}`; - } - return fixedId; -} + const diagramStates = this.getStates(); + const config = getConfig(); + const look = config.look; -/** - * If the id is a start node ( [*] ), then return the start type ('start') - * else return the given type - * - * @param {string} id - * @param {string} type - * @returns {string} - the type that should be used - */ -function startTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { - return id === START_NODE ? START_TYPE : type; -} + resetDataFetching(); + dataFetcher( + undefined, + this.getRootDocV2(), + diagramStates, + this.#nodes, + this.#edges, + true, + look, + this.#classes + ); + this.#nodes.forEach((node) => { + if (Array.isArray(node.label)) { + // add the rest as description + node.description = node.label.slice(1); + if (node.isGroup && node.description.length > 0) { + throw new Error( + 'Group nodes can only have label. Remove the additional description for node [' + + node.id + + ']' + ); + } + // add first description as label + node.label = node.label[0]; + } + }); + }; -/** - * If the id is an end node ( [*] ), then return a new id constructed from - * the end node name and the current start_end node count. - * else return the given id - * - * @param {string} id - * @returns {string} - the id (original or constructed) - */ -function endIdIfNeeded(id = '') { - let fixedId = id; - if (id === END_NODE) { - startEndCount++; - fixedId = `${END_TYPE}${startEndCount}`; - } - return fixedId; -} + /** + * Function called by parser when a node definition has been found. + * + * @param {null | string} id + * @param {null | string} type + * @param {null | string} doc + * @param {null | string | string[]} descr - description for the state. Can be a string or a list or strings + * @param {null | string} note + * @param {null | string | string[]} classes - class styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 class, convert it to an array of that 1 class. + * @param {null | string | string[]} styles - styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 style, convert it to an array of that 1 style. + * @param {null | string | string[]} textStyles - text styles to apply to this state. Can be a string (1 text test) or an array of text styles. If it's just 1 text style, convert it to an array of that 1 text style. + */ + addState = ( + id, + type = DEFAULT_STATE_TYPE, + doc = null, + descr = null, + note = null, + classes = null, + styles = null, + textStyles = null + ) => { + const trimmedId = id?.trim(); + // add the state if needed + if (!this.#currentDocument.states.has(trimmedId)) { + log.info('Adding state ', trimmedId, descr); + this.#currentDocument.states.set(trimmedId, { + id: trimmedId, + descriptions: [], + type, + doc, + note, + classes: [], + styles: [], + textStyles: [], + }); + } else { + if (!this.#currentDocument.states.get(trimmedId).doc) { + this.#currentDocument.states.get(trimmedId).doc = doc; + } + if (!this.#currentDocument.states.get(trimmedId).type) { + this.#currentDocument.states.get(trimmedId).type = type; + } + } -/** - * If the id is an end node ( [*] ), then return the end type - * else return the given type - * - * @param {string} id - * @param {string} type - * @returns {string} - the type that should be used - */ -function endTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { - return id === END_NODE ? END_TYPE : type; -} + if (descr) { + log.info('Setting state description', trimmedId, descr); + if (typeof descr === 'string') { + this.addDescription(trimmedId, descr.trim()); + } -/** - * - * @param item1 - * @param item2 - * @param relationTitle - */ -export function addRelationObjs(item1, item2, relationTitle) { - let id1 = startIdIfNeeded(item1.id.trim()); - let type1 = startTypeIfNeeded(item1.id.trim(), item1.type); - let id2 = startIdIfNeeded(item2.id.trim()); - let type2 = startTypeIfNeeded(item2.id.trim(), item2.type); + if (typeof descr === 'object') { + descr.forEach((des) => this.addDescription(trimmedId, des.trim())); + } + } - addState( - id1, - type1, - item1.doc, - item1.description, - item1.note, - item1.classes, - item1.styles, - item1.textStyles - ); - addState( - id2, - type2, - item2.doc, - item2.description, - item2.note, - item2.classes, - item2.styles, - item2.textStyles - ); + if (note) { + const doc2 = this.#currentDocument.states.get(trimmedId); + doc2.note = note; + doc2.note.text = common.sanitizeText(doc2.note.text, getConfig()); + } - currentDocument.relations.push({ - id1, - id2, - relationTitle: common.sanitizeText(relationTitle, getConfig()), - }); -} + if (classes) { + log.info('Setting state classes', trimmedId, classes); + const classesList = typeof classes === 'string' ? [classes] : classes; + classesList.forEach((cssClass) => this.setCssClass(trimmedId, cssClass.trim())); + } -/** - * Add a relation between two items. The items may be full objects or just the string id of a state. - * - * @param {string | object} item1 - * @param {string | object} item2 - * @param {string} title - */ -export const addRelation = function (item1, item2, title) { - if (typeof item1 === 'object') { - addRelationObjs(item1, item2, title); - } else { - const id1 = startIdIfNeeded(item1.trim()); - const type1 = startTypeIfNeeded(item1); - const id2 = endIdIfNeeded(item2.trim()); - const type2 = endTypeIfNeeded(item2); + if (styles) { + log.info('Setting state styles', trimmedId, styles); + const stylesList = typeof styles === 'string' ? [styles] : styles; + stylesList.forEach((style) => this.setStyle(trimmedId, style.trim())); + } - addState(id1, type1); - addState(id2, type2); - currentDocument.relations.push({ + if (textStyles) { + log.info('Setting state styles', trimmedId, styles); + const textStylesList = typeof textStyles === 'string' ? [textStyles] : textStyles; + textStylesList.forEach((textStyle) => this.setTextStyle(trimmedId, textStyle.trim())); + } + }; + + clear = (saveCommon) => { + this.#nodes = []; + this.#edges = []; + this.#documents = { + root: this.#newDoc(), + }; + this.#currentDocument = this.#documents.root; + + // number of start and end nodes; used to construct ids + this.#startEndCount = 0; + this.classes = this.#newClassesList(); + if (!saveCommon) { + commonClear(); + } + }; + + getState = (id) => { + return this.#currentDocument.states.get(id); + }; + getStates = () => { + return this.#currentDocument.states; + }; + logDocuments = () => { + log.info('Documents = ', this.#documents); + }; + getRelations = () => { + return this.#currentDocument.relations; + }; + + /** + * If the id is a start node ( [*] ), then return a new id constructed from + * the start node name and the current start node count. + * else return the given id + * + * @param {string} id + * @returns {string} - the id (original or constructed) + */ + #startIdIfNeeded = (id = '') => { + let fixedId = id; + if (id === this.#START_NODE) { + this.#startEndCount++; + fixedId = `${this.#START_TYPE}${this.#startEndCount}`; + } + return fixedId; + }; + + /** + * If the id is a start node ( [*] ), then return the start type ('start') + * else return the given type + * + * @param {string} id + * @param {string} type + * @returns {string} - the type that should be used + */ + #startTypeIfNeeded = (id = '', type = DEFAULT_STATE_TYPE) => { + return id === this.#START_NODE ? this.#START_TYPE : type; + }; + + /** + * If the id is an end node ( [*] ), then return a new id constructed from + * the end node name and the current start_end node count. + * else return the given id + * + * @param {string} id + * @returns {string} - the id (original or constructed) + */ + #endIdIfNeeded = (id = '') => { + let fixedId = id; + if (id === this.#END_NODE) { + this.#startEndCount++; + fixedId = `${this.#END_TYPE}${this.#startEndCount}`; + } + return fixedId; + }; + + /** + * If the id is an end node ( [*] ), then return the end type + * else return the given type + * + * @param {string} id + * @param {string} type + * @returns {string} - the type that should be used + */ + #endTypeIfNeeded = (id = '', type = DEFAULT_STATE_TYPE) => { + return id === this.#END_NODE ? this.#END_TYPE : type; + }; + + /** + * + * @param item1 + * @param item2 + * @param relationTitle + */ + addRelationObjs = (item1, item2, relationTitle) => { + let id1 = this.#startIdIfNeeded(item1.id.trim()); + let type1 = this.#startTypeIfNeeded(item1.id.trim(), item1.type); + let id2 = this.#startIdIfNeeded(item2.id.trim()); + let type2 = this.#startTypeIfNeeded(item2.id.trim(), item2.type); + + this.addState( + id1, + type1, + item1.doc, + item1.description, + item1.note, + item1.classes, + item1.styles, + item1.textStyles + ); + this.addState( + id2, + type2, + item2.doc, + item2.description, + item2.note, + item2.classes, + item2.styles, + item2.textStyles + ); + + this.#currentDocument.relations.push({ id1, id2, - title: common.sanitizeText(title, getConfig()), + relationTitle: common.sanitizeText(relationTitle, getConfig()), }); - } -}; + }; -export const addDescription = function (id, descr) { - const theState = currentDocument.states.get(id); - const _descr = descr.startsWith(':') ? descr.replace(':', '').trim() : descr; - theState.descriptions.push(common.sanitizeText(_descr, getConfig())); -}; + /** + * Add a relation between two items. The items may be full objects or just the string id of a state. + * + * @param {string | object} item1 + * @param {string | object} item2 + * @param {string} title + */ + addRelation = (item1, item2, title) => { + if (typeof item1 === 'object') { + this.addRelationObjs(item1, item2, title); + } else { + const id1 = this.#startIdIfNeeded(item1.trim()); + const type1 = this.#startTypeIfNeeded(item1); + const id2 = this.#endIdIfNeeded(item2.trim()); + const type2 = this.#endTypeIfNeeded(item2); -export const cleanupLabel = function (label) { - if (label.substring(0, 1) === ':') { - return label.substr(2).trim(); - } else { - return label.trim(); - } -}; - -const getDividerId = () => { - dividerCnt++; - return 'divider-id-' + dividerCnt; -}; - -/** - * Called when the parser comes across a (style) class definition - * @example classDef my-style fill:#f96; - * - * @param {string} id - the id of this (style) class - * @param {string | null} styleAttributes - the string with 1 or more style attributes (each separated by a comma) - */ -export const addStyleClass = function (id, styleAttributes = '') { - // create a new style class object with this id - if (!classes.has(id)) { - classes.set(id, { id: id, styles: [], textStyles: [] }); // This is a classDef - } - const foundClass = classes.get(id); - if (styleAttributes !== undefined && styleAttributes !== null) { - styleAttributes.split(STYLECLASS_SEP).forEach((attrib) => { - // remove any trailing ; - const fixedAttrib = attrib.replace(/([^;]*);/, '$1').trim(); - - // replace some style keywords - if (RegExp(COLOR_KEYWORD).exec(attrib)) { - const newStyle1 = fixedAttrib.replace(FILL_KEYWORD, BG_FILL); - const newStyle2 = newStyle1.replace(COLOR_KEYWORD, FILL_KEYWORD); - foundClass.textStyles.push(newStyle2); - } - foundClass.styles.push(fixedAttrib); - }); - } -}; - -/** - * Return all of the style classes - * @returns {{} | any | classes} - */ -export const getClasses = function () { - return classes; -}; - -/** - * Add a (style) class or css class to a state with the given id. - * If the state isn't already in the list of known states, add it. - * Might be called by parser when a style class or CSS class should be applied to a state - * - * @param {string | string[]} itemIds The id or a list of ids of the item(s) to apply the css class to - * @param {string} cssClassName CSS class name - */ -export const setCssClass = function (itemIds, cssClassName) { - itemIds.split(',').forEach(function (id) { - let foundState = getState(id); - if (foundState === undefined) { - const trimmedId = id.trim(); - addState(trimmedId); - foundState = getState(trimmedId); + this.addState(id1, type1); + this.addState(id2, type2); + this.#currentDocument.relations.push({ + id1, + id2, + title: common.sanitizeText(title, getConfig()), + }); } - foundState.classes.push(cssClassName); - }); -}; + }; -/** - * Add a style to a state with the given id. - * @example style stateId fill:#f9f,stroke:#333,stroke-width:4px - * where 'style' is the keyword - * stateId is the id of a state - * the rest of the string is the styleText (all of the attributes to be applied to the state) - * - * @param itemId The id of item to apply the style to - * @param styleText - the text of the attributes for the style - */ -export const setStyle = function (itemId, styleText) { - const item = getState(itemId); - if (item !== undefined) { - item.styles.push(styleText); - } -}; + addDescription = (id, descr) => { + const theState = this.#currentDocument.states.get(id); + const _descr = descr.startsWith(':') ? descr.replace(':', '').trim() : descr; + theState.descriptions.push(common.sanitizeText(_descr, getConfig())); + }; -/** - * Add a text style to a state with the given id - * - * @param itemId The id of item to apply the css class to - * @param cssClassName CSS class name - */ -export const setTextStyle = function (itemId, cssClassName) { - const item = getState(itemId); - if (item !== undefined) { - item.textStyles.push(cssClassName); - } -}; + cleanupLabel = (label) => { + if (label.substring(0, 1) === ':') { + return label.substr(2).trim(); + } else { + return label.trim(); + } + }; -const getDirection = () => direction; -const setDirection = (dir) => { - direction = dir; -}; + getDividerId = () => { + this.#dividerCnt++; + return 'divider-id-' + this.#dividerCnt; + }; -const trimColon = (str) => (str && str[0] === ':' ? str.substr(1).trim() : str.trim()); + /** + * Called when the parser comes across a (style) class definition + * @example classDef my-style fill:#f96; + * + * @param {string} id - the id of this (style) class + * @param {string | null} styleAttributes - the string with 1 or more style attributes (each separated by a comma) + */ + addStyleClass = (id, styleAttributes = '') => { + // create a new style class object with this id + if (!this.#classes.has(id)) { + this.#classes.set(id, { id: id, styles: [], textStyles: [] }); // This is a classDef + } + const foundClass = this.#classes.get(id); + if (styleAttributes !== undefined && styleAttributes !== null) { + styleAttributes.split(this.#STYLECLASS_SEP).forEach((attrib) => { + // remove any trailing ; + const fixedAttrib = attrib.replace(/([^;]*);/, '$1').trim(); -export const getData = () => { - const config = getConfig(); - return { nodes, edges, other: {}, config, direction: getDir(getRootDocV2()) }; -}; + // replace some style keywords + if (RegExp(this.#COLOR_KEYWORD).exec(attrib)) { + const newStyle1 = fixedAttrib.replace(this.#FILL_KEYWORD, this.#BG_FILL); + const newStyle2 = newStyle1.replace(this.#COLOR_KEYWORD, this.#FILL_KEYWORD); + foundClass.textStyles.push(newStyle2); + } + foundClass.styles.push(fixedAttrib); + }); + } + }; -export default { - getConfig: () => getConfig().state, - getData, - addState, - clear, - getState, - getStates, - getRelations, - getClasses, - getDirection, - addRelation, - getDividerId, - setDirection, - cleanupLabel, - lineType, - relationType, - logDocuments, - getRootDoc, - setRootDoc, - getRootDocV2, - extract, - trimColon, - getAccTitle, - setAccTitle, - getAccDescription, - setAccDescription, - addStyleClass, - setCssClass, - addDescription, - setDiagramTitle, - getDiagramTitle, -}; + /** + * Return all of the style classes + * @returns {{} | any | classes} + */ + getClasses = () => { + return this.#classes; + }; + + /** + * Add a (style) class or css class to a state with the given id. + * If the state isn't already in the list of known states, add it. + * Might be called by parser when a style class or CSS class should be applied to a state + * + * @param {string | string[]} itemIds The id or a list of ids of the item(s) to apply the css class to + * @param {string} cssClassName CSS class name + */ + setCssClass = (itemIds, cssClassName) => { + itemIds.split(',').forEach((id) => { + let foundState = this.getState(id); + if (foundState === undefined) { + const trimmedId = id.trim(); + this.addState(trimmedId); + foundState = this.getState(trimmedId); + } + foundState.classes.push(cssClassName); + }); + }; + + /** + * Add a style to a state with the given id. + * @example style stateId fill:#f9f,stroke:#333,stroke-width:4px + * where 'style' is the keyword + * stateId is the id of a state + * the rest of the string is the styleText (all of the attributes to be applied to the state) + * + * @param itemId The id of item to apply the style to + * @param styleText - the text of the attributes for the style + */ + setStyle = (itemId, styleText) => { + const item = this.getState(itemId); + if (item !== undefined) { + item.styles.push(styleText); + } + }; + + /** + * Add a text style to a state with the given id + * + * @param itemId The id of item to apply the css class to + * @param cssClassName CSS class name + */ + setTextStyle = (itemId, cssClassName) => { + const item = this.getState(itemId); + if (item !== undefined) { + item.textStyles.push(cssClassName); + } + }; + + getDirection = () => this.#direction; + setDirection = (dir) => { + this.#direction = dir; + }; + + trimColon = (str) => (str && str[0] === ':' ? str.substr(1).trim() : str.trim()); + + getData = () => { + const config = getConfig(); + return { + nodes: this.#nodes, + edges: this.#edges, + other: {}, + config, + direction: getDir(this.getRootDocV2()), + }; + }; + + getConfig = () => getConfig().state; + getAccTitle = getAccTitle; + setAccTitle = setAccTitle; + getAccDescription = getAccDescription; + setAccDescription = setAccDescription; + setDiagramTitle = setDiagramTitle; + getDiagramTitle = getDiagramTitle; +} diff --git a/packages/mermaid/src/diagrams/state/stateDb.spec.js b/packages/mermaid/src/diagrams/state/stateDb.spec.js index ff0581200..ca7a94861 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDb.spec.js @@ -1,8 +1,9 @@ -import stateDb from './stateDb.js'; +import { StateDb } from './stateDb.js'; describe('State Diagram stateDb', () => { + let stateDb; beforeEach(() => { - stateDb.clear(); + stateDb = new StateDb(); }); describe('addStyleClass', () => { @@ -20,8 +21,9 @@ describe('State Diagram stateDb', () => { }); describe('addDescription to a state', () => { + let stateDb; beforeEach(() => { - stateDb.clear(); + stateDb = new StateDb(); stateDb.addState('state1'); }); @@ -73,3 +75,25 @@ describe('State Diagram stateDb', () => { }); }); }); + +describe('state db class', () => { + let stateDb; + beforeEach(() => { + stateDb = new StateDb(); + }); + // This is to ensure that functions used in flow JISON are exposed as function from FlowDb + it('should have functions used in flow JISON as own property', () => { + const functionsUsedInParser = [ + 'setRootDoc', + 'trimColon', + 'getDividerId', + 'setAccTitle', + 'setAccDescription', + 'setDirection', + ]; + + for (const fun of functionsUsedInParser) { + expect(Object.hasOwn(stateDb, fun)).toBe(true); + } + }); +}); diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js index 53063f41a..b6add2ef7 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js @@ -1,11 +1,13 @@ import { parser } from './parser/stateDiagram.jison'; -import stateDb from './stateDb.js'; +import { StateDb } from './stateDb.js'; import stateDiagram from './parser/stateDiagram.jison'; describe('state diagram V2, ', function () { // TODO - these examples should be put into ./parser/stateDiagram.spec.js describe('when parsing an info graph it', function () { + let stateDb; beforeEach(function () { + stateDb = new StateDb(); parser.yy = stateDb; stateDiagram.parser.yy = stateDb; stateDiagram.parser.yy.clear(); diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts b/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts index a27fc1879..b715db0a2 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts @@ -1,13 +1,15 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; // @ts-ignore: JISON doesn't support types import parser from './parser/stateDiagram.jison'; -import db from './stateDb.js'; +import { StateDb } from './stateDb.js'; import styles from './styles.js'; import renderer from './stateRenderer-v3-unified.js'; export const diagram: DiagramDefinition = { parser, - db, + get db() { + return new StateDb(); + }, renderer, styles, init: (cnf) => { @@ -15,6 +17,5 @@ export const diagram: DiagramDefinition = { cnf.state = {}; } cnf.state.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; - db.clear(); }, }; diff --git a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js index 7fcf4d0a6..e39800ee1 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js @@ -1,9 +1,11 @@ import { parser } from './parser/stateDiagram.jison'; -import stateDb from './stateDb.js'; +import { StateDb } from './stateDb.js'; describe('state diagram, ', function () { describe('when parsing an info graph it', function () { + let stateDb; beforeEach(function () { + stateDb = new StateDb(); parser.yy = stateDb; }); diff --git a/packages/mermaid/src/diagrams/state/stateDiagram.ts b/packages/mermaid/src/diagrams/state/stateDiagram.ts index 643e847ce..e81348caf 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram.ts +++ b/packages/mermaid/src/diagrams/state/stateDiagram.ts @@ -1,13 +1,15 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; // @ts-ignore: JISON doesn't support types import parser from './parser/stateDiagram.jison'; -import db from './stateDb.js'; +import { StateDb } from './stateDb.js'; import styles from './styles.js'; import renderer from './stateRenderer.js'; export const diagram: DiagramDefinition = { parser, - db, + get db() { + return new StateDb(); + }, renderer, styles, init: (cnf) => { @@ -15,6 +17,5 @@ export const diagram: DiagramDefinition = { cnf.state = {}; } cnf.state.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; - db.clear(); }, }; diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index 5bd1b1dfc..27fead969 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import assert from 'node:assert'; // ------------------------------------- // Mocks and mocking @@ -69,6 +70,7 @@ import { compile, serialize } from 'stylis'; import { Diagram } from './Diagram.js'; import { decodeEntities, encodeEntities } from './utils.js'; import { toBase64 } from './utils/base64.js'; +import { StateDb } from './diagrams/state/stateDb.js'; /** * @see https://vitest.dev/guide/mocking.html Mock part of a module @@ -832,5 +834,46 @@ graph TD;A--x|text including URL space|B;`) expect(diagram).toBeInstanceOf(Diagram); expect(diagram.type).toBe('flowchart-v2'); }); + + it('should not modify db when rendering different diagrams', async () => { + const classDiagram1 = await mermaidAPI.getDiagramFromText( + `stateDiagram + direction LR + [*] --> Still + Still --> [*] + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*]` + ); + const classDiagram2 = await mermaidAPI.getDiagramFromText( + `stateDiagram + [*] --> Still + Still --> [*] + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*]` + ); + expect(classDiagram1.db).not.toBe(classDiagram2.db); + assert(classDiagram1.db instanceof StateDb); + assert(classDiagram2.db instanceof StateDb); + expect(classDiagram2.db.getDirection()).not.toEqual(classDiagram2.db.getDirection()); + }); + }); + + // 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); }); }); From 3e32332814c659e7ed1bb73d4a26ed4e61b77d59 Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Tue, 14 Jan 2025 15:22:14 +0530 Subject: [PATCH 2/7] Added changeset. --- .changeset/witty-crews-smell.md | 5 ++++ .../mermaid/src/diagrams/state/stateDb.js | 9 +++--- packages/mermaid/src/mermaidAPI.spec.ts | 29 ++++++++++--------- 3 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 .changeset/witty-crews-smell.md diff --git a/.changeset/witty-crews-smell.md b/.changeset/witty-crews-smell.md new file mode 100644 index 000000000..4213083f2 --- /dev/null +++ b/.changeset/witty-crews-smell.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +`mermaidAPI.getDiagramFromText()` now returns a new different db for each state diagram diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.js index 5250eff3d..64953ce69 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.js +++ b/packages/mermaid/src/diagrams/state/stateDb.js @@ -30,12 +30,15 @@ export class StateDb { #START_TYPE = 'start'; #END_NODE = this.#START_NODE; #END_TYPE = 'end'; - #COLOR_KEYWORD = 'color'; #FILL_KEYWORD = 'fill'; #BG_FILL = 'bgFill'; #STYLECLASS_SEP = ','; + constructor() { + this.clear(); + } + /** * Returns a new list of classes. * In the future, this can be replaced with a class common to all diagrams. @@ -54,10 +57,6 @@ export class StateDb { #rootDoc = []; #classes = this.#newClassesList(); // style classes defined by a classDef - constructor() { - this.clear(); - } - // -------------------------------------- #newDoc = () => { diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index 27fead969..dff69742a 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -838,27 +838,28 @@ graph TD;A--x|text including URL space|B;`) it('should not modify db when rendering different diagrams', async () => { const classDiagram1 = await mermaidAPI.getDiagramFromText( `stateDiagram - direction LR - [*] --> Still - Still --> [*] - Still --> Moving - Moving --> Still - Moving --> Crash - Crash --> [*]` + direction LR + [*] --> Still + Still --> [*] + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*]` ); const classDiagram2 = await mermaidAPI.getDiagramFromText( `stateDiagram - [*] --> Still - Still --> [*] - Still --> Moving - Moving --> Still - Moving --> Crash - Crash --> [*]` + direction TB + [*] --> Still + Still --> [*] + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*]` ); expect(classDiagram1.db).not.toBe(classDiagram2.db); assert(classDiagram1.db instanceof StateDb); assert(classDiagram2.db instanceof StateDb); - expect(classDiagram2.db.getDirection()).not.toEqual(classDiagram2.db.getDirection()); + expect(classDiagram1.db.getDirection()).not.toEqual(classDiagram2.db.getDirection()); }); }); From ef9bb53e670f52dd204be53e7632a8f1e84ef213 Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Tue, 14 Jan 2025 15:32:47 +0530 Subject: [PATCH 3/7] Code refactor --- .../src/diagrams/state/parser/state-parser.spec.js | 4 ++-- .../mermaid/src/diagrams/state/parser/state-style.spec.js | 4 ++-- packages/mermaid/src/diagrams/state/shapes.js | 4 ++-- packages/mermaid/src/diagrams/state/stateDb.js | 2 +- packages/mermaid/src/diagrams/state/stateDb.spec.js | 8 ++++---- .../mermaid/src/diagrams/state/stateDiagram-v2.spec.js | 4 ++-- packages/mermaid/src/diagrams/state/stateDiagram-v2.ts | 4 ++-- packages/mermaid/src/diagrams/state/stateDiagram.spec.js | 4 ++-- packages/mermaid/src/diagrams/state/stateDiagram.ts | 4 ++-- packages/mermaid/src/mermaidAPI.spec.ts | 6 +++--- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js index 6d278d3db..bb5345996 100644 --- a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js +++ b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js @@ -1,4 +1,4 @@ -import { StateDb } from '../stateDb.js'; +import { StateDB } from '../stateDb.js'; import stateDiagram from './stateDiagram.jison'; import { setConfig } from '../../../config.js'; @@ -9,7 +9,7 @@ setConfig({ describe('state parser can parse...', () => { let stateDb; beforeEach(function () { - stateDb = new StateDb(); + stateDb = new StateDB(); stateDiagram.parser.yy = stateDb; stateDiagram.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/state/parser/state-style.spec.js b/packages/mermaid/src/diagrams/state/parser/state-style.spec.js index 012a56a16..c37bed3c7 100644 --- a/packages/mermaid/src/diagrams/state/parser/state-style.spec.js +++ b/packages/mermaid/src/diagrams/state/parser/state-style.spec.js @@ -1,4 +1,4 @@ -import { StateDb } from '../stateDb.js'; +import { StateDB } from '../stateDb.js'; import stateDiagram from './stateDiagram.jison'; import { setConfig } from '../../../config.js'; @@ -9,7 +9,7 @@ setConfig({ describe('ClassDefs and classes when parsing a State diagram', () => { let stateDb; beforeEach(function () { - stateDb = new StateDb(); + stateDb = new StateDB(); stateDiagram.parser.yy = stateDb; stateDiagram.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/state/shapes.js b/packages/mermaid/src/diagrams/state/shapes.js index f096f0cbc..00d0459d6 100644 --- a/packages/mermaid/src/diagrams/state/shapes.js +++ b/packages/mermaid/src/diagrams/state/shapes.js @@ -1,6 +1,6 @@ import { line, curveBasis } from 'd3'; import idCache from './id-cache.js'; -import { StateDb } from './stateDb.js'; +import { StateDB } from './stateDb.js'; import utils from '../../utils.js'; import common from '../common/common.js'; import { getConfig } from '../../diagram-api/diagramAPI.js'; @@ -412,7 +412,7 @@ export const drawState = function (elem, stateDef) { let edgeCount = 0; export const drawEdge = function (elem, path, relation) { - const stateDb = new StateDb(); + const stateDb = new StateDB(); const getRelationType = function (type) { switch (type) { case stateDb.relationType.AGGREGATION: diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.js index 64953ce69..9b8c22ecf 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.js +++ b/packages/mermaid/src/diagrams/state/stateDb.js @@ -25,7 +25,7 @@ import { STMT_STYLEDEF, } from './stateCommon.js'; -export class StateDb { +export class StateDB { #START_NODE = '[*]'; #START_TYPE = 'start'; #END_NODE = this.#START_NODE; diff --git a/packages/mermaid/src/diagrams/state/stateDb.spec.js b/packages/mermaid/src/diagrams/state/stateDb.spec.js index ca7a94861..8ff6f5d84 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDb.spec.js @@ -1,9 +1,9 @@ -import { StateDb } from './stateDb.js'; +import { StateDB } from './stateDb.js'; describe('State Diagram stateDb', () => { let stateDb; beforeEach(() => { - stateDb = new StateDb(); + stateDb = new StateDB(); }); describe('addStyleClass', () => { @@ -23,7 +23,7 @@ describe('State Diagram stateDb', () => { describe('addDescription to a state', () => { let stateDb; beforeEach(() => { - stateDb = new StateDb(); + stateDb = new StateDB(); stateDb.addState('state1'); }); @@ -79,7 +79,7 @@ describe('State Diagram stateDb', () => { describe('state db class', () => { let stateDb; beforeEach(() => { - stateDb = new StateDb(); + stateDb = new StateDB(); }); // This is to ensure that functions used in flow JISON are exposed as function from FlowDb it('should have functions used in flow JISON as own property', () => { diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js index b6add2ef7..d1edc5b40 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js @@ -1,5 +1,5 @@ import { parser } from './parser/stateDiagram.jison'; -import { StateDb } from './stateDb.js'; +import { StateDB } from './stateDb.js'; import stateDiagram from './parser/stateDiagram.jison'; describe('state diagram V2, ', function () { @@ -7,7 +7,7 @@ describe('state diagram V2, ', function () { describe('when parsing an info graph it', function () { let stateDb; beforeEach(function () { - stateDb = new StateDb(); + stateDb = new StateDB(); parser.yy = stateDb; stateDiagram.parser.yy = stateDb; stateDiagram.parser.yy.clear(); diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts b/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts index b715db0a2..f7bc716c6 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts @@ -1,14 +1,14 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; // @ts-ignore: JISON doesn't support types import parser from './parser/stateDiagram.jison'; -import { StateDb } from './stateDb.js'; +import { StateDB } from './stateDb.js'; import styles from './styles.js'; import renderer from './stateRenderer-v3-unified.js'; export const diagram: DiagramDefinition = { parser, get db() { - return new StateDb(); + return new StateDB(); }, renderer, styles, diff --git a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js index e39800ee1..8175ef041 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js @@ -1,11 +1,11 @@ import { parser } from './parser/stateDiagram.jison'; -import { StateDb } from './stateDb.js'; +import { StateDB } from './stateDb.js'; describe('state diagram, ', function () { describe('when parsing an info graph it', function () { let stateDb; beforeEach(function () { - stateDb = new StateDb(); + stateDb = new StateDB(); parser.yy = stateDb; }); diff --git a/packages/mermaid/src/diagrams/state/stateDiagram.ts b/packages/mermaid/src/diagrams/state/stateDiagram.ts index e81348caf..a6f9d7c63 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram.ts +++ b/packages/mermaid/src/diagrams/state/stateDiagram.ts @@ -1,14 +1,14 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; // @ts-ignore: JISON doesn't support types import parser from './parser/stateDiagram.jison'; -import { StateDb } from './stateDb.js'; +import { StateDB } from './stateDb.js'; import styles from './styles.js'; import renderer from './stateRenderer.js'; export const diagram: DiagramDefinition = { parser, get db() { - return new StateDb(); + return new StateDB(); }, renderer, styles, diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index dff69742a..d4d1a0c17 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -70,7 +70,7 @@ import { compile, serialize } from 'stylis'; import { Diagram } from './Diagram.js'; import { decodeEntities, encodeEntities } from './utils.js'; import { toBase64 } from './utils/base64.js'; -import { StateDb } from './diagrams/state/stateDb.js'; +import { StateDB } from './diagrams/state/stateDb.js'; /** * @see https://vitest.dev/guide/mocking.html Mock part of a module @@ -857,8 +857,8 @@ graph TD;A--x|text including URL space|B;`) Crash --> [*]` ); expect(classDiagram1.db).not.toBe(classDiagram2.db); - assert(classDiagram1.db instanceof StateDb); - assert(classDiagram2.db instanceof StateDb); + assert(classDiagram1.db instanceof StateDB); + assert(classDiagram2.db instanceof StateDB); expect(classDiagram1.db.getDirection()).not.toEqual(classDiagram2.db.getDirection()); }); }); From bde653b1c2209c7722dedc3e6452c39fbdef7c0e Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Wed, 22 Jan 2025 15:49:51 +0530 Subject: [PATCH 4/7] Updated as per Alois suggestion --- .../mermaid/src/diagrams/state/stateDb.js | 227 +++++++++++++----- 1 file changed, 165 insertions(+), 62 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.js index 9b8c22ecf..4c0a442a2 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.js +++ b/packages/mermaid/src/diagrams/state/stateDb.js @@ -26,17 +26,55 @@ import { } from './stateCommon.js'; export class StateDB { + /** + * @private + * @type {string} + */ #START_NODE = '[*]'; + /** + * @private + * @type {string} + */ #START_TYPE = 'start'; + /** + * @private + * @type {string} + */ #END_NODE = this.#START_NODE; + /** + * @private + * @type {string} + */ #END_TYPE = 'end'; + /** + * @private + * @type {string} + */ #COLOR_KEYWORD = 'color'; + /** + * @private + * @type {string} + */ #FILL_KEYWORD = 'fill'; + /** + * @private + * @type {string} + */ #BG_FILL = 'bgFill'; + /** + * @private + * @type {string} + */ #STYLECLASS_SEP = ','; constructor() { this.clear(); + + // Needed for JISON since it only supports direct properties + this.setRootDoc = this.setRootDoc.bind(this); + this.getDividerId = this.getDividerId.bind(this); + this.setDirection = this.setDirection.bind(this); + this.trimColon = this.trimColon.bind(this); } /** @@ -45,35 +83,76 @@ export class StateDB { * ClassDef information = { id: id, styles: [], textStyles: [] } * * @returns {Map} + * @private */ - #newClassesList = () => { + #newClassesList() { return new Map(); - }; + } + /** + * @private + * @type {Array} + */ #nodes = []; + /** + * @private + * @type {Array} + */ #edges = []; + /** + * @private + * @type {string} + */ #direction = DEFAULT_DIAGRAM_DIRECTION; + /** + * @private + * @type {Array} + */ #rootDoc = []; + /** + * @private + * @type {Map} + */ #classes = this.#newClassesList(); // style classes defined by a classDef // -------------------------------------- - #newDoc = () => { + /** + * @private + * @returns {Object} + */ + #newDoc() { return { /** @type {{ id1: string, id2: string, relationTitle: string }[]} */ relations: [], states: new Map(), documents: {}, }; - }; + } + /** + * @private + * @type {Object} + */ #documents = { root: this.#newDoc(), }; + /** + * @private + * @type {Object} + */ #currentDocument = this.#documents.root; + /** + * @private + * @type {number} + */ #startEndCount = 0; + /** + * @private + * @type {number} + */ #dividerCnt = 0; lineType = { @@ -88,17 +167,31 @@ export class StateDB { DEPENDENCY: 3, }; - #clone = (o) => JSON.parse(JSON.stringify(o)); + /** + * @private + * @param {Object} o + */ + #clone(o) { + return JSON.parse(JSON.stringify(o)); + } - setRootDoc = (o) => { + setRootDoc(o) { log.info('Setting root doc', o); // rootDoc = { id: 'root', doc: o }; this.#rootDoc = o; - }; + } - getRootDoc = () => this.#rootDoc; + getRootDoc() { + return this.#rootDoc; + } - #docTranslator = (parent, node, first) => { + /** + * @private + * @param {Object} parent + * @param {Object} node + * @param {boolean} first + */ + #docTranslator(parent, node, first) { if (node.stmt === STMT_RELATION) { this.#docTranslator(parent, node.state1, true); this.#docTranslator(parent, node.state2, false); @@ -144,12 +237,12 @@ export class StateDB { node.doc.forEach((docNode) => this.#docTranslator(node, docNode, true)); } } - }; - getRootDocV2 = () => { + } + getRootDocV2() { this.#docTranslator({ id: 'root' }, { id: 'root', doc: this.#rootDoc }, true); return { id: 'root', doc: this.#rootDoc }; // Here - }; + } /** * Convert all of the statements (stmts) that were parsed into states and relationships. @@ -162,7 +255,7 @@ export class StateDB { * * @param _doc */ - extract = (_doc) => { + extract(_doc) { // const res = { states: [], relations: [] }; let doc; if (_doc.doc) { @@ -251,7 +344,7 @@ export class StateDB { node.label = node.label[0]; } }); - }; + } /** * Function called by parser when a node definition has been found. @@ -265,7 +358,7 @@ export class StateDB { * @param {null | string | string[]} styles - styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 style, convert it to an array of that 1 style. * @param {null | string | string[]} textStyles - text styles to apply to this state. Can be a string (1 text test) or an array of text styles. If it's just 1 text style, convert it to an array of that 1 text style. */ - addState = ( + addState( id, type = DEFAULT_STATE_TYPE, doc = null, @@ -274,7 +367,7 @@ export class StateDB { classes = null, styles = null, textStyles = null - ) => { + ) { const trimmedId = id?.trim(); // add the state if needed if (!this.#currentDocument.states.has(trimmedId)) { @@ -332,9 +425,9 @@ export class StateDB { const textStylesList = typeof textStyles === 'string' ? [textStyles] : textStyles; textStylesList.forEach((textStyle) => this.setTextStyle(trimmedId, textStyle.trim())); } - }; + } - clear = (saveCommon) => { + clear(saveCommon) { this.#nodes = []; this.#edges = []; this.#documents = { @@ -348,20 +441,20 @@ export class StateDB { if (!saveCommon) { commonClear(); } - }; + } - getState = (id) => { + getState(id) { return this.#currentDocument.states.get(id); - }; - getStates = () => { + } + getStates() { return this.#currentDocument.states; - }; - logDocuments = () => { + } + logDocuments() { log.info('Documents = ', this.#documents); - }; - getRelations = () => { + } + getRelations() { return this.#currentDocument.relations; - }; + } /** * If the id is a start node ( [*] ), then return a new id constructed from @@ -370,15 +463,16 @@ export class StateDB { * * @param {string} id * @returns {string} - the id (original or constructed) + * @private */ - #startIdIfNeeded = (id = '') => { + #startIdIfNeeded(id = '') { let fixedId = id; if (id === this.#START_NODE) { this.#startEndCount++; fixedId = `${this.#START_TYPE}${this.#startEndCount}`; } return fixedId; - }; + } /** * If the id is a start node ( [*] ), then return the start type ('start') @@ -387,10 +481,11 @@ export class StateDB { * @param {string} id * @param {string} type * @returns {string} - the type that should be used + * @private */ - #startTypeIfNeeded = (id = '', type = DEFAULT_STATE_TYPE) => { + #startTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { return id === this.#START_NODE ? this.#START_TYPE : type; - }; + } /** * If the id is an end node ( [*] ), then return a new id constructed from @@ -399,15 +494,16 @@ export class StateDB { * * @param {string} id * @returns {string} - the id (original or constructed) + * @private */ - #endIdIfNeeded = (id = '') => { + #endIdIfNeeded(id = '') { let fixedId = id; if (id === this.#END_NODE) { this.#startEndCount++; fixedId = `${this.#END_TYPE}${this.#startEndCount}`; } return fixedId; - }; + } /** * If the id is an end node ( [*] ), then return the end type @@ -416,10 +512,11 @@ export class StateDB { * @param {string} id * @param {string} type * @returns {string} - the type that should be used + * @private */ - #endTypeIfNeeded = (id = '', type = DEFAULT_STATE_TYPE) => { + #endTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { return id === this.#END_NODE ? this.#END_TYPE : type; - }; + } /** * @@ -427,7 +524,7 @@ export class StateDB { * @param item2 * @param relationTitle */ - addRelationObjs = (item1, item2, relationTitle) => { + addRelationObjs(item1, item2, relationTitle) { let id1 = this.#startIdIfNeeded(item1.id.trim()); let type1 = this.#startTypeIfNeeded(item1.id.trim(), item1.type); let id2 = this.#startIdIfNeeded(item2.id.trim()); @@ -459,7 +556,7 @@ export class StateDB { id2, relationTitle: common.sanitizeText(relationTitle, getConfig()), }); - }; + } /** * Add a relation between two items. The items may be full objects or just the string id of a state. @@ -468,7 +565,7 @@ export class StateDB { * @param {string | object} item2 * @param {string} title */ - addRelation = (item1, item2, title) => { + addRelation(item1, item2, title) { if (typeof item1 === 'object') { this.addRelationObjs(item1, item2, title); } else { @@ -485,26 +582,26 @@ export class StateDB { title: common.sanitizeText(title, getConfig()), }); } - }; + } - addDescription = (id, descr) => { + addDescription(id, descr) { const theState = this.#currentDocument.states.get(id); const _descr = descr.startsWith(':') ? descr.replace(':', '').trim() : descr; theState.descriptions.push(common.sanitizeText(_descr, getConfig())); - }; + } - cleanupLabel = (label) => { + cleanupLabel(label) { if (label.substring(0, 1) === ':') { return label.substr(2).trim(); } else { return label.trim(); } - }; + } - getDividerId = () => { + getDividerId() { this.#dividerCnt++; return 'divider-id-' + this.#dividerCnt; - }; + } /** * Called when the parser comes across a (style) class definition @@ -513,7 +610,7 @@ export class StateDB { * @param {string} id - the id of this (style) class * @param {string | null} styleAttributes - the string with 1 or more style attributes (each separated by a comma) */ - addStyleClass = (id, styleAttributes = '') => { + addStyleClass(id, styleAttributes = '') { // create a new style class object with this id if (!this.#classes.has(id)) { this.#classes.set(id, { id: id, styles: [], textStyles: [] }); // This is a classDef @@ -533,15 +630,15 @@ export class StateDB { foundClass.styles.push(fixedAttrib); }); } - }; + } /** * Return all of the style classes * @returns {{} | any | classes} */ - getClasses = () => { + getClasses() { return this.#classes; - }; + } /** * Add a (style) class or css class to a state with the given id. @@ -551,7 +648,7 @@ export class StateDB { * @param {string | string[]} itemIds The id or a list of ids of the item(s) to apply the css class to * @param {string} cssClassName CSS class name */ - setCssClass = (itemIds, cssClassName) => { + setCssClass(itemIds, cssClassName) { itemIds.split(',').forEach((id) => { let foundState = this.getState(id); if (foundState === undefined) { @@ -561,7 +658,7 @@ export class StateDB { } foundState.classes.push(cssClassName); }); - }; + } /** * Add a style to a state with the given id. @@ -573,12 +670,12 @@ export class StateDB { * @param itemId The id of item to apply the style to * @param styleText - the text of the attributes for the style */ - setStyle = (itemId, styleText) => { + setStyle(itemId, styleText) { const item = this.getState(itemId); if (item !== undefined) { item.styles.push(styleText); } - }; + } /** * Add a text style to a state with the given id @@ -586,21 +683,25 @@ export class StateDB { * @param itemId The id of item to apply the css class to * @param cssClassName CSS class name */ - setTextStyle = (itemId, cssClassName) => { + setTextStyle(itemId, cssClassName) { const item = this.getState(itemId); if (item !== undefined) { item.textStyles.push(cssClassName); } - }; + } - getDirection = () => this.#direction; - setDirection = (dir) => { + getDirection() { + return this.#direction; + } + setDirection(dir) { this.#direction = dir; - }; + } - trimColon = (str) => (str && str[0] === ':' ? str.substr(1).trim() : str.trim()); + trimColon(str) { + return str && str[0] === ':' ? str.substr(1).trim() : str.trim(); + } - getData = () => { + getData() { const config = getConfig(); return { nodes: this.#nodes, @@ -609,9 +710,11 @@ export class StateDB { config, direction: getDir(this.getRootDocV2()), }; - }; + } - getConfig = () => getConfig().state; + getConfig() { + return getConfig().state; + } getAccTitle = getAccTitle; setAccTitle = setAccTitle; getAccDescription = getAccDescription; From 1c45df4567a5aeca21443bf4b9b8b636d7b7675b Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Wed, 22 Jan 2025 17:53:51 +0530 Subject: [PATCH 5/7] Updated as per PR comments --- .../mermaid/src/diagrams/state/stateDb.js | 279 ++++++++---------- .../src/diagrams/state/stateDb.spec.js | 2 +- packages/mermaid/src/mermaidAPI.spec.ts | 12 +- 3 files changed, 122 insertions(+), 171 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.js index 4c0a442a2..2da0719ca 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.js +++ b/packages/mermaid/src/diagrams/state/stateDb.js @@ -25,48 +25,39 @@ import { STMT_STYLEDEF, } from './stateCommon.js'; -export class StateDB { - /** - * @private - * @type {string} - */ - #START_NODE = '[*]'; - /** - * @private - * @type {string} - */ - #START_TYPE = 'start'; - /** - * @private - * @type {string} - */ - #END_NODE = this.#START_NODE; - /** - * @private - * @type {string} - */ - #END_TYPE = 'end'; - /** - * @private - * @type {string} - */ - #COLOR_KEYWORD = 'color'; - /** - * @private - * @type {string} - */ - #FILL_KEYWORD = 'fill'; - /** - * @private - * @type {string} - */ - #BG_FILL = 'bgFill'; - /** - * @private - * @type {string} - */ - #STYLECLASS_SEP = ','; +const START_NODE = '[*]'; +const START_TYPE = 'start'; +const END_NODE = START_NODE; +const END_TYPE = 'end'; +const COLOR_KEYWORD = 'color'; +const FILL_KEYWORD = 'fill'; +const BG_FILL = 'bgFill'; +const STYLECLASS_SEP = ','; + +/** + * Returns a new list of classes. + * In the future, this can be replaced with a class common to all diagrams. + * ClassDef information = { id: id, styles: [], textStyles: [] } + * + * @returns {Map} + */ +function newClassesList() { + return new Map(); +} + +const newDoc = () => { + return { + /** @type {{ id1: string, id2: string, relationTitle: string }[]} */ + relations: [], + states: new Map(), + documents: {}, + }; +}; + +const clone = (o) => JSON.parse(JSON.stringify(o)); + +export class StateDB { constructor() { this.clear(); @@ -78,111 +69,71 @@ export class StateDB { } /** - * Returns a new list of classes. - * In the future, this can be replaced with a class common to all diagrams. - * ClassDef information = { id: id, styles: [], textStyles: [] } - * - * @returns {Map} * @private + * @type {Array} */ - #newClassesList() { - return new Map(); - } - + nodes = []; /** * @private * @type {Array} */ - #nodes = []; - /** - * @private - * @type {Array} - */ - #edges = []; + edges = []; /** * @private * @type {string} */ - #direction = DEFAULT_DIAGRAM_DIRECTION; + direction = DEFAULT_DIAGRAM_DIRECTION; /** * @private * @type {Array} */ - #rootDoc = []; + rootDoc = []; /** * @private * @type {Map} */ - #classes = this.#newClassesList(); // style classes defined by a classDef - - // -------------------------------------- - - /** - * @private - * @returns {Object} - */ - #newDoc() { - return { - /** @type {{ id1: string, id2: string, relationTitle: string }[]} */ - relations: [], - states: new Map(), - documents: {}, - }; - } + classes = newClassesList(); // style classes defined by a classDef /** * @private * @type {Object} */ - #documents = { - root: this.#newDoc(), + documents = { + root: newDoc(), }; /** * @private * @type {Object} */ - #currentDocument = this.#documents.root; + currentDocument = this.documents.root; /** * @private * @type {number} */ - #startEndCount = 0; + startEndCount = 0; /** * @private * @type {number} */ - #dividerCnt = 0; + dividerCnt = 0; - lineType = { - LINE: 0, - DOTTED_LINE: 1, - }; - - relationType = { + static relationType = { AGGREGATION: 0, EXTENSION: 1, COMPOSITION: 2, DEPENDENCY: 3, }; - /** - * @private - * @param {Object} o - */ - #clone(o) { - return JSON.parse(JSON.stringify(o)); - } - setRootDoc(o) { log.info('Setting root doc', o); // rootDoc = { id: 'root', doc: o }; - this.#rootDoc = o; + this.rootDoc = o; } getRootDoc() { - return this.#rootDoc; + return this.rootDoc; } /** @@ -191,10 +142,10 @@ export class StateDB { * @param {Object} node * @param {boolean} first */ - #docTranslator(parent, node, first) { + docTranslator(parent, node, first) { if (node.stmt === STMT_RELATION) { - this.#docTranslator(parent, node.state1, true); - this.#docTranslator(parent, node.state2, false); + this.docTranslator(parent, node.state1, true); + this.docTranslator(parent, node.state2, false); } else { if (node.stmt === STMT_STATE) { if (node.id === '[*]') { @@ -213,8 +164,8 @@ export class StateDB { let i; for (i = 0; i < node.doc.length; i++) { if (node.doc[i].type === DIVIDER_TYPE) { - const newNode = this.#clone(node.doc[i]); - newNode.doc = this.#clone(currentDoc); + const newNode = clone(node.doc[i]); + newNode.doc = clone(currentDoc); doc.push(newNode); currentDoc = []; } else { @@ -228,19 +179,19 @@ export class StateDB { stmt: STMT_STATE, id: generateId(), type: 'divider', - doc: this.#clone(currentDoc), + doc: clone(currentDoc), }; - doc.push(this.#clone(newNode)); + doc.push(clone(newNode)); node.doc = doc; } - node.doc.forEach((docNode) => this.#docTranslator(node, docNode, true)); + node.doc.forEach((docNode) => this.docTranslator(node, docNode, true)); } } } getRootDocV2() { - this.#docTranslator({ id: 'root' }, { id: 'root', doc: this.#rootDoc }, true); - return { id: 'root', doc: this.#rootDoc }; + this.docTranslator({ id: 'root' }, { id: 'root', doc: this.rootDoc }, true); + return { id: 'root', doc: this.rootDoc }; // Here } @@ -323,13 +274,13 @@ export class StateDB { undefined, this.getRootDocV2(), diagramStates, - this.#nodes, - this.#edges, + this.nodes, + this.edges, true, look, - this.#classes + this.classes ); - this.#nodes.forEach((node) => { + this.nodes.forEach((node) => { if (Array.isArray(node.label)) { // add the rest as description node.description = node.label.slice(1); @@ -370,9 +321,9 @@ export class StateDB { ) { const trimmedId = id?.trim(); // add the state if needed - if (!this.#currentDocument.states.has(trimmedId)) { + if (!this.currentDocument.states.has(trimmedId)) { log.info('Adding state ', trimmedId, descr); - this.#currentDocument.states.set(trimmedId, { + this.currentDocument.states.set(trimmedId, { id: trimmedId, descriptions: [], type, @@ -383,11 +334,11 @@ export class StateDB { textStyles: [], }); } else { - if (!this.#currentDocument.states.get(trimmedId).doc) { - this.#currentDocument.states.get(trimmedId).doc = doc; + if (!this.currentDocument.states.get(trimmedId).doc) { + this.currentDocument.states.get(trimmedId).doc = doc; } - if (!this.#currentDocument.states.get(trimmedId).type) { - this.#currentDocument.states.get(trimmedId).type = type; + if (!this.currentDocument.states.get(trimmedId).type) { + this.currentDocument.states.get(trimmedId).type = type; } } @@ -403,7 +354,7 @@ export class StateDB { } if (note) { - const doc2 = this.#currentDocument.states.get(trimmedId); + const doc2 = this.currentDocument.states.get(trimmedId); doc2.note = note; doc2.note.text = common.sanitizeText(doc2.note.text, getConfig()); } @@ -428,32 +379,32 @@ export class StateDB { } clear(saveCommon) { - this.#nodes = []; - this.#edges = []; - this.#documents = { - root: this.#newDoc(), + this.nodes = []; + this.edges = []; + this.documents = { + root: newDoc(), }; - this.#currentDocument = this.#documents.root; + this.currentDocument = this.documents.root; // number of start and end nodes; used to construct ids - this.#startEndCount = 0; - this.classes = this.#newClassesList(); + this.startEndCount = 0; + this.classes = newClassesList(); if (!saveCommon) { commonClear(); } } getState(id) { - return this.#currentDocument.states.get(id); + return this.currentDocument.states.get(id); } getStates() { - return this.#currentDocument.states; + return this.currentDocument.states; } logDocuments() { - log.info('Documents = ', this.#documents); + log.info('Documents = ', this.documents); } getRelations() { - return this.#currentDocument.relations; + return this.currentDocument.relations; } /** @@ -465,11 +416,11 @@ export class StateDB { * @returns {string} - the id (original or constructed) * @private */ - #startIdIfNeeded(id = '') { + startIdIfNeeded(id = '') { let fixedId = id; - if (id === this.#START_NODE) { - this.#startEndCount++; - fixedId = `${this.#START_TYPE}${this.#startEndCount}`; + if (id === START_NODE) { + this.startEndCount++; + fixedId = `${START_TYPE}${this.startEndCount}`; } return fixedId; } @@ -483,8 +434,8 @@ export class StateDB { * @returns {string} - the type that should be used * @private */ - #startTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { - return id === this.#START_NODE ? this.#START_TYPE : type; + startTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { + return id === START_NODE ? START_TYPE : type; } /** @@ -496,11 +447,11 @@ export class StateDB { * @returns {string} - the id (original or constructed) * @private */ - #endIdIfNeeded(id = '') { + endIdIfNeeded(id = '') { let fixedId = id; - if (id === this.#END_NODE) { - this.#startEndCount++; - fixedId = `${this.#END_TYPE}${this.#startEndCount}`; + if (id === END_NODE) { + this.startEndCount++; + fixedId = `${END_TYPE}${this.startEndCount}`; } return fixedId; } @@ -514,8 +465,8 @@ export class StateDB { * @returns {string} - the type that should be used * @private */ - #endTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { - return id === this.#END_NODE ? this.#END_TYPE : type; + endTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { + return id === END_NODE ? END_TYPE : type; } /** @@ -525,10 +476,10 @@ export class StateDB { * @param relationTitle */ addRelationObjs(item1, item2, relationTitle) { - let id1 = this.#startIdIfNeeded(item1.id.trim()); - let type1 = this.#startTypeIfNeeded(item1.id.trim(), item1.type); - let id2 = this.#startIdIfNeeded(item2.id.trim()); - let type2 = this.#startTypeIfNeeded(item2.id.trim(), item2.type); + let id1 = this.startIdIfNeeded(item1.id.trim()); + let type1 = this.startTypeIfNeeded(item1.id.trim(), item1.type); + let id2 = this.startIdIfNeeded(item2.id.trim()); + let type2 = this.startTypeIfNeeded(item2.id.trim(), item2.type); this.addState( id1, @@ -551,7 +502,7 @@ export class StateDB { item2.textStyles ); - this.#currentDocument.relations.push({ + this.currentDocument.relations.push({ id1, id2, relationTitle: common.sanitizeText(relationTitle, getConfig()), @@ -569,14 +520,14 @@ export class StateDB { if (typeof item1 === 'object') { this.addRelationObjs(item1, item2, title); } else { - const id1 = this.#startIdIfNeeded(item1.trim()); - const type1 = this.#startTypeIfNeeded(item1); - const id2 = this.#endIdIfNeeded(item2.trim()); - const type2 = this.#endTypeIfNeeded(item2); + const id1 = this.startIdIfNeeded(item1.trim()); + const type1 = this.startTypeIfNeeded(item1); + const id2 = this.endIdIfNeeded(item2.trim()); + const type2 = this.endTypeIfNeeded(item2); this.addState(id1, type1); this.addState(id2, type2); - this.#currentDocument.relations.push({ + this.currentDocument.relations.push({ id1, id2, title: common.sanitizeText(title, getConfig()), @@ -585,7 +536,7 @@ export class StateDB { } addDescription(id, descr) { - const theState = this.#currentDocument.states.get(id); + const theState = this.currentDocument.states.get(id); const _descr = descr.startsWith(':') ? descr.replace(':', '').trim() : descr; theState.descriptions.push(common.sanitizeText(_descr, getConfig())); } @@ -599,8 +550,8 @@ export class StateDB { } getDividerId() { - this.#dividerCnt++; - return 'divider-id-' + this.#dividerCnt; + this.dividerCnt++; + return 'divider-id-' + this.dividerCnt; } /** @@ -612,19 +563,19 @@ export class StateDB { */ addStyleClass(id, styleAttributes = '') { // create a new style class object with this id - if (!this.#classes.has(id)) { - this.#classes.set(id, { id: id, styles: [], textStyles: [] }); // This is a classDef + if (!this.classes.has(id)) { + this.classes.set(id, { id: id, styles: [], textStyles: [] }); // This is a classDef } - const foundClass = this.#classes.get(id); + const foundClass = this.classes.get(id); if (styleAttributes !== undefined && styleAttributes !== null) { - styleAttributes.split(this.#STYLECLASS_SEP).forEach((attrib) => { + styleAttributes.split(STYLECLASS_SEP).forEach((attrib) => { // remove any trailing ; const fixedAttrib = attrib.replace(/([^;]*);/, '$1').trim(); // replace some style keywords - if (RegExp(this.#COLOR_KEYWORD).exec(attrib)) { - const newStyle1 = fixedAttrib.replace(this.#FILL_KEYWORD, this.#BG_FILL); - const newStyle2 = newStyle1.replace(this.#COLOR_KEYWORD, this.#FILL_KEYWORD); + if (RegExp(COLOR_KEYWORD).exec(attrib)) { + const newStyle1 = fixedAttrib.replace(FILL_KEYWORD, BG_FILL); + const newStyle2 = newStyle1.replace(COLOR_KEYWORD, FILL_KEYWORD); foundClass.textStyles.push(newStyle2); } foundClass.styles.push(fixedAttrib); @@ -637,7 +588,7 @@ export class StateDB { * @returns {{} | any | classes} */ getClasses() { - return this.#classes; + return this.classes; } /** @@ -691,10 +642,10 @@ export class StateDB { } getDirection() { - return this.#direction; + return this.direction; } setDirection(dir) { - this.#direction = dir; + this.direction = dir; } trimColon(str) { @@ -704,8 +655,8 @@ export class StateDB { getData() { const config = getConfig(); return { - nodes: this.#nodes, - edges: this.#edges, + nodes: this.nodes, + edges: this.edges, other: {}, config, direction: getDir(this.getRootDocV2()), diff --git a/packages/mermaid/src/diagrams/state/stateDb.spec.js b/packages/mermaid/src/diagrams/state/stateDb.spec.js index 8ff6f5d84..73f1a4be9 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDb.spec.js @@ -81,7 +81,7 @@ describe('state db class', () => { beforeEach(() => { stateDb = new StateDB(); }); - // This is to ensure that functions used in flow JISON are exposed as function from FlowDb + // This is to ensure that functions used in state JISON are exposed as function from StateDb it('should have functions used in flow JISON as own property', () => { const functionsUsedInParser = [ 'setRootDoc', diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index d4d1a0c17..f11df1393 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -836,7 +836,7 @@ graph TD;A--x|text including URL space|B;`) }); it('should not modify db when rendering different diagrams', async () => { - const classDiagram1 = await mermaidAPI.getDiagramFromText( + const stateDiagram1 = await mermaidAPI.getDiagramFromText( `stateDiagram direction LR [*] --> Still @@ -846,7 +846,7 @@ graph TD;A--x|text including URL space|B;`) Moving --> Crash Crash --> [*]` ); - const classDiagram2 = await mermaidAPI.getDiagramFromText( + const stateDiagram2 = await mermaidAPI.getDiagramFromText( `stateDiagram direction TB [*] --> Still @@ -856,10 +856,10 @@ graph TD;A--x|text including URL space|B;`) Moving --> Crash Crash --> [*]` ); - expect(classDiagram1.db).not.toBe(classDiagram2.db); - assert(classDiagram1.db instanceof StateDB); - assert(classDiagram2.db instanceof StateDB); - expect(classDiagram1.db.getDirection()).not.toEqual(classDiagram2.db.getDirection()); + expect(stateDiagram1.db).not.toBe(stateDiagram2.db); + assert(stateDiagram1.db instanceof StateDB); + assert(stateDiagram2.db instanceof StateDB); + expect(stateDiagram1.db.getDirection()).not.toEqual(stateDiagram2.db.getDirection()); }); }); From 61f3fc5ede72ab2ffcbe46d97d79a332fcdc52bf Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Wed, 22 Jan 2025 19:17:38 +0530 Subject: [PATCH 6/7] Use static reference for relationType in StateDB --- packages/mermaid/src/diagrams/state/shapes.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/shapes.js b/packages/mermaid/src/diagrams/state/shapes.js index 00d0459d6..b18b4ca0e 100644 --- a/packages/mermaid/src/diagrams/state/shapes.js +++ b/packages/mermaid/src/diagrams/state/shapes.js @@ -412,16 +412,15 @@ export const drawState = function (elem, stateDef) { let edgeCount = 0; export const drawEdge = function (elem, path, relation) { - const stateDb = new StateDB(); const getRelationType = function (type) { switch (type) { - case stateDb.relationType.AGGREGATION: + case StateDB.relationType.AGGREGATION: return 'aggregation'; - case stateDb.relationType.EXTENSION: + case StateDB.relationType.EXTENSION: return 'extension'; - case stateDb.relationType.COMPOSITION: + case StateDB.relationType.COMPOSITION: return 'composition'; - case stateDb.relationType.DEPENDENCY: + case StateDB.relationType.DEPENDENCY: return 'dependency'; } }; @@ -460,7 +459,7 @@ export const drawEdge = function (elem, path, relation) { svgPath.attr( 'marker-end', - 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'End' + ')' + 'url(' + url + '#' + getRelationType(StateDB.relationType.DEPENDENCY) + 'End' + ')' ); if (relation.title !== undefined) { From b07bb9b3ff78768be2a593f0ef94f3acb3b0a6f3 Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Thu, 23 Jan 2025 11:44:06 +0530 Subject: [PATCH 7/7] Minor change --- packages/mermaid/src/mermaidAPI.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index f11df1393..cfe8ea84e 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -1,5 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import assert from 'node:assert'; +import { assert, beforeEach, describe, expect, it, vi } from 'vitest'; // ------------------------------------- // Mocks and mocking