From 9f6fc5a074cafa24ca1a444aecde9c5fc3d146ff Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Mon, 29 Apr 2024 11:19:06 +0200 Subject: [PATCH] #5237 WIP --- cypress/platform/knsv2.html | 15 +- .../mermaid/src/diagrams/state/stateCommon.ts | 54 ++- .../mermaid/src/diagrams/state/stateDb.js | 382 ++++++++++++++---- .../src/diagrams/state/stateDiagram-v2.ts | 2 +- .../layout-algorithms/dagre/index.js | 3 + 5 files changed, 369 insertions(+), 87 deletions(-) diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index fd6b2d969..522b28cf7 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -78,16 +78,13 @@
 stateDiagram-v2
-    state fork_state <>
-      [*] --> fork_state
-      fork_state --> State2
-      fork_state --> State3
+    [*] --> First
+    state First {
+        [*] --> second
+        second --> [*]
+    }
+
 
-      state join_state <>
-      State2 --> join_state
-      State3 --> join_state
-      join_state --> State4
-      State4 --> [*]
   
diff --git a/packages/mermaid/src/diagrams/state/stateCommon.ts b/packages/mermaid/src/diagrams/state/stateCommon.ts
index e847d1514..5c28b23a7 100644
--- a/packages/mermaid/src/diagrams/state/stateCommon.ts
+++ b/packages/mermaid/src/diagrams/state/stateCommon.ts
@@ -27,8 +27,36 @@ export const G_EDGE_LABELPOS = 'c';
 export const G_EDGE_LABELTYPE = 'text';
 export const G_EDGE_THICKNESS = 'normal';
 
-export const CSS_EDGE = 'transition';
+export const SHAPE_STATE = 'rect';
+export const SHAPE_STATE_WITH_DESC = 'rectWithTitle';
+export const SHAPE_START = 'stateStart';
+export const SHAPE_END = 'stateEnd';
+export const SHAPE_DIVIDER = 'divider';
+export const SHAPE_GROUP = 'roundedWithTitle';
+export const SHAPE_NOTE = 'note';
+export const SHAPE_NOTEGROUP = 'noteGroup';
+
+// CSS classes
 export const CSS_DIAGRAM = 'statediagram';
+export const CSS_STATE = 'state';
+export const CSS_DIAGRAM_STATE = `${CSS_DIAGRAM}-${CSS_STATE}`;
+export const CSS_EDGE = 'transition';
+export const CSS_NOTE = 'note';
+export const CSS_NOTE_EDGE = 'note-edge';
+export const CSS_EDGE_NOTE_EDGE = `${CSS_EDGE} ${CSS_NOTE_EDGE}`;
+export const CSS_DIAGRAM_NOTE = `${CSS_DIAGRAM}-${CSS_NOTE}`;
+export const CSS_CLUSTER = 'cluster';
+export const CSS_DIAGRAM_CLUSTER = `${CSS_DIAGRAM}-${CSS_CLUSTER}`;
+export const CSS_CLUSTER_ALT = 'cluster-alt';
+export const CSS_DIAGRAM_CLUSTER_ALT = `${CSS_DIAGRAM}-${CSS_CLUSTER_ALT}`;
+
+export const PARENT = 'parent';
+export const NOTE = 'note';
+export const DOMID_STATE = 'state';
+export const DOMID_TYPE_SPACER = '----';
+export const NOTE_ID = `${DOMID_TYPE_SPACER}${NOTE}`;
+export const PARENT_ID = `${DOMID_TYPE_SPACER}${PARENT}`;
+// --------------------------------------
 
 export default {
   DEFAULT_DIAGRAM_DIRECTION,
@@ -46,4 +74,28 @@ export default {
   G_EDGE_THICKNESS,
   CSS_EDGE,
   CSS_DIAGRAM,
+  SHAPE_STATE,
+  SHAPE_STATE_WITH_DESC,
+  SHAPE_START,
+  SHAPE_END,
+  SHAPE_DIVIDER,
+  SHAPE_GROUP,
+  SHAPE_NOTE,
+  SHAPE_NOTEGROUP,
+  CSS_STATE,
+  CSS_DIAGRAM_STATE,
+  CSS_NOTE,
+  CSS_NOTE_EDGE,
+  CSS_EDGE_NOTE_EDGE,
+  CSS_DIAGRAM_NOTE,
+  CSS_CLUSTER,
+  CSS_DIAGRAM_CLUSTER,
+  CSS_CLUSTER_ALT,
+  CSS_DIAGRAM_CLUSTER_ALT,
+  PARENT,
+  NOTE,
+  DOMID_STATE,
+  DOMID_TYPE_SPACER,
+  NOTE_ID,
+  PARENT_ID,
 };
diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.js
index 210d8199f..a2550dff9 100644
--- a/packages/mermaid/src/diagrams/state/stateDb.js
+++ b/packages/mermaid/src/diagrams/state/stateDb.js
@@ -26,8 +26,27 @@ import {
   G_EDGE_LABELTYPE,
   G_EDGE_THICKNESS,
   CSS_EDGE,
+  DEFAULT_NESTED_DOC_DIR,
+  SHAPE_DIVIDER,
+  SHAPE_GROUP,
+  CSS_DIAGRAM_CLUSTER,
+  CSS_DIAGRAM_CLUSTER_ALT,
+  CSS_DIAGRAM_STATE,
+  SHAPE_STATE_WITH_DESC,
+  SHAPE_STATE,
+  SHAPE_START,
+  SHAPE_END,
+  SHAPE_NOTE,
+  SHAPE_NOTEGROUP,
+  CSS_DIAGRAM_NOTE,
+  DOMID_TYPE_SPACER,
+  DOMID_STATE,
+  NOTE_ID,
+  PARENT_ID,
+  NOTE,
+  PARENT,
 } from './stateCommon.js';
-import { rect } from 'dagre-d3-es/src/dagre-js/intersect/index.js';
+import { node } from 'stylis';
 
 const START_NODE = '[*]';
 const START_TYPE = 'start';
@@ -54,6 +73,12 @@ let direction = DEFAULT_DIAGRAM_DIRECTION;
 let rootDoc = [];
 let classes = newClassesList(); // style classes defined by a classDef
 
+// --------------------------------------
+// List of nodes created from the parsed diagram statement items
+let nodeDb = {};
+
+let graphItemCount = 0; // used to construct ids, etc.
+
 const newDoc = () => {
   return {
     relations: [],
@@ -548,97 +573,257 @@ const setDirection = (dir) => {
 
 const trimColon = (str) => (str && str[0] === ':' ? str.substr(1).trim() : str.trim());
 
-const dataFetcher = (parentId, doc, nodes, edges) => {
-  extract(doc);
+const dataFetcher = (parent, parsedItem, diagramStates, nodes, edges, altFlag, useRough) => {
+  console.log(
+    'parent, parsedItemm, diagramStates, nodes, edges, altFlag, useRough:',
+    parent,
+    parsedItem,
+    diagramStates,
+    nodes,
+    edges,
+    altFlag,
+    useRough
+  );
+  const itemId = parsedItem.id;
+  const classStr = getClassesFromDbInfo(diagramStates[itemId]);
 
-  //states
-  const useRough = true;
-  const stateKeys = Object.keys(currentDocument.states);
-
-  stateKeys.forEach((key) => {
-    const item = currentDocument.states[key];
-    let itemShape = 'rect';
-    if (item.type === 'default' && item.id === 'root_start') {
-      itemShape = 'stateStart';
+  if (itemId !== 'root') {
+    let shape = SHAPE_STATE;
+    if (parsedItem.start === true) {
+      shape = SHAPE_START;
     }
-    if (item.type === 'default' && item.id === 'root_end') {
-      itemShape = 'stateEnd';
+    if (parsedItem.start === false) {
+      shape = SHAPE_END;
+    }
+    if (parsedItem.type !== DEFAULT_STATE_TYPE) {
+      shape = parsedItem.type;
     }
 
-    if (item.type === 'fork' || item.type === 'join') {
-      itemShape = 'forkJoin';
+    // Add the node to our list (nodeDb)
+    if (!nodeDb[itemId]) {
+      nodeDb[itemId] = {
+        id: itemId,
+        shape,
+        description: common.sanitizeText(itemId, getConfig()),
+        classes: `${classStr} ${CSS_DIAGRAM_STATE}`,
+      };
     }
 
-    if (item.type === 'choice') {
-      itemShape = 'choice';
+    const newNode = nodeDb[itemId];
+    console.log('New Node:', newNode);
+
+    // 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
+
+    // Build of the array of description strings
+    if (parsedItem.description) {
+      if (Array.isArray(newNode.description)) {
+        // There already is an array of strings,add to it
+        newNode.shape = SHAPE_STATE_WITH_DESC;
+        newNode.description.push(parsedItem.description);
+      } else {
+        if (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) {
+            // If the previous description was this, remove it
+            newNode.description = [parsedItem.description];
+          } else {
+            newNode.description = [newNode.description, parsedItem.description];
+          }
+        } else {
+          newNode.shape = SHAPE_STATE;
+          newNode.description = parsedItem.description;
+        }
+      }
+      newNode.description = common.sanitizeTextOrArray(newNode.description, getConfig());
     }
 
-    if (item.id === '' && item.type === 'default') {
-      //ignore this item
-      return;
+    // If there's only 1 description entry, just use a regular state shape
+    if (newNode.description?.length === 1 && newNode.shape === SHAPE_STATE_WITH_DESC) {
+      newNode.shape = SHAPE_STATE;
     }
 
-    if (item.id === '' && item.type === 'default') {
-      //ignore this item
-      return;
+    // group
+    if (!newNode.type && parsedItem.doc) {
+      log.info('Setting cluster for ', itemId, getDir(parsedItem));
+      newNode.type = 'group';
+      newNode.dir = getDir(parsedItem);
+      newNode.shape = parsedItem.type === DIVIDER_TYPE ? SHAPE_DIVIDER : SHAPE_GROUP;
+      newNode.classes =
+        newNode.classes +
+        ' ' +
+        CSS_DIAGRAM_CLUSTER +
+        ' ' +
+        (altFlag ? CSS_DIAGRAM_CLUSTER_ALT : '');
     }
 
-    if (parentId) {
-      nodes.push({
-        ...item,
-        labelText: item.id,
-        labelType: 'text',
-        parentId,
-        shape: itemShape,
-        useRough,
+    // This is what will be added to the graph
+    const nodeData = {
+      labelStyle: '',
+      shape: newNode.shape,
+      labelText: newNode.description,
+      classes: newNode.classes,
+      style: '',
+      id: itemId,
+      dir: newNode.dir,
+      domId: stateDomId(itemId, graphItemCount),
+      type: newNode.type,
+      padding: 15,
+      rx: 10,
+      ry: 10,
+      useRough,
+    };
+
+    if (parent && parent.id !== 'root') {
+      log.trace('Setting node ', itemId, ' to be child of its parent ', parent.id);
+      nodeData.parentId = parent.id;
+    }
+
+    nodeData.centerLabel = true;
+
+    if (parsedItem.note) {
+      // Todo: set random id
+      const noteData = {
+        labelStyle: '',
+        shape: SHAPE_NOTE,
+        labelText: parsedItem.note.text,
+        classes: CSS_DIAGRAM_NOTE,
+        // useHtmlLabels: false,
+        style: '', // styles.style,
+        id: itemId + NOTE_ID + '-' + graphItemCount,
+        domId: stateDomId(itemId, graphItemCount, NOTE),
+        type: newNode.type,
+        padding: 15, //getConfig().flowchart.padding
+      };
+      const groupData = {
+        labelStyle: '',
+        shape: SHAPE_NOTEGROUP,
+        labelText: parsedItem.note.text,
+        classes: newNode.classes,
+        style: '', // styles.style,
+        id: itemId + PARENT_ID,
+        domId: stateDomId(itemId, graphItemCount, PARENT),
+        type: 'group',
+        padding: 0, //getConfig().flowchart.padding
+      };
+      graphItemCount++;
+
+      const parentNodeId = itemId + PARENT_ID;
+
+      //add parent id to groupData
+      groupData.id = parentNodeId;
+      //add parent id to noteData
+      noteData.parentId = parentId;
+
+      nodes.push(groupData);
+      nodes.push(noteData);
+      nodes.push(nodeData);
+
+      let from = itemId;
+      let to = noteData.id;
+
+      if (parsedItem.note.position === 'left of') {
+        from = noteData.id;
+        to = itemId;
+      }
+
+      edges.push({
+        id: from + '-' + to,
+        start: from,
+        end: to,
+        arrowhead: 'none',
+        arrowTypeEnd: '',
+        style: G_EDGE_STYLE,
+        labelStyle: '',
+        classes: CSS_EDGE_NOTE_EDGE,
+        arrowheadStyle: G_EDGE_ARROWHEADSTYLE,
+        labelpos: G_EDGE_LABELPOS,
+        labelType: G_EDGE_LABELTYPE,
+        thickness: G_EDGE_THICKNESS,
       });
     } else {
-      nodes.push({
-        ...item,
-        id: item.id,
-        labelText: item.descriptions?.length > 0 ? item.descriptions[0] : item.id,
-        // description: item.id,
-        labelType: 'text',
-        labelStyle: '',
-        shape: itemShape,
-        padding: 15,
-        classes: ' statediagram-state',
-        rx: 10,
-        ry: 10,
-        useRough,
-      });
+      nodes.push(nodeData);
+    }
+
+    console.log('Nodes:', nodes);
+  }
+  if (parsedItem.doc) {
+    log.trace('Adding nodes children ');
+    setupDoc(parsedItem, parsedItem.doc, diagramStates, nodes, edges, !altFlag, useRough);
+  }
+};
+
+/**
+ * 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) {
+  const typeStr = type !== null && type.length > 0 ? `${typeSpacer}${type}` : '';
+  return `${DOMID_STATE}-${itemId}${typeStr}-${counter}`;
+}
+
+const setupDoc = (parentParsedItem, doc, diagramStates, nodes, edges, altFlag, useRough) => {
+  // graphItemCount = 0;
+  log.trace('items', doc);
+  doc.forEach((item) => {
+    switch (item.stmt) {
+      case STMT_STATE:
+        dataFetcher(parentParsedItem, item, diagramStates, nodes, edges, altFlag, useRough);
+        break;
+      case DEFAULT_STATE_TYPE:
+        dataFetcher(parentParsedItem, item, diagramStates, nodes, edges, altFlag, useRough);
+        break;
+      case STMT_RELATION:
+        {
+          dataFetcher(
+            parentParsedItem,
+            item.state1,
+            diagramStates,
+            nodes,
+            edges,
+            altFlag,
+            useRough
+          );
+          dataFetcher(
+            parentParsedItem,
+            item.state2,
+            diagramStates,
+            nodes,
+            edges,
+            altFlag,
+            useRough
+          );
+          const edgeData = {
+            id: 'edge' + graphItemCount,
+            start: item.state1.id,
+            end: item.state2.id,
+            arrowhead: 'normal',
+            arrowTypeEnd: 'arrow_barb',
+            style: G_EDGE_STYLE,
+            labelStyle: '',
+            label: common.sanitizeText(item.description, getConfig()),
+            arrowheadStyle: G_EDGE_ARROWHEADSTYLE,
+            labelpos: G_EDGE_LABELPOS,
+            labelType: G_EDGE_LABELTYPE,
+            thickness: G_EDGE_THICKNESS,
+            classes: CSS_EDGE,
+          };
+          edges.push(edgeData);
+          //g.setEdge(item.state1.id, item.state2.id, edgeData, graphItemCount);
+          graphItemCount++;
+        }
+        break;
     }
   });
-
-  //edges
-  currentDocument.relations.forEach((item) => {
-    edges.push({
-      id: item.id1 + '-' + item.id2,
-      start: item.id1,
-      end: item.id2,
-      arrowhead: 'normal',
-      arrowTypeEnd: 'arrow_barb',
-      style: G_EDGE_STYLE,
-      labelStyle: '',
-      label: common.sanitizeText(item.description, getConfig()),
-      arrowheadStyle: G_EDGE_ARROWHEADSTYLE,
-      labelpos: G_EDGE_LABELPOS,
-      labelType: G_EDGE_LABELTYPE,
-      thickness: G_EDGE_THICKNESS,
-      classes: CSS_EDGE,
-      useRough,
-    });
-  });
-
-  // if (item.doc) {
-  //   dataFetcher(item.id, item.doc, nodes, edges);
-  // }
-  //break;
-  //   case STMT_RELATION:
-  //     edges.push(item);
-  //     break;
-  // }
 };
+
 export const getData = () => {
   const nodes = [];
   const edges = [];
@@ -648,11 +833,56 @@ export const getData = () => {
   //     nodes.push({...currentDocument.states[key]});
   //   }
   // }
-  dataFetcher(undefined, rootDoc, nodes, edges);
+  extract(getRootDocV2());
+  const diagramStates = getStates();
+
+  const useRough = true;
+  dataFetcher(undefined, getRootDocV2(), diagramStates, nodes, edges, true, useRough);
 
   return { nodes, edges, other: {} };
 };
 
+/**
+ * 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}
+ */
+const getDir = (parsedItem, defaultDir = DEFAULT_NESTED_DOC_DIR) => {
+  let dir = defaultDir;
+  if (parsedItem.doc) {
+    for (let i = 0; i < parsedItem.doc.length; i++) {
+      const parsedItemDoc = parsedItem.doc[i];
+      if (parsedItemDoc.stmt === 'dir') {
+        dir = parsedItemDoc.value;
+      }
+    }
+  }
+  return dir;
+};
+
+/**
+ * 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 getClassesFromDbInfo(dbInfoItem) {
+  if (dbInfoItem === undefined || dbInfoItem === null) {
+    return '';
+  } else {
+    if (dbInfoItem.classes) {
+      return dbInfoItem.classes.join(' ');
+    } else {
+      return '';
+    }
+  }
+}
+
 export default {
   getConfig: () => getConfig().state,
   getData,
diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts b/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts
index 9d0a82a87..95c10c616 100644
--- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts
+++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts
@@ -3,7 +3,7 @@ import type { DiagramDefinition } from '../../diagram-api/types.js';
 import parser from './parser/stateDiagram.jison';
 import db from './stateDb.js';
 import styles from './styles.js';
-// import renderer from './stateRenderer-v2.js';
+//import renderer from './stateRenderer-v2.js';
 import renderer from './stateRenderer-v3-unified.js';
 
 export const diagram: DiagramDefinition = {
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js
index c08ae0bb4..63652745f 100644
--- a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js
@@ -201,6 +201,9 @@ export const render = async (data4Layout, svg, element) => {
   // Add the nodes and edges to the graph
   data4Layout.nodes.forEach((node) => {
     graph.setNode(node.id, { ...node });
+    if (node.parentId) {
+      graph.setParent(node.id, node.parentId);
+    }
   });
 
   console.log('Edges:', data4Layout.edges);