functions and specs: createCssStyles, appendDivSvgG,cleanUpSvgCode, putIntoIFrame [for render]

This commit is contained in:
Ashley Engelund (weedySeaDragon @ github) 2022-10-15 19:15:59 -07:00
parent d106d3d1b1
commit a3b8c301e2
2 changed files with 543 additions and 153 deletions

View File

@ -1,10 +1,39 @@
'use strict';
import { vi } from 'vitest';
import mermaid from './mermaid';
import mermaidAPI from './mermaidAPI';
import { encodeEntities, decodeEntities } from './mermaidAPI';
import {
encodeEntities,
decodeEntities,
createCssStyles,
appendDivSvgG,
cleanUpSvgCode,
putIntoIFrame,
} from './mermaidAPI';
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('encodeEntities', () => {
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 () {
beforeEach(function () {
document.body.innerHTML = '';

View File

@ -45,18 +45,27 @@ const XMLNS_XLINK_STD = 'http://www.w3.org/1999/xlink';
// ------------------------------
// iFrame
const SANDBOX_IFRAME_STYLE = 'width: 100%; height: 100%;';
const IFRAME_WIDTH = '100%';
const IFRAME_HEIGHT = '100%';
const IFRAME_STYLES = 'border:0;margin:0;';
const IFRAME_BODY_STYLE = 'margin:0';
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
const DOMPURE_TAGS = ['foreignobject'];
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;
};
// 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.
*
@ -154,13 +331,19 @@ const render = async function (
addDiagrams();
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();
log.debug(config);
// Check the maximum allowed text size
if (text.length > config.maxTextSize!) {
text = MAX_TEXTLENGTH_EXCEEDED_MSG;
}
if (text.length > config.maxTextSize!) text = MAX_TEXTLENGTH_EXCEEDED_MSG;
// clean up text CRLFs
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
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 we are in sandboxed mode, we do everyting mermaid related
// in a sandboxed div
const iframe = select(svgContainingElement)
.append('iframe')
.attr('id', iFrameID)
.attr('style', SANDBOX_IFRAME_STYLE)
.attr('sandbox', '');
// const iframeBody = ;
// If we are in sandboxed mode, we do everything mermaid related in a (sandboxed )iFrame
const iframe = sandboxedIframe(select(svgContainingElement), iFrameID);
root = select(iframe.nodes()[0]!.contentDocument!.body);
root.node().style.margin = 0;
} else {
root = select(svgContainingElement);
}
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');
appendDivSvgG(root, id, enclosingDivID, `font-family: ${fontFamily}`, XMLNS_XLINK_STD);
} else {
// No svgContainingElement was provided
// If there is an existing element with the id, we remove it
// this likely a previously rendered diagram
const existingSvg = document.getElementById(id);
if (existingSvg) {
existingSvg.remove();
}
if (existingSvg) existingSvg.remove();
// Remove previous temporary element if it exists
let element;
@ -229,42 +391,22 @@ const render = async function (
} else {
element = document.querySelector(enclosingDivID_selector);
}
if (element) element.remove();
if (element) {
element.remove();
}
// Add the tmp div used for rendering with the id `d${id}`
// d+id it will contain a svg with the id "id"
// Add the temporary div used for rendering with the enclosingDivID.
// This temporary div will contain a svg with the id == id
if (isSandboxed) {
// IF we are in sandboxed mode, we do everything mermaid relate in a (sandboxed) iFrame
const iframe = select('body')
.append('iframe')
.attr('id', iFrameID)
.attr('style', SANDBOX_IFRAME_STYLE)
.attr('sandbox', '');
// If we are in sandboxed mode, we do everything mermaid related in a (sandboxed) iFrame
const iframe = sandboxedIframe(select('body'), iFrameID);
root = select(iframe.nodes()[0]!.contentDocument!.body);
root.node().style.margin = 0;
} else {
root = select('body');
}
} else root = select('body');
// This is the temporary div
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');
appendDivSvgG(root, id, enclosingDivID);
}
// -------------------------------------------------------------------------------
//
text = encodeEntities(text);
// -------------------------------------------------------------------------------
@ -274,13 +416,6 @@ const render = async function (
let diag;
let parseEncounteredException;
// Add Directives (Must do this before creating the diagram.)
const graphInit = utils.detectInit(text);
if (graphInit) {
directiveSanitizer(graphInit);
configApi.addDirective(graphInit);
}
try {
// diag = new Diagram(text);
diag = await getDiagramFromText(text);
@ -289,7 +424,7 @@ const render = async function (
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 graphType = diag.type;
@ -300,62 +435,12 @@ const render = async function (
const svg = element.firstChild;
const firstChild = svg.firstChild;
let userStyles = '';
// user provided theme CSS
// 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 userDefClasses: any = flowRenderer.getClasses(text, diag);
const cssStyles = createCssStyles(config, graphType, userDefClasses);
const stylis = (selector: string, styles: string) =>
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');
style1.innerHTML = `${idSelector} ` + rules;
@ -378,35 +463,13 @@ const render = async function (
let svgCode = root.select(enclosingDivID_selector).node().innerHTML;
log.debug('config.arrowMarkerAbsolute', config.arrowMarkerAbsolute);
if (!evaluate(config.arrowMarkerAbsolute) && config.securityLevel !== SECURITY_LVL_SANDBOX) {
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/>');
// -------------------------------------------------------------------------------
svgCode = cleanUpSvgCode(svgCode, isSandboxed, evaluate(config.arrowMarkerAbsolute));
if (isSandboxed) {
const svgEl = root.select(enclosingDivID_selector + ' svg').node();
const width = IFRAME_WIDTH;
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>`;
svgCode = putIntoIFrame(svgCode, svgEl);
} else {
if (isLooseSecurityLevel) {
// -------------------------------------------------------------------------------
// Sanitize the svgCode using DOMPurify
svgCode = DOMPurify.sanitize(svgCode, {
ADD_TAGS: DOMPURE_TAGS,
@ -433,22 +496,17 @@ const render = async function (
default:
cb(svgCode);
}
} else {
log.debug('CB = undefined!');
}
} else log.debug('CB = undefined!');
attachFunctions();
// -------------------------------------------------------------------------------
// Remove the temporary element if appropriate
const tmpElementSelector = isSandboxed ? iFrameID_selector : enclosingDivID_selector;
const node = select(tmpElementSelector).node();
if (node && 'remove' in node) {
node.remove();
}
if (node && 'remove' in node) node.remove();
if (parseEncounteredException) {
throw parseEncounteredException;
}
if (parseEncounteredException) throw parseEncounteredException;
return svgCode;
};