Merge branch 'develop' into sidv/esbuild

* develop: (50 commits)
  Typo fix
  Fix repo URL
  Revert flowchart change
  Revert flowchart change
  Fix TODO Qs
  chore(deps-dev): bump @commitlint/cli from 17.1.1 to 17.1.2
  chore(deps-dev): bump terser-webpack-plugin from 5.3.5 to 5.3.6
  chore(deps-dev): bump webpack-dev-server from 4.10.0 to 4.10.1
  Fix gitGraph findLane function error
  Update dependabot.yml
  Replacing replaceAll with replace
  Rework 'parseDuration' as a pure duration parsing
  Supports duration in decimal
  Create a more consistent 'parseDuration'
  Remove `@ts-ignore`s.
  Fix svgDraw return types
  ...
This commit is contained in:
Sidharth Vinod
2022-09-01 20:38:21 +05:30
56 changed files with 2474 additions and 1555 deletions

View File

@@ -1,71 +1,68 @@
import utils from './utils';
import * as configApi from './config';
import { log } from './logger';
import { getDiagrams } from './diagram-api/diagramAPI';
import detectType from './diagram-api/detectType';
class Diagram {
import { getDiagram } from './diagram-api/diagramAPI';
import { detectType } from './diagram-api/detectType';
import { isDetailedError } from './utils';
export class Diagram {
type = 'graph';
parser;
renderer;
db;
constructor(txt) {
const diagrams = getDiagrams();
constructor(public txt: string, parseError?: Function) {
const cnf = configApi.getConfig();
this.txt = txt;
this.type = detectType(txt, cnf);
const diagram = getDiagram(this.type);
log.debug('Type ' + this.type);
// console.log('this.type', this.type, diagrams[this.type]);
// Setup diagram
this.db = diagrams[this.type].db;
this.db = diagram.db;
this.db.clear?.();
this.renderer = diagrams[this.type].renderer;
this.parser = diagrams[this.type].parser;
this.renderer = diagram.renderer;
this.parser = diagram.parser;
this.parser.parser.yy = this.db;
if (typeof diagrams[this.type].init === 'function') {
diagrams[this.type].init(cnf);
if (diagram.init) {
diagram.init(cnf);
log.debug('Initialized diagram ' + this.type, cnf);
}
this.txt = this.txt + '\n';
this.txt += '\n';
this.parser.parser.yy.graphType = this.type;
this.parser.parser.yy.parseError = (str, hash) => {
this.parser.parser.yy.parseError = (str: string, hash: string) => {
const error = { str, hash };
throw error;
};
this.parser.parse(this.txt);
this.parse(this.txt, parseError);
}
parse(text) {
var parseEncounteredException = false;
parse(text: string, parseError?: Function): boolean {
try {
text = text + '\n';
this.db.clear();
this.parser.parse(text);
return true;
} catch (error) {
parseEncounteredException = true;
// Is this the correct way to access mermiad's parseError()
// method ? (or global.mermaid.parseError()) ?
if (global.mermaid.parseError) {
if (error.str != undefined) {
if (parseError) {
if (isDetailedError(error)) {
// handle case where error string and hash were
// wrapped in object like`const error = { str, hash };`
global.mermaid.parseError(error.str, error.hash);
parseError(error.str, error.hash);
} else {
// assume it is just error string and pass it on
global.mermaid.parseError(error);
parseError(error);
}
} else {
// No mermaid.parseError() handler defined, so re-throw it
throw error;
}
}
return !parseEncounteredException;
return false;
}
getParser() {
return this.parser;
}
getType() {
return this.type;
}

View File

@@ -3,35 +3,35 @@ import { getConfig } from './config';
let title = '';
let diagramTitle = '';
let description = '';
const sanitizeText = (txt) => _sanitizeText(txt, getConfig());
const sanitizeText = (txt: string): string => _sanitizeText(txt, getConfig());
export const clear = function () {
export const clear = function (): void {
title = '';
description = '';
diagramTitle = '';
};
export const setAccTitle = function (txt) {
export const setAccTitle = function (txt: string): void {
title = sanitizeText(txt).replace(/^\s+/g, '');
};
export const getAccTitle = function () {
export const getAccTitle = function (): string {
return title || diagramTitle;
};
export const setAccDescription = function (txt) {
export const setAccDescription = function (txt: string): void {
description = sanitizeText(txt).replace(/\n\s+/g, '\n');
};
export const getAccDescription = function () {
export const getAccDescription = function (): string {
return description;
};
export const setDiagramTitle = function (txt) {
export const setDiagramTitle = function (txt: string): void {
diagramTitle = sanitizeText(txt);
};
export const getDiagramTitle = function () {
export const getDiagramTitle = function (): string {
return diagramTitle;
};

View File

@@ -2,21 +2,22 @@ import assignWithDepth from './assignWithDepth';
import { log } from './logger';
import theme from './themes';
import config from './defaultConfig';
import type { MermaidConfig } from './config.type';
export const defaultConfig = Object.freeze(config);
export const defaultConfig: MermaidConfig = Object.freeze(config);
let siteConfig = assignWithDepth({}, defaultConfig);
let configFromInitialize;
let directives = [];
let currentConfig = assignWithDepth({}, defaultConfig);
let siteConfig: MermaidConfig = assignWithDepth({}, defaultConfig);
let configFromInitialize: MermaidConfig;
let directives: any[] = [];
let currentConfig: MermaidConfig = assignWithDepth({}, defaultConfig);
export const updateCurrentConfig = (siteCfg, _directives) => {
// start with config beeing the siteConfig
let cfg = assignWithDepth({}, siteCfg);
export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: any[]) => {
// start with config being the siteConfig
let cfg: MermaidConfig = assignWithDepth({}, siteCfg);
// let sCfg = assignWithDepth(defaultConfig, siteConfigDelta);
// Join directives
let sumOfDirectives = {};
let sumOfDirectives: MermaidConfig = {};
for (let i = 0; i < _directives.length; i++) {
const d = _directives[i];
sanitize(d);
@@ -27,13 +28,15 @@ export const updateCurrentConfig = (siteCfg, _directives) => {
cfg = assignWithDepth(cfg, sumOfDirectives);
if (sumOfDirectives.theme && theme[sumOfDirectives.theme]) {
if (sumOfDirectives.theme && sumOfDirectives.theme in theme) {
const tmpConfigFromInitialize = assignWithDepth({}, configFromInitialize);
const themeVariables = assignWithDepth(
tmpConfigFromInitialize.themeVariables || {},
sumOfDirectives.themeVariables
);
cfg.themeVariables = theme[cfg.theme].getThemeVariables(themeVariables);
if (cfg.theme && cfg.theme in theme) {
cfg.themeVariables = theme[cfg.theme as keyof typeof theme].getThemeVariables(themeVariables);
}
}
currentConfig = cfg;
@@ -55,11 +58,13 @@ export const updateCurrentConfig = (siteCfg, _directives) => {
* @param conf - The base currentConfig to use as siteConfig
* @returns {object} - The siteConfig
*/
export const setSiteConfig = (conf) => {
export const setSiteConfig = (conf: MermaidConfig): MermaidConfig => {
siteConfig = assignWithDepth({}, defaultConfig);
siteConfig = assignWithDepth(siteConfig, conf);
// @ts-ignore
if (conf.theme && theme[conf.theme]) {
// @ts-ignore
siteConfig.themeVariables = theme[conf.theme].getThemeVariables(conf.themeVariables);
}
@@ -67,11 +72,11 @@ export const setSiteConfig = (conf) => {
return siteConfig;
};
export const saveConfigFromInitialize = (conf) => {
export const saveConfigFromInitialize = (conf: MermaidConfig): void => {
configFromInitialize = assignWithDepth({}, conf);
};
export const updateSiteConfig = (conf) => {
export const updateSiteConfig = (conf: MermaidConfig): MermaidConfig => {
siteConfig = assignWithDepth(siteConfig, conf);
updateCurrentConfig(siteConfig, directives);
@@ -88,7 +93,7 @@ export const updateSiteConfig = (conf) => {
*
* @returns {object} - The siteConfig
*/
export const getSiteConfig = () => {
export const getSiteConfig = (): MermaidConfig => {
return assignWithDepth({}, siteConfig);
};
/**
@@ -105,7 +110,7 @@ export const getSiteConfig = () => {
* @param {any} conf - The potential currentConfig
* @returns {any} - The currentConfig merged with the sanitized conf
*/
export const setConfig = (conf) => {
export const setConfig = (conf: MermaidConfig): MermaidConfig => {
// sanitize(conf);
// Object.keys(conf).forEach(key => {
// const manipulator = manipulators[key];
@@ -128,7 +133,7 @@ export const setConfig = (conf) => {
*
* @returns {any} - The currentConfig
*/
export const getConfig = () => {
export const getConfig = (): MermaidConfig => {
return assignWithDepth({}, currentConfig);
};
/**
@@ -143,17 +148,14 @@ export const getConfig = () => {
*
* @param {any} options - The potential setConfig parameter
*/
export const sanitize = (options) => {
export const sanitize = (options: any) => {
// Checking that options are not in the list of excluded options
Object.keys(siteConfig.secure).forEach((key) => {
if (typeof options[siteConfig.secure[key]] !== 'undefined') {
// DO NOT attempt to print options[siteConfig.secure[key]] within `${}` as a malicious script
['secure', ...(siteConfig.secure ?? [])].forEach((key) => {
if (typeof options[key] !== 'undefined') {
// DO NOT attempt to print options[key] within `${}` as a malicious script
// can exploit the logger's attempt to stringify the value and execute arbitrary code
log.debug(
`Denied attempt to modify a secure key ${siteConfig.secure[key]}`,
options[siteConfig.secure[key]]
);
delete options[siteConfig.secure[key]];
log.debug(`Denied attempt to modify a secure key ${key}`, options[key]);
delete options[key];
}
});
@@ -186,7 +188,7 @@ export const sanitize = (options) => {
*
* @param {object} directive The directive to push in
*/
export const addDirective = (directive) => {
export const addDirective = (directive: any) => {
if (directive.fontFamily) {
if (!directive.themeVariables) {
directive.themeVariables = { fontFamily: directive.fontFamily };
@@ -215,8 +217,8 @@ export const addDirective = (directive) => {
*
* **Notes**: (default: current siteConfig ) (optional, default `getSiteConfig()`)
*/
export const reset = () => {
export const reset = (config = siteConfig): void => {
// Replace current config with siteConfig
directives = [];
updateCurrentConfig(siteConfig, directives);
updateCurrentConfig(config, directives);
};

351
src/config.type.ts Normal file
View File

@@ -0,0 +1,351 @@
// TODO: This was auto generated from defaultConfig. Needs to be verified.
import DOMPurify from 'dompurify';
export interface MermaidConfig {
theme?: string;
themeVariables?: any;
themeCSS?: string;
maxTextSize?: number;
darkMode?: boolean;
htmlLabels?: boolean;
fontFamily?: string;
altFontFamily?: string;
logLevel?: number;
securityLevel?: string;
startOnLoad?: boolean;
arrowMarkerAbsolute?: boolean;
secure?: string[];
deterministicIds?: boolean;
deterministicIDSeed?: string;
flowchart?: FlowchartDiagramConfig;
sequence?: SequenceDiagramConfig;
gantt?: GanttDiagramConfig;
journey?: JourneyDiagramConfig;
class?: ClassDiagramConfig;
state?: StateDiagramConfig;
er?: ErDiagramConfig;
pie?: PieDiagramConfig;
requirement?: RequirementDiagramConfig;
gitGraph?: GitGraphDiagramConfig;
c4?: C4DiagramConfig;
dompurifyConfig?: DOMPurify.Config;
wrap?: boolean;
}
// TODO: More configs needs to be moved in here
export interface BaseDiagramConfig {
useWidth?: number;
useMaxWidth?: boolean;
}
export interface C4DiagramConfig extends BaseDiagramConfig {
diagramMarginX?: number;
diagramMarginY?: number;
c4ShapeMargin?: number;
c4ShapePadding?: number;
width?: number;
height?: number;
boxMargin?: number;
c4ShapeInRow?: number;
nextLinePaddingX?: number;
c4BoundaryInRow?: number;
personFontSize?: string | number;
personFontFamily?: string;
personFontWeight?: string | number;
external_personFontSize?: string | number;
external_personFontFamily?: string;
external_personFontWeight?: string | number;
systemFontSize?: string | number;
systemFontFamily?: string;
systemFontWeight?: string | number;
external_systemFontSize?: string | number;
external_systemFontFamily?: string;
external_systemFontWeight?: string | number;
system_dbFontSize?: string | number;
system_dbFontFamily?: string;
system_dbFontWeight?: string | number;
external_system_dbFontSize?: string | number;
external_system_dbFontFamily?: string;
external_system_dbFontWeight?: string | number;
system_queueFontSize?: string | number;
system_queueFontFamily?: string;
system_queueFontWeight?: string | number;
external_system_queueFontSize?: string | number;
external_system_queueFontFamily?: string;
external_system_queueFontWeight?: string | number;
boundaryFontSize?: string | number;
boundaryFontFamily?: string;
boundaryFontWeight?: string | number;
messageFontSize?: string | number;
messageFontFamily?: string;
messageFontWeight?: string | number;
containerFontSize?: string | number;
containerFontFamily?: string;
containerFontWeight?: string | number;
external_containerFontSize?: string | number;
external_containerFontFamily?: string;
external_containerFontWeight?: string | number;
container_dbFontSize?: string | number;
container_dbFontFamily?: string;
container_dbFontWeight?: string | number;
external_container_dbFontSize?: string | number;
external_container_dbFontFamily?: string;
external_container_dbFontWeight?: string | number;
container_queueFontSize?: string | number;
container_queueFontFamily?: string;
container_queueFontWeight?: string | number;
external_container_queueFontSize?: string | number;
external_container_queueFontFamily?: string;
external_container_queueFontWeight?: string | number;
componentFontSize?: string | number;
componentFontFamily?: string;
componentFontWeight?: string | number;
external_componentFontSize?: string | number;
external_componentFontFamily?: string;
external_componentFontWeight?: string | number;
component_dbFontSize?: string | number;
component_dbFontFamily?: string;
component_dbFontWeight?: string | number;
external_component_dbFontSize?: string | number;
external_component_dbFontFamily?: string;
external_component_dbFontWeight?: string | number;
component_queueFontSize?: string | number;
component_queueFontFamily?: string;
component_queueFontWeight?: string | number;
external_component_queueFontSize?: string | number;
external_component_queueFontFamily?: string;
external_component_queueFontWeight?: string | number;
wrap?: boolean;
wrapPadding?: number;
person_bg_color?: string;
person_border_color?: string;
external_person_bg_color?: string;
external_person_border_color?: string;
system_bg_color?: string;
system_border_color?: string;
system_db_bg_color?: string;
system_db_border_color?: string;
system_queue_bg_color?: string;
system_queue_border_color?: string;
external_system_bg_color?: string;
external_system_border_color?: string;
external_system_db_bg_color?: string;
external_system_db_border_color?: string;
external_system_queue_bg_color?: string;
external_system_queue_border_color?: string;
container_bg_color?: string;
container_border_color?: string;
container_db_bg_color?: string;
container_db_border_color?: string;
container_queue_bg_color?: string;
container_queue_border_color?: string;
external_container_bg_color?: string;
external_container_border_color?: string;
external_container_db_bg_color?: string;
external_container_db_border_color?: string;
external_container_queue_bg_color?: string;
external_container_queue_border_color?: string;
component_bg_color?: string;
component_border_color?: string;
component_db_bg_color?: string;
component_db_border_color?: string;
component_queue_bg_color?: string;
component_queue_border_color?: string;
external_component_bg_color?: string;
external_component_border_color?: string;
external_component_db_bg_color?: string;
external_component_db_border_color?: string;
external_component_queue_bg_color?: string;
external_component_queue_border_color?: string;
personFont?: FontCalculator;
external_personFont?: FontCalculator;
systemFont?: FontCalculator;
external_systemFont?: FontCalculator;
system_dbFont?: FontCalculator;
external_system_dbFont?: FontCalculator;
system_queueFont?: FontCalculator;
external_system_queueFont?: FontCalculator;
containerFont?: FontCalculator;
external_containerFont?: FontCalculator;
container_dbFont?: FontCalculator;
external_container_dbFont?: FontCalculator;
container_queueFont?: FontCalculator;
external_container_queueFont?: FontCalculator;
componentFont?: FontCalculator;
external_componentFont?: FontCalculator;
component_dbFont?: FontCalculator;
external_component_dbFont?: FontCalculator;
component_queueFont?: FontCalculator;
external_component_queueFont?: FontCalculator;
boundaryFont?: FontCalculator;
messageFont?: FontCalculator;
}
export interface GitGraphDiagramConfig extends BaseDiagramConfig {
diagramPadding?: number;
nodeLabel?: NodeLabel;
mainBranchName?: string;
mainBranchOrder?: number;
showCommitLabel?: boolean;
showBranches?: boolean;
rotateCommitLabel?: boolean;
arrowMarkerAbsolute?: boolean;
}
export interface NodeLabel {
width?: number;
height?: number;
x?: number;
y?: number;
}
export interface RequirementDiagramConfig extends BaseDiagramConfig {
rect_fill?: string;
text_color?: string;
rect_border_size?: string;
rect_border_color?: string;
rect_min_width?: number;
rect_min_height?: number;
fontSize?: number;
rect_padding?: number;
line_height?: number;
}
export interface PieDiagramConfig extends BaseDiagramConfig {}
export interface ErDiagramConfig extends BaseDiagramConfig {
diagramPadding?: number;
layoutDirection?: string;
minEntityWidth?: number;
minEntityHeight?: number;
entityPadding?: number;
stroke?: string;
fill?: string;
fontSize?: number;
}
export interface StateDiagramConfig extends BaseDiagramConfig {
arrowMarkerAbsolute?: boolean;
dividerMargin?: number;
sizeUnit?: number;
padding?: number;
textHeight?: number;
titleShift?: number;
noteMargin?: number;
forkWidth?: number;
forkHeight?: number;
miniPadding?: number;
fontSizeFactor?: number;
fontSize?: number;
labelHeight?: number;
edgeLengthFactor?: string;
compositTitleSize?: number;
radius?: number;
defaultRenderer?: string;
}
export interface ClassDiagramConfig extends BaseDiagramConfig {
arrowMarkerAbsolute?: boolean;
dividerMargin?: number;
padding?: number;
textHeight?: number;
defaultRenderer?: string;
}
export interface JourneyDiagramConfig extends BaseDiagramConfig {
diagramMarginX?: number;
diagramMarginY?: number;
leftMargin?: number;
width?: number;
height?: number;
boxMargin?: number;
boxTextMargin?: number;
noteMargin?: number;
messageMargin?: number;
messageAlign?: string;
bottomMarginAdj?: number;
rightAngles?: boolean;
taskFontSize?: string | number;
taskFontFamily?: string;
taskMargin?: number;
activationWidth?: number;
textPlacement?: string;
actorColours?: string[];
sectionFills?: string[];
sectionColours?: string[];
}
export interface GanttDiagramConfig extends BaseDiagramConfig {
titleTopMargin?: number;
barHeight?: number;
barGap?: number;
topPadding?: number;
rightPadding?: number;
leftPadding?: number;
gridLineStartPadding?: number;
fontSize?: number;
sectionFontSize?: string | number;
numberSectionStyles?: number;
axisFormat?: string;
topAxis?: boolean;
}
export interface SequenceDiagramConfig extends BaseDiagramConfig {
arrowMarkerAbsolute?: boolean;
hideUnusedParticipants?: boolean;
activationWidth?: number;
diagramMarginX?: number;
diagramMarginY?: number;
actorMargin?: number;
width?: number;
height?: number;
boxMargin?: number;
boxTextMargin?: number;
noteMargin?: number;
messageMargin?: number;
messageAlign?: string;
mirrorActors?: boolean;
forceMenus?: boolean;
bottomMarginAdj?: number;
rightAngles?: boolean;
showSequenceNumbers?: boolean;
actorFontSize?: string | number;
actorFontFamily?: string;
actorFontWeight?: string | number;
noteFontSize?: string | number;
noteFontFamily?: string;
noteFontWeight?: string | number;
noteAlign?: string;
messageFontSize?: string | number;
messageFontFamily?: string;
messageFontWeight?: string | number;
wrap?: boolean;
wrapPadding?: number;
labelBoxWidth?: number;
labelBoxHeight?: number;
messageFont?: FontCalculator;
noteFont?: FontCalculator;
actorFont?: FontCalculator;
}
export interface FlowchartDiagramConfig extends BaseDiagramConfig {
arrowMarkerAbsolute?: boolean;
diagramPadding?: number;
htmlLabels?: boolean;
nodeSpacing?: number;
rankSpacing?: number;
curve?: string;
padding?: number;
defaultRenderer?: string;
}
export interface FontConfig {
fontSize?: string | number;
fontFamily?: string;
fontWeight?: string | number;
}
export type FontCalculator = () => Partial<FontConfig>;
export {};

View File

@@ -1,4 +1,5 @@
import theme from './themes';
import { MermaidConfig } from './config.type';
/**
* **Configuration methods in Mermaid version 8.6.0 have been updated, to learn more[[click
* here](8.6.0_docs.md)].**
@@ -21,7 +22,7 @@ import theme from './themes';
*
* @name Configuration
*/
const config = {
const config: Partial<MermaidConfig> = {
/**
* Theme , the CSS style sheet
*
@@ -49,12 +50,13 @@ const config = {
fontFamily: '"trebuchet ms", verdana, arial, sans-serif;',
/**
* | Parameter | Description | Type | Required | Values |
* | --------- | ----------------------------------------------------- | ---------------- | -------- | ------------- |
* | logLevel | This option decides the amount of logging to be used. | string \| number | Required | 1, 2, 3, 4, 5 |
* | Parameter | Description | Type | Required | Values |
* | --------- | ----------------------------------------------------- | ---------------- | -------- | --------------------------------------------- |
* | logLevel | This option decides the amount of logging to be used. | string \| number | Required | 'trace','debug','info','warn','error','fatal' |
*
* **Notes:**
*
* - Trace: 0
* - Debug: 1
* - Info: 2
* - Warn: 3
@@ -1823,11 +1825,11 @@ const config = {
},
};
config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
config.gitGraph.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
if (config.class) config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
if (config.gitGraph) config.gitGraph.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
const keyify = (obj, prefix = '') =>
Object.keys(obj).reduce((res, el) => {
const keyify = (obj: any, prefix = ''): string[] =>
Object.keys(obj).reduce((res: string[], el): string[] => {
if (Array.isArray(obj[el])) {
return res;
} else if (typeof obj[el] === 'object' && obj[el] !== null) {
@@ -1836,5 +1838,5 @@ const keyify = (obj, prefix = '') =>
return [...res, prefix + el];
}, []);
export const configKeys = keyify(config, '');
export const configKeys: string[] = keyify(config, '');
export default config;

View File

@@ -1,100 +0,0 @@
const directive =
/[%]{2}[{]\s*(?:(?:(\w+)\s*:|(\w+))\s*(?:(?:(\w+))|((?:(?![}][%]{2}).|\r?\n)*))?\s*)(?:[}][%]{2})?/gi;
const anyComment = /\s*%%.*\n/gm;
const detectors = {};
/**
* @function detectType Detects the type of the graph text. Takes into consideration the possible
* existence of an %%init directive
*
* ```mermaid
* %%{initialize: {"startOnLoad": true, logLevel: "fatal" }}%%
* graph LR
* a-->b
* b-->c
* c-->d
* d-->e
* e-->f
* f-->g
* g-->h
* ```
* @param {string} text The text defining the graph
* @param {{
* class: { defaultRenderer: string } | undefined;
* state: { defaultRenderer: string } | undefined;
* flowchart: { defaultRenderer: string } | undefined;
* }} [cnf]
* @returns {string} A graph definition key
*/
const detectType = function (text, cnf) {
text = text.replace(directive, '').replace(anyComment, '\n');
if (text.match(/^\s*C4Context|C4Container|C4Component|C4Dynamic|C4Deployment/)) {
return 'c4';
}
if (text.match(/^\s*sequenceDiagram/)) {
return 'sequence';
}
if (text.match(/^\s*gantt/)) {
return 'gantt';
}
if (text.match(/^\s*classDiagram-v2/)) {
return 'classDiagram';
}
if (text.match(/^\s*classDiagram/)) {
if (cnf && cnf.class && cnf.class.defaultRenderer === 'dagre-wrapper') return 'classDiagram';
return 'class';
}
if (text.match(/^\s*stateDiagram-v2/)) {
return 'stateDiagram';
}
if (text.match(/^\s*stateDiagram/)) {
if (cnf && cnf.class && cnf.state.defaultRenderer === 'dagre-wrapper') return 'stateDiagram';
return 'state';
}
// if (text.match(/^\s*gitGraph/)) {
// return 'gitGraph';
// }
if (text.match(/^\s*flowchart/)) {
return 'flowchart-v2';
}
if (text.match(/^\s*info/)) {
return 'info';
}
if (text.match(/^\s*pie/)) {
return 'pie';
}
if (text.match(/^\s*erDiagram/)) {
return 'er';
}
if (text.match(/^\s*journey/)) {
return 'journey';
}
if (text.match(/^\s*requirement/) || text.match(/^\s*requirementDiagram/)) {
return 'requirement';
}
if (cnf && cnf.flowchart && cnf.flowchart.defaultRenderer === 'dagre-wrapper')
return 'flowchart-v2';
const k = Object.keys(detectors);
for (let i = 0; i < k.length; i++) {
const key = k[i];
const dia = detectors[key];
if (dia && dia.detector(text)) {
return key;
}
}
return 'flowchart';
};
export const addDetector = (key, detector) => {
detectors[key] = {
detector,
};
};
export default detectType;

View File

@@ -0,0 +1,82 @@
import { MermaidConfig } from '../config.type';
export type DiagramDetector = (text: string) => boolean;
const directive =
/[%]{2}[{]\s*(?:(?:(\w+)\s*:|(\w+))\s*(?:(?:(\w+))|((?:(?![}][%]{2}).|\r?\n)*))?\s*)(?:[}][%]{2})?/gi;
const anyComment = /\s*%%.*\n/gm;
const detectors: Record<string, DiagramDetector> = {};
const diagramMatchers: Record<string, RegExp> = {
c4: /^\s*C4Context|C4Container|C4Component|C4Dynamic|C4Deployment/,
sequence: /^\s*sequenceDiagram/,
gantt: /^\s*gantt/,
classDiagram: /^\s*classDiagram-v2/,
stateDiagram: /^\s*stateDiagram-v2/,
'flowchart-v2': /^\s*flowchart/, // Might need to add |graph to fix #3391
info: /^\s*info/,
pie: /^\s*pie/,
er: /^\s*erDiagram/,
journey: /^\s*journey/,
// gitGraph: /^\s*gitGraph/,
requirement: /^\s*requirement(Diagram)?/,
};
/**
* @function detectType Detects the type of the graph text. Takes into consideration the possible
* existence of an %%init directive
*
* ```mermaid
* %%{initialize: {"startOnLoad": true, logLevel: "fatal" }}%%
* graph LR
* a-->b
* b-->c
* c-->d
* d-->e
* e-->f
* f-->g
* g-->h
* ```
* @param {string} text The text defining the graph
* @param {{
* class: { defaultRenderer: string } | undefined;
* state: { defaultRenderer: string } | undefined;
* flowchart: { defaultRenderer: string } | undefined;
* }} [config]
* @returns {string} A graph definition key
*/
export const detectType = function (text: string, config?: MermaidConfig): string {
text = text.replace(directive, '').replace(anyComment, '\n');
for (const [diagram, matcher] of Object.entries(diagramMatchers)) {
if (text.match(matcher)) {
return diagram;
}
}
if (text.match(/^\s*classDiagram/)) {
if (config?.class?.defaultRenderer === 'dagre-wrapper') return 'classDiagram';
return 'class';
}
if (text.match(/^\s*stateDiagram/)) {
if (config?.state?.defaultRenderer === 'dagre-wrapper') return 'stateDiagram';
return 'state';
}
if (config?.flowchart?.defaultRenderer === 'dagre-wrapper') {
return 'flowchart-v2';
}
for (const [key, detector] of Object.entries(detectors)) {
if (detector(text)) {
return key;
}
}
// TODO: #3391
// throw new Error(`No diagram type detected for text: ${text}`);
return 'flowchart';
};
export const addDetector = (key: string, detector: DiagramDetector) => {
detectors[key] = detector;
};

View File

@@ -1,13 +1,14 @@
import { registerDiagram } from './diagramAPI.js';
import { registerDiagram } from './diagramAPI';
// import mindmapDb from '../diagrams/mindmap/mindmapDb';
// import mindmapRenderer from '../diagrams/mindmap/mindmapRenderer';
// import mindmapParser from '../diagrams/mindmap/parser/mindmapDiagram';
// import mindmapDetector from '../diagrams/mindmap/mindmapDetector';
// import { mindmapDetector } from '../diagrams/mindmap/mindmapDetector';
import gitGraphDb from '../diagrams/git/gitGraphAst';
import gitGraphRenderer from '../diagrams/git/gitGraphRenderer';
// @ts-ignore
import gitGraphParser from '../diagrams/git/parser/gitGraph';
import gitGraphDetector from '../diagrams/git/gitGraphDetector';
import { gitGraphDetector } from '../diagrams/git/gitGraphDetector';
// Register mindmap and other built-in diagrams
// registerDiagram(
@@ -19,14 +20,10 @@ import gitGraphDetector from '../diagrams/git/gitGraphDetector';
// mindmapRenderer,
// mindmapDetector
// );
const addDiagrams = () => {
export const addDiagrams = () => {
registerDiagram(
'gitGraph',
gitGraphParser,
gitGraphDb,
gitGraphRenderer,
undefined,
{ parser: gitGraphParser, db: gitGraphDb, renderer: gitGraphRenderer },
gitGraphDetector
);
};
export default addDiagrams;

View File

@@ -0,0 +1,28 @@
import { detectType } from './detectType';
import { getDiagram, registerDiagram } from './diagramAPI';
describe('DiagramAPI', () => {
it('should return default diagrams', () => {
expect(getDiagram('sequence')).not.toBeNull();
});
it('should throw error if diagram is not defined', () => {
expect(() => getDiagram('loki')).toThrow();
});
it('should handle diagram registrations', () => {
expect(() => getDiagram('loki')).toThrow();
expect(() => detectType('loki diagram')).not.toThrow(); // TODO: #3391
registerDiagram(
'loki',
{
db: {},
parser: {},
renderer: {},
},
(text: string) => text.includes('loki')
);
expect(getDiagram('loki')).not.toBeNull();
expect(detectType('loki diagram')).toBe('loki');
});
});

View File

@@ -1,42 +1,62 @@
import c4Db from '../diagrams/c4/c4Db';
import c4Renderer from '../diagrams/c4/c4Renderer';
// @ts-ignore
import c4Parser from '../diagrams/c4/parser/c4Diagram';
import classDb from '../diagrams/class/classDb';
import classRenderer from '../diagrams/class/classRenderer';
import classRendererV2 from '../diagrams/class/classRenderer-v2';
// @ts-ignore
import classParser from '../diagrams/class/parser/classDiagram';
import erDb from '../diagrams/er/erDb';
import erRenderer from '../diagrams/er/erRenderer';
// @ts-ignore
import erParser from '../diagrams/er/parser/erDiagram';
import flowDb from '../diagrams/flowchart/flowDb';
import flowRenderer from '../diagrams/flowchart/flowRenderer';
import flowRendererV2 from '../diagrams/flowchart/flowRenderer-v2';
// @ts-ignore
import flowParser from '../diagrams/flowchart/parser/flow';
import ganttDb from '../diagrams/gantt/ganttDb';
import ganttRenderer from '../diagrams/gantt/ganttRenderer';
// @ts-ignore
import ganttParser from '../diagrams/gantt/parser/gantt';
import infoDb from '../diagrams/info/infoDb';
import infoRenderer from '../diagrams/info/infoRenderer';
// @ts-ignore
import infoParser from '../diagrams/info/parser/info';
// @ts-ignore
import pieParser from '../diagrams/pie/parser/pie';
import pieDb from '../diagrams/pie/pieDb';
import pieRenderer from '../diagrams/pie/pieRenderer';
// @ts-ignore
import requirementParser from '../diagrams/requirement/parser/requirementDiagram';
import requirementDb from '../diagrams/requirement/requirementDb';
import requirementRenderer from '../diagrams/requirement/requirementRenderer';
// @ts-ignore
import sequenceParser from '../diagrams/sequence/parser/sequenceDiagram';
import sequenceDb from '../diagrams/sequence/sequenceDb';
import sequenceRenderer from '../diagrams/sequence/sequenceRenderer';
// @ts-ignore
import stateParser from '../diagrams/state/parser/stateDiagram';
import stateDb from '../diagrams/state/stateDb';
import stateRenderer from '../diagrams/state/stateRenderer';
import stateRendererV2 from '../diagrams/state/stateRenderer-v2';
import journeyDb from '../diagrams/user-journey/journeyDb';
import journeyRenderer from '../diagrams/user-journey/journeyRenderer';
// @ts-ignore
import journeyParser from '../diagrams/user-journey/parser/journey';
import { addDetector } from './detectType';
import { addDetector, DiagramDetector } from './detectType';
import { log } from '../logger';
import { MermaidConfig } from '../config.type';
const diagrams = {
export interface DiagramDefinition {
db: any;
renderer: any;
parser: any;
init?: (config: MermaidConfig) => void;
}
const diagrams: Record<string, DiagramDefinition> = {
c4: {
db: c4Db,
renderer: c4Renderer,
@@ -50,6 +70,9 @@ const diagrams = {
renderer: classRenderer,
parser: classParser,
init: (cnf) => {
if (!cnf.class) {
cnf.class = {};
}
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
classDb.clear();
},
@@ -59,6 +82,9 @@ const diagrams = {
renderer: classRendererV2,
parser: classParser,
init: (cnf) => {
if (!cnf.class) {
cnf.class = {};
}
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
classDb.clear();
},
@@ -74,6 +100,9 @@ const diagrams = {
parser: flowParser,
init: (cnf) => {
flowRenderer.setConf(cnf.flowchart);
if (!cnf.flowchart) {
cnf.flowchart = {};
}
cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
flowDb.clear();
flowDb.setGen('gen-1');
@@ -85,6 +114,9 @@ const diagrams = {
parser: flowParser,
init: (cnf) => {
flowRendererV2.setConf(cnf.flowchart);
if (!cnf.flowchart) {
cnf.flowchart = {};
}
cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
flowDb.clear();
flowDb.setGen('gen-2');
@@ -94,15 +126,7 @@ const diagrams = {
db: ganttDb,
renderer: ganttRenderer,
parser: ganttParser,
init: (cnf) => {
ganttRenderer.setConf(cnf.gantt);
},
},
// git: {
// db: gitGraphAst,
// renderer: gitGraphRenderer,
// parser: gitGraphParser,
// },
info: {
db: infoDb,
renderer: infoRenderer,
@@ -123,11 +147,12 @@ const diagrams = {
renderer: sequenceRenderer,
parser: sequenceParser,
init: (cnf) => {
if (!cnf.sequence) {
cnf.sequence = {};
}
cnf.sequence.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
if (cnf.sequenceDiagram) {
// backwards compatibility
sequenceRenderer.setConf(Object.assign(cnf.sequence, cnf.sequenceDiagram));
console.error(
if ('sequenceDiagram' in cnf) {
throw new Error(
'`mermaid config.sequenceDiagram` has been renamed to `config.sequence`. Please update your mermaid config.'
);
}
@@ -140,7 +165,10 @@ const diagrams = {
renderer: stateRenderer,
parser: stateParser,
init: (cnf) => {
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
if (!cnf.state) {
cnf.state = {};
}
cnf.state.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
stateDb.clear();
},
},
@@ -149,7 +177,10 @@ const diagrams = {
renderer: stateRendererV2,
parser: stateParser,
init: (cnf) => {
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
if (!cnf.state) {
cnf.state = {};
}
cnf.state.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
stateDb.clear();
},
},
@@ -163,13 +194,22 @@ const diagrams = {
},
},
};
// console.log(sequenceDb);
export const registerDiagram = (id, parser, db, renderer, init, detector) => {
diagrams[id] = { parser, db, renderer, init };
export const registerDiagram = (
id: string,
diagram: DiagramDefinition,
detector: DiagramDetector
) => {
if (diagrams[id]) {
log.warn(`Diagram ${id} already registered.`);
}
diagrams[id] = diagram;
addDetector(id, detector);
};
export const getDiagrams = () => {
// console.log('diagrams', diagrams);
return diagrams;
export const getDiagram = (name: string): DiagramDefinition => {
if (name in diagrams) {
return diagrams[name];
}
throw new Error(`Diagram ${name} not found.`);
};

View File

@@ -347,10 +347,9 @@ const buildMethodDisplay = function (parsedText) {
};
const buildLegacyDisplay = function (text) {
// if for some reason we dont have any match, use old format to parse text
// if for some reason we don't have any match, use old format to parse text
let displayText = '';
let cssStyle = '';
let memberText = '';
let returnType = '';
let methodStart = text.indexOf('(');
let methodEnd = text.indexOf(')');
@@ -370,26 +369,27 @@ const buildLegacyDisplay = function (text) {
methodName = text.substring(1, methodStart).trim();
}
let parameters = text.substring(methodStart + 1, methodEnd);
let classifier = text.substring(methodEnd + 1, 1);
const parameters = text.substring(methodStart + 1, methodEnd);
const classifier = text.substring(methodEnd + 1, methodEnd + 2);
cssStyle = parseClassifier(classifier);
displayText = visibility + methodName + '(' + parseGenericTypes(parameters.trim()) + ')';
if (methodEnd < memberText.length) {
if (methodEnd <= text.length) {
returnType = text.substring(methodEnd + 2).trim();
if (returnType !== '') {
returnType = ' : ' + parseGenericTypes(returnType);
displayText += returnType;
}
} else {
// finally - if all else fails, just send the text back as written (other than parsing for generic types)
displayText = parseGenericTypes(text);
}
} else {
// finally - if all else fails, just send the text back as written (other than parsing for generic types)
displayText = parseGenericTypes(text);
}
return {
displayText: displayText,
cssStyle: cssStyle,
displayText,
cssStyle,
};
};

View File

@@ -137,6 +137,14 @@ describe('class member Renderer, ', function () {
expect(actual.displayText).toBe('+foo(List<int> ids) : List<Item>');
expect(actual.cssStyle).toBe('font-style:italic;');
});
it('should handle method declaration with nested markup', function () {
const str = '+foo ( List~List~int~~ ids )* List~List~Item~~';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('+foo(List<List<int>> ids) : List<List<Item>>');
expect(actual.cssStyle).toBe('font-style:italic;');
});
});
describe('when parsing text to build field display string', function () {

View File

@@ -1,219 +0,0 @@
import DOMPurify from 'dompurify';
/**
* Gets the number of lines in a string
*
* @param {string | undefined} s The string to check the lines for
* @returns {number} The number of lines in that string
*/
export const getRows = (s) => {
if (!s) return 1;
let str = breakToPlaceholder(s);
str = str.replace(/\\n/g, '#br#');
return str.split('#br#');
};
export const removeEscapes = (text) => {
let newStr = text.replace(/\\u[\dA-F]{4}/gi, function (match) {
return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16));
});
newStr = newStr.replace(/\\x([0-9a-f]{2})/gi, (_, c) => String.fromCharCode(parseInt(c, 16)));
newStr = newStr.replace(/\\[\d\d\d]{3}/gi, function (match) {
return String.fromCharCode(parseInt(match.replace(/\\/g, ''), 8));
});
newStr = newStr.replace(/\\[\d\d\d]{2}/gi, function (match) {
return String.fromCharCode(parseInt(match.replace(/\\/g, ''), 8));
});
return newStr;
};
/**
* Removes script tags from a text
*
* @param {string} txt The text to sanitize
* @returns {string} The safer text
*/
export const removeScript = (txt) => {
var rs = '';
var idx = 0;
while (idx >= 0) {
idx = txt.indexOf('<script');
if (idx >= 0) {
rs += txt.substr(0, idx);
txt = txt.substr(idx + 1);
idx = txt.indexOf('</script>');
if (idx >= 0) {
idx += 9;
txt = txt.substr(idx);
}
} else {
rs += txt;
idx = -1;
break;
}
}
let decodedText = removeEscapes(rs);
decodedText = decodedText.replaceAll(/script>/gi, '#');
decodedText = decodedText.replaceAll(/javascript:/gi, '#');
decodedText = decodedText.replaceAll(/javascript&colon/gi, '#');
decodedText = decodedText.replaceAll(/onerror=/gi, 'onerror:');
decodedText = decodedText.replaceAll(/<iframe/gi, '');
return decodedText;
};
const sanitizeMore = (text, config) => {
let txt = text;
let htmlLabels = true;
if (
config.flowchart &&
(config.flowchart.htmlLabels === false || config.flowchart.htmlLabels === 'false')
) {
htmlLabels = false;
}
if (htmlLabels) {
const level = config.securityLevel;
if (level === 'antiscript' || level === 'strict') {
txt = removeScript(txt);
} else if (level !== 'loose') {
// eslint-disable-line
txt = breakToPlaceholder(txt);
txt = txt.replace(/</g, '&lt;').replace(/>/g, '&gt;');
txt = txt.replace(/=/g, '&equals;');
txt = placeholderToBreak(txt);
}
}
return txt;
};
export const sanitizeText = (text, config) => {
if (!text) return text;
let txt = '';
if (config['dompurifyConfig']) {
txt = DOMPurify.sanitize(sanitizeMore(text, config), config['dompurifyConfig']);
} else {
txt = DOMPurify.sanitize(sanitizeMore(text, config));
}
return txt;
};
export const sanitizeTextOrArray = (a, config) => {
if (typeof a === 'string') return sanitizeText(a, config);
const f = (x) => sanitizeText(x, config);
return a.flat().map(f);
};
export const lineBreakRegex = /<br\s*\/?>/gi;
/**
* Whether or not a text has any linebreaks
*
* @param {string} text The text to test
* @returns {boolean} Whether or not the text has breaks
*/
export const hasBreaks = (text) => {
return lineBreakRegex.test(text);
};
/**
* Splits on <br> tags
*
* @param {string} text Text to split
* @returns {string[]} List of lines as strings
*/
export const splitBreaks = (text) => {
return text.split(lineBreakRegex);
};
/**
* Converts placeholders to linebreaks in HTML
*
* @param {string} s HTML with placeholders
* @returns {string} HTML with breaks instead of placeholders
*/
const placeholderToBreak = (s) => {
return s.replace(/#br#/g, '<br/>');
};
/**
* Opposite of `placeholderToBreak`, converts breaks to placeholders
*
* @param {string} s HTML string
* @returns {string} String with placeholders
*/
const breakToPlaceholder = (s) => {
return s.replace(lineBreakRegex, '#br#');
};
/**
* Gets the current URL
*
* @param {boolean} useAbsolute Whether to return the absolute URL or not
* @returns {string} The current URL
*/
const getUrl = (useAbsolute) => {
let url = '';
if (useAbsolute) {
url =
window.location.protocol +
'//' +
window.location.host +
window.location.pathname +
window.location.search;
url = url.replace(/\(/g, '\\(');
url = url.replace(/\)/g, '\\)');
}
return url;
};
/**
* Converts a string/boolean into a boolean
*
* @param {string | boolean} val String or boolean to convert
* @returns {boolean} The result from the input
*/
export const evaluate = (val) => (val === 'false' || val === false ? false : true);
/**
* Makes generics in typescript syntax
*
* @example <caption>Array of array of strings in typescript syntax</caption>
* // returns "Array<Array<string>>"
* parseGenericTypes('Array~Array~string~~');
*
* @param {string} text The text to convert
* @returns {string} The converted string
*/
export const parseGenericTypes = function (text) {
let cleanedText = text;
if (text.indexOf('~') != -1) {
cleanedText = cleanedText.replace('~', '<');
cleanedText = cleanedText.replace('~', '>');
return parseGenericTypes(cleanedText);
} else {
return cleanedText;
}
};
export default {
getRows,
sanitizeText,
sanitizeTextOrArray,
hasBreaks,
splitBreaks,
lineBreakRegex,
removeScript,
getUrl,
evaluate,
removeEscapes,
};

View File

@@ -1,4 +1,4 @@
import { sanitizeText, removeScript, removeEscapes, parseGenericTypes } from './common';
import { sanitizeText, removeScript, parseGenericTypes } from './common';
describe('when securityLevel is antiscript, all script must be removed', function () {
/**
@@ -6,7 +6,7 @@ describe('when securityLevel is antiscript, all script must be removed', functio
* @param {string} result The expected sanitized text
*/
function compareRemoveScript(original, result) {
expect(removeScript(original)).toEqual(result);
expect(removeScript(original).trim()).toEqual(result);
}
it('should remove all script block, script inline.', function () {
@@ -29,70 +29,24 @@ describe('when securityLevel is antiscript, all script must be removed', functio
compareRemoveScript(
`This is a <a href="javascript:runHijackingScript();">clean link</a> + <a href="javascript:runHijackingScript();">clean link</a>
and <a href="javascript&colon;bipassedMining();">me too</a>`,
`This is a <a href="#runHijackingScript();">clean link</a> + <a href="#runHijackingScript();">clean link</a>
and <a href="#;bipassedMining();">me too</a>`
`This is a <a>clean link</a> + <a>clean link</a>
and <a>me too</a>`
);
});
it('should detect malicious images', function () {
compareRemoveScript(`<img onerror="alert('hello');">`, `<img onerror:"alert('hello');">`);
compareRemoveScript(`<img onerror="alert('hello');">`, `<img>`);
});
it('should detect iframes', function () {
compareRemoveScript(
`<iframe src="http://abc.com/script1.js"></iframe>
<iframe src="http://example.com/iframeexample"></iframe>`,
` src="http://abc.com/script1.js"></iframe>
src="http://example.com/iframeexample"></iframe>`
''
);
});
});
describe('remove escape code in text', function () {
it('should remove a unicode colon', function () {
const labelString = '\\u003A';
const result = removeEscapes(labelString);
expect(result).toEqual(':');
});
it('should remove a hex colon', function () {
const labelString = '\\x3A';
const result = removeEscapes(labelString);
expect(result).toEqual(':');
});
it('should remove a oct colon', function () {
const labelString = '\\72';
const result = removeEscapes(labelString);
expect(result).toEqual(':');
});
it('should remove a oct colon 3 numbers', function () {
const labelString = '\\072';
const result = removeEscapes(labelString);
expect(result).toEqual(':');
});
it('should remove multiple colons 3 numbers', function () {
const labelString = '\\072\\072\\72';
const result = removeEscapes(labelString);
expect(result).toEqual(':::');
});
it('should handle greater and smaller then', function () {
const labelString = '\\74\\076';
const result = removeEscapes(labelString);
expect(result).toEqual('<>');
});
it('should handle letters', function () {
const labelString = '\\u0073\\143ri\\x70\\u0074\\x3A';
const result = removeEscapes(labelString);
expect(result).toEqual('script:');
});
});
describe('Sanitize text', function () {
it('should remove script tag', function () {
const maliciousStr = 'javajavascript:script:alert(1)';
@@ -106,7 +60,13 @@ describe('Sanitize text', function () {
describe('generic parser', function () {
it('should parse generic types', function () {
const result = parseGenericTypes('test~T~');
expect(result).toEqual('test<T>');
expect(parseGenericTypes('test~T~')).toEqual('test<T>');
expect(parseGenericTypes('test~Array~Array~string~~~')).toEqual('test<Array<Array<string>>>');
expect(parseGenericTypes('test~Array~Array~string[]~~~')).toEqual(
'test<Array<Array<string[]>>>'
);
expect(parseGenericTypes('test ~Array~Array~string[]~~~')).toEqual(
'test <Array<Array<string[]>>>'
);
});
});

View File

@@ -0,0 +1,166 @@
import DOMPurify from 'dompurify';
import { MermaidConfig } from '../../config.type';
/**
* Gets the rows of lines in a string
*
* @param {string | undefined} s The string to check the lines for
* @returns {string[]} The rows in that string
*/
export const getRows = (s?: string): string[] => {
if (!s) return [''];
const str = breakToPlaceholder(s).replace(/\\n/g, '#br#');
return str.split('#br#');
};
/**
* Removes script tags from a text
*
* @param {string} txt The text to sanitize
* @returns {string} The safer text
*/
export const removeScript = (txt: string): string => {
return DOMPurify.sanitize(txt);
};
const sanitizeMore = (text: string, config: MermaidConfig) => {
if (config.flowchart?.htmlLabels !== false) {
const level = config.securityLevel;
if (level === 'antiscript' || level === 'strict') {
text = removeScript(text);
} else if (level !== 'loose') {
text = breakToPlaceholder(text);
text = text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
text = text.replace(/=/g, '&equals;');
text = placeholderToBreak(text);
}
}
return text;
};
export const sanitizeText = (text: string, config: MermaidConfig): string => {
if (!text) return text;
if (config.dompurifyConfig) {
text = DOMPurify.sanitize(sanitizeMore(text, config), config.dompurifyConfig).toString();
} else {
text = DOMPurify.sanitize(sanitizeMore(text, config));
}
return text;
};
export const sanitizeTextOrArray = (
a: string | string[] | string[][],
config: MermaidConfig
): string | string[] => {
if (typeof a === 'string') return sanitizeText(a, config);
// TODO: Refactor to avoid flat.
return a.flat().map((x: string) => sanitizeText(x, config));
};
export const lineBreakRegex = /<br\s*\/?>/gi;
/**
* Whether or not a text has any linebreaks
*
* @param {string} text The text to test
* @returns {boolean} Whether or not the text has breaks
*/
export const hasBreaks = (text: string): boolean => {
return lineBreakRegex.test(text);
};
/**
* Splits on <br> tags
*
* @param {string} text Text to split
* @returns {string[]} List of lines as strings
*/
export const splitBreaks = (text: string): string[] => {
return text.split(lineBreakRegex);
};
/**
* Converts placeholders to linebreaks in HTML
*
* @param {string} s HTML with placeholders
* @returns {string} HTML with breaks instead of placeholders
*/
const placeholderToBreak = (s: string): string => {
return s.replace(/#br#/g, '<br/>');
};
/**
* Opposite of `placeholderToBreak`, converts breaks to placeholders
*
* @param {string} s HTML string
* @returns {string} String with placeholders
*/
const breakToPlaceholder = (s: string): string => {
return s.replace(lineBreakRegex, '#br#');
};
/**
* Gets the current URL
*
* @param {boolean} useAbsolute Whether to return the absolute URL or not
* @returns {string} The current URL
*/
const getUrl = (useAbsolute: boolean): string => {
let url = '';
if (useAbsolute) {
url =
window.location.protocol +
'//' +
window.location.host +
window.location.pathname +
window.location.search;
url = url.replaceAll(/\(/g, '\\(');
url = url.replaceAll(/\)/g, '\\)');
}
return url;
};
/**
* Converts a string/boolean into a boolean
*
* @param {string | boolean} val String or boolean to convert
* @returns {boolean} The result from the input
*/
export const evaluate = (val?: string | boolean): boolean =>
val === false || ['false', 'null', '0'].includes(String(val).trim().toLowerCase()) ? false : true;
/**
* Makes generics in typescript syntax
*
* @example <caption>Array of array of strings in typescript syntax</caption>
* // returns "Array<Array<string>>"
* parseGenericTypes('Array~Array~string~~');
*
* @param {string} text The text to convert
* @returns {string} The converted string
*/
export const parseGenericTypes = function (text: string): string {
let cleanedText = text;
if (text.indexOf('~') !== -1) {
cleanedText = cleanedText.replace(/~([^~].*)/, '<$1');
cleanedText = cleanedText.replace(/~([^~]*)$/, '>$1');
return parseGenericTypes(cleanedText);
} else {
return cleanedText;
}
};
export default {
getRows,
sanitizeText,
sanitizeTextOrArray,
hasBreaks,
splitBreaks,
lineBreakRegex,
removeScript,
getUrl,
evaluate,
};

View File

@@ -425,7 +425,7 @@ funs.push(setupToolTips);
*
* @param ver
*/
export const clear = function (ver) {
export const clear = function (ver = 'gen-1') {
vertices = {};
classes = {};
edges = [];
@@ -436,7 +436,7 @@ export const clear = function (ver) {
subCount = 0;
tooltips = [];
firstGraphFlag = true;
version = ver || 'gen-1';
version = ver;
commonClear();
};
export const setGen = (ver) => {

View File

@@ -2,7 +2,6 @@ import graphlib from 'graphlib';
import { select, curveLinear, selectAll } from 'd3';
import flowDb from './flowDb';
import flow from './parser/flow';
import { getConfig } from '../../config';
import { render } from '../../dagre-wrapper/index.js';
@@ -363,11 +362,10 @@ export const draw = function (text, id, _version, diagObj) {
dir = 'TD';
}
const conf = getConfig().flowchart;
const { securityLevel, flowchart: conf } = getConfig();
const nodeSpacing = conf.nodeSpacing || 50;
const rankSpacing = conf.rankSpacing || 50;
const securityLevel = getConfig().securityLevel;
// Handle root and document for when rendering in sandbox mode
let sandboxElement;
if (securityLevel === 'sandbox') {

View File

@@ -296,7 +296,7 @@ export const getClasses = function (text, diagObj) {
export const draw = function (text, id, _version, diagObj) {
log.info('Drawing flowchart');
diagObj.db.clear();
const securityLevel = getConfig().securityLevel;
const { securityLevel, flowchart: conf } = getConfig();
let sandboxElement;
if (securityLevel === 'sandbox') {
sandboxElement = select('#i' + id);
@@ -319,8 +319,6 @@ export const draw = function (text, id, _version, diagObj) {
if (typeof dir === 'undefined') {
dir = 'TD';
}
const conf = getConfig().flowchart;
const nodeSpacing = conf.nodeSpacing || 50;
const rankSpacing = conf.rankSpacing || 50;
@@ -460,7 +458,7 @@ export const draw = function (text, id, _version, diagObj) {
}
// Add label rects for non html labels
if (!evaluate(conf.htmlLabels) || true) { // eslint-disable-line
if (!conf.htmlLabels) {
const labels = doc.querySelectorAll('[id="' + id + '"] .edgeLabel .label');
for (let k = 0; k < labels.length; k++) {
const label = labels[k];

View File

@@ -1,25 +1,21 @@
/**
* Returns the styles given options
*
* @param {{
* fontFamily: string;
* nodeTextColor: string;
* textColor: string;
* titleColor: string;
* mainBkg: string;
* nodeBorder: string;
* arrowheadColor: string;
* lineColor: string;
* edgeLabelBackground: string;
* clusterBkg: string;
* clusterBorder: string;
* tertiaryColor: string;
* border2: string;
* }} options
* The options for the styles
* @returns {string} The resulting styles
*/
const getStyles = (options) =>
/** Returns the styles given options */
export interface FlowChartStyleOptions {
arrowheadColor: string;
border2: string;
clusterBkg: string;
clusterBorder: string;
edgeLabelBackground: string;
fontFamily: string;
lineColor: string;
mainBkg: string;
nodeBorder: string;
nodeTextColor: string;
tertiaryColor: string;
textColor: string;
titleColor: string;
}
const getStyles = (options: FlowChartStyleOptions) =>
`.label {
font-family: ${options.fontFamily};
color: ${options.nodeTextColor || options.textColor};

View File

@@ -230,31 +230,31 @@ const getStartDate = function (prevTime, dateFormat, str) {
return new Date();
};
const durationToDate = function (durationStatement, relativeTime) {
if (durationStatement !== null) {
switch (durationStatement[2]) {
case 'ms':
relativeTime.add(durationStatement[1], 'milliseconds');
break;
case 's':
relativeTime.add(durationStatement[1], 'seconds');
break;
case 'm':
relativeTime.add(durationStatement[1], 'minutes');
break;
case 'h':
relativeTime.add(durationStatement[1], 'hours');
break;
case 'd':
relativeTime.add(durationStatement[1], 'days');
break;
case 'w':
relativeTime.add(durationStatement[1], 'weeks');
break;
}
/**
* Parse a string as a moment duration.
*
* The string have to be compound by a value and a shorthand duration unit. For example `5d`
* representes 5 days.
*
* Shorthand unit supported are:
*
* - `y` for years
* - `M` for months
* - `w` for weeks
* - `d` for days
* - `h` for hours
* - `s` for seconds
* - `ms` for milliseconds
*
* @param {string} str - A string representing the duration.
* @returns {moment.Duration} A moment duration, including an invalid moment for invalid input string.
*/
const parseDuration = function (str) {
const statement = /^(\d+(?:\.\d+)?)([yMwdhms]|ms)$/.exec(str.trim());
if (statement !== null) {
return moment.duration(Number.parseFloat(statement[1]), statement[2]);
}
// Default date - now
return relativeTime.toDate();
return moment.duration.invalid();
};
const getEndDate = function (prevTime, dateFormat, str, inclusive) {
@@ -270,7 +270,12 @@ const getEndDate = function (prevTime, dateFormat, str, inclusive) {
return mDate.toDate();
}
return durationToDate(/^([\d]+)([wdhms]|ms)$/.exec(str.trim()), moment(prevTime));
const endTime = moment(prevTime);
const duration = parseDuration(str);
if (duration.isValid()) {
endTime.add(duration);
}
return endTime.toDate();
};
let taskCnt = 0;
@@ -666,7 +671,7 @@ export default {
setLink,
getLinks,
bindFunctions,
durationToDate,
parseDuration,
isInvalidDate,
};

View File

@@ -6,13 +6,16 @@ describe('when using the ganttDb', function () {
ganttDb.clear();
});
describe('when using relative times', function () {
describe('when using duration', function () {
it.each`
diff | date | expected
${' 1d'} | ${moment('2019-01-01')} | ${moment('2019-01-02').toDate()}
${' 1w'} | ${moment('2019-01-01')} | ${moment('2019-01-08').toDate()}
`('should add $diff to $date resulting in $expected', ({ diff, date, expected }) => {
expect(ganttDb.durationToDate(diff, date)).toEqual(expected);
str | expected
${'1d'} | ${moment.duration(1, 'd')}
${'2w'} | ${moment.duration(2, 'w')}
${'1ms'} | ${moment.duration(1, 'ms')}
${'0.1s'} | ${moment.duration(100, 'ms')}
${'1f'} | ${moment.duration.invalid()}
`('should $str resulting in $expected duration', ({ str, expected }) => {
expect(ganttDb.parseDuration(str)).toEqual(expected);
});
});
@@ -106,7 +109,7 @@ describe('when using the ganttDb', function () {
ganttDb.addTask('test2', 'id2,after id1,5ms');
ganttDb.addSection('testa2');
ganttDb.addTask('test3', 'id3,20,10ms');
ganttDb.addTask('test4', 'id4,after id3,5ms');
ganttDb.addTask('test4', 'id4,after id3,0.005s');
const tasks = ganttDb.getTasks();

View File

@@ -148,16 +148,9 @@ export const branch = function (name, order) {
}
};
/**
* Creates a merge commit.
*
* @param {string} otherBranch - Target branch to merge to.
* @param {string} [tag] - Git tag to use on this merge commit.
* @param {string} [id] - Git commit id.
*/
export const merge = function (otherBranch, tag, id) {
export const merge = function (otherBranch, custom_id, override_type, custom_tag) {
otherBranch = common.sanitizeText(otherBranch, configApi.getConfig());
id = common.sanitizeText(id, configApi.getConfig());
custom_id = common.sanitizeText(custom_id, configApi.getConfig());
const currentCommit = commits[branches[curBranch]];
const otherCommit = commits[branches[otherBranch]];
@@ -216,6 +209,23 @@ export const merge = function (otherBranch, tag, id) {
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
expected: ['branch abc'],
};
throw error;
} else if (custom_id && typeof commits[custom_id] !== 'undefined') {
let error = new Error(
'Incorrect usage of "merge". Commit with id:' +
custom_id +
' already exists, use different custom Id'
);
error.hash = {
text: 'merge ' + otherBranch + custom_id + override_type + custom_tag,
token: 'merge ' + otherBranch + custom_id + override_type + custom_tag,
line: '1',
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
expected: [
'merge ' + otherBranch + ' ' + custom_id + '_UNIQUE ' + override_type + ' ' + custom_tag,
],
};
throw error;
}
// if (isReachableFrom(currentCommit, otherCommit)) {
@@ -228,13 +238,15 @@ export const merge = function (otherBranch, tag, id) {
// } else {
// create merge commit
const commit = {
id: id || seq + '-' + getId(),
id: custom_id ? custom_id : seq + '-' + getId(),
message: 'merged branch ' + otherBranch + ' into ' + curBranch,
seq: seq++,
parents: [head == null ? null : head.id, branches[otherBranch]],
branch: curBranch,
type: commitType.MERGE,
tag: tag ? tag : '',
customType: override_type,
customId: custom_id ? true : false,
tag: custom_tag ? custom_tag : '',
};
head = commit;
commits[commit.id] = commit;

View File

@@ -1,8 +0,0 @@
const detector = (txt) => {
if (txt.match(/^\s*gitGraph/)) {
return 'gitGraph';
}
return null;
};
export default detector;

View File

@@ -0,0 +1,5 @@
import type { DiagramDetector } from '../../diagram-api/detectType';
export const gitGraphDetector: DiagramDetector = (txt) => {
return txt.match(/^\s*gitGraph/) !== null;
};

View File

@@ -4,7 +4,6 @@ import gitGraphAst from './gitGraphAst';
import { parser } from './parser/gitGraph';
//import randomString from 'crypto-random-string';
//import cryptoRandomString from 'crypto-random-string';
import { logger } from '../../logger';
//jest.mock('crypto-random-string');
@@ -496,7 +495,7 @@ describe('when parsing a gitGraph', function () {
]);
});
it('should handle merge ids', function () {
it('should handle merge with custom ids, tags and typr', function () {
const str = `gitGraph:
commit
branch testBranch
@@ -510,7 +509,7 @@ describe('when parsing a gitGraph', function () {
commit
checkout main
%% Merge ID and Tag (reverse order)
merge testBranch2 id: "4-444" tag: "merge-tag2"
merge testBranch2 id: "4-444" tag: "merge-tag2" type:HIGHLIGHT
branch testBranch3
checkout testBranch3
commit
@@ -553,6 +552,8 @@ describe('when parsing a gitGraph', function () {
expect(testBranch2Merge.parents).toStrictEqual([testBranchMerge.id, testBranch2Commit.id]);
expect(testBranch2Merge.tag).toBe('merge-tag2');
expect(testBranch2Merge.id).toBe('4-444');
expect(testBranch2Merge.customType).toBe(2);
expect(testBranch2Merge.customId).toBe(true);
expect(testBranch3Merge.branch).toBe('main');
expect(testBranch3Merge.parents).toStrictEqual([testBranch2Merge.id, testBranch3Commit.id]);
@@ -687,6 +688,27 @@ describe('when parsing a gitGraph', function () {
expect(e.message).toBe('Incorrect usage of "merge". Cannot merge a branch to itself');
}
});
it('should throw error when using existing id as merge ID', function () {
const str = `gitGraph
commit id: "1-111"
branch testBranch
commit id: "2-222"
commit id: "3-333"
checkout main
merge testBranch id: "1-111"
`;
try {
parser.parse(str);
// Fail test if above expression doesn't throw anything.
expect(true).toBe(false);
} catch (e) {
expect(e.message).toBe(
'Incorrect usage of "merge". Commit with id:1-111 already exists, use different custom Id'
);
}
});
it('should throw error when trying to merge branches having same heads', function () {
const str = `gitGraph
commit

View File

@@ -91,7 +91,9 @@ const drawCommits = (svg, commits, modifyGraph) => {
// Don't draw the commits now but calculate the positioning which is used by the branch lines etc.
if (modifyGraph) {
let typeClass;
switch (commit.type) {
let commitSymbolType =
typeof commit.customType !== 'undefined' ? commit.customType : commit.type;
switch (commitSymbolType) {
case commitType.NORMAL:
typeClass = 'commit-normal';
break;
@@ -111,7 +113,7 @@ const drawCommits = (svg, commits, modifyGraph) => {
typeClass = 'commit-normal';
}
if (commit.type === commitType.HIGHLIGHT) {
if (commitSymbolType === commitType.HIGHLIGHT) {
const circle = gBullets.append('rect');
circle.attr('x', x - 10);
circle.attr('y', y - 10);
@@ -135,7 +137,7 @@ const drawCommits = (svg, commits, modifyGraph) => {
branchPos[commit.branch].index % THEME_COLOR_LIMIT
} ${typeClass}-inner`
);
} else if (commit.type === commitType.CHERRY_PICK) {
} else if (commitSymbolType === commitType.CHERRY_PICK) {
gBullets
.append('circle')
.attr('cx', x)
@@ -181,7 +183,7 @@ const drawCommits = (svg, commits, modifyGraph) => {
'class',
`commit ${commit.id} commit${branchPos[commit.branch].index % THEME_COLOR_LIMIT}`
);
if (commit.type === commitType.MERGE) {
if (commitSymbolType === commitType.MERGE) {
const circle2 = gBullets.append('circle');
circle2.attr('cx', x);
circle2.attr('cy', y);
@@ -193,7 +195,7 @@ const drawCommits = (svg, commits, modifyGraph) => {
}`
);
}
if (commit.type === commitType.REVERSE) {
if (commitSymbolType === commitType.REVERSE) {
const cross = gBullets.append('path');
cross
.attr('d', `M ${x - 5},${y - 5}L${x + 5},${y + 5}M${x - 5},${y + 5}L${x + 5},${y - 5}`)
@@ -215,7 +217,12 @@ const drawCommits = (svg, commits, modifyGraph) => {
const px = 4;
const py = 2;
// Draw the commit label
if (commit.type !== commitType.CHERRY_PICK && gitGraphConfig.showCommitLabel) {
if (
commit.type !== commitType.CHERRY_PICK &&
((commit.customId && commit.type === commitType.MERGE) ||
commit.type !== commitType.MERGE) &&
gitGraphConfig.showCommitLabel
) {
const wrapper = gLabels.append('g');
const labelBkg = wrapper.insert('rect').attr('class', 'commit-label-bkg');
@@ -336,7 +343,7 @@ const findLane = (y1, y2, _depth) => {
return candidate;
}
const diff = Math.abs(y1 - y2);
return findLane(y1, y2 - diff / 5, depth);
return findLane(y1, y2 - diff / 5, depth + 1);
};
/**

View File

@@ -121,11 +121,22 @@ cherryPickStatement
;
mergeStatement
: MERGE ID {yy.merge($2)}
| MERGE ID COMMIT_TAG STR {yy.merge($2, $4)}
| MERGE ID COMMIT_ID STR {yy.merge($2, '', $4)}
| MERGE ID COMMIT_TAG STR COMMIT_ID STR {yy.merge($2, $4, $6)}
| MERGE ID COMMIT_ID STR COMMIT_TAG STR {yy.merge($2, $6, $4)}
: MERGE ID {yy.merge($2,'','','')}
| MERGE ID COMMIT_ID STR {yy.merge($2, $4,'','')}
| MERGE ID COMMIT_TYPE commitType {yy.merge($2,'', $4,'')}
| MERGE ID COMMIT_TAG STR {yy.merge($2, '','',$4)}
| MERGE ID COMMIT_TAG STR COMMIT_ID STR {yy.merge($2, $6,'', $4)}
| MERGE ID COMMIT_TAG STR COMMIT_TYPE commitType {yy.merge($2, '',$6, $4)}
| MERGE ID COMMIT_TYPE commitType COMMIT_TAG STR {yy.merge($2, '',$4, $6)}
| MERGE ID COMMIT_ID STR COMMIT_TYPE commitType {yy.merge($2, $4, $6, '')}
| MERGE ID COMMIT_ID STR COMMIT_TAG STR {yy.merge($2, $4, '', $6)}
| MERGE ID COMMIT_TYPE commitType COMMIT_ID STR {yy.merge($2, $6,$4, '')}
| MERGE ID COMMIT_ID STR COMMIT_TYPE commitType COMMIT_TAG STR {yy.merge($2, $4, $6, $8)}
| MERGE ID COMMIT_TYPE commitType COMMIT_TAG STR COMMIT_ID STR {yy.merge($2, $8, $4, $6)}
| MERGE ID COMMIT_ID STR COMMIT_TAG STR COMMIT_TYPE commitType {yy.merge($2, $4, $8, $6)}
| MERGE ID COMMIT_TYPE commitType COMMIT_ID STR COMMIT_TAG STR {yy.merge($2, $6, $4, $8)}
| MERGE ID COMMIT_TAG STR COMMIT_TYPE commitType COMMIT_ID STR {yy.merge($2, $8, $6, $4)}
| MERGE ID COMMIT_TAG STR COMMIT_ID STR COMMIT_TYPE commitType {yy.merge($2, $6, $8, $4)}
;
commitStatement

View File

@@ -15,7 +15,7 @@ export const draw = (text, id, version, diagObj) => {
try {
// const parser = infoParser.parser;
// parser.yy = db;
log.debug('Renering info diagram\n' + text);
log.debug('Rendering info diagram\n' + text);
const securityLevel = getConfig().securityLevel;
// Handle root and Document for when rendering in sanbox mode
@@ -49,6 +49,7 @@ export const draw = (text, id, version, diagObj) => {
svg.attr('width', 400);
// svg.attr('viewBox', '0 0 300 150');
} catch (e) {
console.error(e);
log.error('Error while rendering info diagram');
log.error(e.message);
}

View File

@@ -1,8 +0,0 @@
const detector = (txt) => {
if (txt.match(/^\s*mindmap/)) {
return 'mindmap';
}
return null;
};
export default detector;

View File

@@ -0,0 +1,5 @@
import { DiagramDetector } from '../../diagram-api/detectType';
export const mindmapDetector: DiagramDetector = (txt) => {
return txt.match(/^\s*mindmap/) !== null;
};

View File

@@ -588,8 +588,8 @@ function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoop
* @param {any} diagObj A stanard diagram containing the db and the text and type etc of the diagram
*/
export const draw = function (_text, id, _version, diagObj) {
conf = configApi.getConfig().sequence;
const securityLevel = configApi.getConfig().securityLevel;
const { securityLevel, sequence } = configApi.getConfig();
conf = sequence;
// Handle root and Document for when rendering in sanbox mode
let sandboxElement;
if (securityLevel === 'sandbox') {

View File

@@ -248,12 +248,10 @@ export const draw = function (text, id, _version, diag) {
dir = 'LR';
}
const conf = getConfig().state;
const { securityLevel, state: conf } = getConfig();
const nodeSpacing = conf.nodeSpacing || 50;
const rankSpacing = conf.rankSpacing || 50;
const securityLevel = getConfig().securityLevel;
log.info(diag.db.getRootDocV2());
diag.db.extract(diag.db.getRootDocV2());
log.info(diag.db.getRootDocV2());

View File

@@ -44,8 +44,9 @@ function drawActorLegend(diagram) {
yPos += 20;
});
}
// TODO: Cleanup?
const conf = getConfig().journey;
const LEFT_MARGIN = getConfig().journey.leftMargin;
const LEFT_MARGIN = conf.leftMargin;
export const draw = function (text, id, version, diagObj) {
const conf = getConfig().journey;
diagObj.db.clear();

View File

@@ -1,29 +1,26 @@
/** Created by knut on 14-12-11. */
import { select } from 'd3';
import { log } from './logger';
import { getErrorMessage } from './utils';
const conf = {};
let conf = {};
/**
* Merges the value of `conf` with the passed `cnf`
*
* @param {object} cnf Config to merge
*/
export const setConf = function (cnf) {
const keys = Object.keys(cnf);
keys.forEach(function (key) {
conf[key] = cnf[key];
});
export const setConf = function (cnf: any) {
conf = { ...conf, ...cnf };
};
/**
* Draws a an info picture in the tag with id: id based on the graph definition in text.
*
* @param {string} id The text for the error
* @param {string} ver The version
* @param {string} mermaidVersion The version
*/
export const draw = (id, ver) => {
export const draw = (id: string, mermaidVersion: string) => {
try {
log.debug('Renering svg for syntax error\n');
@@ -86,14 +83,14 @@ export const draw = (id, ver) => {
.attr('y', 400)
.attr('font-size', '100px')
.style('text-anchor', 'middle')
.text('mermaid version ' + ver);
.text('mermaid version ' + mermaidVersion);
svg.attr('height', 100);
svg.attr('width', 400);
svg.attr('viewBox', '768 0 512 512');
} catch (e) {
log.error('Error while rendering info diagram');
log.error(e.message);
log.error(getErrorMessage(e));
}
};

View File

@@ -1,5 +1,5 @@
let interactionFunctions = [];
export const addFunction = (func) => {
let interactionFunctions: (() => {})[] = [];
export const addFunction = (func: () => {}) => {
interactionFunctions.push(func);
};
export const attachFunctions = () => {

View File

@@ -1,9 +1,9 @@
import moment from 'moment-mini';
/** @typedef {'debug' | 'info' | 'warn' | 'error' | 'fatal'} LogLevel A log level */
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
/** @type {Object<LogLevel, number>} */
export const LEVELS = {
export const LEVELS: Record<LogLevel, number> = {
trace: 0,
debug: 1,
info: 2,
warn: 3,
@@ -11,12 +11,13 @@ export const LEVELS = {
fatal: 5,
};
export const log = {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
fatal: () => {},
export const log: Record<keyof typeof LEVELS, typeof console.log> = {
trace: (..._args: any[]) => {},
debug: (..._args: any[]) => {},
info: (..._args: any[]) => {},
warn: (..._args: any[]) => {},
error: (..._args: any[]) => {},
fatal: (..._args: any[]) => {},
};
/**
@@ -24,11 +25,12 @@ export const log = {
*
* @param {LogLevel} [level="fatal"] The level to set the logging to. Default is `"fatal"`
*/
export const setLogLevel = function (level = 'fatal') {
if (isNaN(level)) {
export const setLogLevel = function (level: keyof typeof LEVELS | number | string = 'fatal') {
let numericLevel: number = LEVELS.fatal;
if (typeof level === 'string') {
level = level.toLowerCase();
if (LEVELS[level] !== undefined) {
level = LEVELS[level];
if (level in LEVELS) {
numericLevel = LEVELS[level as keyof typeof LEVELS];
}
}
log.trace = () => {};
@@ -37,31 +39,36 @@ export const setLogLevel = function (level = 'fatal') {
log.warn = () => {};
log.error = () => {};
log.fatal = () => {};
if (level <= LEVELS.fatal) {
if (numericLevel <= LEVELS.fatal) {
log.fatal = console.error
? console.error.bind(console, format('FATAL'), 'color: orange')
: console.log.bind(console, '\x1b[35m', format('FATAL'));
}
if (level <= LEVELS.error) {
if (numericLevel <= LEVELS.error) {
log.error = console.error
? console.error.bind(console, format('ERROR'), 'color: orange')
: console.log.bind(console, '\x1b[31m', format('ERROR'));
}
if (level <= LEVELS.warn) {
if (numericLevel <= LEVELS.warn) {
log.warn = console.warn
? console.warn.bind(console, format('WARN'), 'color: orange')
: console.log.bind(console, `\x1b[33m`, format('WARN'));
}
if (level <= LEVELS.info) {
log.info = console.info // ? console.info.bind(console, '\x1b[34m', format('INFO'), 'color: blue')
if (numericLevel <= LEVELS.info) {
log.info = console.info
? console.info.bind(console, format('INFO'), 'color: lightblue')
: console.log.bind(console, '\x1b[34m', format('INFO'));
}
if (level <= LEVELS.debug) {
if (numericLevel <= LEVELS.debug) {
log.debug = console.debug
? console.debug.bind(console, format('DEBUG'), 'color: lightgreen')
: console.log.bind(console, '\x1b[32m', format('DEBUG'));
}
if (numericLevel <= LEVELS.trace) {
log.trace = console.debug
? console.debug.bind(console, format('TRACE'), 'color: lightgreen')
: console.log.bind(console, '\x1b[32m', format('TRACE'));
}
};
/**
@@ -70,7 +77,7 @@ export const setLogLevel = function (level = 'fatal') {
* @param {LogLevel} level The level for the log format
* @returns {string} The format with the timestamp and log level
*/
const format = (level) => {
const format = (level: string): string => {
const time = moment().format('ss.SSS');
return `%c${time} : ${level} : `;
};

View File

@@ -227,5 +227,14 @@ describe('when using mermaid and ', function () {
'end';
expect(() => mermaid.parse(text)).toThrow();
});
it('should return false for invalid definition WITH a parseError() callback defined', function () {
let parseErrorWasCalled = false;
mermaid.setParseErrorHandler(() => {
parseErrorWasCalled = true;
});
expect(mermaid.parse('this is not a mermaid diagram definition')).toEqual(false);
expect(parseErrorWasCalled).toEqual(true);
});
});
});

View File

@@ -2,9 +2,11 @@
* Web page integration module for the mermaid framework. It uses the mermaidAPI for mermaid
* functionality and to render the diagrams to svg code.
*/
import { MermaidConfig } from './config.type';
import { log } from './logger';
import mermaidAPI from './mermaidAPI';
import utils from './utils';
import { mermaidAPI } from './mermaidAPI';
import { isDetailedError } from './utils';
/**
* ## init
@@ -29,81 +31,70 @@ import utils from './utils';
*
* Renders the mermaid diagrams
*/
const init = function () {
const init = function (
config?: MermaidConfig,
nodes?: string | HTMLElement | NodeListOf<HTMLElement>,
callback?: Function
) {
try {
initThrowsErrors(...arguments);
initThrowsErrors(config, nodes, callback);
} catch (e) {
log.warn('Syntax Error rendering');
log.warn(e.str);
if (this.parseError) {
this.parseError(e);
if (isDetailedError(e)) {
log.warn(e.str);
}
if (mermaid.parseError) {
mermaid.parseError(e);
}
}
};
const initThrowsErrors = function () {
const initThrowsErrors = function (
config?: MermaidConfig,
nodes?: string | HTMLElement | NodeListOf<HTMLElement>,
callback?: Function
) {
const conf = mermaidAPI.getConfig();
// console.log('Starting rendering diagrams (init) - mermaid.init', conf);
let nodes;
if (arguments.length >= 2) {
/*! sequence config was passed as #1 */
if (typeof arguments[0] !== 'undefined') {
mermaid.sequenceConfig = arguments[0];
}
nodes = arguments[1];
} else {
nodes = arguments[0];
if (config) {
// This is a legacy way of setting config. It is not documented and should be removed in the future.
// @ts-ignore
mermaid.sequenceConfig = config;
}
// if last argument is a function this is the callback function
let callback;
if (typeof arguments[arguments.length - 1] === 'function') {
callback = arguments[arguments.length - 1];
log.debug('Callback function found');
log.debug(`${!callback ? 'No ' : ''}Callback function found`);
let nodesToProcess: NodeListOf<HTMLElement>;
if (typeof nodes === 'undefined') {
nodesToProcess = document.querySelectorAll('.mermaid');
} else if (typeof nodes === 'string') {
nodesToProcess = document.querySelectorAll(nodes);
} else if (nodes instanceof HTMLElement) {
nodesToProcess = new NodeList() as NodeListOf<HTMLElement>;
nodesToProcess[0] = nodes;
} else if (nodes instanceof NodeList) {
nodesToProcess = nodes;
} else {
if (typeof conf.mermaid !== 'undefined') {
if (typeof conf.mermaid.callback === 'function') {
callback = conf.mermaid.callback;
log.debug('Callback function found');
} else {
log.debug('No Callback function found');
}
}
}
nodes =
nodes === undefined
? document.querySelectorAll('.mermaid')
: typeof nodes === 'string'
? document.querySelectorAll(nodes)
: nodes instanceof window.Node
? [nodes]
: nodes; // Last case - sequence config was passed pick next
log.debug('Start On Load before: ' + mermaid.startOnLoad);
if (typeof mermaid.startOnLoad !== 'undefined') {
log.debug('Start On Load inner: ' + mermaid.startOnLoad);
mermaidAPI.updateSiteConfig({ startOnLoad: mermaid.startOnLoad });
throw new Error('Invalid argument nodes for mermaid.init');
}
if (typeof mermaid.ganttConfig !== 'undefined') {
mermaidAPI.updateSiteConfig({ gantt: mermaid.ganttConfig });
log.debug(`Found ${nodesToProcess.length} diagrams`);
if (typeof config?.startOnLoad !== 'undefined') {
log.debug('Start On Load: ' + config?.startOnLoad);
mermaidAPI.updateSiteConfig({ startOnLoad: config?.startOnLoad });
}
const idGenerator = new utils.initIdGenerator(conf.deterministicIds, conf.deterministicIDSeed);
let txt;
for (let i = 0; i < nodes.length; i++) {
// element is the current div with mermaid class
const element = nodes[i];
// element is the current div with mermaid class
for (const element of Array.from(nodesToProcess)) {
/*! Check if previously processed */
if (!element.getAttribute('data-processed')) {
element.setAttribute('data-processed', true);
} else {
if (element.getAttribute('data-processed')) {
continue;
}
element.setAttribute('data-processed', 'true');
const id = `mermaid-${idGenerator.next()}`;
@@ -124,7 +115,7 @@ const initThrowsErrors = function () {
mermaidAPI.render(
id,
txt,
(svgCode, bindFunctions) => {
(svgCode: string, bindFunctions?: (el: Element) => void) => {
element.innerHTML = svgCode;
if (typeof callback !== 'undefined') {
callback(id);
@@ -135,24 +126,15 @@ const initThrowsErrors = function () {
);
} catch (error) {
log.warn('Catching Error (bootstrap)');
// @ts-ignore
// TODO: We should be throwing an error object.
throw { error, message: error.str };
}
}
};
const initialize = function (config) {
// mermaidAPI.reset();
if (typeof config.mermaid !== 'undefined') {
if (typeof config.mermaid.startOnLoad !== 'undefined') {
mermaid.startOnLoad = config.mermaid.startOnLoad;
}
if (typeof config.mermaid.htmlLabels !== 'undefined') {
mermaid.htmlLabels =
config.mermaid.htmlLabels === 'false' || config.mermaid.htmlLabels === false ? false : true;
}
}
const initialize = function (config: MermaidConfig) {
mermaidAPI.initialize(config);
// mermaidAPI.reset();
};
/**
@@ -160,22 +142,11 @@ const initialize = function (config) {
* configuration for mermaid rendering and calls init for rendering the mermaid diagrams on the page.
*/
const contentLoaded = function () {
let config;
if (mermaid.startOnLoad) {
// No config found, do check API config
config = mermaidAPI.getConfig();
if (config.startOnLoad) {
const { startOnLoad } = mermaidAPI.getConfig();
if (startOnLoad) {
mermaid.init();
}
} else {
if (typeof mermaid.startOnLoad === 'undefined') {
log.debug('In start, no config');
config = mermaidAPI.getConfig();
if (config.startOnLoad) {
mermaid.init();
}
}
}
};
@@ -206,24 +177,37 @@ if (typeof document !== 'undefined') {
*
* @param {function (err, hash)} newParseErrorHandler New parseError() callback.
*/
const setParseErrorHandler = function (newParseErrorHandler) {
const setParseErrorHandler = function (newParseErrorHandler: (err: any, hash: any) => void) {
mermaid.parseError = newParseErrorHandler;
};
const mermaid = {
const parse = (txt: string) => {
return mermaidAPI.parse(txt, mermaid.parseError);
};
const mermaid: {
startOnLoad: boolean;
diagrams: any;
parseError?: Function;
mermaidAPI: typeof mermaidAPI;
parse: typeof parse;
render: typeof mermaidAPI.render;
init: typeof init;
initThrowsErrors: typeof initThrowsErrors;
initialize: typeof initialize;
contentLoaded: typeof contentLoaded;
setParseErrorHandler: typeof setParseErrorHandler;
} = {
startOnLoad: true,
htmlLabels: true,
diagrams: {},
mermaidAPI,
parse: mermaidAPI != undefined ? mermaidAPI.parse : null,
render: mermaidAPI != undefined ? mermaidAPI.render : null,
parse,
render: mermaidAPI.render,
init,
initThrowsErrors,
initialize,
parseError: undefined,
contentLoaded,
setParseErrorHandler,
};

View File

@@ -47,9 +47,12 @@ describe('when using mermaidAPI and ', function () {
mermaidAPI.setConfig({ securityLevel: 'strict', logLevel: 1 });
expect(mermaidAPI.getConfig().logLevel).toBe(1);
expect(mermaidAPI.getConfig().securityLevel).toBe('strict');
mermaidAPI.globalReset();
mermaidAPI.reset();
expect(mermaidAPI.getConfig().logLevel).toBe(0);
expect(mermaidAPI.getConfig().securityLevel).toBe('loose');
mermaidAPI.globalReset();
expect(mermaidAPI.getConfig().logLevel).toBe(5);
expect(mermaidAPI.getConfig().securityLevel).toBe('strict');
});
it('should prevent changes to site defaults (sneaky)', function () {
@@ -129,15 +132,14 @@ describe('when using mermaidAPI and ', function () {
it('should not throw for a valid definition', function () {
expect(() => mermaidAPI.parse('graph TD;A--x|text including URL space|B;')).not.toThrow();
});
it('should return false for invalid definition WITH a parseError() callback defined', function () {
var parseErrorWasCalled = false;
it('it should return false for invalid definition WITH a parseError() callback defined', function () {
let parseErrorWasCalled = false;
// also test setParseErrorHandler() call working to set mermaid.parseError
mermaid.setParseErrorHandler(function (error, hash) {
// got here.
parseErrorWasCalled = true;
});
expect(mermaid.parseError).not.toEqual(undefined);
expect(mermaidAPI.parse('this is not a mermaid diagram definition')).toEqual(false);
expect(
mermaidAPI.parse('this is not a mermaid diagram definition', () => {
parseErrorWasCalled = true;
})
).toEqual(false);
expect(parseErrorWasCalled).toEqual(true);
});
it('should return true for valid definition', function () {

View File

@@ -17,19 +17,14 @@
*/
import { select } from 'd3';
import { compile, serialize, stringify } from 'stylis';
// @ts-ignore
import pkg from '../package.json';
import * as configApi from './config';
import addDiagrams from './diagram-api/diagram-orchestration';
import { addDiagrams } from './diagram-api/diagram-orchestration';
import classDb from './diagrams/class/classDb';
import flowDb from './diagrams/flowchart/flowDb';
import flowRenderer from './diagrams/flowchart/flowRenderer';
import flowRendererV2 from './diagrams/flowchart/flowRenderer-v2';
import ganttDb from './diagrams/gantt/ganttDb';
import ganttRenderer from './diagrams/gantt/ganttRenderer';
import sequenceRenderer from './diagrams/sequence/sequenceRenderer';
import stateRenderer from './diagrams/state/stateRenderer';
import stateRendererV2 from './diagrams/state/stateRenderer-v2';
import journeyRenderer from './diagrams/user-journey/journeyRenderer';
import Diagram from './Diagram';
import errorRenderer from './errorRenderer';
import { attachFunctions } from './interactionDb';
@@ -37,50 +32,22 @@ import { log, setLogLevel } from './logger';
import getStyles from './styles';
import theme from './themes';
import utils, { directiveSanitizer } from './utils';
import assignWithDepth from './assignWithDepth';
import DOMPurify from 'dompurify';
import mermaid from './mermaid';
import { MermaidConfig } from './config.type';
import { evaluate } from './diagrams/common/common';
let hasLoadedDiagrams = false;
/**
* @param text
* @param dia
* @returns {any}
*/
function parse(text, dia) {
function parse(text: string, parseError?: Function): boolean {
if (!hasLoadedDiagrams) {
addDiagrams();
hasLoadedDiagrams = true;
}
var parseEncounteredException = false;
try {
const diag = dia ? dia : new Diagram(text);
diag.db.clear();
return diag.parse(text);
} catch (error) {
parseEncounteredException = true;
// Is this the correct way to access mermiad's parseError()
// method ? (or global.mermaid.parseError()) ?
if (mermaid.parseError) {
if (error.str != undefined) {
// handle case where error string and hash were
// wrapped in object like`const error = { str, hash };`
mermaid.parseError(error.str, error.hash);
} else {
// assume it is just error string and pass it on
mermaid.parseError(error);
}
} else {
// No mermaid.parseError() handler defined, so re-throw it
throw error;
}
}
return !parseEncounteredException;
const diagram = new Diagram(text, parseError);
return diagram.parse(text, parseError);
}
export const encodeEntities = function (text) {
export const encodeEntities = function (text: string): string {
let txt = text;
txt = txt.replace(/style.*:\S*#.*;/g, function (s) {
@@ -106,7 +73,7 @@ export const encodeEntities = function (text) {
return txt;
};
export const decodeEntities = function (text) {
export const decodeEntities = function (text: string): string {
let txt = text;
txt = txt.replace(/fl°°/g, function () {
@@ -137,18 +104,24 @@ export const decodeEntities = function (text) {
* });
* ```
*
* @param {any} id The id of the element to be rendered
* @param {any} _txt The graph definition
* @param {any} cb Callback which is called after rendering is finished with the svg code as inparam.
* @param {any} container Selector to element in which a div with the graph temporarily will be
* @param {string} id The id of the element to be rendered
* @param {string} text The graph definition
* @param {(svgCode: string, bindFunctions?: (element: Element) => void) => void} cb Callback which
* is called after rendering is finished with the svg code as inparam.
* @param {Element} container Selector to element in which a div with the graph temporarily will be
* inserted. If one is provided a hidden div will be inserted in the body of the page instead. The
* element will be removed when rendering is completed.
* @returns {any}
* @returns {void}
*/
const render = function (id, _txt, cb, container) {
const render = function (
id: string,
text: string,
cb: (svgCode: string, bindFunctions?: (element: Element) => void) => void,
container?: Element
): void {
configApi.reset();
let txt = _txt.replace(/\r\n?/g, '\n'); // parser problems on CRLF ignore all CR and leave LF;;
const graphInit = utils.detectInit(txt);
text = text.replace(/\r\n?/g, '\n'); // parser problems on CRLF ignore all CR and leave LF;;
const graphInit = utils.detectInit(text);
if (graphInit) {
directiveSanitizer(graphInit);
configApi.addDirective(graphInit);
@@ -158,28 +131,18 @@ const render = function (id, _txt, cb, container) {
log.debug(cnf);
// Check the maximum allowed text size
if (_txt.length > cnf.maxTextSize) {
txt = 'graph TB;a[Maximum text size in diagram exceeded];style a fill:#faa';
if (text.length > cnf.maxTextSize!) {
text = 'graph TB;a[Maximum text size in diagram exceeded];style a fill:#faa';
}
let root = select('body');
let root: any = select('body');
// In regular execution the container will be the div with a mermaid class
if (typeof container !== 'undefined') {
if (cnf.securityLevel === 'sandbox') {
// IF we are in sandboxed mode, we do everyting mermaid related
// in a sandboxed div
const iframe = select('body')
.append('iframe')
.attr('id', 'i' + id)
.attr('style', 'width: 100%; height: 100%;')
.attr('sandbox', '');
root = select(iframe.nodes()[0].contentDocument.body);
root.node().style.margin = 0;
}
// A container was provided by the caller
container.innerHTML = '';
if (container) {
container.innerHTML = '';
}
if (cnf.securityLevel === 'sandbox') {
// IF we are in sandboxed mode, we do everyting mermaid related
@@ -190,7 +153,7 @@ const render = function (id, _txt, cb, container) {
.attr('style', 'width: 100%; height: 100%;')
.attr('sandbox', '');
// const iframeBody = ;
root = select(iframe.nodes()[0].contentDocument.body);
root = select(iframe.nodes()[0]!.contentDocument!.body);
root.node().style.margin = 0;
} else {
root = select(container);
@@ -217,11 +180,12 @@ const render = function (id, _txt, cb, container) {
// Remove previous tpm element if it exists
let element;
if (cnf.securityLevel !== 'sandbox') {
element = document.querySelector('#' + 'd' + id);
if (cnf.securityLevel === 'sandbox') {
element = document.querySelector('#i' + id);
} else {
element = document.querySelector('#' + 'i' + id);
element = document.querySelector('#d' + id);
}
if (element) {
element.remove();
}
@@ -238,7 +202,7 @@ const render = function (id, _txt, cb, container) {
.attr('style', 'width: 100%; height: 100%;')
.attr('sandbox', '');
root = select(iframe.nodes()[0].contentDocument.body);
root = select(iframe.nodes()[0]!.contentDocument!.body);
root.node().style.margin = 0;
} else {
root = select('body');
@@ -256,10 +220,10 @@ const render = function (id, _txt, cb, container) {
.append('g');
}
txt = encodeEntities(txt);
text = encodeEntities(text);
// Important that we do not create the diagram until after the directives have been included
const diag = new Diagram(txt);
const diag = new Diagram(text);
// Get the tmp element containing the the svg
const element = root.select('#d' + id).node();
const graphType = diag.type;
@@ -286,8 +250,8 @@ const render = function (id, _txt, cb, container) {
// classDef
if (graphType === 'flowchart' || graphType === 'flowchart-v2' || graphType === 'graph') {
const classes = flowRenderer.getClasses(txt, diag);
const htmlLabels = cnf.htmlLabels || cnf.flowchart.htmlLabels;
const classes: any = flowRenderer.getClasses(text, diag);
const htmlLabels = cnf.htmlLabels || cnf.flowchart?.htmlLabels;
for (const className in classes) {
if (htmlLabels) {
userStyles += `\n.${className} > * { ${classes[className].styles.join(
@@ -321,7 +285,8 @@ const render = function (id, _txt, cb, container) {
}
}
const stylis = (selector, styles) => serialize(compile(`${selector}{${styles}}`), stringify);
const stylis = (selector: string, styles: string) =>
serialize(compile(`${selector}{${styles}}`), stringify);
const rules = stylis(`#${id}`, getStyles(graphType, userStyles, cnf.themeVariables));
const style1 = document.createElement('style');
@@ -329,7 +294,7 @@ const render = function (id, _txt, cb, container) {
svg.insertBefore(style1, firstChild);
try {
diag.renderer.draw(txt, id, pkg.version, diag);
diag.renderer.draw(text, id, pkg.version, diag);
} catch (e) {
errorRenderer.draw(id, pkg.version);
throw e;
@@ -344,10 +309,7 @@ const render = function (id, _txt, cb, container) {
let svgCode = root.select('#d' + id).node().innerHTML;
log.debug('cnf.arrowMarkerAbsolute', cnf.arrowMarkerAbsolute);
if (
(!cnf.arrowMarkerAbsolute || cnf.arrowMarkerAbsolute === 'false') &&
cnf.arrowMarkerAbsolute !== 'sandbox'
) {
if (!evaluate(cnf.arrowMarkerAbsolute) && cnf.securityLevel !== 'sandbox') {
svgCode = svgCode.replace(/marker-end="url\(.*?#/g, 'marker-end="url(#', 'g');
}
@@ -400,16 +362,16 @@ const render = function (id, _txt, cb, container) {
const tmpElementSelector = cnf.securityLevel === 'sandbox' ? '#i' + id : '#d' + id;
const node = select(tmpElementSelector).node();
if (node !== null && typeof node.remove === 'function') {
select(tmpElementSelector).node().remove();
if (node && 'remove' in node) {
node.remove();
}
return svgCode;
};
let currentDirective = {};
let currentDirective: { type?: string; args?: any } | undefined = {};
const parseDirective = function (p, statement, context, type) {
const parseDirective = function (p: any, statement: string, context: string, type: string): void {
try {
if (statement !== undefined) {
statement = statement.trim();
@@ -418,14 +380,16 @@ const parseDirective = function (p, statement, context, type) {
currentDirective = {};
break;
case 'type_directive':
if (!currentDirective) throw new Error('currentDirective is undefined');
currentDirective.type = statement.toLowerCase();
break;
case 'arg_directive':
if (!currentDirective) throw new Error('currentDirective is undefined');
currentDirective.args = JSON.parse(statement);
break;
case 'close_directive':
handleDirective(p, currentDirective, type);
currentDirective = null;
currentDirective = undefined;
break;
}
}
@@ -433,11 +397,12 @@ const parseDirective = function (p, statement, context, type) {
log.error(
`Error while rendering sequenceDiagram directive: ${statement} jison context: ${context}`
);
// @ts-ignore
log.error(error.message);
}
};
const handleDirective = function (p, directive, type) {
const handleDirective = function (p: any, directive: any, type: string): void {
log.debug(`Directive type=${directive.type} with args:`, directive.args);
switch (directive.type) {
case 'init':
@@ -477,27 +442,8 @@ const handleDirective = function (p, directive, type) {
}
};
/** @param {any} conf */
function updateRendererConfigs(conf) {
// Todo remove, all diagrams should get config on demand from the config object, no need for this
flowRenderer.setConf(conf.flowchart);
flowRendererV2.setConf(conf.flowchart);
if (typeof conf['sequenceDiagram'] !== 'undefined') {
sequenceRenderer.setConf(assignWithDepth(conf.sequence, conf['sequenceDiagram']));
}
sequenceRenderer.setConf(conf.sequence);
ganttRenderer.setConf(conf.gantt);
// classRenderer.setConf(conf.class);
stateRenderer.setConf(conf.state);
stateRendererV2.setConf(conf.state);
// infoRenderer.setConf(conf.class);
journeyRenderer.setConf(conf.journey);
errorRenderer.setConf(conf.class);
}
/** @param {any} options */
function initialize(options) {
/** @param {MermaidConfig} options */
function initialize(options: MermaidConfig) {
// Handle legacy location of font-family configuration
if (options?.fontFamily) {
if (!options.themeVariables?.fontFamily) {
@@ -508,9 +454,11 @@ function initialize(options) {
// Set default options
configApi.saveConfigFromInitialize(options);
if (options?.theme && theme[options.theme]) {
if (options?.theme && options.theme in theme) {
// Todo merge with user options
options.themeVariables = theme[options.theme].getThemeVariables(options.themeVariables);
options.themeVariables = theme[options.theme as keyof typeof theme].getThemeVariables(
options.themeVariables
);
} else if (options) {
options.themeVariables = theme.default.getThemeVariables(options.themeVariables);
}
@@ -518,7 +466,6 @@ function initialize(options) {
const config =
typeof options === 'object' ? configApi.setSiteConfig(options) : configApi.getSiteConfig();
updateRendererConfigs(config);
setLogLevel(config.logLevel);
if (!hasLoadedDiagrams) {
addDiagrams();
@@ -526,7 +473,7 @@ function initialize(options) {
}
}
const mermaidAPI = Object.freeze({
export const mermaidAPI = Object.freeze({
render,
parse,
parseDirective,
@@ -540,14 +487,12 @@ const mermaidAPI = Object.freeze({
},
globalReset: () => {
configApi.reset(configApi.defaultConfig);
updateRendererConfigs(configApi.getConfig());
},
defaultConfig: configApi.defaultConfig,
});
setLogLevel(configApi.getConfig().logLevel);
configApi.reset(configApi.getConfig());
export default mermaidAPI;
/**
* ## mermaidAPI configuration defaults

View File

@@ -10,8 +10,10 @@ import sequence from './diagrams/sequence/styles';
import stateDiagram from './diagrams/state/styles';
import journey from './diagrams/user-journey/styles';
import c4 from './diagrams/c4/styles';
import { FlowChartStyleOptions } from './diagrams/flowchart/styles';
import { log } from './logger';
// TODO @knut: Inject from registerDiagram.
const themes = {
flowchart,
'flowchart-v2': flowchart,
@@ -31,12 +33,24 @@ const themes = {
c4,
};
export const calcThemeVariables = (theme, userOverRides) => {
log.info('userOverides', userOverRides);
return theme.calcColors(userOverRides);
};
const getStyles = (type, userStyles, options) => {
const getStyles = (
type: string,
userStyles: string,
options: {
fontFamily: string;
fontSize: string;
textColor: string;
errorBkgColor: string;
errorTextColor: string;
lineColor: string;
} & FlowChartStyleOptions
) => {
let diagramStyles: string = '';
if (type in themes && themes[type as keyof typeof themes]) {
diagramStyles = themes[type as keyof typeof themes](options);
} else {
log.warn(`No theme found for ${type}`);
}
return ` {
font-family: ${options.fontFamily};
font-size: ${options.fontSize};
@@ -83,7 +97,7 @@ const getStyles = (type, userStyles, options) => {
font-size: ${options.fontSize};
}
${themes[type](options)}
${diagramStyles}
${userStyles}
`;

View File

@@ -1,7 +1,7 @@
import utils from './utils';
import assignWithDepth from './assignWithDepth';
import detectType from './diagram-api/detectType';
import addDiagrams from './diagram-api/diagram-orchestration';
import { detectType } from './diagram-api/detectType';
import { addDiagrams } from './diagram-api/diagram-orchestration';
addDiagrams();

View File

@@ -1,3 +1,4 @@
// @ts-nocheck
import { sanitizeUrl } from '@braintree/sanitize-url';
import {
curveBasis,
@@ -16,8 +17,9 @@ import {
import common from './diagrams/common/common';
import { configKeys } from './defaultConfig';
import { log } from './logger';
import detectType from './diagram-api/detectType';
import { detectType } from './diagram-api/detectType';
import assignWithDepth from './assignWithDepth';
import { MermaidConfig } from './config.type';
// Effectively an enum of the supported curve types, accessible by name
const d3CurveTypes = {
@@ -71,7 +73,7 @@ const anyComment = /\s*%%.*\n/gm;
* @param {any} cnf
* @returns {object} The json object representing the init passed to mermaid.initialize()
*/
export const detectInit = function (text, cnf) {
export const detectInit = function (text: string, config?: MermaidConfig): MermaidConfig {
let inits = detectDirective(text, /(?:init\b)|(?:initialize\b)/);
let results = {};
@@ -84,7 +86,7 @@ export const detectInit = function (text, cnf) {
results = inits.args;
}
if (results) {
let type = detectType(text, cnf);
let type = detectType(text, config);
['config'].forEach((prop) => {
if (typeof results[prop] !== 'undefined') {
if (type === 'flowchart-v2') {
@@ -821,6 +823,7 @@ export const setupGraphViewbox = function (graph, svgElem, padding, useMaxWidth)
export const initIdGenerator = class iterator {
constructor(deterministic, seed) {
this.deterministic = deterministic;
// TODO: Seed is only used for length?
this.seed = seed;
this.count = seed ? seed.length : 0;
@@ -937,6 +940,20 @@ export const sanitizeCss = (str) => {
return str;
};
export interface DetailedError {
str: string;
hash: any;
}
export function isDetailedError(error: unknown): error is DetailedError {
return 'str' in error;
}
export function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
return String(error);
}
export default {
assignWithDepth,
wrapLabel,