|
|
|
@ -1,4 +1,3 @@
|
|
|
|
|
// @ts-nocheck : TODO Fix ts errors
|
|
|
|
|
import { sanitizeUrl } from '@braintree/sanitize-url';
|
|
|
|
|
import type { CurveFactory } from 'd3';
|
|
|
|
|
import {
|
|
|
|
@ -33,6 +32,8 @@ import type { MermaidConfig } from './config.type.js';
|
|
|
|
|
import memoize from 'lodash-es/memoize.js';
|
|
|
|
|
import merge from 'lodash-es/merge.js';
|
|
|
|
|
import { directiveRegex } from './diagram-api/regexes.js';
|
|
|
|
|
import type { D3Element } from './mermaidAPI.js';
|
|
|
|
|
import type { Point, TextDimensionConfig, TextDimensions } from './types.js';
|
|
|
|
|
|
|
|
|
|
export const ZERO_WIDTH_SPACE = '\u200b';
|
|
|
|
|
|
|
|
|
@ -58,7 +59,7 @@ const d3CurveTypes = {
|
|
|
|
|
curveStep: curveStep,
|
|
|
|
|
curveStepAfter: curveStepAfter,
|
|
|
|
|
curveStepBefore: curveStepBefore,
|
|
|
|
|
};
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
const directiveWithoutOpen =
|
|
|
|
|
/\s*(?:(\w+)(?=:):|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
|
|
|
|
@ -101,14 +102,14 @@ export const detectInit = function (
|
|
|
|
|
config?: MermaidConfig
|
|
|
|
|
): MermaidConfig | undefined {
|
|
|
|
|
const inits = detectDirective(text, /(?:init\b)|(?:initialize\b)/);
|
|
|
|
|
let results = {};
|
|
|
|
|
let results: MermaidConfig & { config?: unknown } = {};
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(inits)) {
|
|
|
|
|
const args = inits.map((init) => init.args);
|
|
|
|
|
sanitizeDirective(args);
|
|
|
|
|
results = assignWithDepth(results, [...args]);
|
|
|
|
|
} else {
|
|
|
|
|
results = inits.args;
|
|
|
|
|
results = inits.args as MermaidConfig;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!results) {
|
|
|
|
@ -116,19 +117,24 @@ export const detectInit = function (
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let type = detectType(text, config);
|
|
|
|
|
['config'].forEach((prop) => {
|
|
|
|
|
if (results[prop] !== undefined) {
|
|
|
|
|
if (type === 'flowchart-v2') {
|
|
|
|
|
type = 'flowchart';
|
|
|
|
|
}
|
|
|
|
|
results[type] = results[prop];
|
|
|
|
|
delete results[prop];
|
|
|
|
|
|
|
|
|
|
// Move the `config` value to appropriate diagram type value
|
|
|
|
|
const prop = 'config';
|
|
|
|
|
if (results[prop] !== undefined) {
|
|
|
|
|
if (type === 'flowchart-v2') {
|
|
|
|
|
type = 'flowchart';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
results[type as keyof MermaidConfig] = results[prop];
|
|
|
|
|
delete results[prop];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return results;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface Directive {
|
|
|
|
|
type?: string;
|
|
|
|
|
args?: unknown;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Detects the directive from the text.
|
|
|
|
|
*
|
|
|
|
@ -154,8 +160,8 @@ export const detectInit = function (
|
|
|
|
|
*/
|
|
|
|
|
export const detectDirective = function (
|
|
|
|
|
text: string,
|
|
|
|
|
type: string | RegExp = null
|
|
|
|
|
): { type?: string; args?: any } | { type?: string; args?: any }[] {
|
|
|
|
|
type: string | RegExp | null = null
|
|
|
|
|
): Directive | Directive[] {
|
|
|
|
|
try {
|
|
|
|
|
const commentWithoutDirectives = new RegExp(
|
|
|
|
|
`[%]{2}(?![{]${directiveWithoutOpen.source})(?=[}][%]{2}).*\n`,
|
|
|
|
@ -165,8 +171,8 @@ export const detectDirective = function (
|
|
|
|
|
log.debug(
|
|
|
|
|
`Detecting diagram directive${type !== null ? ' type:' + type : ''} based on the text:${text}`
|
|
|
|
|
);
|
|
|
|
|
let match;
|
|
|
|
|
const result = [];
|
|
|
|
|
let match: RegExpExecArray | null;
|
|
|
|
|
const result: Directive[] = [];
|
|
|
|
|
while ((match = directiveRegex.exec(text)) !== null) {
|
|
|
|
|
// This is necessary to avoid infinite loops with zero-width matches
|
|
|
|
|
if (match.index === directiveRegex.lastIndex) {
|
|
|
|
@ -183,16 +189,17 @@ export const detectDirective = function (
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (result.length === 0) {
|
|
|
|
|
result.push({ type: text, args: null });
|
|
|
|
|
return { type: text, args: null };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result.length === 1 ? result[0] : result;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
log.error(
|
|
|
|
|
`ERROR: ${error.message} - Unable to parse directive
|
|
|
|
|
${type !== null ? ' type:' + type : ''} based on the text:${text}`
|
|
|
|
|
`ERROR: ${
|
|
|
|
|
(error as Error).message
|
|
|
|
|
} - Unable to parse directive type: '${type}' based on the text: '${text}'`
|
|
|
|
|
);
|
|
|
|
|
return { type: null, args: null };
|
|
|
|
|
return { type: undefined, args: null };
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@ -231,7 +238,9 @@ export function interpolateToCurve(
|
|
|
|
|
return defaultCurve;
|
|
|
|
|
}
|
|
|
|
|
const curveName = `curve${interpolate.charAt(0).toUpperCase() + interpolate.slice(1)}`;
|
|
|
|
|
return d3CurveTypes[curveName] || defaultCurve;
|
|
|
|
|
|
|
|
|
|
// @ts-ignore TODO: Fix issue with curve type
|
|
|
|
|
return d3CurveTypes[curveName as keyof typeof d3CurveTypes] ?? defaultCurve;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -244,13 +253,15 @@ export function interpolateToCurve(
|
|
|
|
|
export function formatUrl(linkStr: string, config: MermaidConfig): string | undefined {
|
|
|
|
|
const url = linkStr.trim();
|
|
|
|
|
|
|
|
|
|
if (url) {
|
|
|
|
|
if (config.securityLevel !== 'loose') {
|
|
|
|
|
return sanitizeUrl(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return url;
|
|
|
|
|
if (!url) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.securityLevel !== 'loose') {
|
|
|
|
|
return sanitizeUrl(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return url;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -259,7 +270,7 @@ export function formatUrl(linkStr: string, config: MermaidConfig): string | unde
|
|
|
|
|
* @param functionName - A dot separated path to the function relative to the `window`
|
|
|
|
|
* @param params - Parameters to pass to the function
|
|
|
|
|
*/
|
|
|
|
|
export const runFunc = (functionName: string, ...params) => {
|
|
|
|
|
export const runFunc = (functionName: string, ...params: unknown[]) => {
|
|
|
|
|
const arrPaths = functionName.split('.');
|
|
|
|
|
|
|
|
|
|
const len = arrPaths.length - 1;
|
|
|
|
@ -267,23 +278,16 @@ export const runFunc = (functionName: string, ...params) => {
|
|
|
|
|
|
|
|
|
|
let obj = window;
|
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
|
obj = obj[arrPaths[i]];
|
|
|
|
|
obj = obj[arrPaths[i] as keyof typeof obj];
|
|
|
|
|
if (!obj) {
|
|
|
|
|
log.error(`Function name: ${functionName} not found in window`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj[fnName](...params);
|
|
|
|
|
obj[fnName as keyof typeof obj](...params);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** A (x, y) point */
|
|
|
|
|
interface Point {
|
|
|
|
|
/** The x value */
|
|
|
|
|
x: number;
|
|
|
|
|
/** The y value */
|
|
|
|
|
y: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Finds the distance between two points using the Distance Formula
|
|
|
|
|
*
|
|
|
|
@ -291,8 +295,11 @@ interface Point {
|
|
|
|
|
* @param p2 - The second point
|
|
|
|
|
* @returns The distance between the two points.
|
|
|
|
|
*/
|
|
|
|
|
function distance(p1: Point, p2: Point): number {
|
|
|
|
|
return p1 && p2 ? Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)) : 0;
|
|
|
|
|
function distance(p1?: Point, p2?: Point): number {
|
|
|
|
|
if (!p1 || !p2) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -301,7 +308,7 @@ function distance(p1: Point, p2: Point): number {
|
|
|
|
|
* @param points - List of points
|
|
|
|
|
*/
|
|
|
|
|
function traverseEdge(points: Point[]): Point {
|
|
|
|
|
let prevPoint;
|
|
|
|
|
let prevPoint: Point | undefined;
|
|
|
|
|
let totalDistance = 0;
|
|
|
|
|
|
|
|
|
|
points.forEach((point) => {
|
|
|
|
@ -310,35 +317,8 @@ function traverseEdge(points: Point[]): Point {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Traverse half of total distance along points
|
|
|
|
|
let remainingDistance = totalDistance / 2;
|
|
|
|
|
let center = undefined;
|
|
|
|
|
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 remainingDistance = totalDistance / 2;
|
|
|
|
|
return calculatePoint(points, remainingDistance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -351,20 +331,16 @@ function calcLabelPosition(points: Point[]): Point {
|
|
|
|
|
return traverseEdge(points);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const calcCardinalityPosition = (isRelationTypePresent, points, initialPosition) => {
|
|
|
|
|
let prevPoint;
|
|
|
|
|
log.info(`our points ${JSON.stringify(points)}`);
|
|
|
|
|
if (points[0] !== initialPosition) {
|
|
|
|
|
points = points.reverse();
|
|
|
|
|
}
|
|
|
|
|
// Traverse only 25 total distance along points to find cardinality point
|
|
|
|
|
const distanceToCardinalityPoint = 25;
|
|
|
|
|
export const roundNumber = (num: number, precision = 2) => {
|
|
|
|
|
const factor = Math.pow(10, precision);
|
|
|
|
|
return Math.round(num * factor) / factor;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let remainingDistance = distanceToCardinalityPoint;
|
|
|
|
|
let center;
|
|
|
|
|
prevPoint = undefined;
|
|
|
|
|
points.forEach((point) => {
|
|
|
|
|
if (prevPoint && !center) {
|
|
|
|
|
export const calculatePoint = (points: Point[], distanceToTraverse: number): Point => {
|
|
|
|
|
let prevPoint: Point | undefined = undefined;
|
|
|
|
|
let remainingDistance = distanceToTraverse;
|
|
|
|
|
for (const point of points) {
|
|
|
|
|
if (prevPoint) {
|
|
|
|
|
const vectorDistance = distance(point, prevPoint);
|
|
|
|
|
if (vectorDistance < remainingDistance) {
|
|
|
|
|
remainingDistance -= vectorDistance;
|
|
|
|
@ -373,27 +349,42 @@ const calcCardinalityPosition = (isRelationTypePresent, points, initialPosition)
|
|
|
|
|
// Calculate the coordinates
|
|
|
|
|
const distanceRatio = remainingDistance / vectorDistance;
|
|
|
|
|
if (distanceRatio <= 0) {
|
|
|
|
|
center = prevPoint;
|
|
|
|
|
return prevPoint;
|
|
|
|
|
}
|
|
|
|
|
if (distanceRatio >= 1) {
|
|
|
|
|
center = { x: point.x, y: point.y };
|
|
|
|
|
return { 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,
|
|
|
|
|
return {
|
|
|
|
|
x: roundNumber((1 - distanceRatio) * prevPoint.x + distanceRatio * point.x, 5),
|
|
|
|
|
y: roundNumber((1 - distanceRatio) * prevPoint.y + distanceRatio * point.y, 5),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
prevPoint = point;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
throw new Error('Could not find a suitable point for the given distance');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const calcCardinalityPosition = (
|
|
|
|
|
isRelationTypePresent: boolean,
|
|
|
|
|
points: Point[],
|
|
|
|
|
initialPosition: Point
|
|
|
|
|
) => {
|
|
|
|
|
log.info(`our points ${JSON.stringify(points)}`);
|
|
|
|
|
if (points[0] !== initialPosition) {
|
|
|
|
|
points = points.reverse();
|
|
|
|
|
}
|
|
|
|
|
// Traverse only 25 total distance along points to find cardinality point
|
|
|
|
|
const distanceToCardinalityPoint = 25;
|
|
|
|
|
const center = calculatePoint(points, distanceToCardinalityPoint);
|
|
|
|
|
// if relation is present (Arrows will be added), change cardinality point off-set distance (d)
|
|
|
|
|
const d = isRelationTypePresent ? 10 : 5;
|
|
|
|
|
//Calculate Angle for x and y axis
|
|
|
|
|
const angle = Math.atan2(points[0].y - center.y, points[0].x - center.x);
|
|
|
|
|
const cardinalityPosition = { x: 0, y: 0 };
|
|
|
|
|
//Calculation cardinality position using angle, center point on the line/curve but pendicular and with offset-distance
|
|
|
|
|
//Calculation cardinality position using angle, center point on the line/curve but perpendicular 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;
|
|
|
|
@ -412,71 +403,36 @@ function calcTerminalLabelPosition(
|
|
|
|
|
position: 'start_left' | 'start_right' | 'end_left' | 'end_right',
|
|
|
|
|
_points: Point[]
|
|
|
|
|
): Point {
|
|
|
|
|
// Todo looking to faster cloning method
|
|
|
|
|
let points = JSON.parse(JSON.stringify(_points));
|
|
|
|
|
let prevPoint;
|
|
|
|
|
const points = structuredClone(_points);
|
|
|
|
|
log.info('our points', points);
|
|
|
|
|
if (position !== 'start_left' && position !== 'start_right') {
|
|
|
|
|
points = points.reverse();
|
|
|
|
|
points.reverse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
points.forEach((point) => {
|
|
|
|
|
prevPoint = point;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Traverse only 25 total distance along points to find cardinality point
|
|
|
|
|
const distanceToCardinalityPoint = 25 + terminalMarkerSize;
|
|
|
|
|
const center = calculatePoint(points, distanceToCardinalityPoint);
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
const d = 10 + terminalMarkerSize * 0.5;
|
|
|
|
|
//Calculate Angle for x and y axis
|
|
|
|
|
const angle = Math.atan2(points[0].y - center.y, points[0].x - center.x);
|
|
|
|
|
|
|
|
|
|
const cardinalityPosition = { x: 0, y: 0 };
|
|
|
|
|
const cardinalityPosition: Point = { x: 0, y: 0 };
|
|
|
|
|
//Calculation cardinality position using angle, center point on the line/curve but perpendicular and with offset-distance
|
|
|
|
|
|
|
|
|
|
//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') {
|
|
|
|
|
} else 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') {
|
|
|
|
|
} else 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;
|
|
|
|
|
} else {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
@ -502,7 +458,7 @@ export function getStylesFromArray(arr: string[]): { style: string; labelStyle:
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { style: style, labelStyle: labelStyle };
|
|
|
|
|
return { style, labelStyle };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let cnt = 0;
|
|
|
|
@ -514,10 +470,10 @@ export const generateId = () => {
|
|
|
|
|
/**
|
|
|
|
|
* Generates a random hexadecimal id of the given length.
|
|
|
|
|
*
|
|
|
|
|
* @param length - Length of ID.
|
|
|
|
|
* @returns The generated ID.
|
|
|
|
|
* @param length - Length of string.
|
|
|
|
|
* @returns The generated string.
|
|
|
|
|
*/
|
|
|
|
|
function makeid(length: number): string {
|
|
|
|
|
function makeRandomHex(length: number): string {
|
|
|
|
|
let result = '';
|
|
|
|
|
const characters = '0123456789abcdef';
|
|
|
|
|
const charactersLength = characters.length;
|
|
|
|
@ -527,8 +483,8 @@ function makeid(length: number): string {
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const random = (options) => {
|
|
|
|
|
return makeid(options.length);
|
|
|
|
|
export const random = (options: { length: number }) => {
|
|
|
|
|
return makeRandomHex(options.length);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getTextObj = function () {
|
|
|
|
@ -544,6 +500,7 @@ export const getTextObj = function () {
|
|
|
|
|
rx: 0,
|
|
|
|
|
ry: 0,
|
|
|
|
|
valign: undefined,
|
|
|
|
|
text: '',
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@ -574,7 +531,7 @@ export const drawSimpleText = function (
|
|
|
|
|
|
|
|
|
|
const [, _fontSizePx] = parseFontSize(textData.fontSize);
|
|
|
|
|
|
|
|
|
|
const textElem = elem.append('text');
|
|
|
|
|
const textElem = elem.append('text') as any;
|
|
|
|
|
textElem.attr('x', textData.x);
|
|
|
|
|
textElem.attr('y', textData.y);
|
|
|
|
|
textElem.style('text-anchor', textData.anchor);
|
|
|
|
@ -582,6 +539,7 @@ export const drawSimpleText = function (
|
|
|
|
|
textElem.style('font-size', _fontSizePx);
|
|
|
|
|
textElem.style('font-weight', textData.fontWeight);
|
|
|
|
|
textElem.attr('fill', textData.fill);
|
|
|
|
|
|
|
|
|
|
if (textData.class !== undefined) {
|
|
|
|
|
textElem.attr('class', textData.class);
|
|
|
|
|
}
|
|
|
|
@ -601,9 +559,9 @@ interface WrapLabelConfig {
|
|
|
|
|
joinWith: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const wrapLabel: (label: string, maxWidth: string, config: WrapLabelConfig) => string =
|
|
|
|
|
export const wrapLabel: (label: string, maxWidth: number, config: WrapLabelConfig) => string =
|
|
|
|
|
memoize(
|
|
|
|
|
(label: string, maxWidth: string, config: WrapLabelConfig): string => {
|
|
|
|
|
(label: string, maxWidth: number, config: WrapLabelConfig): string => {
|
|
|
|
|
if (!label) {
|
|
|
|
|
return label;
|
|
|
|
|
}
|
|
|
|
@ -615,7 +573,7 @@ export const wrapLabel: (label: string, maxWidth: string, config: WrapLabelConfi
|
|
|
|
|
return label;
|
|
|
|
|
}
|
|
|
|
|
const words = label.split(' ');
|
|
|
|
|
const completedLines = [];
|
|
|
|
|
const completedLines: string[] = [];
|
|
|
|
|
let nextLine = '';
|
|
|
|
|
words.forEach((word, index) => {
|
|
|
|
|
const wordLength = calculateTextWidth(`${word} `, config);
|
|
|
|
@ -700,10 +658,6 @@ export function calculateTextHeight(
|
|
|
|
|
text: Parameters<typeof calculateTextDimensions>[0],
|
|
|
|
|
config: Parameters<typeof calculateTextDimensions>[1]
|
|
|
|
|
): ReturnType<typeof calculateTextDimensions>['height'] {
|
|
|
|
|
config = Object.assign(
|
|
|
|
|
{ fontSize: 12, fontWeight: 400, fontFamily: 'Arial', margin: 15 },
|
|
|
|
|
config
|
|
|
|
|
);
|
|
|
|
|
return calculateTextDimensions(text, config).height;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -719,20 +673,9 @@ export function calculateTextWidth(
|
|
|
|
|
text: Parameters<typeof calculateTextDimensions>[0],
|
|
|
|
|
config: Parameters<typeof calculateTextDimensions>[1]
|
|
|
|
|
): ReturnType<typeof calculateTextDimensions>['width'] {
|
|
|
|
|
config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config);
|
|
|
|
|
return calculateTextDimensions(text, config).width;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface TextDimensionConfig {
|
|
|
|
|
fontSize?: number;
|
|
|
|
|
fontWeight?: number;
|
|
|
|
|
fontFamily?: string;
|
|
|
|
|
}
|
|
|
|
|
interface TextDimensions {
|
|
|
|
|
width: number;
|
|
|
|
|
height: number;
|
|
|
|
|
lineHeight?: number;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* This calculates the dimensions of the given text, font size, font family, font weight, and
|
|
|
|
|
* margins.
|
|
|
|
@ -747,8 +690,7 @@ export const calculateTextDimensions: (
|
|
|
|
|
config: TextDimensionConfig
|
|
|
|
|
) => TextDimensions = memoize(
|
|
|
|
|
(text: string, config: TextDimensionConfig): TextDimensions => {
|
|
|
|
|
config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config);
|
|
|
|
|
const { fontSize, fontFamily, fontWeight } = config;
|
|
|
|
|
const { fontSize = 12, fontFamily = 'Arial', fontWeight = 400 } = config;
|
|
|
|
|
if (!text) {
|
|
|
|
|
return { width: 0, height: 0 };
|
|
|
|
|
}
|
|
|
|
@ -772,12 +714,14 @@ export const calculateTextDimensions: (
|
|
|
|
|
const g = body.append('svg');
|
|
|
|
|
|
|
|
|
|
for (const fontFamily of fontFamilies) {
|
|
|
|
|
let cheight = 0;
|
|
|
|
|
let cHeight = 0;
|
|
|
|
|
const dim = { width: 0, height: 0, lineHeight: 0 };
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
const textObj = getTextObj();
|
|
|
|
|
textObj.text = line || ZERO_WIDTH_SPACE;
|
|
|
|
|
// @ts-ignore TODO: Fix D3 types
|
|
|
|
|
const textElem = drawSimpleText(g, textObj)
|
|
|
|
|
// @ts-ignore TODO: Fix D3 types
|
|
|
|
|
.style('font-size', _fontSizePx)
|
|
|
|
|
.style('font-weight', fontWeight)
|
|
|
|
|
.style('font-family', fontFamily);
|
|
|
|
@ -787,9 +731,9 @@ export const calculateTextDimensions: (
|
|
|
|
|
throw new Error('svg element not in render tree');
|
|
|
|
|
}
|
|
|
|
|
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));
|
|
|
|
|
cHeight = Math.round(bBox.height);
|
|
|
|
|
dim.height += cHeight;
|
|
|
|
|
dim.lineHeight = Math.round(Math.max(dim.lineHeight, cHeight));
|
|
|
|
|
}
|
|
|
|
|
dims.push(dim);
|
|
|
|
|
}
|
|
|
|
@ -810,25 +754,18 @@ export const calculateTextDimensions: (
|
|
|
|
|
(text, config) => `${text}${config.fontSize}${config.fontWeight}${config.fontFamily}`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const initIdGenerator = class iterator {
|
|
|
|
|
constructor(deterministic, seed?: any) {
|
|
|
|
|
this.deterministic = deterministic;
|
|
|
|
|
export class InitIDGenerator {
|
|
|
|
|
private count = 0;
|
|
|
|
|
public next: () => number;
|
|
|
|
|
constructor(deterministic = false, seed?: string) {
|
|
|
|
|
// TODO: Seed is only used for length?
|
|
|
|
|
this.seed = seed;
|
|
|
|
|
|
|
|
|
|
// v11: Use the actual value of seed string to generate an initial value for count.
|
|
|
|
|
this.count = seed ? seed.length : 0;
|
|
|
|
|
this.next = deterministic ? () => this.count++ : () => Date.now();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
next() {
|
|
|
|
|
if (!this.deterministic) {
|
|
|
|
|
return Date.now();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.count++;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let decoder;
|
|
|
|
|
let decoder: HTMLDivElement;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Decodes HTML, source: {@link https://github.com/shrpne/entity-decode/blob/v2.0.1/browser.js}
|
|
|
|
@ -840,20 +777,23 @@ export const entityDecode = function (html: string): string {
|
|
|
|
|
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);
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
|
return unescape(decoder.textContent!);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export interface DetailedError {
|
|
|
|
|
str: string;
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
hash: any;
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
error?: any;
|
|
|
|
|
message?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param error - The error to check */
|
|
|
|
|
export function isDetailedError(error: unknown): error is DetailedError {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
export function isDetailedError(error: any): error is DetailedError {
|
|
|
|
|
return 'str' in error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -874,7 +814,7 @@ export function getErrorMessage(error: unknown): string {
|
|
|
|
|
* @param title - The title. If empty, returns immediately.
|
|
|
|
|
*/
|
|
|
|
|
export const insertTitle = (
|
|
|
|
|
parent,
|
|
|
|
|
parent: D3Element,
|
|
|
|
|
cssClass: string,
|
|
|
|
|
titleTopMargin: number,
|
|
|
|
|
title?: string
|
|
|
|
@ -882,7 +822,10 @@ export const insertTitle = (
|
|
|
|
|
if (!title) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const bounds = parent.node().getBBox();
|
|
|
|
|
const bounds = parent.node()?.getBBox();
|
|
|
|
|
if (!bounds) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
parent
|
|
|
|
|
.append('text')
|
|
|
|
|
.text(title)
|
|
|
|
@ -905,7 +848,7 @@ export const parseFontSize = (fontSize: string | number | undefined): [number?,
|
|
|
|
|
return [fontSize, fontSize + 'px'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fontSizeNumber = parseInt(fontSize, 10);
|
|
|
|
|
const fontSizeNumber = parseInt(fontSize ?? '', 10);
|
|
|
|
|
if (Number.isNaN(fontSizeNumber)) {
|
|
|
|
|
// if a number value can't be parsed, return null for both values
|
|
|
|
|
return [undefined, undefined];
|
|
|
|
@ -941,7 +884,7 @@ export default {
|
|
|
|
|
random,
|
|
|
|
|
runFunc,
|
|
|
|
|
entityDecode,
|
|
|
|
|
initIdGenerator,
|
|
|
|
|
insertTitle,
|
|
|
|
|
parseFontSize,
|
|
|
|
|
InitIDGenerator,
|
|
|
|
|
};
|
|
|
|
|