#5342 Initial commit

This commit is contained in:
Knut Sveidqvist
2024-10-04 16:03:55 +02:00
parent ddf18dd233
commit 2d7686eb65
9 changed files with 1304 additions and 0 deletions

View 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;

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

View 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');
});
});

View 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;

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

View 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;

View 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) }; }
;
%%

View 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;

View 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 + ')');
};