#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"> <pre id="diagram" class="mermaid">
block-beta block-beta
columns 3 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 A
space space
block block
@@ -80,6 +96,7 @@ block-beta
F F
end end
E -- "apa" --> A E -- "apa" --> A
</pre> </pre>
<pre id="diagram" class="mermaid2"> <pre id="diagram" class="mermaid2">
block-beta block-beta
@@ -538,7 +555,7 @@ mindmap
// console.error('Mermaid error: ', err); // console.error('Mermaid error: ', err);
}; };
mermaid.initialize({ mermaid.initialize({
// theme: 'forest', theme: 'forest',
startOnLoad: true, startOnLoad: true,
logLevel: 0, logLevel: 0,
flowchart: { flowchart: {

View File

@@ -1,6 +1,6 @@
// import type { BlockDB } from './blockTypes.js'; // import type { BlockDB } from './blockTypes.js';
import type { DiagramDB } from '../../diagram-api/types.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 * as configApi from '../../config.js';
import { import {
@@ -20,10 +20,112 @@ let blockDatabase: Record<string, Block> = {};
let edgeList: Block[] = []; let edgeList: Block[] = [];
let edgeCount: Record<string, number> = {}; 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 populateBlockDatabase = (_blockList: Block[], parent: Block): void => {
const blockList = _blockList.flat(); const blockList = _blockList.flat();
const children = []; const children = [];
for (const block of blockList) { 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') { if (block.type === 'column-setting') {
parent.columns = block.columns || -1; parent.columns = block.columns || -1;
} else if (block.type === 'edge') { } else if (block.type === 'edge') {
@@ -87,6 +189,7 @@ const clear = (): void => {
rootBlock = { id: 'root', type: 'composite', children: [], columns: -1 } as Block; rootBlock = { id: 'root', type: 'composite', children: [], columns: -1 } as Block;
blockDatabase = { root: rootBlock }; blockDatabase = { root: rootBlock };
blocks = [] as Block[]; blocks = [] as Block[];
classes = {} as Record<string, ClassDef>;
edgeList = []; edgeList = [];
edgeCount = {}; edgeCount = {};
@@ -166,7 +269,7 @@ const setHierarchy = (block: Block[]): void => {
log.debug('The document from parsing', JSON.stringify(block, null, 2)); log.debug('The document from parsing', JSON.stringify(block, null, 2));
rootBlock.children = block; rootBlock.children = block;
populateBlockDatabase(block, rootBlock); 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; blocks = rootBlock.children;
}; };
@@ -231,6 +334,15 @@ const getLinks: IGetLinks = () => links;
type IGetLogger = () => Console; type IGetLogger = () => Console;
const getLogger: 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 { export interface BlockDB extends DiagramDB {
clear: () => void; clear: () => void;
getConfig: () => BlockConfig | undefined; getConfig: () => BlockConfig | undefined;
@@ -243,6 +355,7 @@ export interface BlockDB extends DiagramDB {
setBlock: ISetBlock; setBlock: ISetBlock;
getLinks: IGetLinks; getLinks: IGetLinks;
getColumns: IGetColumns; getColumns: IGetColumns;
getClasses: IGetClasses;
typeStr2Type: ITypeStr2Type; typeStr2Type: ITypeStr2Type;
edgeTypeStr2Type: IEdgeTypeStr2Type; edgeTypeStr2Type: IEdgeTypeStr2Type;
edgeStrToEdgeData: IEdgeStrToEdgeDataType; edgeStrToEdgeData: IEdgeStrToEdgeDataType;
@@ -271,6 +384,7 @@ const db: BlockDB = {
// getDiagramTitle, // getDiagramTitle,
// setDiagramTitle, // setDiagramTitle,
getColumns, getColumns,
getClasses,
clear, clear,
generateId, generateId,
}; };

View File

@@ -17,6 +17,22 @@ import type { Block } from './blockTypes.js';
// import { diagram as BlockDiagram } from './blockDiagram.js'; // import { diagram as BlockDiagram } from './blockDiagram.js';
import { configureSvgSize } from '../../setupGraphViewbox.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 ( export const draw = async function (
text: string, text: string,
id: string, id: string,
@@ -99,4 +115,5 @@ export const draw = async function (
export default { export default {
draw, draw,
getClasses,
}; };

View File

@@ -28,6 +28,8 @@ export type BlockType =
| 'cylinder' | 'cylinder'
| 'group' | 'group'
| 'doublecircle' | 'doublecircle'
| 'classDef'
| 'applyClass'
| 'composite'; | 'composite';
export interface Block { export interface Block {
@@ -53,9 +55,17 @@ export interface Block {
columns?: number; // | TBlockColumnsDefaultValue; columns?: number; // | TBlockColumnsDefaultValue;
classes?: string[]; classes?: string[];
directions?: string[]; directions?: string[];
css?: string;
styleClass?: string;
} }
export interface Link { export interface Link {
source: Block; source: Block;
target: Block; target: Block;
} }
export interface ClassDef {
id: string;
textStyles: string[];
styles: string[];
}

View File

@@ -19,6 +19,10 @@
%x BLOCK_ARROW %x BLOCK_ARROW
%x ARROW_DIR %x ARROW_DIR
%x LLABEL %x LLABEL
%x CLASS
%x CLASS_STYLE
%x CLASSDEF
%x CLASSDEFID
// as per section 6.1 of RFC 2234 [2] // 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'; "default" return 'DEFAULT';
"linkStyle" return 'LINKSTYLE'; "linkStyle" return 'LINKSTYLE';
"interpolate" return 'INTERPOLATE'; "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'; } accTitle\s*":"\s* { this.pushState("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; } <acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.pushState("acc_descr");return 'acc_descr'; } accDescr\s*":"\s* { this.pushState("acc_descr");return 'acc_descr'; }
@@ -194,6 +206,8 @@ statement
| SPACE_BLOCK | SPACE_BLOCK
{ const num=parseInt($1); const spaceId = yy.generateId(); $$ = { id: spaceId, type:'space', label:'', width: num, children: [] }} { const num=parseInt($1); const spaceId = yy.generateId(); $$ = { id: spaceId, type:'space', label:'', width: num, children: [] }}
| blockStatement | blockStatement
| classDefStatement
| cssClassStatement
; ;
nodeStatement 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}; } { 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'; let classStr = 'default';
if ((vertex?.classes?.length || 0) > 0) { if ((vertex?.classes?.length || 0) > 0) {
console.log('abc88 vertex.classes', block.id, vertex?.classes);
classStr = (vertex?.classes || []).join(' '); classStr = (vertex?.classes || []).join(' ');
} }
console.log('abc88 vertex.classes done');
classStr = classStr + ' flowchart-label'; classStr = classStr + ' flowchart-label';
// We create a SVG label, either by delegating to addHtmlLabel or manually // 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 _version
* @param diagObj * @param diagObj
*/ */
// [MermaidChart: 33a97b35-1f95-4ce9-81b5-3038669bc170]
export const draw = async function (text, id, _version, diagObj) { export const draw = async function (text, id, _version, diagObj) {
log.info('Drawing flowchart'); log.info('Drawing flowchart');
diagObj.db.clear(); diagObj.db.clear();

View File

@@ -39,6 +39,7 @@ const CLASSDEF_DIAGRAMS = [
'flowchart-elk', 'flowchart-elk',
'stateDiagram', 'stateDiagram',
'stateDiagram-v2', 'stateDiagram-v2',
'block',
]; ];
const MAX_TEXTLENGTH = 50_000; const MAX_TEXTLENGTH = 50_000;
const MAX_TEXTLENGTH_EXCEEDED_MSG = const MAX_TEXTLENGTH_EXCEEDED_MSG =
@@ -203,6 +204,8 @@ export const createCssStyles = (
cssStyles += `\n:root { --mermaid-alt-font-family: ${config.altFontFamily}}`; cssStyles += `\n:root { --mermaid-alt-font-family: ${config.altFontFamily}}`;
} }
console.log('abc88 expr check', !isEmpty(classDefs), classDefs);
// classDefs defined in the diagram text // classDefs defined in the diagram text
if (!isEmpty(classDefs) && CLASSDEF_DIAGRAMS.includes(graphType)) { if (!isEmpty(classDefs) && CLASSDEF_DIAGRAMS.includes(graphType)) {
const htmlLabels = config.htmlLabels || config.flowchart?.htmlLabels; // TODO why specifically check the Flowchart diagram config? const htmlLabels = config.htmlLabels || config.flowchart?.htmlLabels; // TODO why specifically check the Flowchart diagram config?