From fcb1de915bad30d011a9ea95387652ea268c1408 Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Mon, 13 Jan 2025 20:30:38 +0530 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 8fba9c12363c2f1ffe8df9335428f24920b049e4 Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Wed, 15 Jan 2025 18:02:04 +0530 Subject: [PATCH 04/12] convert flowDb to class and added test cases. --- .../mermaid/src/diagrams/class/classDb.ts | 1283 ++++++++--------- .../class/classDiagram-styles.spec.js | 4 +- .../src/diagrams/class/classDiagram-v2.ts | 7 +- .../src/diagrams/class/classDiagram.spec.ts | 68 +- .../src/diagrams/class/classDiagram.ts | 7 +- .../src/diagrams/class/parser/class.spec.js | 4 +- packages/mermaid/src/mermaidAPI.spec.ts | 44 + 7 files changed, 748 insertions(+), 669 deletions(-) diff --git a/packages/mermaid/src/diagrams/class/classDb.ts b/packages/mermaid/src/diagrams/class/classDb.ts index 569943736..67e0b30a8 100644 --- a/packages/mermaid/src/diagrams/class/classDb.ts +++ b/packages/mermaid/src/diagrams/class/classDb.ts @@ -24,713 +24,694 @@ import type { Interface, } from './classTypes.js'; import type { Node, Edge } from '../../rendering-util/types.js'; +import type { DiagramDB } from '../../diagram-api/types.js'; const MERMAID_DOM_ID_PREFIX = 'classId-'; - -let relations: ClassRelation[] = []; -let classes = new Map(); -const styleClasses = new Map(); -let notes: ClassNote[] = []; -let interfaces: Interface[] = []; let classCounter = 0; -let namespaces = new Map(); -let namespaceCounter = 0; -let functions: any[] = []; +export class ClassDB implements DiagramDB { + private relations: ClassRelation[] = []; + private classes = new Map(); + private readonly styleClasses = new Map(); + private notes: ClassNote[] = []; + private interfaces: Interface[] = []; + // private static classCounter = 0; + private namespaces = new Map(); + private namespaceCounter = 0; -const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig()); + private functions: any[] = []; -const splitClassNameAndType = function (_id: string) { - const id = common.sanitizeText(_id, getConfig()); - let genericType = ''; - let className = id; + private readonly sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig()); - if (id.indexOf('~') > 0) { - const split = id.split('~'); - className = sanitizeText(split[0]); - genericType = sanitizeText(split[1]); + constructor() { + this.functions.push(this.setupToolTips); + this.clear(); } - return { className: className, type: genericType }; -}; + private readonly splitClassNameAndType = (_id: string) => { + const id = common.sanitizeText(_id, getConfig()); + let genericType = ''; + let className = id; -export const setClassLabel = function (_id: string, label: string) { - const id = common.sanitizeText(_id, getConfig()); - if (label) { - label = sanitizeText(label); - } + if (id.indexOf('~') > 0) { + const split = id.split('~'); + className = this.sanitizeText(split[0]); + genericType = this.sanitizeText(split[1]); + } - const { className } = splitClassNameAndType(id); - classes.get(className)!.label = label; - classes.get(className)!.text = - `${label}${classes.get(className)!.type ? `<${classes.get(className)!.type}>` : ''}`; -}; - -/** - * Function called by parser when a node definition has been found. - * - * @param id - Id of the class to add - * @public - */ -export const addClass = function (_id: string) { - const id = common.sanitizeText(_id, getConfig()); - const { className, type } = splitClassNameAndType(id); - // Only add class if not exists - if (classes.has(className)) { - return; - } - // alert('Adding class: ' + className); - const name = common.sanitizeText(className, getConfig()); - // alert('Adding class after: ' + name); - classes.set(name, { - id: name, - type: type, - label: name, - text: `${name}${type ? `<${type}>` : ''}`, - shape: 'classBox', - cssClasses: 'default', - methods: [], - members: [], - annotations: [], - styles: [], - domId: MERMAID_DOM_ID_PREFIX + name + '-' + classCounter, - } as ClassNode); - - classCounter++; -}; - -const addInterface = function (label: string, classId: string) { - const classInterface: Interface = { - id: `interface${interfaces.length}`, - label, - classId, + return { className: className, type: genericType }; }; - interfaces.push(classInterface); -}; - -/** - * Function to lookup domId from id in the graph definition. - * - * @param id - class ID to lookup - * @public - */ -export const lookUpDomId = function (_id: string): string { - const id = common.sanitizeText(_id, getConfig()); - if (classes.has(id)) { - return classes.get(id)!.domId; - } - throw new Error('Class not found: ' + id); -}; - -export const clear = function () { - relations = []; - classes = new Map(); - notes = []; - interfaces = []; - functions = []; - functions.push(setupToolTips); - namespaces = new Map(); - namespaceCounter = 0; - direction = 'TB'; - commonClear(); -}; - -export const getClass = function (id: string): ClassNode { - return classes.get(id)!; -}; - -export const getClasses = function (): ClassMap { - return classes; -}; - -export const getRelations = function (): ClassRelation[] { - return relations; -}; - -export const getNotes = function () { - return notes; -}; - -export const addRelation = function (classRelation: ClassRelation) { - log.debug('Adding relation: ' + JSON.stringify(classRelation)); - // Due to relationType cannot just check if it is equal to 'none' or it complains, can fix this later - const invalidTypes = [ - relationType.LOLLIPOP, - relationType.AGGREGATION, - relationType.COMPOSITION, - relationType.DEPENDENCY, - relationType.EXTENSION, - ]; - - if ( - classRelation.relation.type1 === relationType.LOLLIPOP && - !invalidTypes.includes(classRelation.relation.type2) - ) { - addClass(classRelation.id2); - addInterface(classRelation.id1, classRelation.id2); - classRelation.id1 = `interface${interfaces.length - 1}`; - } else if ( - classRelation.relation.type2 === relationType.LOLLIPOP && - !invalidTypes.includes(classRelation.relation.type1) - ) { - addClass(classRelation.id1); - addInterface(classRelation.id2, classRelation.id1); - classRelation.id2 = `interface${interfaces.length - 1}`; - } else { - addClass(classRelation.id1); - addClass(classRelation.id2); - } - - classRelation.id1 = splitClassNameAndType(classRelation.id1).className; - classRelation.id2 = splitClassNameAndType(classRelation.id2).className; - - classRelation.relationTitle1 = common.sanitizeText( - classRelation.relationTitle1.trim(), - getConfig() - ); - - classRelation.relationTitle2 = common.sanitizeText( - classRelation.relationTitle2.trim(), - getConfig() - ); - - relations.push(classRelation); -}; - -/** - * Adds an annotation to the specified class Annotations mark special properties of the given type - * (like 'interface' or 'service') - * - * @param className - The class name - * @param annotation - The name of the annotation without any brackets - * @public - */ -export const addAnnotation = function (className: string, annotation: string) { - const validatedClassName = splitClassNameAndType(className).className; - classes.get(validatedClassName)!.annotations.push(annotation); -}; - -/** - * Adds a member to the specified class - * - * @param className - The class name - * @param member - The full name of the member. If the member is enclosed in `<>` it is - * treated as an annotation If the member is ending with a closing bracket ) it is treated as a - * method Otherwise the member will be treated as a normal property - * @public - */ -export const addMember = function (className: string, member: string) { - addClass(className); - - const validatedClassName = splitClassNameAndType(className).className; - const theClass = classes.get(validatedClassName)!; - - if (typeof member === 'string') { - // Member can contain white spaces, we trim them out - const memberString = member.trim(); - - if (memberString.startsWith('<<') && memberString.endsWith('>>')) { - // its an annotation - theClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2))); - } else if (memberString.indexOf(')') > 0) { - //its a method - theClass.methods.push(new ClassMember(memberString, 'method')); - } else if (memberString) { - theClass.members.push(new ClassMember(memberString, 'attribute')); + public setClassLabel = (_id: string, label: string) => { + const id = common.sanitizeText(_id, getConfig()); + if (label) { + label = this.sanitizeText(label); } - } -}; -export const addMembers = function (className: string, members: string[]) { - if (Array.isArray(members)) { - members.reverse(); - members.forEach((member) => addMember(className, member)); - } -}; - -export const addNote = function (text: string, className: string) { - const note = { - id: `note${notes.length}`, - class: className, - text: text, + const { className } = this.splitClassNameAndType(id); + this.classes.get(className)!.label = label; + this.classes.get(className)!.text = + `${label}${this.classes.get(className)!.type ? `<${this.classes.get(className)!.type}>` : ''}`; }; - notes.push(note); -}; -export const cleanupLabel = function (label: string) { - if (label.startsWith(':')) { - label = label.substring(1); - } - return sanitizeText(label.trim()); -}; - -/** - * Called by parser when assigning cssClass to a class - * - * @param ids - Comma separated list of ids - * @param className - Class to add - */ -export const setCssClass = function (ids: string, className: string) { - ids.split(',').forEach(function (_id) { - let id = _id; - if (/\d/.exec(_id[0])) { - id = MERMAID_DOM_ID_PREFIX + id; + /** + * Function called by parser when a node definition has been found. + * + * @param id - Id of the class to add + * @public + */ + public addClass = (_id: string) => { + const id = common.sanitizeText(_id, getConfig()); + const { className, type } = this.splitClassNameAndType(id); + // Only add class if not exists + if (this.classes.has(className)) { + return; } - const classNode = classes.get(id); - if (classNode) { - classNode.cssClasses += ' ' + className; - } - }); -}; + // alert('Adding class: ' + className); + const name = common.sanitizeText(className, getConfig()); + // alert('Adding class after: ' + name); + this.classes.set(name, { + id: name, + type: type, + label: name, + text: `${name}${type ? `<${type}>` : ''}`, + shape: 'classBox', + cssClasses: 'default', + methods: [], + members: [], + annotations: [], + styles: [], + domId: MERMAID_DOM_ID_PREFIX + name + '-' + classCounter, + } as ClassNode); -export const defineClass = function (ids: string[], style: string[]) { - for (const id of ids) { - let styleClass = styleClasses.get(id); - if (styleClass === undefined) { - styleClass = { id, styles: [], textStyles: [] }; - styleClasses.set(id, styleClass); + classCounter++; + }; + + private readonly addInterface = (label: string, classId: string) => { + const classInterface: Interface = { + id: `interface${this.interfaces.length}`, + label, + classId, + }; + + this.interfaces.push(classInterface); + }; + + /** + * Function to lookup domId from id in the graph definition. + * + * @param id - class ID to lookup + * @public + */ + public lookUpDomId = (_id: string): string => { + const id = common.sanitizeText(_id, getConfig()); + if (this.classes.has(id)) { + return this.classes.get(id)!.domId; + } + throw new Error('Class not found: ' + id); + }; + + public clear = () => { + this.relations = []; + this.classes = new Map(); + this.notes = []; + this.interfaces = []; + this.functions = []; + this.functions.push(this.setupToolTips); + this.namespaces = new Map(); + this.namespaceCounter = 0; + this.direction = 'TB'; + commonClear(); + }; + + public getClass = (id: string): ClassNode => { + return this.classes.get(id)!; + }; + + public getClasses = (): ClassMap => { + return this.classes; + }; + + public getRelations = (): ClassRelation[] => { + return this.relations; + }; + + public getNotes = () => { + return this.notes; + }; + + public addRelation = (classRelation: ClassRelation) => { + log.debug('Adding relation: ' + JSON.stringify(classRelation)); + // Due to relationType cannot just check if it is equal to 'none' or it complains, can fix this later + const invalidTypes = [ + this.relationType.LOLLIPOP, + this.relationType.AGGREGATION, + this.relationType.COMPOSITION, + this.relationType.DEPENDENCY, + this.relationType.EXTENSION, + ]; + + if ( + classRelation.relation.type1 === this.relationType.LOLLIPOP && + !invalidTypes.includes(classRelation.relation.type2) + ) { + this.addClass(classRelation.id2); + this.addInterface(classRelation.id1, classRelation.id2); + classRelation.id1 = `interface${this.interfaces.length - 1}`; + } else if ( + classRelation.relation.type2 === this.relationType.LOLLIPOP && + !invalidTypes.includes(classRelation.relation.type1) + ) { + this.addClass(classRelation.id1); + this.addInterface(classRelation.id2, classRelation.id1); + classRelation.id2 = `interface${this.interfaces.length - 1}`; + } else { + this.addClass(classRelation.id1); + this.addClass(classRelation.id2); } - if (style) { - style.forEach(function (s) { - if (/color/.exec(s)) { - const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill'); - styleClass.textStyles.push(newStyle); + classRelation.id1 = this.splitClassNameAndType(classRelation.id1).className; + classRelation.id2 = this.splitClassNameAndType(classRelation.id2).className; + + classRelation.relationTitle1 = common.sanitizeText( + classRelation.relationTitle1.trim(), + getConfig() + ); + + classRelation.relationTitle2 = common.sanitizeText( + classRelation.relationTitle2.trim(), + getConfig() + ); + + this.relations.push(classRelation); + }; + + /** + * Adds an annotation to the specified class Annotations mark special properties of the given type + * (like 'interface' or 'service') + * + * @param className - The class name + * @param annotation - The name of the annotation without any brackets + * @public + */ + public addAnnotation = (className: string, annotation: string) => { + const validatedClassName = this.splitClassNameAndType(className).className; + this.classes.get(validatedClassName)!.annotations.push(annotation); + }; + + /** + * Adds a member to the specified class + * + * @param className - The class name + * @param member - The full name of the member. If the member is enclosed in `<>` it is + * treated as an annotation If the member is ending with a closing bracket ) it is treated as a + * method Otherwise the member will be treated as a normal property + * @public + */ + public addMember = (className: string, member: string) => { + this.addClass(className); + + const validatedClassName = this.splitClassNameAndType(className).className; + const theClass = this.classes.get(validatedClassName)!; + + if (typeof member === 'string') { + // Member can contain white spaces, we trim them out + const memberString = member.trim(); + + if (memberString.startsWith('<<') && memberString.endsWith('>>')) { + // its an annotation + theClass.annotations.push( + this.sanitizeText(memberString.substring(2, memberString.length - 2)) + ); + } else if (memberString.indexOf(')') > 0) { + //its a method + theClass.methods.push(new ClassMember(memberString, 'method')); + } else if (memberString) { + theClass.members.push(new ClassMember(memberString, 'attribute')); + } + } + }; + + public addMembers = (className: string, members: string[]) => { + if (Array.isArray(members)) { + members.reverse(); + members.forEach((member) => this.addMember(className, member)); + } + }; + + public addNote = (text: string, className: string) => { + const note = { + id: `note${this.notes.length}`, + class: className, + text: text, + }; + this.notes.push(note); + }; + + public cleanupLabel = (label: string) => { + if (label.startsWith(':')) { + label = label.substring(1); + } + return this.sanitizeText(label.trim()); + }; + + /** + * Called by parser when assigning cssClass to a class + * + * @param ids - Comma separated list of ids + * @param className - Class to add + */ + public setCssClass = (ids: string, className: string) => { + ids.split(',').forEach((_id) => { + let id = _id; + if (/\d/.exec(_id[0])) { + id = MERMAID_DOM_ID_PREFIX + id; + } + const classNode = this.classes.get(id); + if (classNode) { + classNode.cssClasses += ' ' + className; + } + }); + }; + + public defineClass = (ids: string[], style: string[]) => { + for (const id of ids) { + let styleClass = this.styleClasses.get(id); + if (styleClass === undefined) { + styleClass = { id, styles: [], textStyles: [] }; + this.styleClasses.set(id, styleClass); + } + + if (style) { + style.forEach((s) => { + if (/color/.exec(s)) { + const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill'); + styleClass.textStyles.push(newStyle); + } + styleClass.styles.push(s); + }); + } + + this.classes.forEach((value) => { + if (value.cssClasses.includes(id)) { + value.styles.push(...style.flatMap((s) => s.split(','))); } - styleClass.styles.push(s); }); } + }; - classes.forEach((value) => { - if (value.cssClasses.includes(id)) { - value.styles.push(...style.flatMap((s) => s.split(','))); + /** + * Called by parser when a tooltip is found, e.g. a clickable element. + * + * @param ids - Comma separated list of ids + * @param tooltip - Tooltip to add + */ + public setTooltip = (ids: string, tooltip?: string) => { + ids.split(',').forEach((id) => { + if (tooltip !== undefined) { + this.classes.get(id)!.tooltip = this.sanitizeText(tooltip); } }); - } -}; + }; -/** - * Called by parser when a tooltip is found, e.g. a clickable element. - * - * @param ids - Comma separated list of ids - * @param tooltip - Tooltip to add - */ -const setTooltip = function (ids: string, tooltip?: string) { - ids.split(',').forEach(function (id) { - if (tooltip !== undefined) { - classes.get(id)!.tooltip = sanitizeText(tooltip); + public getTooltip = (id: string, namespace?: string) => { + if (namespace && this.namespaces.has(namespace)) { + return this.namespaces.get(namespace)!.classes.get(id)!.tooltip; } - }); -}; -export const getTooltip = function (id: string, namespace?: string) { - if (namespace && namespaces.has(namespace)) { - return namespaces.get(namespace)!.classes.get(id)!.tooltip; - } + return this.classes.get(id)!.tooltip; + }; - return classes.get(id)!.tooltip; -}; - -/** - * Called by parser when a link is found. Adds the URL to the vertex data. - * - * @param ids - Comma separated list of ids - * @param linkStr - URL to create a link for - * @param target - Target of the link, _blank by default as originally defined in the svgDraw.js file - */ -export const setLink = function (ids: string, linkStr: string, target: string) { - const config = getConfig(); - ids.split(',').forEach(function (_id) { - let id = _id; - if (/\d/.exec(_id[0])) { - id = MERMAID_DOM_ID_PREFIX + id; - } - const theClass = classes.get(id); - if (theClass) { - theClass.link = utils.formatUrl(linkStr, config); - if (config.securityLevel === 'sandbox') { - theClass.linkTarget = '_top'; - } else if (typeof target === 'string') { - theClass.linkTarget = sanitizeText(target); - } else { - theClass.linkTarget = '_blank'; + /** + * Called by parser when a link is found. Adds the URL to the vertex data. + * + * @param ids - Comma separated list of ids + * @param linkStr - URL to create a link for + * @param target - Target of the link, _blank by default as originally defined in the svgDraw.js file + */ + public setLink = (ids: string, linkStr: string, target: string) => { + const config = getConfig(); + ids.split(',').forEach((_id) => { + let id = _id; + if (/\d/.exec(_id[0])) { + id = MERMAID_DOM_ID_PREFIX + id; } - } - }); - setCssClass(ids, 'clickable'); -}; - -/** - * Called by parser when a click definition is found. Registers an event handler. - * - * @param ids - Comma separated list of ids - * @param functionName - Function to be called on click - * @param functionArgs - Function args the function should be called with - */ -export const setClickEvent = function (ids: string, functionName: string, functionArgs: string) { - ids.split(',').forEach(function (id) { - setClickFunc(id, functionName, functionArgs); - classes.get(id)!.haveCallback = true; - }); - setCssClass(ids, 'clickable'); -}; - -const setClickFunc = function (_domId: string, functionName: string, functionArgs: string) { - const domId = common.sanitizeText(_domId, getConfig()); - const config = getConfig(); - if (config.securityLevel !== 'loose') { - return; - } - if (functionName === undefined) { - return; - } - - const id = domId; - if (classes.has(id)) { - const elemId = lookUpDomId(id); - let argList: string[] = []; - if (typeof functionArgs === 'string') { - /* Splits functionArgs by ',', ignoring all ',' in double quoted strings */ - argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/); - for (let i = 0; i < argList.length; i++) { - let item = argList[i].trim(); - /* Removes all double quotes at the start and end of an argument */ - /* This preserves all starting and ending whitespace inside */ - if (item.startsWith('"') && item.endsWith('"')) { - item = item.substr(1, item.length - 2); + const theClass = this.classes.get(id); + if (theClass) { + theClass.link = utils.formatUrl(linkStr, config); + if (config.securityLevel === 'sandbox') { + theClass.linkTarget = '_top'; + } else if (typeof target === 'string') { + theClass.linkTarget = this.sanitizeText(target); + } else { + theClass.linkTarget = '_blank'; } - argList[i] = item; - } - } - - /* if no arguments passed into callback, default to passing in id */ - if (argList.length === 0) { - argList.push(elemId); - } - - functions.push(function () { - const elem = document.querySelector(`[id="${elemId}"]`); - if (elem !== null) { - elem.addEventListener( - 'click', - function () { - utils.runFunc(functionName, ...argList); - }, - false - ); } }); - } -}; + this.setCssClass(ids, 'clickable'); + }; -export const bindFunctions = function (element: Element) { - functions.forEach(function (fun) { - fun(element); - }); -}; - -export const lineType = { - LINE: 0, - DOTTED_LINE: 1, -}; - -export const relationType = { - AGGREGATION: 0, - EXTENSION: 1, - COMPOSITION: 2, - DEPENDENCY: 3, - LOLLIPOP: 4, -}; - -const setupToolTips = function (element: Element) { - let tooltipElem: Selection = - select('.mermaidTooltip'); - // @ts-expect-error - Incorrect types - if ((tooltipElem._groups || tooltipElem)[0][0] === null) { - tooltipElem = select('body').append('div').attr('class', 'mermaidTooltip').style('opacity', 0); - } - - const svg = select(element).select('svg'); - - const nodes = svg.selectAll('g.node'); - nodes - .on('mouseover', function () { - const el = select(this); - const title = el.attr('title'); - // Don't try to draw a tooltip if no data is provided - if (title === null) { - return; - } - // @ts-ignore - getBoundingClientRect is not part of the d3 type definition - const rect = this.getBoundingClientRect(); - - tooltipElem.transition().duration(200).style('opacity', '.9'); - tooltipElem - .text(el.attr('title')) - .style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px') - .style('top', window.scrollY + rect.top - 14 + document.body.scrollTop + 'px'); - tooltipElem.html(tooltipElem.html().replace(/<br\/>/g, '
')); - el.classed('hover', true); - }) - .on('mouseout', function () { - tooltipElem.transition().duration(500).style('opacity', 0); - const el = select(this); - el.classed('hover', false); + /** + * Called by parser when a click definition is found. Registers an event handler. + * + * @param ids - Comma separated list of ids + * @param functionName - Function to be called on click + * @param functionArgs - Function args the function should be called with + */ + public setClickEvent = (ids: string, functionName: string, functionArgs: string) => { + ids.split(',').forEach((id) => { + this.setClickFunc(id, functionName, functionArgs); + this.classes.get(id)!.haveCallback = true; }); -}; -functions.push(setupToolTips); + this.setCssClass(ids, 'clickable'); + }; -let direction = 'TB'; -const getDirection = () => direction; -const setDirection = (dir: string) => { - direction = dir; -}; - -/** - * Function called by parser when a namespace definition has been found. - * - * @param id - Id of the namespace to add - * @public - */ -export const addNamespace = function (id: string) { - if (namespaces.has(id)) { - return; - } - - namespaces.set(id, { - id: id, - classes: new Map(), - children: {}, - domId: MERMAID_DOM_ID_PREFIX + id + '-' + namespaceCounter, - } as NamespaceNode); - - namespaceCounter++; -}; - -const getNamespace = function (name: string): NamespaceNode { - return namespaces.get(name)!; -}; - -const getNamespaces = function (): NamespaceMap { - return namespaces; -}; - -/** - * Function called by parser when a namespace definition has been found. - * - * @param id - Id of the namespace to add - * @param classNames - Ids of the class to add - * @public - */ -export const addClassesToNamespace = function (id: string, classNames: string[]) { - if (!namespaces.has(id)) { - return; - } - for (const name of classNames) { - const { className } = splitClassNameAndType(name); - classes.get(className)!.parent = id; - namespaces.get(id)!.classes.set(className, classes.get(className)!); - } -}; - -export const setCssStyle = function (id: string, styles: string[]) { - const thisClass = classes.get(id); - if (!styles || !thisClass) { - return; - } - for (const s of styles) { - if (s.includes(',')) { - thisClass.styles.push(...s.split(',')); - } else { - thisClass.styles.push(s); + private readonly setClickFunc = (_domId: string, functionName: string, functionArgs: string) => { + const domId = common.sanitizeText(_domId, getConfig()); + const config = getConfig(); + if (config.securityLevel !== 'loose') { + return; + } + if (functionName === undefined) { + return; } - } -}; -/** - * Gets the arrow marker for a type index - * - * @param type - The type to look for - * @returns The arrow marker - */ -function getArrowMarker(type: number) { - let marker; - switch (type) { - case 0: - marker = 'aggregation'; - break; - case 1: - marker = 'extension'; - break; - case 2: - marker = 'composition'; - break; - case 3: - marker = 'dependency'; - break; - case 4: - marker = 'lollipop'; - break; - default: - marker = 'none'; - } - return marker; -} + const id = domId; + if (this.classes.has(id)) { + const elemId = this.lookUpDomId(id); + let argList: string[] = []; + if (typeof functionArgs === 'string') { + /* Splits functionArgs by ',', ignoring all ',' in double quoted strings */ + argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/); + for (let i = 0; i < argList.length; i++) { + let item = argList[i].trim(); + /* Removes all double quotes at the start and end of an argument */ + /* This preserves all starting and ending whitespace inside */ + if (item.startsWith('"') && item.endsWith('"')) { + item = item.substr(1, item.length - 2); + } + argList[i] = item; + } + } -export const getData = () => { - const nodes: Node[] = []; - const edges: Edge[] = []; - const config = getConfig(); + /* if no arguments passed into callback, default to passing in id */ + if (argList.length === 0) { + argList.push(elemId); + } - for (const namespaceKey of namespaces.keys()) { - const namespace = namespaces.get(namespaceKey); - if (namespace) { - const node: Node = { - id: namespace.id, - label: namespace.id, - isGroup: true, - padding: config.class!.padding ?? 16, - // parent node must be one of [rect, roundedWithTitle, noteGroup, divider] - shape: 'rect', - cssStyles: ['fill: none', 'stroke: black'], + this.functions.push(() => { + const elem = document.querySelector(`[id="${elemId}"]`); + if (elem !== null) { + elem.addEventListener( + 'click', + () => { + utils.runFunc(functionName, ...argList); + }, + false + ); + } + }); + } + }; + + public bindFunctions = (element: Element) => { + this.functions.forEach((fun) => { + fun(element); + }); + }; + + public lineType = { + LINE: 0, + DOTTED_LINE: 1, + }; + + public relationType = { + AGGREGATION: 0, + EXTENSION: 1, + COMPOSITION: 2, + DEPENDENCY: 3, + LOLLIPOP: 4, + }; + + private readonly setupToolTips = (element: Element) => { + let tooltipElem: Selection = + select('.mermaidTooltip'); + // @ts-expect-error - Incorrect types + if ((tooltipElem._groups || tooltipElem)[0][0] === null) { + tooltipElem = select('body') + .append('div') + .attr('class', 'mermaidTooltip') + .style('opacity', 0); + } + + const svg = select(element).select('svg'); + + const nodes = svg.selectAll('g.node'); + nodes + .on('mouseover', (event: MouseEvent) => { + const el = select(event.currentTarget as HTMLElement); + const title = el.attr('title'); + // Don't try to draw a tooltip if no data is provided + if (title === null) { + return; + } + // @ts-ignore - getBoundingClientRect is not part of the d3 type definition + const rect = this.getBoundingClientRect(); + + tooltipElem.transition().duration(200).style('opacity', '.9'); + tooltipElem + .text(el.attr('title')) + .style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px') + .style('top', window.scrollY + rect.top - 14 + document.body.scrollTop + 'px'); + tooltipElem.html(tooltipElem.html().replace(/<br\/>/g, '
')); + el.classed('hover', true); + }) + .on('mouseout', (event: MouseEvent) => { + tooltipElem.transition().duration(500).style('opacity', 0); + const el = select(event.currentTarget as HTMLElement); + el.classed('hover', false); + }); + }; + + private direction = 'TB'; + public getDirection = () => this.direction; + public setDirection = (dir: string) => { + this.direction = dir; + }; + + /** + * Function called by parser when a namespace definition has been found. + * + * @param id - Id of the namespace to add + * @public + */ + public addNamespace = (id: string) => { + if (this.namespaces.has(id)) { + return; + } + + this.namespaces.set(id, { + id: id, + classes: new Map(), + children: {}, + domId: MERMAID_DOM_ID_PREFIX + id + '-' + this.namespaceCounter, + } as NamespaceNode); + + this.namespaceCounter++; + }; + + public getNamespace = (name: string): NamespaceNode => { + return this.namespaces.get(name)!; + }; + + public getNamespaces = (): NamespaceMap => { + return this.namespaces; + }; + + /** + * Function called by parser when a namespace definition has been found. + * + * @param id - Id of the namespace to add + * @param classNames - Ids of the class to add + * @public + */ + public addClassesToNamespace = (id: string, classNames: string[]) => { + if (!this.namespaces.has(id)) { + return; + } + for (const name of classNames) { + const { className } = this.splitClassNameAndType(name); + this.classes.get(className)!.parent = id; + this.namespaces.get(id)!.classes.set(className, this.classes.get(className)!); + } + }; + + public setCssStyle = (id: string, styles: string[]) => { + const thisClass = this.classes.get(id); + if (!styles || !thisClass) { + return; + } + for (const s of styles) { + if (s.includes(',')) { + thisClass.styles.push(...s.split(',')); + } else { + thisClass.styles.push(s); + } + } + }; + + /** + * Gets the arrow marker for a type index + * + * @param type - The type to look for + * @returns The arrow marker + */ + private readonly getArrowMarker = (type: number) => { + let marker; + switch (type) { + case 0: + marker = 'aggregation'; + break; + case 1: + marker = 'extension'; + break; + case 2: + marker = 'composition'; + break; + case 3: + marker = 'dependency'; + break; + case 4: + marker = 'lollipop'; + break; + default: + marker = 'none'; + } + return marker; + }; + + public getData = () => { + const nodes: Node[] = []; + const edges: Edge[] = []; + const config = getConfig(); + + for (const namespaceKey of this.namespaces.keys()) { + const namespace = this.namespaces.get(namespaceKey); + if (namespace) { + const node: Node = { + id: namespace.id, + label: namespace.id, + isGroup: true, + padding: config.class!.padding ?? 16, + // parent node must be one of [rect, roundedWithTitle, noteGroup, divider] + shape: 'rect', + cssStyles: ['fill: none', 'stroke: black'], + look: config.look, + }; + nodes.push(node); + } + } + + for (const classKey of this.classes.keys()) { + const classNode = this.classes.get(classKey); + if (classNode) { + const node = classNode as unknown as Node; + node.parentId = classNode.parent; + node.look = config.look; + nodes.push(node); + } + } + + let cnt = 0; + for (const note of this.notes) { + cnt++; + const noteNode: Node = { + id: note.id, + label: note.text, + isGroup: false, + shape: 'note', + padding: config.class!.padding ?? 6, + cssStyles: [ + 'text-align: left', + 'white-space: nowrap', + `fill: ${config.themeVariables.noteBkgColor}`, + `stroke: ${config.themeVariables.noteBorderColor}`, + ], look: config.look, }; - nodes.push(node); + nodes.push(noteNode); + + const noteClassId = this.classes.get(note.class)?.id ?? ''; + + if (noteClassId) { + const edge: Edge = { + id: `edgeNote${cnt}`, + start: note.id, + end: noteClassId, + type: 'normal', + thickness: 'normal', + classes: 'relation', + arrowTypeStart: 'none', + arrowTypeEnd: 'none', + arrowheadStyle: '', + labelStyle: [''], + style: ['fill: none'], + pattern: 'dotted', + look: config.look, + }; + edges.push(edge); + } } - } - for (const classKey of classes.keys()) { - const classNode = classes.get(classKey); - if (classNode) { - const node = classNode as unknown as Node; - node.parentId = classNode.parent; - node.look = config.look; - nodes.push(node); + for (const _interface of this.interfaces) { + const interfaceNode: Node = { + id: _interface.id, + label: _interface.label, + isGroup: false, + shape: 'rect', + cssStyles: ['opacity: 0;'], + look: config.look, + }; + nodes.push(interfaceNode); } - } - let cnt = 0; - for (const note of notes) { - cnt++; - const noteNode: Node = { - id: note.id, - label: note.text, - isGroup: false, - shape: 'note', - padding: config.class!.padding ?? 6, - cssStyles: [ - 'text-align: left', - 'white-space: nowrap', - `fill: ${config.themeVariables.noteBkgColor}`, - `stroke: ${config.themeVariables.noteBorderColor}`, - ], - look: config.look, - }; - nodes.push(noteNode); - - const noteClassId = classes.get(note.class)?.id ?? ''; - - if (noteClassId) { + cnt = 0; + for (const classRelation of this.relations) { + cnt++; const edge: Edge = { - id: `edgeNote${cnt}`, - start: note.id, - end: noteClassId, + id: getEdgeId(classRelation.id1, classRelation.id2, { + prefix: 'id', + counter: cnt, + }), + start: classRelation.id1, + end: classRelation.id2, type: 'normal', + label: classRelation.title, + labelpos: 'c', thickness: 'normal', classes: 'relation', - arrowTypeStart: 'none', - arrowTypeEnd: 'none', + arrowTypeStart: this.getArrowMarker(classRelation.relation.type1), + arrowTypeEnd: this.getArrowMarker(classRelation.relation.type2), + startLabelRight: + classRelation.relationTitle1 === 'none' ? '' : classRelation.relationTitle1, + endLabelLeft: classRelation.relationTitle2 === 'none' ? '' : classRelation.relationTitle2, arrowheadStyle: '', - labelStyle: [''], - style: ['fill: none'], - pattern: 'dotted', + labelStyle: ['display: inline-block'], + style: classRelation.style || '', + pattern: classRelation.relation.lineType == 1 ? 'dashed' : 'solid', look: config.look, }; edges.push(edge); } - } - for (const _interface of interfaces) { - const interfaceNode: Node = { - id: _interface.id, - label: _interface.label, - isGroup: false, - shape: 'rect', - cssStyles: ['opacity: 0;'], - look: config.look, - }; - nodes.push(interfaceNode); - } + return { nodes, edges, other: {}, config, direction: this.getDirection() }; + }; - cnt = 0; - for (const classRelation of relations) { - cnt++; - const edge: Edge = { - id: getEdgeId(classRelation.id1, classRelation.id2, { - prefix: 'id', - counter: cnt, - }), - start: classRelation.id1, - end: classRelation.id2, - type: 'normal', - label: classRelation.title, - labelpos: 'c', - thickness: 'normal', - classes: 'relation', - arrowTypeStart: getArrowMarker(classRelation.relation.type1), - arrowTypeEnd: getArrowMarker(classRelation.relation.type2), - startLabelRight: classRelation.relationTitle1 === 'none' ? '' : classRelation.relationTitle1, - endLabelLeft: classRelation.relationTitle2 === 'none' ? '' : classRelation.relationTitle2, - arrowheadStyle: '', - labelStyle: ['display: inline-block'], - style: classRelation.style || '', - pattern: classRelation.relation.lineType == 1 ? 'dashed' : 'solid', - look: config.look, - }; - edges.push(edge); - } - - return { nodes, edges, other: {}, config, direction: getDirection() }; -}; - -export default { - setAccTitle, - getAccTitle, - getAccDescription, - setAccDescription, - getConfig: () => getConfig().class, - addClass, - bindFunctions, - clear, - getClass, - getClasses, - getNotes, - addAnnotation, - addNote, - getRelations, - addRelation, - getDirection, - setDirection, - addMember, - addMembers, - cleanupLabel, - lineType, - relationType, - setClickEvent, - setCssClass, - defineClass, - setLink, - getTooltip, - setTooltip, - lookUpDomId, - setDiagramTitle, - getDiagramTitle, - setClassLabel, - addNamespace, - addClassesToNamespace, - getNamespace, - getNamespaces, - setCssStyle, - getData, -}; + public setAccTitle = setAccTitle; + public getAccTitle = getAccTitle; + public setAccDescription = setAccDescription; + public getAccDescription = getAccDescription; + public setDiagramTitle = setDiagramTitle; + public getDiagramTitle = getDiagramTitle; + public getConfig = () => getConfig().class; +} diff --git a/packages/mermaid/src/diagrams/class/classDiagram-styles.spec.js b/packages/mermaid/src/diagrams/class/classDiagram-styles.spec.js index 18bdaade5..71f322478 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram-styles.spec.js +++ b/packages/mermaid/src/diagrams/class/classDiagram-styles.spec.js @@ -1,9 +1,11 @@ import { parser } from './parser/classDiagram.jison'; -import classDb from './classDb.js'; +import { ClassDB } from './classDb.js'; describe('class diagram, ', function () { describe('when parsing data from a classDiagram it', function () { + let classDb; beforeEach(function () { + classDb = new ClassDB(); parser.yy = classDb; parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/class/classDiagram-v2.ts b/packages/mermaid/src/diagrams/class/classDiagram-v2.ts index 6a3747e41..9111fe658 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram-v2.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram-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/classDiagram.jison'; -import db from './classDb.js'; +import { ClassDB } from './classDb.js'; import styles from './styles.js'; import renderer from './classRenderer-v3-unified.js'; export const diagram: DiagramDefinition = { parser, - db, + get db() { + return new ClassDB(); + }, renderer, styles, init: (cnf) => { @@ -15,6 +17,5 @@ export const diagram: DiagramDefinition = { cnf.class = {}; } cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; - db.clear(); }, }; diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts index 40027f27e..ed508a5d8 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts @@ -1,6 +1,6 @@ // @ts-expect-error Jison doesn't export types import { parser } from './parser/classDiagram.jison'; -import classDb from './classDb.js'; +import { ClassDB } from './classDb.js'; import { vi, describe, it, expect } from 'vitest'; import type { ClassMap, NamespaceNode } from './classTypes.js'; const spyOn = vi.spyOn; @@ -10,8 +10,9 @@ const abstractCssStyle = 'font-style:italic;'; describe('given a basic class diagram, ', function () { describe('when parsing class definition', function () { + let classDb: ClassDB; beforeEach(function () { - classDb.clear(); + classDb = new ClassDB(); parser.yy = classDb; }); it('should handle classes within namespaces', () => { @@ -564,8 +565,9 @@ class C13["With Città foreign language"] }); describe('when parsing class defined in brackets', function () { + let classDb: ClassDB; beforeEach(function () { - classDb.clear(); + classDb = new ClassDB(); parser.yy = classDb; }); @@ -656,8 +658,9 @@ class C13["With Città foreign language"] }); describe('when parsing comments', function () { + let classDb: ClassDB; beforeEach(function () { - classDb.clear(); + classDb = new ClassDB(); parser.yy = classDb; }); @@ -746,8 +749,9 @@ foo() }); describe('when parsing click statements', function () { + let classDb: ClassDB; beforeEach(function () { - classDb.clear(); + classDb = new ClassDB(); parser.yy = classDb; }); it('should handle href link', function () { @@ -857,8 +861,9 @@ foo() }); describe('when parsing annotations', function () { + let classDb: ClassDB; beforeEach(function () { - classDb.clear(); + classDb = new ClassDB(); parser.yy = classDb; }); @@ -921,8 +926,9 @@ foo() describe('given a class diagram with members and methods ', function () { describe('when parsing members', function () { + let classDb: ClassDB; beforeEach(function () { - classDb.clear(); + classDb = new ClassDB(); parser.yy = classDb; }); @@ -980,8 +986,9 @@ describe('given a class diagram with members and methods ', function () { }); describe('when parsing method definition', function () { + let classDb: ClassDB; beforeEach(function () { - classDb.clear(); + classDb = new ClassDB(); parser.yy = classDb; }); @@ -1067,8 +1074,9 @@ describe('given a class diagram with members and methods ', function () { describe('given a class diagram with generics, ', function () { describe('when parsing valid generic classes', function () { + let classDb: ClassDB; beforeEach(function () { - classDb.clear(); + classDb = new ClassDB(); parser.yy = classDb; }); @@ -1180,8 +1188,9 @@ namespace space { describe('given a class diagram with relationships, ', function () { describe('when parsing basic relationships', function () { + let classDb: ClassDB; beforeEach(function () { - classDb.clear(); + classDb = new ClassDB(); parser.yy = classDb; }); @@ -1714,7 +1723,9 @@ class Class2 }); describe('when parsing classDiagram with text labels', () => { + let classDb: ClassDB; beforeEach(function () { + classDb = new ClassDB(); parser.yy = classDb; parser.yy.clear(); }); @@ -1897,3 +1908,40 @@ class C13["With Città foreign language"] }); }); }); + +describe('class db class', () => { + let classDb: ClassDB; + beforeEach(() => { + classDb = new ClassDB(); + }); + // This is to ensure that functions used in class JISON are exposed as function from ClassDB + it('should have functions used in class JISON as own property', () => { + const functionsUsedInParser = [ + 'addRelation', + 'cleanupLabel', + 'setAccTitle', + 'setAccDescription', + 'addClassesToNamespace', + 'addNamespace', + 'setCssClass', + 'addMembers', + 'addClass', + 'setClassLabel', + 'addAnnotation', + 'addMember', + 'addNote', + 'defineClass', + 'setDirection', + 'relationType', + 'lineType', + 'setClickEvent', + 'setTooltip', + 'setLink', + 'setCssStyle', + ] as const satisfies (keyof ClassDB)[]; + + for (const fun of functionsUsedInParser) { + expect(Object.hasOwn(classDb, fun)).toBe(true); + } + }); +}); diff --git a/packages/mermaid/src/diagrams/class/classDiagram.ts b/packages/mermaid/src/diagrams/class/classDiagram.ts index 6a3747e41..9111fe658 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.ts @@ -1,13 +1,15 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; // @ts-ignore: JISON doesn't support types import parser from './parser/classDiagram.jison'; -import db from './classDb.js'; +import { ClassDB } from './classDb.js'; import styles from './styles.js'; import renderer from './classRenderer-v3-unified.js'; export const diagram: DiagramDefinition = { parser, - db, + get db() { + return new ClassDB(); + }, renderer, styles, init: (cnf) => { @@ -15,6 +17,5 @@ export const diagram: DiagramDefinition = { cnf.class = {}; } cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; - db.clear(); }, }; diff --git a/packages/mermaid/src/diagrams/class/parser/class.spec.js b/packages/mermaid/src/diagrams/class/parser/class.spec.js index d611dfc02..fe0077a29 100644 --- a/packages/mermaid/src/diagrams/class/parser/class.spec.js +++ b/packages/mermaid/src/diagrams/class/parser/class.spec.js @@ -1,8 +1,10 @@ import { parser } from './classDiagram.jison'; -import classDb from '../classDb.js'; +import { ClassDB } from '../classDb.js'; describe('class diagram', function () { + let classDb; beforeEach(function () { + classDb = new ClassDB(); parser.yy = classDb; parser.yy.clear(); }); diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index 5bd1b1dfc..161b247b5 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 { ClassDB } from './diagrams/class/classDb.js'; /** * @see https://vitest.dev/guide/mocking.html Mock part of a module @@ -833,4 +835,46 @@ graph TD;A--x|text including URL space|B;`) expect(diagram.type).toBe('flowchart-v2'); }); }); + + it('should not modify db when rendering different diagrams', async () => { + const classDiagram1 = await mermaidAPI.getDiagramFromText( + `classDiagram + direction TB + class Student { + -idCard : IdCard + } + class IdCard{ + -id : int + -name : string + } + class Bike{ + -id : int + -name : string + } + Student "1" --o "1" IdCard : carries + Student "1" --o "1" Bike : rides` + ); + const classDiagram2 = await mermaidAPI.getDiagramFromText( + `classDiagram + direction LR + class Student { + -idCard : IdCard + } + class IdCard{ + -id : int + -name : string + } + class Bike{ + -id : int + -name : string + } + Student "1" --o "1" IdCard : carries + Student "1" --o "1" Bike : rides` + ); + // Since classDiagram will return new Db object each time, we can compare the db to be different. + expect(classDiagram1.db).not.toBe(classDiagram2.db); + assert(classDiagram1.db instanceof ClassDB); + assert(classDiagram2.db instanceof ClassDB); + 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 05/12] 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 06/12] 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 92efc24283716acd5147bdf1fb00c0997fca4c0f Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Wed, 22 Jan 2025 19:07:53 +0530 Subject: [PATCH 07/12] Refactored arrow functions to standard function --- .../mermaid/src/diagrams/class/classDb.ts | 154 ++++++++++-------- 1 file changed, 90 insertions(+), 64 deletions(-) diff --git a/packages/mermaid/src/diagrams/class/classDb.ts b/packages/mermaid/src/diagrams/class/classDb.ts index 67e0b30a8..cb102ae84 100644 --- a/packages/mermaid/src/diagrams/class/classDb.ts +++ b/packages/mermaid/src/diagrams/class/classDb.ts @@ -41,14 +41,38 @@ export class ClassDB implements DiagramDB { private functions: any[] = []; - private readonly sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig()); + private sanitizeText(txt: string) { + return common.sanitizeText(txt, getConfig()); + } constructor() { this.functions.push(this.setupToolTips); this.clear(); + + // Needed for JISON since it only supports direct properties + this.addRelation = this.addRelation.bind(this); + this.addClassesToNamespace = this.addClassesToNamespace.bind(this); + this.addNamespace = this.addNamespace.bind(this); + this.setCssClass = this.setCssClass.bind(this); + this.addMembers = this.addMembers.bind(this); + this.addClass = this.addClass.bind(this); + this.setClassLabel = this.setClassLabel.bind(this); + this.addAnnotation = this.addAnnotation.bind(this); + this.addMember = this.addMember.bind(this); + this.cleanupLabel = this.cleanupLabel.bind(this); + this.addNote = this.addNote.bind(this); + this.defineClass = this.defineClass.bind(this); + this.setDirection = this.setDirection.bind(this); + this.setLink = this.setLink.bind(this); + this.setTooltip = this.setTooltip.bind(this); + this.setClickEvent = this.setClickEvent.bind(this); + this.setCssStyle = this.setCssStyle.bind(this); + this.setClickFunc = this.setClickFunc.bind(this); + this.bindFunctions = this.bindFunctions.bind(this); + this.clear = this.clear.bind(this); } - private readonly splitClassNameAndType = (_id: string) => { + private splitClassNameAndType(_id: string) { const id = common.sanitizeText(_id, getConfig()); let genericType = ''; let className = id; @@ -60,9 +84,9 @@ export class ClassDB implements DiagramDB { } return { className: className, type: genericType }; - }; + } - public setClassLabel = (_id: string, label: string) => { + public setClassLabel(_id: string, label: string) { const id = common.sanitizeText(_id, getConfig()); if (label) { label = this.sanitizeText(label); @@ -72,7 +96,7 @@ export class ClassDB implements DiagramDB { this.classes.get(className)!.label = label; this.classes.get(className)!.text = `${label}${this.classes.get(className)!.type ? `<${this.classes.get(className)!.type}>` : ''}`; - }; + } /** * Function called by parser when a node definition has been found. @@ -80,7 +104,7 @@ export class ClassDB implements DiagramDB { * @param id - Id of the class to add * @public */ - public addClass = (_id: string) => { + public addClass(_id: string) { const id = common.sanitizeText(_id, getConfig()); const { className, type } = this.splitClassNameAndType(id); // Only add class if not exists @@ -105,9 +129,9 @@ export class ClassDB implements DiagramDB { } as ClassNode); classCounter++; - }; + } - private readonly addInterface = (label: string, classId: string) => { + private addInterface(label: string, classId: string) { const classInterface: Interface = { id: `interface${this.interfaces.length}`, label, @@ -115,7 +139,7 @@ export class ClassDB implements DiagramDB { }; this.interfaces.push(classInterface); - }; + } /** * Function to lookup domId from id in the graph definition. @@ -123,15 +147,15 @@ export class ClassDB implements DiagramDB { * @param id - class ID to lookup * @public */ - public lookUpDomId = (_id: string): string => { + public lookUpDomId(_id: string): string { const id = common.sanitizeText(_id, getConfig()); if (this.classes.has(id)) { return this.classes.get(id)!.domId; } throw new Error('Class not found: ' + id); - }; + } - public clear = () => { + public clear() { this.relations = []; this.classes = new Map(); this.notes = []; @@ -142,25 +166,25 @@ export class ClassDB implements DiagramDB { this.namespaceCounter = 0; this.direction = 'TB'; commonClear(); - }; + } - public getClass = (id: string): ClassNode => { + public getClass(id: string): ClassNode { return this.classes.get(id)!; - }; + } - public getClasses = (): ClassMap => { + public getClasses(): ClassMap { return this.classes; - }; + } - public getRelations = (): ClassRelation[] => { + public getRelations(): ClassRelation[] { return this.relations; - }; + } - public getNotes = () => { + public getNotes() { return this.notes; - }; + } - public addRelation = (classRelation: ClassRelation) => { + public addRelation(classRelation: ClassRelation) { log.debug('Adding relation: ' + JSON.stringify(classRelation)); // Due to relationType cannot just check if it is equal to 'none' or it complains, can fix this later const invalidTypes = [ @@ -204,7 +228,7 @@ export class ClassDB implements DiagramDB { ); this.relations.push(classRelation); - }; + } /** * Adds an annotation to the specified class Annotations mark special properties of the given type @@ -214,10 +238,10 @@ export class ClassDB implements DiagramDB { * @param annotation - The name of the annotation without any brackets * @public */ - public addAnnotation = (className: string, annotation: string) => { + public addAnnotation(className: string, annotation: string) { const validatedClassName = this.splitClassNameAndType(className).className; this.classes.get(validatedClassName)!.annotations.push(annotation); - }; + } /** * Adds a member to the specified class @@ -228,7 +252,7 @@ export class ClassDB implements DiagramDB { * method Otherwise the member will be treated as a normal property * @public */ - public addMember = (className: string, member: string) => { + public addMember(className: string, member: string) { this.addClass(className); const validatedClassName = this.splitClassNameAndType(className).className; @@ -250,30 +274,30 @@ export class ClassDB implements DiagramDB { theClass.members.push(new ClassMember(memberString, 'attribute')); } } - }; + } - public addMembers = (className: string, members: string[]) => { + public addMembers(className: string, members: string[]) { if (Array.isArray(members)) { members.reverse(); members.forEach((member) => this.addMember(className, member)); } - }; + } - public addNote = (text: string, className: string) => { + public addNote(text: string, className: string) { const note = { id: `note${this.notes.length}`, class: className, text: text, }; this.notes.push(note); - }; + } - public cleanupLabel = (label: string) => { + public cleanupLabel(label: string) { if (label.startsWith(':')) { label = label.substring(1); } return this.sanitizeText(label.trim()); - }; + } /** * Called by parser when assigning cssClass to a class @@ -281,7 +305,7 @@ export class ClassDB implements DiagramDB { * @param ids - Comma separated list of ids * @param className - Class to add */ - public setCssClass = (ids: string, className: string) => { + public setCssClass(ids: string, className: string) { ids.split(',').forEach((_id) => { let id = _id; if (/\d/.exec(_id[0])) { @@ -292,9 +316,9 @@ export class ClassDB implements DiagramDB { classNode.cssClasses += ' ' + className; } }); - }; + } - public defineClass = (ids: string[], style: string[]) => { + public defineClass(ids: string[], style: string[]) { for (const id of ids) { let styleClass = this.styleClasses.get(id); if (styleClass === undefined) { @@ -318,7 +342,7 @@ export class ClassDB implements DiagramDB { } }); } - }; + } /** * Called by parser when a tooltip is found, e.g. a clickable element. @@ -326,21 +350,21 @@ export class ClassDB implements DiagramDB { * @param ids - Comma separated list of ids * @param tooltip - Tooltip to add */ - public setTooltip = (ids: string, tooltip?: string) => { + public setTooltip(ids: string, tooltip?: string) { ids.split(',').forEach((id) => { if (tooltip !== undefined) { this.classes.get(id)!.tooltip = this.sanitizeText(tooltip); } }); - }; + } - public getTooltip = (id: string, namespace?: string) => { + public getTooltip(id: string, namespace?: string) { if (namespace && this.namespaces.has(namespace)) { return this.namespaces.get(namespace)!.classes.get(id)!.tooltip; } return this.classes.get(id)!.tooltip; - }; + } /** * Called by parser when a link is found. Adds the URL to the vertex data. @@ -349,7 +373,7 @@ export class ClassDB implements DiagramDB { * @param linkStr - URL to create a link for * @param target - Target of the link, _blank by default as originally defined in the svgDraw.js file */ - public setLink = (ids: string, linkStr: string, target: string) => { + public setLink(ids: string, linkStr: string, target: string) { const config = getConfig(); ids.split(',').forEach((_id) => { let id = _id; @@ -369,7 +393,7 @@ export class ClassDB implements DiagramDB { } }); this.setCssClass(ids, 'clickable'); - }; + } /** * Called by parser when a click definition is found. Registers an event handler. @@ -378,15 +402,15 @@ export class ClassDB implements DiagramDB { * @param functionName - Function to be called on click * @param functionArgs - Function args the function should be called with */ - public setClickEvent = (ids: string, functionName: string, functionArgs: string) => { + public setClickEvent(ids: string, functionName: string, functionArgs: string) { ids.split(',').forEach((id) => { this.setClickFunc(id, functionName, functionArgs); this.classes.get(id)!.haveCallback = true; }); this.setCssClass(ids, 'clickable'); - }; + } - private readonly setClickFunc = (_domId: string, functionName: string, functionArgs: string) => { + private setClickFunc(_domId: string, functionName: string, functionArgs: string) { const domId = common.sanitizeText(_domId, getConfig()); const config = getConfig(); if (config.securityLevel !== 'loose') { @@ -432,13 +456,13 @@ export class ClassDB implements DiagramDB { } }); } - }; + } - public bindFunctions = (element: Element) => { + public bindFunctions(element: Element) { this.functions.forEach((fun) => { fun(element); }); - }; + } public lineType = { LINE: 0, @@ -494,10 +518,12 @@ export class ClassDB implements DiagramDB { }; private direction = 'TB'; - public getDirection = () => this.direction; - public setDirection = (dir: string) => { + public getDirection() { + return this.direction; + } + public setDirection(dir: string) { this.direction = dir; - }; + } /** * Function called by parser when a namespace definition has been found. @@ -505,7 +531,7 @@ export class ClassDB implements DiagramDB { * @param id - Id of the namespace to add * @public */ - public addNamespace = (id: string) => { + public addNamespace(id: string) { if (this.namespaces.has(id)) { return; } @@ -518,15 +544,15 @@ export class ClassDB implements DiagramDB { } as NamespaceNode); this.namespaceCounter++; - }; + } - public getNamespace = (name: string): NamespaceNode => { + public getNamespace(name: string): NamespaceNode { return this.namespaces.get(name)!; - }; + } - public getNamespaces = (): NamespaceMap => { + public getNamespaces(): NamespaceMap { return this.namespaces; - }; + } /** * Function called by parser when a namespace definition has been found. @@ -535,7 +561,7 @@ export class ClassDB implements DiagramDB { * @param classNames - Ids of the class to add * @public */ - public addClassesToNamespace = (id: string, classNames: string[]) => { + public addClassesToNamespace(id: string, classNames: string[]) { if (!this.namespaces.has(id)) { return; } @@ -544,9 +570,9 @@ export class ClassDB implements DiagramDB { this.classes.get(className)!.parent = id; this.namespaces.get(id)!.classes.set(className, this.classes.get(className)!); } - }; + } - public setCssStyle = (id: string, styles: string[]) => { + public setCssStyle(id: string, styles: string[]) { const thisClass = this.classes.get(id); if (!styles || !thisClass) { return; @@ -558,7 +584,7 @@ export class ClassDB implements DiagramDB { thisClass.styles.push(s); } } - }; + } /** * Gets the arrow marker for a type index @@ -590,7 +616,7 @@ export class ClassDB implements DiagramDB { return marker; }; - public getData = () => { + public getData() { const nodes: Node[] = []; const edges: Edge[] = []; const config = getConfig(); @@ -705,7 +731,7 @@ export class ClassDB implements DiagramDB { } return { nodes, edges, other: {}, config, direction: this.getDirection() }; - }; + } public setAccTitle = setAccTitle; public getAccTitle = getAccTitle; From 90bbf90a83bf5da53fc8030cf1370bc8238fa4aa Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Wed, 22 Jan 2025 19:10:48 +0530 Subject: [PATCH 08/12] Added changeset --- .changeset/chilly-years-cheat.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilly-years-cheat.md diff --git a/.changeset/chilly-years-cheat.md b/.changeset/chilly-years-cheat.md new file mode 100644 index 000000000..e665af75b --- /dev/null +++ b/.changeset/chilly-years-cheat.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each class diagram From 61f3fc5ede72ab2ffcbe46d97d79a332fcdc52bf Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Wed, 22 Jan 2025 19:17:38 +0530 Subject: [PATCH 09/12] 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 10/12] 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 From b09f2e836af33a5e79082a0b72c57889a6ddeaf3 Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Thu, 23 Jan 2025 12:25:38 +0530 Subject: [PATCH 11/12] Updated as per PR comments --- .../mermaid/src/diagrams/class/classDb.ts | 38 ++++++++----------- .../src/diagrams/class/classDiagram.spec.ts | 1 + 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/packages/mermaid/src/diagrams/class/classDb.ts b/packages/mermaid/src/diagrams/class/classDb.ts index cb102ae84..cd9130365 100644 --- a/packages/mermaid/src/diagrams/class/classDb.ts +++ b/packages/mermaid/src/diagrams/class/classDb.ts @@ -29,6 +29,8 @@ import type { DiagramDB } from '../../diagram-api/types.js'; const MERMAID_DOM_ID_PREFIX = 'classId-'; let classCounter = 0; +const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig()); + export class ClassDB implements DiagramDB { private relations: ClassRelation[] = []; private classes = new Map(); @@ -41,12 +43,8 @@ export class ClassDB implements DiagramDB { private functions: any[] = []; - private sanitizeText(txt: string) { - return common.sanitizeText(txt, getConfig()); - } - constructor() { - this.functions.push(this.setupToolTips); + this.functions.push(this.setupToolTips.bind(this)); this.clear(); // Needed for JISON since it only supports direct properties @@ -64,10 +62,6 @@ export class ClassDB implements DiagramDB { this.defineClass = this.defineClass.bind(this); this.setDirection = this.setDirection.bind(this); this.setLink = this.setLink.bind(this); - this.setTooltip = this.setTooltip.bind(this); - this.setClickEvent = this.setClickEvent.bind(this); - this.setCssStyle = this.setCssStyle.bind(this); - this.setClickFunc = this.setClickFunc.bind(this); this.bindFunctions = this.bindFunctions.bind(this); this.clear = this.clear.bind(this); } @@ -79,8 +73,8 @@ export class ClassDB implements DiagramDB { if (id.indexOf('~') > 0) { const split = id.split('~'); - className = this.sanitizeText(split[0]); - genericType = this.sanitizeText(split[1]); + className = sanitizeText(split[0]); + genericType = sanitizeText(split[1]); } return { className: className, type: genericType }; @@ -89,7 +83,7 @@ export class ClassDB implements DiagramDB { public setClassLabel(_id: string, label: string) { const id = common.sanitizeText(_id, getConfig()); if (label) { - label = this.sanitizeText(label); + label = sanitizeText(label); } const { className } = this.splitClassNameAndType(id); @@ -161,7 +155,7 @@ export class ClassDB implements DiagramDB { this.notes = []; this.interfaces = []; this.functions = []; - this.functions.push(this.setupToolTips); + this.functions.push(this.setupToolTips.bind(this)); this.namespaces = new Map(); this.namespaceCounter = 0; this.direction = 'TB'; @@ -264,9 +258,7 @@ export class ClassDB implements DiagramDB { if (memberString.startsWith('<<') && memberString.endsWith('>>')) { // its an annotation - theClass.annotations.push( - this.sanitizeText(memberString.substring(2, memberString.length - 2)) - ); + theClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2))); } else if (memberString.indexOf(')') > 0) { //its a method theClass.methods.push(new ClassMember(memberString, 'method')); @@ -296,7 +288,7 @@ export class ClassDB implements DiagramDB { if (label.startsWith(':')) { label = label.substring(1); } - return this.sanitizeText(label.trim()); + return sanitizeText(label.trim()); } /** @@ -353,7 +345,7 @@ export class ClassDB implements DiagramDB { public setTooltip(ids: string, tooltip?: string) { ids.split(',').forEach((id) => { if (tooltip !== undefined) { - this.classes.get(id)!.tooltip = this.sanitizeText(tooltip); + this.classes.get(id)!.tooltip = sanitizeText(tooltip); } }); } @@ -386,7 +378,7 @@ export class ClassDB implements DiagramDB { if (config.securityLevel === 'sandbox') { theClass.linkTarget = '_top'; } else if (typeof target === 'string') { - theClass.linkTarget = this.sanitizeText(target); + theClass.linkTarget = sanitizeText(target); } else { theClass.linkTarget = '_blank'; } @@ -464,12 +456,12 @@ export class ClassDB implements DiagramDB { }); } - public lineType = { + public readonly lineType = { LINE: 0, DOTTED_LINE: 1, }; - public relationType = { + public readonly relationType = { AGGREGATION: 0, EXTENSION: 1, COMPOSITION: 2, @@ -592,7 +584,7 @@ export class ClassDB implements DiagramDB { * @param type - The type to look for * @returns The arrow marker */ - private readonly getArrowMarker = (type: number) => { + private getArrowMarker(type: number) { let marker; switch (type) { case 0: @@ -614,7 +606,7 @@ export class ClassDB implements DiagramDB { marker = 'none'; } return marker; - }; + } public getData() { const nodes: Node[] = []; diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts index ed508a5d8..35a37f903 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/unbound-method -- Broken for Vitest mocks, see https://github.com/vitest-dev/eslint-plugin-vitest/pull/286 */ // @ts-expect-error Jison doesn't export types import { parser } from './parser/classDiagram.jison'; import { ClassDB } from './classDb.js'; From 202ef3107159efdef496225194989f32f0342c38 Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Thu, 23 Jan 2025 14:58:16 +0530 Subject: [PATCH 12/12] Fixed binding issue --- packages/mermaid/src/diagrams/class/classDb.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/mermaid/src/diagrams/class/classDb.ts b/packages/mermaid/src/diagrams/class/classDb.ts index cd9130365..b2c3c944c 100644 --- a/packages/mermaid/src/diagrams/class/classDb.ts +++ b/packages/mermaid/src/diagrams/class/classDb.ts @@ -64,6 +64,10 @@ export class ClassDB implements DiagramDB { this.setLink = this.setLink.bind(this); this.bindFunctions = this.bindFunctions.bind(this); this.clear = this.clear.bind(this); + + this.setTooltip = this.setTooltip.bind(this); + this.setClickEvent = this.setClickEvent.bind(this); + this.setCssStyle = this.setCssStyle.bind(this); } private splitClassNameAndType(_id: string) {