fix: Type of DiagramStyleClassDef, general cleanup

This commit is contained in:
Sidharth Vinod
2022-10-19 22:31:37 +05:30
parent ea86ef3995
commit 377b22e82b
6 changed files with 70 additions and 54 deletions

View File

@@ -22,7 +22,7 @@ import { MermaidConfig } from './config.type';
* *
* @name Configuration * @name Configuration
*/ */
const config: Partial<MermaidConfig> = { const config: MermaidConfig = {
/** /**
* Theme , the CSS style sheet * Theme , the CSS style sheet
* *
@@ -1069,6 +1069,7 @@ const config: Partial<MermaidConfig> = {
showCommitLabel: true, showCommitLabel: true,
showBranches: true, showBranches: true,
rotateCommitLabel: true, rotateCommitLabel: true,
arrowMarkerAbsolute: false,
}, },
/** The object containing configurations specific for c4 diagrams */ /** The object containing configurations specific for c4 diagrams */
@@ -1833,9 +1834,6 @@ const config: Partial<MermaidConfig> = {
fontSize: 16, fontSize: 16,
}; };
if (config.class) config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
if (config.gitGraph) config.gitGraph.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
const keyify = (obj: any, prefix = ''): string[] => const keyify = (obj: any, prefix = ''): string[] =>
Object.keys(obj).reduce((res: string[], el): string[] => { Object.keys(obj).reduce((res: string[], el): string[] => {
if (Array.isArray(obj[el])) { if (Array.isArray(obj[el])) {

View File

@@ -17,7 +17,7 @@ let vertexCounter = 0;
let config = configApi.getConfig(); let config = configApi.getConfig();
let vertices = {}; let vertices = {};
let edges = []; let edges = [];
let classes = []; let classes = {};
let subGraphs = []; let subGraphs = [];
let subGraphLookup = {}; let subGraphLookup = {};
let tooltips = {}; let tooltips = {};

View File

@@ -279,7 +279,8 @@ export const getClasses = function (text, diagObj) {
diagObj.parse(text); diagObj.parse(text);
return diagObj.db.getClasses(); return diagObj.db.getClasses();
} catch (e) { } catch (e) {
return; log.error(e);
return {};
} }
}; };

View File

@@ -256,11 +256,11 @@ describe('mermaidAPI', function () {
expect(styles).toMatch(/^\ndefault(.*)/); expect(styles).toMatch(/^\ndefault(.*)/);
}); });
it('gets the fontFamily from the config', () => { it('gets the fontFamily from the config', () => {
const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', null); const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', {});
expect(styles).toMatch(/(.*)\n:root \{ --mermaid-font-family: serif(.*)/); expect(styles).toMatch(/(.*)\n:root \{ --mermaid-font-family: serif(.*)/);
}); });
it('gets the alt fontFamily from the config', () => { it('gets the alt fontFamily from the config', () => {
const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', null); const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', undefined);
expect(styles).toMatch(/(.*)\n:root \{ --mermaid-alt-font-family: sans-serif(.*)/); expect(styles).toMatch(/(.*)\n:root \{ --mermaid-alt-font-family: sans-serif(.*)/);
}); });
@@ -268,7 +268,7 @@ describe('mermaidAPI', function () {
const classDef1 = { id: 'classDef1', styles: ['style1-1', 'style1-2'], textStyles: [] }; const classDef1 = { id: 'classDef1', styles: ['style1-1', 'style1-2'], textStyles: [] };
const classDef2 = { id: 'classDef2', styles: [], textStyles: ['textStyle2-1'] }; const classDef2 = { id: 'classDef2', styles: [], textStyles: ['textStyle2-1'] };
const classDef3 = { id: 'classDef3', textStyles: ['textStyle3-1', 'textStyle3-2'] }; const classDef3 = { id: 'classDef3', textStyles: ['textStyle3-1', 'textStyle3-2'] };
const classDefs = [classDef1, classDef2, classDef3]; const classDefs = { classDef1, classDef2, classDef3 };
describe('the graph supports classDefs', () => { describe('the graph supports classDefs', () => {
const graphType = 'flowchart-v2'; const graphType = 'flowchart-v2';
@@ -431,7 +431,7 @@ describe('mermaidAPI', function () {
it('gets the css styles created', () => { 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. // @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'); createUserStyles(mockConfig, 'flowchart-v2', { classDef1 }, 'someId');
const expectedStyles = const expectedStyles =
'\ndefault' + '\ndefault' +
'\n.classDef1 > * { style1-1 !important; }' + '\n.classDef1 > * { style1-1 !important; }' +
@@ -442,12 +442,12 @@ describe('mermaidAPI', function () {
}); });
it('calls getStyles to get css for all graph, user css styles, and config theme variables', () => { it('calls getStyles to get css for all graph, user css styles, and config theme variables', () => {
createUserStyles(mockConfig, 'someDiagram', null, 'someId'); createUserStyles(mockConfig, 'someDiagram', {}, 'someId');
expect(getStyles).toHaveBeenCalled(); expect(getStyles).toHaveBeenCalled();
}); });
it('returns the result of compiling, stringifying, and serializing the css code with stylis', () => { it('returns the result of compiling, stringifying, and serializing the css code with stylis', () => {
const result = createUserStyles(mockConfig, 'someDiagram', null, 'someId'); const result = createUserStyles(mockConfig, 'someDiagram', {}, 'someId');
expect(compile).toHaveBeenCalled(); expect(compile).toHaveBeenCalled();
expect(serialize).toHaveBeenCalled(); expect(serialize).toHaveBeenCalled();
expect(result).toEqual('stylis serialized css'); expect(result).toEqual('stylis serialized css');

View File

@@ -28,7 +28,7 @@ import { attachFunctions } from './interactionDb';
import { log, setLogLevel } from './logger'; import { log, setLogLevel } from './logger';
import getStyles from './styles'; import getStyles from './styles';
import theme from './themes'; import theme from './themes';
import utils, { directiveSanitizer } from './utils'; import utils, { directiveSanitizer, isNonEmptyArray } from './utils';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { MermaidConfig } from './config.type'; import { MermaidConfig } from './config.type';
import { evaluate } from './diagrams/common/common'; import { evaluate } from './diagrams/common/common';
@@ -59,8 +59,11 @@ const DOMPURE_ATTR = ['dominant-baseline'];
// This is what is returned from getClasses(...) methods. // 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 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. // 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. interface DiagramStyleClassDef {
type DiagramStyleClassDef = any; id: string;
styles?: string[];
textStyles?: string[];
}
// 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. // 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. // @ts-ignore Could replicate the type definition in d3. This also makes it possible to use the untyped info from the js diagram files.
@@ -151,29 +154,32 @@ export const cssImportantStyles = (
* *
* @param {MermaidConfig} config * @param {MermaidConfig} config
* @param {string} graphType * @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(...) * @param {Record<string, DiagramStyleClassDef> | null | undefined} 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 * @returns {string} the string with all the user styles
*/ */
export const createCssStyles = ( export const createCssStyles = (
config: MermaidConfig, config: MermaidConfig,
graphType: string, graphType: string,
classDefs: DiagramStyleClassDef[] | null | undefined classDefs: Record<string, DiagramStyleClassDef> | null | undefined = {}
): string => { ): string => {
let cssStyles = ''; let cssStyles = '';
// user provided theme CSS info // user provided theme CSS info
// If you add more configuration driven data into the user styles make sure that the value is // 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 // 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.themeCSS !== undefined) {
cssStyles += `\n${config.themeCSS}`;
}
if (config.fontFamily !== undefined) if (config.fontFamily !== undefined) {
cssStyles += `\n:root { --mermaid-font-family: ${config.fontFamily}}`; cssStyles += `\n:root { --mermaid-font-family: ${config.fontFamily}}`;
}
if (config.altFontFamily !== undefined) if (config.altFontFamily !== undefined) {
cssStyles += `\n:root { --mermaid-alt-font-family: ${config.altFontFamily}}`; cssStyles += `\n:root { --mermaid-alt-font-family: ${config.altFontFamily}}`;
}
// classDefs defined in the diagram text // classDefs defined in the diagram text
if (classDefs !== undefined && classDefs !== null && classDefs.length > 0) { if (classDefs && Object.keys(classDefs).length > 0) {
if (graphType === 'flowchart' || graphType === 'flowchart-v2' || graphType === 'graph') { if (graphType === 'flowchart' || graphType === 'flowchart-v2' || graphType === 'graph') {
const htmlLabels = config.htmlLabels || config.flowchart?.htmlLabels; const htmlLabels = config.htmlLabels || config.flowchart?.htmlLabels;
@@ -186,22 +192,14 @@ export const createCssStyles = (
for (const classId in classDefs) { for (const classId in classDefs) {
const styleClassDef = classDefs[classId]; const styleClassDef = classDefs[classId];
// create the css styles for each cssElement and the styles (only if there are styles) // create the css styles for each cssElement and the styles (only if there are styles)
if (styleClassDef['styles'] && styleClassDef['styles'].length > 0) { if (isNonEmptyArray(styleClassDef.styles)) {
cssElements.forEach((cssElement) => { cssElements.forEach((cssElement) => {
cssStyles += cssImportantStyles( cssStyles += cssImportantStyles(styleClassDef.id, cssElement, styleClassDef.styles);
styleClassDef['id'],
cssElement,
styleClassDef['styles']
);
}); });
} }
// create the css styles for the tspan element and the text styles (only if there are textStyles) // create the css styles for the tspan element and the text styles (only if there are textStyles)
if (styleClassDef['textStyles'] && styleClassDef['textStyles'].length > 0) { if (isNonEmptyArray(styleClassDef.textStyles)) {
cssStyles += cssImportantStyles( cssStyles += cssImportantStyles(styleClassDef.id, 'tspan', styleClassDef.textStyles);
styleClassDef['id'],
'tspan',
styleClassDef['textStyles']
);
} }
} }
} }
@@ -212,7 +210,7 @@ export const createCssStyles = (
export const createUserStyles = ( export const createUserStyles = (
config: MermaidConfig, config: MermaidConfig,
graphType: string, graphType: string,
classDefs: null | DiagramStyleClassDef, classDefs: Record<string, DiagramStyleClassDef>,
svgId: string svgId: string
): string => { ): string => {
const userCSSstyles = createCssStyles(config, graphType, classDefs); const userCSSstyles = createCssStyles(config, graphType, classDefs);
@@ -261,8 +259,7 @@ export const cleanUpSvgCode = (
* @todo TODO replace btoa(). Replace with buf.toString('base64')? * @todo TODO replace btoa(). Replace with buf.toString('base64')?
*/ */
export const putIntoIFrame = (svgCode = '', svgElement?: D3Element): string => { export const putIntoIFrame = (svgCode = '', svgElement?: D3Element): string => {
let height = IFRAME_HEIGHT; // default iFrame height const height = svgElement ? svgElement.viewBox.baseVal.height + 'px' : IFRAME_HEIGHT;
if (svgElement) height = svgElement.viewBox.baseVal.height + 'px';
const base64encodedSrc = btoa('<body style="' + IFRAME_BODY_STYLE + '">' + svgCode + '</body>'); 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}"> 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_NOT_SUPPORTED_MSG}
@@ -291,14 +288,18 @@ export const appendDivSvgG = (
): D3Element => { ): D3Element => {
const enclosingDiv = parentRoot.append('div'); const enclosingDiv = parentRoot.append('div');
enclosingDiv.attr('id', enclosingDivId); enclosingDiv.attr('id', enclosingDivId);
if (divStyle) enclosingDiv.attr('style', divStyle); if (divStyle) {
enclosingDiv.attr('style', divStyle);
}
const svgNode = enclosingDiv const svgNode = enclosingDiv
.append('svg') .append('svg')
.attr('id', id) .attr('id', id)
.attr('width', '100%') .attr('width', '100%')
.attr('xmlns', XMLNS_SVG_STD); .attr('xmlns', XMLNS_SVG_STD);
if (svgXlink) svgNode.attr('xmlns:xlink', svgXlink); if (svgXlink) {
svgNode.attr('xmlns:xlink', svgXlink);
}
svgNode.append('g'); svgNode.append('g');
return parentRoot; return parentRoot;
@@ -337,11 +338,15 @@ export const removeExistingElements = (
) => { ) => {
// Remove existing SVG element if it exists // Remove existing SVG element if it exists
const existingSvg = doc.getElementById(id); const existingSvg = doc.getElementById(id);
if (existingSvg) existingSvg.remove(); if (existingSvg) {
existingSvg.remove();
}
// Remove previous temporary element if it exists // Remove previous temporary element if it exists
const element = isSandboxed ? doc.querySelector(iFrameSelector) : doc.querySelector(divSelector); const element = isSandboxed ? doc.querySelector(iFrameSelector) : doc.querySelector(divSelector);
if (element) element.remove(); if (element) {
element.remove();
}
}; };
/** /**
@@ -389,7 +394,10 @@ const render = async function (
log.debug(config); log.debug(config);
// Check the maximum allowed text size // Check the maximum allowed text size
if (text.length > config.maxTextSize!) text = MAX_TEXTLENGTH_EXCEEDED_MSG; // TODO: Remove magic number
if (text.length > (config?.maxTextSize ?? 50000)) {
text = MAX_TEXTLENGTH_EXCEEDED_MSG;
}
// clean up text CRLFs // clean up text CRLFs
text = text.replace(/\r\n?/g, '\n'); // parser problems on CRLF ignore all CR and leave LF;; text = text.replace(/\r\n?/g, '\n'); // parser problems on CRLF ignore all CR and leave LF;;
@@ -472,6 +480,7 @@ const render = async function (
const rules = createUserStyles( const rules = createUserStyles(
config, config,
graphType, graphType,
// @ts-ignore convert flowRender to TS.
flowRenderer.getClasses(text, diag), flowRenderer.getClasses(text, diag),
idSelector idSelector
); );
@@ -485,7 +494,7 @@ const render = async function (
try { try {
await diag.renderer.draw(text, id, pkg.version, diag); await diag.renderer.draw(text, id, pkg.version, diag);
} catch (e) { } catch (e) {
await errorRenderer.draw(text, id, pkg.version); errorRenderer.draw(text, id, pkg.version);
throw e; throw e;
} }
@@ -502,14 +511,12 @@ const render = async function (
if (isSandboxed) { if (isSandboxed) {
const svgEl = root.select(enclosingDivID_selector + ' svg').node(); const svgEl = root.select(enclosingDivID_selector + ' svg').node();
svgCode = putIntoIFrame(svgCode, svgEl); svgCode = putIntoIFrame(svgCode, svgEl);
} else { } else if (isLooseSecurityLevel) {
if (isLooseSecurityLevel) { // Sanitize the svgCode using DOMPurify
// Sanitize the svgCode using DOMPurify svgCode = DOMPurify.sanitize(svgCode, {
svgCode = DOMPurify.sanitize(svgCode, { ADD_TAGS: DOMPURE_TAGS,
ADD_TAGS: DOMPURE_TAGS, ADD_ATTR: DOMPURE_ATTR,
ADD_ATTR: DOMPURE_ATTR, });
});
}
} }
// ------------------------------------------------------------------------------- // -------------------------------------------------------------------------------
@@ -530,7 +537,9 @@ const render = async function (
default: default:
cb(svgCode); cb(svgCode);
} }
} else log.debug('CB = undefined!'); } else {
log.debug('CB = undefined!');
}
attachFunctions(); attachFunctions();
@@ -538,9 +547,13 @@ const render = async function (
// Remove the temporary element if appropriate // Remove the temporary element if appropriate
const tmpElementSelector = isSandboxed ? iFrameID_selector : enclosingDivID_selector; const tmpElementSelector = isSandboxed ? iFrameID_selector : enclosingDivID_selector;
const node = select(tmpElementSelector).node(); const node = select(tmpElementSelector).node();
if (node && 'remove' in node) node.remove(); if (node && 'remove' in node) {
node.remove();
}
if (parseEncounteredException) throw parseEncounteredException; if (parseEncounteredException) {
throw parseEncounteredException;
}
return svgCode; return svgCode;
}; };

View File

@@ -838,6 +838,10 @@ export function getErrorMessage(error: unknown): string {
return String(error); return String(error);
} }
export const isNonEmptyArray = (array: unknown[] | undefined): array is unknown[] => {
return array && array.length > 0;
};
export default { export default {
assignWithDepth, assignWithDepth,
wrapLabel, wrapLabel,