diagram-v2: store results of stateDb.extract(), apply class to state; code cleanup

This commit is contained in:
Ashley Engelund (weedySeaDragon @ github) 2022-10-11 10:19:28 -07:00
parent 3c0727c744
commit ba71afcce5

View File

@ -7,7 +7,23 @@ import { configureSvgSize } from '../../setupGraphViewbox';
import common from '../common/common'; import common from '../common/common';
import addSVGAccessibilityFields from '../../accessibility'; import addSVGAccessibilityFields from '../../accessibility';
const DEFAULT_DIR = 'TD';
// When information is parsed and processed (extracted) by stateDb.extract()
// These are globals so the information can be accessed as needed (e.g. in setUpNode, etc.)
let diagramStates = [];
let diagramClasses = [];
// List of nodes created from the parsed diagram statement items
let nodeDb = {};
let graphItemCount = 0; // used to construct ids, etc.
// Configuration
const conf = {}; const conf = {};
// -----------------------------------------------------------------------
export const setConf = function (cnf) { export const setConf = function (cnf) {
const keys = Object.keys(cnf); const keys = Object.keys(cnf);
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
@ -15,150 +31,187 @@ export const setConf = function (cnf) {
} }
}; };
let nodeDb = {};
/** /**
* Returns the all the styles from classDef statements in the graph definition. * Returns the all the styles from classDef statements in the graph definition.
* *
* @param {any} text * @param {string} text - the diagram text to be parsed
* @param diag * @param {Diagram} diagramObj
* @returns {object} ClassDef styles * @returns {object} ClassDef styles
*/ */
export const getClasses = function (text, diag) { export const getClasses = function (text, diagramObj) {
log.trace('Extracting classes'); log.trace('Extracting classes');
diag.sb.clear(); if (diagramClasses.length > 0) return diagramClasses; // we have already extracted the classes
// Parse the graph definition diagramObj.db.clear();
diag.parser.parse(text); try {
return diag.sb.getClasses(); // Parse the graph definition
diagramObj.parser.parse(text);
// must run extract() to turn the parsed statements into states, relationships, classes, etc.
diagramObj.db.extract(diagramObj.db.getRootDocV2());
return diagramObj.db.getClasses();
} catch (e) {
return e;
}
}; };
const setupNode = (g, parent, node, altFlag) => { /**
// Add the node * Get classes from the db info item.
if (node.id !== 'root') { * 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 (typeof dbInfoItem === 'undefined' || dbInfoItem === null) return '';
else {
if (dbInfoItem.classes) {
return dbInfoItem.classes.join(' ');
} else return '';
}
}
/**
* Create a graph node based on the statement information
*
* @param g - graph
* @param {object} parent
* @param {object} parsedItem - parsed statement item
* @param {object} diagramDb
* @param {boolean} altFlag - for clusters, add the "statediagram-cluster-alt" CSS class
* @todo This duplicates some of what is done in stateDb.js extract method
*/
const setupNode = (g, parent, parsedItem, diagramDb, altFlag) => {
const itemId = parsedItem.id;
const classStr = getClassesFromDbInfo(diagramStates[itemId]);
if (itemId !== 'root') {
let shape = 'rect'; let shape = 'rect';
if (node.start === true) { if (parsedItem.start === true) {
shape = 'start'; shape = 'start';
} }
if (node.start === false) { if (parsedItem.start === false) {
shape = 'end'; shape = 'end';
} }
if (node.type !== 'default') { if (parsedItem.type !== 'default') {
shape = node.type; shape = parsedItem.type;
} }
if (!nodeDb[node.id]) { // Add the node to our list (nodeDb)
nodeDb[node.id] = { if (!nodeDb[itemId]) {
id: node.id, nodeDb[itemId] = {
id: itemId,
shape, shape,
description: common.sanitizeText(node.id, getConfig()), description: common.sanitizeText(itemId, getConfig()),
classes: 'statediagram-state', classes: classStr + ' statediagram-state',
}; };
} }
// Build of the array of description strings accordinging const newNode = nodeDb[itemId];
if (node.description) {
if (Array.isArray(nodeDb[node.id].description)) { // 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 // There already is an array of strings,add to it
nodeDb[node.id].shape = 'rectWithTitle'; newNode.shape = 'rectWithTitle';
nodeDb[node.id].description.push(node.description); newNode.description.push(parsedItem.description);
} else { } else {
if (nodeDb[node.id].description.length > 0) { if (newNode.description.length > 0) {
// if there is a description already transformit to an array // if there is a description already transform it to an array
nodeDb[node.id].shape = 'rectWithTitle'; newNode.shape = 'rectWithTitle';
if (nodeDb[node.id].description === node.id) { if (newNode.description === itemId) {
// If the previous description was the is, remove it // If the previous description was the is, remove it
nodeDb[node.id].description = [node.description]; newNode.description = [parsedItem.description];
} else { } else {
nodeDb[node.id].description = [nodeDb[node.id].description, node.description]; newNode.description = [newNode.description, parsedItem.description];
} }
} else { } else {
nodeDb[node.id].shape = 'rect'; newNode.shape = 'rect';
nodeDb[node.id].description = node.description; newNode.description = parsedItem.description;
} }
} }
nodeDb[node.id].description = common.sanitizeTextOrArray( newNode.description = common.sanitizeTextOrArray(newNode.description, getConfig());
nodeDb[node.id].description,
getConfig()
);
} }
// // update the node shape
if (nodeDb[node.id].description.length === 1 && nodeDb[node.id].shape === 'rectWithTitle') { if (newNode.description.length === 1 && newNode.shape === 'rectWithTitle') {
nodeDb[node.id].shape = 'rect'; newNode.shape = 'rect';
} }
// Save data for description and group so that for instance a statement without description overwrites // Save data for description and group so that for instance a statement without description overwrites
// one with description // one with description
// group // group
if (!nodeDb[node.id].type && node.doc) { if (!newNode.type && parsedItem.doc) {
log.info('Setting cluster for ', node.id, getDir(node)); log.info('Setting cluster for ', itemId, getDir(parsedItem));
nodeDb[node.id].type = 'group'; newNode.type = 'group';
nodeDb[node.id].dir = getDir(node); newNode.dir = getDir(parsedItem);
nodeDb[node.id].shape = node.type === 'divider' ? 'divider' : 'roundedWithTitle'; newNode.shape = parsedItem.type === 'divider' ? 'divider' : 'roundedWithTitle';
nodeDb[node.id].classes =
nodeDb[node.id].classes + newNode.classes =
newNode.classes +
' ' + ' ' +
(altFlag ? 'statediagram-cluster statediagram-cluster-alt' : 'statediagram-cluster'); (altFlag ? 'statediagram-cluster statediagram-cluster-alt' : 'statediagram-cluster');
} }
// This is what will be added to the graph
const nodeData = { const nodeData = {
labelStyle: '', labelStyle: '',
shape: nodeDb[node.id].shape, shape: newNode.shape,
labelText: nodeDb[node.id].description, labelText: newNode.description,
// typeof nodeDb[node.id].description === 'object' // typeof newNode.description === 'object'
// ? nodeDb[node.id].description[0] // ? newNode.description[0]
// : nodeDb[node.id].description, // : newNode.description,
classes: nodeDb[node.id].classes, //classStr, classes: newNode.classes,
style: '', //styles.style, style: '', //styles.style,
id: node.id, id: itemId,
dir: nodeDb[node.id].dir, dir: newNode.dir,
domId: 'state-' + node.id + '-' + cnt, domId: 'state-' + itemId + '-' + graphItemCount,
type: nodeDb[node.id].type, type: newNode.type,
padding: 15, //getConfig().flowchart.padding padding: 15, //getConfig().flowchart.padding
}; };
if (node.note) { if (parsedItem.note) {
// Todo: set random id // Todo: set random id
const noteData = { const noteData = {
labelStyle: '', labelStyle: '',
shape: 'note', shape: 'note',
labelText: node.note.text, labelText: parsedItem.note.text,
classes: 'statediagram-note', //classStr, classes: 'statediagram-note', //classStr,
style: '', //styles.style, style: '', // styles.style,
id: node.id + '----note-' + cnt, id: itemId + '----note-' + graphItemCount,
domId: 'state-' + node.id + '----note-' + cnt, domId: 'state-' + itemId + '----note-' + graphItemCount,
type: nodeDb[node.id].type, type: newNode.type,
padding: 15, //getConfig().flowchart.padding padding: 15, //getConfig().flowchart.padding
}; };
const groupData = { const groupData = {
labelStyle: '', labelStyle: '',
shape: 'noteGroup', shape: 'noteGroup',
labelText: node.note.text, labelText: parsedItem.note.text,
classes: nodeDb[node.id].classes, //classStr, classes: newNode.classes, //classStr,
style: '', //styles.style, style: '', // styles.style,
id: node.id + '----parent', id: itemId + '----parent',
domId: 'state-' + node.id + '----parent-' + cnt, domId: 'state-' + itemId + '----parent-' + graphItemCount,
type: 'group', type: 'group',
padding: 0, //getConfig().flowchart.padding padding: 0, //getConfig().flowchart.padding
}; };
cnt++; graphItemCount++;
g.setNode(node.id + '----parent', groupData); g.setNode(itemId + '----parent', groupData);
g.setNode(noteData.id, noteData); g.setNode(noteData.id, noteData);
g.setNode(node.id, nodeData); g.setNode(itemId, nodeData);
g.setParent(node.id, node.id + '----parent'); g.setParent(itemId, itemId + '----parent');
g.setParent(noteData.id, node.id + '----parent'); g.setParent(noteData.id, itemId + '----parent');
let from = node.id; let from = itemId;
let to = noteData.id; let to = noteData.id;
if (node.note.position === 'left of') { if (parsedItem.note.position === 'left of') {
from = noteData.id; from = noteData.id;
to = node.id; to = itemId;
} }
g.setEdge(from, to, { g.setEdge(from, to, {
arrowhead: 'none', arrowhead: 'none',
@ -172,66 +225,92 @@ const setupNode = (g, parent, node, altFlag) => {
thickness: 'normal', thickness: 'normal',
}); });
} else { } else {
g.setNode(node.id, nodeData); g.setNode(itemId, nodeData);
} }
} }
if (parent) { if (parent) {
if (parent.id !== 'root') { if (parent.id !== 'root') {
log.trace('Setting node ', node.id, ' to be child of its parent ', parent.id); log.trace('Setting node ', itemId, ' to be child of its parent ', parent.id);
g.setParent(node.id, parent.id); g.setParent(itemId, parent.id);
} }
} }
if (node.doc) { if (parsedItem.doc) {
log.trace('Adding nodes children '); log.trace('Adding nodes children ');
setupDoc(g, node, node.doc, !altFlag); setupDoc(g, parsedItem, parsedItem.doc, diagramDb, !altFlag);
} }
}; };
let cnt = 0;
const setupDoc = (g, parent, doc, altFlag) => { /**
// cnt = 0; * Turn parsed statements (item.stmt) into nodes, relationships, etc. for a document.
* (A document may be nested within others.)
*
* @param g
* @param parentParsedItem - parsed Item that is the parent of this document (doc)
* @param doc - the document to set up
* @param diagramDb
* @param altFlag
* @todo This duplicates some of what is done in stateDb.js extract method
*/
const setupDoc = (g, parentParsedItem, doc, diagramDb, altFlag) => {
// graphItemCount = 0;
log.trace('items', doc); log.trace('items', doc);
doc.forEach((item) => { doc.forEach((item) => {
if (item.stmt === 'state' || item.stmt === 'default') { switch (item.stmt) {
setupNode(g, parent, item, altFlag); case 'state':
} else if (item.stmt === 'relation') { setupNode(g, parentParsedItem, item, diagramDb, altFlag);
setupNode(g, parent, item.state1, altFlag); break;
setupNode(g, parent, item.state2, altFlag); case 'default':
const edgeData = { setupNode(g, parentParsedItem, item, diagramDb, altFlag);
id: 'edge' + cnt, break;
arrowhead: 'normal', case 'relation':
arrowTypeEnd: 'arrow_barb', {
style: 'fill:none', setupNode(g, parentParsedItem, item.state1, diagramDb, altFlag);
labelStyle: '', setupNode(g, parentParsedItem, item.state2, diagramDb, altFlag);
label: common.sanitizeText(item.description, getConfig()), const edgeData = {
arrowheadStyle: 'fill: #333', id: 'edge' + graphItemCount,
labelpos: 'c', arrowhead: 'normal',
labelType: 'text', arrowTypeEnd: 'arrow_barb',
thickness: 'normal', style: 'fill:none',
classes: 'transition', labelStyle: '',
}; label: common.sanitizeText(item.description, getConfig()),
let startId = item.state1.id; arrowheadStyle: 'fill: #333',
let endId = item.state2.id; labelpos: 'c',
labelType: 'text',
g.setEdge(startId, endId, edgeData, cnt); thickness: 'normal',
cnt++; classes: 'transition',
};
g.setEdge(item.state1.id, item.state2.id, edgeData, graphItemCount);
graphItemCount++;
}
break;
} }
}); });
}; };
const getDir = (nodes, defaultDir) => {
let dir = defaultDir || 'TB'; /**
if (nodes.doc) { * Get the direction from the statement items. Default is TB (top to bottom).
for (let i = 0; i < nodes.doc.length; i++) { * Look through all of the documents (docs) in the parsedItems
const node = nodes.doc[i]; *
if (node.stmt === 'dir') { * @param {object[]} parsedItem - the parsed statement item to look through
dir = node.value; * @param [defaultDir='TB'] - the direction to use if none is found
* @returns {string}
*/
const getDir = (parsedItem, defaultDir = DEFAULT_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; return dir;
}; };
/** /**
* Draws a flowchart in the tag with id: id based on the graph definition in text. * Draws a state diagram in the tag with id: id based on the graph definition in text.
* *
* @param {any} text * @param {any} text
* @param {any} id * @param {any} id
@ -244,18 +323,21 @@ export const draw = function (text, id, _version, diag) {
nodeDb = {}; nodeDb = {};
// Fetch the default direction, use TD if none was found // Fetch the default direction, use TD if none was found
let dir = diag.db.getDirection(); let dir = diag.db.getDirection();
if (typeof dir === 'undefined') { if (typeof dir === 'undefined') dir = DEFAULT_DIR;
dir = 'LR';
}
const { securityLevel, state: conf } = getConfig(); const { securityLevel, state: conf } = getConfig();
const nodeSpacing = conf.nodeSpacing || 50; const nodeSpacing = conf.nodeSpacing || 50;
const rankSpacing = conf.rankSpacing || 50; const rankSpacing = conf.rankSpacing || 50;
log.info(diag.db.getRootDocV2()); log.info(diag.db.getRootDocV2());
// This parses the diagram text and sets the classes, relations, styles, classDefs, etc.
diag.db.extract(diag.db.getRootDocV2()); diag.db.extract(diag.db.getRootDocV2());
log.info(diag.db.getRootDocV2()); log.info(diag.db.getRootDocV2());
diagramStates = diag.db.getStates();
diagramClasses = diag.db.getClasses();
// Create the input mermaid.graph // Create the input mermaid.graph
const g = new graphlib.Graph({ const g = new graphlib.Graph({
multigraph: true, multigraph: true,
@ -272,7 +354,7 @@ export const draw = function (text, id, _version, diag) {
return {}; return {};
}); });
setupNode(g, undefined, diag.db.getRootDocV2(), true); setupNode(g, undefined, diag.db.getRootDocV2(), diag.db, true);
// Set up an SVG group so that we can translate the final graph. // Set up an SVG group so that we can translate the final graph.
let sandboxElement; let sandboxElement;