From a3b8c301e27f0d7b43372d76b1b75fae4bbfb567 Mon Sep 17 00:00:00 2001 From: "Ashley Engelund (weedySeaDragon @ github)" Date: Sat, 15 Oct 2022 19:15:59 -0700 Subject: [PATCH] functions and specs: createCssStyles, appendDivSvgG,cleanUpSvgCode, putIntoIFrame [for render] --- packages/mermaid/src/mermaidAPI.spec.js | 334 +++++++++++++++++++++- packages/mermaid/src/mermaidAPI.ts | 362 ++++++++++++++---------- 2 files changed, 543 insertions(+), 153 deletions(-) diff --git a/packages/mermaid/src/mermaidAPI.spec.js b/packages/mermaid/src/mermaidAPI.spec.js index 35473d1bf..f697891a4 100644 --- a/packages/mermaid/src/mermaidAPI.spec.js +++ b/packages/mermaid/src/mermaidAPI.spec.js @@ -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('
brrrr
', true, true); + expect(result).toEqual('
brrrr
'); + }); + }); + + 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 svgCode', () => { + const base64encodedSrc = btoa('' + inputSvgCode + ''); + 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 = ''; diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index 5a3793787..0165aaeff 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -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(/
/g, '
'); + + 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('' + svgCode + ''); + return ``; +}; + +/** + * 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(/
/g, '
'); - - // ------------------------------------------------------------------------------- + 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 = ``; + 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; };