diff --git a/cSpell.json b/cSpell.json index e8d718316c..e67c7d48e6 100644 --- a/cSpell.json +++ b/cSpell.json @@ -22,6 +22,7 @@ "brkt", "brolin", "brotli", + "catmull", "città", "classdef", "codedoc", diff --git a/package.json b/package.json index b0031f4daf..7a6a032d72 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "build:mermaid": "pnpm build:vite --mermaid", "build:viz": "pnpm build:mermaid --visualize", "build:types": "tsc -p ./packages/mermaid/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-zenuml/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-example-diagram/tsconfig.json --emitDeclarationOnly", + "build:types:watch": "tsc -p ./packages/mermaid/tsconfig.json --emitDeclarationOnly --watch", "build:watch": "pnpm build:vite --watch", "build": "pnpm run -r clean && pnpm build:types && pnpm build:vite", "dev": "concurrently \"pnpm build:vite --watch\" \"ts-node-esm .vite/server.ts\"", diff --git a/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts b/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts index 9c11627620..5b740b0e0e 100644 --- a/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts +++ b/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts @@ -4,17 +4,13 @@ import { log } from '../../logger.js'; import type { BaseDiagramConfig, QuadrantChartConfig } from '../../config.type.js'; import defaultConfig from '../../defaultConfig.js'; import { getThemeVariables } from '../../themes/theme-default.js'; +import type { Point } from '../../types.js'; const defaultThemeVariables = getThemeVariables(); export type TextVerticalPos = 'left' | 'center' | 'right'; export type TextHorizontalPos = 'top' | 'middle' | 'bottom'; -export interface Point { - x: number; - y: number; -} - export interface QuadrantPointInputType extends Point { text: string; } diff --git a/packages/mermaid/src/mermaid.ts b/packages/mermaid/src/mermaid.ts index caf4a2b9b3..a6d4954714 100644 --- a/packages/mermaid/src/mermaid.ts +++ b/packages/mermaid/src/mermaid.ts @@ -136,7 +136,7 @@ const runThrowsErrors = async function ( } // generate the id of the diagram - const idGenerator = new utils.initIdGenerator(conf.deterministicIds, conf.deterministicIDSeed); + const idGenerator = new utils.InitIDGenerator(conf.deterministicIds, conf.deterministicIDSeed); let txt: string; const errors: DetailedError[] = []; diff --git a/packages/mermaid/src/types.ts b/packages/mermaid/src/types.ts new file mode 100644 index 0000000000..4b9eedad6c --- /dev/null +++ b/packages/mermaid/src/types.ts @@ -0,0 +1,16 @@ +export interface Point { + x: number; + y: number; +} + +export interface TextDimensionConfig { + fontSize?: number; + fontWeight?: number; + fontFamily?: string; +} + +export interface TextDimensions { + width: number; + height: number; + lineHeight?: number; +} diff --git a/packages/mermaid/src/utils.spec.ts b/packages/mermaid/src/utils.spec.ts index e1398efc73..3be3bc2141 100644 --- a/packages/mermaid/src/utils.spec.ts +++ b/packages/mermaid/src/utils.spec.ts @@ -1,5 +1,5 @@ import { vi } from 'vitest'; -import utils, { cleanAndMerge, detectDirective } from './utils.js'; +import utils, { calculatePoint, cleanAndMerge, detectDirective } from './utils.js'; import assignWithDepth from './assignWithDepth.js'; import { detectType } from './diagram-api/detectType.js'; import { addDiagrams } from './diagram-api/diagram-orchestration.js'; @@ -352,7 +352,7 @@ describe('when initializing the id generator', function () { }); it('should return a random number generator based on Date', function () { - const idGenerator = new utils.initIdGenerator(false); + const idGenerator = new utils.InitIDGenerator(false); expect(typeof idGenerator.next).toEqual('function'); const lastId = idGenerator.next(); vi.advanceTimersByTime(1000); @@ -360,7 +360,7 @@ describe('when initializing the id generator', function () { }); it('should return a non random number generator', function () { - const idGenerator = new utils.initIdGenerator(true); + const idGenerator = new utils.InitIDGenerator(true); expect(typeof idGenerator.next).toEqual('function'); const start = 0; const lastId = idGenerator.next(); @@ -369,7 +369,7 @@ describe('when initializing the id generator', function () { }); it('should return a non random number generator based on seed', function () { - const idGenerator = new utils.initIdGenerator(true, 'thisIsASeed'); + const idGenerator = new utils.InitIDGenerator(true, 'thisIsASeed'); expect(typeof idGenerator.next).toEqual('function'); const start = 11; const lastId = idGenerator.next(); @@ -490,3 +490,107 @@ describe('cleanAndMerge', () => { expect(inputDeep).toEqual({ a: { b: 1 } }); }); }); + +describe('calculatePoint', () => { + it('should calculate a point on a straight line', () => { + const points = [ + { x: 0, y: 0 }, + { x: 0, y: 10 }, + { x: 0, y: 20 }, + ]; + expect(calculatePoint(points, 0)).toEqual({ x: 0, y: 0 }); + expect(calculatePoint(points, 5)).toEqual({ x: 0, y: 5 }); + expect(calculatePoint(points, 10)).toEqual({ x: 0, y: 10 }); + }); + + it('should calculate a point on a straight line with slope', () => { + const points = [ + { x: 0, y: 0 }, + { x: 10, y: 10 }, + { x: 20, y: 20 }, + ]; + expect(calculatePoint(points, 0)).toMatchInlineSnapshot(` + { + "x": 0, + "y": 0, + } + `); + expect(calculatePoint(points, 5)).toMatchInlineSnapshot(` + { + "x": 3.53553, + "y": 3.53553, + } + `); + expect(calculatePoint(points, 10)).toMatchInlineSnapshot(` + { + "x": 7.07107, + "y": 7.07107, + } + `); + }); + + it('should calculate a point on a straight line with negative slope', () => { + const points = [ + { x: 20, y: 20 }, + { x: 10, y: 10 }, + { x: 15, y: 15 }, + { x: 0, y: 0 }, + ]; + expect(calculatePoint(points, 0)).toMatchInlineSnapshot(` + { + "x": 20, + "y": 20, + } + `); + expect(calculatePoint(points, 5)).toMatchInlineSnapshot(` + { + "x": 16.46447, + "y": 16.46447, + } + `); + expect(calculatePoint(points, 10)).toMatchInlineSnapshot(` + { + "x": 12.92893, + "y": 12.92893, + } + `); + }); + + it('should calculate a point on a curved line', () => { + const points = [ + { x: 0, y: 0 }, + { x: 10, y: 10 }, + { x: 20, y: 0 }, + ]; + expect(calculatePoint(points, 0)).toMatchInlineSnapshot(` + { + "x": 0, + "y": 0, + } + `); + expect(calculatePoint(points, 15)).toMatchInlineSnapshot(` + { + "x": 10.6066, + "y": 9.3934, + } + `); + expect(calculatePoint(points, 20)).toMatchInlineSnapshot(` + { + "x": 14.14214, + "y": 5.85786, + } + `); + }); + + it('should throw an error if the new point cannot be found', () => { + const points = [ + { x: 0, y: 0 }, + { x: 10, y: 10 }, + { x: 20, y: 20 }, + ]; + const distanceToTraverse = 30; + expect(() => calculatePoint(points, distanceToTraverse)).toThrow( + 'Could not find a suitable point for the given distance' + ); + }); +}); diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts index 70de197dad..e706ef1227 100644 --- a/packages/mermaid/src/utils.ts +++ b/packages/mermaid/src/utils.ts @@ -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); - } + if (!url) { + return undefined; + } - return url; + 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 }; - - //Calculation cardinality position using angle, center point on the line/curve but pendicular and with offset-distance + const cardinalityPosition: Point = { x: 0, y: 0 }; + //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; 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[0], config: Parameters[1] ): ReturnType['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[0], config: Parameters[1] ): ReturnType['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, };