mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-16 13:59:54 +02:00
fix: error mermaid.parse
on invalid shapes
Currently, invalid shapes cause an error when rendering, but not when parsing. This confuses the Mermaid Live Editor, e.g. by not showing the error message. Instead, I've added an `isValidShape()` that validates if the shape is valid at parse time. This only checks shapes using the new extended shapes syntax. Currenlty, using `A(-this is an ellipse node-)` is broken (see #5976) and also causes an invalid shape error at render time, but I've ignored it in this PR, so it will continue pass at parse-time (we have unit tests checking ellipse parsing). See: https://github.com/mermaid-js/mermaid/issues/5976
This commit is contained in:
5
.changeset/thick-elephants-search.md
Normal file
5
.changeset/thick-elephants-search.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'mermaid': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: error `mermaid.parse` on an invalid shape, so that it matches the errors thrown by `mermaid.render`
|
@@ -2,6 +2,7 @@ import { select } from 'd3';
|
|||||||
import utils, { getEdgeId } from '../../utils.js';
|
import utils, { getEdgeId } from '../../utils.js';
|
||||||
import { getConfig, defaultConfig } from '../../diagram-api/diagramAPI.js';
|
import { getConfig, defaultConfig } from '../../diagram-api/diagramAPI.js';
|
||||||
import common from '../common/common.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 type { Node, Edge } from '../../rendering-util/types.js';
|
||||||
import { log } from '../../logger.js';
|
import { log } from '../../logger.js';
|
||||||
import * as yaml from 'js-yaml';
|
import * as yaml from 'js-yaml';
|
||||||
@@ -140,14 +141,15 @@ export const addVertex = function (
|
|||||||
}
|
}
|
||||||
// console.log('yamlData', yamlData);
|
// console.log('yamlData', yamlData);
|
||||||
const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData;
|
const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData;
|
||||||
if (doc.shape && (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_'))) {
|
if (doc.shape) {
|
||||||
throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`);
|
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)) {
|
||||||
// console.log('yamlData doc', doc);
|
throw new Error(`No such shape: ${doc.shape}.`);
|
||||||
if (doc?.shape) {
|
}
|
||||||
vertex.type = doc?.shape;
|
vertex.type = doc?.shape;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc?.label) {
|
if (doc?.label) {
|
||||||
vertex.text = doc?.label;
|
vertex.text = doc?.label;
|
||||||
}
|
}
|
||||||
@@ -823,7 +825,7 @@ export const lex = {
|
|||||||
firstGraph,
|
firstGraph,
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTypeFromVertex = (vertex: FlowVertex) => {
|
const getTypeFromVertex = (vertex: FlowVertex): ShapeID => {
|
||||||
if (vertex.img) {
|
if (vertex.img) {
|
||||||
return 'imageSquare';
|
return 'imageSquare';
|
||||||
}
|
}
|
||||||
@@ -845,6 +847,9 @@ const getTypeFromVertex = (vertex: FlowVertex) => {
|
|||||||
return 'squareRect';
|
return 'squareRect';
|
||||||
case 'round':
|
case 'round':
|
||||||
return 'roundedRect';
|
return 'roundedRect';
|
||||||
|
case 'ellipse':
|
||||||
|
// @ts-expect-error -- Ellipses are broken, see https://github.com/mermaid-js/mermaid/issues/5976
|
||||||
|
return 'ellipse';
|
||||||
default:
|
default:
|
||||||
return vertex.type;
|
return vertex.type;
|
||||||
}
|
}
|
||||||
|
@@ -197,6 +197,21 @@ describe('when parsing directions', function () {
|
|||||||
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
|
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
|
||||||
expect(data4Layout.nodes[0].label).toEqual('This is }');
|
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 () {
|
it('Diamond shapes should work as usual', function () {
|
||||||
const res = flow.parser.parse(`flowchart TB
|
const res = flow.parser.parse(`flowchart TB
|
||||||
A{This is a label}
|
A{This is a label}
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ShapeID } from '../../rendering-util/rendering-elements/shapes.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valid `type` args to `yy.addVertex` taken from
|
* Valid `type` args to `yy.addVertex` taken from
|
||||||
* `packages/mermaid/src/diagrams/flowchart/parser/flow.jison`
|
* `packages/mermaid/src/diagrams/flowchart/parser/flow.jison`
|
||||||
@@ -33,7 +35,7 @@ export interface FlowVertex {
|
|||||||
props?: any;
|
props?: any;
|
||||||
styles: string[];
|
styles: string[];
|
||||||
text?: string;
|
text?: string;
|
||||||
type?: string;
|
type?: ShapeID | FlowVertexTypeParam;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
form?: string;
|
form?: string;
|
||||||
pos?: 't' | 'b';
|
pos?: 't' | 'b';
|
||||||
|
@@ -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) {
|
if (!shapeHandler) {
|
||||||
throw new Error(`No such shape: ${node.shape}. Please check your syntax.`);
|
throw new Error(`No such shape: ${node.shape}. Please check your syntax.`);
|
||||||
|
@@ -490,4 +490,8 @@ const generateShapeMap = () => {
|
|||||||
|
|
||||||
export const shapes = generateShapeMap();
|
export const shapes = generateShapeMap();
|
||||||
|
|
||||||
|
export function isValidShape(shape: string): shape is ShapeID {
|
||||||
|
return shape in shapes;
|
||||||
|
}
|
||||||
|
|
||||||
export type ShapeID = keyof typeof shapes;
|
export type ShapeID = keyof typeof shapes;
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
export type MarkdownWordType = 'normal' | 'strong' | 'em';
|
export type MarkdownWordType = 'normal' | 'strong' | 'em';
|
||||||
import type { MermaidConfig } from '../config.type.js';
|
import type { MermaidConfig } from '../config.type.js';
|
||||||
|
import type { ShapeID } from './rendering-elements/shapes.js';
|
||||||
export interface MarkdownWord {
|
export interface MarkdownWord {
|
||||||
content: string;
|
content: string;
|
||||||
type: MarkdownWordType;
|
type: MarkdownWordType;
|
||||||
@@ -37,7 +38,7 @@ export interface Node {
|
|||||||
linkTarget?: string;
|
linkTarget?: string;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
padding?: number; //REMOVE?, use from LayoutData.config - Keep, this could be shape specific
|
padding?: number; //REMOVE?, use from LayoutData.config - Keep, this could be shape specific
|
||||||
shape?: string;
|
shape?: ShapeID;
|
||||||
isGroup: boolean;
|
isGroup: boolean;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
Reference in New Issue
Block a user