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
commit 78587e11c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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',
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

View File

@ -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.

View File

@ -110,7 +110,7 @@ function processAndSetConfigs(text: string) {
*/
async function parse(
text: string,
parseOptions: ParseOptions & { suppressErrors: true }
parseOptions: ParseOptions & { suppressErrors: true },
): Promise<ParseResult | false>;
async function parse(text: string, parseOptions?: ParseOptions): Promise<ParseResult>;
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 = (
cssClass: string,
element: string,
cssClasses: string[] = []
cssClasses: string[] = [],
): string => {
return `\n.${cssClass} ${element} { ${cssClasses.join(' !important; ')} !important; }`;
};
@ -152,7 +152,7 @@ export const cssImportantStyles = (
*/
export const createCssStyles = (
config: MermaidConfig,
classDefs: Record<string, DiagramStyleClassDef> | null | undefined = {}
classDefs: Record<string, DiagramStyleClassDef> | null | undefined = {},
): string => {
let cssStyles = '';
@ -201,7 +201,7 @@ export const createUserStyles = (
config: MermaidConfig,
graphType: string,
classDefs: Record<string, DiagramStyleClassDef> | 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<RenderResult> {
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,

View File

@ -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: