diff --git a/.changeset/dull-tips-cough.md b/.changeset/dull-tips-cough.md new file mode 100644 index 000000000..1f5179417 --- /dev/null +++ b/.changeset/dull-tips-cough.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: revert state db to resolve getData returning empty nodes and edges 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 bb5345996..9fa8acab8 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,9 +7,7 @@ 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 c37bed3c7..fed63c444 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,9 +7,7 @@ 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 b18b4ca0e..f0ab4136b 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'; @@ -414,13 +414,13 @@ let edgeCount = 0; export const drawEdge = function (elem, path, relation) { 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'; } }; @@ -459,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) { diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.js index 0d8ed80f4..c66465941 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.js +++ b/packages/mermaid/src/diagrams/state/stateDb.js @@ -1,28 +1,28 @@ -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 { - clear as commonClear, - getAccDescription, - getAccTitle, - getDiagramTitle, - setAccDescription, setAccTitle, + getAccTitle, + getAccDescription, + setAccDescription, + clear as commonClear, setDiagramTitle, + getDiagramTitle, } 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 = '[*]'; @@ -46,6 +46,15 @@ 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 }[]} */ @@ -54,623 +63,564 @@ const newDoc = () => { documents: {}, }; }; +let documents = { + root: newDoc(), +}; + +let currentDocument = documents.root; +let startEndCount = 0; +let dividerCnt = 0; + +export const lineType = { + LINE: 0, + DOTTED_LINE: 1, +}; + +export const relationType = { + AGGREGATION: 0, + EXTENSION: 1, + COMPOSITION: 2, + DEPENDENCY: 3, +}; const clone = (o) => JSON.parse(JSON.stringify(o)); -export class StateDB { - constructor() { - this.clear(); +const setRootDoc = (o) => { + log.info('Setting root doc', o); + // rootDoc = { id: 'root', doc: o }; + rootDoc = o; +}; - // 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); +const getRootDoc = () => rootDoc; + +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(); + } + } + + 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 = []; + } else { + currentDoc.push(node.doc[i]); + } + } + + // 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()); + }); + } + 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, config); + 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]; + } + }); +}; + +/** + * 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; + } } - /** - * @private - * @type {Array} - */ + 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 = []; - /** - * @private - * @type {Array} - */ edges = []; - - /** - * @private - * @type {string} - */ - direction = DEFAULT_DIAGRAM_DIRECTION; - /** - * @private - * @type {Array} - */ - rootDoc = []; - /** - * @private - * @type {Map} - */ - classes = newClassesList(); // style classes defined by a classDef - - /** - * @private - * @type {Object} - */ documents = { root: newDoc(), }; + currentDocument = documents.root; - /** - * @private - * @type {Object} - */ - currentDocument = this.documents.root; - /** - * @private - * @type {number} - */ + // number of start and end nodes; used to construct ids startEndCount = 0; - /** - * @private - * @type {number} - */ - dividerCnt = 0; - - static relationType = { - AGGREGATION: 0, - EXTENSION: 1, - COMPOSITION: 2, - DEPENDENCY: 3, - }; - - setRootDoc(o) { - log.info('Setting root doc', o); - // rootDoc = { id: 'root', doc: o }; - this.rootDoc = o; + classes = newClassesList(); + if (!saveCommon) { + commonClear(); } +}; - getRootDoc() { - return this.rootDoc; +export const getState = function (id) { + return currentDocument.states.get(id); +}; + +export const getStates = function () { + return currentDocument.states; +}; +export const logDocuments = function () { + log.info('Documents = ', documents); +}; +export const getRelations = function () { + return 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) + */ +function startIdIfNeeded(id = '') { + let fixedId = id; + if (id === START_NODE) { + startEndCount++; + fixedId = `${START_TYPE}${startEndCount}`; } - - /** - * @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); - } 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(); - } - } - - 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 = clone(node.doc[i]); - newNode.doc = clone(currentDoc); - doc.push(newNode); - currentDoc = []; - } else { - currentDoc.push(node.doc[i]); - } - } - - // 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) => this.docTranslator(node, docNode, true)); - } - } - } - 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. - * 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); - - log.info('Extract initial document:', doc); - - 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; - } - }); - - const diagramStates = this.getStates(); - const config = getConfig(); - const look = config.look; - - resetDataFetching(); - dataFetcher( - undefined, - this.getRootDocV2(), - diagramStates, - this.nodes, - this.edges, - true, - look, - this.classes, - config - ); - 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]; - } - }); - } - - /** - * 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 (descr) { - log.info('Setting state description', trimmedId, descr); - if (typeof descr === 'string') { - this.addDescription(trimmedId, descr.trim()); - } - - if (typeof descr === 'object') { - descr.forEach((des) => this.addDescription(trimmedId, des.trim())); - } - } - - if (note) { - const doc2 = this.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) => this.setCssClass(trimmedId, cssClass.trim())); - } - - if (styles) { - log.info('Setting state styles', trimmedId, styles); - const stylesList = typeof styles === 'string' ? [styles] : styles; - stylesList.forEach((style) => this.setStyle(trimmedId, style.trim())); - } - - 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: newDoc(), - }; - this.currentDocument = this.documents.root; - - // number of start and end nodes; used to construct ids - this.startEndCount = 0; - this.classes = 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) - * @private - */ - startIdIfNeeded(id = '') { - let fixedId = id; - if (id === START_NODE) { - this.startEndCount++; - fixedId = `${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 - * @private - */ - startTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { - return id === START_NODE ? 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) - * @private - */ - endIdIfNeeded(id = '') { - let fixedId = id; - if (id === END_NODE) { - this.startEndCount++; - fixedId = `${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 - * @private - */ - endTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { - return id === END_NODE ? 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, - 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. - * - * @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); - - this.addState(id1, type1); - this.addState(id2, type2); - this.currentDocument.relations.push({ - id1, - id2, - title: common.sanitizeText(title, getConfig()), - }); - } - } - - 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) { - if (label.substring(0, 1) === ':') { - return label.substr(2).trim(); - } else { - return label.trim(); - } - } - - getDividerId() { - this.dividerCnt++; - return 'divider-id-' + this.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) - */ - 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(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} - */ - 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() { - return this.direction; - } - setDirection(dir) { - this.direction = dir; - } - - trimColon(str) { - return 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() { - return getConfig().state; - } - getAccTitle = getAccTitle; - setAccTitle = setAccTitle; - getAccDescription = getAccDescription; - setAccDescription = setAccDescription; - setDiagramTitle = setDiagramTitle; - getDiagramTitle = getDiagramTitle; + 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 + */ +function startTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { + return id === START_NODE ? 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) + */ +function endIdIfNeeded(id = '') { + let fixedId = id; + if (id === END_NODE) { + startEndCount++; + fixedId = `${END_TYPE}${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 + */ +function endTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { + return id === END_NODE ? END_TYPE : type; +} + +/** + * + * @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); + + 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 + ); + + currentDocument.relations.push({ + id1, + 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. + * + * @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); + + addState(id1, type1); + addState(id2, type2); + currentDocument.relations.push({ + id1, + id2, + title: common.sanitizeText(title, 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())); +}; + +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); + } + 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); + } +}; + +/** + * 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); + } +}; + +const getDirection = () => direction; +const setDirection = (dir) => { + direction = dir; +}; + +const trimColon = (str) => (str && str[0] === ':' ? str.substr(1).trim() : str.trim()); + +export const getData = () => { + const config = getConfig(); + return { nodes, edges, other: {}, config, direction: getDir(getRootDocV2()) }; +}; + +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, +}; diff --git a/packages/mermaid/src/diagrams/state/stateDb.spec.js b/packages/mermaid/src/diagrams/state/stateDb.spec.js index 73f1a4be9..ff0581200 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDb.spec.js @@ -1,9 +1,8 @@ -import { StateDB } from './stateDb.js'; +import stateDb from './stateDb.js'; describe('State Diagram stateDb', () => { - let stateDb; beforeEach(() => { - stateDb = new StateDB(); + stateDb.clear(); }); describe('addStyleClass', () => { @@ -21,9 +20,8 @@ describe('State Diagram stateDb', () => { }); describe('addDescription to a state', () => { - let stateDb; beforeEach(() => { - stateDb = new StateDB(); + stateDb.clear(); stateDb.addState('state1'); }); @@ -75,25 +73,3 @@ describe('State Diagram stateDb', () => { }); }); }); - -describe('state db class', () => { - let stateDb; - beforeEach(() => { - stateDb = new StateDB(); - }); - // 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', - '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 d1edc5b40..53063f41a 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js @@ -1,13 +1,11 @@ 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 f7bc716c6..8fd98e930 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts @@ -1,15 +1,13 @@ 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 db 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(); - }, + db, renderer, styles, init: (cnf) => { diff --git a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js index 8175ef041..7fcf4d0a6 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js @@ -1,11 +1,9 @@ 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 a6f9d7c63..bd8383287 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram.ts +++ b/packages/mermaid/src/diagrams/state/stateDiagram.ts @@ -1,15 +1,13 @@ 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 db from './stateDb.js'; import styles from './styles.js'; import renderer from './stateRenderer.js'; export const diagram: DiagramDefinition = { parser, - get db() { - return new StateDB(); - }, + db, renderer, styles, init: (cnf) => { diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index 0b1a0e4de..b07786a81 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -70,7 +70,6 @@ import { Diagram } from './Diagram.js'; import { decodeEntities, encodeEntities } from './utils.js'; import { toBase64 } from './utils/base64.js'; import { ClassDB } from './diagrams/class/classDb.js'; -import { StateDB } from './diagrams/state/stateDb.js'; import { FlowDB } from './diagrams/flowchart/flowDb.js'; /** @@ -855,32 +854,6 @@ graph TD;A--x|text including URL space|B;` }); it('should not modify db when rendering different diagrams', async () => { - const stateDiagram1 = await mermaidAPI.getDiagramFromText( - `stateDiagram - direction LR - [*] --> Still - Still --> [*] - Still --> Moving - Moving --> Still - Moving --> Crash - Crash --> [*]` - ); - const stateDiagram2 = await mermaidAPI.getDiagramFromText( - `stateDiagram - direction TB - [*] --> Still - Still --> [*] - Still --> Moving - Moving --> Still - Moving --> Crash - Crash --> [*]` - ); - // Since stateDiagram will return new Db object each time, we can compare the db to be different. - 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()); - const flowDiagram1 = await mermaidAPI.getDiagramFromText( `flowchart LR A -- text --> B -- text2 --> C`