mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-16 05:49:43 +02:00
functions and specs: createCssStyles, appendDivSvgG,cleanUpSvgCode, putIntoIFrame [for render]
This commit is contained in:

parent
d106d3d1b1
commit
a3b8c301e2
@@ -1,10 +1,39 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
import mermaid from './mermaid';
|
import mermaid from './mermaid';
|
||||||
import mermaidAPI from './mermaidAPI';
|
import mermaidAPI from './mermaidAPI';
|
||||||
import { encodeEntities, decodeEntities } from './mermaidAPI';
|
import {
|
||||||
|
encodeEntities,
|
||||||
|
decodeEntities,
|
||||||
|
createCssStyles,
|
||||||
|
appendDivSvgG,
|
||||||
|
cleanUpSvgCode,
|
||||||
|
putIntoIFrame,
|
||||||
|
} from './mermaidAPI';
|
||||||
|
|
||||||
import assignWithDepth from './assignWithDepth';
|
import assignWithDepth from './assignWithDepth';
|
||||||
|
|
||||||
|
// To mock a module, first define a mock for it, then import it. Be sure the path points to exactly the same file as is imported in mermaidAPI (the module being tested)
|
||||||
|
vi.mock('./styles', () => {
|
||||||
|
return {
|
||||||
|
addStylesForDiagram: vi.fn(),
|
||||||
|
default: vi.fn().mockReturnValue(' .userStyle { font-weight:bold; }'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
import getStyles from './styles';
|
||||||
|
|
||||||
|
vi.mock('stylis', () => {
|
||||||
|
return {
|
||||||
|
stringify: vi.fn(),
|
||||||
|
compile: vi.fn(),
|
||||||
|
serialize: vi.fn().mockReturnValue('stylis serialized css'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
import { compile, serialize } from 'stylis';
|
||||||
|
|
||||||
|
import { MockedD3 } from './tests/MockedD3';
|
||||||
|
|
||||||
describe('when using mermaidAPI and ', function () {
|
describe('when using mermaidAPI and ', function () {
|
||||||
describe('encodeEntities', () => {
|
describe('encodeEntities', () => {
|
||||||
it('removes the ending ; from style [text1]:[optional word]#[text2]; with ', () => {
|
it('removes the ending ; from style [text1]:[optional word]#[text2]; with ', () => {
|
||||||
@@ -73,6 +102,309 @@ describe('when using mermaidAPI and ', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('cleanUpSvgCode', () => {
|
||||||
|
it('replaces marker end URLs with just the anchor if not sandboxed and not useMarkerUrls', () => {
|
||||||
|
const markerFullUrl = 'marker-end="url(some-URI#that)"';
|
||||||
|
let useArrowMarkerUrls = false;
|
||||||
|
let isSandboxed = false;
|
||||||
|
let result = cleanUpSvgCode(markerFullUrl, isSandboxed, useArrowMarkerUrls);
|
||||||
|
expect(result).toEqual('marker-end="url(#that)"');
|
||||||
|
|
||||||
|
useArrowMarkerUrls = true;
|
||||||
|
result = cleanUpSvgCode(markerFullUrl, isSandboxed, useArrowMarkerUrls);
|
||||||
|
expect(result).toEqual(markerFullUrl); // not changed
|
||||||
|
|
||||||
|
useArrowMarkerUrls = false;
|
||||||
|
isSandboxed = true;
|
||||||
|
result = cleanUpSvgCode(markerFullUrl, isSandboxed, useArrowMarkerUrls);
|
||||||
|
expect(result).toEqual(markerFullUrl); // not changed
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decodesEntities', () => {
|
||||||
|
const result = cleanUpSvgCode('¶ß brrrr', true, true);
|
||||||
|
expect(result).toEqual('; brrrr');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces old style br tags with new style', () => {
|
||||||
|
const result = cleanUpSvgCode('<br> brrrr<br>', true, true);
|
||||||
|
expect(result).toEqual('<br/> brrrr<br/>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('putIntoIFrame', () => {
|
||||||
|
const inputSvgCode = 'this is the SVG code';
|
||||||
|
|
||||||
|
it('uses the default SVG iFrame height is used if no svgElement given', () => {
|
||||||
|
const result = putIntoIFrame(inputSvgCode);
|
||||||
|
expect(result).toMatch(/style="(.*)height:100%(.*);"/);
|
||||||
|
});
|
||||||
|
it('default style attributes are: width: 100%, height: 100%, border: 0, margin: 0', () => {
|
||||||
|
const result = putIntoIFrame(inputSvgCode);
|
||||||
|
expect(result).toMatch(/style="(.*)width:100%(.*);"/);
|
||||||
|
expect(result).toMatch(/style="(.*)height:100%(.*);"/);
|
||||||
|
expect(result).toMatch(/style="(.*)border:0(.*);"/);
|
||||||
|
expect(result).toMatch(/style="(.*)margin:0(.*);"/);
|
||||||
|
});
|
||||||
|
it('sandbox="allow-top-navigation-by-user-activation allow-popups">', () => {
|
||||||
|
const result = putIntoIFrame(inputSvgCode);
|
||||||
|
expect(result).toMatch(/sandbox="allow-top-navigation-by-user-activation allow-popups">/);
|
||||||
|
});
|
||||||
|
it('msg shown is "The "iframe" tag is not supported by your browser.\\n" if iFrames are not supported in the browser', () => {
|
||||||
|
const result = putIntoIFrame(inputSvgCode);
|
||||||
|
expect(result).toMatch(/\s*The "iframe" tag is not supported by your browser\./);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets src to base64 version of <body style="IFRAME_SVG_BODY_STYLE">svgCode<//body>', () => {
|
||||||
|
const base64encodedSrc = btoa('<body style="' + 'margin:0' + '">' + inputSvgCode + '</body>');
|
||||||
|
const expectedRegExp = new RegExp('src="data:text/html;base64,' + base64encodedSrc + '"');
|
||||||
|
|
||||||
|
const result = putIntoIFrame(inputSvgCode);
|
||||||
|
expect(result).toMatch(expectedRegExp);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the height and appends px from the svgElement given', () => {
|
||||||
|
const faux_svgElement = {
|
||||||
|
viewBox: {
|
||||||
|
baseVal: {
|
||||||
|
height: 42,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = putIntoIFrame(inputSvgCode, faux_svgElement);
|
||||||
|
expect(result).toMatch(/style="(.*)height:42px;/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const fauxParentNode = new MockedD3();
|
||||||
|
const fauxEnclosingDiv = new MockedD3();
|
||||||
|
const fauxSvgNode = new MockedD3();
|
||||||
|
|
||||||
|
describe('appendDivSvgG', () => {
|
||||||
|
const fauxGNode = new MockedD3();
|
||||||
|
const parent_append_spy = vi.spyOn(fauxParentNode, 'append').mockReturnValue(fauxEnclosingDiv);
|
||||||
|
const div_append_spy = vi.spyOn(fauxEnclosingDiv, 'append').mockReturnValue(fauxSvgNode);
|
||||||
|
const div_attr_spy = vi.spyOn(fauxEnclosingDiv, 'attr').mockReturnValue(fauxEnclosingDiv);
|
||||||
|
const svg_append_spy = vi.spyOn(fauxSvgNode, 'append').mockReturnValue(fauxGNode);
|
||||||
|
const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||||
|
|
||||||
|
it('appends a div node', () => {
|
||||||
|
appendDivSvgG(fauxParentNode, 'theId');
|
||||||
|
expect(parent_append_spy).toHaveBeenCalledWith('div');
|
||||||
|
expect(div_append_spy).toHaveBeenCalledWith('svg');
|
||||||
|
});
|
||||||
|
it('the id for the div is "d" with the id appended', () => {
|
||||||
|
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||||
|
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the style for the div if one is given', () => {
|
||||||
|
appendDivSvgG(fauxParentNode, 'theId', 'dtheId', 'given div style', 'given x link');
|
||||||
|
expect(div_attr_spy).toHaveBeenCalledWith('style', 'given div style');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('appends a svg node to the div node', () => {
|
||||||
|
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||||
|
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId');
|
||||||
|
});
|
||||||
|
it('sets the svg width to 100%', () => {
|
||||||
|
appendDivSvgG(fauxParentNode, 'theId');
|
||||||
|
expect(svg_attr_spy).toHaveBeenCalledWith('width', '100%');
|
||||||
|
});
|
||||||
|
it('the svg id is the id', () => {
|
||||||
|
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||||
|
expect(svg_attr_spy).toHaveBeenCalledWith('id', 'theId');
|
||||||
|
});
|
||||||
|
it('the svg xml namespace is the 2000 standard', () => {
|
||||||
|
appendDivSvgG(fauxParentNode, 'theId');
|
||||||
|
expect(svg_attr_spy).toHaveBeenCalledWith('xmlns', 'http://www.w3.org/2000/svg');
|
||||||
|
});
|
||||||
|
it('sets the svg xlink if one is given', () => {
|
||||||
|
appendDivSvgG(fauxParentNode, 'theId', 'dtheId', 'div style', 'given x link');
|
||||||
|
expect(svg_attr_spy).toHaveBeenCalledWith('xmlns:xlink', 'given x link');
|
||||||
|
});
|
||||||
|
it('appends a g (group) node to the svg node', () => {
|
||||||
|
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
|
||||||
|
expect(svg_append_spy).toHaveBeenCalledWith('g');
|
||||||
|
});
|
||||||
|
it('returns the given parentRoot d3 nodes', () => {
|
||||||
|
expect(appendDivSvgG(fauxParentNode, 'theId', 'dtheId')).toEqual(fauxParentNode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createCssStyles', () => {
|
||||||
|
const serif = 'serif';
|
||||||
|
const sansSerif = 'sans-serif';
|
||||||
|
const mocked_config_with_htmlLabels = {
|
||||||
|
themeCSS: 'default',
|
||||||
|
fontFamily: serif,
|
||||||
|
altFontFamily: sansSerif,
|
||||||
|
htmlLabels: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('gets the cssStyles from the theme', () => {
|
||||||
|
const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', null);
|
||||||
|
expect(styles).toMatch(/^\ndefault(.*)/);
|
||||||
|
});
|
||||||
|
it('gets the fontFamily from the config', () => {
|
||||||
|
const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', null);
|
||||||
|
expect(styles).toMatch(/(.*)\n:root \{ --mermaid-font-family: serif(.*)/);
|
||||||
|
});
|
||||||
|
it('gets the alt fontFamily from the config', () => {
|
||||||
|
const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', null);
|
||||||
|
expect(styles).toMatch(/(.*)\n:root \{ --mermaid-alt-font-family: sans-serif(.*)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('there are some classDefs', () => {
|
||||||
|
const classDef1 = { id: 'classDef1', styles: ['style1-1', 'style1-2'], textStyles: [] };
|
||||||
|
const classDef2 = { id: 'classDef2', styles: [], textStyles: ['textStyle2-1'] };
|
||||||
|
const classDef3 = { id: 'classDef3', textStyles: ['textStyle3-1', 'textStyle3-2'] };
|
||||||
|
const classDefs = [classDef1, classDef2, classDef3];
|
||||||
|
|
||||||
|
describe('the graph supports classDefs', () => {
|
||||||
|
const graphType = 'flowchart-v2';
|
||||||
|
|
||||||
|
const REGEXP_SPECIALS = ['^', '$', '?', '(', '{', '[', '.', '*', '!'];
|
||||||
|
|
||||||
|
// prefix any special RegExp characters in the given string with a \ so we can use the literal character in a RegExp
|
||||||
|
function escapeForRegexp(str) {
|
||||||
|
const strChars = str.split(''); // split into array of every char
|
||||||
|
const strEscaped = strChars.map((char) => {
|
||||||
|
if (REGEXP_SPECIALS.includes(char)) return `\\${char}`;
|
||||||
|
else return char;
|
||||||
|
});
|
||||||
|
return strEscaped.join('');
|
||||||
|
}
|
||||||
|
function expect_styles_matchesHtmlElements(styles, htmlElement) {
|
||||||
|
expect(styles).toMatch(
|
||||||
|
new RegExp(
|
||||||
|
`\\.classDef1 ${escapeForRegexp(
|
||||||
|
htmlElement
|
||||||
|
)} \\{ style1-1 !important; style1-2 !important; }`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
// no CSS styles are created if there are no styles for a classDef
|
||||||
|
expect(styles).not.toMatch(
|
||||||
|
new RegExp(`\\.classDef2 ${escapeForRegexp(htmlElement)} \\{ style(.*) !important; }`)
|
||||||
|
);
|
||||||
|
expect(styles).not.toMatch(
|
||||||
|
new RegExp(`\\.classDef3 ${escapeForRegexp(htmlElement)} \\{ style(.*) !important; }`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expect_textStyles_matchesHtmlElements(styles, htmlElement) {
|
||||||
|
expect(styles).toMatch(
|
||||||
|
new RegExp(
|
||||||
|
`\\.classDef2 ${escapeForRegexp(htmlElement)} \\{ textStyle2-1 !important; }`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
expect(styles).toMatch(
|
||||||
|
new RegExp(
|
||||||
|
`\\.classDef3 ${escapeForRegexp(
|
||||||
|
htmlElement
|
||||||
|
)} \\{ textStyle3-1 !important; textStyle3-2 !important; }`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// no CSS styles are created if there are no textStyles for a classDef
|
||||||
|
expect(styles).not.toMatch(
|
||||||
|
new RegExp(
|
||||||
|
`\\.classDef1 ${escapeForRegexp(htmlElement)} \\{ textStyle(.*) !important; }`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expect_correct_styles_with_htmlElements(mocked_config) {
|
||||||
|
describe('creates styles for "> *" and "span" elements', () => {
|
||||||
|
const htmlElements = ['> *', 'span'];
|
||||||
|
|
||||||
|
it('creates CSS styles for every style and textStyle in every classDef', () => {
|
||||||
|
// @todo TODO Can't figure out how to spy on the cssImportantStyles method. That would be a much better approach than manually checking the result
|
||||||
|
|
||||||
|
const styles = createCssStyles(mocked_config, graphType, classDefs);
|
||||||
|
htmlElements.forEach((htmlElement) => {
|
||||||
|
expect_styles_matchesHtmlElements(styles, htmlElement);
|
||||||
|
});
|
||||||
|
expect_textStyles_matchesHtmlElements(styles, 'tspan');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('there are htmlLabels in the configuration', () => {
|
||||||
|
expect_correct_styles_with_htmlElements(mocked_config_with_htmlLabels);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('there are flowchart.htmlLabels in the configuration', () => {
|
||||||
|
const mocked_config_flowchart_htmlLabels = {
|
||||||
|
themeCSS: 'default',
|
||||||
|
fontFamily: 'serif',
|
||||||
|
altFontFamily: 'sans-serif',
|
||||||
|
flowchart: {
|
||||||
|
htmlLabels: 'flowchart-htmlLables',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect_correct_styles_with_htmlElements(mocked_config_flowchart_htmlLabels);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('no htmlLabels in the configuration', () => {
|
||||||
|
const mocked_config_no_htmlLabels = {
|
||||||
|
themeCSS: 'default',
|
||||||
|
fontFamily: 'serif',
|
||||||
|
altFontFamily: 'sans-serif',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('creates styles for shape elements "rect", "polygon", "ellipse", and "circle"', () => {
|
||||||
|
const htmlElements = ['rect', 'polygon', 'ellipse', 'circle'];
|
||||||
|
|
||||||
|
it('creates CSS styles for every style and textStyle in every classDef', () => {
|
||||||
|
// @todo TODO Can't figure out how to spy on the cssImportantStyles method. That would be a much better approach than manually checking the result
|
||||||
|
|
||||||
|
const styles = createCssStyles(mocked_config_no_htmlLabels, graphType, classDefs);
|
||||||
|
htmlElements.forEach((htmlElement) => {
|
||||||
|
expect_styles_matchesHtmlElements(styles, htmlElement);
|
||||||
|
});
|
||||||
|
expect_textStyles_matchesHtmlElements(styles, 'tspan');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// describe('createUserStyles', () => {
|
||||||
|
// const mockConfig = {
|
||||||
|
// themeCSS: 'default',
|
||||||
|
// htmlLabels: 'htmlLabels',
|
||||||
|
// themeVariables: { fontFamily: 'serif' },
|
||||||
|
// };
|
||||||
|
// const classDef1 = { id: 'classDef1', styles: ['style1-1'], textStyles: [] };
|
||||||
|
//
|
||||||
|
// it('gets the css styles created', () => {
|
||||||
|
// // @todo TODO if a single function in the module can be mocked, do it for createCssStyles and mock the results.
|
||||||
|
//
|
||||||
|
// createUserStyles(mockConfig, 'flowchart-v2', [classDef1], 'someId');
|
||||||
|
// const expectedStyles =
|
||||||
|
// '\ndefault' +
|
||||||
|
// '\n.classDef1 > * { style1-1 !important; }' +
|
||||||
|
// '\n.classDef1 span { style1-1 !important; }';
|
||||||
|
// expect(getStyles).toHaveBeenCalledWith('flowchart-v2', expectedStyles, {
|
||||||
|
// fontFamily: 'serif',
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('calls getStyles to get css for all graph, user css styles, and config theme variables', () => {
|
||||||
|
// createUserStyles(mockConfig, 'someDiagram', null, 'someId');
|
||||||
|
// expect(getStyles).toHaveBeenCalled();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('returns the result of compiling, stringifying, and serializing the css code with stylis', () => {
|
||||||
|
// const result = createUserStyles(mockConfig, 'someDiagram', null, 'someId');
|
||||||
|
// expect(compile).toHaveBeenCalled();
|
||||||
|
// expect(serialize).toHaveBeenCalled();
|
||||||
|
// expect(result).toEqual('stylis serialized css');
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
describe('doing initialize ', function () {
|
describe('doing initialize ', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
document.body.innerHTML = '';
|
document.body.innerHTML = '';
|
||||||
|
@@ -45,18 +45,27 @@ const XMLNS_XLINK_STD = 'http://www.w3.org/1999/xlink';
|
|||||||
|
|
||||||
// ------------------------------
|
// ------------------------------
|
||||||
// iFrame
|
// iFrame
|
||||||
const SANDBOX_IFRAME_STYLE = 'width: 100%; height: 100%;';
|
|
||||||
const IFRAME_WIDTH = '100%';
|
const IFRAME_WIDTH = '100%';
|
||||||
const IFRAME_HEIGHT = '100%';
|
const IFRAME_HEIGHT = '100%';
|
||||||
const IFRAME_STYLES = 'border:0;margin:0;';
|
const IFRAME_STYLES = 'border:0;margin:0;';
|
||||||
const IFRAME_BODY_STYLE = 'margin:0';
|
const IFRAME_BODY_STYLE = 'margin:0';
|
||||||
const IFRAME_SANDBOX_OPTS = 'allow-top-navigation-by-user-activation allow-popups';
|
const IFRAME_SANDBOX_OPTS = 'allow-top-navigation-by-user-activation allow-popups';
|
||||||
const IFRAME_NOT_SUPPORTED_MSG = 'The “iframe” tag is not supported by your browser.';
|
const IFRAME_NOT_SUPPORTED_MSG = 'The "iframe" tag is not supported by your browser.';
|
||||||
|
|
||||||
// DOMPurify settings for svgCode
|
// DOMPurify settings for svgCode
|
||||||
const DOMPURE_TAGS = ['foreignobject'];
|
const DOMPURE_TAGS = ['foreignobject'];
|
||||||
const DOMPURE_ATTR = ['dominant-baseline'];
|
const DOMPURE_ATTR = ['dominant-baseline'];
|
||||||
|
|
||||||
|
// This is what is returned from getClasses(...) methods.
|
||||||
|
// It is slightly renamed to ..StyleClassDef instead of just ClassDef because "class" is a greatly ambiguous and overloaded word.
|
||||||
|
// It makes it clear we're working with a style class definition, even though defining the type is currently difficult.
|
||||||
|
// @ts-ignore This is an alias for a js construct used in diagrams.
|
||||||
|
type DiagramStyleClassDef = any;
|
||||||
|
|
||||||
|
// This makes it clear that we're working with a d3 selected element of some kind, even though it's hard to specify the exact type.
|
||||||
|
// @ts-ignore Could replicate the type definition in d3. This also makes it possible to use the untyped info from the js diagram files.
|
||||||
|
type D3Element = any;
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -121,6 +130,174 @@ export const decodeEntities = function (text: string): string {
|
|||||||
return txt;
|
return txt;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// append !important; to each cssClass followed by a final !important, all enclosed in { }
|
||||||
|
//
|
||||||
|
/**
|
||||||
|
* Create a CSS style that starts with the given class name, then the element,
|
||||||
|
* with an enclosing block that has each of the cssClasses followed by !important;
|
||||||
|
* @param {string} cssClass
|
||||||
|
* @param {string} element
|
||||||
|
* @param {string[]} cssClasses
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export const cssImportantStyles = (
|
||||||
|
cssClass: string,
|
||||||
|
element: string,
|
||||||
|
cssClasses: string[] = []
|
||||||
|
): string => {
|
||||||
|
return `\n.${cssClass} ${element} { ${cssClasses.join(' !important; ')} !important; }`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the user styles
|
||||||
|
*
|
||||||
|
* @param {MermaidConfig} config
|
||||||
|
* @param {string} graphType
|
||||||
|
* @param {null | DiagramStyleClassDef[]} classDefs - the classDefs in the diagram text. Might be null if none were defined. Usually is the result of a call to getClasses(...)
|
||||||
|
* @returns {string} the string with all the user styles
|
||||||
|
*/
|
||||||
|
export const createCssStyles = (
|
||||||
|
config: MermaidConfig,
|
||||||
|
graphType: string,
|
||||||
|
classDefs: DiagramStyleClassDef[] | null | undefined
|
||||||
|
): string => {
|
||||||
|
let cssStyles = '';
|
||||||
|
|
||||||
|
// user provided theme CSS info
|
||||||
|
// If you add more configuration driven data into the user styles make sure that the value is
|
||||||
|
// sanitized by the santizeCSS function @todo TODO where is this method? what should be used to replace it? refactor so that it's always sanitized
|
||||||
|
if (config.themeCSS !== undefined) cssStyles += `\n${config.themeCSS}`;
|
||||||
|
|
||||||
|
if (config.fontFamily !== undefined)
|
||||||
|
cssStyles += `\n:root { --mermaid-font-family: ${config.fontFamily}}`;
|
||||||
|
|
||||||
|
if (config.altFontFamily !== undefined)
|
||||||
|
cssStyles += `\n:root { --mermaid-alt-font-family: ${config.altFontFamily}}`;
|
||||||
|
|
||||||
|
// classDefs defined in the diagram text
|
||||||
|
if (classDefs !== undefined && classDefs !== null && classDefs.length > 0) {
|
||||||
|
if (graphType === 'flowchart' || graphType === 'flowchart-v2' || graphType === 'graph') {
|
||||||
|
const htmlLabels = config.htmlLabels || config.flowchart?.htmlLabels;
|
||||||
|
|
||||||
|
const cssHtmlElements = ['> *', 'span']; // @todo TODO make a constant
|
||||||
|
const cssShapeElements = ['rect', 'polygon', 'ellipse', 'circle']; // @todo TODO make a constant
|
||||||
|
|
||||||
|
const cssElements = htmlLabels ? cssHtmlElements : cssShapeElements;
|
||||||
|
|
||||||
|
// create the CSS styles needed for each styleClass definition and css element
|
||||||
|
for (const classId in classDefs) {
|
||||||
|
const styleClassDef = classDefs[classId];
|
||||||
|
// create the css styles for each cssElement and the styles (only if there are styles)
|
||||||
|
if (styleClassDef['styles'] && styleClassDef['styles'].length > 0) {
|
||||||
|
cssElements.forEach((cssElement) => {
|
||||||
|
cssStyles += cssImportantStyles(
|
||||||
|
styleClassDef['id'],
|
||||||
|
cssElement,
|
||||||
|
styleClassDef['styles']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// create the css styles for the tspan element and the text styles (only if there are textStyles)
|
||||||
|
if (styleClassDef['textStyles'] && styleClassDef['textStyles'].length > 0) {
|
||||||
|
cssStyles += cssImportantStyles(
|
||||||
|
styleClassDef['id'],
|
||||||
|
'tspan',
|
||||||
|
styleClassDef['textStyles']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cssStyles;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanUpSvgCode = (
|
||||||
|
svgCode = '',
|
||||||
|
inSandboxMode: boolean,
|
||||||
|
useArrowMarkerUrls: boolean
|
||||||
|
): string => {
|
||||||
|
let cleanedUpSvg = svgCode;
|
||||||
|
|
||||||
|
// Replace marker-end urls with just the # anchor (remove the preceding part of the URL)
|
||||||
|
if (!useArrowMarkerUrls && !inSandboxMode) {
|
||||||
|
cleanedUpSvg = cleanedUpSvg.replace(/marker-end="url\(.*?#/g, 'marker-end="url(#');
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanedUpSvg = decodeEntities(cleanedUpSvg);
|
||||||
|
|
||||||
|
// replace old br tags with newer style
|
||||||
|
cleanedUpSvg = cleanedUpSvg.replace(/<br>/g, '<br/>');
|
||||||
|
|
||||||
|
return cleanedUpSvg;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Put the svgCode into an iFrame. Return the iFrame code
|
||||||
|
*
|
||||||
|
* @param {string} svgCode
|
||||||
|
* @param {D3Element} svgElement - the d3 node that has the current svgElement so we can get the height from it
|
||||||
|
* @returns {string} - the code with the iFrame that now contains the svgCode
|
||||||
|
* @todo TODO replace btoa(). Replace with buf.toString('base64')?
|
||||||
|
*/
|
||||||
|
export const putIntoIFrame = (svgCode = '', svgElement?: D3Element): string => {
|
||||||
|
let height = IFRAME_HEIGHT; // default iFrame height
|
||||||
|
if (svgElement) height = svgElement.viewBox.baseVal.height + 'px';
|
||||||
|
const base64encodedSrc = btoa('<body style="' + IFRAME_BODY_STYLE + '">' + svgCode + '</body>');
|
||||||
|
return `<iframe style="width:${IFRAME_WIDTH};height:${height};${IFRAME_STYLES}" src="data:text/html;base64,${base64encodedSrc}" sandbox="${IFRAME_SANDBOX_OPTS}">
|
||||||
|
${IFRAME_NOT_SUPPORTED_MSG}
|
||||||
|
</iframe>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append an enclosing div, then svg, then g (group) to the d3 parentRoot. Set attributes.
|
||||||
|
* Only set the style attribute on the enclosing div if divStyle is given.
|
||||||
|
* Only set the xmlns:xlink attribute on svg if svgXlink is given.
|
||||||
|
* Return the last node appended
|
||||||
|
*
|
||||||
|
* @param {D3Element} parentRoot - the d3 node to append things to
|
||||||
|
* @param {string} id
|
||||||
|
* @param enclosingDivId
|
||||||
|
* @param {string} divStyle
|
||||||
|
* @param {string} svgXlink
|
||||||
|
* @returns {D3Element} - returns the parentRoot that had nodes appended
|
||||||
|
*/
|
||||||
|
export const appendDivSvgG = (
|
||||||
|
parentRoot: D3Element,
|
||||||
|
id: string,
|
||||||
|
enclosingDivId: string,
|
||||||
|
divStyle?: string,
|
||||||
|
svgXlink?: string
|
||||||
|
): D3Element => {
|
||||||
|
const enclosingDiv = parentRoot.append('div');
|
||||||
|
enclosingDiv.attr('id', enclosingDivId);
|
||||||
|
if (divStyle) enclosingDiv.attr('style', divStyle);
|
||||||
|
|
||||||
|
const svgNode = enclosingDiv
|
||||||
|
.append('svg')
|
||||||
|
.attr('id', id)
|
||||||
|
.attr('width', '100%')
|
||||||
|
.attr('xmlns', XMLNS_SVG_STD);
|
||||||
|
if (svgXlink) svgNode.attr('xmlns:xlink', svgXlink);
|
||||||
|
|
||||||
|
svgNode.append('g');
|
||||||
|
return parentRoot;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Append an iFrame node to the given parentNode and set the id, style, and 'sandbox' attributes
|
||||||
|
* Return the appended iframe d3 node
|
||||||
|
*
|
||||||
|
* @param {D3Element} parentNode
|
||||||
|
* @param {string} iFrameId - id to use for the iFrame
|
||||||
|
* @returns {D3Element} the appended iframe d3 node
|
||||||
|
*/
|
||||||
|
function sandboxedIframe(parentNode: D3Element, iFrameId: string): D3Element {
|
||||||
|
return parentNode
|
||||||
|
.append('iframe')
|
||||||
|
.attr('id', iFrameId)
|
||||||
|
.attr('style', 'width: 100%; height: 100%;')
|
||||||
|
.attr('sandbox', '');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function that renders an svg with a graph from a chart definition. Usage example below.
|
* Function that renders an svg with a graph from a chart definition. Usage example below.
|
||||||
*
|
*
|
||||||
@@ -154,13 +331,19 @@ const render = async function (
|
|||||||
addDiagrams();
|
addDiagrams();
|
||||||
|
|
||||||
configApi.reset();
|
configApi.reset();
|
||||||
|
|
||||||
|
// Add Directives. Must do this before getting the config and before creating the diagram.
|
||||||
|
const graphInit = utils.detectInit(text);
|
||||||
|
if (graphInit) {
|
||||||
|
directiveSanitizer(graphInit);
|
||||||
|
configApi.addDirective(graphInit);
|
||||||
|
}
|
||||||
|
|
||||||
const config = configApi.getConfig();
|
const config = configApi.getConfig();
|
||||||
log.debug(config);
|
log.debug(config);
|
||||||
|
|
||||||
// Check the maximum allowed text size
|
// Check the maximum allowed text size
|
||||||
if (text.length > config.maxTextSize!) {
|
if (text.length > config.maxTextSize!) text = MAX_TEXTLENGTH_EXCEEDED_MSG;
|
||||||
text = MAX_TEXTLENGTH_EXCEEDED_MSG;
|
|
||||||
}
|
|
||||||
|
|
||||||
// clean up text CRLFs
|
// clean up text CRLFs
|
||||||
text = text.replace(/\r\n?/g, '\n'); // parser problems on CRLF ignore all CR and leave LF;;
|
text = text.replace(/\r\n?/g, '\n'); // parser problems on CRLF ignore all CR and leave LF;;
|
||||||
@@ -183,44 +366,23 @@ const render = async function (
|
|||||||
|
|
||||||
// In regular execution the svgContainingElement will be the element with a mermaid class
|
// In regular execution the svgContainingElement will be the element with a mermaid class
|
||||||
if (typeof svgContainingElement !== 'undefined') {
|
if (typeof svgContainingElement !== 'undefined') {
|
||||||
// A svgContainingElement was provided by the caller. Clear the inner HTML if there is any
|
if (svgContainingElement) svgContainingElement.innerHTML = '';
|
||||||
if (svgContainingElement) {
|
|
||||||
svgContainingElement.innerHTML = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSandboxed) {
|
if (isSandboxed) {
|
||||||
// IF we are in sandboxed mode, we do everyting mermaid related
|
// If we are in sandboxed mode, we do everything mermaid related in a (sandboxed )iFrame
|
||||||
// in a sandboxed div
|
const iframe = sandboxedIframe(select(svgContainingElement), iFrameID);
|
||||||
const iframe = select(svgContainingElement)
|
|
||||||
.append('iframe')
|
|
||||||
.attr('id', iFrameID)
|
|
||||||
.attr('style', SANDBOX_IFRAME_STYLE)
|
|
||||||
.attr('sandbox', '');
|
|
||||||
// const iframeBody = ;
|
|
||||||
root = select(iframe.nodes()[0]!.contentDocument!.body);
|
root = select(iframe.nodes()[0]!.contentDocument!.body);
|
||||||
root.node().style.margin = 0;
|
root.node().style.margin = 0;
|
||||||
} else {
|
} else {
|
||||||
root = select(svgContainingElement);
|
root = select(svgContainingElement);
|
||||||
}
|
}
|
||||||
|
appendDivSvgG(root, id, enclosingDivID, `font-family: ${fontFamily}`, XMLNS_XLINK_STD);
|
||||||
root
|
|
||||||
.append('div')
|
|
||||||
.attr('id', enclosingDivID)
|
|
||||||
.attr('style', 'font-family: ' + fontFamily)
|
|
||||||
.append('svg')
|
|
||||||
.attr('id', id)
|
|
||||||
.attr('width', '100%')
|
|
||||||
.attr('xmlns', XMLNS_SVG_STD)
|
|
||||||
.attr('xmlns:xlink', XMLNS_XLINK_STD)
|
|
||||||
.append('g');
|
|
||||||
} else {
|
} else {
|
||||||
// No svgContainingElement was provided
|
// No svgContainingElement was provided
|
||||||
// If there is an existing element with the id, we remove it
|
// If there is an existing element with the id, we remove it
|
||||||
// this likely a previously rendered diagram
|
// this likely a previously rendered diagram
|
||||||
const existingSvg = document.getElementById(id);
|
const existingSvg = document.getElementById(id);
|
||||||
if (existingSvg) {
|
if (existingSvg) existingSvg.remove();
|
||||||
existingSvg.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove previous temporary element if it exists
|
// Remove previous temporary element if it exists
|
||||||
let element;
|
let element;
|
||||||
@@ -229,42 +391,22 @@ const render = async function (
|
|||||||
} else {
|
} else {
|
||||||
element = document.querySelector(enclosingDivID_selector);
|
element = document.querySelector(enclosingDivID_selector);
|
||||||
}
|
}
|
||||||
|
if (element) element.remove();
|
||||||
|
|
||||||
if (element) {
|
// Add the temporary div used for rendering with the enclosingDivID.
|
||||||
element.remove();
|
// This temporary div will contain a svg with the id == id
|
||||||
}
|
|
||||||
|
|
||||||
// Add the tmp div used for rendering with the id `d${id}`
|
|
||||||
// d+id it will contain a svg with the id "id"
|
|
||||||
|
|
||||||
if (isSandboxed) {
|
if (isSandboxed) {
|
||||||
// IF we are in sandboxed mode, we do everything mermaid relate in a (sandboxed) iFrame
|
// If we are in sandboxed mode, we do everything mermaid related in a (sandboxed) iFrame
|
||||||
const iframe = select('body')
|
const iframe = sandboxedIframe(select('body'), iFrameID);
|
||||||
.append('iframe')
|
|
||||||
.attr('id', iFrameID)
|
|
||||||
.attr('style', SANDBOX_IFRAME_STYLE)
|
|
||||||
.attr('sandbox', '');
|
|
||||||
|
|
||||||
root = select(iframe.nodes()[0]!.contentDocument!.body);
|
root = select(iframe.nodes()[0]!.contentDocument!.body);
|
||||||
root.node().style.margin = 0;
|
root.node().style.margin = 0;
|
||||||
} else {
|
} else root = select('body');
|
||||||
root = select('body');
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is the temporary div
|
appendDivSvgG(root, id, enclosingDivID);
|
||||||
root
|
|
||||||
.append('div')
|
|
||||||
.attr('id', enclosingDivID)
|
|
||||||
// this is the seed of the svg to be rendered
|
|
||||||
.append('svg')
|
|
||||||
.attr('id', id)
|
|
||||||
.attr('width', '100%')
|
|
||||||
.attr('xmlns', XMLNS_SVG_STD)
|
|
||||||
.append('g');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------
|
|
||||||
//
|
|
||||||
text = encodeEntities(text);
|
text = encodeEntities(text);
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------
|
||||||
@@ -274,13 +416,6 @@ const render = async function (
|
|||||||
let diag;
|
let diag;
|
||||||
let parseEncounteredException;
|
let parseEncounteredException;
|
||||||
|
|
||||||
// Add Directives (Must do this before creating the diagram.)
|
|
||||||
const graphInit = utils.detectInit(text);
|
|
||||||
if (graphInit) {
|
|
||||||
directiveSanitizer(graphInit);
|
|
||||||
configApi.addDirective(graphInit);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// diag = new Diagram(text);
|
// diag = new Diagram(text);
|
||||||
diag = await getDiagramFromText(text);
|
diag = await getDiagramFromText(text);
|
||||||
@@ -289,7 +424,7 @@ const render = async function (
|
|||||||
parseEncounteredException = error;
|
parseEncounteredException = error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the tmp element containing the the svg
|
// Get the tmp div element containing the svg
|
||||||
const element = root.select(enclosingDivID_selector).node();
|
const element = root.select(enclosingDivID_selector).node();
|
||||||
const graphType = diag.type;
|
const graphType = diag.type;
|
||||||
|
|
||||||
@@ -300,62 +435,12 @@ const render = async function (
|
|||||||
const svg = element.firstChild;
|
const svg = element.firstChild;
|
||||||
const firstChild = svg.firstChild;
|
const firstChild = svg.firstChild;
|
||||||
|
|
||||||
let userStyles = '';
|
const userDefClasses: any = flowRenderer.getClasses(text, diag);
|
||||||
// user provided theme CSS
|
const cssStyles = createCssStyles(config, graphType, userDefClasses);
|
||||||
// If you add more configuration driven data into the user styles make sure that the value is
|
|
||||||
// sanitized bye the santiizeCSS function
|
|
||||||
if (config.themeCSS !== undefined) {
|
|
||||||
userStyles += `\n${config.themeCSS}`;
|
|
||||||
}
|
|
||||||
// user provided theme CSS
|
|
||||||
if (fontFamily !== undefined) {
|
|
||||||
userStyles += `\n:root { --mermaid-font-family: ${fontFamily}}`;
|
|
||||||
}
|
|
||||||
// user provided theme CSS
|
|
||||||
if (config.altFontFamily !== undefined) {
|
|
||||||
userStyles += `\n:root { --mermaid-alt-font-family: ${config.altFontFamily}}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// classDef
|
|
||||||
if (graphType === 'flowchart' || graphType === 'flowchart-v2' || graphType === 'graph') {
|
|
||||||
const classes: any = flowRenderer.getClasses(text, diag);
|
|
||||||
const htmlLabels = config.htmlLabels || config.flowchart?.htmlLabels;
|
|
||||||
for (const className in classes) {
|
|
||||||
if (htmlLabels) {
|
|
||||||
userStyles += `\n.${className} > * { ${classes[className].styles.join(
|
|
||||||
' !important; '
|
|
||||||
)} !important; }`;
|
|
||||||
userStyles += `\n.${className} span { ${classes[className].styles.join(
|
|
||||||
' !important; '
|
|
||||||
)} !important; }`;
|
|
||||||
} else {
|
|
||||||
userStyles += `\n.${className} path { ${classes[className].styles.join(
|
|
||||||
' !important; '
|
|
||||||
)} !important; }`;
|
|
||||||
userStyles += `\n.${className} rect { ${classes[className].styles.join(
|
|
||||||
' !important; '
|
|
||||||
)} !important; }`;
|
|
||||||
userStyles += `\n.${className} polygon { ${classes[className].styles.join(
|
|
||||||
' !important; '
|
|
||||||
)} !important; }`;
|
|
||||||
userStyles += `\n.${className} ellipse { ${classes[className].styles.join(
|
|
||||||
' !important; '
|
|
||||||
)} !important; }`;
|
|
||||||
userStyles += `\n.${className} circle { ${classes[className].styles.join(
|
|
||||||
' !important; '
|
|
||||||
)} !important; }`;
|
|
||||||
if (classes[className].textStyles) {
|
|
||||||
userStyles += `\n.${className} tspan { ${classes[className].textStyles.join(
|
|
||||||
' !important; '
|
|
||||||
)} !important; }`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stylis = (selector: string, styles: string) =>
|
const stylis = (selector: string, styles: string) =>
|
||||||
serialize(compile(`${selector}{${styles}}`), stringify);
|
serialize(compile(`${selector}{${styles}}`), stringify);
|
||||||
const rules = stylis(`${idSelector}`, getStyles(graphType, userStyles, config.themeVariables));
|
const rules = stylis(`${idSelector}`, getStyles(graphType, cssStyles, config.themeVariables));
|
||||||
|
|
||||||
const style1 = document.createElement('style');
|
const style1 = document.createElement('style');
|
||||||
style1.innerHTML = `${idSelector} ` + rules;
|
style1.innerHTML = `${idSelector} ` + rules;
|
||||||
@@ -378,35 +463,13 @@ const render = async function (
|
|||||||
let svgCode = root.select(enclosingDivID_selector).node().innerHTML;
|
let svgCode = root.select(enclosingDivID_selector).node().innerHTML;
|
||||||
|
|
||||||
log.debug('config.arrowMarkerAbsolute', config.arrowMarkerAbsolute);
|
log.debug('config.arrowMarkerAbsolute', config.arrowMarkerAbsolute);
|
||||||
if (!evaluate(config.arrowMarkerAbsolute) && config.securityLevel !== SECURITY_LVL_SANDBOX) {
|
svgCode = cleanUpSvgCode(svgCode, isSandboxed, evaluate(config.arrowMarkerAbsolute));
|
||||||
svgCode = svgCode.replace(/marker-end="url\(.*?#/g, 'marker-end="url(#', 'g');
|
|
||||||
}
|
|
||||||
|
|
||||||
svgCode = decodeEntities(svgCode);
|
|
||||||
|
|
||||||
// Fix for when the br tag is used
|
|
||||||
svgCode = svgCode.replace(/<br>/g, '<br/>');
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
if (isSandboxed) {
|
if (isSandboxed) {
|
||||||
const svgEl = root.select(enclosingDivID_selector + ' svg').node();
|
const svgEl = root.select(enclosingDivID_selector + ' svg').node();
|
||||||
const width = IFRAME_WIDTH;
|
svgCode = putIntoIFrame(svgCode, svgEl);
|
||||||
let height = IFRAME_HEIGHT;
|
|
||||||
|
|
||||||
// set the svg element height to px
|
|
||||||
if (svgEl) {
|
|
||||||
height = svgEl.viewBox.baseVal.height + 'px';
|
|
||||||
}
|
|
||||||
// Insert iFrame code into svg code
|
|
||||||
svgCode = `<iframe style="width:${width};height:${height};${IFRAME_STYLES}" src="data:text/html;base64,${btoa(
|
|
||||||
`<body style="${IFRAME_BODY_STYLE}">` + svgCode + '</body>'
|
|
||||||
)}" sandbox="${IFRAME_SANDBOX_OPTS}">
|
|
||||||
${IFRAME_NOT_SUPPORTED_MSG}
|
|
||||||
</iframe>`;
|
|
||||||
} else {
|
} else {
|
||||||
if (isLooseSecurityLevel) {
|
if (isLooseSecurityLevel) {
|
||||||
// -------------------------------------------------------------------------------
|
|
||||||
// Sanitize the svgCode using DOMPurify
|
// Sanitize the svgCode using DOMPurify
|
||||||
svgCode = DOMPurify.sanitize(svgCode, {
|
svgCode = DOMPurify.sanitize(svgCode, {
|
||||||
ADD_TAGS: DOMPURE_TAGS,
|
ADD_TAGS: DOMPURE_TAGS,
|
||||||
@@ -433,22 +496,17 @@ const render = async function (
|
|||||||
default:
|
default:
|
||||||
cb(svgCode);
|
cb(svgCode);
|
||||||
}
|
}
|
||||||
} else {
|
} else log.debug('CB = undefined!');
|
||||||
log.debug('CB = undefined!');
|
|
||||||
}
|
|
||||||
attachFunctions();
|
attachFunctions();
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------
|
||||||
// Remove the temporary element if appropriate
|
// Remove the temporary element if appropriate
|
||||||
const tmpElementSelector = isSandboxed ? iFrameID_selector : enclosingDivID_selector;
|
const tmpElementSelector = isSandboxed ? iFrameID_selector : enclosingDivID_selector;
|
||||||
const node = select(tmpElementSelector).node();
|
const node = select(tmpElementSelector).node();
|
||||||
if (node && 'remove' in node) {
|
if (node && 'remove' in node) node.remove();
|
||||||
node.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parseEncounteredException) {
|
if (parseEncounteredException) throw parseEncounteredException;
|
||||||
throw parseEncounteredException;
|
|
||||||
}
|
|
||||||
|
|
||||||
return svgCode;
|
return svgCode;
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user