2022-02-10 20:32:21 +01:00
|
|
|
import { sanitizeUrl } from '@braintree/sanitize-url';
|
2020-05-11 08:10:04 +02:00
|
|
|
import {
|
|
|
|
curveBasis,
|
|
|
|
curveBasisClosed,
|
|
|
|
curveBasisOpen,
|
|
|
|
curveLinear,
|
|
|
|
curveLinearClosed,
|
|
|
|
curveMonotoneX,
|
|
|
|
curveMonotoneY,
|
|
|
|
curveNatural,
|
|
|
|
curveStep,
|
|
|
|
curveStepAfter,
|
2020-07-01 12:18:48 +02:00
|
|
|
curveStepBefore,
|
2022-02-10 20:32:21 +01:00
|
|
|
select,
|
2020-05-11 08:10:04 +02:00
|
|
|
} from 'd3';
|
2020-07-01 12:18:48 +02:00
|
|
|
import common from './diagrams/common/common';
|
2022-02-10 20:32:21 +01:00
|
|
|
import { configKeys } from './defaultConfig';
|
|
|
|
import { log } from './logger';
|
2015-10-17 10:39:20 +02:00
|
|
|
|
2020-05-11 08:10:04 +02: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,
|
2022-02-10 20:32:21 +01:00
|
|
|
curveStepBefore: curveStepBefore,
|
2020-05-11 08:10:04 +02:00
|
|
|
};
|
2022-02-10 20:32:21 +01: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;
|
2020-06-14 17:34:39 +02:00
|
|
|
const anyComment = /\s*%%.*\n/gm;
|
2020-06-08 20:48:03 +02:00
|
|
|
|
|
|
|
/**
|
2022-02-10 20:32:21 +01:00
|
|
|
* @function detectInit Detects the init config object from the text
|
|
|
|
*
|
|
|
|
* ```mermaid
|
|
|
|
* %%{init: {"theme": "debug", "logLevel": 1 }}%%
|
|
|
|
* graph LR
|
|
|
|
* a-->b
|
|
|
|
* b-->c
|
|
|
|
* c-->d
|
|
|
|
* d-->e
|
|
|
|
* e-->f
|
|
|
|
* f-->g
|
|
|
|
* g-->h
|
2020-06-08 20:48:03 +02:00
|
|
|
* ```
|
|
|
|
*
|
2022-02-10 20:32:21 +01:00
|
|
|
* Or
|
|
|
|
*
|
|
|
|
* ```mermaid
|
|
|
|
* %%{initialize: {"theme": "dark", logLevel: "debug" }}%%
|
|
|
|
* graph LR
|
|
|
|
* a-->b
|
|
|
|
* b-->c
|
|
|
|
* c-->d
|
|
|
|
* d-->e
|
|
|
|
* e-->f
|
|
|
|
* f-->g
|
|
|
|
* g-->h
|
|
|
|
* ```
|
2020-06-08 20:48:03 +02:00
|
|
|
* @param {string} text The text defining the graph
|
2022-02-10 20:32:21 +01:00
|
|
|
* @param {any} cnf
|
|
|
|
* @returns {object} The json object representing the init passed to mermaid.initialize()
|
2020-06-08 20:48:03 +02:00
|
|
|
*/
|
2022-02-10 20:32:21 +01:00
|
|
|
export const detectInit = function (text, cnf) {
|
2020-06-14 17:34:39 +02:00
|
|
|
let inits = detectDirective(text, /(?:init\b)|(?:initialize\b)/);
|
|
|
|
let results = {};
|
2022-02-10 20:32:21 +01:00
|
|
|
|
2020-06-14 17:34:39 +02:00
|
|
|
if (Array.isArray(inits)) {
|
2022-02-10 20:32:21 +01:00
|
|
|
let args = inits.map((init) => init.args);
|
|
|
|
directiveSanitizer(args);
|
|
|
|
|
2020-07-01 12:18:48 +02:00
|
|
|
results = assignWithDepth(results, [...args]);
|
2020-06-14 17:34:39 +02:00
|
|
|
} else {
|
|
|
|
results = inits.args;
|
|
|
|
}
|
2020-07-01 12:18:48 +02:00
|
|
|
if (results) {
|
2022-02-10 20:32:21 +01:00
|
|
|
let type = detectType(text, cnf);
|
|
|
|
['config'].forEach((prop) => {
|
2020-07-01 12:18:48 +02:00
|
|
|
if (typeof results[prop] !== 'undefined') {
|
|
|
|
if (type === 'flowchart-v2') {
|
|
|
|
type = 'flowchart';
|
|
|
|
}
|
|
|
|
results[type] = results[prop];
|
|
|
|
delete results[prop];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2022-02-10 20:32:21 +01:00
|
|
|
|
|
|
|
// Todo: refactor this, these results are never used
|
2020-06-14 17:34:39 +02:00
|
|
|
return results;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2022-02-10 20:32:21 +01:00
|
|
|
* @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
|
2020-06-14 17:34:39 +02:00
|
|
|
*
|
2022-02-10 20:32:21 +01:00
|
|
|
* ```mermaid
|
|
|
|
* graph LR
|
|
|
|
* %%{somedirective}%%
|
|
|
|
* a-->b
|
|
|
|
* b-->c
|
|
|
|
* c-->d
|
|
|
|
* d-->e
|
|
|
|
* e-->f
|
|
|
|
* f-->g
|
|
|
|
* g-->h
|
|
|
|
* ```
|
2020-06-14 17:34:39 +02:00
|
|
|
* @param {string} text The text defining the graph
|
2022-02-10 20:32:21 +01:00
|
|
|
* @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 } matched by the input type if a single directive was found, that directive object
|
|
|
|
* will be returned.
|
2020-06-14 17:34:39 +02:00
|
|
|
*/
|
2022-02-10 20:32:21 +01:00
|
|
|
export const detectDirective = function (text, type = null) {
|
2020-06-14 17:34:39 +02:00
|
|
|
try {
|
|
|
|
const commentWithoutDirectives = new RegExp(
|
|
|
|
`[%]{2}(?![{]${directiveWithoutOpen.source})(?=[}][%]{2}).*\n`,
|
|
|
|
'ig'
|
|
|
|
);
|
2022-02-10 20:32:21 +01:00
|
|
|
text = text.trim().replace(commentWithoutDirectives, '').replace(/'/gm, '"');
|
|
|
|
log.debug(
|
2020-06-14 17:34:39 +02:00
|
|
|
`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) {
|
2022-02-10 20:32:21 +01:00
|
|
|
log.error(
|
|
|
|
`ERROR: ${error.message} - Unable to parse directive
|
|
|
|
${type !== null ? ' type:' + type : ''} based on the text:${text}`
|
2020-06-08 20:48:03 +02:00
|
|
|
);
|
2020-06-14 17:34:39 +02:00
|
|
|
return { type: null, args: null };
|
2020-06-08 20:48:03 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-10-17 10:39:20 +02:00
|
|
|
/**
|
2022-02-10 20:32:21 +01:00
|
|
|
* @function detectType 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
|
|
|
*
|
2022-02-10 20:32:21 +01:00
|
|
|
* ```mermaid
|
|
|
|
* %%{initialize: {"startOnLoad": true, logLevel: "fatal" }}%%
|
|
|
|
* graph LR
|
|
|
|
* a-->b
|
|
|
|
* b-->c
|
|
|
|
* c-->d
|
|
|
|
* d-->e
|
|
|
|
* e-->f
|
|
|
|
* f-->g
|
|
|
|
* g-->h
|
|
|
|
* ```
|
2015-10-17 10:39:20 +02:00
|
|
|
* @param {string} text The text defining the graph
|
2022-02-10 20:32:21 +01:00
|
|
|
* @param {{
|
|
|
|
* class: { defaultRenderer: string } | undefined;
|
|
|
|
* state: { defaultRenderer: string } | undefined;
|
|
|
|
* flowchart: { defaultRenderer: string } | undefined;
|
|
|
|
* }} [cnf]
|
2015-10-17 10:39:20 +02:00
|
|
|
* @returns {string} A graph definition key
|
|
|
|
*/
|
2022-02-10 20:32:21 +01:00
|
|
|
export const detectType = function (text, cnf) {
|
2020-06-14 17:34:39 +02:00
|
|
|
text = text.replace(directive, '').replace(anyComment, '\n');
|
2017-04-11 16:14:25 +02:00
|
|
|
if (text.match(/^\s*sequenceDiagram/)) {
|
2019-09-12 21:58:57 +02:00
|
|
|
return 'sequence';
|
2017-04-11 16:14:25 +02:00
|
|
|
}
|
2015-10-17 10:39:20 +02:00
|
|
|
|
2017-04-11 16:14:25 +02:00
|
|
|
if (text.match(/^\s*gantt/)) {
|
2019-09-12 21:58:57 +02:00
|
|
|
return 'gantt';
|
2017-04-11 16:14:25 +02:00
|
|
|
}
|
2020-06-24 19:34:12 +02:00
|
|
|
if (text.match(/^\s*classDiagram-v2/)) {
|
|
|
|
return 'classDiagram';
|
|
|
|
}
|
2017-04-11 16:14:25 +02:00
|
|
|
if (text.match(/^\s*classDiagram/)) {
|
2022-02-10 20:32:21 +01:00
|
|
|
if (cnf && cnf.class && cnf.class.defaultRenderer === 'dagre-wrapper') return 'classDiagram';
|
2019-09-12 21:58:57 +02:00
|
|
|
return 'class';
|
2017-04-11 16:14:25 +02: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/)) {
|
2022-02-10 20:32:21 +01:00
|
|
|
if (cnf && cnf.class && cnf.state.defaultRenderer === 'dagre-wrapper') return 'stateDiagram';
|
2019-09-25 21:01:21 +02:00
|
|
|
return 'state';
|
|
|
|
}
|
|
|
|
|
2017-04-11 16:14:25 +02:00
|
|
|
if (text.match(/^\s*gitGraph/)) {
|
2019-09-12 21:58:57 +02:00
|
|
|
return 'git';
|
2017-04-11 16:14:25 +02:00
|
|
|
}
|
2020-03-04 20:35:59 +01:00
|
|
|
if (text.match(/^\s*flowchart/)) {
|
|
|
|
return 'flowchart-v2';
|
2017-04-11 16:14:25 +02:00
|
|
|
}
|
2019-06-15 14:29:26 +02:00
|
|
|
|
|
|
|
if (text.match(/^\s*info/)) {
|
2019-09-12 21:58:57 +02: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 21:58:57 +02:00
|
|
|
return 'pie';
|
2019-09-11 21:20:28 +02:00
|
|
|
}
|
2019-06-15 14:29:26 +02:00
|
|
|
|
2020-03-03 22:44:18 +01:00
|
|
|
if (text.match(/^\s*erDiagram/)) {
|
|
|
|
return 'er';
|
|
|
|
}
|
|
|
|
|
2020-04-04 18:50:02 +02:00
|
|
|
if (text.match(/^\s*journey/)) {
|
|
|
|
return 'journey';
|
|
|
|
}
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
if (text.match(/^\s*requirement/) || text.match(/^\s*requirementDiagram/)) {
|
|
|
|
return 'requirement';
|
|
|
|
}
|
|
|
|
if (cnf && cnf.flowchart && cnf.flowchart.defaultRenderer === 'dagre-wrapper')
|
|
|
|
return 'flowchart-v2';
|
|
|
|
|
2019-09-12 21:58:57 +02:00
|
|
|
return 'flowchart';
|
|
|
|
};
|
2015-10-17 10:39:20 +02:00
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
/**
|
|
|
|
* Caches results of functions based on input
|
|
|
|
*
|
|
|
|
* @param {Function} fn Function to run
|
|
|
|
* @param {Function} resolver Function that resolves to an ID given arguments the `fn` takes
|
|
|
|
* @returns {Function} An optimized caching function
|
|
|
|
*/
|
2020-07-01 12:18:48 +02:00
|
|
|
const memoize = (fn, resolver) => {
|
|
|
|
let cache = {};
|
|
|
|
return (...args) => {
|
|
|
|
let n = resolver ? resolver.apply(this, args) : args[0];
|
|
|
|
if (n in cache) {
|
|
|
|
return cache[n];
|
|
|
|
} else {
|
|
|
|
let result = fn(...args);
|
|
|
|
cache[n] = result;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2017-01-07 18:53:49 +01:00
|
|
|
/**
|
2022-02-10 20:32:21 +01:00
|
|
|
* @function isSubstringInArray Detects whether a substring in present in a given array
|
2017-01-07 18:53:49 +01:00
|
|
|
* @param {string} str The substring to detect
|
2022-02-10 20:32:21 +01:00
|
|
|
* @param {Array} arr The array to search
|
|
|
|
* @returns {number} The array index containing the substring or -1 if not present
|
|
|
|
*/
|
|
|
|
export const isSubstringInArray = function (str, arr) {
|
2017-09-09 08:46:58 +02:00
|
|
|
for (let i = 0; i < arr.length; i++) {
|
2019-09-12 21:58:57 +02:00
|
|
|
if (arr[i].match(str)) return i;
|
2017-01-07 18:53:49 +01:00
|
|
|
}
|
2019-09-12 21:58:57 +02:00
|
|
|
return -1;
|
|
|
|
};
|
2017-09-10 13:41:34 +02:00
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
/**
|
|
|
|
* Returns a d3 curve given a curve name
|
|
|
|
*
|
|
|
|
* @param {string | undefined} interpolate The interpolation name
|
|
|
|
* @param {any} defaultCurve The default curve to return
|
|
|
|
* @returns {import('d3-shape').CurveFactory} The curve factory to use
|
|
|
|
*/
|
2018-03-09 06:33:35 +01:00
|
|
|
export const interpolateToCurve = (interpolate, defaultCurve) => {
|
2018-03-18 02:35:28 +01:00
|
|
|
if (!interpolate) {
|
2019-09-12 21:58:57 +02:00
|
|
|
return defaultCurve;
|
2018-03-18 02:35:28 +01:00
|
|
|
}
|
2019-09-12 21:58:57 +02:00
|
|
|
const curveName = `curve${interpolate.charAt(0).toUpperCase() + interpolate.slice(1)}`;
|
2020-05-11 08:10:04 +02:00
|
|
|
return d3CurveTypes[curveName] || defaultCurve;
|
2019-09-12 21:58:57 +02:00
|
|
|
};
|
2018-03-09 06:33:35 +01:00
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
/**
|
|
|
|
* Formats a URL string
|
|
|
|
*
|
|
|
|
* @param {string} linkStr String of the URL
|
|
|
|
* @param {{ securityLevel: string }} config Configuration passed to MermaidJS
|
|
|
|
* @returns {string | undefined} The formatted URL
|
|
|
|
*/
|
2020-01-02 20:04:37 +01:00
|
|
|
export const formatUrl = (linkStr, config) => {
|
2020-01-02 20:24:06 +01:00
|
|
|
let url = linkStr.trim();
|
2020-01-02 20:04:37 +01:00
|
|
|
|
2020-01-02 20:24:06 +01:00
|
|
|
if (url) {
|
|
|
|
if (config.securityLevel !== 'loose') {
|
|
|
|
return sanitizeUrl(url);
|
2020-01-02 20:04:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return url;
|
|
|
|
}
|
2020-01-02 20:24:06 +01:00
|
|
|
};
|
2020-01-02 20:04:37 +01:00
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
/**
|
|
|
|
* Runs a function
|
|
|
|
*
|
|
|
|
* @param {string} functionName A dot seperated path to the function relative to the `window`
|
|
|
|
* @param {...any} params Parameters to pass to the function
|
|
|
|
*/
|
2020-06-13 12:41:52 +02:00
|
|
|
export const runFunc = (functionName, ...params) => {
|
2020-07-01 12:18:48 +02:00
|
|
|
const arrPaths = functionName.split('.');
|
2020-06-13 12:41:52 +02:00
|
|
|
|
2020-07-01 12:18:48 +02:00
|
|
|
const len = arrPaths.length - 1;
|
|
|
|
const fnName = arrPaths[len];
|
2020-06-13 12:41:52 +02:00
|
|
|
|
2020-07-01 12:18:48 +02:00
|
|
|
let obj = window;
|
|
|
|
for (let i = 0; i < len; i++) {
|
2020-06-13 12:41:52 +02:00
|
|
|
obj = obj[arrPaths[i]];
|
|
|
|
if (!obj) return;
|
|
|
|
}
|
|
|
|
|
|
|
|
obj[fnName](...params);
|
|
|
|
};
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
/**
|
|
|
|
* @typedef {object} Point A (x, y) point
|
|
|
|
* @property {number} x The x value
|
|
|
|
* @property {number} y The y value
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Finds the distance between two points using the Distance Formula
|
|
|
|
*
|
|
|
|
* @param {Point} p1 The first point
|
|
|
|
* @param {Point} p2 The second point
|
|
|
|
* @returns {number} The distance
|
|
|
|
*/
|
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;
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
/**
|
|
|
|
* @param {Point[]} points List of points
|
|
|
|
* @returns {Point}
|
|
|
|
* @todo Give this a description
|
|
|
|
*/
|
|
|
|
const traverseEdge = (points) => {
|
2019-09-28 13:31:10 +02:00
|
|
|
let prevPoint;
|
|
|
|
let totalDistance = 0;
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
points.forEach((point) => {
|
2019-09-28 13:31:10 +02:00
|
|
|
totalDistance += distance(point, prevPoint);
|
|
|
|
prevPoint = point;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Traverse half of total distance along points
|
2020-07-01 12:18:48 +02:00
|
|
|
let remainingDistance = totalDistance / 2;
|
|
|
|
let center = undefined;
|
2019-09-28 13:31:10 +02:00
|
|
|
prevPoint = undefined;
|
2022-02-10 20:32:21 +01:00
|
|
|
points.forEach((point) => {
|
2019-09-28 13:31:10 +02:00
|
|
|
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,
|
2022-02-10 20:32:21 +01:00
|
|
|
y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y,
|
2019-09-28 13:31:10 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
prevPoint = point;
|
|
|
|
});
|
|
|
|
return center;
|
|
|
|
};
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
/**
|
|
|
|
* Alias for `traverseEdge`
|
|
|
|
*
|
|
|
|
* @param {Point[]} points List of points
|
|
|
|
* @returns {Point} Return result of `transverseEdge`
|
|
|
|
*/
|
|
|
|
const calcLabelPosition = (points) => {
|
2020-07-01 12:18:48 +02:00
|
|
|
return traverseEdge(points);
|
2019-09-28 13:31:10 +02:00
|
|
|
};
|
|
|
|
|
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
|
2022-02-10 20:32:21 +01:00
|
|
|
log.info('our points', points);
|
2019-10-08 21:42:49 +02:00
|
|
|
if (points[0] !== initialPosition) {
|
|
|
|
points = points.reverse();
|
|
|
|
}
|
2022-02-10 20:32:21 +01:00
|
|
|
points.forEach((point) => {
|
2019-10-08 21:42:49 +02:00
|
|
|
totalDistance += distance(point, prevPoint);
|
|
|
|
prevPoint = point;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Traverse only 25 total distance along points to find cardinality point
|
|
|
|
const distanceToCardinalityPoint = 25;
|
|
|
|
|
|
|
|
let remainingDistance = distanceToCardinalityPoint;
|
2020-07-22 17:35:01 +02:00
|
|
|
let center;
|
2019-10-08 21:42:49 +02:00
|
|
|
prevPoint = undefined;
|
2022-02-10 20:32:21 +01:00
|
|
|
points.forEach((point) => {
|
2019-10-08 21:42:49 +02:00
|
|
|
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,
|
2022-02-10 20:32:21 +01:00
|
|
|
y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y,
|
2019-10-08 21:42:49 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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-08-07 14:52:42 +02:00
|
|
|
/**
|
2022-02-10 20:32:21 +01:00
|
|
|
* Position ['start_left', 'start_right', 'end_left', 'end_right']
|
|
|
|
*
|
|
|
|
* @param {any} terminalMarkerSize
|
|
|
|
* @param {any} position
|
|
|
|
* @param {any} _points
|
|
|
|
* @returns {any}
|
2020-08-07 14:52:42 +02:00
|
|
|
*/
|
|
|
|
const calcTerminalLabelPosition = (terminalMarkerSize, position, _points) => {
|
|
|
|
// Todo looking to faster cloning method
|
|
|
|
let points = JSON.parse(JSON.stringify(_points));
|
|
|
|
let prevPoint;
|
|
|
|
let totalDistance = 0; // eslint-disable-line
|
2022-02-10 20:32:21 +01:00
|
|
|
log.info('our points', points);
|
2020-08-07 14:52:42 +02:00
|
|
|
if (position !== 'start_left' && position !== 'start_right') {
|
|
|
|
points = points.reverse();
|
|
|
|
}
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
points.forEach((point) => {
|
2020-08-07 14:52:42 +02:00
|
|
|
totalDistance += distance(point, prevPoint);
|
|
|
|
prevPoint = point;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Traverse only 25 total distance along points to find cardinality point
|
2022-02-10 20:32:21 +01:00
|
|
|
const distanceToCardinalityPoint = 25 + terminalMarkerSize;
|
2020-08-07 14:52:42 +02:00
|
|
|
|
|
|
|
let remainingDistance = distanceToCardinalityPoint;
|
|
|
|
let center;
|
|
|
|
prevPoint = undefined;
|
2022-02-10 20:32:21 +01:00
|
|
|
points.forEach((point) => {
|
2020-08-07 14:52:42 +02:00
|
|
|
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,
|
2022-02-10 20:32:21 +01:00
|
|
|
y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y,
|
2020-08-07 14:52:42 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
prevPoint = point;
|
|
|
|
});
|
|
|
|
// if relation is present (Arrows will be added), change cardinality point off-set distance (d)
|
2022-02-10 20:32:21 +01:00
|
|
|
let d = 10 + terminalMarkerSize * 0.5;
|
2020-08-07 14:52:42 +02:00
|
|
|
//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;
|
|
|
|
if (position === 'start_left') {
|
|
|
|
cardinalityPosition.x = Math.sin(angle + Math.PI) * d + (points[0].x + center.x) / 2;
|
|
|
|
cardinalityPosition.y = -Math.cos(angle + Math.PI) * d + (points[0].y + center.y) / 2;
|
|
|
|
}
|
|
|
|
if (position === 'end_right') {
|
|
|
|
cardinalityPosition.x = Math.sin(angle - Math.PI) * d + (points[0].x + center.x) / 2 - 5;
|
|
|
|
cardinalityPosition.y = -Math.cos(angle - Math.PI) * d + (points[0].y + center.y) / 2 - 5;
|
|
|
|
}
|
|
|
|
if (position === 'end_left') {
|
|
|
|
cardinalityPosition.x = Math.sin(angle) * d + (points[0].x + center.x) / 2 - 5;
|
|
|
|
cardinalityPosition.y = -Math.cos(angle) * d + (points[0].y + center.y) / 2 - 5;
|
|
|
|
}
|
|
|
|
return cardinalityPosition;
|
|
|
|
};
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
/**
|
|
|
|
* Gets styles from an array of declarations
|
|
|
|
*
|
|
|
|
* @param {string[]} arr Declarations
|
|
|
|
* @returns {{ style: string; labelStyle: string }} The styles grouped as strings
|
|
|
|
*/
|
|
|
|
export const getStylesFromArray = (arr) => {
|
2020-02-04 00:24:50 +01:00
|
|
|
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++;
|
2022-02-10 20:32:21 +01:00
|
|
|
return 'id-' + Math.random().toString(36).substr(2, 12) + '-' + cnt;
|
2020-04-02 19:35:12 +02:00
|
|
|
};
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
/**
|
|
|
|
* @param {any} length
|
|
|
|
* @returns {any}
|
|
|
|
*/
|
2020-07-16 10:48:37 +02:00
|
|
|
function makeid(length) {
|
|
|
|
var result = '';
|
|
|
|
var characters = '0123456789abcdef';
|
|
|
|
var charactersLength = characters.length;
|
|
|
|
for (var i = 0; i < length; i++) {
|
|
|
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
export const random = (options) => {
|
2020-07-16 10:48:37 +02:00
|
|
|
return makeid(options.length);
|
2020-07-01 12:18:48 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2022-02-10 20:32:21 +01:00
|
|
|
* @function assignWithDepth Extends the functionality of {@link ObjectConstructor.assign} with the
|
|
|
|
* ability to merge arbitrary-depth objects For each key in src with path `k` (recursively)
|
|
|
|
* performs an Object.assign(dst[`k`], src[`k`]) with a slight change from the typical handling of
|
|
|
|
* undefined for dst[`k`]: instead of raising an error, dst[`k`] is auto-initialized to {} and
|
|
|
|
* effectively merged with src[`k`]<p> Additionally, dissimilar types will not clobber unless the
|
|
|
|
* config.clobber parameter === true. Example:
|
|
|
|
*
|
|
|
|
* ```js
|
|
|
|
* let config_0 = { foo: { bar: 'bar' }, bar: 'foo' };
|
|
|
|
* let config_1 = { foo: 'foo', bar: 'bar' };
|
|
|
|
* let result = assignWithDepth(config_0, config_1);
|
|
|
|
* console.log(result);
|
|
|
|
* //-> result: { foo: { bar: 'bar' }, bar: 'bar' }
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* Traditional Object.assign would have clobbered foo in config_0 with foo in config_1. If src is a
|
|
|
|
* destructured array of objects and dst is not an array, assignWithDepth will apply each element
|
|
|
|
* of src to dst in order.
|
|
|
|
* @param dst
|
|
|
|
* @param src
|
|
|
|
* @param config
|
|
|
|
* @param dst
|
|
|
|
* @param src
|
|
|
|
* @param config
|
|
|
|
* @param dst
|
|
|
|
* @param src
|
|
|
|
* @param config
|
|
|
|
* @param {any} dst - The destination of the merge
|
|
|
|
* @param {any} src - The source object(s) to merge into destination
|
|
|
|
* @param {{ depth: number; clobber: boolean }} [config={ depth: 2, clobber: false }] - Depth: depth
|
|
|
|
* to traverse within src and dst for merging - clobber: should dissimilar types clobber (default:
|
|
|
|
* { depth: 2, clobber: false }). Default is `{ depth: 2, clobber: false }`
|
|
|
|
* @returns {any}
|
2020-07-01 12:18:48 +02:00
|
|
|
*/
|
2022-02-10 20:32:21 +01:00
|
|
|
export const assignWithDepth = function (dst, src, config) {
|
2020-07-01 12:18:48 +02:00
|
|
|
const { depth, clobber } = Object.assign({ depth: 2, clobber: false }, config);
|
|
|
|
if (Array.isArray(src) && !Array.isArray(dst)) {
|
2022-02-10 20:32:21 +01:00
|
|
|
src.forEach((s) => assignWithDepth(dst, s, config));
|
2020-07-01 12:18:48 +02:00
|
|
|
return dst;
|
2020-07-08 14:41:35 +02:00
|
|
|
} else if (Array.isArray(src) && Array.isArray(dst)) {
|
2022-02-10 20:32:21 +01:00
|
|
|
src.forEach((s) => {
|
2020-07-08 14:41:35 +02:00
|
|
|
if (dst.indexOf(s) === -1) {
|
|
|
|
dst.push(s);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return dst;
|
2020-07-01 12:18:48 +02:00
|
|
|
}
|
|
|
|
if (typeof dst === 'undefined' || depth <= 0) {
|
|
|
|
if (dst !== undefined && dst !== null && typeof dst === 'object' && typeof src === 'object') {
|
|
|
|
return Object.assign(dst, src);
|
|
|
|
} else {
|
|
|
|
return src;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (typeof src !== 'undefined' && typeof dst === 'object' && typeof src === 'object') {
|
2022-02-10 20:32:21 +01:00
|
|
|
Object.keys(src).forEach((key) => {
|
2020-07-01 12:18:48 +02:00
|
|
|
if (
|
|
|
|
typeof src[key] === 'object' &&
|
|
|
|
(dst[key] === undefined || typeof dst[key] === 'object')
|
|
|
|
) {
|
|
|
|
if (dst[key] === undefined) {
|
|
|
|
dst[key] = Array.isArray(src[key]) ? [] : {};
|
|
|
|
}
|
|
|
|
dst[key] = assignWithDepth(dst[key], src[key], { depth: depth - 1, clobber });
|
|
|
|
} else if (clobber || (typeof dst[key] !== 'object' && typeof src[key] !== 'object')) {
|
|
|
|
dst[key] = src[key];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return dst;
|
|
|
|
};
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
export const getTextObj = function () {
|
2020-07-01 12:18:48 +02:00
|
|
|
return {
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
fill: undefined,
|
|
|
|
anchor: 'start',
|
|
|
|
style: '#666',
|
|
|
|
width: 100,
|
|
|
|
height: 100,
|
|
|
|
textMargin: 0,
|
|
|
|
rx: 0,
|
|
|
|
ry: 0,
|
2022-02-10 20:32:21 +01:00
|
|
|
valign: undefined,
|
2020-07-01 12:18:48 +02:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
/**
|
|
|
|
* Adds text to an element
|
|
|
|
*
|
|
|
|
* @param {SVGElement} elem Element to add text to
|
|
|
|
* @param {{
|
|
|
|
* text: string;
|
|
|
|
* x: number;
|
|
|
|
* y: number;
|
|
|
|
* anchor: 'start' | 'middle' | 'end';
|
|
|
|
* fontFamily: string;
|
|
|
|
* fontSize: string | number;
|
|
|
|
* fontWeight: string | number;
|
|
|
|
* fill: string;
|
|
|
|
* class: string | undefined;
|
|
|
|
* textMargin: number;
|
|
|
|
* }} textData
|
|
|
|
* @returns {SVGTextElement} Text element with given styling and content
|
|
|
|
*/
|
|
|
|
export const drawSimpleText = function (elem, textData) {
|
2020-07-01 12:18:48 +02:00
|
|
|
// Remove and ignore br:s
|
|
|
|
const nText = textData.text.replace(common.lineBreakRegex, ' ');
|
|
|
|
|
|
|
|
const textElem = elem.append('text');
|
|
|
|
textElem.attr('x', textData.x);
|
|
|
|
textElem.attr('y', textData.y);
|
|
|
|
textElem.style('text-anchor', textData.anchor);
|
|
|
|
textElem.style('font-family', textData.fontFamily);
|
|
|
|
textElem.style('font-size', textData.fontSize);
|
|
|
|
textElem.style('font-weight', textData.fontWeight);
|
|
|
|
textElem.attr('fill', textData.fill);
|
|
|
|
if (typeof textData.class !== 'undefined') {
|
|
|
|
textElem.attr('class', textData.class);
|
|
|
|
}
|
|
|
|
|
|
|
|
const span = textElem.append('tspan');
|
|
|
|
span.attr('x', textData.x + textData.textMargin * 2);
|
|
|
|
span.attr('fill', textData.fill);
|
|
|
|
span.text(nText);
|
|
|
|
|
|
|
|
return textElem;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const wrapLabel = memoize(
|
|
|
|
(label, maxWidth, config) => {
|
|
|
|
if (!label) {
|
|
|
|
return label;
|
|
|
|
}
|
|
|
|
config = Object.assign(
|
|
|
|
{ fontSize: 12, fontWeight: 400, fontFamily: 'Arial', joinWith: '<br/>' },
|
|
|
|
config
|
|
|
|
);
|
|
|
|
if (common.lineBreakRegex.test(label)) {
|
|
|
|
return label;
|
|
|
|
}
|
|
|
|
const words = label.split(' ');
|
|
|
|
const completedLines = [];
|
|
|
|
let nextLine = '';
|
|
|
|
words.forEach((word, index) => {
|
|
|
|
const wordLength = calculateTextWidth(`${word} `, config);
|
|
|
|
const nextLineLength = calculateTextWidth(nextLine, config);
|
|
|
|
if (wordLength > maxWidth) {
|
|
|
|
const { hyphenatedStrings, remainingWord } = breakString(word, maxWidth, '-', config);
|
|
|
|
completedLines.push(nextLine, ...hyphenatedStrings);
|
|
|
|
nextLine = remainingWord;
|
|
|
|
} else if (nextLineLength + wordLength >= maxWidth) {
|
|
|
|
completedLines.push(nextLine);
|
|
|
|
nextLine = word;
|
|
|
|
} else {
|
|
|
|
nextLine = [nextLine, word].filter(Boolean).join(' ');
|
|
|
|
}
|
|
|
|
const currentWord = index + 1;
|
|
|
|
const isLastWord = currentWord === words.length;
|
|
|
|
if (isLastWord) {
|
|
|
|
completedLines.push(nextLine);
|
|
|
|
}
|
|
|
|
});
|
2022-02-10 20:32:21 +01:00
|
|
|
return completedLines.filter((line) => line !== '').join(config.joinWith);
|
2020-07-01 12:18:48 +02:00
|
|
|
},
|
|
|
|
(label, maxWidth, config) =>
|
|
|
|
`${label}-${maxWidth}-${config.fontSize}-${config.fontWeight}-${config.fontFamily}-${config.joinWith}`
|
|
|
|
);
|
|
|
|
|
|
|
|
const breakString = memoize(
|
|
|
|
(word, maxWidth, hyphenCharacter = '-', config) => {
|
|
|
|
config = Object.assign(
|
|
|
|
{ fontSize: 12, fontWeight: 400, fontFamily: 'Arial', margin: 0 },
|
|
|
|
config
|
|
|
|
);
|
|
|
|
const characters = word.split('');
|
|
|
|
const lines = [];
|
|
|
|
let currentLine = '';
|
|
|
|
characters.forEach((character, index) => {
|
|
|
|
const nextLine = `${currentLine}${character}`;
|
|
|
|
const lineWidth = calculateTextWidth(nextLine, config);
|
|
|
|
if (lineWidth >= maxWidth) {
|
|
|
|
const currentCharacter = index + 1;
|
|
|
|
const isLastLine = characters.length === currentCharacter;
|
|
|
|
const hyphenatedNextLine = `${nextLine}${hyphenCharacter}`;
|
|
|
|
lines.push(isLastLine ? nextLine : hyphenatedNextLine);
|
|
|
|
currentLine = '';
|
|
|
|
} else {
|
|
|
|
currentLine = nextLine;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return { hyphenatedStrings: lines, remainingWord: currentLine };
|
|
|
|
},
|
|
|
|
(word, maxWidth, hyphenCharacter = '-', config) =>
|
|
|
|
`${word}-${maxWidth}-${hyphenCharacter}-${config.fontSize}-${config.fontWeight}-${config.fontFamily}`
|
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
2022-02-10 20:32:21 +01:00
|
|
|
* This calculates the text's height, taking into account the wrap breaks and both the statically
|
|
|
|
* configured height, width, and the length of the text (in pixels).
|
2020-07-01 12:18:48 +02:00
|
|
|
*
|
2022-02-10 20:32:21 +01:00
|
|
|
* If the wrapped text text has greater height, we extend the height, so it's value won't overflow.
|
2020-07-01 12:18:48 +02:00
|
|
|
*
|
2022-02-10 20:32:21 +01:00
|
|
|
* @param {any} text The text to measure
|
|
|
|
* @param {any} config - The config for fontSize, fontFamily, and fontWeight all impacting the resulting size
|
|
|
|
* @returns {any} - The height for the given text
|
2020-07-01 12:18:48 +02:00
|
|
|
*/
|
2022-02-10 20:32:21 +01:00
|
|
|
export const calculateTextHeight = function (text, config) {
|
2020-07-01 12:18:48 +02:00
|
|
|
config = Object.assign(
|
|
|
|
{ fontSize: 12, fontWeight: 400, fontFamily: 'Arial', margin: 15 },
|
|
|
|
config
|
|
|
|
);
|
|
|
|
return calculateTextDimensions(text, config).height;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This calculates the width of the given text, font size and family.
|
|
|
|
*
|
2022-02-10 20:32:21 +01:00
|
|
|
* @param {any} text - The text to calculate the width of
|
|
|
|
* @param {any} config - The config for fontSize, fontFamily, and fontWeight all impacting the resulting size
|
|
|
|
* @returns {any} - The width for the given text
|
2020-07-01 12:18:48 +02:00
|
|
|
*/
|
2022-02-10 20:32:21 +01:00
|
|
|
export const calculateTextWidth = function (text, config) {
|
2020-07-01 12:18:48 +02:00
|
|
|
config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config);
|
|
|
|
return calculateTextDimensions(text, config).width;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This calculates the dimensions of the given text, font size, font family, font weight, and margins.
|
|
|
|
*
|
2022-02-10 20:32:21 +01:00
|
|
|
* @param {any} text - The text to calculate the width of
|
|
|
|
* @param {any} config - The config for fontSize, fontFamily, fontWeight, and margin all impacting
|
|
|
|
* the resulting size
|
|
|
|
* @returns - The width for the given text
|
2020-07-01 12:18:48 +02:00
|
|
|
*/
|
|
|
|
export const calculateTextDimensions = memoize(
|
2022-02-10 20:32:21 +01:00
|
|
|
function (text, config) {
|
2020-07-01 12:18:48 +02:00
|
|
|
config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config);
|
|
|
|
const { fontSize, fontFamily, fontWeight } = config;
|
|
|
|
if (!text) {
|
|
|
|
return { width: 0, height: 0 };
|
|
|
|
}
|
|
|
|
|
|
|
|
// We can't really know if the user supplied font family will render on the user agent;
|
|
|
|
// thus, we'll take the max width between the user supplied font family, and a default
|
|
|
|
// of sans-serif.
|
|
|
|
const fontFamilies = ['sans-serif', fontFamily];
|
|
|
|
const lines = text.split(common.lineBreakRegex);
|
|
|
|
let dims = [];
|
|
|
|
|
|
|
|
const body = select('body');
|
|
|
|
// We don't want to leak DOM elements - if a removal operation isn't available
|
|
|
|
// for any reason, do not continue.
|
|
|
|
if (!body.remove) {
|
|
|
|
return { width: 0, height: 0, lineHeight: 0 };
|
|
|
|
}
|
|
|
|
|
|
|
|
const g = body.append('svg');
|
|
|
|
|
|
|
|
for (let fontFamily of fontFamilies) {
|
|
|
|
let cheight = 0;
|
|
|
|
let dim = { width: 0, height: 0, lineHeight: 0 };
|
|
|
|
for (let line of lines) {
|
|
|
|
const textObj = getTextObj();
|
|
|
|
textObj.text = line;
|
|
|
|
const textElem = drawSimpleText(g, textObj)
|
|
|
|
.style('font-size', fontSize)
|
|
|
|
.style('font-weight', fontWeight)
|
|
|
|
.style('font-family', fontFamily);
|
|
|
|
|
|
|
|
let bBox = (textElem._groups || textElem)[0][0].getBBox();
|
|
|
|
dim.width = Math.round(Math.max(dim.width, bBox.width));
|
|
|
|
cheight = Math.round(bBox.height);
|
|
|
|
dim.height += cheight;
|
|
|
|
dim.lineHeight = Math.round(Math.max(dim.lineHeight, cheight));
|
|
|
|
}
|
|
|
|
dims.push(dim);
|
|
|
|
}
|
|
|
|
|
|
|
|
g.remove();
|
|
|
|
|
|
|
|
let index =
|
|
|
|
isNaN(dims[1].height) ||
|
|
|
|
isNaN(dims[1].width) ||
|
|
|
|
isNaN(dims[1].lineHeight) ||
|
|
|
|
(dims[0].height > dims[1].height &&
|
|
|
|
dims[0].width > dims[1].width &&
|
|
|
|
dims[0].lineHeight > dims[1].lineHeight)
|
|
|
|
? 0
|
|
|
|
: 1;
|
|
|
|
return dims[index];
|
|
|
|
},
|
|
|
|
(text, config) => `${text}-${config.fontSize}-${config.fontWeight}-${config.fontFamily}`
|
|
|
|
);
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
/**
|
|
|
|
* Applys d3 attributes
|
|
|
|
*
|
|
|
|
* @param {any} d3Elem D3 Element to apply the attributes onto
|
|
|
|
* @param {[string, string][]} attrs Object.keys equivalent format of key to value mapping of attributes
|
|
|
|
*/
|
|
|
|
const d3Attrs = function (d3Elem, attrs) {
|
2020-08-23 15:45:23 +02:00
|
|
|
for (let attr of attrs) {
|
|
|
|
d3Elem.attr(attr[0], attr[1]);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
/**
|
|
|
|
* Gives attributes for an SVG's size given arguments
|
|
|
|
*
|
|
|
|
* @param {number} height The height of the SVG
|
|
|
|
* @param {number} width The width of the SVG
|
|
|
|
* @param {boolean} useMaxWidth Whether or not to use max-width and set width to 100%
|
|
|
|
* @returns {Map<'height' | 'width' | 'style', string>} Attributes for the SVG
|
|
|
|
*/
|
|
|
|
export const calculateSvgSizeAttrs = function (height, width, useMaxWidth) {
|
2020-08-23 15:45:23 +02:00
|
|
|
let attrs = new Map();
|
|
|
|
attrs.set('height', height);
|
|
|
|
if (useMaxWidth) {
|
|
|
|
attrs.set('width', '100%');
|
|
|
|
attrs.set('style', `max-width: ${width}px;`);
|
|
|
|
} else {
|
|
|
|
attrs.set('width', width);
|
|
|
|
}
|
|
|
|
return attrs;
|
|
|
|
};
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
/**
|
|
|
|
* Applies attributes from `calculateSvgSizeAttrs`
|
|
|
|
*
|
|
|
|
* @param {SVGSVGElement} svgElem The SVG Element to configure
|
|
|
|
* @param {number} height The height of the SVG
|
|
|
|
* @param {number} width The width of the SVG
|
|
|
|
* @param {boolean} useMaxWidth Whether or not to use max-width and set width to 100%
|
|
|
|
*/
|
|
|
|
export const configureSvgSize = function (svgElem, height, width, useMaxWidth) {
|
2020-08-23 15:45:23 +02:00
|
|
|
const attrs = calculateSvgSizeAttrs(height, width, useMaxWidth);
|
|
|
|
d3Attrs(svgElem, attrs);
|
|
|
|
};
|
|
|
|
|
2022-02-10 20:32:21 +01:00
|
|
|
export const initIdGeneratior = class iterator {
|
|
|
|
constructor(deterministic, seed) {
|
|
|
|
this.deterministic = deterministic;
|
|
|
|
this.seed = seed;
|
|
|
|
|
|
|
|
this.count = seed ? seed.length : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
next() {
|
|
|
|
if (!this.deterministic) return Date.now();
|
|
|
|
|
|
|
|
return this.count++;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let decoder;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Decodes HTML, source: {@link https://github.com/shrpne/entity-decode/blob/v2.0.1/browser.js}
|
|
|
|
*
|
|
|
|
* @param {string} html HTML as a string
|
|
|
|
* @returns Unescaped HTML
|
|
|
|
*/
|
|
|
|
export const entityDecode = function (html) {
|
|
|
|
decoder = decoder || document.createElement('div');
|
|
|
|
// Escape HTML before decoding for HTML Entities
|
|
|
|
html = escape(html).replace(/%26/g, '&').replace(/%23/g, '#').replace(/%3B/g, ';');
|
|
|
|
// decoding
|
|
|
|
decoder.innerHTML = html;
|
|
|
|
return unescape(decoder.textContent);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sanitizes directive objects
|
|
|
|
*
|
|
|
|
* @param {object} args Directive's JSON
|
|
|
|
*/
|
|
|
|
export const directiveSanitizer = (args) => {
|
|
|
|
log.debug('directiveSanitizer called with', args);
|
|
|
|
if (typeof args === 'object') {
|
|
|
|
// check for array
|
|
|
|
if (args.length) {
|
|
|
|
args.forEach((arg) => directiveSanitizer(arg));
|
|
|
|
} else {
|
|
|
|
// This is an object
|
|
|
|
Object.keys(args).forEach((key) => {
|
|
|
|
log.debug('Checking key', key);
|
|
|
|
if (key.indexOf('__') === 0) {
|
|
|
|
log.debug('sanitize deleting __ option', key);
|
|
|
|
delete args[key];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (key.indexOf('proto') >= 0) {
|
|
|
|
log.debug('sanitize deleting proto option', key);
|
|
|
|
delete args[key];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (key.indexOf('constr') >= 0) {
|
|
|
|
log.debug('sanitize deleting constr option', key);
|
|
|
|
delete args[key];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (key.indexOf('themeCSS') >= 0) {
|
|
|
|
log.debug('sanitizing themeCss option');
|
|
|
|
args[key] = sanitizeCss(args[key]);
|
|
|
|
}
|
|
|
|
if (configKeys.indexOf(key) < 0) {
|
|
|
|
log.debug('sanitize deleting option', key);
|
|
|
|
delete args[key];
|
|
|
|
} else {
|
|
|
|
if (typeof args[key] === 'object') {
|
|
|
|
log.debug('sanitize deleting object', key);
|
|
|
|
directiveSanitizer(args[key]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
export const sanitizeCss = (str) => {
|
|
|
|
const stringsearch = 'o';
|
|
|
|
const startCnt = (str.match(/\{/g) || []).length;
|
|
|
|
const endCnt = (str.match(/\}/g) || []).length;
|
|
|
|
if (startCnt !== endCnt) {
|
|
|
|
return '{ /* ERROR: Unbalanced CSS */ }';
|
|
|
|
}
|
|
|
|
// Todo add more checks here
|
|
|
|
return str;
|
|
|
|
};
|
|
|
|
|
2017-09-10 13:41:34 +02:00
|
|
|
export default {
|
2020-07-01 12:18:48 +02:00
|
|
|
assignWithDepth,
|
|
|
|
wrapLabel,
|
|
|
|
calculateTextHeight,
|
|
|
|
calculateTextWidth,
|
|
|
|
calculateTextDimensions,
|
2020-08-23 15:45:23 +02:00
|
|
|
calculateSvgSizeAttrs,
|
|
|
|
configureSvgSize,
|
2020-06-08 20:48:03 +02:00
|
|
|
detectInit,
|
2020-06-14 17:34:39 +02:00
|
|
|
detectDirective,
|
2017-09-10 13:41:34 +02:00
|
|
|
detectType,
|
2018-03-09 06:33:35 +01:00
|
|
|
isSubstringInArray,
|
2019-09-28 13:31:10 +02:00
|
|
|
interpolateToCurve,
|
2019-10-08 21:42:49 +02:00
|
|
|
calcLabelPosition,
|
2020-01-02 20:04:37 +01:00
|
|
|
calcCardinalityPosition,
|
2020-08-07 14:52:42 +02:00
|
|
|
calcTerminalLabelPosition,
|
2020-02-04 00:24:50 +01:00
|
|
|
formatUrl,
|
2020-04-02 19:35:12 +02:00
|
|
|
getStylesFromArray,
|
2020-06-13 12:41:52 +02:00
|
|
|
generateId,
|
2020-07-01 12:18:48 +02:00
|
|
|
random,
|
|
|
|
memoize,
|
2022-02-10 20:32:21 +01:00
|
|
|
runFunc,
|
|
|
|
entityDecode,
|
|
|
|
initIdGeneratior,
|
|
|
|
directiveSanitizer,
|
|
|
|
sanitizeCss,
|
2019-09-12 21:58:57 +02:00
|
|
|
};
|