2020-05-11 07:10:04 +01:00
|
|
|
import {
|
|
|
|
curveBasis,
|
|
|
|
curveBasisClosed,
|
|
|
|
curveBasisOpen,
|
|
|
|
curveLinear,
|
|
|
|
curveLinearClosed,
|
|
|
|
curveMonotoneX,
|
|
|
|
curveMonotoneY,
|
|
|
|
curveNatural,
|
|
|
|
curveStep,
|
|
|
|
curveStepAfter,
|
2020-06-19 10:52:20 +02:00
|
|
|
curveStepBefore
|
2020-05-11 07:10:04 +01:00
|
|
|
} from 'd3';
|
2019-09-12 12:58:57 -07:00
|
|
|
import { logger } from './logger';
|
2020-01-02 11:04:37 -08:00
|
|
|
import { sanitizeUrl } from '@braintree/sanitize-url';
|
2015-10-17 10:39:20 +02:00
|
|
|
|
2020-05-11 07:10:04 +01:00
|
|
|
// Effectively an enum of the supported curve types, accessible by name
|
|
|
|
const d3CurveTypes = {
|
|
|
|
curveBasis: curveBasis,
|
|
|
|
curveBasisClosed: curveBasisClosed,
|
|
|
|
curveBasisOpen: curveBasisOpen,
|
|
|
|
curveLinear: curveLinear,
|
|
|
|
curveLinearClosed: curveLinearClosed,
|
|
|
|
curveMonotoneX: curveMonotoneX,
|
|
|
|
curveMonotoneY: curveMonotoneY,
|
|
|
|
curveNatural: curveNatural,
|
|
|
|
curveStep: curveStep,
|
|
|
|
curveStepAfter: curveStepAfter,
|
|
|
|
curveStepBefore: curveStepBefore
|
|
|
|
};
|
2020-06-14 11:34:39 -04:00
|
|
|
const directive = /[%]{2}[{]\s*(?:(?:(\w+)\s*:|(\w+))\s*(?:(?:(\w+))|((?:(?![}][%]{2}).|\r?\n)*))?\s*)(?:[}][%]{2})?/gi;
|
|
|
|
const directiveWithoutOpen = /\s*(?:(?:(\w+)(?=:):|(\w+))\s*(?:(?:(\w+))|((?:(?![}][%]{2}).|\r?\n)*))?\s*)(?:[}][%]{2})?/gi;
|
|
|
|
const anyComment = /\s*%%.*\n/gm;
|
2020-06-08 14:48:03 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @function detectInit
|
2020-06-19 10:52:20 +02:00
|
|
|
* Detects the init config object from the text
|
2020-06-08 14:48:03 -04:00
|
|
|
* ```mermaid
|
2020-06-14 11:34:39 -04:00
|
|
|
* %%{init: {"theme": "debug", "logLevel": 1 }}%%
|
2020-06-08 14:48:03 -04:00
|
|
|
* graph LR
|
|
|
|
* a-->b
|
|
|
|
* b-->c
|
|
|
|
* c-->d
|
|
|
|
* d-->e
|
|
|
|
* e-->f
|
|
|
|
* f-->g
|
|
|
|
* g-->h
|
|
|
|
* ```
|
|
|
|
* or
|
|
|
|
* ```mermaid
|
2020-06-14 11:34:39 -04:00
|
|
|
* %%{initialize: {"theme": "dark", logLevel: "debug" }}%%
|
2020-06-08 14:48:03 -04:00
|
|
|
* graph LR
|
|
|
|
* a-->b
|
|
|
|
* b-->c
|
|
|
|
* c-->d
|
|
|
|
* d-->e
|
|
|
|
* e-->f
|
|
|
|
* f-->g
|
|
|
|
* g-->h
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param {string} text The text defining the graph
|
2020-06-19 10:52:20 +02:00
|
|
|
* @returns {object} the json object representing the init to pass to mermaid.initialize()
|
2020-06-08 14:48:03 -04:00
|
|
|
*/
|
|
|
|
export const detectInit = function(text) {
|
2020-06-14 11:34:39 -04:00
|
|
|
let inits = detectDirective(text, /(?:init\b)|(?:initialize\b)/);
|
|
|
|
let results = {};
|
|
|
|
if (Array.isArray(inits)) {
|
|
|
|
let args = inits.map(init => init.args);
|
2020-06-19 10:52:20 +02:00
|
|
|
results = Object.assign(results, ...args);
|
2020-06-14 11:34:39 -04:00
|
|
|
} else {
|
|
|
|
results = inits.args;
|
|
|
|
}
|
|
|
|
return results;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @function detectDirective
|
|
|
|
* Detects the directive from the text. Text can be single line or multiline. If type is null or omitted
|
|
|
|
* the first directive encountered in text will be returned
|
|
|
|
* ```mermaid
|
|
|
|
* graph LR
|
|
|
|
* %%{somedirective}%%
|
|
|
|
* a-->b
|
|
|
|
* b-->c
|
|
|
|
* c-->d
|
|
|
|
* d-->e
|
|
|
|
* e-->f
|
|
|
|
* f-->g
|
|
|
|
* g-->h
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param {string} text The text defining the graph
|
|
|
|
* @param {string|RegExp} type The directive to return (default: null
|
|
|
|
* @returns {object | Array} An object or Array representing the directive(s): { type: string, args: object|null } matchd by the input type
|
|
|
|
* if a single directive was found, that directive object will be returned.
|
|
|
|
*/
|
|
|
|
export const detectDirective = function(text, type = null) {
|
|
|
|
try {
|
|
|
|
const commentWithoutDirectives = new RegExp(
|
|
|
|
`[%]{2}(?![{]${directiveWithoutOpen.source})(?=[}][%]{2}).*\n`,
|
|
|
|
'ig'
|
|
|
|
);
|
|
|
|
text = text
|
|
|
|
.trim()
|
|
|
|
.replace(commentWithoutDirectives, '')
|
|
|
|
.replace(/'/gm, '"');
|
|
|
|
logger.debug(
|
|
|
|
`Detecting diagram directive${type !== null ? ' type:' + type : ''} based on the text:${text}`
|
|
|
|
);
|
|
|
|
let match,
|
|
|
|
result = [];
|
|
|
|
while ((match = directive.exec(text)) !== null) {
|
|
|
|
// This is necessary to avoid infinite loops with zero-width matches
|
|
|
|
if (match.index === directive.lastIndex) {
|
|
|
|
directive.lastIndex++;
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
(match && !type) ||
|
|
|
|
(type && match[1] && match[1].match(type)) ||
|
|
|
|
(type && match[2] && match[2].match(type))
|
|
|
|
) {
|
|
|
|
let type = match[1] ? match[1] : match[2];
|
|
|
|
let args = match[3] ? match[3].trim() : match[4] ? JSON.parse(match[4].trim()) : null;
|
|
|
|
result.push({ type, args });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (result.length === 0) {
|
|
|
|
result.push({ type: text, args: null });
|
|
|
|
}
|
|
|
|
|
|
|
|
return result.length === 1 ? result[0] : result;
|
|
|
|
} catch (error) {
|
|
|
|
logger.error(
|
|
|
|
`ERROR: ${error.message} - Unable to parse directive${
|
|
|
|
type !== null ? ' type:' + type : ''
|
|
|
|
} based on the text:${text}`
|
2020-06-08 14:48:03 -04:00
|
|
|
);
|
2020-06-14 11:34:39 -04:00
|
|
|
return { type: null, args: null };
|
2020-06-08 14:48:03 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-10-17 10:39:20 +02:00
|
|
|
/**
|
|
|
|
* @function detectType
|
2020-06-11 15:31:59 -04:00
|
|
|
* Detects the type of the graph text. Takes into consideration the possible existence of an %%init
|
|
|
|
* directive
|
2015-10-17 10:39:20 +02:00
|
|
|
* ```mermaid
|
2020-06-11 15:31:59 -04:00
|
|
|
* %%{initialize: {"startOnLoad": true, logLevel: "fatal" }}%%
|
2015-10-17 10:39:20 +02:00
|
|
|
* graph LR
|
|
|
|
* a-->b
|
|
|
|
* b-->c
|
|
|
|
* c-->d
|
|
|
|
* d-->e
|
|
|
|
* e-->f
|
|
|
|
* f-->g
|
|
|
|
* g-->h
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param {string} text The text defining the graph
|
|
|
|
* @returns {string} A graph definition key
|
|
|
|
*/
|
2019-09-12 12:58:57 -07:00
|
|
|
export const detectType = function(text) {
|
2020-06-14 11:34:39 -04:00
|
|
|
text = text.replace(directive, '').replace(anyComment, '\n');
|
2019-09-12 12:58:57 -07:00
|
|
|
logger.debug('Detecting diagram type based on the text ' + text);
|
2017-04-11 22:14:25 +08:00
|
|
|
if (text.match(/^\s*sequenceDiagram/)) {
|
2019-09-12 12:58:57 -07:00
|
|
|
return 'sequence';
|
2017-04-11 22:14:25 +08:00
|
|
|
}
|
2015-10-17 10:39:20 +02:00
|
|
|
|
2017-04-11 22:14:25 +08:00
|
|
|
if (text.match(/^\s*gantt/)) {
|
2019-09-12 12:58:57 -07:00
|
|
|
return 'gantt';
|
2017-04-11 22:14:25 +08:00
|
|
|
}
|
2020-06-24 19:34:12 +02:00
|
|
|
if (text.match(/^\s*classDiagram-v2/)) {
|
|
|
|
return 'classDiagram';
|
|
|
|
}
|
2017-04-11 22:14:25 +08:00
|
|
|
if (text.match(/^\s*classDiagram/)) {
|
2019-09-12 12:58:57 -07:00
|
|
|
return 'class';
|
2017-04-11 22:14:25 +08:00
|
|
|
}
|
2020-06-24 19:34:12 +02:00
|
|
|
|
2020-03-22 21:45:14 +01:00
|
|
|
if (text.match(/^\s*stateDiagram-v2/)) {
|
|
|
|
return 'stateDiagram';
|
|
|
|
}
|
2015-10-30 10:47:25 +01:00
|
|
|
|
2019-09-25 21:01:21 +02:00
|
|
|
if (text.match(/^\s*stateDiagram/)) {
|
|
|
|
return 'state';
|
|
|
|
}
|
|
|
|
|
2017-04-11 22:14:25 +08:00
|
|
|
if (text.match(/^\s*gitGraph/)) {
|
2019-09-12 12:58:57 -07:00
|
|
|
return 'git';
|
2017-04-11 22:14:25 +08:00
|
|
|
}
|
2020-03-04 20:35:59 +01:00
|
|
|
if (text.match(/^\s*flowchart/)) {
|
|
|
|
return 'flowchart-v2';
|
2017-04-11 22:14:25 +08:00
|
|
|
}
|
2019-06-15 14:29:26 +02:00
|
|
|
|
|
|
|
if (text.match(/^\s*info/)) {
|
2019-09-12 12:58:57 -07:00
|
|
|
return 'info';
|
2019-06-15 14:29:26 +02:00
|
|
|
}
|
2019-09-11 21:20:28 +02:00
|
|
|
if (text.match(/^\s*pie/)) {
|
2019-09-12 12:58:57 -07:00
|
|
|
return 'pie';
|
2019-09-11 21:20:28 +02:00
|
|
|
}
|
2019-06-15 14:29:26 +02:00
|
|
|
|
2020-03-03 21:44:18 +00:00
|
|
|
if (text.match(/^\s*erDiagram/)) {
|
|
|
|
return 'er';
|
|
|
|
}
|
|
|
|
|
2020-04-04 17:50:02 +01:00
|
|
|
if (text.match(/^\s*journey/)) {
|
|
|
|
return 'journey';
|
|
|
|
}
|
|
|
|
|
2019-09-12 12:58:57 -07:00
|
|
|
return 'flowchart';
|
|
|
|
};
|
2015-10-17 10:39:20 +02:00
|
|
|
|
2017-01-07 12:53:49 -05:00
|
|
|
/**
|
|
|
|
* @function isSubstringInArray
|
|
|
|
* Detects whether a substring in present in a given array
|
|
|
|
* @param {string} str The substring to detect
|
|
|
|
* @param {array} arr The array to search
|
|
|
|
* @returns {number} the array index containing the substring or -1 if not present
|
|
|
|
**/
|
2019-09-12 12:58:57 -07:00
|
|
|
export const isSubstringInArray = function(str, arr) {
|
2017-09-09 14:46:58 +08:00
|
|
|
for (let i = 0; i < arr.length; i++) {
|
2019-09-12 12:58:57 -07:00
|
|
|
if (arr[i].match(str)) return i;
|
2017-01-07 12:53:49 -05:00
|
|
|
}
|
2019-09-12 12:58:57 -07:00
|
|
|
return -1;
|
|
|
|
};
|
2017-09-10 19:41:34 +08:00
|
|
|
|
2018-03-09 13:33:35 +08:00
|
|
|
export const interpolateToCurve = (interpolate, defaultCurve) => {
|
2018-03-18 09:35:28 +08:00
|
|
|
if (!interpolate) {
|
2019-09-12 12:58:57 -07:00
|
|
|
return defaultCurve;
|
2018-03-18 09:35:28 +08:00
|
|
|
}
|
2019-09-12 12:58:57 -07:00
|
|
|
const curveName = `curve${interpolate.charAt(0).toUpperCase() + interpolate.slice(1)}`;
|
2020-05-11 07:10:04 +01:00
|
|
|
return d3CurveTypes[curveName] || defaultCurve;
|
2019-09-12 12:58:57 -07:00
|
|
|
};
|
2018-03-09 13:33:35 +08:00
|
|
|
|
2020-01-02 11:04:37 -08:00
|
|
|
export const formatUrl = (linkStr, config) => {
|
2020-01-02 11:24:06 -08:00
|
|
|
let url = linkStr.trim();
|
2020-01-02 11:04:37 -08:00
|
|
|
|
2020-01-02 11:24:06 -08:00
|
|
|
if (url) {
|
|
|
|
if (config.securityLevel !== 'loose') {
|
|
|
|
return sanitizeUrl(url);
|
2020-01-02 11:04:37 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
return url;
|
|
|
|
}
|
2020-01-02 11:24:06 -08:00
|
|
|
};
|
2020-01-02 11:04:37 -08:00
|
|
|
|
2020-06-13 17:41:52 +07:00
|
|
|
export const runFunc = (functionName, ...params) => {
|
|
|
|
var arrPaths = functionName.split('.');
|
|
|
|
|
|
|
|
var len = arrPaths.length - 1;
|
|
|
|
var fnName = arrPaths[len];
|
|
|
|
|
|
|
|
var obj = window;
|
|
|
|
for (var i = 0; i < len; i++) {
|
|
|
|
obj = obj[arrPaths[i]];
|
|
|
|
if (!obj) return;
|
|
|
|
}
|
|
|
|
|
|
|
|
obj[fnName](...params);
|
|
|
|
};
|
|
|
|
|
2019-09-28 13:31:10 +02:00
|
|
|
const distance = (p1, p2) =>
|
|
|
|
p1 && p2 ? Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)) : 0;
|
|
|
|
|
|
|
|
const traverseEdge = points => {
|
|
|
|
let prevPoint;
|
|
|
|
let totalDistance = 0;
|
|
|
|
|
|
|
|
points.forEach(point => {
|
|
|
|
totalDistance += distance(point, prevPoint);
|
|
|
|
prevPoint = point;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Traverse half of total distance along points
|
|
|
|
const distanceToLabel = totalDistance / 2;
|
|
|
|
|
|
|
|
let remainingDistance = distanceToLabel;
|
|
|
|
let center;
|
|
|
|
prevPoint = undefined;
|
|
|
|
points.forEach(point => {
|
|
|
|
if (prevPoint && !center) {
|
|
|
|
const vectorDistance = distance(point, prevPoint);
|
|
|
|
if (vectorDistance < remainingDistance) {
|
|
|
|
remainingDistance -= vectorDistance;
|
|
|
|
} else {
|
|
|
|
// The point is remainingDistance from prevPoint in the vector between prevPoint and point
|
|
|
|
// Calculate the coordinates
|
|
|
|
const distanceRatio = remainingDistance / vectorDistance;
|
|
|
|
if (distanceRatio <= 0) center = prevPoint;
|
|
|
|
if (distanceRatio >= 1) center = { x: point.x, y: point.y };
|
|
|
|
if (distanceRatio > 0 && distanceRatio < 1) {
|
|
|
|
center = {
|
|
|
|
x: (1 - distanceRatio) * prevPoint.x + distanceRatio * point.x,
|
|
|
|
y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
prevPoint = point;
|
|
|
|
});
|
|
|
|
return center;
|
|
|
|
};
|
|
|
|
|
|
|
|
const calcLabelPosition = points => {
|
|
|
|
const p = traverseEdge(points);
|
|
|
|
return p;
|
|
|
|
};
|
|
|
|
|
2019-10-08 21:42:49 +02:00
|
|
|
const calcCardinalityPosition = (isRelationTypePresent, points, initialPosition) => {
|
|
|
|
let prevPoint;
|
2019-10-27 15:24:56 +01:00
|
|
|
let totalDistance = 0; // eslint-disable-line
|
2019-10-08 21:42:49 +02:00
|
|
|
if (points[0] !== initialPosition) {
|
|
|
|
points = points.reverse();
|
|
|
|
}
|
|
|
|
points.forEach(point => {
|
|
|
|
totalDistance += distance(point, prevPoint);
|
|
|
|
prevPoint = point;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Traverse only 25 total distance along points to find cardinality point
|
|
|
|
const distanceToCardinalityPoint = 25;
|
|
|
|
|
|
|
|
let remainingDistance = distanceToCardinalityPoint;
|
|
|
|
let center;
|
|
|
|
prevPoint = undefined;
|
|
|
|
points.forEach(point => {
|
|
|
|
if (prevPoint && !center) {
|
|
|
|
const vectorDistance = distance(point, prevPoint);
|
|
|
|
if (vectorDistance < remainingDistance) {
|
|
|
|
remainingDistance -= vectorDistance;
|
|
|
|
} else {
|
|
|
|
// The point is remainingDistance from prevPoint in the vector between prevPoint and point
|
|
|
|
// Calculate the coordinates
|
|
|
|
const distanceRatio = remainingDistance / vectorDistance;
|
|
|
|
if (distanceRatio <= 0) center = prevPoint;
|
|
|
|
if (distanceRatio >= 1) center = { x: point.x, y: point.y };
|
|
|
|
if (distanceRatio > 0 && distanceRatio < 1) {
|
|
|
|
center = {
|
|
|
|
x: (1 - distanceRatio) * prevPoint.x + distanceRatio * point.x,
|
|
|
|
y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
prevPoint = point;
|
|
|
|
});
|
|
|
|
// if relation is present (Arrows will be added), change cardinality point off-set distance (d)
|
|
|
|
let d = isRelationTypePresent ? 10 : 5;
|
|
|
|
//Calculate Angle for x and y axis
|
|
|
|
let angle = Math.atan2(points[0].y - center.y, points[0].x - center.x);
|
|
|
|
let cardinalityPosition = { x: 0, y: 0 };
|
|
|
|
//Calculation cardinality position using angle, center point on the line/curve but pendicular and with offset-distance
|
|
|
|
cardinalityPosition.x = Math.sin(angle) * d + (points[0].x + center.x) / 2;
|
|
|
|
cardinalityPosition.y = -Math.cos(angle) * d + (points[0].y + center.y) / 2;
|
|
|
|
return cardinalityPosition;
|
|
|
|
};
|
|
|
|
|
2020-02-04 00:24:50 +01:00
|
|
|
export const getStylesFromArray = arr => {
|
|
|
|
let style = '';
|
|
|
|
let labelStyle = '';
|
|
|
|
|
|
|
|
for (let i = 0; i < arr.length; i++) {
|
|
|
|
if (typeof arr[i] !== 'undefined') {
|
|
|
|
// add text properties to label style definition
|
|
|
|
if (arr[i].startsWith('color:') || arr[i].startsWith('text-align:')) {
|
|
|
|
labelStyle = labelStyle + arr[i] + ';';
|
|
|
|
} else {
|
|
|
|
style = style + arr[i] + ';';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return { style: style, labelStyle: labelStyle };
|
|
|
|
};
|
|
|
|
|
2020-04-02 19:35:12 +02:00
|
|
|
let cnt = 0;
|
|
|
|
export const generateId = () => {
|
|
|
|
cnt++;
|
|
|
|
return (
|
|
|
|
'id-' +
|
|
|
|
Math.random()
|
|
|
|
.toString(36)
|
|
|
|
.substr(2, 12) +
|
|
|
|
'-' +
|
|
|
|
cnt
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2017-09-10 19:41:34 +08:00
|
|
|
export default {
|
2020-06-08 14:48:03 -04:00
|
|
|
detectInit,
|
2020-06-14 11:34:39 -04:00
|
|
|
detectDirective,
|
2017-09-10 19:41:34 +08:00
|
|
|
detectType,
|
2018-03-09 13:33:35 +08:00
|
|
|
isSubstringInArray,
|
2019-09-28 13:31:10 +02:00
|
|
|
interpolateToCurve,
|
2019-10-08 21:42:49 +02:00
|
|
|
calcLabelPosition,
|
2020-01-02 11:04:37 -08:00
|
|
|
calcCardinalityPosition,
|
2020-02-04 00:24:50 +01:00
|
|
|
formatUrl,
|
2020-04-02 19:35:12 +02:00
|
|
|
getStylesFromArray,
|
2020-06-13 17:41:52 +07:00
|
|
|
generateId,
|
2020-06-17 18:12:01 -04:00
|
|
|
runFunc
|
2019-09-12 12:58:57 -07:00
|
|
|
};
|