mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-14 06:43:25 +08: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';
|
||||
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 = '';
|
||||
|
@ -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;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user