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

fix: make mermaid.parse throw an error on invalid shapes #6002

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thick-elephants-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'mermaid': patch
---

fix: error `mermaid.parse` on an invalid shape, so that it matches the errors thrown by `mermaid.render`
47 changes: 30 additions & 17 deletions packages/mermaid/src/diagrams/flowchart/flowDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { select } from 'd3';
import utils, { getEdgeId } from '../../utils.js';
import { getConfig, defaultConfig } from '../../diagram-api/diagramAPI.js';
import common from '../common/common.js';
import { isValidShape, type ShapeID } from '../../rendering-util/rendering-elements/shapes.js';
import type { Node, Edge } from '../../rendering-util/types.js';
import { log } from '../../logger.js';
import * as yaml from 'js-yaml';
Expand All @@ -14,7 +15,15 @@ import {
setDiagramTitle,
getDiagramTitle,
} from '../common/commonDb.js';
import type { FlowVertex, FlowClass, FlowSubGraph, FlowText, FlowEdge, FlowLink } from './types.js';
import type {
FlowVertex,
FlowClass,
FlowSubGraph,
FlowText,
FlowEdge,
FlowLink,
FlowVertexTypeParam,
} from './types.js';
import type { NodeMetaData } from '../../types.js';

const MERMAID_DOM_ID_PREFIX = 'flowchart-';
Expand Down Expand Up @@ -53,12 +62,11 @@ export const lookUpDomId = function (id: string) {

/**
* Function called by parser when a node definition has been found
*
*/
export const addVertex = function (
id: string,
textObj: FlowText,
type: 'group',
type: FlowVertexTypeParam,
style: string[],
classes: string[],
dir: string,
Expand Down Expand Up @@ -133,14 +141,15 @@ export const addVertex = function (
}
// console.log('yamlData', yamlData);
const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData;
if (doc.shape && (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_'))) {
throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`);
}

// console.log('yamlData doc', doc);
if (doc?.shape) {
if (doc.shape) {
if (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_')) {
throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`);
} else if (!isValidShape(doc.shape)) {
throw new Error(`No such shape: ${doc.shape}.`);
}
vertex.type = doc?.shape;
}

if (doc?.label) {
vertex.text = doc?.label;
}
Expand Down Expand Up @@ -816,7 +825,7 @@ export const lex = {
firstGraph,
};

const getTypeFromVertex = (vertex: FlowVertex) => {
const getTypeFromVertex = (vertex: FlowVertex): ShapeID => {
if (vertex.img) {
return 'imageSquare';
}
Expand All @@ -832,14 +841,18 @@ const getTypeFromVertex = (vertex: FlowVertex) => {
}
return 'icon';
}
if (vertex.type === 'square') {
return 'squareRect';
}
if (vertex.type === 'round') {
return 'roundedRect';
switch (vertex.type) {
case 'square':
case undefined:
return 'squareRect';
case 'round':
return 'roundedRect';
case 'ellipse':
// @ts-expect-error -- Ellipses are broken, see https://github.com/mermaid-js/mermaid/issues/5976
return 'ellipse';
default:
return vertex.type;
}

return vertex.type ?? 'squareRect';
};

const findNode = (nodes: Node[], id: string) => nodes.find((node) => node.id === id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,21 @@ describe('when parsing directions', function () {
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is }');
});
it('should error on non-existent shape', function () {
expect(() => {
flow.parser.parse(`flowchart TB
A@{ shape: this-shape-does-not-exist }
`);
}).toThrow('No such shape: this-shape-does-not-exist.');
});
it('should error on internal-only shape', function () {
expect(() => {
// this shape does exist, but it's only supposed to be for internal/backwards compatibility use
flow.parser.parse(`flowchart TB
A@{ shape: rect_left_inv_arrow }
`);
}).toThrow('No such shape: rect_left_inv_arrow. Shape names should be lowercase.');
});
it('Diamond shapes should work as usual', function () {
const res = flow.parser.parse(`flowchart TB
A{This is a label}
Expand Down
27 changes: 26 additions & 1 deletion packages/mermaid/src/diagrams/flowchart/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
import type { ShapeID } from '../../rendering-util/rendering-elements/shapes.js';

/**
* Valid `type` args to `yy.addVertex` taken from
* `packages/mermaid/src/diagrams/flowchart/parser/flow.jison`
*/
export type FlowVertexTypeParam =
| undefined
| 'square'
| 'doublecircle'
| 'circle'
| 'ellipse'
| 'stadium'
| 'subroutine'
| 'rect'
| 'cylinder'
| 'round'
| 'diamond'
| 'hexagon'
| 'odd'
| 'trapezoid'
| 'inv_trapezoid'
| 'lean_right'
| 'lean_left';

export interface FlowVertex {
classes: string[];
dir?: string;
Expand All @@ -10,7 +35,7 @@ export interface FlowVertex {
props?: any;
styles: string[];
text?: string;
type?: string;
type?: ShapeID | FlowVertexTypeParam;
icon?: string;
form?: string;
pos?: 't' | 'b';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function insertNode(elem: SVGGroup, node: Node, renderOptions: Shap
}
}

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

if (!shapeHandler) {
throw new Error(`No such shape: ${node.shape}. Please check your syntax.`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -500,4 +500,8 @@ const generateShapeMap = () => {

export const shapes = generateShapeMap();

export function isValidShape(shape: string): shape is ShapeID {
return shape in shapes;
}

export type ShapeID = keyof typeof shapes;
3 changes: 2 additions & 1 deletion packages/mermaid/src/rendering-util/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type MarkdownWordType = 'normal' | 'strong' | 'em';
import type { MermaidConfig } from '../config.type.js';
import type { ShapeID } from './rendering-elements/shapes.js';
export interface MarkdownWord {
content: string;
type: MarkdownWordType;
Expand Down Expand Up @@ -37,7 +38,7 @@ export interface Node {
linkTarget?: string;
tooltip?: string;
padding?: number; //REMOVE?, use from LayoutData.config - Keep, this could be shape specific
shape?: string;
shape?: ShapeID;
isGroup: boolean;
width?: number;
height?: number;
Expand Down
Loading