#3358 Adding support for classDef and class statements

This commit is contained in:
Knut Sveidqvist
2024-01-05 15:13:15 +01:00
parent 72135c294e
commit 275e01acba
8 changed files with 200 additions and 6 deletions

View File

@@ -73,6 +73,22 @@ block-beta
<pre id="diagram" class="mermaid">
block-beta
columns 3
classDef green fill:#9f6,stroke:#333,stroke-width:2px;
A
B
class A green
</pre>
<pre id="diagram" class="mermaid">
stateDiagram-v2
classDef green fill:#9f6,stroke:#333,stroke-width:2px;
A
class A green
</pre>
<pre id="diagram" class="mermaid2">
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
</pre>
<pre id="diagram" class="mermaid2">
block-beta
@@ -538,7 +555,7 @@ mindmap
// console.error('Mermaid error: ', err);
};
mermaid.initialize({
// theme: 'forest',
theme: 'forest',
startOnLoad: true,
logLevel: 0,
flowchart: {

View File

@@ -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<string, Block> = {};
let edgeList: Block[] = [];
let edgeCount: Record<string, number> = {};
const COLOR_KEYWORD = 'color';
const FILL_KEYWORD = 'fill';
const BG_FILL = 'bgFill';
const STYLECLASS_SEP = ',';
let classes = {} as Record<string, ClassDef>;
/**
* 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<string, ClassDef>;
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<string, ClassDef>;
/**
* 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,
};

View File

@@ -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,
};

View File

@@ -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[];
}

View File

@@ -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'; }
<CLASSDEF>DEFAULT\s+ { this.popState(); this.pushState('CLASSDEFID'); return 'DEFAULT_CLASSDEF_ID' }
<CLASSDEF>\w+\s+ { this.popState(); this.pushState('CLASSDEFID'); return 'CLASSDEF_ID' }
<CLASSDEFID>[^\n]* { this.popState(); return 'CLASSDEF_STYLEOPTS' }
"class"\s+ { this.pushState('CLASS'); return 'class'; }
<CLASS>(\w+)+((","\s*\w+)*) { this.popState(); this.pushState('CLASS_STYLE'); return 'CLASSENTITY_IDS' }
<CLASS_STYLE>[^\n]* { this.popState(); return 'STYLECLASS' }
accTitle\s*":"\s* { this.pushState("acc_title");return 'acc_title'; }
<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() };
}
;
%%

View File

@@ -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

View File

@@ -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();

View File

@@ -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?