mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-14 17:54:13 +01:00
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:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
351
src/config.type.ts
Normal 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 {};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
82
src/diagram-api/detectType.ts
Normal file
82
src/diagram-api/detectType.ts
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
28
src/diagram-api/diagramAPI.spec.ts
Normal file
28
src/diagram-api/diagramAPI.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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.`);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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, '<').replace(/>/g, '>');
|
||||
txt = txt.replace(/=/g, '=');
|
||||
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,
|
||||
};
|
||||
@@ -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: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[]>>>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
166
src/diagrams/common/common.ts
Normal file
166
src/diagrams/common/common.ts
Normal 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, '<').replace(/>/g, '>');
|
||||
text = text.replace(/=/g, '=');
|
||||
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,
|
||||
};
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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};
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
const detector = (txt) => {
|
||||
if (txt.match(/^\s*gitGraph/)) {
|
||||
return 'gitGraph';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default detector;
|
||||
5
src/diagrams/git/gitGraphDetector.ts
Normal file
5
src/diagrams/git/gitGraphDetector.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { DiagramDetector } from '../../diagram-api/detectType';
|
||||
|
||||
export const gitGraphDetector: DiagramDetector = (txt) => {
|
||||
return txt.match(/^\s*gitGraph/) !== null;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
const detector = (txt) => {
|
||||
if (txt.match(/^\s*mindmap/)) {
|
||||
return 'mindmap';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default detector;
|
||||
5
src/diagrams/mindmap/mindamapDetector.ts
Normal file
5
src/diagrams/mindmap/mindamapDetector.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { DiagramDetector } from '../../diagram-api/detectType';
|
||||
|
||||
export const mindmapDetector: DiagramDetector = (txt) => {
|
||||
return txt.match(/^\s*mindmap/) !== null;
|
||||
};
|
||||
@@ -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') {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
let interactionFunctions = [];
|
||||
export const addFunction = (func) => {
|
||||
let interactionFunctions: (() => {})[] = [];
|
||||
export const addFunction = (func: () => {}) => {
|
||||
interactionFunctions.push(func);
|
||||
};
|
||||
export const attachFunctions = () => {
|
||||
@@ -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} : `;
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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
|
||||
@@ -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}
|
||||
`;
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
Reference in New Issue
Block a user