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

60 changes: 33 additions & 27 deletions packages/mermaid-layout-elk/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
import { type TreeData, findCommonAncestor } from './find-common-ancestor.js';

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

interface NodeWithVertex extends Omit<Node, 'domId'> {
children?: unknown[];
labelData?: any;

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

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
saurabhg772244 marked this conversation as resolved.
Show resolved Hide resolved
domId?: Node['domId'] | SVGGroup | d3.Selection<SVGAElement, unknown, Element | null, unknown>;
}

export const render = async (
data4Layout: LayoutData,
svg: SVG,
Expand All @@ -21,30 +29,40 @@
}: InternalHelpers,
{ algorithm }: RenderOptions
) => {
const nodeDb: Record<string, any> = {};

Check warning on line 32 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 33 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 addVertex = async (
nodeEl: SVGGroup,
graph: { children: NodeWithVertex[] },
nodeArr: Node[],
node: Node
) => {
const labelData: any = { width: 0, height: 0 };

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

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type

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 +86,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,18 +105,18 @@
const drawNodes = async (
relX: number,
relY: number,
nodeArray: any[],

Check warning on line 108 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 109 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 116 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 117 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 119 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;
labelData: any;
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,21 @@
import { log } from '../../logger.js';
import { shapes } from './shapes.js';
import type { Node } from '../types.js';
import type { MermaidConfig, 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: { config: MermaidConfig; dir: Node['dir'] }
aloisklink marked this conversation as resolved.
Show resolved Hide resolved
) {
let newEl: NodeElement | undefined;
let el;

//special check for rect shape (with or without rounded corners)
Expand All @@ -16,7 +27,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 +41,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 +57,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;
};
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { labelHelper, 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 { insertPolygonShape } from './insertPolygonShape.js';
import { createPathFromPoints } from './util.js';
import type { D3Selection } from '../../../types.js';

// const createPathFromPoints = (points: { x: number; y: number }[]): string => {
// const pointStrings = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`);
// pointStrings.push('Z');
// return pointStrings.join(' ');
// };

export async function card(parent: SVGAElement, node: Node): Promise<SVGAElement> {
export async function card<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 @@ -34,10 +35,11 @@ export async function card(parent: SVGAElement, node: Node): Promise<SVGAElement
{ x: left + padding, y: top },
];

let polygon: d3.Selection<SVGPolygonElement | SVGGElement, unknown, null, undefined>;
let polygon: D3Selection<SVGGElement> | Awaited<ReturnType<typeof insertPolygonShape>>;
const { cssStyles } = node;

if (node.look === 'handDrawn') {
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const pathData = createPathFromPoints(points);
Expand Down
Loading
Loading