diff --git a/.cspell/code-terms.txt b/.cspell/code-terms.txt index f2b37dcaf..bdde26d14 100644 --- a/.cspell/code-terms.txt +++ b/.cspell/code-terms.txt @@ -110,6 +110,7 @@ strikethrough stringifying struct STYLECLASS +STYLEDEF STYLEOPTS subcomponent subcomponents diff --git a/cypress/integration/rendering/stateDiagram-v2.spec.js b/cypress/integration/rendering/stateDiagram-v2.spec.js index 9a1a27abe..7bc467c83 100644 --- a/cypress/integration/rendering/stateDiagram-v2.spec.js +++ b/cypress/integration/rendering/stateDiagram-v2.spec.js @@ -558,6 +558,29 @@ stateDiagram-v2 { logLevel: 0, fontFamily: 'courier' } ); }); + it(' can have styles applied ', () => { + imgSnapshotTest( + ` +stateDiagram-v2 +AState +style AState fill:#636,border:1px solid red,color:white; + `, + { logLevel: 0, fontFamily: 'courier' } + ); + }); + it(' should let styles take preceedence over classes', () => { + imgSnapshotTest( + ` +stateDiagram-v2 +AState: Should NOT be white +BState +classDef exampleStyleClass fill:#fff,color: blue; +class AState,BState exampleStyleClass +style AState fill:#636,border:1px solid red,color:white; + `, + { logLevel: 0, fontFamily: 'courier' } + ); + }); }); it('1433: should render a simple state diagram with a title', () => { imgSnapshotTest( diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index f94c6d82e..7f3c1a5b7 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -75,25 +75,127 @@
----- -config: - theme: neo - look: neo - layout: elk - elk.mergeEdges: true - themeVariables: {} ---- +Case 1
++++stateDiagram-v2 +AState: Should NOT be white +BState +classDef exampleStyleClass fill:#fff,color: blue; +class AState,BState exampleStyleClass +style AState fill:#636,border:1px solid red,color:white; +++ %%{init: {"look": "classic"} }%% +stateDiagram-v2 +AState: Should NOT be white +BState +classDef exampleStyleClass fill:#fff,color: blue; +class AState,BState exampleStyleClass +style AState fill:#636,border:1px solid red,color:white; +++stateDiagram-v2 +classDef exampleStyleClass background:#bbb,border:1px solid red; +a --> b +class a exampleStyleClass +%% a:::exampleStyleClass ++stateDiagram - direction TB - A --> B - A --> C - A --> D - A --> E - A --> F -+ direction TB + + accTitle: This is the accessible title + accDescr: This is an accessible description + + classDef notMoving fill:white + classDef movement font-style:italic; + classDef badBadEvent fill:#f00,color:white,font-weight:bold,stroke-width:2px,stroke:yellow + + [*] --> Still:::notMoving + Still --> [*] + Still --> Moving:::movement + Moving --> Still + Moving --> Crash:::movement + Crash:::badBadEvent --> [*] + ++stateDiagram-v2 + MyState + note left of MyState : I am a leftie + note right of MyState : I am a rightie + +++stateDiagram +%% direction LR + + state C0 { + A0 --> B0 + } + + C0 --> Apa0 + +++stateDiagram +direction LR + + + state C1 { + A1 --> B1 + } + + C1 --> Apa1 + ++Case 2
+++stateDiagram +direction LR + state Gorilla0 { + state Apa0 { + A0 --> B0 + } + + } + Apa --> Gorilla0:Label + A0 --> C0 + %% C1: "`This is C`" +++stateDiagram +direction LR + state Apa1 { + A1 + } + + Apa11 --> Apa1 + A1 --> C1 + %% C1: "`This is C`" +++stateDiagram + [*] --> Level1 + + state Level1 { + [*] --> Level2 + + state Level2 { + [*] --> level2 + level2 --> Level3 + + state Level3 { + [*] --> level3 + level3 --> [*] + } + } + } ++--- config: @@ -184,20 +286,7 @@ Apa --> C A --> B & C["C"]--stateDiagram -direction LR - state Gorilla0 { - state Apa0 { - A0 --> B0 - } - } - Apa0 --> C0 - A0 --> C0 - C1: "`This is C`" -flowchart LR subgraph Gorilla @@ -237,7 +326,7 @@ stateDiagram-+flowchart LR Apa --Hello--> C @@ -375,7 +464,7 @@ stateDiagram-v2 mermaid.initialize({ theme: 'neo', // handdrawnSeed: 12, - look: 'neo', + look: 'handdrawn', // 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX', // layout: 'dagre', // layout: 'elk', diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.ts index 7fe2e3a73..66c852a14 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDb.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDb.ts @@ -806,7 +806,8 @@ const addNodeFromVertex = ( labelStyle: '', parentId, padding: config.flowchart?.padding || 8, - cssStyles: vertex.styles.join(' '), + cssStyles: vertex.styles, + cssCompiledStyles: getCompiledStyles(vertex.classes), cssClasses: vertex.classes.join(' '), shape: getTypeFromVertex(vertex), dir: vertex.dir, @@ -820,6 +821,22 @@ const addNodeFromVertex = ( } }; +function getCompiledStyles(classDefs: string[]) { + let compiledStyles: string[] = []; + for (const customClass of classDefs) { + const cssClass = classes.get(customClass); + if (cssClass) { + if (cssClass.styles) { + compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])]; + } + if (cssClass.textStyles) { + compiledStyles = [...compiledStyles, ...(cssClass.textStyles ?? [])]; + } + } + } + return compiledStyles; +} + export const getData = () => { const config = getConfig(); const nodes: Node[] = []; @@ -844,7 +861,7 @@ export const getData = () => { labelStyle: '', parentId: parentDB.get(subGraph.id), padding: config.flowchart?.padding || 8, - cssStyles: '', + cssStyles: [], cssClasses: '', shape: 'rect', dir: subGraph.dir, diff --git a/packages/mermaid/src/diagrams/state/dataFetcher.js b/packages/mermaid/src/diagrams/state/dataFetcher.js index a94fb681d..cfc78ff88 100644 --- a/packages/mermaid/src/diagrams/state/dataFetcher.js +++ b/packages/mermaid/src/diagrams/state/dataFetcher.js @@ -54,21 +54,39 @@ export function stateDomId(itemId = '', counter = 0, type = '', typeSpacer = DOM return `${DOMID_STATE}-${itemId}${typeStr}-${counter}`; } -const setupDoc = (parentParsedItem, doc, diagramStates, nodes, edges, altFlag, look) => { +const setupDoc = (parentParsedItem, doc, diagramStates, nodes, edges, altFlag, look, classes) => { // graphItemCount = 0; log.trace('items', doc); doc.forEach((item) => { switch (item.stmt) { case STMT_STATE: - dataFetcher(parentParsedItem, item, diagramStates, nodes, edges, altFlag, look); + dataFetcher(parentParsedItem, item, diagramStates, nodes, edges, altFlag, look, classes); break; case DEFAULT_STATE_TYPE: - dataFetcher(parentParsedItem, item, diagramStates, nodes, edges, altFlag, look); + dataFetcher(parentParsedItem, item, diagramStates, nodes, edges, altFlag, look, classes); break; case STMT_RELATION: { - dataFetcher(parentParsedItem, item.state1, diagramStates, nodes, edges, altFlag, look); - dataFetcher(parentParsedItem, item.state2, diagramStates, nodes, edges, altFlag, look); + dataFetcher( + parentParsedItem, + item.state1, + diagramStates, + nodes, + edges, + altFlag, + look, + classes + ); + dataFetcher( + parentParsedItem, + item.state2, + diagramStates, + nodes, + edges, + altFlag, + look, + classes + ); const edgeData = { id: 'edge' + graphItemCount, start: item.state1.id, @@ -134,8 +152,9 @@ let cssClasses = newClassesList(); // style classes defined by a classDef * * @param nodes * @param nodeData + * @param classes */ -function insertOrUpdateNode(nodes, nodeData) { +function insertOrUpdateNode(nodes, nodeData, classes) { if (!nodeData.id || nodeData.id === '' || nodeData.id === '') { return; } @@ -143,29 +162,62 @@ function insertOrUpdateNode(nodes, nodeData) { //Populate node style attributes if nodeData has classes defined if (nodeData.cssClasses) { nodeData.cssClasses.split(' ').forEach((cssClass) => { - if (cssClasses[cssClass]) { - cssClasses[cssClass].styles.forEach((style) => { - // Populate nodeData with style attributes specifically to be used by rough.js - if (style && style.startsWith('fill:')) { - nodeData.backgroundColor = style.replace('fill:', ''); - } - if (style && style.startsWith('stroke:')) { - nodeData.borderColor = style.replace('stroke:', ''); - } - if (style && style.startsWith('stroke-width:')) { - nodeData.borderWidth = style.replace('stroke-width:', ''); - } + if (classes.get(cssClass)) { + const classDef = classes.get(cssClass); + // classDef.styles.forEach((style) => { + // nodeData.cssCompiledStyles += style + ','; + // // Populate nodeData with style attributes specifically to be used by rough.js + // if (style && style.startsWith('fill:')) { + // nodeData.backgroundColor = style.replace('fill:', ''); + // } + // if (style && style.startsWith('stroke:')) { + // nodeData.borderColor = style.replace('stroke:', ''); + // } + // if (style && style.startsWith('stroke-width:')) { + // nodeData.borderWidth = style.replace('stroke-width:', ''); + // } - nodeData.cssStyles += style + ';'; - }); - cssClasses[cssClass].textStyles.forEach((style) => { - nodeData.labelStyle += style + ';'; - if (style && style.startsWith('fill:')) { - nodeData.labelTextColor = style.replace('fill:', ''); - } - }); + // nodeData.cssStyles += style + ';'; + // }); + + // classDef.textStyles.forEach((style) => { + // nodeData.labelStyle += style + ';'; + // if (style && style.startsWith('fill:')) { + // nodeData.labelTextColor = style.replace('fill:', ''); + // } + // }); + nodeData.cssCompiledStyles = [...nodeData.cssCompiledStyles, ...classDef.styles]; } }); + //Populate node style attributes if nodeData has classes defined + if (nodeData.cssStyles) { + // nodeData.cssStyles.split(' ').forEach((csStyle) => { + // if (classes[cssClass]) { + // classes[cssClass].styles.forEach((style) => { + // // Populate nodeData with style attributes specifically to be used by rough.js + // if (style && style.startsWith('fill:')) { + // nodeData.backgroundColor = style.replace('fill:', ''); + // } + // if (style && style.startsWith('stroke:')) { + // nodeData.borderColor = style.replace('stroke:', ''); + // } + // if (style && style.startsWith('stroke-width:')) { + // nodeData.borderWidth = style.replace('stroke-width:', ''); + // } + // nodeData.cssStyles += style + ';'; + // }); + // classes[cssClass].textStyles.forEach((style) => { + // nodeData.labelStyle += style + ';'; + // if (style && style.startsWith('fill:')) { + // nodeData.labelTextColor = style.replace('fill:', ''); + // } + // }); + // } + // }); + } + // nodeData.labelTextColor = '#ffffff'; + // nodeData.labelStyle = 'color:#ffffff'; + // nodeData.cssStyles = 'fill:#f77'; } const existingNodeData = nodes.find((node) => node.id === nodeData.id); if (existingNodeData) { @@ -197,7 +249,7 @@ function getClassesFromDbInfo(dbInfoItem) { } //add comma for all other classes else { - classStr += dbInfoItem.classes[i] + ' '; + classStr += dbInfoItem.classes[i] + ','; } } return classStr; @@ -206,9 +258,41 @@ function getClassesFromDbInfo(dbInfoItem) { } } } -export const dataFetcher = (parent, parsedItem, diagramStates, nodes, edges, altFlag, look) => { +/** + * Get classes from the db for the info item. + * 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 getStylesFromDbInfo(dbInfoItem) { + if (dbInfoItem === undefined || dbInfoItem === null) { + return; + } else { + if (dbInfoItem.styles) { + return dbInfoItem.styles; + } else { + return []; + } + } +} +export const dataFetcher = ( + parent, + parsedItem, + diagramStates, + nodes, + edges, + altFlag, + look, + classes +) => { const itemId = parsedItem.id; - const classStr = getClassesFromDbInfo(diagramStates.get(itemId)); + const dbState = diagramStates.get(itemId); + const classStr = getClassesFromDbInfo(dbState); + const style = getStylesFromDbInfo(dbState); + + log.info('dataFetcher parsedItem', parsedItem, dbState, style); if (itemId !== 'root') { let shape = SHAPE_STATE; @@ -229,6 +313,7 @@ export const dataFetcher = (parent, parsedItem, diagramStates, nodes, edges, alt shape, description: common.sanitizeText(itemId, getConfig()), cssClasses: `${classStr} ${CSS_DIAGRAM_STATE}`, + cssStyles: style, }; } @@ -287,7 +372,8 @@ export const dataFetcher = (parent, parsedItem, diagramStates, nodes, edges, alt shape: newNode.shape, label: newNode.description, cssClasses: newNode.cssClasses, - cssStyles: '', + cssCompiledStyles: [], + cssStyles: newNode.cssStyles, id: itemId, dir: newNode.dir, domId: stateDomId(itemId, graphItemCount), @@ -319,7 +405,8 @@ export const dataFetcher = (parent, parsedItem, diagramStates, nodes, edges, alt label: parsedItem.note.text, cssClasses: CSS_DIAGRAM_NOTE, // useHtmlLabels: false, - cssStyles: '', // styles.style, + cssStyles: [], + cssCompilesStyles: [], id: itemId + NOTE_ID + '-' + graphItemCount, domId: stateDomId(itemId, graphItemCount, NOTE), type: newNode.type, @@ -334,7 +421,7 @@ export const dataFetcher = (parent, parsedItem, diagramStates, nodes, edges, alt shape: SHAPE_NOTEGROUP, label: parsedItem.note.text, cssClasses: newNode.cssClasses, - cssStyles: '', // styles.style, + cssStyles: [], id: itemId + PARENT_ID, domId: stateDomId(itemId, graphItemCount, PARENT), type: 'group', @@ -352,11 +439,11 @@ export const dataFetcher = (parent, parsedItem, diagramStates, nodes, edges, alt nodeData.parentId = parentNodeId; //insert groupData - insertOrUpdateNode(nodes, groupData); + insertOrUpdateNode(nodes, groupData, classes); //insert noteData - insertOrUpdateNode(nodes, noteData); + insertOrUpdateNode(nodes, noteData, classes); //insert nodeData - insertOrUpdateNode(nodes, nodeData); + insertOrUpdateNode(nodes, nodeData, classes); let from = itemId; let to = noteData.id; @@ -382,12 +469,12 @@ export const dataFetcher = (parent, parsedItem, diagramStates, nodes, edges, alt look, }); } else { - insertOrUpdateNode(nodes, nodeData); + insertOrUpdateNode(nodes, nodeData, classes); } } if (parsedItem.doc) { log.trace('Adding nodes children '); - setupDoc(parsedItem, parsedItem.doc, diagramStates, nodes, edges, !altFlag, look); + setupDoc(parsedItem, parsedItem.doc, diagramStates, nodes, edges, !altFlag, look, classes); } }; 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 1be9148b8..fed63c444 100644 --- a/packages/mermaid/src/diagrams/state/parser/state-style.spec.js +++ b/packages/mermaid/src/diagrams/state/parser/state-style.spec.js @@ -217,4 +217,50 @@ describe('ClassDefs and classes when parsing a State diagram', () => { }); }); }); + + describe('style statement for a state (style)', () => { + describe('defining (style)', () => { + it('has "style" as a keyword, an id, and can set a css style attribute', function () { + stateDiagram.parser.parse(`stateDiagram-v2 + id1 + style id1 background:#bbb`); + stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); + const data4Layout = stateDiagram.parser.yy.getData(); + + expect(data4Layout.nodes[0].cssStyles).toEqual(['background:#bbb']); + }); + it('has "style" as a keyword, an id, and can set a css style attribute', function () { + stateDiagram.parser.parse(`stateDiagram-v2 + id1 + id2 + style id1,id2 background:#bbb`); + stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); + const data4Layout = stateDiagram.parser.yy.getData(); + + expect(data4Layout.nodes[0].cssStyles).toEqual(['background:#bbb']); + expect(data4Layout.nodes[1].cssStyles).toEqual(['background:#bbb']); + }); + + it('can define multiple attributes separated by commas', function () { + stateDiagram.parser.parse(`stateDiagram-v2 + id1 + id2 + style id1,id2 background:#bbb, font-weight:bold, font-style:italic;`); + + stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); + const data4Layout = stateDiagram.parser.yy.getData(); + + expect(data4Layout.nodes[0].cssStyles).toEqual([ + 'background:#bbb', + 'font-weight:bold', + 'font-style:italic', + ]); + expect(data4Layout.nodes[1].cssStyles).toEqual([ + 'background:#bbb', + 'font-weight:bold', + 'font-style:italic', + ]); + }); + }); + }); }); diff --git a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison index 44235ecd4..e3bc51235 100644 --- a/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison +++ b/packages/mermaid/src/diagrams/state/parser/stateDiagram.jison @@ -27,6 +27,13 @@ %x CLASSDEFID %x CLASS %x CLASS_STYLE + +// Style statement states +%x STYLE +%x STYLE_IDS +%x STYLEDEF_STYLES +%x STYLEDEF_STYLEOPTS + %x NOTE %x NOTE_ID %x NOTE_TEXT @@ -75,6 +82,10 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili(\w+)+((","\s*\w+)*) { this.popState(); this.pushState('CLASS_STYLE'); return 'CLASSENTITY_IDS' } [^\n]* { this.popState(); return 'STYLECLASS' } + "style"\s+ { this.pushState('STYLE'); return 'style'; } +