Skip to content

Commit

Permalink
Merge pull request #5974 from aloisklink/refactor/improving-rendering…
Browse files Browse the repository at this point in the history
…-shape-types

refactor: TypeScript improvements to `packages/mermaid/src/rendering-util/rendering-elements`
  • Loading branch information
sidharthv96 authored Oct 28, 2024
2 parents d16e46a + b5cd101 commit e765007
Show file tree
Hide file tree
Showing 69 changed files with 587 additions and 324 deletions.
6 changes: 3 additions & 3 deletions docs/config/setup/interfaces/mermaid.RenderResult.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
#### Defined in
[packages/mermaid/src/types.ts:90](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L90)
[packages/mermaid/src/types.ts:95](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L95)
---
Expand All @@ -51,7 +51,7 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
#### Defined in
[packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L80)
[packages/mermaid/src/types.ts:85](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L85)
---
Expand All @@ -63,4 +63,4 @@ The svg code for the rendered graph.
#### Defined in
[packages/mermaid/src/types.ts:76](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L76)
[packages/mermaid/src/types.ts:81](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L81)
69 changes: 41 additions & 28 deletions packages/mermaid-layout-elk/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@ import ELK from 'elkjs/lib/elk.bundled.js';
import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
import { type TreeData, findCommonAncestor } from './find-common-ancestor.js';

type Node = LayoutData['nodes'][number];

interface LabelData {
width: number;
height: number;
wrappingWidth?: number;
labelNode?: SVGGElement | null;
}

interface NodeWithVertex extends Omit<Node, 'domId'> {
children?: unknown[];
labelData?: LabelData;
domId?: Node['domId'] | SVGGroup | d3.Selection<SVGAElement, unknown, Element | null, unknown>;
}

export const render = async (
data4Layout: LayoutData,
svg: SVG,
Expand All @@ -24,27 +39,37 @@ export const render = async (
const nodeDb: Record<string, any> = {};
const clusterDb: Record<string, any> = {};

const addVertex = async (nodeEl: any, graph: { children: any[] }, nodeArr: any, node: any) => {
const labelData: any = { width: 0, height: 0 };
const addVertex = async (
nodeEl: SVGGroup,
graph: { children: NodeWithVertex[] },
nodeArr: Node[],
node: Node
) => {
const labelData: LabelData = { width: 0, height: 0 };

let boundingBox;
const child = {
...node,
};
graph.children.push(child);
nodeDb[node.id] = child;
const config = getConfig();

// Add the element to the DOM
if (!node.isGroup) {
const child: NodeWithVertex = {
...node,
};
graph.children.push(child);
nodeDb[node.id] = child;

const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir });
boundingBox = childNodeEl.node().getBBox();
const boundingBox = childNodeEl.node()!.getBBox();
child.domId = childNodeEl;
child.width = boundingBox.width;
child.height = boundingBox.height;
} else {
// A subgraph
child.children = [];
const child: NodeWithVertex & { children: NodeWithVertex[] } = {
...node,
children: [],
};
graph.children.push(child);
nodeDb[node.id] = child;
await addVertices(nodeEl, nodeArr, child, node.id);

if (node.label) {
Expand All @@ -68,28 +93,16 @@ export const render = async (
};

const addVertices = async function (
nodeEl: any,
nodeArr: any[],
graph: {
id: string;
layoutOptions: {
'elk.hierarchyHandling': string;
'elk.algorithm': any;
'nodePlacement.strategy': any;
'elk.layered.mergeEdges': any;
'elk.direction': string;
'spacing.baseValue': number;
};
children: never[];
edges: never[];
},
parentId?: undefined
nodeEl: SVGGroup,
nodeArr: Node[],
graph: { children: NodeWithVertex[] },
parentId?: string
) {
const siblings = nodeArr.filter((node: { parentId: any }) => node.parentId === parentId);
const siblings = nodeArr.filter((node) => node?.parentId === parentId);
log.info('addVertices APA12', siblings, parentId);
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
await Promise.all(
siblings.map(async (node: any) => {
siblings.map(async (node) => {
await addVertex(nodeEl, graph, nodeArr, node);
})
);
Expand Down
2 changes: 1 addition & 1 deletion packages/mermaid/src/rendering-util/createText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export const createText = async (
width = 200,
addSvgBackground = false,
} = {},
config: MermaidConfig
config?: MermaidConfig
) => {
log.debug(
'XYZ createText',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe('Test Alias for shapes', function () {
});

it('should support alias for shadedProcess shape ', function () {
const aliases = ['lined-process', 'lined-rectangle', 'lin-proc', 'lin-rect'];
const aliases = ['lined-process', 'lined-rectangle', 'lin-proc', 'lin-rect'] as const;
for (const alias of aliases) {
expect(shapes[alias]).toBe(shapes['shaded-process']);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { log } from '../../logger.js';
import { shapes } from './shapes.js';
import type { Node, ShapeRenderOptions } from '../types.js';
import type { SVGGroup } from '../../mermaid.js';
import type { D3Selection } from '../../types.js';
import type { graphlib } from 'dagre-d3-es';

const nodeElems = new Map();
type ShapeHandler = (typeof shapes)[keyof typeof shapes];
type NodeElement = D3Selection<SVGAElement> | Awaited<ReturnType<ShapeHandler>>;

export const insertNode = async (elem, node, renderOptions) => {
let newEl;
const nodeElems = new Map<string, NodeElement>();

export async function insertNode(elem: SVGGroup, node: Node, renderOptions: ShapeRenderOptions) {
let newEl: NodeElement | undefined;
let el;

//special check for rect shape (with or without rounded corners)
Expand All @@ -16,7 +23,7 @@ export const insertNode = async (elem, node, renderOptions) => {
}
}

const shapeHandler = shapes[node.shape];
const shapeHandler = shapes[(node.shape ?? 'undefined') as keyof typeof shapes];

if (!shapeHandler) {
throw new Error(`No such shape: ${node.shape}. Please check your syntax.`);
Expand All @@ -30,7 +37,10 @@ export const insertNode = async (elem, node, renderOptions) => {
} else if (node.linkTarget) {
target = node.linkTarget || '_blank';
}
newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target);
newEl = elem
.insert<SVGAElement>('svg:a')
.attr('xlink:href', node.link)
.attr('target', target ?? null);
el = await shapeHandler(newEl, node, renderOptions);
} else {
el = await shapeHandler(elem, node, renderOptions);
Expand All @@ -43,21 +53,21 @@ export const insertNode = async (elem, node, renderOptions) => {
nodeElems.set(node.id, newEl);

if (node.haveCallback) {
nodeElems.get(node.id).attr('class', nodeElems.get(node.id).attr('class') + ' clickable');
newEl.attr('class', newEl.attr('class') + ' clickable');
}
return newEl;
};
}

export const setNodeElem = (elem, node) => {
export const setNodeElem = (elem: NodeElement, node: Pick<Node, 'id'>) => {
nodeElems.set(node.id, elem);
};

export const clear = () => {
nodeElems.clear();
};

export const positionNode = (node) => {
const el = nodeElems.get(node.id);
export const positionNode = (node: ReturnType<graphlib.Graph['node']>) => {
const el = nodeElems.get(node.id)!;
log.trace(
'Transforming node',
node.diff,
Expand Down
44 changes: 28 additions & 16 deletions packages/mermaid/src/rendering-util/rendering-elements/shapes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Entries } from 'type-fest';
import type { D3Selection, MaybePromise } from '../../types.js';
import type { Node, ShapeRenderOptions } from '../types.js';
import { anchor } from './shapes/anchor.js';
import { bowTieRect } from './shapes/bowTieRect.js';
Expand Down Expand Up @@ -56,8 +58,11 @@ import { waveEdgedRectangle } from './shapes/waveEdgedRectangle.js';
import { waveRectangle } from './shapes/waveRectangle.js';
import { windowPane } from './shapes/windowPane.js';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ShapeHandler = (parent: any, node: Node, options: ShapeRenderOptions) => unknown;
type ShapeHandler = <T extends SVGGraphicsElement>(
parent: D3Selection<T>,
node: Node,
options: ShapeRenderOptions
) => MaybePromise<D3Selection<SVGGElement>>;

export interface ShapeDefinition {
semanticName: string;
Expand All @@ -75,7 +80,7 @@ export interface ShapeDefinition {
handler: ShapeHandler;
}

export const shapesDefs: ShapeDefinition[] = [
export const shapesDefs = [
{
semanticName: 'Process',
name: 'Rectangle',
Expand Down Expand Up @@ -442,11 +447,11 @@ export const shapesDefs: ShapeDefinition[] = [
aliases: ['lined-document'],
handler: linedWaveEdgedRect,
},
];
] as const satisfies ShapeDefinition[];

const generateShapeMap = () => {
// These are the shapes that didn't have documentation present
const shapeMap: Record<string, ShapeHandler> = {
const undocumentedShapes = {
// States
state,
choice,
Expand All @@ -464,18 +469,25 @@ const generateShapeMap = () => {
imageSquare,

anchor,
};
} as const;

for (const shape of shapesDefs) {
for (const alias of [
shape.shortName,
...(shape.aliases ?? []),
...(shape.internalAliases ?? []),
]) {
shapeMap[alias] = shape.handler;
}
}
return shapeMap;
const entries = [
...(Object.entries(undocumentedShapes) as Entries<typeof undocumentedShapes>),
...shapesDefs.flatMap((shape) => {
const aliases = [
shape.shortName,
...('aliases' in shape ? shape.aliases : []),
...('internalAliases' in shape ? shape.internalAliases : []),
];
return aliases.map((alias) => [alias, shape.handler] as const);
}),
];
return Object.fromEntries(entries) as Record<
(typeof entries)[number][0],
(typeof entries)[number][1]
> satisfies Record<string, ShapeHandler>;
};

export const shapes = generateShapeMap();

export type ShapeID = keyof typeof shapes;
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { log } from '../../../logger.js';
import { updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import type { Node } from '../../types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import { handleUndefinedAttr } from '../../../utils.js';
import type { D3Selection } from '../../../types.js';

export const anchor = (parent: SVGAElement, node: Node): Promise<SVGAElement> => {
export function anchor<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
const { labelStyles } = styles2String(node);
node.labelStyle = labelStyles;
const classes = getNodeClasses(node);
Expand All @@ -14,7 +16,6 @@ export const anchor = (parent: SVGAElement, node: Node): Promise<SVGAElement> =>
cssClasses = 'anchor';
}
const shapeSvg = parent
// @ts-ignore - SVGElement is not typed
.insert('g')
.attr('class', cssClasses)
.attr('id', node.domId || node.id);
Expand All @@ -23,6 +24,7 @@ export const anchor = (parent: SVGAElement, node: Node): Promise<SVGAElement> =>

const { cssStyles } = node;

// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, { fill: 'black', stroke: 'none', fillStyle: 'solid' });

Expand All @@ -31,7 +33,7 @@ export const anchor = (parent: SVGAElement, node: Node): Promise<SVGAElement> =>
}
const roughNode = rc.circle(0, 0, radius * 2, options);
const circleElem = shapeSvg.insert(() => roughNode, ':first-child');
circleElem.attr('class', 'anchor').attr('style', cssStyles);
circleElem.attr('class', 'anchor').attr('style', handleUndefinedAttr(cssStyles));

updateNodeBounds(node, circleElem);

Expand All @@ -41,4 +43,4 @@ export const anchor = (parent: SVGAElement, node: Node): Promise<SVGAElement> =>
};

return shapeSvg;
};
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import type { Node } from '../../types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import type { D3Selection } from '../../../types.js';

function generateArcPoints(
x1: number,
Expand Down Expand Up @@ -70,7 +71,7 @@ function generateArcPoints(
return points;
}

export const bowTieRect = async (parent: SVGAElement, node: Node) => {
export async function bowTieRect<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
Expand All @@ -91,6 +92,7 @@ export const bowTieRect = async (parent: SVGAElement, node: Node) => {
...generateArcPoints(w / 2, h / 2, w / 2, -h / 2, rx, ry, true),
];

// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});

Expand Down Expand Up @@ -122,4 +124,4 @@ export const bowTieRect = async (parent: SVGAElement, node: Node) => {
};

return shapeSvg;
};
}
Loading

0 comments on commit e765007

Please sign in to comment.