diff --git a/cypress/integration/other/configuration.spec.js b/cypress/integration/other/configuration.spec.js index 23338271f5..9fd0815676 100644 --- a/cypress/integration/other/configuration.spec.js +++ b/cypress/integration/other/configuration.spec.js @@ -125,4 +125,46 @@ describe('Configuration', () => { ); }); }); + + describe('suppressErrorRendering', () => { + beforeEach(() => { + cy.on('uncaught:exception', (err, runnable) => { + return !err.message.includes('Parse error on line'); + }); + }); + + it('should not render error diagram if suppressErrorRendering is set', () => { + const url = 'http://localhost:9000/suppressError.html?suppressErrorRendering=true'; + cy.visit(url); + cy.window().should('have.property', 'rendered', true); + cy.get('#test') + .find('svg') + .should(($svg) => { + // all failing diagrams should not appear! + expect($svg).to.have.length(2); + // none of the diagrams should be error diagrams + expect($svg).to.not.contain('Syntax error'); + }); + cy.matchImageSnapshot( + 'configuration.spec-should-not-render-error-diagram-if-suppressErrorRendering-is-set' + ); + }); + + it('should render error diagram if suppressErrorRendering is not set', () => { + const url = 'http://localhost:9000/suppressError.html'; + cy.visit(url); + cy.window().should('have.property', 'rendered', true); + cy.get('#test') + .find('svg') + .should(($svg) => { + // all five diagrams should be rendered + expect($svg).to.have.length(5); + // some of the diagrams should be error diagrams + expect($svg).to.contain('Syntax error'); + }); + cy.matchImageSnapshot( + 'configuration.spec-should-render-error-diagram-if-suppressErrorRendering-is-not-set' + ); + }); + }); }); diff --git a/cypress/platform/suppressError.html b/cypress/platform/suppressError.html new file mode 100644 index 0000000000..347a82c79c --- /dev/null +++ b/cypress/platform/suppressError.html @@ -0,0 +1,59 @@ + + + + + + Mermaid Quick Test Page + + + +
+
+  flowchart
+      a[This should be visible]
+    
+
+  flowchart
+    a --< b
+    
+
+  flowchart
+      a[This should be visible]
+    
+
+  ---
+  config:
+    suppressErrorRendering: true # This should not affect anything, as suppressErrorRendering is a secure config
+  ---
+  flowchart
+    a --< b
+    
+
+  ---
+  config:
+    suppressErrorRendering: false # This should not affect anything, as suppressErrorRendering is a secure config
+  ---
+  flowchart
+    a --< b
+    
+
+ + + diff --git a/docs/config/setup/modules/mermaidAPI.md b/docs/config/setup/modules/mermaidAPI.md index b078a688fb..1a68b05bd0 100644 --- a/docs/config/setup/modules/mermaidAPI.md +++ b/docs/config/setup/modules/mermaidAPI.md @@ -43,6 +43,7 @@ const config = { securityLevel: 'strict', startOnLoad: true, arrowMarkerAbsolute: false, + suppressErrorRendering: false, er: { diagramPadding: 20, @@ -97,7 +98,7 @@ mermaid.initialize(config); #### Defined in -[mermaidAPI.ts:622](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L622) +[mermaidAPI.ts:635](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L635) ## Functions diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 97ed4eb8dc..2ff19c2d6e 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -159,6 +159,12 @@ export interface MermaidConfig { dompurifyConfig?: DOMPurifyConfiguration; wrap?: boolean; fontSize?: number; + /** + * Suppresses inserting 'Syntax error' diagram in the DOM. + * This is useful when you want to control how to handle syntax errors in your application. + * + */ + suppressErrorRendering?: boolean; } /** * The object containing configurations specific for packet diagrams. diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index a3eee5d047..f394ef7f1d 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -110,7 +110,7 @@ function processAndSetConfigs(text: string) { */ async function parse( text: string, - parseOptions: ParseOptions & { suppressErrors: true } + parseOptions: ParseOptions & { suppressErrors: true }, ): Promise; async function parse(text: string, parseOptions?: ParseOptions): Promise; async function parse(text: string, parseOptions?: ParseOptions): Promise { @@ -138,7 +138,7 @@ async function parse(text: string, parseOptions?: ParseOptions): Promise { return `\n.${cssClass} ${element} { ${cssClasses.join(' !important; ')} !important; }`; }; @@ -152,7 +152,7 @@ export const cssImportantStyles = ( */ export const createCssStyles = ( config: MermaidConfig, - classDefs: Record | null | undefined = {} + classDefs: Record | null | undefined = {}, ): string => { let cssStyles = ''; @@ -201,7 +201,7 @@ export const createUserStyles = ( config: MermaidConfig, graphType: string, classDefs: Record | undefined, - svgId: string + svgId: string, ): string => { const userCSSstyles = createCssStyles(config, classDefs); const allStyles = getStyles(graphType, userCSSstyles, config.themeVariables); @@ -223,7 +223,7 @@ export const createUserStyles = ( export const cleanUpSvgCode = ( svgCode = '', inSandboxMode: boolean, - useArrowMarkerUrls: boolean + useArrowMarkerUrls: boolean, ): string => { let cleanedUpSvg = svgCode; @@ -231,7 +231,7 @@ export const cleanUpSvgCode = ( if (!useArrowMarkerUrls && !inSandboxMode) { cleanedUpSvg = cleanedUpSvg.replace( /marker-end="url\([\d+./:=?A-Za-z-]*?#/g, - 'marker-end="url(#' + 'marker-end="url(#', ); } @@ -279,7 +279,7 @@ export const appendDivSvgG = ( id: string, enclosingDivId: string, divStyle?: string, - svgXlink?: string + svgXlink?: string, ): D3Element => { const enclosingDiv = parentRoot.append('div'); enclosingDiv.attr('id', enclosingDivId); @@ -328,7 +328,7 @@ export const removeExistingElements = ( doc: Document, id: string, divId: string, - iFrameId: string + iFrameId: string, ) => { // Remove existing SVG element if it exists doc.getElementById(id)?.remove(); @@ -347,7 +347,7 @@ export const removeExistingElements = ( const render = async function ( id: string, text: string, - svgContainingElement?: Element + svgContainingElement?: Element, ): Promise { addDiagrams(); @@ -368,6 +368,16 @@ const render = async function ( const enclosingDivID = 'd' + id; const enclosingDivID_selector = '#' + enclosingDivID; + const removeTempElements = () => { + // ------------------------------------------------------------------------------- + // Remove the temporary HTML element if appropriate + const tmpElementSelector = isSandboxed ? iFrameID_selector : enclosingDivID_selector; + const node = select(tmpElementSelector).node(); + if (node && 'remove' in node) { + node.remove(); + } + }; + let root: any = select('body'); const isSandboxed = config.securityLevel === SECURITY_LVL_SANDBOX; @@ -424,6 +434,10 @@ const render = async function ( try { diag = await Diagram.fromText(text, { title: processed.title }); } catch (error) { + if (config.suppressErrorRendering) { + removeTempElements(); + throw error; + } diag = await Diagram.fromText('error'); parseEncounteredException = error; } @@ -451,7 +465,11 @@ const render = async function ( try { await diag.renderer.draw(text, id, version, diag); } catch (e) { - errorRenderer.draw(text, id, version); + if (config.suppressErrorRendering) { + removeTempElements(); + } else { + errorRenderer.draw(text, id, version); + } throw e; } @@ -487,13 +505,7 @@ const render = async function ( throw parseEncounteredException; } - // ------------------------------------------------------------------------------- - // Remove the temporary HTML element if appropriate - const tmpElementSelector = isSandboxed ? iFrameID_selector : enclosingDivID_selector; - const node = select(tmpElementSelector).node(); - if (node && 'remove' in node) { - node.remove(); - } + removeTempElements(); return { diagramType, @@ -520,7 +532,7 @@ function initialize(options: MermaidConfig = {}) { if (options?.theme && options.theme in theme) { // Todo merge with user options options.themeVariables = theme[options.theme as keyof typeof theme].getThemeVariables( - options.themeVariables + options.themeVariables, ); } else if (options) { options.themeVariables = theme.default.getThemeVariables(options.themeVariables); @@ -550,7 +562,7 @@ function addA11yInfo( diagramType: string, svgNode: D3Element, a11yTitle?: string, - a11yDescr?: string + a11yDescr?: string, ): void { setA11yDiagramInfo(svgNode, diagramType); addSVGa11yTitleDescription(svgNode, a11yTitle, a11yDescr, svgNode.attr('id')); @@ -566,6 +578,7 @@ function addA11yInfo( * securityLevel: 'strict', * startOnLoad: true, * arrowMarkerAbsolute: false, + * suppressErrorRendering: false, * * er: { * diagramPadding: 20, diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index d6d084f8c7..d580300744 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -159,7 +159,15 @@ properties: in the current `currentConfig`. This prevents malicious graph directives from overriding a site's default security. - default: ['secure', 'securityLevel', 'startOnLoad', 'maxTextSize', 'maxEdges'] + default: + [ + 'secure', + 'securityLevel', + 'startOnLoad', + 'maxTextSize', + 'suppressErrorRendering', + 'maxEdges', + ] type: array items: type: string @@ -235,6 +243,12 @@ properties: fontSize: type: number default: 16 + suppressErrorRendering: + type: boolean + default: false + description: | + Suppresses inserting 'Syntax error' diagram in the DOM. + This is useful when you want to control how to handle syntax errors in your application. $defs: # JSON Schema definition (maybe we should move these to a separate file) BaseDiagramConfig: