From 31984acfe0a17b27fb317371b30725b79f446fc4 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Sat, 15 Feb 2025 22:52:35 +0530 Subject: [PATCH 01/10] chore: Rename stateDB --- packages/mermaid/src/diagrams/state/{stateDb.js => stateDb.ts} | 1 - 1 file changed, 1 deletion(-) rename packages/mermaid/src/diagrams/state/{stateDb.js => stateDb.ts} (99%) diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.ts similarity index 99% rename from packages/mermaid/src/diagrams/state/stateDb.js rename to packages/mermaid/src/diagrams/state/stateDb.ts index cc44659ebb..ccf94463bc 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.js +++ b/packages/mermaid/src/diagrams/state/stateDb.ts @@ -13,7 +13,6 @@ import { } from '../common/commonDb.js'; import { dataFetcher, reset as resetDataFetching } from './dataFetcher.js'; import { getDir } from './stateRenderer-v3-unified.js'; - import { DEFAULT_DIAGRAM_DIRECTION, DEFAULT_STATE_TYPE, From e89c77a5cac9f906319339558d20c5d1813812b3 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Mon, 17 Feb 2025 00:27:02 +0530 Subject: [PATCH 02/10] chore: Remove id-cache --- packages/mermaid/src/diagrams/state/id-cache.js | 16 ---------------- packages/mermaid/src/diagrams/state/shapes.js | 3 --- 2 files changed, 19 deletions(-) delete mode 100644 packages/mermaid/src/diagrams/state/id-cache.js diff --git a/packages/mermaid/src/diagrams/state/id-cache.js b/packages/mermaid/src/diagrams/state/id-cache.js deleted file mode 100644 index 875dc62b08..0000000000 --- a/packages/mermaid/src/diagrams/state/id-cache.js +++ /dev/null @@ -1,16 +0,0 @@ -const idCache = {}; - -export const set = (key, val) => { - idCache[key] = val; -}; - -export const get = (k) => idCache[k]; -export const keys = () => Object.keys(idCache); -export const size = () => keys().length; - -export default { - get, - set, - keys, - size, -}; diff --git a/packages/mermaid/src/diagrams/state/shapes.js b/packages/mermaid/src/diagrams/state/shapes.js index b18b4ca0e8..5fa964a4ab 100644 --- a/packages/mermaid/src/diagrams/state/shapes.js +++ b/packages/mermaid/src/diagrams/state/shapes.js @@ -1,5 +1,4 @@ import { line, curveBasis } from 'd3'; -import idCache from './id-cache.js'; import { StateDB } from './stateDb.js'; import utils from '../../utils.js'; import common from '../common/common.js'; @@ -405,8 +404,6 @@ export const drawState = function (elem, stateDef) { stateInfo.width = stateBox.width + 2 * getConfig().state.padding; stateInfo.height = stateBox.height + 2 * getConfig().state.padding; - idCache.set(id, stateInfo); - // stateCnt++; return stateInfo; }; From 438f388b5c5db9f74b90f9cccabc5c2dfabde54c Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Mon, 17 Feb 2025 00:27:43 +0530 Subject: [PATCH 03/10] chore: Rename dataFetcher --- .../mermaid/src/diagrams/state/{dataFetcher.js => dataFetcher.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/mermaid/src/diagrams/state/{dataFetcher.js => dataFetcher.ts} (100%) diff --git a/packages/mermaid/src/diagrams/state/dataFetcher.js b/packages/mermaid/src/diagrams/state/dataFetcher.ts similarity index 100% rename from packages/mermaid/src/diagrams/state/dataFetcher.js rename to packages/mermaid/src/diagrams/state/dataFetcher.ts From 7ca9242b246aaac0b43ba1b38824038e11af7724 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Mon, 17 Feb 2025 23:47:56 +0530 Subject: [PATCH 04/10] chore: Add types to stateDB and dataFetcher --- .../mermaid/src/diagrams/state/dataFetcher.ts | 84 +-- .../mermaid/src/diagrams/state/stateDb.ts | 541 +++++++++--------- 2 files changed, 324 insertions(+), 301 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/dataFetcher.ts b/packages/mermaid/src/diagrams/state/dataFetcher.ts index 921544ff27..b390b9d658 100644 --- a/packages/mermaid/src/diagrams/state/dataFetcher.ts +++ b/packages/mermaid/src/diagrams/state/dataFetcher.ts @@ -1,3 +1,4 @@ +import type { MermaidConfig } from '../../config.type.js'; import { getConfig } from '../../diagram-api/diagramAPI.js'; import { log } from '../../logger.js'; import common from '../common/common.js'; @@ -33,9 +34,10 @@ import { STMT_RELATION, STMT_STATE, } from './stateCommon.js'; +import type { Edge, NodeData, State, StateStmt, Stmt, StyleClass } from './stateDb.js'; // List of nodes created from the parsed diagram statement items -let nodeDb = new Map(); +const nodeDb = new Map(); let graphItemCount = 0; // used to construct ids, etc. @@ -43,18 +45,27 @@ let graphItemCount = 0; // used to construct ids, etc. * Create a standard string for the dom ID of an item. * If a type is given, insert that before the counter, preceded by the type spacer * - * @param itemId - * @param counter - * @param {string | null} type - * @param typeSpacer - * @returns {string} */ -export function stateDomId(itemId = '', counter = 0, type = '', typeSpacer = DOMID_TYPE_SPACER) { +export function stateDomId( + itemId = '', + counter = 0, + type: string | null = '', + typeSpacer = DOMID_TYPE_SPACER +) { const typeStr = type !== null && type.length > 0 ? `${typeSpacer}${type}` : ''; return `${DOMID_STATE}-${itemId}${typeStr}-${counter}`; } -const setupDoc = (parentParsedItem, doc, diagramStates, nodes, edges, altFlag, look, classes) => { +const setupDoc = ( + parentParsedItem: StateStmt | undefined, + doc: Stmt[], + diagramStates: Map, + nodes: NodeData[], + edges: Edge[], + altFlag: boolean, + look: MermaidConfig['look'], + classes: Map +) => { // graphItemCount = 0; log.trace('items', doc); doc.forEach((item) => { @@ -95,7 +106,7 @@ const setupDoc = (parentParsedItem, doc, diagramStates, nodes, edges, altFlag, l arrowTypeEnd: 'arrow_barb', style: G_EDGE_STYLE, labelStyle: '', - label: common.sanitizeText(item.description, getConfig()), + label: common.sanitizeText(item.description ?? '', getConfig()), arrowheadStyle: G_EDGE_ARROWHEADSTYLE, labelpos: G_EDGE_LABELPOS, labelType: G_EDGE_LABELTYPE, @@ -115,11 +126,10 @@ const setupDoc = (parentParsedItem, doc, diagramStates, nodes, edges, altFlag, l * Get the direction from the statement items. * Look through all of the documents (docs) in the parsedItems * Because is a _document_ direction, the default direction is not necessarily the same as the overall default _diagram_ direction. - * @param {object[]} parsedItem - the parsed statement item to look through - * @param [defaultDir] - the direction to use if none is found - * @returns {string} + * @param parsedItem - the parsed statement item to look through + * @param defaultDir - the direction to use if none is found */ -const getDir = (parsedItem, defaultDir = DEFAULT_NESTED_DOC_DIR) => { +const getDir = (parsedItem: { doc?: Stmt[] }, defaultDir = DEFAULT_NESTED_DOC_DIR) => { let dir = defaultDir; if (parsedItem.doc) { for (const parsedItemDoc of parsedItem.doc) { @@ -131,7 +141,11 @@ const getDir = (parsedItem, defaultDir = DEFAULT_NESTED_DOC_DIR) => { return dir; }; -function insertOrUpdateNode(nodes, nodeData, classes) { +function insertOrUpdateNode( + nodes: NodeData[], + nodeData: NodeData, + classes: Map +) { if (!nodeData.id || nodeData.id === '' || nodeData.id === '') { return; } @@ -143,9 +157,9 @@ function insertOrUpdateNode(nodes, nodeData, classes) { } nodeData.cssClasses.split(' ').forEach((cssClass) => { - if (classes.get(cssClass)) { - const classDef = classes.get(cssClass); - nodeData.cssCompiledStyles = [...nodeData.cssCompiledStyles, ...classDef.styles]; + const classDef = classes.get(cssClass); + if (classDef) { + nodeData.cssCompiledStyles = [...(nodeData.cssCompiledStyles ?? []), ...classDef.styles]; } }); } @@ -162,26 +176,24 @@ function insertOrUpdateNode(nodes, nodeData, classes) { * If there aren't any or if dbInfoItem isn't defined, return an empty string. * Else create 1 string from the list of classes found * - * @param {undefined | null | object} dbInfoItem - * @returns {string} */ -function getClassesFromDbInfo(dbInfoItem) { +function getClassesFromDbInfo(dbInfoItem?: State): string { return dbInfoItem?.classes?.join(' ') ?? ''; } -function getStylesFromDbInfo(dbInfoItem) { +function getStylesFromDbInfo(dbInfoItem?: State): string[] { return dbInfoItem?.styles ?? []; } export const dataFetcher = ( - parent, - parsedItem, - diagramStates, - nodes, - edges, - altFlag, - look, - classes + parent: StateStmt | undefined, + parsedItem: StateStmt, + diagramStates: Map, + nodes: NodeData[], + edges: Edge[], + altFlag: boolean, + look: MermaidConfig['look'], + classes: Map ) => { const itemId = parsedItem.id; const dbState = diagramStates.get(itemId); @@ -213,7 +225,7 @@ export const dataFetcher = ( }); } - const newNode = nodeDb.get(itemId); + const newNode = nodeDb.get(itemId)!; // Save data for description and group so that for instance a statement without description overwrites // one with description @todo TODO What does this mean? If important, add a test for it @@ -225,7 +237,7 @@ export const dataFetcher = ( newNode.shape = SHAPE_STATE_WITH_DESC; newNode.description.push(parsedItem.description); } else { - if (newNode.description?.length > 0) { + if (newNode.description?.length && newNode.description.length > 0) { // if there is a description already transform it to an array newNode.shape = SHAPE_STATE_WITH_DESC; if (newNode.description === itemId) { @@ -262,7 +274,7 @@ export const dataFetcher = ( } // This is what will be added to the graph - const nodeData = { + const nodeData: NodeData = { labelStyle: '', shape: newNode.shape, label: newNode.description, @@ -294,19 +306,19 @@ export const dataFetcher = ( if (parsedItem.note) { // Todo: set random id - const noteData = { + const noteData: NodeData = { labelStyle: '', shape: SHAPE_NOTE, label: parsedItem.note.text, cssClasses: CSS_DIAGRAM_NOTE, // useHtmlLabels: false, cssStyles: [], - cssCompilesStyles: [], + cssCompiledStyles: [], id: itemId + NOTE_ID + '-' + graphItemCount, domId: stateDomId(itemId, graphItemCount, NOTE), type: newNode.type, isGroup: newNode.type === 'group', - padding: getConfig().flowchart.padding, + padding: getConfig().flowchart!.padding, look, position: parsedItem.note.position, }; @@ -343,7 +355,7 @@ export const dataFetcher = ( let from = itemId; let to = noteData.id; - if (parsedItem.note.position === 'left of') { + if (parsedItem.note.position === 'left_of') { from = noteData.id; to = itemId; } diff --git a/packages/mermaid/src/diagrams/state/stateDb.ts b/packages/mermaid/src/diagrams/state/stateDb.ts index ccf94463bc..7109508e24 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.ts +++ b/packages/mermaid/src/diagrams/state/stateDb.ts @@ -23,40 +23,185 @@ import { STMT_STATE, STMT_STYLEDEF, } from './stateCommon.js'; +import type { MermaidConfig } from '../../config.type.js'; 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 = ','; +interface BaseStmt { + stmt: 'applyClass' | 'classDef' | 'dir' | 'relation' | 'state' | 'style' | 'root' | 'default'; +} + +interface ApplyClassStmt extends BaseStmt { + stmt: 'applyClass'; + id: string; + styleClass: string; +} + +interface ClassDefStmt extends BaseStmt { + stmt: 'classDef'; + id: string; + classes: string; +} + +interface DirectionStmt extends BaseStmt { + stmt: 'dir'; + value: 'TB' | 'BT' | 'RL' | 'LR'; +} + +interface RelationStmt extends BaseStmt { + stmt: 'relation'; + state1: StateStmt; + state2: StateStmt; + description?: string; +} + +export interface StateStmt extends BaseStmt { + stmt: 'state' | 'default'; + id: string; + type: 'default' | 'fork' | 'join' | 'choice' | 'divider'; + description?: string; + doc?: Stmt[]; + note?: Note; + start?: boolean; +} + +interface StyleStmt extends BaseStmt { + stmt: 'style'; + id: string; + styleClass: string; +} + +export interface RootStmt { + id: 'root'; + stmt: 'root'; + doc?: Stmt[]; +} + +interface Note { + position: 'left_of' | 'right_of'; + text: string; +} + +export type Stmt = + | ApplyClassStmt + | ClassDefStmt + | DirectionStmt + | RelationStmt + | StateStmt + | StyleStmt + | RootStmt; + +export interface State { + id: string; + descriptions: string[]; + type: string; + doc: Stmt[] | null; + note: { position?: string; text: string } | null; + classes: string[]; + styles: string[]; + textStyles: string[]; +} + +interface DiagramEdge { + id1: string; + id2: string; + relationTitle?: string; +} + +interface Document { + relations: DiagramEdge[]; + states: Map; + documents: Record; +} + +export interface StyleClass { + id: string; + styles: string[]; + textStyles: string[]; +} + +export interface NodeData { + labelStyle?: string; + shape: string; + label?: string | string[]; + cssClasses: string; + cssCompiledStyles?: string[]; + cssStyles: string[]; + id: string; + dir?: string; + domId?: string; + type?: string; + isGroup?: boolean; + padding?: number; + rx?: number; + ry?: number; + look?: MermaidConfig['look']; + parentId?: string; + centerLabel?: boolean; + position?: string; + description?: string | string[]; +} + +export interface Edge { + id: string; + start: string; + end: string; + arrowhead: string; + arrowTypeEnd: string; + style: string; + labelStyle: string; + label?: string; + arrowheadStyle: string; + labelpos: string; + labelType: string; + thickness: string; + classes: string; + look: MermaidConfig['look']; +} /** * 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} + * ClassDef information = \{ id: id, styles: [], textStyles: [] \} */ -function newClassesList() { - return new Map(); +function newClassesList(): Map { + return new Map(); } -const newDoc = () => { +const newDoc = (): Document => { return { - /** @type {{ id1: string, id2: string, relationTitle: string }[]} */ relations: [], states: new Map(), documents: {}, }; }; -const clone = (o) => JSON.parse(JSON.stringify(o)); +const clone = (o: unknown) => JSON.parse(JSON.stringify(o)); export class StateDB { + private nodes: NodeData[] = []; + private edges: Edge[] = []; + private direction: string = DEFAULT_DIAGRAM_DIRECTION; + private rootDoc: Stmt[] = []; + private classes: Map = newClassesList(); + private documents: { root: Document } = { root: newDoc() }; + private currentDocument: Document = this.documents.root; + private startEndCount = 0; + private dividerCnt = 0; + + static relationType = { + AGGREGATION: 0, + EXTENSION: 1, + COMPOSITION: 2, + DEPENDENCY: 3, + } as const; + constructor() { this.clear(); @@ -67,132 +212,68 @@ export class StateDB { this.trimColon = this.trimColon.bind(this); } - /** - * @private - * @type {Array} - */ - 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(), - }; - - /** - * @private - * @type {Object} - */ - currentDocument = this.documents.root; - /** - * @private - * @type {number} - */ - startEndCount = 0; - /** - * @private - * @type {number} - */ - dividerCnt = 0; - - static relationType = { - AGGREGATION: 0, - EXTENSION: 1, - COMPOSITION: 2, - DEPENDENCY: 3, - }; - - setRootDoc(o) { + setRootDoc(o: Stmt[]) { log.info('Setting root doc', o); - // rootDoc = { id: 'root', doc: o }; this.rootDoc = o; this.extract(o); } - getRootDoc() { - return this.rootDoc; - } - - /** - * @private - * @param {Object} parent - * @param {Object} node - * @param {boolean} first - */ - docTranslator(parent, node, first) { + docTranslator(parent: RootStmt | StateStmt, node: Stmt, first: boolean) { 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(); - } - } + return; + } - 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 (node.stmt !== STMT_STATE) { + return; + } - // 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; - } + if (node.id === '[*]') { + node.id = parent.id + (first ? '_start' : '_end'); + node.start = first; + } else { + node.id = node.id.trim(); + } + + if (!node.doc) { + return; + } - node.doc.forEach((docNode) => this.docTranslator(node, docNode, true)); + const doc = []; + let currentDoc = []; + for (const docItem of node.doc) { + if ('type' in docItem && docItem.type === DIVIDER_TYPE) { + const newNode = clone(docItem); + newNode.doc = clone(currentDoc); + doc.push(newNode); + currentDoc = []; + } else { + currentDoc.push(docItem); } } + + 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); + this.docTranslator( + { id: 'root', stmt: 'root' }, + { id: 'root', stmt: 'root', doc: this.rootDoc }, + true + ); return { id: 'root', doc: this.rootDoc }; - // Here } /** @@ -203,40 +284,16 @@ export class StateDB { * 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. - * @private - * @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); + extract(_statements: Stmt[] | { doc: Stmt[] }) { + // console.trace('Statements', _statements); this.clear(true); - - log.info('Extract initial document:', doc); - - doc.forEach((item) => { - log.warn('Statement', item.stmt); + const statements = Array.isArray(_statements) ? _statements : _statements.doc; + statements.forEach((item) => { + log.warn('Statement', item); 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 - ); + this.addState(item.id.trim(), item.type, item.doc, item.description, item.note); break; case STMT_RELATION: this.addRelation(item.state1, item.state2, item.description); @@ -255,7 +312,7 @@ export class StateDB { this.addState(trimmedId); foundState = this.getState(trimmedId); } - foundState.styles = styles.map((s) => s.replace(/;/g, '')?.trim()); + foundState!.styles = styles.map((s) => s.replace(/;/g, '')?.trim()); }); } break; @@ -272,7 +329,7 @@ export class StateDB { resetDataFetching(); dataFetcher( undefined, - this.getRootDocV2(), + this.getRootDocV2() as StateStmt, diagramStates, this.nodes, this.edges, @@ -282,7 +339,6 @@ export class StateDB { ); 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( @@ -300,27 +356,22 @@ export class StateDB { /** * 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. + * @param descr - description for the state. Can be a string or a list or strings + * @param 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 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 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 + id: string, + type: string = DEFAULT_STATE_TYPE, + doc: Stmt[] | null = null, + descr: string | string[] | null = null, + note: { position?: string; text: string } | null = null, + classes: string | string[] | null = null, + styles: string | string[] | null = null, + textStyles: string | string[] | null = 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, { @@ -334,11 +385,15 @@ export class StateDB { textStyles: [], }); } else { - if (!this.currentDocument.states.get(trimmedId).doc) { - this.currentDocument.states.get(trimmedId).doc = doc; + const state = this.currentDocument.states.get(trimmedId); + if (!state) { + throw new Error(`State not found: ${trimmedId}`); + } + if (!state.doc) { + state.doc = doc; } - if (!this.currentDocument.states.get(trimmedId).type) { - this.currentDocument.states.get(trimmedId).type = type; + if (!state.type) { + state.type = type; } } @@ -349,12 +404,15 @@ export class StateDB { } if (typeof descr === 'object') { - descr.forEach((des) => this.addDescription(trimmedId, des.trim())); + descr.forEach((des: string) => this.addDescription(trimmedId, des.trim())); } } if (note) { const doc2 = this.currentDocument.states.get(trimmedId); + if (!doc2) { + throw new Error(`State not found: ${trimmedId}`); + } doc2.note = note; doc2.note.text = common.sanitizeText(doc2.note.text, getConfig()); } @@ -362,23 +420,23 @@ export class StateDB { if (classes) { log.info('Setting state classes', trimmedId, classes); const classesList = typeof classes === 'string' ? [classes] : classes; - classesList.forEach((cssClass) => this.setCssClass(trimmedId, cssClass.trim())); + classesList.forEach((cssClass: string) => 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())); + stylesList.forEach((style: string) => 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())); + textStylesList.forEach((textStyle: string) => this.setTextStyle(trimmedId, textStyle.trim())); } } - clear(saveCommon) { + clear(saveCommon?: boolean) { this.nodes = []; this.edges = []; this.documents = { @@ -394,15 +452,18 @@ export class StateDB { } } - getState(id) { + getState(id: string) { return this.currentDocument.states.get(id); } + getStates() { return this.currentDocument.states; } + logDocuments() { log.info('Documents = ', this.documents); } + getRelations() { return this.currentDocument.relations; } @@ -411,10 +472,6 @@ export class StateDB { * 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; @@ -428,11 +485,6 @@ export class StateDB { /** * 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; @@ -442,10 +494,6 @@ export class StateDB { * 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; @@ -460,47 +508,19 @@ export class StateDB { * 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); + addRelationObjs(item1: StateStmt, item2: StateStmt, relationTitle = '') { + const id1 = this.startIdIfNeeded(item1.id.trim()); + const type1 = this.startTypeIfNeeded(item1.id.trim(), item1.type); + const id2 = this.startIdIfNeeded(item2.id.trim()); + const 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.addState(id1, type1, item1.doc, item1.description, item1.note); + this.addState(id2, type2, item2.doc, item2.description, item2.note); this.currentDocument.relations.push({ id1, @@ -511,15 +531,11 @@ export class StateDB { /** * 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') { + addRelation(item1: string | StateStmt, item2: string | StateStmt, title?: string) { + if (typeof item1 === 'object' && typeof item2 === 'object') { this.addRelationObjs(item1, item2, title); - } else { + } else if (typeof item1 === 'string' && typeof item2 === 'string') { const id1 = this.startIdIfNeeded(item1.trim()); const type1 = this.startTypeIfNeeded(item1); const id2 = this.endIdIfNeeded(item2.trim()); @@ -530,19 +546,19 @@ export class StateDB { this.currentDocument.relations.push({ id1, id2, - title: common.sanitizeText(title, getConfig()), + relationTitle: title ? common.sanitizeText(title, getConfig()) : undefined, }); } } - addDescription(id, descr) { + addDescription(id: string, descr: string) { const theState = this.currentDocument.states.get(id); const _descr = descr.startsWith(':') ? descr.replace(':', '').trim() : descr; - theState.descriptions.push(common.sanitizeText(_descr, getConfig())); + theState!.descriptions.push(common.sanitizeText(_descr, getConfig())); } - cleanupLabel(label) { - if (label.substring(0, 1) === ':') { + cleanupLabel(label: string) { + if (label.startsWith(':')) { return label.substr(2).trim(); } else { return label.trim(); @@ -558,21 +574,18 @@ export class StateDB { * 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) + * @param id - the id of this (style) class + * @param styleAttributes - the string with 1 or more style attributes (each separated by a comma) */ - addStyleClass(id, styleAttributes = '') { + addStyleClass(id: string, 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 + this.classes.set(id, { id, styles: [], textStyles: [] }); } const foundClass = this.classes.get(id); - if (styleAttributes !== undefined && styleAttributes !== null) { - styleAttributes.split(STYLECLASS_SEP).forEach((attrib) => { - // remove any trailing ; + if (styleAttributes !== undefined && styleAttributes !== null && foundClass) { + styleAttributes.split(STYLECLASS_SEP).forEach((attrib: string) => { 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); @@ -583,10 +596,6 @@ export class StateDB { } } - /** - * Return all of the style classes - * @returns {{} | any | classes} - */ getClasses() { return this.classes; } @@ -596,18 +605,18 @@ export class StateDB { * 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 + * @param itemIds - The id or a list of ids of the item(s) to apply the css class to + * @param cssClassName - CSS class name */ - setCssClass(itemIds, cssClassName) { - itemIds.split(',').forEach((id) => { + setCssClass(itemIds: string, cssClassName: string) { + itemIds.split(',').forEach((id: string) => { let foundState = this.getState(id); if (foundState === undefined) { const trimmedId = id.trim(); this.addState(trimmedId); foundState = this.getState(trimmedId); } - foundState.classes.push(cssClassName); + foundState!.classes.push(cssClassName); }); } @@ -618,10 +627,10 @@ export class StateDB { * 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 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: string, styleText: string) { const item = this.getState(itemId); if (item !== undefined) { item.styles.push(styleText); @@ -631,10 +640,10 @@ export class StateDB { /** * 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 + * @param itemId - The id of item to apply the css class to + * @param cssClassName - CSS class name */ - setTextStyle(itemId, cssClassName) { + setTextStyle(itemId: string, cssClassName: string) { const item = this.getState(itemId); if (item !== undefined) { item.textStyles.push(cssClassName); @@ -644,12 +653,13 @@ export class StateDB { getDirection() { return this.direction; } - setDirection(dir) { + + setDirection(dir: string) { this.direction = dir; } - trimColon(str) { - return str && str[0] === ':' ? str.substr(1).trim() : str.trim(); + trimColon(str: string) { + return str.startsWith(':') ? str.substr(1).trim() : str.trim(); } getData() { @@ -666,6 +676,7 @@ export class StateDB { getConfig() { return getConfig().state; } + getAccTitle = getAccTitle; setAccTitle = setAccTitle; getAccDescription = getAccDescription; From d2996dd553c5c19ea08e5c6b6526fcf15308108d Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 18 Feb 2025 00:23:58 +0530 Subject: [PATCH 05/10] fix: Statement handling --- .../mermaid/src/diagrams/state/stateCommon.ts | 4 + .../mermaid/src/diagrams/state/stateDb.ts | 113 ++++++++++-------- 2 files changed, 66 insertions(+), 51 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/stateCommon.ts b/packages/mermaid/src/diagrams/state/stateCommon.ts index 17a1bd24a2..04e58e3fde 100644 --- a/packages/mermaid/src/diagrams/state/stateCommon.ts +++ b/packages/mermaid/src/diagrams/state/stateCommon.ts @@ -10,6 +10,10 @@ export const DEFAULT_NESTED_DOC_DIR = 'TB'; // parsed statement type for a state export const STMT_STATE = 'state'; + +// parsed statement type for a root +export const STMT_ROOT = 'root'; + // parsed statement type for a relation export const STMT_RELATION = 'relation'; // parsed statement type for a classDef diff --git a/packages/mermaid/src/diagrams/state/stateDb.ts b/packages/mermaid/src/diagrams/state/stateDb.ts index d4f1808b15..faf5e18f51 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.ts +++ b/packages/mermaid/src/diagrams/state/stateDb.ts @@ -20,6 +20,7 @@ import { STMT_APPLYCLASS, STMT_CLASSDEF, STMT_RELATION, + STMT_ROOT, STMT_STATE, STMT_STYLEDEF, } from './stateCommon.js'; @@ -65,11 +66,15 @@ interface RelationStmt extends BaseStmt { export interface StateStmt extends BaseStmt { stmt: 'state' | 'default'; id: string; - type: 'default' | 'fork' | 'join' | 'choice' | 'divider'; + type: 'default' | 'fork' | 'join' | 'choice' | 'divider' | 'start' | 'end'; description?: string; + descriptions?: string[]; doc?: Stmt[]; note?: Note; start?: boolean; + classes?: string[]; + styles?: string[]; + textStyles?: string[]; } interface StyleStmt extends BaseStmt { @@ -85,7 +90,7 @@ export interface RootStmt { } interface Note { - position: 'left_of' | 'right_of'; + position?: 'left_of' | 'right_of'; text: string; } @@ -98,17 +103,6 @@ export type Stmt = | StyleStmt | RootStmt; -export interface State { - id: string; - descriptions: string[]; - type: string; - doc: Stmt[] | null; - note: { position?: string; text: string } | null; - classes: string[]; - styles: string[]; - textStyles: string[]; -} - interface DiagramEdge { id1: string; id2: string; @@ -117,7 +111,7 @@ interface DiagramEdge { interface Document { relations: DiagramEdge[]; - states: Map; + states: Map; documents: Record; } @@ -229,34 +223,35 @@ export class StateDB { return; } - if (node.stmt !== STMT_STATE) { - return; - } - - if (node.id === '[*]') { - node.id = parent.id + (first ? '_start' : '_end'); - node.start = first; - } else { - node.id = node.id.trim(); + if (node.stmt === STMT_STATE) { + if (node.id === '[*]') { + node.id = parent.id + (first ? '_start' : '_end'); + node.start = first; + } else { + // This is just a plain state, not a start or end + node.id = node.id.trim(); + } } - if (!node.doc) { + if ((node.stmt !== STMT_ROOT && node.stmt !== STMT_STATE) || !node.doc) { return; } const doc = []; + // Check for concurrency let currentDoc = []; - for (const docItem of node.doc) { - if ('type' in docItem && docItem.type === DIVIDER_TYPE) { - const newNode = clone(docItem); + for (const stmt of node.doc) { + if ((stmt as StateStmt).type === DIVIDER_TYPE) { + const newNode = clone(stmt); newNode.doc = clone(currentDoc); doc.push(newNode); currentDoc = []; } else { - currentDoc.push(docItem); + currentDoc.push(stmt); } } + // If any divider was encountered if (doc.length > 0 && currentDoc.length > 0) { const newNode = { stmt: STMT_STATE, @@ -273,11 +268,11 @@ export class StateDB { private getRootDocV2() { this.docTranslator( - { id: 'root', stmt: 'root' }, - { id: 'root', stmt: 'root', doc: this.rootDoc }, + { id: STMT_ROOT, stmt: STMT_ROOT }, + { id: STMT_ROOT, stmt: STMT_ROOT, doc: this.rootDoc }, true ); - return { id: 'root', doc: this.rootDoc }; + return { id: STMT_ROOT, doc: this.rootDoc }; } /** @@ -290,7 +285,6 @@ export class StateDB { * This will push the statement into the list of statements for the current document. */ extract(_statements: Stmt[] | { doc: Stmt[] }) { - // console.trace('Statements', _statements); this.clear(true); const statements = Array.isArray(_statements) ? _statements : _statements.doc; statements.forEach((item) => { @@ -367,18 +361,19 @@ export class StateDB { */ addState( id: string, - type: string = DEFAULT_STATE_TYPE, - doc: Stmt[] | null = null, - descr: string | string[] | null = null, - note: { position?: string; text: string } | null = null, - classes: string | string[] | null = null, - styles: string | string[] | null = null, - textStyles: string | string[] | null = null + type: StateStmt['type'] = DEFAULT_STATE_TYPE, + doc: Stmt[] | undefined = undefined, + descr: string | string[] | undefined = undefined, + note: Note | undefined = undefined, + classes: string | string[] | undefined = undefined, + styles: string | string[] | undefined = undefined, + textStyles: string | string[] | undefined = undefined ) { const trimmedId = id?.trim(); if (!this.currentDocument.states.has(trimmedId)) { log.info('Adding state ', trimmedId, descr); this.currentDocument.states.set(trimmedId, { + stmt: STMT_STATE, id: trimmedId, descriptions: [], type, @@ -490,7 +485,7 @@ export class StateDB { * If the id is a start node ( [*] ), then return the start type ('start') * else return the given type */ - startTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { + startTypeIfNeeded(id = '', type: StateStmt['type'] = DEFAULT_STATE_TYPE) { return id === START_NODE ? START_TYPE : type; } @@ -513,7 +508,7 @@ export class StateDB { * else return the given type * */ - endTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { + endTypeIfNeeded(id = '', type: StateStmt['type'] = DEFAULT_STATE_TYPE) { return id === END_NODE ? END_TYPE : type; } @@ -522,10 +517,26 @@ export class StateDB { const type1 = this.startTypeIfNeeded(item1.id.trim(), item1.type); const id2 = this.startIdIfNeeded(item2.id.trim()); const type2 = this.startTypeIfNeeded(item2.id.trim(), item2.type); - - this.addState(id1, type1, item1.doc, item1.description, item1.note); - this.addState(id2, type2, item2.doc, item2.description, item2.note); - + 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, @@ -558,12 +569,12 @@ export class StateDB { addDescription(id: string, descr: string) { const theState = this.currentDocument.states.get(id); const _descr = descr.startsWith(':') ? descr.replace(':', '').trim() : descr; - theState!.descriptions.push(common.sanitizeText(_descr, getConfig())); + theState?.descriptions?.push(common.sanitizeText(_descr, getConfig())); } cleanupLabel(label: string) { if (label.startsWith(':')) { - return label.substr(2).trim(); + return label.slice(2).trim(); } else { return label.trim(); } @@ -620,7 +631,7 @@ export class StateDB { this.addState(trimmedId); foundState = this.getState(trimmedId); } - foundState!.classes.push(cssClassName); + foundState?.classes?.push(cssClassName); }); } @@ -637,7 +648,7 @@ export class StateDB { setStyle(itemId: string, styleText: string) { const item = this.getState(itemId); if (item !== undefined) { - item.styles.push(styleText); + item.styles?.push(styleText); } } @@ -650,7 +661,7 @@ export class StateDB { setTextStyle(itemId: string, cssClassName: string) { const item = this.getState(itemId); if (item !== undefined) { - item.textStyles.push(cssClassName); + item.textStyles?.push(cssClassName); } } @@ -663,7 +674,7 @@ export class StateDB { } trimColon(str: string) { - return str.startsWith(':') ? str.substr(1).trim() : str.trim(); + return str.startsWith(':') ? str.slice(1).trim() : str.trim(); } getData() { From 91cbe5bc0133cf01abf1a39a43af7f7fd8457d55 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 18 Feb 2025 00:29:57 +0530 Subject: [PATCH 06/10] fix: StateStmt import --- packages/mermaid/src/diagrams/state/dataFetcher.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/dataFetcher.ts b/packages/mermaid/src/diagrams/state/dataFetcher.ts index b390b9d658..d15217ae3f 100644 --- a/packages/mermaid/src/diagrams/state/dataFetcher.ts +++ b/packages/mermaid/src/diagrams/state/dataFetcher.ts @@ -34,7 +34,7 @@ import { STMT_RELATION, STMT_STATE, } from './stateCommon.js'; -import type { Edge, NodeData, State, StateStmt, Stmt, StyleClass } from './stateDb.js'; +import type { Edge, NodeData, StateStmt, Stmt, StyleClass } from './stateDb.js'; // List of nodes created from the parsed diagram statement items const nodeDb = new Map(); @@ -59,7 +59,7 @@ export function stateDomId( const setupDoc = ( parentParsedItem: StateStmt | undefined, doc: Stmt[], - diagramStates: Map, + diagramStates: Map, nodes: NodeData[], edges: Edge[], altFlag: boolean, @@ -177,18 +177,18 @@ function insertOrUpdateNode( * Else create 1 string from the list of classes found * */ -function getClassesFromDbInfo(dbInfoItem?: State): string { +function getClassesFromDbInfo(dbInfoItem?: StateStmt): string { return dbInfoItem?.classes?.join(' ') ?? ''; } -function getStylesFromDbInfo(dbInfoItem?: State): string[] { +function getStylesFromDbInfo(dbInfoItem?: StateStmt): string[] { return dbInfoItem?.styles ?? []; } export const dataFetcher = ( parent: StateStmt | undefined, parsedItem: StateStmt, - diagramStates: Map, + diagramStates: Map, nodes: NodeData[], edges: Edge[], altFlag: boolean, From a4754ad195e70d52fbd46ef44f40797d2d215e41 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 18 Feb 2025 00:33:47 +0530 Subject: [PATCH 07/10] chore: Add changeset --- .changeset/eleven-wolves-deny.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eleven-wolves-deny.md diff --git a/.changeset/eleven-wolves-deny.md b/.changeset/eleven-wolves-deny.md new file mode 100644 index 0000000000..76bb69ec58 --- /dev/null +++ b/.changeset/eleven-wolves-deny.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +chore: Convert StateDB into TypeScript From 6650efc1a6e5d1390dd355742c3955f069bcb661 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 18 Feb 2025 00:51:38 +0530 Subject: [PATCH 08/10] fix: Note position --- .../mermaid/src/diagrams/state/dataFetcher.ts | 2 +- .../mermaid/src/diagrams/state/stateDb.ts | 2 +- .../diagrams/state/stateDiagram-v2.spec.js | 32 +++++++++++++++++-- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/dataFetcher.ts b/packages/mermaid/src/diagrams/state/dataFetcher.ts index d15217ae3f..6a84996f83 100644 --- a/packages/mermaid/src/diagrams/state/dataFetcher.ts +++ b/packages/mermaid/src/diagrams/state/dataFetcher.ts @@ -355,7 +355,7 @@ export const dataFetcher = ( let from = itemId; let to = noteData.id; - if (parsedItem.note.position === 'left_of') { + if (parsedItem.note.position === 'left of') { from = noteData.id; to = itemId; } diff --git a/packages/mermaid/src/diagrams/state/stateDb.ts b/packages/mermaid/src/diagrams/state/stateDb.ts index faf5e18f51..9de4a7a94e 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.ts +++ b/packages/mermaid/src/diagrams/state/stateDb.ts @@ -90,7 +90,7 @@ export interface RootStmt { } interface Note { - position?: 'left_of' | 'right_of'; + position?: 'left of' | 'right of'; text: string; } diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js index d5dc96f58e..b84eb18e99 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js @@ -4,6 +4,7 @@ import { StateDB } from './stateDb.js'; describe('state diagram V2, ', function () { // TODO - these examples should be put into ./parser/stateDiagram.spec.js describe('when parsing an info graph it', function () { + /** @type {StateDB} */ let stateDb; beforeEach(function () { stateDb = new StateDB(2); @@ -346,6 +347,20 @@ describe('state diagram V2, ', function () { `; parser.parse(str); + expect(stateDb.getState('Active').note).toMatchInlineSnapshot(` + { + "position": "left of", + "text": "this is a short
note", + } + `); + expect(stateDb.getState('Inactive').note).toMatchInlineSnapshot(` + { + "position": "right of", + "text": "A note can also + be defined on + several lines", + } + `); }); it('should handle multiline notes with different line breaks', function () { const str = `stateDiagram-v2 @@ -356,6 +371,12 @@ describe('state diagram V2, ', function () { `; parser.parse(str); + expect(stateDb.getStates().get('State1').note).toMatchInlineSnapshot(` + { + "position": "right of", + "text": "Line1
Line2
Line3
Line4
Line5", + } + `); }); it('should handle floating notes', function () { const str = `stateDiagram-v2 @@ -366,15 +387,14 @@ describe('state diagram V2, ', function () { parser.parse(str); }); it('should handle floating notes', function () { - const str = `stateDiagram-v2\n + const str = `stateDiagram-v2 state foo note "This is a floating note" as N1 `; - parser.parse(str); }); it('should handle notes for composite (nested) states', function () { - const str = `stateDiagram-v2\n + const str = `stateDiagram-v2 [*] --> NotShooting state "Not Shooting State" as NotShooting { @@ -389,6 +409,12 @@ describe('state diagram V2, ', function () { `; parser.parse(str); + expect(stateDb.getState('NotShooting').note).toMatchInlineSnapshot(` + { + "position": "right of", + "text": "This is a note on a composite state", + } + `); }); it('A composite state should be able to link to itself', () => { From 39a5abc714a8c0e82137f7191b131f8dd4e66bef Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 18 Feb 2025 13:50:49 +0530 Subject: [PATCH 09/10] chore: Minor refactors to stateDB --- .../mermaid/src/diagrams/state/stateDb.ts | 289 ++++++++---------- 1 file changed, 134 insertions(+), 155 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/stateDb.ts b/packages/mermaid/src/diagrams/state/stateDb.ts index 9de4a7a94e..52e01d13cf 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.ts +++ b/packages/mermaid/src/diagrams/state/stateDb.ts @@ -11,7 +11,7 @@ import { setAccTitle, setDiagramTitle, } from '../common/commonDb.js'; -import { dataFetcher, reset as resetDataFetching } from './dataFetcher.js'; +import { dataFetcher, reset as resetDataFetcher } from './dataFetcher.js'; import { getDir } from './stateRenderer-v3-unified.js'; import { DEFAULT_DIAGRAM_DIRECTION, @@ -26,14 +26,16 @@ import { } from './stateCommon.js'; import type { MermaidConfig } from '../../config.type.js'; -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 = ','; +const CONSTANTS = { + START_NODE: '[*]', + START_TYPE: 'start', + END_NODE: '[*]', + END_TYPE: 'end', + COLOR_KEYWORD: 'color', + FILL_KEYWORD: 'fill', + BG_FILL: 'bgFill', + STYLECLASS_SEP: ',', +} as const; interface BaseStmt { stmt: 'applyClass' | 'classDef' | 'dir' | 'relation' | 'state' | 'style' | 'root' | 'default'; @@ -159,37 +161,32 @@ export interface Edge { classes: string; look: MermaidConfig['look']; } + /** * 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: [] \} */ -function newClassesList(): Map { - return new Map(); -} - -const newDoc = (): Document => { - return { - relations: [], - states: new Map(), - documents: {}, - }; -}; - -const clone = (o: unknown) => JSON.parse(JSON.stringify(o)); +const newClassesList = (): Map => new Map(); +const newDoc = (): Document => ({ + relations: [], + states: new Map(), + documents: {}, +}); +const clone = (o: T): T => JSON.parse(JSON.stringify(o)); export class StateDB { private nodes: NodeData[] = []; private edges: Edge[] = []; - private direction: string = DEFAULT_DIAGRAM_DIRECTION; + private direction = DEFAULT_DIAGRAM_DIRECTION; private rootDoc: Stmt[] = []; - private classes: Map = newClassesList(); - private documents: { root: Document } = { root: newDoc() }; - private currentDocument: Document = this.documents.root; + private classes = newClassesList(); + private documents = { root: newDoc() }; + private currentDocument = this.documents.root; private startEndCount = 0; private dividerCnt = 0; - static relationType = { + static readonly relationType = { AGGREGATION: 0, EXTENSION: 1, COMPOSITION: 2, @@ -198,14 +195,91 @@ export class StateDB { constructor(private version: 1 | 2) { this.clear(); - - // Needed for JISON since it only supports direct properties + // Bind methods used by JISON this.setRootDoc = this.setRootDoc.bind(this); this.getDividerId = this.getDividerId.bind(this); this.setDirection = this.setDirection.bind(this); this.trimColon = this.trimColon.bind(this); } + /** + * 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. + */ + extract(statements: Stmt[] | { doc: Stmt[] }) { + this.clear(true); + for (const item of Array.isArray(statements) ? statements : statements.doc) { + switch (item.stmt) { + case STMT_STATE: + this.addState(item.id.trim(), item.type, item.doc, item.description, item.note); + 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: + this.handleStyleDef(item); + break; + case STMT_APPLYCLASS: + this.setCssClass(item.id.trim(), item.styleClass); + break; + } + } + const diagramStates = this.getStates(); + const config = getConfig(); + + resetDataFetcher(); + dataFetcher( + undefined, + this.getRootDocV2() as StateStmt, + diagramStates, + this.nodes, + this.edges, + true, + config.look, + this.classes + ); + + // Process node labels + for (const node of this.nodes) { + if (!Array.isArray(node.label)) { + continue; + } + + 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}]` + ); + } + node.label = node.label[0]; + } + } + + private handleStyleDef(item: StyleStmt) { + const ids = item.id.trim().split(','); + const styles = item.styleClass.split(','); + + for (const id of ids) { + let state = this.getState(id); + if (!state) { + const trimmedId = id.trim(); + this.addState(trimmedId); + state = this.getState(trimmedId); + } + if (state) { + state.styles = styles.map((s) => s.replace(/;/g, '')?.trim()); + } + } + } + setRootDoc(o: Stmt[]) { log.info('Setting root doc', o); this.rootDoc = o; @@ -224,7 +298,7 @@ export class StateDB { } if (node.stmt === STMT_STATE) { - if (node.id === '[*]') { + if (node.id === CONSTANTS.START_NODE) { node.id = parent.id + (first ? '_start' : '_end'); node.start = first; } else { @@ -242,7 +316,7 @@ export class StateDB { let currentDoc = []; for (const stmt of node.doc) { if ((stmt as StateStmt).type === DIVIDER_TYPE) { - const newNode = clone(stmt); + const newNode = clone(stmt as StateStmt); newNode.doc = clone(currentDoc); doc.push(newNode); currentDoc = []; @@ -258,7 +332,7 @@ export class StateDB { id: generateId(), type: 'divider', doc: clone(currentDoc), - }; + } satisfies StateStmt; doc.push(clone(newNode)); node.doc = doc; } @@ -275,82 +349,6 @@ export class StateDB { return { id: STMT_ROOT, doc: this.rootDoc }; } - /** - * 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. - */ - extract(_statements: Stmt[] | { doc: Stmt[] }) { - this.clear(true); - const statements = Array.isArray(_statements) ? _statements : _statements.doc; - statements.forEach((item) => { - log.warn('Statement', item); - switch (item.stmt) { - case STMT_STATE: - this.addState(item.id.trim(), item.type, item.doc, item.description, item.note); - 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() as StateStmt, - diagramStates, - this.nodes, - this.edges, - true, - look, - this.classes - ); - this.nodes.forEach((node) => { - if (Array.isArray(node.label)) { - 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. * @@ -398,13 +396,8 @@ export class StateDB { 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: string) => this.addDescription(trimmedId, des.trim())); - } + const descriptions = Array.isArray(descr) ? descr : [descr]; + descriptions.forEach((des) => this.addDescription(trimmedId, des.trim())); } if (note) { @@ -418,29 +411,27 @@ export class StateDB { if (classes) { log.info('Setting state classes', trimmedId, classes); - const classesList = typeof classes === 'string' ? [classes] : classes; - classesList.forEach((cssClass: string) => this.setCssClass(trimmedId, cssClass.trim())); + const classesList = Array.isArray(classes) ? 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: string) => this.setStyle(trimmedId, style.trim())); + const stylesList = Array.isArray(styles) ? 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: string) => this.setTextStyle(trimmedId, textStyle.trim())); + const textStylesList = Array.isArray(textStyles) ? textStyles : [textStyles]; + textStylesList.forEach((textStyle) => this.setTextStyle(trimmedId, textStyle.trim())); } } clear(saveCommon?: boolean) { this.nodes = []; this.edges = []; - this.documents = { - root: newDoc(), - }; + this.documents = { root: newDoc() }; this.currentDocument = this.documents.root; // number of start and end nodes; used to construct ids @@ -473,12 +464,11 @@ export class StateDB { * else return the given id */ startIdIfNeeded(id = '') { - let fixedId = id; - if (id === START_NODE) { + if (id === CONSTANTS.START_NODE) { this.startEndCount++; - fixedId = `${START_TYPE}${this.startEndCount}`; + return `${CONSTANTS.START_TYPE}${this.startEndCount}`; } - return fixedId; + return id; } /** @@ -486,7 +476,7 @@ export class StateDB { * else return the given type */ startTypeIfNeeded(id = '', type: StateStmt['type'] = DEFAULT_STATE_TYPE) { - return id === START_NODE ? START_TYPE : type; + return id === CONSTANTS.START_NODE ? CONSTANTS.START_TYPE : type; } /** @@ -495,12 +485,11 @@ export class StateDB { * else return the given id */ endIdIfNeeded(id = '') { - let fixedId = id; - if (id === END_NODE) { + if (id === CONSTANTS.END_NODE) { this.startEndCount++; - fixedId = `${END_TYPE}${this.startEndCount}`; + return `${CONSTANTS.END_TYPE}${this.startEndCount}`; } - return fixedId; + return id; } /** @@ -509,7 +498,7 @@ export class StateDB { * */ endTypeIfNeeded(id = '', type: StateStmt['type'] = DEFAULT_STATE_TYPE) { - return id === END_NODE ? END_TYPE : type; + return id === CONSTANTS.END_NODE ? CONSTANTS.END_TYPE : type; } addRelationObjs(item1: StateStmt, item2: StateStmt, relationTitle = '') { @@ -573,16 +562,12 @@ export class StateDB { } cleanupLabel(label: string) { - if (label.startsWith(':')) { - return label.slice(2).trim(); - } else { - return label.trim(); - } + return label.startsWith(':') ? label.slice(2).trim() : label.trim(); } getDividerId() { this.dividerCnt++; - return 'divider-id-' + this.dividerCnt; + return `divider-id-${this.dividerCnt}`; } /** @@ -598,12 +583,12 @@ export class StateDB { this.classes.set(id, { id, styles: [], textStyles: [] }); } const foundClass = this.classes.get(id); - if (styleAttributes !== undefined && styleAttributes !== null && foundClass) { - styleAttributes.split(STYLECLASS_SEP).forEach((attrib: string) => { + if (styleAttributes && foundClass) { + styleAttributes.split(CONSTANTS.STYLECLASS_SEP).forEach((attrib) => { const fixedAttrib = attrib.replace(/([^;]*);/, '$1').trim(); - if (RegExp(COLOR_KEYWORD).exec(attrib)) { - const newStyle1 = fixedAttrib.replace(FILL_KEYWORD, BG_FILL); - const newStyle2 = newStyle1.replace(COLOR_KEYWORD, FILL_KEYWORD); + if (RegExp(CONSTANTS.COLOR_KEYWORD).exec(attrib)) { + const newStyle1 = fixedAttrib.replace(CONSTANTS.FILL_KEYWORD, CONSTANTS.BG_FILL); + const newStyle2 = newStyle1.replace(CONSTANTS.COLOR_KEYWORD, CONSTANTS.FILL_KEYWORD); foundClass.textStyles.push(newStyle2); } foundClass.styles.push(fixedAttrib); @@ -624,9 +609,9 @@ export class StateDB { * @param cssClassName - CSS class name */ setCssClass(itemIds: string, cssClassName: string) { - itemIds.split(',').forEach((id: string) => { + itemIds.split(',').forEach((id) => { let foundState = this.getState(id); - if (foundState === undefined) { + if (!foundState) { const trimmedId = id.trim(); this.addState(trimmedId); foundState = this.getState(trimmedId); @@ -646,10 +631,7 @@ export class StateDB { * @param styleText - the text of the attributes for the style */ setStyle(itemId: string, styleText: string) { - const item = this.getState(itemId); - if (item !== undefined) { - item.styles?.push(styleText); - } + this.getState(itemId)?.styles?.push(styleText); } /** @@ -659,10 +641,7 @@ export class StateDB { * @param cssClassName - CSS class name */ setTextStyle(itemId: string, cssClassName: string) { - const item = this.getState(itemId); - if (item !== undefined) { - item.textStyles?.push(cssClassName); - } + this.getState(itemId)?.textStyles?.push(cssClassName); } getDirection() { From bb4b92a66350f8f48d87f8a31a79a22153059455 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Wed, 19 Feb 2025 18:59:30 +0530 Subject: [PATCH 10/10] fix: Type issue in getDirectionStatement --- packages/mermaid/src/diagrams/state/stateDb.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/state/stateDb.ts b/packages/mermaid/src/diagrams/state/stateDb.ts index a578356cb0..853a0e22f6 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.ts +++ b/packages/mermaid/src/diagrams/state/stateDb.ts @@ -649,7 +649,7 @@ export class StateDB { * @returns the direction statement if present */ private getDirectionStatement() { - return this.rootDoc.find((doc) => doc.stmt === STMT_DIRECTION); + return this.rootDoc.find((doc): doc is DirectionStmt => doc.stmt === STMT_DIRECTION); } getDirection() {