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?