From 275e01acba557addbf2d863bf963946f1ee512e9 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Fri, 5 Jan 2024 15:13:15 +0100 Subject: [PATCH] #3358 Adding support for classDef and class statements --- cypress/platform/knsv2.html | 19 ++- .../mermaid/src/diagrams/block/blockDB.ts | 118 +++++++++++++++++- .../src/diagrams/block/blockRenderer.ts | 17 +++ .../mermaid/src/diagrams/block/blockTypes.ts | 10 ++ .../src/diagrams/block/parser/block.jison | 35 +++++- .../src/diagrams/block/renderHelpers.ts | 2 + .../src/diagrams/flowchart/flowRenderer-v2.js | 2 +- packages/mermaid/src/mermaidAPI.ts | 3 + 8 files changed, 200 insertions(+), 6 deletions(-) diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index 82381d3cf..a6553304c 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -73,6 +73,22 @@ block-beta
 block-beta
       columns 3
+      classDef green fill:#9f6,stroke:#333,stroke-width:2px;
+      A
+      B
+      class A green
+    
+
+stateDiagram-v2
+      classDef green fill:#9f6,stroke:#333,stroke-width:2px;
+      A
+      class A green
+
+    
+
+block-beta
+      columns 3
+      classDef green fill:#9f6,stroke:#333,stroke-width:2px;
       A
       space
       block
@@ -80,6 +96,7 @@ block-beta
         F
       end
       E -- "apa" --> A
+
     
 block-beta
@@ -538,7 +555,7 @@ mindmap
         // console.error('Mermaid error: ', err);
       };
       mermaid.initialize({
-        // theme: 'forest',
+        theme: 'forest',
         startOnLoad: true,
         logLevel: 0,
         flowchart: {
diff --git a/packages/mermaid/src/diagrams/block/blockDB.ts b/packages/mermaid/src/diagrams/block/blockDB.ts
index c3861feab..4a3a6a146 100644
--- a/packages/mermaid/src/diagrams/block/blockDB.ts
+++ b/packages/mermaid/src/diagrams/block/blockDB.ts
@@ -1,6 +1,6 @@
 // import type { BlockDB } from './blockTypes.js';
 import type { DiagramDB } from '../../diagram-api/types.js';
-import type { BlockConfig, BlockType, Block, Link } from './blockTypes.js';
+import type { BlockConfig, BlockType, Block, Link, ClassDef } from './blockTypes.js';
 
 import * as configApi from '../../config.js';
 import {
@@ -20,10 +20,112 @@ let blockDatabase: Record = {};
 let edgeList: Block[] = [];
 let edgeCount: Record = {};
 
+const COLOR_KEYWORD = 'color';
+const FILL_KEYWORD = 'fill';
+const BG_FILL = 'bgFill';
+const STYLECLASS_SEP = ',';
+
+let classes = {} as Record;
+
+/**
+ * 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: string, styleAttributes = '') {
+  // create a new style class object with this id
+  if (classes[id] === undefined) {
+    classes[id] = { id: id, styles: [], textStyles: [] }; // This is a classDef
+  }
+  const foundClass = classes[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 (attrib.match(COLOR_KEYWORD)) {
+        const newStyle1 = fixedAttrib.replace(FILL_KEYWORD, BG_FILL);
+        const newStyle2 = newStyle1.replace(COLOR_KEYWORD, FILL_KEYWORD);
+        foundClass.textStyles.push(newStyle2);
+      }
+      foundClass.styles.push(fixedAttrib);
+    });
+  }
+};
+
+/**
+ * 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: string, cssClassName: string) {
+  console.log('abc88 setCssClass enter', itemIds, cssClassName);
+  itemIds.split(',').forEach(function (id: string) {
+    let foundBlock = blockDatabase[id];
+    if (foundBlock === undefined) {
+      const trimmedId = id.trim();
+      blockDatabase[trimmedId] = { id: trimmedId, type: 'na', children: [] } as Block;
+      foundBlock = blockDatabase[trimmedId];
+    }
+    if (!foundBlock.classes) {
+      foundBlock.classes = [];
+    }
+    foundBlock.classes.push(cssClassName);
+    console.log('abc88 setCssClass', foundBlock);
+  });
+};
+
+// /**
+//  * 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.textStyles.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 populateBlockDatabase = (_blockList: Block[], parent: Block): void => {
   const blockList = _blockList.flat();
   const children = [];
   for (const block of blockList) {
+    if (block.type === 'classDef') {
+      console.log('abc88 classDef', block);
+      addStyleClass(block.id, block.css);
+      continue;
+    }
+    if (block.type === 'applyClass') {
+      console.log('abc88 applyClass', block);
+      // addStyleClass(block.id, block.css);
+      setCssClass(block.id, block.styleClass);
+      continue;
+    }
     if (block.type === 'column-setting') {
       parent.columns = block.columns || -1;
     } else if (block.type === 'edge') {
@@ -87,6 +189,7 @@ const clear = (): void => {
   rootBlock = { id: 'root', type: 'composite', children: [], columns: -1 } as Block;
   blockDatabase = { root: rootBlock };
   blocks = [] as Block[];
+  classes = {} as Record;
 
   edgeList = [];
   edgeCount = {};
@@ -166,7 +269,7 @@ const setHierarchy = (block: Block[]): void => {
   log.debug('The document from parsing', JSON.stringify(block, null, 2));
   rootBlock.children = block;
   populateBlockDatabase(block, rootBlock);
-  log.debug('The document after popuplation', JSON.stringify(rootBlock, null, 2));
+  log.debug('abc88 The document after popuplation', JSON.stringify(rootBlock, null, 2));
   blocks = rootBlock.children;
 };
 
@@ -231,6 +334,15 @@ const getLinks: IGetLinks = () => links;
 type IGetLogger = () => Console;
 const getLogger: IGetLogger = () => console;
 
+type IGetClasses = () => Record;
+/**
+ * Return all of the style classes
+ * @returns {{} | any | classes}
+ */
+export const getClasses = function () {
+  console.log('abc88 block db getClasses', classes);
+  return classes;
+};
 export interface BlockDB extends DiagramDB {
   clear: () => void;
   getConfig: () => BlockConfig | undefined;
@@ -243,6 +355,7 @@ export interface BlockDB extends DiagramDB {
   setBlock: ISetBlock;
   getLinks: IGetLinks;
   getColumns: IGetColumns;
+  getClasses: IGetClasses;
   typeStr2Type: ITypeStr2Type;
   edgeTypeStr2Type: IEdgeTypeStr2Type;
   edgeStrToEdgeData: IEdgeStrToEdgeDataType;
@@ -271,6 +384,7 @@ const db: BlockDB = {
   // getDiagramTitle,
   // setDiagramTitle,
   getColumns,
+  getClasses,
   clear,
   generateId,
 };
diff --git a/packages/mermaid/src/diagrams/block/blockRenderer.ts b/packages/mermaid/src/diagrams/block/blockRenderer.ts
index 77e9cd939..69f0eefb0 100644
--- a/packages/mermaid/src/diagrams/block/blockRenderer.ts
+++ b/packages/mermaid/src/diagrams/block/blockRenderer.ts
@@ -17,6 +17,22 @@ import type { Block } from './blockTypes.js';
 // import { diagram as BlockDiagram } from './blockDiagram.js';
 import { configureSvgSize } from '../../setupGraphViewbox.js';
 
+/**
+ * Returns the all the styles from classDef statements in the graph definition.
+ *
+ * @param text
+ * @param diagObj
+ * @returns {object} ClassDef styles
+ */
+export const getClasses = function (text: any, diagObj: any) {
+  log.info('abc88 Extracting classes', diagObj.db.getClasses());
+  try {
+    return diagObj.db.getClasses();
+  } catch (e) {
+    return;
+  }
+};
+
 export const draw = async function (
   text: string,
   id: string,
@@ -99,4 +115,5 @@ export const draw = async function (
 
 export default {
   draw,
+  getClasses,
 };
diff --git a/packages/mermaid/src/diagrams/block/blockTypes.ts b/packages/mermaid/src/diagrams/block/blockTypes.ts
index 4be03d959..241ad3934 100644
--- a/packages/mermaid/src/diagrams/block/blockTypes.ts
+++ b/packages/mermaid/src/diagrams/block/blockTypes.ts
@@ -28,6 +28,8 @@ export type BlockType =
   | 'cylinder'
   | 'group'
   | 'doublecircle'
+  | 'classDef'
+  | 'applyClass'
   | 'composite';
 
 export interface Block {
@@ -53,9 +55,17 @@ export interface Block {
   columns?: number; // | TBlockColumnsDefaultValue;
   classes?: string[];
   directions?: string[];
+  css?: string;
+  styleClass?: string;
 }
 
 export interface Link {
   source: Block;
   target: Block;
 }
+
+export interface ClassDef {
+  id: string;
+  textStyles: string[];
+  styles: string[];
+}
diff --git a/packages/mermaid/src/diagrams/block/parser/block.jison b/packages/mermaid/src/diagrams/block/parser/block.jison
index 13932abd1..9228a3b8a 100644
--- a/packages/mermaid/src/diagrams/block/parser/block.jison
+++ b/packages/mermaid/src/diagrams/block/parser/block.jison
@@ -19,6 +19,10 @@
 %x BLOCK_ARROW
 %x ARROW_DIR
 %x LLABEL
+%x CLASS
+%x CLASS_STYLE
+%x CLASSDEF
+%x CLASSDEFID
 
 
 // as per section 6.1 of RFC 2234 [2]
@@ -53,8 +57,16 @@ space                  { yytext = '1'; yy.getLogger().info('COLUMNS (LEX)', yyte
 "default"             return 'DEFAULT';
 "linkStyle"           return 'LINKSTYLE';
 "interpolate"         return 'INTERPOLATE';
-"classDef"            return 'CLASSDEF';
-"class"               return 'CLASS';
+
+"classDef"\s+   { this.pushState('CLASSDEF'); return 'classDef'; }
+DEFAULT\s+            { this.popState(); this.pushState('CLASSDEFID'); return 'DEFAULT_CLASSDEF_ID' }
+\w+\s+                { this.popState(); this.pushState('CLASSDEFID'); return 'CLASSDEF_ID' }
+[^\n]*              { this.popState(); return 'CLASSDEF_STYLEOPTS' }
+
+"class"\s+      { this.pushState('CLASS'); return 'class'; }
+(\w+)+((","\s*\w+)*)     { this.popState(); this.pushState('CLASS_STYLE'); return 'CLASSENTITY_IDS' }
+[^\n]*             { this.popState(); return 'STYLECLASS' }
+
 accTitle\s*":"\s*                                               { this.pushState("acc_title");return 'acc_title'; }
 (?!\n|;|#)*[^\n]*                                    { this.popState(); return "acc_title_value"; }
 accDescr\s*":"\s*                                               { this.pushState("acc_descr");return 'acc_descr'; }
@@ -194,6 +206,8 @@ statement
   | SPACE_BLOCK
     { const num=parseInt($1); const spaceId = yy.generateId(); $$ = { id: spaceId, type:'space', label:'', width: num, children: [] }}
   | blockStatement
+  | classDefStatement
+  | cssClassStatement
 	;
 
 nodeStatement
@@ -240,4 +254,21 @@ nodeShapeNLabel
     	      { yy.getLogger().info("Rule: BLOCK_ARROW nodeShapeNLabel: ", $1, $2, " #3:",$3, $4); $$ = { typeStr: $1 + $4, label: $2, directions: $3}; }
   ;
 
+
+classDefStatement
+  : classDef CLASSDEF_ID CLASSDEF_STYLEOPTS {
+      $$ = { type: 'classDef', id: $2.trim(), css: $3.trim() };
+      }
+  | classDef DEFAULT CLASSDEF_STYLEOPTS {
+      $$ = { type: 'classDef', id: $2.trim(), css: $3.trim() };
+      }
+  ;
+
+cssClassStatement
+    : class CLASSENTITY_IDS STYLECLASS {
+        //console.log('apply class: id(s): ',$2, '  style class: ', $3);
+        $$={ type: 'applyClass', id: $2.trim(), styleClass: $3.trim() };
+        }
+    ;
+
 %%
diff --git a/packages/mermaid/src/diagrams/block/renderHelpers.ts b/packages/mermaid/src/diagrams/block/renderHelpers.ts
index 107139ff6..ebe88efdc 100644
--- a/packages/mermaid/src/diagrams/block/renderHelpers.ts
+++ b/packages/mermaid/src/diagrams/block/renderHelpers.ts
@@ -16,8 +16,10 @@ function getNodeFromBlock(block: Block, db: BlockDB, positioned = false) {
 
   let classStr = 'default';
   if ((vertex?.classes?.length || 0) > 0) {
+    console.log('abc88 vertex.classes', block.id, vertex?.classes);
     classStr = (vertex?.classes || []).join(' ');
   }
+  console.log('abc88 vertex.classes done');
   classStr = classStr + ' flowchart-label';
 
   // We create a SVG label, either by delegating to addHtmlLabel or manually
diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js
index a887511d5..7c964b4e7 100644
--- a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js
+++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js
@@ -363,7 +363,7 @@ export const getClasses = function (text, diagObj) {
  * @param _version
  * @param diagObj
  */
-// [MermaidChart: 33a97b35-1f95-4ce9-81b5-3038669bc170]
+
 export const draw = async function (text, id, _version, diagObj) {
   log.info('Drawing flowchart');
   diagObj.db.clear();
diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts
index 4d8d95290..467f60c4e 100644
--- a/packages/mermaid/src/mermaidAPI.ts
+++ b/packages/mermaid/src/mermaidAPI.ts
@@ -39,6 +39,7 @@ const CLASSDEF_DIAGRAMS = [
   'flowchart-elk',
   'stateDiagram',
   'stateDiagram-v2',
+  'block',
 ];
 const MAX_TEXTLENGTH = 50_000;
 const MAX_TEXTLENGTH_EXCEEDED_MSG =
@@ -203,6 +204,8 @@ export const createCssStyles = (
     cssStyles += `\n:root { --mermaid-alt-font-family: ${config.altFontFamily}}`;
   }
 
+  console.log('abc88 expr check', !isEmpty(classDefs), classDefs);
+
   // classDefs defined in the diagram text
   if (!isEmpty(classDefs) && CLASSDEF_DIAGRAMS.includes(graphType)) {
     const htmlLabels = config.htmlLabels || config.flowchart?.htmlLabels; // TODO why specifically check the Flowchart diagram config?