Merge pull request #6751 from mermaid-js/6750-convert-mindmap-to-class-based-approach

6750: Change MindmapDB to class based architecture
This commit is contained in:
Sidharth Vinod
2025-07-15 14:40:29 +00:00
committed by GitHub
7 changed files with 159 additions and 150 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
chore: Update MindmapDB to use class based approach

View File

@@ -1,12 +1,14 @@
// @ts-ignore: JISON doesn't support types // @ts-ignore: JISON doesn't support types
import parser from './parser/mindmap.jison'; import parser from './parser/mindmap.jison';
import db from './mindmapDb.js'; import { MindmapDB } from './mindmapDb.js';
import renderer from './mindmapRenderer.js'; import renderer from './mindmapRenderer.js';
import styles from './styles.js'; import styles from './styles.js';
import type { DiagramDefinition } from '../../diagram-api/types.js'; import type { DiagramDefinition } from '../../diagram-api/types.js';
export const diagram: DiagramDefinition = { export const diagram: DiagramDefinition = {
db, get db() {
return new MindmapDB();
},
renderer, renderer,
parser, parser,
styles, styles,

View File

@@ -1,12 +1,12 @@
// @ts-expect-error No types available for JISON // @ts-expect-error No types available for JISON
import { parser as mindmap } from './parser/mindmap.jison'; import { parser as mindmap } from './parser/mindmap.jison';
import mindmapDB from './mindmapDb.js'; import { MindmapDB } from './mindmapDb.js';
// Todo fix utils functions for tests // Todo fix utils functions for tests
import { setLogLevel } from '../../diagram-api/diagramAPI.js'; import { setLogLevel } from '../../diagram-api/diagramAPI.js';
describe('when parsing a mindmap ', function () { describe('when parsing a mindmap ', function () {
beforeEach(function () { beforeEach(function () {
mindmap.yy = mindmapDB; mindmap.yy = new MindmapDB();
mindmap.yy.clear(); mindmap.yy.clear();
setLogLevel('trace'); setLogLevel('trace');
}); });

View File

@@ -5,70 +5,6 @@ import { log } from '../../logger.js';
import type { MindmapNode } from './mindmapTypes.js'; import type { MindmapNode } from './mindmapTypes.js';
import defaultConfig from '../../defaultConfig.js'; import defaultConfig from '../../defaultConfig.js';
let nodes: MindmapNode[] = [];
let cnt = 0;
let elements: Record<number, D3Element> = {};
const clear = () => {
nodes = [];
cnt = 0;
elements = {};
};
const getParent = function (level: number) {
for (let i = nodes.length - 1; i >= 0; i--) {
if (nodes[i].level < level) {
return nodes[i];
}
}
// No parent found
return null;
};
const getMindmap = () => {
return nodes.length > 0 ? nodes[0] : null;
};
const addNode = (level: number, id: string, descr: string, type: number) => {
log.info('addNode', level, id, descr, type);
const conf = getConfig();
let padding: number = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
switch (type) {
case nodeType.ROUNDED_RECT:
case nodeType.RECT:
case nodeType.HEXAGON:
padding *= 2;
}
const node = {
id: cnt++,
nodeId: sanitizeText(id, conf),
level,
descr: sanitizeText(descr, conf),
type,
children: [],
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
padding,
} satisfies MindmapNode;
const parent = getParent(level);
if (parent) {
parent.children.push(node);
// Keep all nodes in the list
nodes.push(node);
} else {
if (nodes.length === 0) {
// First node, the root
nodes.push(node);
} else {
// Syntax error ... there can only bee one root
throw new Error(
'There can be only one root. No parent could be found for ("' + node.descr + '")'
);
}
}
};
const nodeType = { const nodeType = {
DEFAULT: 0, DEFAULT: 0,
NO_BORDER: 0, NO_BORDER: 0,
@@ -78,82 +14,149 @@ const nodeType = {
CLOUD: 4, CLOUD: 4,
BANG: 5, BANG: 5,
HEXAGON: 6, HEXAGON: 6,
} as const;
export class MindmapDB {
private nodes: MindmapNode[] = [];
private count = 0;
private elements: Record<number, D3Element> = {};
public readonly nodeType: typeof nodeType;
constructor() {
this.getLogger = this.getLogger.bind(this);
this.nodeType = nodeType;
this.clear();
this.getType = this.getType.bind(this);
this.getMindmap = this.getMindmap.bind(this);
this.getElementById = this.getElementById.bind(this);
this.getParent = this.getParent.bind(this);
this.getMindmap = this.getMindmap.bind(this);
this.addNode = this.addNode.bind(this);
this.decorateNode = this.decorateNode.bind(this);
}
public clear() {
this.nodes = [];
this.count = 0;
this.elements = {};
}
public getParent(level: number): MindmapNode | null {
for (let i = this.nodes.length - 1; i >= 0; i--) {
if (this.nodes[i].level < level) {
return this.nodes[i];
}
}
return null;
}
public getMindmap(): MindmapNode | null {
return this.nodes.length > 0 ? this.nodes[0] : null;
}
public addNode(level: number, id: string, descr: string, type: number): void {
log.info('addNode', level, id, descr, type);
const conf = getConfig();
let padding = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
switch (type) {
case this.nodeType.ROUNDED_RECT:
case this.nodeType.RECT:
case this.nodeType.HEXAGON:
padding *= 2;
break;
}
const node: MindmapNode = {
id: this.count++,
nodeId: sanitizeText(id, conf),
level,
descr: sanitizeText(descr, conf),
type,
children: [],
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
padding,
}; };
const getType = (startStr: string, endStr: string): number => { const parent = this.getParent(level);
if (parent) {
parent.children.push(node);
this.nodes.push(node);
} else {
if (this.nodes.length === 0) {
this.nodes.push(node);
} else {
throw new Error(
`There can be only one root. No parent could be found for ("${node.descr}")`
);
}
}
}
public getType(startStr: string, endStr: string) {
log.debug('In get type', startStr, endStr); log.debug('In get type', startStr, endStr);
switch (startStr) { switch (startStr) {
case '[': case '[':
return nodeType.RECT; return this.nodeType.RECT;
case '(': case '(':
return endStr === ')' ? nodeType.ROUNDED_RECT : nodeType.CLOUD; return endStr === ')' ? this.nodeType.ROUNDED_RECT : this.nodeType.CLOUD;
case '((': case '((':
return nodeType.CIRCLE; return this.nodeType.CIRCLE;
case ')': case ')':
return nodeType.CLOUD; return this.nodeType.CLOUD;
case '))': case '))':
return nodeType.BANG; return this.nodeType.BANG;
case '{{': case '{{':
return nodeType.HEXAGON; return this.nodeType.HEXAGON;
default: default:
return nodeType.DEFAULT; return this.nodeType.DEFAULT;
}
} }
};
const setElementForId = (id: number, element: D3Element) => { public setElementForId(id: number, element: D3Element): void {
elements[id] = element; this.elements[id] = element;
}; }
public getElementById(id: number) {
return this.elements[id];
}
const decorateNode = (decoration?: { class?: string; icon?: string }) => { public decorateNode(decoration?: { class?: string; icon?: string }): void {
if (!decoration) { if (!decoration) {
return; return;
} }
const config = getConfig(); const config = getConfig();
const node = nodes[nodes.length - 1]; const node = this.nodes[this.nodes.length - 1];
if (decoration.icon) { if (decoration.icon) {
node.icon = sanitizeText(decoration.icon, config); node.icon = sanitizeText(decoration.icon, config);
} }
if (decoration.class) { if (decoration.class) {
node.class = sanitizeText(decoration.class, config); node.class = sanitizeText(decoration.class, config);
} }
}; }
const type2Str = (type: number) => { type2Str(type: number): string {
switch (type) { switch (type) {
case nodeType.DEFAULT: case this.nodeType.DEFAULT:
return 'no-border'; return 'no-border';
case nodeType.RECT: case this.nodeType.RECT:
return 'rect'; return 'rect';
case nodeType.ROUNDED_RECT: case this.nodeType.ROUNDED_RECT:
return 'rounded-rect'; return 'rounded-rect';
case nodeType.CIRCLE: case this.nodeType.CIRCLE:
return 'circle'; return 'circle';
case nodeType.CLOUD: case this.nodeType.CLOUD:
return 'cloud'; return 'cloud';
case nodeType.BANG: case this.nodeType.BANG:
return 'bang'; return 'bang';
case nodeType.HEXAGON: case this.nodeType.HEXAGON:
return 'hexgon'; // cspell: disable-line return 'hexgon'; // cspell: disable-line
default: default:
return 'no-border'; return 'no-border';
} }
}; }
// Expose logger to grammar public getLogger() {
const getLogger = () => log; return log;
const getElementById = (id: number) => elements[id]; }
}
const db = {
clear,
addNode,
getMindmap,
nodeType,
getType,
setElementForId,
decorateNode,
type2Str,
getLogger,
getElementById,
} as const;
export default db;

View File

@@ -9,10 +9,10 @@ import { log } from '../../logger.js';
import type { D3Element } from '../../types.js'; import type { D3Element } from '../../types.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { setupGraphViewbox } from '../../setupGraphViewbox.js'; import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import type { FilledMindMapNode, MindmapDB, MindmapNode } from './mindmapTypes.js'; import type { FilledMindMapNode, MindmapNode } from './mindmapTypes.js';
import { drawNode, positionNode } from './svgDraw.js'; import { drawNode, positionNode } from './svgDraw.js';
import defaultConfig from '../../defaultConfig.js'; import defaultConfig from '../../defaultConfig.js';
import type { MindmapDB } from './mindmapDb.js';
// Inject the layout algorithm into cytoscape // Inject the layout algorithm into cytoscape
cytoscape.use(coseBilkent); cytoscape.use(coseBilkent);

View File

@@ -1,5 +1,4 @@
import type { RequiredDeep } from 'type-fest'; import type { RequiredDeep } from 'type-fest';
import type mindmapDb from './mindmapDb.js';
export interface MindmapNode { export interface MindmapNode {
id: number; id: number;
@@ -19,4 +18,3 @@ export interface MindmapNode {
} }
export type FilledMindMapNode = RequiredDeep<MindmapNode>; export type FilledMindMapNode = RequiredDeep<MindmapNode>;
export type MindmapDB = typeof mindmapDb;

View File

@@ -1,8 +1,9 @@
import { createText } from '../../rendering-util/createText.js'; import { createText } from '../../rendering-util/createText.js';
import type { FilledMindMapNode, MindmapDB } from './mindmapTypes.js'; import type { FilledMindMapNode } from './mindmapTypes.js';
import type { Point, D3Element } from '../../types.js'; import type { Point, D3Element } from '../../types.js';
import { parseFontSize } from '../../utils.js'; import { parseFontSize } from '../../utils.js';
import type { MermaidConfig } from '../../config.type.js'; import type { MermaidConfig } from '../../config.type.js';
import type { MindmapDB } from './mindmapDb.js';
const MAX_SECTIONS = 12; const MAX_SECTIONS = 12;