mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-19 23:39:50 +02:00
#5342 Initial commit
This commit is contained in:
23
packages/mermaid/src/diagrams/kanban/detector.ts
Normal file
23
packages/mermaid/src/diagrams/kanban/detector.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type {
|
||||||
|
DiagramDetector,
|
||||||
|
DiagramLoader,
|
||||||
|
ExternalDiagramDefinition,
|
||||||
|
} from '../../diagram-api/types.js';
|
||||||
|
const id = 'kanban';
|
||||||
|
|
||||||
|
const detector: DiagramDetector = (txt) => {
|
||||||
|
return /^\s*kanban/.test(txt);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loader: DiagramLoader = async () => {
|
||||||
|
const { diagram } = await import('./kanban-definition.js');
|
||||||
|
return { id, diagram };
|
||||||
|
};
|
||||||
|
|
||||||
|
const plugin: ExternalDiagramDefinition = {
|
||||||
|
id,
|
||||||
|
detector,
|
||||||
|
loader,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default plugin;
|
13
packages/mermaid/src/diagrams/kanban/kanban-definition.ts
Normal file
13
packages/mermaid/src/diagrams/kanban/kanban-definition.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// @ts-ignore: JISON doesn't support types
|
||||||
|
import parser from './parser/mindmap.jison';
|
||||||
|
import db from './kanbanDb.js';
|
||||||
|
import renderer from './kanbanRenderer.js';
|
||||||
|
import styles from './styles.js';
|
||||||
|
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||||
|
|
||||||
|
export const diagram: DiagramDefinition = {
|
||||||
|
db,
|
||||||
|
renderer,
|
||||||
|
parser,
|
||||||
|
styles,
|
||||||
|
};
|
365
packages/mermaid/src/diagrams/kanban/kanban.spec.ts
Normal file
365
packages/mermaid/src/diagrams/kanban/kanban.spec.ts
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
// @ts-expect-error No types available for JISON
|
||||||
|
import { parser as kanban } from './parser/kanban.jison';
|
||||||
|
import kanbanDB from './kanbanDb.js';
|
||||||
|
// Todo fix utils functions for tests
|
||||||
|
import { setLogLevel } from '../../diagram-api/diagramAPI.js';
|
||||||
|
|
||||||
|
describe('when parsing a kanban ', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
kanban.yy = kanbanDB;
|
||||||
|
kanban.yy.clear();
|
||||||
|
setLogLevel('trace');
|
||||||
|
});
|
||||||
|
describe('hiearchy', function () {
|
||||||
|
it('KNBN-1 should handle a simple root definition abc122', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root`;
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
// console.log('Time for checks', kanban.yy.getMindmap().descr);
|
||||||
|
expect(kanban.yy.getMindmap().descr).toEqual('root');
|
||||||
|
});
|
||||||
|
it('KNBN-2 should handle a hierachial kanban definition', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root
|
||||||
|
child1
|
||||||
|
child2
|
||||||
|
`;
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.descr).toEqual('root');
|
||||||
|
expect(mm.children.length).toEqual(2);
|
||||||
|
expect(mm.children[0].descr).toEqual('child1');
|
||||||
|
expect(mm.children[1].descr).toEqual('child2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('3 should handle a simple root definition with a shape and without an id abc123', function () {
|
||||||
|
const str = `kanban
|
||||||
|
(root)`;
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
// console.log('Time for checks', kanban.yy.getMindmap().descr);
|
||||||
|
expect(kanban.yy.getMindmap().descr).toEqual('root');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('KNBN-4 should handle a deeper hierachial kanban definition', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root
|
||||||
|
child1
|
||||||
|
leaf1
|
||||||
|
child2`;
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.descr).toEqual('root');
|
||||||
|
expect(mm.children.length).toEqual(2);
|
||||||
|
expect(mm.children[0].descr).toEqual('child1');
|
||||||
|
expect(mm.children[0].children[0].descr).toEqual('leaf1');
|
||||||
|
expect(mm.children[1].descr).toEqual('child2');
|
||||||
|
});
|
||||||
|
it('5 Multiple roots are illegal', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root
|
||||||
|
fakeRoot`;
|
||||||
|
|
||||||
|
expect(() => kanban.parse(str)).toThrow(
|
||||||
|
'There can be only one root. No parent could be found for ("fakeRoot")'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('KNBN-6 real root in wrong place', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root
|
||||||
|
fakeRoot
|
||||||
|
realRootWrongPlace`;
|
||||||
|
expect(() => kanban.parse(str)).toThrow(
|
||||||
|
'There can be only one root. No parent could be found for ("fakeRoot")'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('nodes', function () {
|
||||||
|
it('KNBN-7 should handle an id and type for a node definition', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root[The root]
|
||||||
|
`;
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.nodeId).toEqual('root');
|
||||||
|
expect(mm.descr).toEqual('The root');
|
||||||
|
expect(mm.type).toEqual(kanban.yy.nodeType.RECT);
|
||||||
|
});
|
||||||
|
it('KNBN-8 should handle an id and type for a node definition', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root
|
||||||
|
theId(child1)`;
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.descr).toEqual('root');
|
||||||
|
expect(mm.children.length).toEqual(1);
|
||||||
|
const child = mm.children[0];
|
||||||
|
expect(child.descr).toEqual('child1');
|
||||||
|
expect(child.nodeId).toEqual('theId');
|
||||||
|
expect(child.type).toEqual(kanban.yy.nodeType.ROUNDED_RECT);
|
||||||
|
});
|
||||||
|
it('KNBN-9 should handle an id and type for a node definition', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root
|
||||||
|
theId(child1)`;
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.descr).toEqual('root');
|
||||||
|
expect(mm.children.length).toEqual(1);
|
||||||
|
const child = mm.children[0];
|
||||||
|
expect(child.descr).toEqual('child1');
|
||||||
|
expect(child.nodeId).toEqual('theId');
|
||||||
|
expect(child.type).toEqual(kanban.yy.nodeType.ROUNDED_RECT);
|
||||||
|
});
|
||||||
|
it('KNBN-10 multiple types (circle)', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root((the root))
|
||||||
|
`;
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.descr).toEqual('the root');
|
||||||
|
expect(mm.children.length).toEqual(0);
|
||||||
|
expect(mm.type).toEqual(kanban.yy.nodeType.CIRCLE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('KNBN-11 multiple types (cloud)', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root)the root(
|
||||||
|
`;
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.descr).toEqual('the root');
|
||||||
|
expect(mm.children.length).toEqual(0);
|
||||||
|
expect(mm.type).toEqual(kanban.yy.nodeType.CLOUD);
|
||||||
|
});
|
||||||
|
it('KNBN-12 multiple types (bang)', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root))the root((
|
||||||
|
`;
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.descr).toEqual('the root');
|
||||||
|
expect(mm.children.length).toEqual(0);
|
||||||
|
expect(mm.type).toEqual(kanban.yy.nodeType.BANG);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('KNBN-12-a multiple types (hexagon)', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root{{the root}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.type).toEqual(kanban.yy.nodeType.HEXAGON);
|
||||||
|
expect(mm.descr).toEqual('the root');
|
||||||
|
expect(mm.children.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('decorations', function () {
|
||||||
|
it('KNBN-13 should be possible to set an icon for the node', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root[The root]
|
||||||
|
::icon(bomb)
|
||||||
|
`;
|
||||||
|
// ::class1 class2
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.nodeId).toEqual('root');
|
||||||
|
expect(mm.descr).toEqual('The root');
|
||||||
|
expect(mm.type).toEqual(kanban.yy.nodeType.RECT);
|
||||||
|
expect(mm.icon).toEqual('bomb');
|
||||||
|
});
|
||||||
|
it('KNBN-14 should be possible to set classes for the node', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root[The root]
|
||||||
|
:::m-4 p-8
|
||||||
|
`;
|
||||||
|
// ::class1 class2
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.nodeId).toEqual('root');
|
||||||
|
expect(mm.descr).toEqual('The root');
|
||||||
|
expect(mm.type).toEqual(kanban.yy.nodeType.RECT);
|
||||||
|
expect(mm.class).toEqual('m-4 p-8');
|
||||||
|
});
|
||||||
|
it('KNBN-15 should be possible to set both classes and icon for the node', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root[The root]
|
||||||
|
:::m-4 p-8
|
||||||
|
::icon(bomb)
|
||||||
|
`;
|
||||||
|
// ::class1 class2
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.nodeId).toEqual('root');
|
||||||
|
expect(mm.descr).toEqual('The root');
|
||||||
|
expect(mm.type).toEqual(kanban.yy.nodeType.RECT);
|
||||||
|
expect(mm.class).toEqual('m-4 p-8');
|
||||||
|
expect(mm.icon).toEqual('bomb');
|
||||||
|
});
|
||||||
|
it('KNBN-16 should be possible to set both classes and icon for the node', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root[The root]
|
||||||
|
::icon(bomb)
|
||||||
|
:::m-4 p-8
|
||||||
|
`;
|
||||||
|
// ::class1 class2
|
||||||
|
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.nodeId).toEqual('root');
|
||||||
|
expect(mm.descr).toEqual('The root');
|
||||||
|
expect(mm.type).toEqual(kanban.yy.nodeType.RECT);
|
||||||
|
expect(mm.class).toEqual('m-4 p-8');
|
||||||
|
expect(mm.icon).toEqual('bomb');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('descriptions', function () {
|
||||||
|
it('KNBN-17 should be possible to use node syntax in the descriptions', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root["String containing []"]
|
||||||
|
`;
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.nodeId).toEqual('root');
|
||||||
|
expect(mm.descr).toEqual('String containing []');
|
||||||
|
});
|
||||||
|
it('KNBN-18 should be possible to use node syntax in the descriptions in children', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root["String containing []"]
|
||||||
|
child1["String containing ()"]
|
||||||
|
`;
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.nodeId).toEqual('root');
|
||||||
|
expect(mm.descr).toEqual('String containing []');
|
||||||
|
expect(mm.children.length).toEqual(1);
|
||||||
|
expect(mm.children[0].descr).toEqual('String containing ()');
|
||||||
|
});
|
||||||
|
it('KNBN-19 should be possible to have a child after a class assignment', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root(Root)
|
||||||
|
Child(Child)
|
||||||
|
:::hot
|
||||||
|
a(a)
|
||||||
|
b[New Stuff]`;
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.nodeId).toEqual('root');
|
||||||
|
expect(mm.descr).toEqual('Root');
|
||||||
|
expect(mm.children.length).toEqual(1);
|
||||||
|
|
||||||
|
const child = mm.children[0];
|
||||||
|
expect(child.nodeId).toEqual('Child');
|
||||||
|
expect(child.children[0].nodeId).toEqual('a');
|
||||||
|
expect(child.children.length).toEqual(2);
|
||||||
|
expect(child.children[1].nodeId).toEqual('b');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('KNBN-20 should be possible to have meaningless empty rows in a kanban abc124', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root(Root)
|
||||||
|
Child(Child)
|
||||||
|
a(a)
|
||||||
|
|
||||||
|
b[New Stuff]`;
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.nodeId).toEqual('root');
|
||||||
|
expect(mm.descr).toEqual('Root');
|
||||||
|
expect(mm.children.length).toEqual(1);
|
||||||
|
|
||||||
|
const child = mm.children[0];
|
||||||
|
expect(child.nodeId).toEqual('Child');
|
||||||
|
expect(child.children[0].nodeId).toEqual('a');
|
||||||
|
expect(child.children.length).toEqual(2);
|
||||||
|
expect(child.children[1].nodeId).toEqual('b');
|
||||||
|
});
|
||||||
|
it('KNBN-21 should be possible to have comments in a kanban', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root(Root)
|
||||||
|
Child(Child)
|
||||||
|
a(a)
|
||||||
|
|
||||||
|
%% This is a comment
|
||||||
|
b[New Stuff]`;
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.nodeId).toEqual('root');
|
||||||
|
expect(mm.descr).toEqual('Root');
|
||||||
|
expect(mm.children.length).toEqual(1);
|
||||||
|
|
||||||
|
const child = mm.children[0];
|
||||||
|
expect(child.nodeId).toEqual('Child');
|
||||||
|
expect(child.children[0].nodeId).toEqual('a');
|
||||||
|
expect(child.children.length).toEqual(2);
|
||||||
|
expect(child.children[1].nodeId).toEqual('b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('KNBN-22 should be possible to have comments at the end of a line', function () {
|
||||||
|
const str = `kanban
|
||||||
|
root(Root)
|
||||||
|
Child(Child)
|
||||||
|
a(a) %% This is a comment
|
||||||
|
b[New Stuff]`;
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.nodeId).toEqual('root');
|
||||||
|
expect(mm.descr).toEqual('Root');
|
||||||
|
expect(mm.children.length).toEqual(1);
|
||||||
|
|
||||||
|
const child = mm.children[0];
|
||||||
|
expect(child.nodeId).toEqual('Child');
|
||||||
|
expect(child.children[0].nodeId).toEqual('a');
|
||||||
|
expect(child.children.length).toEqual(2);
|
||||||
|
expect(child.children[1].nodeId).toEqual('b');
|
||||||
|
});
|
||||||
|
it('KNBN-23 Rows with only spaces should not interfere', function () {
|
||||||
|
const str = 'kanban\nroot\n A\n \n\n B';
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.nodeId).toEqual('root');
|
||||||
|
expect(mm.children.length).toEqual(2);
|
||||||
|
|
||||||
|
const child = mm.children[0];
|
||||||
|
expect(child.nodeId).toEqual('A');
|
||||||
|
const child2 = mm.children[1];
|
||||||
|
expect(child2.nodeId).toEqual('B');
|
||||||
|
});
|
||||||
|
it('KNBN-24 Handle rows above the kanban declarations', function () {
|
||||||
|
const str = '\n \nkanban\nroot\n A\n \n\n B';
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.nodeId).toEqual('root');
|
||||||
|
expect(mm.children.length).toEqual(2);
|
||||||
|
|
||||||
|
const child = mm.children[0];
|
||||||
|
expect(child.nodeId).toEqual('A');
|
||||||
|
const child2 = mm.children[1];
|
||||||
|
expect(child2.nodeId).toEqual('B');
|
||||||
|
});
|
||||||
|
it('KNBN-25 Handle rows above the kanban declarations, no space', function () {
|
||||||
|
const str = '\n\n\nkanban\nroot\n A\n \n\n B';
|
||||||
|
kanban.parse(str);
|
||||||
|
const mm = kanban.yy.getMindmap();
|
||||||
|
expect(mm.nodeId).toEqual('root');
|
||||||
|
expect(mm.children.length).toEqual(2);
|
||||||
|
|
||||||
|
const child = mm.children[0];
|
||||||
|
expect(child.nodeId).toEqual('A');
|
||||||
|
const child2 = mm.children[1];
|
||||||
|
expect(child2.nodeId).toEqual('B');
|
||||||
|
});
|
||||||
|
});
|
159
packages/mermaid/src/diagrams/kanban/kanbanDb.ts
Normal file
159
packages/mermaid/src/diagrams/kanban/kanbanDb.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||||
|
import type { D3Element } from '../../types.js';
|
||||||
|
import { sanitizeText } from '../../diagrams/common/common.js';
|
||||||
|
import { log } from '../../logger.js';
|
||||||
|
import type { MindmapNode } from './kanbanTypes.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 = {
|
||||||
|
DEFAULT: 0,
|
||||||
|
NO_BORDER: 0,
|
||||||
|
ROUNDED_RECT: 1,
|
||||||
|
RECT: 2,
|
||||||
|
CIRCLE: 3,
|
||||||
|
CLOUD: 4,
|
||||||
|
BANG: 5,
|
||||||
|
HEXAGON: 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getType = (startStr: string, endStr: string): number => {
|
||||||
|
log.debug('In get type', startStr, endStr);
|
||||||
|
switch (startStr) {
|
||||||
|
case '[':
|
||||||
|
return nodeType.RECT;
|
||||||
|
case '(':
|
||||||
|
return endStr === ')' ? nodeType.ROUNDED_RECT : nodeType.CLOUD;
|
||||||
|
case '((':
|
||||||
|
return nodeType.CIRCLE;
|
||||||
|
case ')':
|
||||||
|
return nodeType.CLOUD;
|
||||||
|
case '))':
|
||||||
|
return nodeType.BANG;
|
||||||
|
case '{{':
|
||||||
|
return nodeType.HEXAGON;
|
||||||
|
default:
|
||||||
|
return nodeType.DEFAULT;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setElementForId = (id: number, element: D3Element) => {
|
||||||
|
elements[id] = element;
|
||||||
|
};
|
||||||
|
|
||||||
|
const decorateNode = (decoration?: { class?: string; icon?: string }) => {
|
||||||
|
if (!decoration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const config = getConfig();
|
||||||
|
const node = nodes[nodes.length - 1];
|
||||||
|
if (decoration.icon) {
|
||||||
|
node.icon = sanitizeText(decoration.icon, config);
|
||||||
|
}
|
||||||
|
if (decoration.class) {
|
||||||
|
node.class = sanitizeText(decoration.class, config);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const type2Str = (type: number) => {
|
||||||
|
switch (type) {
|
||||||
|
case nodeType.DEFAULT:
|
||||||
|
return 'no-border';
|
||||||
|
case nodeType.RECT:
|
||||||
|
return 'rect';
|
||||||
|
case nodeType.ROUNDED_RECT:
|
||||||
|
return 'rounded-rect';
|
||||||
|
case nodeType.CIRCLE:
|
||||||
|
return 'circle';
|
||||||
|
case nodeType.CLOUD:
|
||||||
|
return 'cloud';
|
||||||
|
case nodeType.BANG:
|
||||||
|
return 'bang';
|
||||||
|
case nodeType.HEXAGON:
|
||||||
|
return 'hexgon'; // cspell: disable-line
|
||||||
|
default:
|
||||||
|
return 'no-border';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Expose logger to grammar
|
||||||
|
const getLogger = () => log;
|
||||||
|
const getElementById = (id: number) => elements[id];
|
||||||
|
|
||||||
|
const db = {
|
||||||
|
clear,
|
||||||
|
addNode,
|
||||||
|
getMindmap,
|
||||||
|
nodeType,
|
||||||
|
getType,
|
||||||
|
setElementForId,
|
||||||
|
decorateNode,
|
||||||
|
type2Str,
|
||||||
|
getLogger,
|
||||||
|
getElementById,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default db;
|
203
packages/mermaid/src/diagrams/kanban/kanbanRenderer.ts
Normal file
203
packages/mermaid/src/diagrams/kanban/kanbanRenderer.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import cytoscape from 'cytoscape';
|
||||||
|
// @ts-expect-error No types available
|
||||||
|
import coseBilkent from 'cytoscape-cose-bilkent';
|
||||||
|
import { select } from 'd3';
|
||||||
|
import type { MermaidConfig } from '../../config.type.js';
|
||||||
|
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||||
|
import type { DrawDefinition } from '../../diagram-api/types.js';
|
||||||
|
import { log } from '../../logger.js';
|
||||||
|
import type { D3Element } from '../../types.js';
|
||||||
|
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||||
|
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||||
|
import type { FilledMindMapNode, MindmapDB, MindmapNode } from './kanbanTypes.js';
|
||||||
|
import { drawNode, positionNode } from './svgDraw.js';
|
||||||
|
import defaultConfig from '../../defaultConfig.js';
|
||||||
|
|
||||||
|
// Inject the layout algorithm into cytoscape
|
||||||
|
cytoscape.use(coseBilkent);
|
||||||
|
|
||||||
|
async function drawNodes(
|
||||||
|
db: MindmapDB,
|
||||||
|
svg: D3Element,
|
||||||
|
mindmap: FilledMindMapNode,
|
||||||
|
section: number,
|
||||||
|
conf: MermaidConfig
|
||||||
|
) {
|
||||||
|
await drawNode(db, svg, mindmap, section, conf);
|
||||||
|
if (mindmap.children) {
|
||||||
|
await Promise.all(
|
||||||
|
mindmap.children.map((child, index) =>
|
||||||
|
drawNodes(db, svg, child, section < 0 ? index : section, conf)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'cytoscape' {
|
||||||
|
interface EdgeSingular {
|
||||||
|
_private: {
|
||||||
|
bodyBounds: unknown;
|
||||||
|
rscratch: {
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
midX: number;
|
||||||
|
midY: number;
|
||||||
|
endX: number;
|
||||||
|
endY: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawEdges(edgesEl: D3Element, cy: cytoscape.Core) {
|
||||||
|
cy.edges().map((edge, id) => {
|
||||||
|
const data = edge.data();
|
||||||
|
if (edge[0]._private.bodyBounds) {
|
||||||
|
const bounds = edge[0]._private.rscratch;
|
||||||
|
log.trace('Edge: ', id, data);
|
||||||
|
edgesEl
|
||||||
|
.insert('path')
|
||||||
|
.attr(
|
||||||
|
'd',
|
||||||
|
`M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} `
|
||||||
|
)
|
||||||
|
.attr('class', 'edge section-edge-' + data.section + ' edge-depth-' + data.depth);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNodes(mindmap: MindmapNode, cy: cytoscape.Core, conf: MermaidConfig, level: number) {
|
||||||
|
cy.add({
|
||||||
|
group: 'nodes',
|
||||||
|
data: {
|
||||||
|
id: mindmap.id.toString(),
|
||||||
|
labelText: mindmap.descr,
|
||||||
|
height: mindmap.height,
|
||||||
|
width: mindmap.width,
|
||||||
|
level: level,
|
||||||
|
nodeId: mindmap.id,
|
||||||
|
padding: mindmap.padding,
|
||||||
|
type: mindmap.type,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: mindmap.x!,
|
||||||
|
y: mindmap.y!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (mindmap.children) {
|
||||||
|
mindmap.children.forEach((child) => {
|
||||||
|
addNodes(child, cy, conf, level + 1);
|
||||||
|
cy.add({
|
||||||
|
group: 'edges',
|
||||||
|
data: {
|
||||||
|
id: `${mindmap.id}_${child.id}`,
|
||||||
|
source: mindmap.id,
|
||||||
|
target: child.id,
|
||||||
|
depth: level,
|
||||||
|
section: child.section,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function layoutMindmap(node: MindmapNode, conf: MermaidConfig): Promise<cytoscape.Core> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Add temporary render element
|
||||||
|
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
|
||||||
|
const cy = cytoscape({
|
||||||
|
container: document.getElementById('cy'), // container to render in
|
||||||
|
style: [
|
||||||
|
{
|
||||||
|
selector: 'edge',
|
||||||
|
style: {
|
||||||
|
'curve-style': 'bezier',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
// Remove element after layout
|
||||||
|
renderEl.remove();
|
||||||
|
addNodes(node, cy, conf, 0);
|
||||||
|
|
||||||
|
// Make cytoscape care about the dimensions of the nodes
|
||||||
|
cy.nodes().forEach(function (n) {
|
||||||
|
n.layoutDimensions = () => {
|
||||||
|
const data = n.data();
|
||||||
|
return { w: data.width, h: data.height };
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.layout({
|
||||||
|
name: 'cose-bilkent',
|
||||||
|
// @ts-ignore Types for cose-bilkent are not correct?
|
||||||
|
quality: 'proof',
|
||||||
|
styleEnabled: false,
|
||||||
|
animate: false,
|
||||||
|
}).run();
|
||||||
|
cy.ready((e) => {
|
||||||
|
log.info('Ready', e);
|
||||||
|
resolve(cy);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionNodes(db: MindmapDB, cy: cytoscape.Core) {
|
||||||
|
cy.nodes().map((node, id) => {
|
||||||
|
const data = node.data();
|
||||||
|
data.x = node.position().x;
|
||||||
|
data.y = node.position().y;
|
||||||
|
positionNode(db, data);
|
||||||
|
const el = db.getElementById(data.nodeId);
|
||||||
|
log.info('Id:', id, 'Position: (', node.position().x, ', ', node.position().y, ')', data);
|
||||||
|
el.attr(
|
||||||
|
'transform',
|
||||||
|
`translate(${node.position().x - data.width / 2}, ${node.position().y - data.height / 2})`
|
||||||
|
);
|
||||||
|
el.attr('attr', `apa-${id})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
|
||||||
|
log.debug('Rendering mindmap diagram\n' + text);
|
||||||
|
|
||||||
|
const db = diagObj.db as MindmapDB;
|
||||||
|
const mm = db.getMindmap();
|
||||||
|
if (!mm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conf = getConfig();
|
||||||
|
conf.htmlLabels = false;
|
||||||
|
|
||||||
|
const svg = selectSvgElement(id);
|
||||||
|
|
||||||
|
// Draw the graph and start with drawing the nodes without proper position
|
||||||
|
// this gives us the size of the nodes and we can set the positions later
|
||||||
|
|
||||||
|
const edgesElem = svg.append('g');
|
||||||
|
edgesElem.attr('class', 'mindmap-edges');
|
||||||
|
const nodesElem = svg.append('g');
|
||||||
|
nodesElem.attr('class', 'mindmap-nodes');
|
||||||
|
await drawNodes(db, nodesElem, mm as FilledMindMapNode, -1, conf);
|
||||||
|
|
||||||
|
// Next step is to layout the mindmap, giving each node a position
|
||||||
|
|
||||||
|
const cy = await layoutMindmap(mm, conf);
|
||||||
|
|
||||||
|
// After this we can draw, first the edges and the then nodes with the correct position
|
||||||
|
drawEdges(edgesElem, cy);
|
||||||
|
positionNodes(db, cy);
|
||||||
|
|
||||||
|
// Setup the view box and size of the svg element
|
||||||
|
setupGraphViewbox(
|
||||||
|
undefined,
|
||||||
|
svg,
|
||||||
|
conf.mindmap?.padding ?? defaultConfig.mindmap.padding,
|
||||||
|
conf.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
draw,
|
||||||
|
};
|
22
packages/mermaid/src/diagrams/kanban/kanbanTypes.ts
Normal file
22
packages/mermaid/src/diagrams/kanban/kanbanTypes.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { RequiredDeep } from 'type-fest';
|
||||||
|
import type mindmapDb from './kanbanDb.js';
|
||||||
|
|
||||||
|
export interface MindmapNode {
|
||||||
|
id: number;
|
||||||
|
nodeId: string;
|
||||||
|
level: number;
|
||||||
|
descr: string;
|
||||||
|
type: number;
|
||||||
|
children: MindmapNode[];
|
||||||
|
width: number;
|
||||||
|
padding: number;
|
||||||
|
section?: number;
|
||||||
|
height?: number;
|
||||||
|
class?: string;
|
||||||
|
icon?: string;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FilledMindMapNode = RequiredDeep<MindmapNode>;
|
||||||
|
export type MindmapDB = typeof mindmapDb;
|
127
packages/mermaid/src/diagrams/kanban/parser/kanban.jison
Normal file
127
packages/mermaid/src/diagrams/kanban/parser/kanban.jison
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/** mermaid
|
||||||
|
* https://knsv.github.io/mermaid
|
||||||
|
* (c) 2015 Knut Sveidqvist
|
||||||
|
* MIT license.
|
||||||
|
*/
|
||||||
|
%lex
|
||||||
|
|
||||||
|
%options case-insensitive
|
||||||
|
|
||||||
|
%{
|
||||||
|
// Pre-lexer code can go here
|
||||||
|
%}
|
||||||
|
%x NODE
|
||||||
|
%x NSTR
|
||||||
|
%x NSTR2
|
||||||
|
%x ICON
|
||||||
|
%x CLASS
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
||||||
|
\s*\%\%.* {yy.getLogger().trace('Found comment',yytext); return 'SPACELINE';}
|
||||||
|
// \%\%[^\n]*\n /* skip comments */
|
||||||
|
"kanban" return 'MINDMAP';
|
||||||
|
":::" { this.begin('CLASS'); }
|
||||||
|
<CLASS>.+ { this.popState();return 'CLASS'; }
|
||||||
|
<CLASS>\n { this.popState();}
|
||||||
|
// [\s]*"::icon(" { this.begin('ICON'); }
|
||||||
|
"::icon(" { yy.getLogger().trace('Begin icon');this.begin('ICON'); }
|
||||||
|
[\s]+[\n] {yy.getLogger().trace('SPACELINE');return 'SPACELINE' /* skip all whitespace */ ;}
|
||||||
|
[\n]+ return 'NL';
|
||||||
|
<ICON>[^\)]+ { return 'ICON'; }
|
||||||
|
<ICON>\) {yy.getLogger().trace('end icon');this.popState();}
|
||||||
|
"-)" { yy.getLogger().trace('Exploding node'); this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
"(-" { yy.getLogger().trace('Cloud'); this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
"))" { yy.getLogger().trace('Explosion Bang'); this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
")" { yy.getLogger().trace('Cloud Bang'); this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
"((" { this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
"{{" { this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
"(" { this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
"[" { this.begin('NODE');return 'NODE_DSTART'; }
|
||||||
|
[\s]+ return 'SPACELIST' /* skip all whitespace */ ;
|
||||||
|
// !(-\() return 'NODE_ID';
|
||||||
|
[^\(\[\n\)\{\}]+ return 'NODE_ID';
|
||||||
|
<<EOF>> return 'EOF';
|
||||||
|
<NODE>["][`] { this.begin("NSTR2");}
|
||||||
|
<NSTR2>[^`"]+ { return "NODE_DESCR";}
|
||||||
|
<NSTR2>[`]["] { this.popState();}
|
||||||
|
<NODE>["] { yy.getLogger().trace('Starting NSTR');this.begin("NSTR");}
|
||||||
|
<NSTR>[^"]+ { yy.getLogger().trace('description:', yytext); return "NODE_DESCR";}
|
||||||
|
<NSTR>["] {this.popState();}
|
||||||
|
<NODE>[\)]\) {this.popState();yy.getLogger().trace('node end ))');return "NODE_DEND";}
|
||||||
|
<NODE>[\)] {this.popState();yy.getLogger().trace('node end )');return "NODE_DEND";}
|
||||||
|
<NODE>[\]] {this.popState();yy.getLogger().trace('node end ...',yytext);return "NODE_DEND";}
|
||||||
|
<NODE>"}}" {this.popState();yy.getLogger().trace('node end ((');return "NODE_DEND";}
|
||||||
|
<NODE>"(-" {this.popState();yy.getLogger().trace('node end (-');return "NODE_DEND";}
|
||||||
|
<NODE>"-)" {this.popState();yy.getLogger().trace('node end (-');return "NODE_DEND";}
|
||||||
|
<NODE>"((" {this.popState();yy.getLogger().trace('node end ((');return "NODE_DEND";}
|
||||||
|
<NODE>"(" {this.popState();yy.getLogger().trace('node end ((');return "NODE_DEND";}
|
||||||
|
<NODE>[^\)\]\(\}]+ { yy.getLogger().trace('Long description:', yytext); return 'NODE_DESCR';}
|
||||||
|
<NODE>.+(?!\(\() { yy.getLogger().trace('Long description:', yytext); return 'NODE_DESCR';}
|
||||||
|
// [\[] return 'NODE_START';
|
||||||
|
// .+ return 'TXT' ;
|
||||||
|
|
||||||
|
/lex
|
||||||
|
|
||||||
|
%start start
|
||||||
|
|
||||||
|
%% /* language grammar */
|
||||||
|
|
||||||
|
start
|
||||||
|
// %{ : info document 'EOF' { return yy; } }
|
||||||
|
: mindMap
|
||||||
|
| spaceLines mindMap
|
||||||
|
;
|
||||||
|
|
||||||
|
spaceLines
|
||||||
|
: SPACELINE
|
||||||
|
| spaceLines SPACELINE
|
||||||
|
| spaceLines NL
|
||||||
|
;
|
||||||
|
|
||||||
|
mindMap
|
||||||
|
: MINDMAP document { return yy; }
|
||||||
|
| MINDMAP NL document { return yy; }
|
||||||
|
;
|
||||||
|
|
||||||
|
stop
|
||||||
|
: NL {yy.getLogger().trace('Stop NL ');}
|
||||||
|
| EOF {yy.getLogger().trace('Stop EOF ');}
|
||||||
|
| SPACELINE
|
||||||
|
| stop NL {yy.getLogger().trace('Stop NL2 ');}
|
||||||
|
| stop EOF {yy.getLogger().trace('Stop EOF2 ');}
|
||||||
|
;
|
||||||
|
document
|
||||||
|
: document statement stop
|
||||||
|
| statement stop
|
||||||
|
;
|
||||||
|
|
||||||
|
statement
|
||||||
|
: SPACELIST node { yy.getLogger().info('Node: ',$2.id);yy.addNode($1.length, $2.id, $2.descr, $2.type); }
|
||||||
|
| SPACELIST ICON { yy.getLogger().trace('Icon: ',$2);yy.decorateNode({icon: $2}); }
|
||||||
|
| SPACELIST CLASS { yy.decorateNode({class: $2}); }
|
||||||
|
| SPACELINE { yy.getLogger().trace('SPACELIST');}
|
||||||
|
| node { yy.getLogger().trace('Node: ',$1.id);yy.addNode(0, $1.id, $1.descr, $1.type); }
|
||||||
|
| ICON { yy.decorateNode({icon: $1}); }
|
||||||
|
| CLASS { yy.decorateNode({class: $1}); }
|
||||||
|
| SPACELIST
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
node
|
||||||
|
:nodeWithId
|
||||||
|
|nodeWithoutId
|
||||||
|
;
|
||||||
|
|
||||||
|
nodeWithoutId
|
||||||
|
: NODE_DSTART NODE_DESCR NODE_DEND
|
||||||
|
{ yy.getLogger().trace("node found ..", $1); $$ = { id: $2, descr: $2, type: yy.getType($1, $3) }; }
|
||||||
|
;
|
||||||
|
|
||||||
|
nodeWithId
|
||||||
|
: NODE_ID { $$ = { id: $1, descr: $1, type: yy.nodeType.DEFAULT }; }
|
||||||
|
| NODE_ID NODE_DSTART NODE_DESCR NODE_DEND
|
||||||
|
{ yy.getLogger().trace("node found ..", $1); $$ = { id: $1, descr: $3, type: yy.getType($2, $4) }; }
|
||||||
|
;
|
||||||
|
%%
|
84
packages/mermaid/src/diagrams/kanban/styles.ts
Normal file
84
packages/mermaid/src/diagrams/kanban/styles.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// @ts-expect-error Incorrect khroma types
|
||||||
|
import { darken, lighten, isDark } from 'khroma';
|
||||||
|
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
|
||||||
|
|
||||||
|
const genSections: DiagramStylesProvider = (options) => {
|
||||||
|
let sections = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < options.THEME_COLOR_LIMIT; i++) {
|
||||||
|
options['lineColor' + i] = options['lineColor' + i] || options['cScaleInv' + i];
|
||||||
|
if (isDark(options['lineColor' + i])) {
|
||||||
|
options['lineColor' + i] = lighten(options['lineColor' + i], 20);
|
||||||
|
} else {
|
||||||
|
options['lineColor' + i] = darken(options['lineColor' + i], 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < options.THEME_COLOR_LIMIT; i++) {
|
||||||
|
const sw = '' + (17 - 3 * i);
|
||||||
|
sections += `
|
||||||
|
.section-${i - 1} rect, .section-${i - 1} path, .section-${i - 1} circle, .section-${
|
||||||
|
i - 1
|
||||||
|
} polygon, .section-${i - 1} path {
|
||||||
|
fill: ${options['cScale' + i]};
|
||||||
|
}
|
||||||
|
.section-${i - 1} text {
|
||||||
|
fill: ${options['cScaleLabel' + i]};
|
||||||
|
}
|
||||||
|
.node-icon-${i - 1} {
|
||||||
|
font-size: 40px;
|
||||||
|
color: ${options['cScaleLabel' + i]};
|
||||||
|
}
|
||||||
|
.section-edge-${i - 1}{
|
||||||
|
stroke: ${options['cScale' + i]};
|
||||||
|
}
|
||||||
|
.edge-depth-${i - 1}{
|
||||||
|
stroke-width: ${sw};
|
||||||
|
}
|
||||||
|
.section-${i - 1} line {
|
||||||
|
stroke: ${options['cScaleInv' + i]} ;
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled, .disabled circle, .disabled text {
|
||||||
|
fill: lightgray;
|
||||||
|
}
|
||||||
|
.disabled text {
|
||||||
|
fill: #efefef;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: These options seem incorrect.
|
||||||
|
const getStyles: DiagramStylesProvider = (options) =>
|
||||||
|
`
|
||||||
|
.edge {
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
${genSections(options)}
|
||||||
|
.section-root rect, .section-root path, .section-root circle, .section-root polygon {
|
||||||
|
fill: ${options.git0};
|
||||||
|
}
|
||||||
|
.section-root text {
|
||||||
|
fill: ${options.gitBranchLabel0};
|
||||||
|
}
|
||||||
|
.icon-container {
|
||||||
|
height:100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.edge {
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
.mindmap-node-label {
|
||||||
|
dy: 1em;
|
||||||
|
alignment-baseline: middle;
|
||||||
|
text-anchor: middle;
|
||||||
|
dominant-baseline: middle;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export default getStyles;
|
308
packages/mermaid/src/diagrams/kanban/svgDraw.ts
Normal file
308
packages/mermaid/src/diagrams/kanban/svgDraw.ts
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
import { createText } from '../../rendering-util/createText.js';
|
||||||
|
import type { FilledMindMapNode, MindmapDB } from './kanbanTypes.js';
|
||||||
|
import type { Point, D3Element } from '../../types.js';
|
||||||
|
import { parseFontSize } from '../../utils.js';
|
||||||
|
import type { MermaidConfig } from '../../config.type.js';
|
||||||
|
|
||||||
|
const MAX_SECTIONS = 12;
|
||||||
|
|
||||||
|
type ShapeFunction = (
|
||||||
|
db: MindmapDB,
|
||||||
|
elem: D3Element,
|
||||||
|
node: FilledMindMapNode,
|
||||||
|
section?: number
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
const defaultBkg: ShapeFunction = function (db, elem, node, section) {
|
||||||
|
const rd = 5;
|
||||||
|
elem
|
||||||
|
.append('path')
|
||||||
|
.attr('id', 'node-' + node.id)
|
||||||
|
.attr('class', 'node-bkg node-' + db.type2Str(node.type))
|
||||||
|
.attr(
|
||||||
|
'd',
|
||||||
|
`M0 ${node.height - rd} v${-node.height + 2 * rd} q0,-5 5,-5 h${
|
||||||
|
node.width - 2 * rd
|
||||||
|
} q5,0 5,5 v${node.height - rd} H0 Z`
|
||||||
|
);
|
||||||
|
|
||||||
|
elem
|
||||||
|
.append('line')
|
||||||
|
.attr('class', 'node-line-' + section)
|
||||||
|
.attr('x1', 0)
|
||||||
|
.attr('y1', node.height)
|
||||||
|
.attr('x2', node.width)
|
||||||
|
.attr('y2', node.height);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rectBkg: ShapeFunction = function (db, elem, node) {
|
||||||
|
elem
|
||||||
|
.append('rect')
|
||||||
|
.attr('id', 'node-' + node.id)
|
||||||
|
.attr('class', 'node-bkg node-' + db.type2Str(node.type))
|
||||||
|
.attr('height', node.height)
|
||||||
|
.attr('width', node.width);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cloudBkg: ShapeFunction = function (db, elem, node) {
|
||||||
|
const w = node.width;
|
||||||
|
const h = node.height;
|
||||||
|
const r1 = 0.15 * w;
|
||||||
|
const r2 = 0.25 * w;
|
||||||
|
const r3 = 0.35 * w;
|
||||||
|
const r4 = 0.2 * w;
|
||||||
|
elem
|
||||||
|
.append('path')
|
||||||
|
.attr('id', 'node-' + node.id)
|
||||||
|
.attr('class', 'node-bkg node-' + db.type2Str(node.type))
|
||||||
|
.attr(
|
||||||
|
'd',
|
||||||
|
`M0 0 a${r1},${r1} 0 0,1 ${w * 0.25},${-1 * w * 0.1}
|
||||||
|
a${r3},${r3} 1 0,1 ${w * 0.4},${-1 * w * 0.1}
|
||||||
|
a${r2},${r2} 1 0,1 ${w * 0.35},${1 * w * 0.2}
|
||||||
|
|
||||||
|
a${r1},${r1} 1 0,1 ${w * 0.15},${1 * h * 0.35}
|
||||||
|
a${r4},${r4} 1 0,1 ${-1 * w * 0.15},${1 * h * 0.65}
|
||||||
|
|
||||||
|
a${r2},${r1} 1 0,1 ${-1 * w * 0.25},${w * 0.15}
|
||||||
|
a${r3},${r3} 1 0,1 ${-1 * w * 0.5},${0}
|
||||||
|
a${r1},${r1} 1 0,1 ${-1 * w * 0.25},${-1 * w * 0.15}
|
||||||
|
|
||||||
|
a${r1},${r1} 1 0,1 ${-1 * w * 0.1},${-1 * h * 0.35}
|
||||||
|
a${r4},${r4} 1 0,1 ${w * 0.1},${-1 * h * 0.65}
|
||||||
|
|
||||||
|
H0 V0 Z`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bangBkg: ShapeFunction = function (db, elem, node) {
|
||||||
|
const w = node.width;
|
||||||
|
const h = node.height;
|
||||||
|
const r = 0.15 * w;
|
||||||
|
elem
|
||||||
|
.append('path')
|
||||||
|
.attr('id', 'node-' + node.id)
|
||||||
|
.attr('class', 'node-bkg node-' + db.type2Str(node.type))
|
||||||
|
.attr(
|
||||||
|
'd',
|
||||||
|
`M0 0 a${r},${r} 1 0,0 ${w * 0.25},${-1 * h * 0.1}
|
||||||
|
a${r},${r} 1 0,0 ${w * 0.25},${0}
|
||||||
|
a${r},${r} 1 0,0 ${w * 0.25},${0}
|
||||||
|
a${r},${r} 1 0,0 ${w * 0.25},${1 * h * 0.1}
|
||||||
|
|
||||||
|
a${r},${r} 1 0,0 ${w * 0.15},${1 * h * 0.33}
|
||||||
|
a${r * 0.8},${r * 0.8} 1 0,0 ${0},${1 * h * 0.34}
|
||||||
|
a${r},${r} 1 0,0 ${-1 * w * 0.15},${1 * h * 0.33}
|
||||||
|
|
||||||
|
a${r},${r} 1 0,0 ${-1 * w * 0.25},${h * 0.15}
|
||||||
|
a${r},${r} 1 0,0 ${-1 * w * 0.25},${0}
|
||||||
|
a${r},${r} 1 0,0 ${-1 * w * 0.25},${0}
|
||||||
|
a${r},${r} 1 0,0 ${-1 * w * 0.25},${-1 * h * 0.15}
|
||||||
|
|
||||||
|
a${r},${r} 1 0,0 ${-1 * w * 0.1},${-1 * h * 0.33}
|
||||||
|
a${r * 0.8},${r * 0.8} 1 0,0 ${0},${-1 * h * 0.34}
|
||||||
|
a${r},${r} 1 0,0 ${w * 0.1},${-1 * h * 0.33}
|
||||||
|
|
||||||
|
H0 V0 Z`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const circleBkg: ShapeFunction = function (db, elem, node) {
|
||||||
|
elem
|
||||||
|
.append('circle')
|
||||||
|
.attr('id', 'node-' + node.id)
|
||||||
|
.attr('class', 'node-bkg node-' + db.type2Str(node.type))
|
||||||
|
.attr('r', node.width / 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
function insertPolygonShape(
|
||||||
|
parent: D3Element,
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
points: Point[],
|
||||||
|
node: FilledMindMapNode
|
||||||
|
) {
|
||||||
|
return parent
|
||||||
|
.insert('polygon', ':first-child')
|
||||||
|
.attr(
|
||||||
|
'points',
|
||||||
|
points
|
||||||
|
.map(function (d) {
|
||||||
|
return d.x + ',' + d.y;
|
||||||
|
})
|
||||||
|
.join(' ')
|
||||||
|
)
|
||||||
|
.attr('transform', 'translate(' + (node.width - w) / 2 + ', ' + h + ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexagonBkg: ShapeFunction = function (
|
||||||
|
_db: MindmapDB,
|
||||||
|
elem: D3Element,
|
||||||
|
node: FilledMindMapNode
|
||||||
|
) {
|
||||||
|
const h = node.height;
|
||||||
|
const f = 4;
|
||||||
|
const m = h / f;
|
||||||
|
const w = node.width - node.padding + 2 * m;
|
||||||
|
const points: Point[] = [
|
||||||
|
{ x: m, y: 0 },
|
||||||
|
{ x: w - m, y: 0 },
|
||||||
|
{ x: w, y: -h / 2 },
|
||||||
|
{ x: w - m, y: -h },
|
||||||
|
{ x: m, y: -h },
|
||||||
|
{ x: 0, y: -h / 2 },
|
||||||
|
];
|
||||||
|
insertPolygonShape(elem, w, h, points, node);
|
||||||
|
};
|
||||||
|
|
||||||
|
const roundedRectBkg: ShapeFunction = function (db, elem, node) {
|
||||||
|
elem
|
||||||
|
.append('rect')
|
||||||
|
.attr('id', 'node-' + node.id)
|
||||||
|
.attr('class', 'node-bkg node-' + db.type2Str(node.type))
|
||||||
|
.attr('height', node.height)
|
||||||
|
.attr('rx', node.padding)
|
||||||
|
.attr('ry', node.padding)
|
||||||
|
.attr('width', node.width);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param db - The database
|
||||||
|
* @param elem - The D3 dom element in which the node is to be added
|
||||||
|
* @param node - The node to be added
|
||||||
|
* @param fullSection - ?
|
||||||
|
* @param conf - The configuration object
|
||||||
|
* @returns The height nodes dom element
|
||||||
|
*/
|
||||||
|
export const drawNode = async function (
|
||||||
|
db: MindmapDB,
|
||||||
|
elem: D3Element,
|
||||||
|
node: FilledMindMapNode,
|
||||||
|
fullSection: number,
|
||||||
|
conf: MermaidConfig
|
||||||
|
): Promise<number> {
|
||||||
|
const htmlLabels = conf.htmlLabels;
|
||||||
|
const section = fullSection % (MAX_SECTIONS - 1);
|
||||||
|
const nodeElem = elem.append('g');
|
||||||
|
node.section = section;
|
||||||
|
let sectionClass = 'section-' + section;
|
||||||
|
if (section < 0) {
|
||||||
|
sectionClass += ' section-root';
|
||||||
|
}
|
||||||
|
nodeElem.attr('class', (node.class ? node.class + ' ' : '') + 'mindmap-node ' + sectionClass);
|
||||||
|
const bkgElem = nodeElem.append('g');
|
||||||
|
|
||||||
|
// Create the wrapped text element
|
||||||
|
const textElem = nodeElem.append('g');
|
||||||
|
const description = node.descr.replace(/(<br\/*>)/g, '\n');
|
||||||
|
await createText(
|
||||||
|
textElem,
|
||||||
|
description,
|
||||||
|
{
|
||||||
|
useHtmlLabels: htmlLabels,
|
||||||
|
width: node.width,
|
||||||
|
classes: 'mindmap-node-label',
|
||||||
|
},
|
||||||
|
conf
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!htmlLabels) {
|
||||||
|
textElem
|
||||||
|
.attr('dy', '1em')
|
||||||
|
.attr('alignment-baseline', 'middle')
|
||||||
|
.attr('dominant-baseline', 'middle')
|
||||||
|
.attr('text-anchor', 'middle');
|
||||||
|
}
|
||||||
|
const bbox = textElem.node().getBBox();
|
||||||
|
const [fontSize] = parseFontSize(conf.fontSize);
|
||||||
|
node.height = bbox.height + fontSize! * 1.1 * 0.5 + node.padding;
|
||||||
|
node.width = bbox.width + 2 * node.padding;
|
||||||
|
if (node.icon) {
|
||||||
|
if (node.type === db.nodeType.CIRCLE) {
|
||||||
|
node.height += 50;
|
||||||
|
node.width += 50;
|
||||||
|
const icon = nodeElem
|
||||||
|
.append('foreignObject')
|
||||||
|
.attr('height', '50px')
|
||||||
|
.attr('width', node.width)
|
||||||
|
.attr('style', 'text-align: center;');
|
||||||
|
icon
|
||||||
|
.append('div')
|
||||||
|
.attr('class', 'icon-container')
|
||||||
|
.append('i')
|
||||||
|
.attr('class', 'node-icon-' + section + ' ' + node.icon);
|
||||||
|
textElem.attr(
|
||||||
|
'transform',
|
||||||
|
'translate(' + node.width / 2 + ', ' + (node.height / 2 - 1.5 * node.padding) + ')'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
node.width += 50;
|
||||||
|
const orgHeight = node.height;
|
||||||
|
node.height = Math.max(orgHeight, 60);
|
||||||
|
const heightDiff = Math.abs(node.height - orgHeight);
|
||||||
|
const icon = nodeElem
|
||||||
|
.append('foreignObject')
|
||||||
|
.attr('width', '60px')
|
||||||
|
.attr('height', node.height)
|
||||||
|
.attr('style', 'text-align: center;margin-top:' + heightDiff / 2 + 'px;');
|
||||||
|
|
||||||
|
icon
|
||||||
|
.append('div')
|
||||||
|
.attr('class', 'icon-container')
|
||||||
|
.append('i')
|
||||||
|
.attr('class', 'node-icon-' + section + ' ' + node.icon);
|
||||||
|
textElem.attr(
|
||||||
|
'transform',
|
||||||
|
'translate(' + (25 + node.width / 2) + ', ' + (heightDiff / 2 + node.padding / 2) + ')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!htmlLabels) {
|
||||||
|
const dx = node.width / 2;
|
||||||
|
const dy = node.padding / 2;
|
||||||
|
textElem.attr('transform', 'translate(' + dx + ', ' + dy + ')');
|
||||||
|
// textElem.attr('transform', 'translate(' + node.width / 2 + ', ' + node.padding / 2 + ')');
|
||||||
|
} else {
|
||||||
|
const dx = (node.width - bbox.width) / 2;
|
||||||
|
const dy = (node.height - bbox.height) / 2;
|
||||||
|
textElem.attr('transform', 'translate(' + dx + ', ' + dy + ')');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (node.type) {
|
||||||
|
case db.nodeType.DEFAULT:
|
||||||
|
defaultBkg(db, bkgElem, node, section);
|
||||||
|
break;
|
||||||
|
case db.nodeType.ROUNDED_RECT:
|
||||||
|
roundedRectBkg(db, bkgElem, node, section);
|
||||||
|
break;
|
||||||
|
case db.nodeType.RECT:
|
||||||
|
rectBkg(db, bkgElem, node, section);
|
||||||
|
break;
|
||||||
|
case db.nodeType.CIRCLE:
|
||||||
|
bkgElem.attr('transform', 'translate(' + node.width / 2 + ', ' + +node.height / 2 + ')');
|
||||||
|
circleBkg(db, bkgElem, node, section);
|
||||||
|
break;
|
||||||
|
case db.nodeType.CLOUD:
|
||||||
|
cloudBkg(db, bkgElem, node, section);
|
||||||
|
break;
|
||||||
|
case db.nodeType.BANG:
|
||||||
|
bangBkg(db, bkgElem, node, section);
|
||||||
|
break;
|
||||||
|
case db.nodeType.HEXAGON:
|
||||||
|
hexagonBkg(db, bkgElem, node, section);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.setElementForId(node.id, nodeElem);
|
||||||
|
return node.height;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const positionNode = function (db: MindmapDB, node: FilledMindMapNode) {
|
||||||
|
const nodeElem = db.getElementById(node.id);
|
||||||
|
|
||||||
|
const x = node.x || 0;
|
||||||
|
const y = node.y || 0;
|
||||||
|
// Position the node to its coordinate
|
||||||
|
nodeElem.attr('transform', 'translate(' + x + ',' + y + ')');
|
||||||
|
};
|
Reference in New Issue
Block a user