diff --git a/packages/mermaid/src/assignWithDepth.js b/packages/mermaid/src/assignWithDepth.js index 6f2e706abe..54302103b8 100644 --- a/packages/mermaid/src/assignWithDepth.js +++ b/packages/mermaid/src/assignWithDepth.js @@ -20,7 +20,7 @@ * of src to dst in order. * @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: depth + * @param {{ depth: number; clobber?: boolean }} [config] - 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} diff --git a/packages/mermaid/src/cleanClone.ts b/packages/mermaid/src/cleanClone.ts deleted file mode 100644 index 8f9cb7c266..0000000000 --- a/packages/mermaid/src/cleanClone.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -export function removeUndefined(data: any): any { - if (typeof data === 'object') { - const entries: [string, any][] = Object.entries(data).filter( - ([, value]: [string, any]) => value !== undefined - ); - - const clean: any[][] = entries.map(([key, v]: [string, any]) => { - const value = typeof v == 'object' ? removeUndefined(v) : v; - return [key, value]; - }); - - return Object.fromEntries(clean); - } else if (Array.isArray(data)) { - return data.filter((value: any) => value !== undefined); - } - return data; -} - -export function structuredCleanClone(defaultData: T, data?: Partial): T { - const cleanValue: T = removeUndefined(data); - return structuredClone({ ...defaultData, ...cleanValue }); -} diff --git a/packages/mermaid/src/diagrams/pie/pieDb.ts b/packages/mermaid/src/diagrams/pie/pieDb.ts index bdd7957c2b..d381aa9851 100644 --- a/packages/mermaid/src/diagrams/pie/pieDb.ts +++ b/packages/mermaid/src/diagrams/pie/pieDb.ts @@ -15,7 +15,7 @@ import type { ParseDirectiveDefinition } from '../../diagram-api/types.js'; import type { PieFields, PieDB, Sections } from './pieTypes.js'; import type { RequiredDeep } from 'type-fest'; import type { PieDiagramConfig } from '../../config.type.js'; -import { structuredCleanClone } from '../../cleanClone.js'; +import { cleanAndMerge } from '../../utils.js'; export const DEFAULT_PIE_CONFIG: Required = { useMaxWidth: true, @@ -31,16 +31,16 @@ export const DEFAULT_PIE_DB: RequiredDeep = { let sections: Sections = DEFAULT_PIE_DB.sections; let showData: boolean = DEFAULT_PIE_DB.showData; -let config: Required = structuredCleanClone(DEFAULT_PIE_CONFIG); +let config: Required = structuredClone(DEFAULT_PIE_CONFIG); const setConfig = (conf: PieDiagramConfig): void => { - config = structuredCleanClone(DEFAULT_PIE_CONFIG, conf); + config = cleanAndMerge(DEFAULT_PIE_CONFIG, conf); }; const getConfig = (): Required => config; const resetConfig = (): void => { - config = structuredCleanClone(DEFAULT_PIE_CONFIG); + config = structuredClone(DEFAULT_PIE_CONFIG); }; const parseDirective: ParseDirectiveDefinition = (statement, context, type) => { @@ -48,7 +48,7 @@ const parseDirective: ParseDirectiveDefinition = (statement, context, type) => { }; const clear = (): void => { - sections = structuredCleanClone(DEFAULT_PIE_DB.sections); + sections = structuredClone(DEFAULT_PIE_DB.sections); showData = DEFAULT_PIE_DB.showData; commonClear(); resetConfig(); diff --git a/packages/mermaid/src/utils.spec.js b/packages/mermaid/src/utils.spec.ts similarity index 79% rename from packages/mermaid/src/utils.spec.js rename to packages/mermaid/src/utils.spec.ts index ae3234cb91..271dc588c6 100644 --- a/packages/mermaid/src/utils.spec.js +++ b/packages/mermaid/src/utils.spec.ts @@ -1,5 +1,5 @@ import { vi } from 'vitest'; -import utils from './utils.js'; +import utils, { cleanAndMerge } from './utils.js'; import assignWithDepth from './assignWithDepth.js'; import { detectType } from './diagram-api/detectType.js'; import { addDiagrams } from './diagram-api/diagram-orchestration.js'; @@ -10,51 +10,51 @@ addDiagrams(); describe('when assignWithDepth: should merge objects within objects', function () { it('should handle simple, depth:1 types (identity)', function () { - let config_0 = { foo: 'bar', bar: 0 }; - let config_1 = { foo: 'bar', bar: 0 }; - let result = assignWithDepth(config_0, config_1); + const config_0 = { foo: 'bar', bar: 0 }; + const config_1 = { foo: 'bar', bar: 0 }; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual(config_1); }); it('should handle simple, depth:1 types (dst: undefined)', function () { - let config_0 = undefined; - let config_1 = { foo: 'bar', bar: 0 }; - let result = assignWithDepth(config_0, config_1); + const config_0 = undefined; + const config_1 = { foo: 'bar', bar: 0 }; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual(config_1); }); it('should handle simple, depth:1 types (src: undefined)', function () { - let config_0 = { foo: 'bar', bar: 0 }; - let config_1 = undefined; - let result = assignWithDepth(config_0, config_1); + const config_0 = { foo: 'bar', bar: 0 }; + const config_1 = undefined; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual(config_0); }); it('should handle simple, depth:1 types (merge)', function () { - let config_0 = { foo: 'bar', bar: 0 }; - let config_1 = { foo: 'foo' }; - let result = assignWithDepth(config_0, config_1); + const config_0 = { foo: 'bar', bar: 0 }; + const config_1 = { foo: 'foo' }; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual({ foo: 'foo', bar: 0 }); }); it('should handle depth:2 types (dst: orphan)', function () { - let config_0 = { foo: 'bar', bar: { foo: 'bar' } }; - let config_1 = { foo: 'bar' }; - let result = assignWithDepth(config_0, config_1); + const config_0 = { foo: 'bar', bar: { foo: 'bar' } }; + const config_1 = { foo: 'bar' }; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual(config_0); }); it('should handle depth:2 types (dst: object, src: simple type)', function () { - let config_0 = { foo: 'bar', bar: { foo: 'bar' } }; - let config_1 = { foo: 'foo', bar: 'should NOT clobber' }; - let result = assignWithDepth(config_0, config_1); + const config_0 = { foo: 'bar', bar: { foo: 'bar' } }; + const config_1 = { foo: 'foo', bar: 'should NOT clobber' }; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual({ foo: 'foo', bar: { foo: 'bar' } }); }); it('should handle depth:2 types (src: orphan)', function () { - let config_0 = { foo: 'bar' }; - let config_1 = { foo: 'bar', bar: { foo: 'bar' } }; - let result = assignWithDepth(config_0, config_1); + const config_0 = { foo: 'bar' }; + const config_1 = { foo: 'bar', bar: { foo: 'bar' } }; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual(config_1); }); it('should handle depth:2 types (merge)', function () { - let config_0 = { foo: 'bar', bar: { foo: 'bar' }, boofar: 1 }; - let config_1 = { foo: 'foo', bar: { bar: 0 }, foobar: 'foobar' }; - let result = assignWithDepth(config_0, config_1); + const config_0 = { foo: 'bar', bar: { foo: 'bar' }, boofar: 1 }; + const config_1 = { foo: 'foo', bar: { bar: 0 }, foobar: 'foobar' }; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual({ foo: 'foo', bar: { foo: 'bar', bar: 0 }, @@ -63,17 +63,17 @@ describe('when assignWithDepth: should merge objects within objects', function ( }); }); it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 2)', function () { - let config_0 = { + const config_0 = { foo: 'bar', bar: { foo: 'bar', bar: { foo: { message: 'this', willbe: 'clobbered' } } }, boofar: 1, }; - let config_1 = { + const config_1 = { foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } }, foobar: 'foobar', }; - let result = assignWithDepth(config_0, config_1); + const result = assignWithDepth(config_0, config_1); expect(result).toEqual({ foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } }, @@ -82,7 +82,7 @@ describe('when assignWithDepth: should merge objects within objects', function ( }); }); it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 1)', function () { - let config_0 = { + const config_0 = { foo: 'bar', bar: { foo: 'bar', @@ -90,12 +90,12 @@ describe('when assignWithDepth: should merge objects within objects', function ( }, boofar: 1, }; - let config_1 = { + const config_1 = { foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'this' } } }, foobar: 'foobar', }; - let result = assignWithDepth(config_0, config_1, { depth: 1 }); + const result = assignWithDepth(config_0, config_1, { depth: 1 }); expect(result).toEqual({ foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'this' } } }, @@ -104,17 +104,17 @@ describe('when assignWithDepth: should merge objects within objects', function ( }); }); it('should handle depth:3 types (merge with no clobber because assignWithDepth::depth == 3)', function () { - let config_0 = { + const config_0 = { foo: 'bar', bar: { foo: 'bar', bar: { foo: { message: '', willbe: 'present' } } }, boofar: 1, }; - let config_1 = { + const config_1 = { foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'this' } } }, foobar: 'foobar', }; - let result = assignWithDepth(config_0, config_1, { depth: 3 }); + const result = assignWithDepth(config_0, config_1, { depth: 3 }); expect(result).toEqual({ foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'this', willbe: 'present' } } }, @@ -125,8 +125,8 @@ describe('when assignWithDepth: should merge objects within objects', function ( }); describe('when memoizing', function () { it('should return the same value', function () { - const fib = memoize( - function (n, x, canary) { + const fib: any = memoize( + function (n: number, x: string, canary: { flag: boolean }) { canary.flag = true; if (n < 2) { return 1; @@ -260,7 +260,7 @@ describe('when formatting urls', function () { it('should handle links', function () { const url = 'https://mermaid-js.github.io/mermaid/#/'; - let config = { securityLevel: 'loose' }; + const config = { securityLevel: 'loose' }; let result = utils.formatUrl(url, config); expect(result).toEqual(url); @@ -271,7 +271,7 @@ describe('when formatting urls', function () { it('should handle anchors', function () { const url = '#interaction'; - let config = { securityLevel: 'loose' }; + const config = { securityLevel: 'loose' }; let result = utils.formatUrl(url, config); expect(result).toEqual(url); @@ -282,7 +282,7 @@ describe('when formatting urls', function () { it('should handle mailto', function () { const url = 'mailto:user@user.user'; - let config = { securityLevel: 'loose' }; + const config = { securityLevel: 'loose' }; let result = utils.formatUrl(url, config); expect(result).toEqual(url); @@ -293,7 +293,7 @@ describe('when formatting urls', function () { it('should handle other protocols', function () { const url = 'notes://do-your-thing/id'; - let config = { securityLevel: 'loose' }; + const config = { securityLevel: 'loose' }; let result = utils.formatUrl(url, config); expect(result).toEqual(url); @@ -304,7 +304,7 @@ describe('when formatting urls', function () { it('should handle scripts', function () { const url = 'javascript:alert("test")'; - let config = { securityLevel: 'loose' }; + const config = { securityLevel: 'loose' }; let result = utils.formatUrl(url, config); expect(result).toEqual(url); @@ -425,6 +425,42 @@ describe('when parsing font sizes', function () { }); it('handles unparseable input', function () { + // @ts-expect-error Explicitly testing unparsable input expect(utils.parseFontSize({ fontSize: 14 })).toEqual([undefined, undefined]); }); }); + +describe('cleanAndMerge', () => { + test('should merge objects', () => { + expect(cleanAndMerge({ a: 1, b: 2 }, { b: 3 })).toEqual({ a: 1, b: 3 }); + expect(cleanAndMerge({ a: 1 }, { a: 2 })).toEqual({ a: 2 }); + }); + + test('should remove undefined values', () => { + expect(cleanAndMerge({ a: 1, b: 2 }, { b: undefined })).toEqual({ a: 1, b: 2 }); + expect(cleanAndMerge({ a: 1, b: 2 }, { a: 2, b: undefined })).toEqual({ a: 2, b: 2 }); + expect(cleanAndMerge({ a: 1, b: { c: 2 } }, { a: 2, b: undefined })).toEqual({ + a: 2, + b: { c: 2 }, + }); + // @ts-expect-error Explicitly testing different type + expect(cleanAndMerge({ a: 1, b: { c: 2 } }, { a: 2, b: { c: undefined } })).toEqual({ + a: 2, + b: { c: 2 }, + }); + }); + + test('should create deep copies of object', () => { + const input: { a: number; b?: number } = { a: 1 }; + const output = cleanAndMerge(input, { b: 2 }); + expect(output).toEqual({ a: 1, b: 2 }); + output.b = 3; + expect(input).toEqual({ a: 1 }); + + const inputDeep = { a: { b: 1 } }; + const outputDeep = cleanAndMerge(inputDeep, { a: { b: 2 } }); + expect(outputDeep).toEqual({ a: { b: 2 } }); + outputDeep.a.b = 3; + expect(inputDeep).toEqual({ a: { b: 1 } }); + }); +}); diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts index e48b49fcda..937f3f8f84 100644 --- a/packages/mermaid/src/utils.ts +++ b/packages/mermaid/src/utils.ts @@ -31,6 +31,7 @@ import { detectType } from './diagram-api/detectType.js'; import assignWithDepth from './assignWithDepth.js'; import { MermaidConfig } from './config.type.js'; import memoize from 'lodash-es/memoize.js'; +import merge from 'lodash-es/merge.js'; export const ZERO_WIDTH_SPACE = '\u200b'; @@ -802,7 +803,7 @@ export const calculateTextDimensions: ( ); export const initIdGenerator = class iterator { - constructor(deterministic, seed) { + constructor(deterministic, seed?: any) { this.deterministic = deterministic; // TODO: Seed is only used for length? this.seed = seed; @@ -994,12 +995,17 @@ export const parseFontSize = (fontSize: string | number | undefined): [number?, } }; +export function cleanAndMerge(defaultData: T, data?: Partial): T { + return merge({}, defaultData, data); +} + export default { assignWithDepth, wrapLabel, calculateTextHeight, calculateTextWidth, calculateTextDimensions, + cleanAndMerge, detectInit, detectDirective, isSubstringInArray,