Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: TypeScript improvements to packages/mermaid/src/rendering-util/rendering-elements #5974

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 type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
import { type TreeData, findCommonAncestor } from './find-common-ancestor.js';

type Node = LayoutData['nodes'][number];
sidharthv96 marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -21,30 +36,40 @@
}: InternalHelpers,
{ algorithm }: RenderOptions
) => {
const nodeDb: Record<string, any> = {};

Check warning on line 39 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
const clusterDb: Record<string, any> = {};

Check warning on line 40 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type

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();
saurabhg772244 marked this conversation as resolved.
Show resolved Hide resolved
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 @@
};

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 All @@ -99,21 +112,21 @@
const drawNodes = async (
relX: number,
relY: number,
nodeArray: any[],

Check warning on line 115 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
svg: any,

Check warning on line 116 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
subgraphsEl: SVGGroup,
depth: number
) => {
await Promise.all(
nodeArray.map(async function (node: {
id: string | number;
x: any;

Check warning on line 123 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
y: any;

Check warning on line 124 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
width: number;
labels: { width: any }[];

Check warning on line 126 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
height: number;
isGroup: any;

Check warning on line 128 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
labelData: any;

Check warning on line 129 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
offset: { posX: number; posY: number };
shape: any;
domId: { node: () => any; attr: (arg0: string, arg1: string) => void };
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
45 changes: 29 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 } 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,12 @@ 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 MaybePromise<T> = T | Promise<T>;
sidharthv96 marked this conversation as resolved.
Show resolved Hide resolved
type ShapeHandler = <T extends SVGGraphicsElement>(
parent: D3Selection<T>,
node: Node,
options: ShapeRenderOptions
) => MaybePromise<D3Selection<SVGGElement>>;

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

export const shapesDefs: ShapeDefinition[] = [
export const shapesDefs = [
{
semanticName: 'Process',
name: 'Rectangle',
Expand Down Expand Up @@ -442,11 +448,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 +470,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
Loading