Merge pull request #4359 from rhysd/bug/4358_suppress_error_rendering

Add `suppressErrorRendering` option to avoid inserting 'Syntax error' message to DOM directly
This commit is contained in:
Sidharth Vinod
2024-03-23 15:30:43 +05:30
committed by GitHub
6 changed files with 156 additions and 21 deletions

View File

@@ -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'
);
});
});
}); });

View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Mermaid Quick Test Page</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
</head>
<body>
<div id="test">
<pre class="mermaid">
flowchart
a[This should be visible]
</pre
>
<pre class="mermaid">
flowchart
a --< b
</pre
>
<pre class="mermaid">
flowchart
a[This should be visible]
</pre
>
<pre class="mermaid">
---
config:
suppressErrorRendering: true # This should not affect anything, as suppressErrorRendering is a secure config
---
flowchart
a --< b
</pre
>
<pre class="mermaid">
---
config:
suppressErrorRendering: false # This should not affect anything, as suppressErrorRendering is a secure config
---
flowchart
a --< b
</pre
>
</div>
<script type="module">
import mermaid from './mermaid.esm.mjs';
const shouldSuppress =
new URLSearchParams(window.location.search).get('suppressErrorRendering') === 'true';
mermaid.initialize({ startOnLoad: false, suppressErrorRendering: shouldSuppress });
try {
await mermaid.run();
} catch {
if (window.Cypress) {
window.rendered = true;
}
}
</script>
</body>
</html>

View File

@@ -43,6 +43,7 @@ const config = {
securityLevel: 'strict', securityLevel: 'strict',
startOnLoad: true, startOnLoad: true,
arrowMarkerAbsolute: false, arrowMarkerAbsolute: false,
suppressErrorRendering: false,
er: { er: {
diagramPadding: 20, diagramPadding: 20,
@@ -97,7 +98,7 @@ mermaid.initialize(config);
#### Defined in #### 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 ## Functions

View File

@@ -159,6 +159,12 @@ export interface MermaidConfig {
dompurifyConfig?: DOMPurifyConfiguration; dompurifyConfig?: DOMPurifyConfiguration;
wrap?: boolean; wrap?: boolean;
fontSize?: number; 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. * The object containing configurations specific for packet diagrams.

View File

@@ -110,7 +110,7 @@ function processAndSetConfigs(text: string) {
*/ */
async function parse( async function parse(
text: string, text: string,
parseOptions: ParseOptions & { suppressErrors: true } parseOptions: ParseOptions & { suppressErrors: true },
): Promise<ParseResult | false>; ): Promise<ParseResult | false>;
async function parse(text: string, parseOptions?: ParseOptions): Promise<ParseResult>; async function parse(text: string, parseOptions?: ParseOptions): Promise<ParseResult>;
async function parse(text: string, parseOptions?: ParseOptions): Promise<ParseResult | false> { async function parse(text: string, parseOptions?: ParseOptions): Promise<ParseResult | false> {
@@ -138,7 +138,7 @@ async function parse(text: string, parseOptions?: ParseOptions): Promise<ParseRe
export const cssImportantStyles = ( export const cssImportantStyles = (
cssClass: string, cssClass: string,
element: string, element: string,
cssClasses: string[] = [] cssClasses: string[] = [],
): string => { ): string => {
return `\n.${cssClass} ${element} { ${cssClasses.join(' !important; ')} !important; }`; return `\n.${cssClass} ${element} { ${cssClasses.join(' !important; ')} !important; }`;
}; };
@@ -152,7 +152,7 @@ export const cssImportantStyles = (
*/ */
export const createCssStyles = ( export const createCssStyles = (
config: MermaidConfig, config: MermaidConfig,
classDefs: Record<string, DiagramStyleClassDef> | null | undefined = {} classDefs: Record<string, DiagramStyleClassDef> | null | undefined = {},
): string => { ): string => {
let cssStyles = ''; let cssStyles = '';
@@ -201,7 +201,7 @@ export const createUserStyles = (
config: MermaidConfig, config: MermaidConfig,
graphType: string, graphType: string,
classDefs: Record<string, DiagramStyleClassDef> | undefined, classDefs: Record<string, DiagramStyleClassDef> | undefined,
svgId: string svgId: string,
): string => { ): string => {
const userCSSstyles = createCssStyles(config, classDefs); const userCSSstyles = createCssStyles(config, classDefs);
const allStyles = getStyles(graphType, userCSSstyles, config.themeVariables); const allStyles = getStyles(graphType, userCSSstyles, config.themeVariables);
@@ -223,7 +223,7 @@ export const createUserStyles = (
export const cleanUpSvgCode = ( export const cleanUpSvgCode = (
svgCode = '', svgCode = '',
inSandboxMode: boolean, inSandboxMode: boolean,
useArrowMarkerUrls: boolean useArrowMarkerUrls: boolean,
): string => { ): string => {
let cleanedUpSvg = svgCode; let cleanedUpSvg = svgCode;
@@ -231,7 +231,7 @@ export const cleanUpSvgCode = (
if (!useArrowMarkerUrls && !inSandboxMode) { if (!useArrowMarkerUrls && !inSandboxMode) {
cleanedUpSvg = cleanedUpSvg.replace( cleanedUpSvg = cleanedUpSvg.replace(
/marker-end="url\([\d+./:=?A-Za-z-]*?#/g, /marker-end="url\([\d+./:=?A-Za-z-]*?#/g,
'marker-end="url(#' 'marker-end="url(#',
); );
} }
@@ -279,7 +279,7 @@ export const appendDivSvgG = (
id: string, id: string,
enclosingDivId: string, enclosingDivId: string,
divStyle?: string, divStyle?: string,
svgXlink?: string svgXlink?: string,
): D3Element => { ): D3Element => {
const enclosingDiv = parentRoot.append('div'); const enclosingDiv = parentRoot.append('div');
enclosingDiv.attr('id', enclosingDivId); enclosingDiv.attr('id', enclosingDivId);
@@ -328,7 +328,7 @@ export const removeExistingElements = (
doc: Document, doc: Document,
id: string, id: string,
divId: string, divId: string,
iFrameId: string iFrameId: string,
) => { ) => {
// Remove existing SVG element if it exists // Remove existing SVG element if it exists
doc.getElementById(id)?.remove(); doc.getElementById(id)?.remove();
@@ -347,7 +347,7 @@ export const removeExistingElements = (
const render = async function ( const render = async function (
id: string, id: string,
text: string, text: string,
svgContainingElement?: Element svgContainingElement?: Element,
): Promise<RenderResult> { ): Promise<RenderResult> {
addDiagrams(); addDiagrams();
@@ -368,6 +368,16 @@ const render = async function (
const enclosingDivID = 'd' + id; const enclosingDivID = 'd' + id;
const enclosingDivID_selector = '#' + enclosingDivID; 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'); let root: any = select('body');
const isSandboxed = config.securityLevel === SECURITY_LVL_SANDBOX; const isSandboxed = config.securityLevel === SECURITY_LVL_SANDBOX;
@@ -424,6 +434,10 @@ const render = async function (
try { try {
diag = await Diagram.fromText(text, { title: processed.title }); diag = await Diagram.fromText(text, { title: processed.title });
} catch (error) { } catch (error) {
if (config.suppressErrorRendering) {
removeTempElements();
throw error;
}
diag = await Diagram.fromText('error'); diag = await Diagram.fromText('error');
parseEncounteredException = error; parseEncounteredException = error;
} }
@@ -451,7 +465,11 @@ const render = async function (
try { try {
await diag.renderer.draw(text, id, version, diag); await diag.renderer.draw(text, id, version, diag);
} catch (e) { } catch (e) {
if (config.suppressErrorRendering) {
removeTempElements();
} else {
errorRenderer.draw(text, id, version); errorRenderer.draw(text, id, version);
}
throw e; throw e;
} }
@@ -487,13 +505,7 @@ const render = async function (
throw parseEncounteredException; throw parseEncounteredException;
} }
// ------------------------------------------------------------------------------- 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();
}
return { return {
diagramType, diagramType,
@@ -520,7 +532,7 @@ function initialize(options: MermaidConfig = {}) {
if (options?.theme && options.theme in theme) { if (options?.theme && options.theme in theme) {
// Todo merge with user options // Todo merge with user options
options.themeVariables = theme[options.theme as keyof typeof theme].getThemeVariables( options.themeVariables = theme[options.theme as keyof typeof theme].getThemeVariables(
options.themeVariables options.themeVariables,
); );
} else if (options) { } else if (options) {
options.themeVariables = theme.default.getThemeVariables(options.themeVariables); options.themeVariables = theme.default.getThemeVariables(options.themeVariables);
@@ -550,7 +562,7 @@ function addA11yInfo(
diagramType: string, diagramType: string,
svgNode: D3Element, svgNode: D3Element,
a11yTitle?: string, a11yTitle?: string,
a11yDescr?: string a11yDescr?: string,
): void { ): void {
setA11yDiagramInfo(svgNode, diagramType); setA11yDiagramInfo(svgNode, diagramType);
addSVGa11yTitleDescription(svgNode, a11yTitle, a11yDescr, svgNode.attr('id')); addSVGa11yTitleDescription(svgNode, a11yTitle, a11yDescr, svgNode.attr('id'));
@@ -566,6 +578,7 @@ function addA11yInfo(
* securityLevel: 'strict', * securityLevel: 'strict',
* startOnLoad: true, * startOnLoad: true,
* arrowMarkerAbsolute: false, * arrowMarkerAbsolute: false,
* suppressErrorRendering: false,
* *
* er: { * er: {
* diagramPadding: 20, * diagramPadding: 20,

View File

@@ -159,7 +159,15 @@ properties:
in the current `currentConfig`. in the current `currentConfig`.
This prevents malicious graph directives from overriding a site's default security. 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 type: array
items: items:
type: string type: string
@@ -235,6 +243,12 @@ properties:
fontSize: fontSize:
type: number type: number
default: 16 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) $defs: # JSON Schema definition (maybe we should move these to a separate file)
BaseDiagramConfig: BaseDiagramConfig: