diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index ef170fb71..f26bf4ab8 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -2,6 +2,8 @@ name: autofix.ci # needed to securely identify the workflow on: pull_request: + branches-ignore: + - 'renovate/**' permissions: contents: read diff --git a/cypress/platform/flowchart-refactor.html b/cypress/platform/flowchart-refactor.html index 034e79a52..6d9ce423f 100644 --- a/cypress/platform/flowchart-refactor.html +++ b/cypress/platform/flowchart-refactor.html @@ -822,7 +822,7 @@ flowchart LR - \ No newline at end of file + diff --git a/cypress/platform/viewer.js b/cypress/platform/viewer.js index 0b480b8bc..77da253c2 100644 --- a/cypress/platform/viewer.js +++ b/cypress/platform/viewer.js @@ -1,7 +1,7 @@ -import mermaid from './mermaid.esm.mjs'; -import { layouts } from './mermaid-layout-elk.esm.mjs'; import externalExample from './mermaid-example-diagram.esm.mjs'; +import layouts from './mermaid-layout-elk.esm.mjs'; import zenUml from './mermaid-zenuml.esm.mjs'; +import mermaid from './mermaid.esm.mjs'; function b64ToUtf8(str) { return decodeURIComponent(escape(window.atob(str))); diff --git a/docs/config/setup/interfaces/mermaid.LayoutLoaderDefinition.md b/docs/config/setup/interfaces/mermaid.LayoutLoaderDefinition.md index cb8e1a00b..441eb9209 100644 --- a/docs/config/setup/interfaces/mermaid.LayoutLoaderDefinition.md +++ b/docs/config/setup/interfaces/mermaid.LayoutLoaderDefinition.md @@ -16,7 +16,7 @@ #### Defined in -[packages/mermaid/src/rendering-util/render.ts:9](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L9) +[packages/mermaid/src/rendering-util/render.ts:25](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L25) --- @@ -26,7 +26,7 @@ #### Defined in -[packages/mermaid/src/rendering-util/render.ts:8](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L8) +[packages/mermaid/src/rendering-util/render.ts:24](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L24) --- @@ -36,4 +36,4 @@ #### Defined in -[packages/mermaid/src/rendering-util/render.ts:7](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L7) +[packages/mermaid/src/rendering-util/render.ts:23](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L23) diff --git a/docs/config/setup/interfaces/mermaid.Mermaid.md b/docs/config/setup/interfaces/mermaid.Mermaid.md index 43d25bbeb..485bc77eb 100644 --- a/docs/config/setup/interfaces/mermaid.Mermaid.md +++ b/docs/config/setup/interfaces/mermaid.Mermaid.md @@ -28,7 +28,7 @@ page. #### Defined in -[packages/mermaid/src/mermaid.ts:438](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L438) +[packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L442) --- @@ -59,7 +59,7 @@ A graph definition key #### Defined in -[packages/mermaid/src/mermaid.ts:440](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L440) +[packages/mermaid/src/mermaid.ts:444](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L444) --- @@ -89,7 +89,7 @@ Use [initialize](mermaid.Mermaid.md#initialize) and [run](mermaid.Mermaid.md#run #### Defined in -[packages/mermaid/src/mermaid.ts:433](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L433) +[packages/mermaid/src/mermaid.ts:437](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L437) --- @@ -116,56 +116,13 @@ This function should be called before the run function. #### Defined in -[packages/mermaid/src/mermaid.ts:437](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L437) - ---- - -### internalHelpers - -• **internalHelpers**: `Object` - -Internal helpers for mermaid - -**`Deprecated`** - -- This should not be used by external packages, as the definitions will change without notice. - -#### Type declaration - -| Name | Type | -| :--------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `common` | { `evaluate`: (`val?`: `string` \| `boolean`) => `boolean` ; `getMax`: (...`values`: `number`\[]) => `number` ; `getMin`: (...`values`: `number`\[]) => `number` ; `getRows`: (`s?`: `string`) => `string`\[] ; `getUrl`: (`useAbsolute`: `boolean`) => `string` ; `hasBreaks`: (`text`: `string`) => `boolean` ; `lineBreakRegex`: `RegExp` ; `removeScript`: (`txt`: `string`) => `string` ; `sanitizeText`: (`text`: `string`, `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` ; `sanitizeTextOrArray`: (`a`: `string` \| `string`\[] \| `string`\[]\[], `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` \| `string`\[] ; `splitBreaks`: (`text`: `string`) => `string`\[] } | -| `common.evaluate` | (`val?`: `string` \| `boolean`) => `boolean` | -| `common.getMax` | (...`values`: `number`\[]) => `number` | -| `common.getMin` | (...`values`: `number`\[]) => `number` | -| `common.getRows` | (`s?`: `string`) => `string`\[] | -| `common.getUrl` | (`useAbsolute`: `boolean`) => `string` | -| `common.hasBreaks` | (`text`: `string`) => `boolean` | -| `common.lineBreakRegex` | `RegExp` | -| `common.removeScript` | (`txt`: `string`) => `string` | -| `common.sanitizeText` | (`text`: `string`, `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` | -| `common.sanitizeTextOrArray` | (`a`: `string` \| `string`\[] \| `string`\[]\[], `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` \| `string`\[] | -| `common.splitBreaks` | (`text`: `string`) => `string`\[] | -| `getConfig` | () => [`MermaidConfig`](mermaid.MermaidConfig.md) | -| `insertCluster` | (`elem`: `any`, `node`: `any`) => `Promise`<`any`> | -| `insertEdge` | (`elem`: `any`, `edge`: `any`, `clusterDb`: `any`, `diagramType`: `any`, `startNode`: `any`, `endNode`: `any`, `id`: `any`) => { `originalPath`: `any` ; `updatedPath`: `any` } | -| `insertEdgeLabel` | (`elem`: `any`, `edge`: `any`) => `Promise`<`any`> | -| `insertMarkers` | (`elem`: `any`, `markerArray`: `any`, `type`: `any`, `id`: `any`) => `void` | -| `insertNode` | (`elem`: `any`, `node`: `any`, `dir`: `any`) => `Promise`<`any`> | -| `interpolateToCurve` | (`interpolate`: `undefined` \| `string`, `defaultCurve`: `CurveFactory`) => `CurveFactory` | -| `labelHelper` | (`parent`: `any`, `node`: `any`, `_classes`: `any`) => `Promise`<{ `bbox`: `any` ; `halfPadding`: `number` ; `label`: `any` = labelEl; `shapeSvg`: `any` }> | -| `log` | `Record`<`LogLevel`, (...`data`: `any`\[]) => `void`(`message?`: `any`, ...`optionalParams`: `any`\[]) => `void`> | -| `positionEdgeLabel` | (`edge`: `any`, `paths`: `any`) => `void` | - -#### Defined in - -[packages/mermaid/src/mermaid.ts:445](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L445) +[packages/mermaid/src/mermaid.ts:441](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L441) --- ### mermaidAPI -• **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.defaultConfig; `getConfig`: () => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.getConfig; `getDiagramFromText`: (`text`: `string`, `metadata`: `Pick`<`DiagramMetadata`, `"title"`>) => `Promise`<`Diagram`> ; `getSiteConfig`: () => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.getSiteConfig; `globalReset`: () => `void` ; `initialize`: (`options`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `void` ; `parse`: (`text`: `string`, `parseOptions`: [`ParseOptions`](mermaid.ParseOptions.md) & { `suppressErrors`: `true` }) => `Promise`<[`ParseResult`](mermaid.ParseResult.md) | `false`>(`text`: `string`, `parseOptions?`: [`ParseOptions`](mermaid.ParseOptions.md)) => `Promise`<[`ParseResult`](mermaid.ParseResult.md)> ; `render`: (`id`: `string`, `text`: `string`, `svgContainingElement?`: `Element`) => `Promise`<[`RenderResult`](mermaid.RenderResult.md)> ; `reset`: () => `void` ; `setConfig`: (`conf`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.setConfig; `updateSiteConfig`: (`conf`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.updateSiteConfig }> +• **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.defaultConfig; `getConfig`: () => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.getConfig; `getDiagramFromText`: (`text`: `string`, `metadata`: `Pick`<`DiagramMetadata`, `"title"`>) => `Promise`<`Diagram`> ; `getSiteConfig`: () => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.getSiteConfig; `globalReset`: () => `void` ; `initialize`: (`userOptions`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `void` ; `parse`: (`text`: `string`, `parseOptions`: [`ParseOptions`](mermaid.ParseOptions.md) & { `suppressErrors`: `true` }) => `Promise`<[`ParseResult`](mermaid.ParseResult.md) | `false`>(`text`: `string`, `parseOptions?`: [`ParseOptions`](mermaid.ParseOptions.md)) => `Promise`<[`ParseResult`](mermaid.ParseResult.md)> ; `render`: (`id`: `string`, `text`: `string`, `svgContainingElement?`: `Element`) => `Promise`<[`RenderResult`](mermaid.RenderResult.md)> ; `reset`: () => `void` ; `setConfig`: (`conf`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.setConfig; `updateSiteConfig`: (`conf`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.updateSiteConfig }> **`Deprecated`** @@ -173,7 +130,7 @@ Use [parse](mermaid.Mermaid.md#parse) and [render](mermaid.Mermaid.md#render) in #### Defined in -[packages/mermaid/src/mermaid.ts:427](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L427) +[packages/mermaid/src/mermaid.ts:431](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L431) --- @@ -223,7 +180,7 @@ Error if the diagram is invalid and parseOptions.suppressErrors is false or not #### Defined in -[packages/mermaid/src/mermaid.ts:428](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L428) +[packages/mermaid/src/mermaid.ts:432](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L432) --- @@ -233,7 +190,7 @@ Error if the diagram is invalid and parseOptions.suppressErrors is false or not #### Defined in -[packages/mermaid/src/mermaid.ts:422](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L422) +[packages/mermaid/src/mermaid.ts:426](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L426) --- @@ -261,7 +218,7 @@ Used to register external diagram types. #### Defined in -[packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L436) +[packages/mermaid/src/mermaid.ts:440](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L440) --- @@ -285,7 +242,7 @@ Used to register external diagram types. #### Defined in -[packages/mermaid/src/mermaid.ts:435](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L435) +[packages/mermaid/src/mermaid.ts:439](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L439) --- @@ -311,7 +268,7 @@ Used to register external diagram types. #### Defined in -[packages/mermaid/src/mermaid.ts:429](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L429) +[packages/mermaid/src/mermaid.ts:433](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L433) --- @@ -359,7 +316,7 @@ Renders the mermaid diagrams #### Defined in -[packages/mermaid/src/mermaid.ts:434](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L434) +[packages/mermaid/src/mermaid.ts:438](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L438) --- @@ -394,7 +351,7 @@ to it (eg. dart interop wrapper). (Initially there is no parseError member of me #### Defined in -[packages/mermaid/src/mermaid.ts:439](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L439) +[packages/mermaid/src/mermaid.ts:443](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L443) --- @@ -404,4 +361,4 @@ to it (eg. dart interop wrapper). (Initially there is no parseError member of me #### Defined in -[packages/mermaid/src/mermaid.ts:421](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L421) +[packages/mermaid/src/mermaid.ts:425](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L425) diff --git a/docs/config/setup/interfaces/mermaid.RenderOptions.md b/docs/config/setup/interfaces/mermaid.RenderOptions.md new file mode 100644 index 000000000..9319cb3b1 --- /dev/null +++ b/docs/config/setup/interfaces/mermaid.RenderOptions.md @@ -0,0 +1,19 @@ +> **Warning** +> +> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. +> +> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/interfaces/mermaid.RenderOptions.md](../../../../packages/mermaid/src/docs/config/setup/interfaces/mermaid.RenderOptions.md). + +# Interface: RenderOptions + +[mermaid](../modules/mermaid.md).RenderOptions + +## Properties + +### algorithm + +• `Optional` **algorithm**: `string` + +#### Defined in + +[packages/mermaid/src/rendering-util/render.ts:8](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L8) diff --git a/docs/config/setup/interfaces/mermaid.RunOptions.md b/docs/config/setup/interfaces/mermaid.RunOptions.md index 8fc16a415..8d38080bf 100644 --- a/docs/config/setup/interfaces/mermaid.RunOptions.md +++ b/docs/config/setup/interfaces/mermaid.RunOptions.md @@ -18,7 +18,7 @@ The nodes to render. If this is set, `querySelector` will be ignored. #### Defined in -[packages/mermaid/src/mermaid.ts:51](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L51) +[packages/mermaid/src/mermaid.ts:55](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L55) --- @@ -44,7 +44,7 @@ A callback to call after each diagram is rendered. #### Defined in -[packages/mermaid/src/mermaid.ts:55](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L55) +[packages/mermaid/src/mermaid.ts:59](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L59) --- @@ -56,7 +56,7 @@ The query selector to use when finding elements to render. Default: `".mermaid"` #### Defined in -[packages/mermaid/src/mermaid.ts:47](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L47) +[packages/mermaid/src/mermaid.ts:51](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L51) --- @@ -68,4 +68,4 @@ If `true`, errors will be logged to the console, but not thrown. Default: `false #### Defined in -[packages/mermaid/src/mermaid.ts:59](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L59) +[packages/mermaid/src/mermaid.ts:63](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L63) diff --git a/docs/config/setup/modules/defaultConfig.md b/docs/config/setup/modules/defaultConfig.md index 0f0ace33c..0a3e15855 100644 --- a/docs/config/setup/modules/defaultConfig.md +++ b/docs/config/setup/modules/defaultConfig.md @@ -14,7 +14,7 @@ #### Defined in -[packages/mermaid/src/defaultConfig.ts:279](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L279) +[packages/mermaid/src/defaultConfig.ts:266](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L266) --- diff --git a/docs/config/setup/modules/mermaid.md b/docs/config/setup/modules/mermaid.md index 680c4cafc..a4361298e 100644 --- a/docs/config/setup/modules/mermaid.md +++ b/docs/config/setup/modules/mermaid.md @@ -20,6 +20,7 @@ - [MermaidConfig](../interfaces/mermaid.MermaidConfig.md) - [ParseOptions](../interfaces/mermaid.ParseOptions.md) - [ParseResult](../interfaces/mermaid.ParseResult.md) +- [RenderOptions](../interfaces/mermaid.RenderOptions.md) - [RenderResult](../interfaces/mermaid.RenderResult.md) - [RunOptions](../interfaces/mermaid.RunOptions.md) @@ -60,6 +61,16 @@ --- +### InternalHelpers + +Ƭ **InternalHelpers**: typeof `internalHelpers` + +#### Defined in + +[packages/mermaid/src/internals.ts:33](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/internals.ts#L33) + +--- + ### ParseErrorFunction Ƭ **ParseErrorFunction**: (`err`: `string` | [`DetailedError`](../interfaces/mermaid.DetailedError.md) | `unknown`, `hash?`: `any`) => `void` @@ -83,6 +94,26 @@ [packages/mermaid/src/Diagram.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/Diagram.ts#L10) +--- + +### SVG + +Ƭ **SVG**: `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`> + +#### Defined in + +[packages/mermaid/src/diagram-api/types.ts:130](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L130) + +--- + +### SVGGroup + +Ƭ **SVGGroup**: `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`> + +#### Defined in + +[packages/mermaid/src/diagram-api/types.ts:132](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L132) + ## Variables ### default @@ -91,7 +122,7 @@ #### Defined in -[packages/mermaid/src/mermaid.ts:448](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L448) +[packages/mermaid/src/mermaid.ts:447](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L447) ## Functions diff --git a/docs/ecosystem/integrations-community.md b/docs/ecosystem/integrations-community.md index 6b9190a9d..25be52fd6 100644 --- a/docs/ecosystem/integrations-community.md +++ b/docs/ecosystem/integrations-community.md @@ -56,6 +56,7 @@ To add an integration to this list, see the [Integrations - create page](./integ - [SVG diagram generator](https://github.com/SimonKenyonShepard/mermaidjs-github-svg-generator) - [GitLab](https://docs.gitlab.com/ee/user/markdown.html#diagrams-and-flowcharts) ✅ - [Mermaid Plugin for JetBrains IDEs](https://plugins.jetbrains.com/plugin/20146-mermaid) +- [MonsterWriter](https://www.monsterwriter.com/) ✅ - [Joplin](https://joplinapp.org) ✅ - [LiveBook](https://livebook.dev) ✅ - [Tuleap](https://docs.tuleap.org/user-guide/writing-in-tuleap.html#graphs) ✅ diff --git a/package.json b/package.json index 4e17d516c..fecb919d6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "10.2.4", "description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.", "type": "module", - "packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a", + "packageManager": "pnpm@9.7.1+sha512.faf344af2d6ca65c4c5c8c2224ea77a81a5e8859cbc4e06b1511ddce2f0151512431dd19e6aff31f2c6a8f5f2aced9bd2273e1fed7dd4de1868984059d2c4247", "keywords": [ "diagram", "markdown", diff --git a/packages/mermaid-flowchart-elk/package.json b/packages/mermaid-flowchart-elk/package.json deleted file mode 100644 index dc3af195c..000000000 --- a/packages/mermaid-flowchart-elk/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "@mermaid-js/flowchart-elk", - "version": "1.0.0-rc.1", - "description": "Flowchart plugin for mermaid with ELK layout", - "module": "dist/mermaid-flowchart-elk.core.mjs", - "types": "dist/packages/mermaid-flowchart-elk/src/detector.d.ts", - "type": "module", - "exports": { - ".": { - "import": "./dist/mermaid-flowchart-elk.core.mjs", - "types": "./dist/packages/mermaid-flowchart-elk/src/detector.d.ts" - }, - "./*": "./*" - }, - "keywords": [ - "diagram", - "markdown", - "flowchart", - "elk", - "mermaid" - ], - "scripts": { - "prepublishOnly": "pnpm -w run build" - }, - "repository": { - "type": "git", - "url": "https://github.com/mermaid-js/mermaid" - }, - "author": "Knut Sveidqvist", - "license": "MIT", - "dependencies": { - "d3": "^7.9.0", - "dagre-d3-es": "7.0.10", - "elkjs": "^0.9.2", - "khroma": "^2.1.0" - }, - "devDependencies": { - "concurrently": "^8.2.2", - "mermaid": "workspace:^", - "rimraf": "^5.0.5" - }, - "files": [ - "dist" - ] -} diff --git a/packages/mermaid-flowchart-elk/src/detector.spec.ts b/packages/mermaid-flowchart-elk/src/detector.spec.ts deleted file mode 100644 index 94ac22d26..000000000 --- a/packages/mermaid-flowchart-elk/src/detector.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import plugin from './detector.js'; -import { describe, it } from 'vitest'; - -const { detector } = plugin; - -describe('flowchart-elk detector', () => { - it('should fail for dagre-d3', () => { - expect( - detector('flowchart', { - flowchart: { - defaultRenderer: 'dagre-d3', - }, - }) - ).toBe(false); - }); - it('should fail for dagre-wrapper', () => { - expect( - detector('flowchart', { - flowchart: { - defaultRenderer: 'dagre-wrapper', - }, - }) - ).toBe(false); - }); - it('should succeed for elk', () => { - expect( - detector('flowchart', { - flowchart: { - defaultRenderer: 'elk', - }, - }) - ).toBe(true); - expect( - detector('graph', { - flowchart: { - defaultRenderer: 'elk', - }, - }) - ).toBe(true); - }); - - // The error from the issue was reproduced with mindmap, so this is just an example - // what matters is the keyword somewhere inside graph definition - it('should check only the beginning of the line in search of keywords', () => { - expect( - detector('mindmap ["Descendant node in flowchart"]', { - flowchart: { - defaultRenderer: 'elk', - }, - }) - ).toBe(false); - - expect( - detector('mindmap ["Descendant node in graph"]', { - flowchart: { - defaultRenderer: 'elk', - }, - }) - ).toBe(false); - }); - - it('should detect flowchart-elk', () => { - expect(detector('flowchart-elk')).toBe(true); - }); - - it('should not detect class with defaultRenderer set to elk', () => { - expect( - detector('class', { - flowchart: { - defaultRenderer: 'elk', - }, - }) - ).toBe(false); - }); -}); diff --git a/packages/mermaid-flowchart-elk/src/detector.ts b/packages/mermaid-flowchart-elk/src/detector.ts deleted file mode 100644 index 3b168bd61..000000000 --- a/packages/mermaid-flowchart-elk/src/detector.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { - ExternalDiagramDefinition, - DiagramDetector, - DiagramLoader, -} from '../../mermaid/src/diagram-api/types.js'; - -const id = 'flowchart-elk'; - -const detector: DiagramDetector = (txt, config): boolean => { - if ( - // If diagram explicitly states flowchart-elk - /^\s*flowchart-elk/.test(txt) || - // If a flowchart/graph diagram has their default renderer set to elk - (/^\s*(flowchart|graph)/.test(txt) && config?.flowchart?.defaultRenderer === 'elk') - ) { - return true; - } - return false; -}; - -const loader: DiagramLoader = async () => { - const { diagram } = await import('./diagram-definition.js'); - return { id, diagram }; -}; - -const plugin: ExternalDiagramDefinition = { - id, - detector, - loader, -}; - -export default plugin; diff --git a/packages/mermaid-flowchart-elk/src/diagram-definition.ts b/packages/mermaid-flowchart-elk/src/diagram-definition.ts deleted file mode 100644 index 4a6b5b076..000000000 --- a/packages/mermaid-flowchart-elk/src/diagram-definition.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-ignore: JISON typing missing -import parser from '../../mermaid/src/diagrams/flowchart/parser/flow.jison'; -import db from '../../mermaid/src/diagrams/flowchart/flowDb.js'; -import styles from '../../mermaid/src/diagrams/flowchart/styles.js'; -import renderer from './flowRenderer-elk.js'; - -export const diagram = { - db, - renderer, - parser, - styles, -}; diff --git a/packages/mermaid-flowchart-elk/src/flowRenderer-elk.js b/packages/mermaid-flowchart-elk/src/flowRenderer-elk.js deleted file mode 100644 index fc540073a..000000000 --- a/packages/mermaid-flowchart-elk/src/flowRenderer-elk.js +++ /dev/null @@ -1,888 +0,0 @@ -import { select, line, curveLinear } from 'd3'; -import { insertNode } from '../../mermaid/src/dagre-wrapper/nodes.js'; -import insertMarkers from '../../mermaid/src/dagre-wrapper/markers.js'; -import { insertEdgeLabel } from '../../mermaid/src/dagre-wrapper/edges.js'; -import { findCommonAncestor } from './render-utils.js'; -import { labelHelper } from '../../mermaid/src/dagre-wrapper/shapes/util.js'; -import { getConfig } from '../../mermaid/src/config.js'; -import { log } from '../../mermaid/src/logger.js'; -import utils from '../../mermaid/src/utils.js'; -import { setupGraphViewbox } from '../../mermaid/src/setupGraphViewbox.js'; -import common from '../../mermaid/src/diagrams/common/common.js'; -import { interpolateToCurve, getStylesFromArray } from '../../mermaid/src/utils.js'; -import ELK from 'elkjs/lib/elk.bundled.js'; -import { getLineFunctionsWithOffset } from '../../mermaid/src/utils/lineWithOffset.js'; -import { addEdgeMarkers } from '../../mermaid/src/dagre-wrapper/edgeMarker.js'; - -const elk = new ELK(); - -let portPos = {}; - -const conf = {}; -export const setConf = function (cnf) { - const keys = Object.keys(cnf); - for (const key of keys) { - conf[key] = cnf[key]; - } -}; - -let nodeDb = {}; - -// /** -// * Function that adds the vertices found during parsing to the graph to be rendered. -// * -// * @param vert Object containing the vertices. -// * @param g The graph that is to be drawn. -// * @param svgId -// * @param root -// * @param doc -// * @param diagObj -// */ -export const addVertices = async function (vert, svgId, root, doc, diagObj, parentLookupDb, graph) { - const svg = root.select(`[id="${svgId}"]`); - const nodes = svg.insert('g').attr('class', 'nodes'); - const keys = [...vert.keys()]; - - // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition - await Promise.all( - keys.map(async function (id) { - const vertex = vert.get(id); - - /** - * Variable for storing the classes for the vertex - * - * @type {string} - */ - let classStr = 'default'; - if (vertex.classes.length > 0) { - classStr = vertex.classes.join(' '); - } - classStr = classStr + ' flowchart-label'; - const styles = getStylesFromArray(vertex.styles); - - // Use vertex id as text in the box if no text is provided by the graph definition - let vertexText = vertex.text !== undefined ? vertex.text : vertex.id; - - // We create a SVG label, either by delegating to addHtmlLabel or manually - const labelData = { width: 0, height: 0 }; - - const ports = [ - { - id: vertex.id + '-west', - layoutOptions: { - 'port.side': 'WEST', - }, - }, - { - id: vertex.id + '-east', - layoutOptions: { - 'port.side': 'EAST', - }, - }, - { - id: vertex.id + '-south', - layoutOptions: { - 'port.side': 'SOUTH', - }, - }, - { - id: vertex.id + '-north', - layoutOptions: { - 'port.side': 'NORTH', - }, - }, - ]; - - let radius = 0; - let _shape = ''; - let layoutOptions = {}; - // Set the shape based parameters - switch (vertex.type) { - case 'round': - radius = 5; - _shape = 'rect'; - break; - case 'square': - _shape = 'rect'; - break; - case 'diamond': - _shape = 'question'; - layoutOptions = { - portConstraints: 'FIXED_SIDE', - }; - break; - case 'hexagon': - _shape = 'hexagon'; - break; - case 'odd': - _shape = 'rect_left_inv_arrow'; - break; - case 'lean_right': - _shape = 'lean_right'; - break; - case 'lean_left': - _shape = 'lean_left'; - break; - case 'trapezoid': - _shape = 'trapezoid'; - break; - case 'inv_trapezoid': - _shape = 'inv_trapezoid'; - break; - case 'odd_right': - _shape = 'rect_left_inv_arrow'; - break; - case 'circle': - _shape = 'circle'; - break; - case 'ellipse': - _shape = 'ellipse'; - break; - case 'stadium': - _shape = 'stadium'; - break; - case 'subroutine': - _shape = 'subroutine'; - break; - case 'cylinder': - _shape = 'cylinder'; - break; - case 'group': - _shape = 'rect'; - break; - case 'doublecircle': - _shape = 'doublecircle'; - break; - default: - _shape = 'rect'; - } - - // Add the node - const node = { - labelStyle: styles.labelStyle, - shape: _shape, - labelText: vertexText, - labelType: vertex.labelType, - rx: radius, - ry: radius, - class: classStr, - style: styles.style, - id: vertex.id, - link: vertex.link, - linkTarget: vertex.linkTarget, - tooltip: diagObj.db.getTooltip(vertex.id) || '', - domId: diagObj.db.lookUpDomId(vertex.id), - haveCallback: vertex.haveCallback, - width: vertex.type === 'group' ? 500 : undefined, - dir: vertex.dir, - type: vertex.type, - props: vertex.props, - padding: getConfig().flowchart.padding, - }; - let boundingBox; - let nodeEl; - - // Add the element to the DOM - if (node.type !== 'group') { - nodeEl = await insertNode(nodes, node, vertex.dir); - boundingBox = nodeEl.node().getBBox(); - } else { - const { shapeSvg, bbox } = await labelHelper(nodes, node, undefined, true); - labelData.width = bbox.width; - labelData.wrappingWidth = getConfig().flowchart.wrappingWidth; - labelData.height = bbox.height; - labelData.labelNode = shapeSvg.node(); - node.labelData = labelData; - } - // const { shapeSvg, bbox } = await labelHelper(svg, node, undefined, true); - - const data = { - id: vertex.id, - ports: vertex.type === 'diamond' ? ports : [], - // labelStyle: styles.labelStyle, - // shape: _shape, - layoutOptions, - labelText: vertexText, - labelData, - // labels: [{ text: vertexText }], - // rx: radius, - // ry: radius, - // class: classStr, - // style: styles.style, - // link: vertex.link, - // linkTarget: vertex.linkTarget, - // tooltip: diagObj.db.getTooltip(vertex.id) || '', - domId: diagObj.db.lookUpDomId(vertex.id), - // haveCallback: vertex.haveCallback, - width: boundingBox?.width, - height: boundingBox?.height, - // dir: vertex.dir, - type: vertex.type, - // props: vertex.props, - // padding: getConfig().flowchart.padding, - // boundingBox, - el: nodeEl, - parent: parentLookupDb.parentById[vertex.id], - }; - // if (!Object.keys(parentLookupDb.childrenById).includes(vertex.id)) { - // graph.children.push({ - // ...data, - // }); - // } - nodeDb[node.id] = data; - // log.trace('setNode', { - // labelStyle: styles.labelStyle, - // shape: _shape, - // labelText: vertexText, - // rx: radius, - // ry: radius, - // class: classStr, - // style: styles.style, - // id: vertex.id, - // domId: diagObj.db.lookUpDomId(vertex.id), - // width: vertex.type === 'group' ? 500 : undefined, - // type: vertex.type, - // dir: vertex.dir, - // props: vertex.props, - // padding: getConfig().flowchart.padding, - // parent: parentLookupDb.parentById[vertex.id], - // }); - }) - ); - return graph; -}; - -const getNextPosition = (position, edgeDirection, graphDirection) => { - const portPos = { - TB: { - in: { - north: 'north', - }, - out: { - south: 'west', - west: 'east', - east: 'south', - }, - }, - LR: { - in: { - west: 'west', - }, - out: { - east: 'south', - south: 'north', - north: 'east', - }, - }, - RL: { - in: { - east: 'east', - }, - out: { - west: 'north', - north: 'south', - south: 'west', - }, - }, - BT: { - in: { - south: 'south', - }, - out: { - north: 'east', - east: 'west', - west: 'north', - }, - }, - }; - portPos.TD = portPos.TB; - return portPos[graphDirection][edgeDirection][position]; - // return 'south'; -}; - -const getNextPort = (node, edgeDirection, graphDirection) => { - log.info('getNextPort', { node, edgeDirection, graphDirection }); - if (!portPos[node]) { - switch (graphDirection) { - case 'TB': - case 'TD': - portPos[node] = { - inPosition: 'north', - outPosition: 'south', - }; - break; - case 'BT': - portPos[node] = { - inPosition: 'south', - outPosition: 'north', - }; - break; - case 'RL': - portPos[node] = { - inPosition: 'east', - outPosition: 'west', - }; - break; - case 'LR': - portPos[node] = { - inPosition: 'west', - outPosition: 'east', - }; - break; - } - } - const result = edgeDirection === 'in' ? portPos[node].inPosition : portPos[node].outPosition; - - if (edgeDirection === 'in') { - portPos[node].inPosition = getNextPosition( - portPos[node].inPosition, - edgeDirection, - graphDirection - ); - } else { - portPos[node].outPosition = getNextPosition( - portPos[node].outPosition, - edgeDirection, - graphDirection - ); - } - return result; -}; - -const getEdgeStartEndPoint = (edge, dir) => { - let source = edge.start; - let target = edge.end; - - // Save the original source and target - const sourceId = source; - const targetId = target; - - const startNode = nodeDb[source]; - const endNode = nodeDb[target]; - - if (!startNode || !endNode) { - return { source, target }; - } - - if (startNode.type === 'diamond') { - source = `${source}-${getNextPort(source, 'out', dir)}`; - } - - if (endNode.type === 'diamond') { - target = `${target}-${getNextPort(target, 'in', dir)}`; - } - - // Add the edge to the graph - return { source, target, sourceId, targetId }; -}; - -/** - * Add edges to graph based on parsed graph definition - * - * @param {object} edges The edges to add to the graph - * @param {object} g The graph object - * @param cy - * @param diagObj - * @param graph - * @param svg - */ -export const addEdges = function (edges, diagObj, graph, svg) { - log.info('abc78 edges = ', edges); - const labelsEl = svg.insert('g').attr('class', 'edgeLabels'); - let linkIdCnt = {}; - let dir = diagObj.db.getDirection(); - let defaultStyle; - let defaultLabelStyle; - - if (edges.defaultStyle !== undefined) { - const defaultStyles = getStylesFromArray(edges.defaultStyle); - defaultStyle = defaultStyles.style; - defaultLabelStyle = defaultStyles.labelStyle; - } - - edges.forEach(function (edge) { - // Identify Link - const linkIdBase = 'L-' + edge.start + '-' + edge.end; - // count the links from+to the same node to give unique id - if (linkIdCnt[linkIdBase] === undefined) { - linkIdCnt[linkIdBase] = 0; - log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); - } else { - linkIdCnt[linkIdBase]++; - log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); - } - let linkId = linkIdBase + '-' + linkIdCnt[linkIdBase]; - log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]); - const linkNameStart = 'LS-' + edge.start; - const linkNameEnd = 'LE-' + edge.end; - - const edgeData = { style: '', labelStyle: '' }; - edgeData.minlen = edge.length || 1; - //edgeData.id = 'id' + cnt; - - // Set link type for rendering - if (edge.type === 'arrow_open') { - edgeData.arrowhead = 'none'; - } else { - edgeData.arrowhead = 'normal'; - } - - // Check of arrow types, placed here in order not to break old rendering - edgeData.arrowTypeStart = 'arrow_open'; - edgeData.arrowTypeEnd = 'arrow_open'; - - /* eslint-disable no-fallthrough */ - switch (edge.type) { - case 'double_arrow_cross': - edgeData.arrowTypeStart = 'arrow_cross'; - case 'arrow_cross': - edgeData.arrowTypeEnd = 'arrow_cross'; - break; - case 'double_arrow_point': - edgeData.arrowTypeStart = 'arrow_point'; - case 'arrow_point': - edgeData.arrowTypeEnd = 'arrow_point'; - break; - case 'double_arrow_circle': - edgeData.arrowTypeStart = 'arrow_circle'; - case 'arrow_circle': - edgeData.arrowTypeEnd = 'arrow_circle'; - break; - } - - let style = ''; - let labelStyle = ''; - - switch (edge.stroke) { - case 'normal': - style = 'fill:none;'; - if (defaultStyle !== undefined) { - style = defaultStyle; - } - if (defaultLabelStyle !== undefined) { - labelStyle = defaultLabelStyle; - } - edgeData.thickness = 'normal'; - edgeData.pattern = 'solid'; - break; - case 'dotted': - edgeData.thickness = 'normal'; - edgeData.pattern = 'dotted'; - edgeData.style = 'fill:none;stroke-width:2px;stroke-dasharray:3;'; - break; - case 'thick': - edgeData.thickness = 'thick'; - edgeData.pattern = 'solid'; - edgeData.style = 'stroke-width: 3.5px;fill:none;'; - break; - } - if (edge.style !== undefined) { - const styles = getStylesFromArray(edge.style); - style = styles.style; - labelStyle = styles.labelStyle; - } - - edgeData.style = edgeData.style += style; - edgeData.labelStyle = edgeData.labelStyle += labelStyle; - - if (edge.interpolate !== undefined) { - edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear); - } else if (edges.defaultInterpolate !== undefined) { - edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear); - } else { - edgeData.curve = interpolateToCurve(conf.curve, curveLinear); - } - - if (edge.text === undefined) { - if (edge.style !== undefined) { - edgeData.arrowheadStyle = 'fill: #333'; - } - } else { - edgeData.arrowheadStyle = 'fill: #333'; - edgeData.labelpos = 'c'; - } - - edgeData.labelType = edge.labelType; - edgeData.label = edge.text.replace(common.lineBreakRegex, '\n'); - - if (edge.style === undefined) { - edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;'; - } - - edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:'); - - edgeData.id = linkId; - edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd; - - const labelEl = insertEdgeLabel(labelsEl, edgeData); - - // calculate start and end points of the edge, note that the source and target - // can be modified for shapes that have ports - const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge, dir); - log.debug('abc78 source and target', source, target); - // Add the edge to the graph - graph.edges.push({ - id: 'e' + edge.start + edge.end, - sources: [source], - targets: [target], - sourceId, - targetId, - labelEl: labelEl, - labels: [ - { - width: edgeData.width, - height: edgeData.height, - orgWidth: edgeData.width, - orgHeight: edgeData.height, - text: edgeData.label, - layoutOptions: { - 'edgeLabels.inline': 'true', - 'edgeLabels.placement': 'CENTER', - }, - }, - ], - edgeData, - }); - }); - return graph; -}; - -// TODO: break out and share with dagre wrapper. The current code in dagre wrapper also adds -// adds the line to the graph, but we don't need that here. This is why we can't use the dagre -// wrapper directly for this -/** - * Add the markers to the edge depending on the type of arrow is - * @param svgPath - * @param edgeData - * @param diagramType - * @param arrowMarkerAbsolute - * @param id - */ -const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAbsolute, id) { - let url = ''; - // Check configuration for absolute path - if (arrowMarkerAbsolute) { - url = - window.location.protocol + - '//' + - window.location.host + - window.location.pathname + - window.location.search; - url = url.replace(/\(/g, '\\('); - url = url.replace(/\)/g, '\\)'); - } - - // look in edge data and decide which marker to use - addEdgeMarkers(svgPath, edgeData, url, id, diagramType); -}; - -/** - * Returns the all the styles from classDef statements in the graph definition. - * - * @param text - * @param diagObj - * @returns {Map} ClassDef styles - */ -export const getClasses = function (text, diagObj) { - log.info('Extracting classes'); - return diagObj.db.getClasses(); -}; - -const addSubGraphs = function (db) { - const parentLookupDb = { parentById: {}, childrenById: {} }; - const subgraphs = db.getSubGraphs(); - log.info('Subgraphs - ', subgraphs); - subgraphs.forEach(function (subgraph) { - subgraph.nodes.forEach(function (node) { - parentLookupDb.parentById[node] = subgraph.id; - if (parentLookupDb.childrenById[subgraph.id] === undefined) { - parentLookupDb.childrenById[subgraph.id] = []; - } - parentLookupDb.childrenById[subgraph.id].push(node); - }); - }); - - subgraphs.forEach(function (subgraph) { - const data = { id: subgraph.id }; - if (parentLookupDb.parentById[subgraph.id] !== undefined) { - data.parent = parentLookupDb.parentById[subgraph.id]; - } - }); - return parentLookupDb; -}; - -const calcOffset = function (src, dest, parentLookupDb) { - const ancestor = findCommonAncestor(src, dest, parentLookupDb); - if (ancestor === undefined || ancestor === 'root') { - return { x: 0, y: 0 }; - } - - const ancestorOffset = nodeDb[ancestor].offset; - return { x: ancestorOffset.posX, y: ancestorOffset.posY }; -}; - -const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb, id) { - const offset = calcOffset(edge.sourceId, edge.targetId, parentLookupDb); - - const src = edge.sections[0].startPoint; - const dest = edge.sections[0].endPoint; - const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : []; - - const segPoints = segments.map((segment) => [segment.x + offset.x, segment.y + offset.y]); - const points = [ - [src.x + offset.x, src.y + offset.y], - ...segPoints, - [dest.x + offset.x, dest.y + offset.y], - ]; - - const { x, y } = getLineFunctionsWithOffset(edge.edgeData); - const curve = line().x(x).y(y).curve(curveLinear); - const edgePath = edgesEl - .insert('path') - .attr('d', curve(points)) - .attr('class', 'path ' + edgeData.classes) - .attr('fill', 'none'); - Object.entries(edgeData).forEach(([key, value]) => { - if (key !== 'classes') { - edgePath.attr(key, value); - } - }); - const edgeG = edgesEl.insert('g').attr('class', 'edgeLabel'); - const edgeWithLabel = select(edgeG.node().appendChild(edge.labelEl)); - const box = edgeWithLabel.node().firstChild.getBoundingClientRect(); - edgeWithLabel.attr('width', box.width); - edgeWithLabel.attr('height', box.height); - - edgeG.attr( - 'transform', - `translate(${edge.labels[0].x + offset.x}, ${edge.labels[0].y + offset.y})` - ); - addMarkersToEdge(edgePath, edgeData, diagObj.type, diagObj.arrowMarkerAbsolute, id); -}; - -/** - * Recursive function that iterates over an array of nodes and inserts the children of each node. - * It also recursively populates the inserts the children of the children and so on. - * @param nodeArray - * @param parentLookupDb - */ -const insertChildren = (nodeArray, parentLookupDb) => { - nodeArray.forEach((node) => { - // Check if we have reached the end of the tree - if (!node.children) { - node.children = []; - } - // Check if the node has children - const childIds = parentLookupDb.childrenById[node.id]; - // If the node has children, add them to the node - if (childIds) { - childIds.forEach((childId) => { - node.children.push(nodeDb[childId]); - }); - } - // Recursive call - insertChildren(node.children, parentLookupDb); - }); -}; - -/** - * Draws a flowchart in the tag with id: id based on the graph definition in text. - * - * @param text - * @param id - */ - -export const draw = async function (text, id, _version, diagObj) { - const { securityLevel, flowchart: conf } = getConfig(); - nodeDb = {}; - portPos = {}; - const renderEl = select('body').append('div').attr('style', 'height:400px').attr('id', 'cy'); - let graph = { - id: 'root', - layoutOptions: { - 'elk.hierarchyHandling': 'INCLUDE_CHILDREN', - 'elk.layered.spacing.edgeNodeBetweenLayers': conf?.nodeSpacing ? `${conf.nodeSpacing}` : '30', - // 'elk.layered.mergeEdges': 'true', - 'elk.direction': 'DOWN', - // 'elk.ports.sameLayerEdges': true, - // 'nodePlacement.strategy': 'SIMPLE', - }, - children: [], - edges: [], - }; - log.info('Drawing flowchart using v3 renderer', elk); - - // Set the direction, - // Fetch the default direction, use TD if none was found - let dir = diagObj.db.getDirection(); - switch (dir) { - case 'BT': - graph.layoutOptions['elk.direction'] = 'UP'; - break; - case 'TB': - graph.layoutOptions['elk.direction'] = 'DOWN'; - break; - case 'LR': - graph.layoutOptions['elk.direction'] = 'RIGHT'; - break; - case 'RL': - graph.layoutOptions['elk.direction'] = 'LEFT'; - break; - } - - // Find the root dom node to ne used in rendering - // Handle root and document for when rendering in sandbox mode - let sandboxElement; - if (securityLevel === 'sandbox') { - sandboxElement = select('#i' + id); - } - const root = - securityLevel === 'sandbox' - ? select(sandboxElement.nodes()[0].contentDocument.body) - : select('body'); - const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document; - - const svg = root.select(`[id="${id}"]`); - // Define the supported markers for the diagram - const markers = ['point', 'circle', 'cross']; - - // Add the marker definitions to the svg as marker tags - insertMarkers(svg, markers, diagObj.type, id); - - // Fetch the vertices/nodes and edges/links from the parsed graph definition - const vert = diagObj.db.getVertices(); - - // Setup nodes from the subgraphs with type group, these will be used - // as nodes with children in the subgraph - let subG; - const subGraphs = diagObj.db.getSubGraphs(); - log.info('Subgraphs - ', subGraphs); - for (let i = subGraphs.length - 1; i >= 0; i--) { - subG = subGraphs[i]; - diagObj.db.addVertex( - subG.id, - { text: subG.title, type: subG.labelType }, - 'group', - undefined, - subG.classes, - subG.dir - ); - } - - // debugger; - // Add an element in the svg to be used to hold the subgraphs container - // elements - const subGraphsEl = svg.insert('g').attr('class', 'subgraphs'); - - // Create the lookup db for the subgraphs and their children to used when creating - // the tree structured graph - const parentLookupDb = addSubGraphs(diagObj.db); - - // Add the nodes to the graph, this will entail creating the actual nodes - // in order to get the size of the node. You can't get the size of a node - // that is not in the dom so we need to add it to the dom, get the size - // we will position the nodes when we get the layout from elkjs - graph = await addVertices(vert, id, root, doc, diagObj, parentLookupDb, graph); - - // Time for the edges, we start with adding an element in the node to hold the edges - const edgesEl = svg.insert('g').attr('class', 'edges edgePath'); - // Fetch the edges form the parsed graph definition - const edges = diagObj.db.getEdges(); - - // Add the edges to the graph, this will entail creating the actual edges - graph = addEdges(edges, diagObj, graph, svg); - - // Iterate through all nodes and add the top level nodes to the graph - const nodes = Object.keys(nodeDb); - nodes.forEach((nodeId) => { - const node = nodeDb[nodeId]; - if (!node.parent) { - graph.children.push(node); - } - // Subgraph - if (parentLookupDb.childrenById[nodeId] !== undefined) { - node.labels = [ - { - text: node.labelText, - layoutOptions: { - 'nodeLabels.placement': '[H_CENTER, V_TOP, INSIDE]', - }, - width: node.labelData.width, - height: node.labelData.height, - // width: 100, - // height: 100, - }, - ]; - delete node.x; - delete node.y; - delete node.width; - delete node.height; - } - }); - - insertChildren(graph.children, parentLookupDb); - log.info('after layout', JSON.stringify(graph, null, 2)); - const g = await elk.layout(graph); - drawNodes(0, 0, g.children, svg, subGraphsEl, diagObj, 0); - utils.insertTitle(svg, 'flowchartTitleText', conf.titleTopMargin, diagObj.db.getDiagramTitle()); - log.info('after layout', g); - g.edges?.map((edge) => { - insertEdge(edgesEl, edge, edge.edgeData, diagObj, parentLookupDb, id); - }); - setupGraphViewbox({}, svg, conf.diagramPadding, conf.useMaxWidth); - // Remove element after layout - renderEl.remove(); -}; - -const drawNodes = (relX, relY, nodeArray, svg, subgraphsEl, diagObj, depth) => { - nodeArray.forEach(function (node) { - if (node) { - nodeDb[node.id].offset = { - posX: node.x + relX, - posY: node.y + relY, - x: relX, - y: relY, - depth, - width: node.width, - height: node.height, - }; - if (node.type === 'group') { - const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph'); - subgraphEl - .insert('rect') - .attr('class', 'subgraph subgraph-lvl-' + (depth % 5) + ' node') - .attr('x', node.x + relX) - .attr('y', node.y + relY) - .attr('width', node.width) - .attr('height', node.height); - const label = subgraphEl.insert('g').attr('class', 'label'); - const labelCentering = getConfig().flowchart.htmlLabels ? node.labelData.width / 2 : 0; - label.attr( - 'transform', - `translate(${node.labels[0].x + relX + node.x + labelCentering}, ${ - node.labels[0].y + relY + node.y + 3 - })` - ); - label.node().appendChild(node.labelData.labelNode); - - log.info('Id (UGH)= ', node.type, node.labels); - } else { - log.info('Id (UGH)= ', node.id); - node.el.attr( - 'transform', - `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})` - ); - } - } - }); - nodeArray.forEach(function (node) { - if (node && node.type === 'group') { - drawNodes(relX + node.x, relY + node.y, node.children, svg, subgraphsEl, diagObj, depth + 1); - } - }); -}; - -export default { - getClasses, - draw, -}; diff --git a/packages/mermaid-flowchart-elk/src/render-utils.spec.ts b/packages/mermaid-flowchart-elk/src/render-utils.spec.ts deleted file mode 100644 index 046ed43c1..000000000 --- a/packages/mermaid-flowchart-elk/src/render-utils.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { TreeData } from './render-utils.js'; -import { findCommonAncestor } from './render-utils.js'; -describe('when rendering a flowchart using elk ', () => { - let lookupDb: TreeData; - beforeEach(() => { - lookupDb = { - parentById: { - B4: 'inner', - B5: 'inner', - C4: 'inner2', - C5: 'inner2', - B2: 'Ugge', - B3: 'Ugge', - inner: 'Ugge', - inner2: 'Ugge', - B6: 'outer', - }, - childrenById: { - inner: ['B4', 'B5'], - inner2: ['C4', 'C5'], - Ugge: ['B2', 'B3', 'inner', 'inner2'], - outer: ['B6'], - }, - }; - }); - it('to find parent of siblings in a subgraph', () => { - expect(findCommonAncestor('B4', 'B5', lookupDb)).toBe('inner'); - }); - it('to find an uncle', () => { - expect(findCommonAncestor('B4', 'B2', lookupDb)).toBe('Ugge'); - }); - it('to find a cousin', () => { - expect(findCommonAncestor('B4', 'C4', lookupDb)).toBe('Ugge'); - }); - it('to find a grandparent', () => { - expect(findCommonAncestor('B4', 'B6', lookupDb)).toBe('root'); - }); - it('to find ancestor of siblings in the root', () => { - expect(findCommonAncestor('B1', 'outer', lookupDb)).toBe('root'); - }); -}); diff --git a/packages/mermaid-flowchart-elk/src/render-utils.ts b/packages/mermaid-flowchart-elk/src/render-utils.ts deleted file mode 100644 index ebdc01cf7..000000000 --- a/packages/mermaid-flowchart-elk/src/render-utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -export interface TreeData { - parentById: Record; - childrenById: Record; -} - -export const findCommonAncestor = (id1: string, id2: string, treeData: TreeData) => { - const { parentById } = treeData; - const visited = new Set(); - let currentId = id1; - while (currentId) { - visited.add(currentId); - if (currentId === id2) { - return currentId; - } - currentId = parentById[currentId]; - } - currentId = id2; - while (currentId) { - if (visited.has(currentId)) { - return currentId; - } - currentId = parentById[currentId]; - } - return 'root'; -}; diff --git a/packages/mermaid-flowchart-elk/src/styles.ts b/packages/mermaid-flowchart-elk/src/styles.ts deleted file mode 100644 index 60659df45..000000000 --- a/packages/mermaid-flowchart-elk/src/styles.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** Returns the styles given options */ -export interface FlowChartStyleOptions { - arrowheadColor: string; - border2: string; - clusterBkg: string; - clusterBorder: string; - edgeLabelBackground: string; - fontFamily: string; - lineColor: string; - mainBkg: string; - nodeBorder: string; - nodeTextColor: string; - tertiaryColor: string; - textColor: string; - titleColor: string; - [key: string]: string; -} - -const genSections = (options: FlowChartStyleOptions) => { - let sections = ''; - - for (let i = 0; i < 5; i++) { - sections += ` - .subgraph-lvl-${i} { - fill: ${options[`surface${i}`]}; - stroke: ${options[`surfacePeer${i}`]}; - } - `; - } - return sections; -}; - -const getStyles = (options: FlowChartStyleOptions) => - `.label { - font-family: ${options.fontFamily}; - color: ${options.nodeTextColor || options.textColor}; - } - .cluster-label text { - fill: ${options.titleColor}; - } - .cluster-label span { - color: ${options.titleColor}; - } - - .label text,span { - fill: ${options.nodeTextColor || options.textColor}; - color: ${options.nodeTextColor || options.textColor}; - } - - .node rect, - .node circle, - .node ellipse, - .node polygon, - .node path { - fill: ${options.mainBkg}; - stroke: ${options.nodeBorder}; - stroke-width: 1px; - } - - .node .label { - text-align: center; - } - .node.clickable { - cursor: pointer; - } - - .arrowheadPath { - fill: ${options.arrowheadColor}; - } - - .edgePath .path { - stroke: ${options.lineColor}; - stroke-width: 2.0px; - } - - .flowchart-link { - stroke: ${options.lineColor}; - fill: none; - } - - .edgeLabel { - background-color: ${options.edgeLabelBackground}; - rect { - opacity: 0.85; - background-color: ${options.edgeLabelBackground}; - fill: ${options.edgeLabelBackground}; - } - text-align: center; - } - - .cluster rect { - fill: ${options.clusterBkg}; - stroke: ${options.clusterBorder}; - stroke-width: 1px; - } - - .cluster text { - fill: ${options.titleColor}; - } - - .cluster span { - color: ${options.titleColor}; - } - /* .cluster div { - color: ${options.titleColor}; - } */ - - div.mermaidTooltip { - position: absolute; - text-align: center; - max-width: 200px; - padding: 2px; - font-family: ${options.fontFamily}; - font-size: 12px; - background: ${options.tertiaryColor}; - border: 1px solid ${options.border2}; - border-radius: 2px; - pointer-events: none; - z-index: 100; - } - - .flowchartTitleText { - text-anchor: middle; - font-size: 18px; - fill: ${options.textColor}; - } - .subgraph { - stroke-width:2; - rx:3; - } - // .subgraph-lvl-1 { - // fill:#ccc; - // // stroke:black; - // } - - .flowchart-label text { - text-anchor: middle; - } - - ${genSections(options)} -`; - -export default getStyles; diff --git a/packages/mermaid-flowchart-elk/tsconfig.json b/packages/mermaid-flowchart-elk/tsconfig.json deleted file mode 100644 index 5a41d0603..000000000 --- a/packages/mermaid-flowchart-elk/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "../..", - "outDir": "./dist", - "types": ["vitest/importMeta", "vitest/globals"] - }, - "include": ["./src/**/*.ts"], - "typeRoots": ["./src/types"] -} diff --git a/packages/mermaid-layout-elk/README.md b/packages/mermaid-layout-elk/README.md new file mode 100644 index 000000000..c32803292 --- /dev/null +++ b/packages/mermaid-layout-elk/README.md @@ -0,0 +1,72 @@ +# @mermaid-js/layout-elk + +This package provides a layout engine for Mermaid based on the [ELK](https://www.eclipse.org/elk/) layout engine. + +> [!NOTE] +> The ELK Layout engine will not be available in all providers that support mermaid by default. +> The websites will have to install the `@mermaid-js/layout-elk` package to use the ELK layout engine. + +## Usage + +``` +flowchart-elk TD + A --> B + A --> C +``` + +``` +--- +config: + layout: elk +--- + +flowchart TD + A --> B + A --> C +``` + +``` +--- +config: + layout: elk.stress +--- + +flowchart TD + A --> B + A --> C +``` + +### With bundlers + +```sh +npm install @mermaid-js/layout-elk +``` + +```ts +import mermaid from 'mermaid'; +import elkLayouts from '@mermaid-js/layout-elk'; + +mermaid.registerLayoutLoaders(elkLayouts); +``` + +### With CDN + +```html + +``` + +## Supported layouts + +- `elk`: The default layout, which is `elk.layered`. +- `elk.layered`: Layered layout +- `elk.stress`: Stress layout +- `elk.force`: Force layout +- `elk.mrtree`: Multi-root tree layout +- `elk.sporeOverlap`: Spore overlap layout + + diff --git a/packages/mermaid-layout-elk/package.json b/packages/mermaid-layout-elk/package.json index 4f8054682..f833dba52 100644 --- a/packages/mermaid-layout-elk/package.json +++ b/packages/mermaid-layout-elk/package.json @@ -31,13 +31,16 @@ ], "license": "MIT", "dependencies": { - "elkjs": "^0.9.3", - "d3": "^7.9.0" + "d3": "^7.9.0", + "elkjs": "^0.9.3" }, "peerDependencies": { "mermaid": "workspace:^" }, "files": [ "dist" - ] + ], + "devDependencies": { + "@types/d3": "^7.4.3" + } } diff --git a/packages/mermaid-layout-elk/src/layouts.ts b/packages/mermaid-layout-elk/src/layouts.ts index a6075386b..dc48ccbfa 100644 --- a/packages/mermaid-layout-elk/src/layouts.ts +++ b/packages/mermaid-layout-elk/src/layouts.ts @@ -3,7 +3,7 @@ import type { LayoutLoaderDefinition } from 'mermaid'; const loader = async () => await import(`./render.js`); const algos = ['elk.stress', 'elk.force', 'elk.mrtree', 'elk.sporeOverlap']; -export const layouts: LayoutLoaderDefinition[] = [ +const layouts: LayoutLoaderDefinition[] = [ { name: 'elk', loader, @@ -15,3 +15,5 @@ export const layouts: LayoutLoaderDefinition[] = [ algorithm: algo, })), ]; + +export default layouts; diff --git a/packages/mermaid-layout-elk/src/render.ts b/packages/mermaid-layout-elk/src/render.ts index 81453f47f..0fac3d867 100644 --- a/packages/mermaid-layout-elk/src/render.ts +++ b/packages/mermaid-layout-elk/src/render.ts @@ -1,466 +1,750 @@ -// @ts-nocheck File not ready to check types import { curveLinear } from 'd3'; import ELK from 'elkjs/lib/elk.bundled.js'; -import mermaid, { type LayoutData } from 'mermaid'; +import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid'; import { type TreeData, findCommonAncestor } from './find-common-ancestor.js'; -const { - common, - getConfig, - insertCluster, - insertEdge, - insertEdgeLabel, - insertMarkers, - insertNode, - interpolateToCurve, - labelHelper, - log, - positionEdgeLabel, -} = mermaid.internalHelpers; -// import { insertEdge } from '../../mermaid/src/rendering-util/rendering-elements/edges.js'; -const nodeDb = {}; -const portPos = {}; -const clusterDb = {}; +export const render = async ( + data4Layout: LayoutData, + svg: SVG, + element: any, + { + common, + getConfig, + insertCluster, + insertEdge, + insertEdgeLabel, + insertMarkers, + insertNode, + interpolateToCurve, + labelHelper, + log, + positionEdgeLabel, + }: InternalHelpers, + { algorithm }: RenderOptions +) => { + const nodeDb: Record = {}; + const clusterDb: Record = {}; -export const addVertex = async (nodeEl, graph, nodeArr, node) => { - const labelData = { width: 0, height: 0 }; - // const ports = [ - // { - // id: node.id + '-west', - // layoutOptions: { - // 'port.side': 'WEST', - // }, - // }, - // { - // id: node.id + '-east', - // layoutOptions: { - // 'port.side': 'EAST', - // }, - // }, - // { - // id: node.id + '-south', - // layoutOptions: { - // 'port.side': 'SOUTH', - // }, - // }, - // { - // id: node.id + '-north', - // layoutOptions: { - // 'port.side': 'NORTH', - // }, - // }, - // ]; + const addVertex = async (nodeEl: any, graph: { children: any[] }, nodeArr: any, node: any) => { + const labelData: any = { width: 0, height: 0 }; - let boundingBox; - const child = { - ...node, - // ports: node.shape === 'diamond' ? ports : [], - }; - graph.children.push(child); - nodeDb[node.id] = child; + let boundingBox; + const child = { + ...node, + }; + graph.children.push(child); + nodeDb[node.id] = child; - // Add the element to the DOM - if (!node.isGroup) { - const childNodeEl = await insertNode(nodeEl, node, node.dir); - boundingBox = childNodeEl.node().getBBox(); - child.domId = childNodeEl; - child.width = boundingBox.width; - child.height = boundingBox.height; - } else { - // A subgraph - child.children = []; - await addVertices(nodeEl, nodeArr, child, node.id); - - if (node.label) { - const { shapeSvg, bbox } = await labelHelper(nodeEl, node, undefined, true); - labelData.width = bbox.width; - labelData.wrappingWidth = getConfig().flowchart.wrappingWidth; - // Give some padding for elk - labelData.height = bbox.height - 2; - labelData.labelNode = shapeSvg.node(); - // We need the label hight to be able to size the subgraph; - shapeSvg.remove(); + // Add the element to the DOM + if (!node.isGroup) { + const childNodeEl = await insertNode(nodeEl, node, node.dir); + boundingBox = childNodeEl.node().getBBox(); + child.domId = childNodeEl; + child.width = boundingBox.width; + child.height = boundingBox.height; } else { - // Subgraph without label - labelData.width = 0; - labelData.height = 0; - } - child.labelData = labelData; - child.domId = nodeEl; - } -}; + // A subgraph + child.children = []; + await addVertices(nodeEl, nodeArr, child, node.id); -export const addVertices = async function (nodeEl, nodeArr, graph, parentId) { - const siblings = nodeArr.filter((node) => node.parentId === parentId); - log.info('addVertices APA12', siblings, parentId); - // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition - await Promise.all( - siblings.map(async (node) => { - await addVertex(nodeEl, graph, nodeArr, node); - }) - ); - return graph; -}; - -const drawNodes = async (relX, relY, nodeArray, svg, subgraphsEl, depth) => { - await Promise.all( - nodeArray.map(async function (node) { - if (node) { - nodeDb[node.id] = node; - nodeDb[node.id].offset = { - posX: node.x + relX, - posY: node.y + relY, - x: relX, - y: relY, - depth, - width: Math.max(node.width, node.labels ? node.labels[0]?.width || 0 : 0), - height: node.height, - }; - if (node.isGroup) { - log.debug('Id abc88 subgraph = ', node.id, node.x, node.y, node.labelData); - const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph'); - // TODO use faster way of cloning - const clusterNode = JSON.parse(JSON.stringify(node)); - clusterNode.x = node.offset.posX + node.width / 2; - clusterNode.y = node.offset.posY + node.height / 2; - await insertCluster(subgraphEl, clusterNode); - - log.debug('Id (UIO)= ', node.id, node.width, node.shape, node.labels); - } else { - log.info( - 'Id NODE = ', - node.id, - node.x, - node.y, - relX, - relY, - node.domId.node(), - `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})` - ); - node.domId.attr( - 'transform', - `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})` - ); - } + if (node.label) { + // @ts-ignore TODO: fix this + const { shapeSvg, bbox } = await labelHelper(nodeEl, node, undefined, true); + labelData.width = bbox.width; + labelData.wrappingWidth = getConfig().flowchart!.wrappingWidth; + // Give some padding for elk + labelData.height = bbox.height - 2; + labelData.labelNode = shapeSvg.node(); + // We need the label hight to be able to size the subgraph; + shapeSvg.remove(); + } else { + // Subgraph without label + labelData.width = 0; + labelData.height = 0; } - }) - ); - - await Promise.all( - nodeArray.map(async function (node) { - if (node?.isGroup) { - await drawNodes(relX + node.x, relY + node.y, node.children, svg, subgraphsEl, depth + 1); - } - }) - ); -}; - -const getNextPort = (node, edgeDirection, graphDirection) => { - log.info('getNextPort abc88', { node, edgeDirection, graphDirection }); - if (!portPos[node]) { - switch (graphDirection) { - case 'TB': - case 'TD': - portPos[node] = { - inPosition: 'north', - outPosition: 'south', - }; - break; - case 'BT': - portPos[node] = { - inPosition: 'south', - outPosition: 'north', - }; - break; - case 'RL': - portPos[node] = { - inPosition: 'east', - outPosition: 'west', - }; - break; - case 'LR': - portPos[node] = { - inPosition: 'west', - outPosition: 'east', - }; - break; + child.labelData = labelData; + child.domId = nodeEl; } - } - const result = edgeDirection === 'in' ? portPos[node].inPosition : portPos[node].outPosition; + }; - if (edgeDirection === 'in') { - portPos[node].inPosition = getNextPosition( - portPos[node].inPosition, - edgeDirection, - graphDirection + const addVertices = async function ( + nodeEl: any, + nodeArr: any[], + graph: { + id: string; + layoutOptions: { + 'elk.hierarchyHandling': string; + 'elk.algorithm': any; + 'nodePlacement.strategy': any; + 'elk.layered.mergeEdges': any; + 'elk.direction': string; + 'spacing.baseValue': number; + }; + children: never[]; + edges: never[]; + }, + parentId?: undefined + ) { + const siblings = nodeArr.filter((node: { parentId: any }) => node.parentId === parentId); + log.info('addVertices APA12', siblings, parentId); + // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition + await Promise.all( + siblings.map(async (node: any) => { + await addVertex(nodeEl, graph, nodeArr, node); + }) ); - } else { - portPos[node].outPosition = getNextPosition( - portPos[node].outPosition, - edgeDirection, - graphDirection - ); - } - return result; -}; + return graph; + }; -const addSubGraphs = (nodeArr): TreeData => { - const parentLookupDb: TreeData = { parentById: {}, childrenById: {} }; - const subgraphs = nodeArr.filter((node) => node.isGroup); - log.info('Subgraphs - ', subgraphs); - subgraphs.forEach((subgraph) => { - const children = nodeArr.filter((node) => node.parentId === subgraph.id); - children.forEach((node) => { - parentLookupDb.parentById[node.id] = subgraph.id; - if (parentLookupDb.childrenById[subgraph.id] === undefined) { - parentLookupDb.childrenById[subgraph.id] = []; - } - parentLookupDb.childrenById[subgraph.id].push(node); - }); - }); + const drawNodes = async ( + relX: number, + relY: number, + nodeArray: any[], + svg: any, + subgraphsEl: SVGGroup, + depth: number + ) => { + await Promise.all( + nodeArray.map(async function (node: { + id: string | number; + x: any; + y: any; + width: number; + labels: { width: any }[]; + height: number; + isGroup: any; + labelData: any; + offset: { posX: number; posY: number }; + shape: any; + domId: { node: () => any; attr: (arg0: string, arg1: string) => void }; + }) { + if (node) { + nodeDb[node.id] = node; + nodeDb[node.id].offset = { + posX: node.x + relX, + posY: node.y + relY, + x: relX, + y: relY, + depth, + width: Math.max(node.width, node.labels ? node.labels[0]?.width || 0 : 0), + height: node.height, + }; + if (node.isGroup) { + log.debug('Id abc88 subgraph = ', node.id, node.x, node.y, node.labelData); + const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph'); + // TODO use faster way of cloning + const clusterNode = JSON.parse(JSON.stringify(node)); + clusterNode.x = node.offset.posX + node.width / 2; + clusterNode.y = node.offset.posY + node.height / 2; + await insertCluster(subgraphEl, clusterNode); - subgraphs.forEach(function (subgraph) { - const data = { id: subgraph.id }; - if (parentLookupDb.parentById[subgraph.id] !== undefined) { - data.parent = parentLookupDb.parentById[subgraph.id]; - } - }); - return parentLookupDb; -}; - -const getEdgeStartEndPoint = (edge, dir) => { - let source = edge.start; - let target = edge.end; - - // Save the original source and target - const sourceId = source; - const targetId = target; - - const startNode = nodeDb[edge.start.id]; - const endNode = nodeDb[edge.end.id]; - - if (!startNode || !endNode) { - return { source, target }; - } - - if (startNode.shape === 'diamond') { - source = `${source}-${getNextPort(source, 'out', dir)}`; - } - - if (endNode.shape === 'diamond') { - target = `${target}-${getNextPort(target, 'in', dir)}`; - } - - // Add the edge to the graph - return { source, target, sourceId, targetId }; -}; - -const calcOffset = function (src: string, dest: string, parentLookupDb: TreeData) { - const ancestor = findCommonAncestor(src, dest, parentLookupDb); - if (ancestor === undefined || ancestor === 'root') { - return { x: 0, y: 0 }; - } - - const ancestorOffset = nodeDb[ancestor].offset; - return { x: ancestorOffset.posX, y: ancestorOffset.posY }; -}; - -/** - * Add edges to graph based on parsed graph definition - */ -export const addEdges = async function (dataForLayout, graph, svg) { - log.info('abc78 DAGA edges = ', dataForLayout); - const edges = dataForLayout.edges; - const labelsEl = svg.insert('g').attr('class', 'edgeLabels'); - const linkIdCnt = {}; - const dir = dataForLayout.direction || 'DOWN'; - let defaultStyle; - let defaultLabelStyle; - - await Promise.all( - edges.map(async function (edge) { - // Identify Link - const linkIdBase = edge.id; // 'L-' + edge.start + '-' + edge.end; - // count the links from+to the same node to give unique id - if (linkIdCnt[linkIdBase] === undefined) { - linkIdCnt[linkIdBase] = 0; - log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); - } else { - linkIdCnt[linkIdBase]++; - log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); - } - const linkId = linkIdBase + '_' + linkIdCnt[linkIdBase]; - edge.id = linkId; - log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]); - const linkNameStart = 'LS_' + edge.start; - const linkNameEnd = 'LE_' + edge.end; - - const edgeData = { style: '', labelStyle: '' }; - edgeData.minlen = edge.length || 1; - edge.text = edge.label; - // Set link type for rendering - if (edge.type === 'arrow_open') { - edgeData.arrowhead = 'none'; - } else { - edgeData.arrowhead = 'normal'; - } - - // Check of arrow types, placed here in order not to break old rendering - edgeData.arrowTypeStart = 'arrow_open'; - edgeData.arrowTypeEnd = 'arrow_open'; - - /* eslint-disable no-fallthrough */ - switch (edge.type) { - case 'double_arrow_cross': - edgeData.arrowTypeStart = 'arrow_cross'; - case 'arrow_cross': - edgeData.arrowTypeEnd = 'arrow_cross'; - break; - case 'double_arrow_point': - edgeData.arrowTypeStart = 'arrow_point'; - case 'arrow_point': - edgeData.arrowTypeEnd = 'arrow_point'; - break; - case 'double_arrow_circle': - edgeData.arrowTypeStart = 'arrow_circle'; - case 'arrow_circle': - edgeData.arrowTypeEnd = 'arrow_circle'; - break; - } - - let style = ''; - let labelStyle = ''; - - switch (edge.stroke) { - case 'normal': - style = 'fill:none;'; - if (defaultStyle !== undefined) { - style = defaultStyle; + log.debug('Id (UIO)= ', node.id, node.width, node.shape, node.labels); + } else { + log.info( + 'Id NODE = ', + node.id, + node.x, + node.y, + relX, + relY, + node.domId.node(), + `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})` + ); + node.domId.attr( + 'transform', + `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})` + ); } - if (defaultLabelStyle !== undefined) { - labelStyle = defaultLabelStyle; - } - edgeData.thickness = 'normal'; - edgeData.pattern = 'solid'; - break; - case 'dotted': - edgeData.thickness = 'normal'; - edgeData.pattern = 'dotted'; - edgeData.style = 'fill:none;stroke-width:2px;stroke-dasharray:3;'; - break; - case 'thick': - edgeData.thickness = 'thick'; - edgeData.pattern = 'solid'; - edgeData.style = 'stroke-width: 3.5px;fill:none;'; - break; - } - - edgeData.style = edgeData.style += style; - edgeData.labelStyle = edgeData.labelStyle += labelStyle; - - const conf = getConfig(); - if (edge.interpolate !== undefined) { - edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear); - } else if (edges.defaultInterpolate !== undefined) { - edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear); - } else { - edgeData.curve = interpolateToCurve(conf.curve, curveLinear); - } - - if (edge.text === undefined) { - if (edge.style !== undefined) { - edgeData.arrowheadStyle = 'fill: #333'; } - } else { - edgeData.arrowheadStyle = 'fill: #333'; - edgeData.labelpos = 'c'; - } + }) + ); - edgeData.labelType = edge.labelType; - edgeData.label = (edge?.text || '').replace(common.lineBreakRegex, '\n'); + await Promise.all( + nodeArray.map(async function (node: { isGroup: any; x: any; y: any; children: any }) { + if (node?.isGroup) { + await drawNodes(relX + node.x, relY + node.y, node.children, svg, subgraphsEl, depth + 1); + } + }) + ); + }; - if (edge.style === undefined) { - edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;'; - } - - edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:'); - - edgeData.id = linkId; - edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd; - - const labelEl = await insertEdgeLabel(labelsEl, edgeData); - - // calculate start and end points of the edge, note that the source and target - // can be modified for shapes that have ports - const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge, dir); - log.debug('abc78 source and target', source, target); - // Add the edge to the graph - graph.edges.push({ - id: 'e' + edge.start + edge.end, - ...edge, - sources: [source], - targets: [target], - sourceId, - targetId, - labelEl: labelEl, - labels: [ - { - width: edgeData.width, - height: edgeData.height, - orgWidth: edgeData.width, - orgHeight: edgeData.height, - text: edgeData.label, - layoutOptions: { - 'edgeLabels.inline': 'true', - 'edgeLabels.placement': 'CENTER', - }, - }, - ], - edgeData, + const addSubGraphs = (nodeArr: any[]): TreeData => { + const parentLookupDb: TreeData = { parentById: {}, childrenById: {} }; + const subgraphs = nodeArr.filter((node: { isGroup: any }) => node.isGroup); + log.info('Subgraphs - ', subgraphs); + subgraphs.forEach((subgraph: { id: string }) => { + const children = nodeArr.filter((node: { parentId: any }) => node.parentId === subgraph.id); + children.forEach((node: any) => { + parentLookupDb.parentById[node.id] = subgraph.id; + if (parentLookupDb.childrenById[subgraph.id] === undefined) { + parentLookupDb.childrenById[subgraph.id] = []; + } + parentLookupDb.childrenById[subgraph.id].push(node); }); - }) - ); - return graph; -}; + }); -function dir2ElkDirection(dir) { - switch (dir) { - case 'LR': - return 'RIGHT'; - case 'RL': - return 'LEFT'; - case 'TB': - return 'DOWN'; - case 'BT': - return 'UP'; - default: - return 'DOWN'; - } -} + subgraphs.forEach(function (subgraph: { id: string | number }) { + const data: any = { id: subgraph.id }; + if (parentLookupDb.parentById[subgraph.id] !== undefined) { + data.parent = parentLookupDb.parentById[subgraph.id]; + } + }); + return parentLookupDb; + }; -function setIncludeChildrenPolicy(nodeId: string, ancestorId: string) { - const node = nodeDb[nodeId]; + const getEdgeStartEndPoint = (edge: any) => { + const source: any = edge.start; + const target: any = edge.end; - if (!node) { - return; - } - if (node?.layoutOptions === undefined) { - node.layoutOptions = {}; - } - node.layoutOptions['elk.hierarchyHandling'] = 'INCLUDE_CHILDREN'; - if (node.id !== ancestorId) { - setIncludeChildrenPolicy(node.parentId, ancestorId); - } -} + // Save the original source and target + const sourceId = source; + const targetId = target; -export const render = async (data4Layout: LayoutData, svg, element, algorithm) => { + const startNode = nodeDb[edge.start.id]; + const endNode = nodeDb[edge.end.id]; + + if (!startNode || !endNode) { + return { source, target }; + } + + // Add the edge to the graph + return { source, target, sourceId, targetId }; + }; + + const calcOffset = function (src: string, dest: string, parentLookupDb: TreeData) { + const ancestor = findCommonAncestor(src, dest, parentLookupDb); + if (ancestor === undefined || ancestor === 'root') { + return { x: 0, y: 0 }; + } + + const ancestorOffset = nodeDb[ancestor].offset; + return { x: ancestorOffset.posX, y: ancestorOffset.posY }; + }; + + /** + * Add edges to graph based on parsed graph definition + */ + const addEdges = async function ( + dataForLayout: { edges: any; direction: string }, + graph: { + id?: string; + layoutOptions?: { + 'elk.hierarchyHandling': string; + 'elk.algorithm': any; + 'nodePlacement.strategy': any; + 'elk.layered.mergeEdges': any; + 'elk.direction': string; + 'spacing.baseValue': number; + }; + children?: never[]; + edges: any; + }, + svg: SVG + ) { + log.info('abc78 DAGA edges = ', dataForLayout); + const edges = dataForLayout.edges; + const labelsEl = svg.insert('g').attr('class', 'edgeLabels'); + const linkIdCnt: any = {}; + const dir = dataForLayout.direction || 'DOWN'; + let defaultStyle: string | undefined; + let defaultLabelStyle: string | undefined; + + await Promise.all( + edges.map(async function (edge: { + id: string; + start: string; + end: string; + length: number; + text: undefined; + label: any; + type: string; + stroke: any; + interpolate: undefined; + style: undefined; + labelType: any; + }) { + // Identify Link + const linkIdBase = edge.id; // 'L-' + edge.start + '-' + edge.end; + // count the links from+to the same node to give unique id + if (linkIdCnt[linkIdBase] === undefined) { + linkIdCnt[linkIdBase] = 0; + log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); + } else { + linkIdCnt[linkIdBase]++; + log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); + } + const linkId = linkIdBase + '_' + linkIdCnt[linkIdBase]; + edge.id = linkId; + log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]); + const linkNameStart = 'LS_' + edge.start; + const linkNameEnd = 'LE_' + edge.end; + + const edgeData: any = { style: '', labelStyle: '' }; + edgeData.minlen = edge.length || 1; + edge.text = edge.label; + // Set link type for rendering + if (edge.type === 'arrow_open') { + edgeData.arrowhead = 'none'; + } else { + edgeData.arrowhead = 'normal'; + } + + // Check of arrow types, placed here in order not to break old rendering + edgeData.arrowTypeStart = 'arrow_open'; + edgeData.arrowTypeEnd = 'arrow_open'; + + /* eslint-disable no-fallthrough */ + switch (edge.type) { + case 'double_arrow_cross': + edgeData.arrowTypeStart = 'arrow_cross'; + case 'arrow_cross': + edgeData.arrowTypeEnd = 'arrow_cross'; + break; + case 'double_arrow_point': + edgeData.arrowTypeStart = 'arrow_point'; + case 'arrow_point': + edgeData.arrowTypeEnd = 'arrow_point'; + break; + case 'double_arrow_circle': + edgeData.arrowTypeStart = 'arrow_circle'; + case 'arrow_circle': + edgeData.arrowTypeEnd = 'arrow_circle'; + break; + } + + let style = ''; + let labelStyle = ''; + + switch (edge.stroke) { + case 'normal': + style = 'fill:none;'; + if (defaultStyle !== undefined) { + style = defaultStyle; + } + if (defaultLabelStyle !== undefined) { + labelStyle = defaultLabelStyle; + } + edgeData.thickness = 'normal'; + edgeData.pattern = 'solid'; + break; + case 'dotted': + edgeData.thickness = 'normal'; + edgeData.pattern = 'dotted'; + edgeData.style = 'fill:none;stroke-width:2px;stroke-dasharray:3;'; + break; + case 'thick': + edgeData.thickness = 'thick'; + edgeData.pattern = 'solid'; + edgeData.style = 'stroke-width: 3.5px;fill:none;'; + break; + } + + edgeData.style = edgeData.style += style; + edgeData.labelStyle = edgeData.labelStyle += labelStyle; + + const conf = getConfig(); + if (edge.interpolate !== undefined) { + edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear); + } else if (edges.defaultInterpolate !== undefined) { + edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear); + } else { + // @ts-ignore TODO: fix this + edgeData.curve = interpolateToCurve(conf.curve, curveLinear); + } + + if (edge.text === undefined) { + if (edge.style !== undefined) { + edgeData.arrowheadStyle = 'fill: #333'; + } + } else { + edgeData.arrowheadStyle = 'fill: #333'; + edgeData.labelpos = 'c'; + } + + edgeData.labelType = edge.labelType; + edgeData.label = (edge?.text || '').replace(common.lineBreakRegex, '\n'); + + if (edge.style === undefined) { + edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;'; + } + + edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:'); + + edgeData.id = linkId; + edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd; + + const labelEl = await insertEdgeLabel(labelsEl, edgeData); + + // calculate start and end points of the edge, note that the source and target + // can be modified for shapes that have ports + const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge, dir); + log.debug('abc78 source and target', source, target); + // Add the edge to the graph + graph.edges.push({ + // @ts-ignore TODO: fix this + id: 'e' + edge.start + edge.end, + ...edge, + sources: [source], + targets: [target], + sourceId, + targetId, + labelEl: labelEl, + labels: [ + { + width: edgeData.width, + height: edgeData.height, + orgWidth: edgeData.width, + orgHeight: edgeData.height, + text: edgeData.label, + layoutOptions: { + 'edgeLabels.inline': 'true', + 'edgeLabels.placement': 'CENTER', + }, + }, + ], + edgeData, + }); + }) + ); + return graph; + }; + + function dir2ElkDirection(dir: any) { + switch (dir) { + case 'LR': + return 'RIGHT'; + case 'RL': + return 'LEFT'; + case 'TB': + return 'DOWN'; + case 'BT': + return 'UP'; + default: + return 'DOWN'; + } + } + + function setIncludeChildrenPolicy(nodeId: string, ancestorId: string) { + const node = nodeDb[nodeId]; + + if (!node) { + return; + } + if (node?.layoutOptions === undefined) { + node.layoutOptions = {}; + } + node.layoutOptions['elk.hierarchyHandling'] = 'INCLUDE_CHILDREN'; + if (node.id !== ancestorId) { + setIncludeChildrenPolicy(node.parentId, ancestorId); + } + } + + function intersectLine( + p1: { y: number; x: number }, + p2: { y: number; x: number }, + q1: { x: any; y: any }, + q2: { x: any; y: any } + ) { + log.debug('UIO intersectLine', p1, p2, q1, q2); + // Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994, + // p7 and p473. + + // let a1, a2, b1, b2, c1, c2; + // let r1, r2, r3, r4; + // let denom, offset, num; + // let x, y; + + // Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x + + // b1 y + c1 = 0. + const a1 = p2.y - p1.y; + const b1 = p1.x - p2.x; + const c1 = p2.x * p1.y - p1.x * p2.y; + + // Compute r3 and r4. + const r3 = a1 * q1.x + b1 * q1.y + c1; + const r4 = a1 * q2.x + b1 * q2.y + c1; + + // Check signs of r3 and r4. If both point 3 and point 4 lie on + // same side of line 1, the line segments do not intersect. + if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) { + return /*DON'T_INTERSECT*/; + } + + // Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0 + const a2 = q2.y - q1.y; + const b2 = q1.x - q2.x; + const c2 = q2.x * q1.y - q1.x * q2.y; + + // Compute r1 and r2 + const r1 = a2 * p1.x + b2 * p1.y + c2; + const r2 = a2 * p2.x + b2 * p2.y + c2; + + // Check signs of r1 and r2. If both point 1 and point 2 lie + // on same side of second line segment, the line segments do + // not intersect. + if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) { + return /*DON'T_INTERSECT*/; + } + + // Line segments intersect: compute intersection point. + const denom = a1 * b2 - a2 * b1; + if (denom === 0) { + return /*COLLINEAR*/; + } + + const offset = Math.abs(denom / 2); + + // The denom/2 is to get rounding instead of truncating. It + // is added or subtracted to the numerator, depending upon the + // sign of the numerator. + let num = b1 * c2 - b2 * c1; + const x = num < 0 ? (num - offset) / denom : (num + offset) / denom; + + num = a2 * c1 - a1 * c2; + const y = num < 0 ? (num - offset) / denom : (num + offset) / denom; + + return { x: x, y: y }; + } + + function sameSign(r1: number, r2: number) { + return r1 * r2 > 0; + } + const diamondIntersection = ( + bounds: { x: any; y: any; width: any; height: any }, + outsidePoint: { x: number; y: number }, + insidePoint: any + ) => { + const x1 = bounds.x; + const y1 = bounds.y; + + const w = bounds.width; //+ bounds.padding; + const h = bounds.height; // + bounds.padding; + + const polyPoints = [ + { x: x1, y: y1 - h / 2 }, + { x: x1 + w / 2, y: y1 }, + { x: x1, y: y1 + h / 2 }, + { x: x1 - w / 2, y: y1 }, + ]; + log.debug( + `UIO diamondIntersection calc abc89: + outsidePoint: ${JSON.stringify(outsidePoint)} + insidePoint : ${JSON.stringify(insidePoint)} + node : x:${bounds.x} y:${bounds.y} w:${bounds.width} h:${bounds.height}`, + polyPoints + ); + + const intersections = []; + + let minX = Number.POSITIVE_INFINITY; + let minY = Number.POSITIVE_INFINITY; + + polyPoints.forEach(function (entry) { + minX = Math.min(minX, entry.x); + minY = Math.min(minY, entry.y); + }); + + // const left = x1 - w / 2; + // const top = y1 + h / 2; + + for (let i = 0; i < polyPoints.length; i++) { + const p1 = polyPoints[i]; + const p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0]; + const intersect = intersectLine( + bounds, + outsidePoint, + { x: p1.x, y: p1.y }, + { x: p2.x, y: p2.y } + ); + + if (intersect) { + intersections.push(intersect); + } + } + + if (!intersections.length) { + return bounds; + } + + log.debug('UIO intersections', intersections); + + if (intersections.length > 1) { + // More intersections, find the one nearest to edge end point + intersections.sort(function (p, q) { + const pdx = p.x - outsidePoint.x; + const pdy = p.y - outsidePoint.y; + const distp = Math.sqrt(pdx * pdx + pdy * pdy); + + const qdx = q.x - outsidePoint.x; + const qdy = q.y - outsidePoint.y; + const distq = Math.sqrt(qdx * qdx + qdy * qdy); + + return distp < distq ? -1 : distp === distq ? 0 : 1; + }); + } + + return intersections[0]; + }; + + const intersection = ( + node: { x: any; y: any; width: number; height: number }, + outsidePoint: { x: number; y: number }, + insidePoint: { x: number; y: number } + ) => { + log.debug(`intersection calc abc89: + outsidePoint: ${JSON.stringify(outsidePoint)} + insidePoint : ${JSON.stringify(insidePoint)} + node : x:${node.x} y:${node.y} w:${node.width} h:${node.height}`); + const x = node.x; + const y = node.y; + + const dx = Math.abs(x - insidePoint.x); + // const dy = Math.abs(y - insidePoint.y); + const w = node.width / 2; + let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx; + const h = node.height / 2; + + const Q = Math.abs(outsidePoint.y - insidePoint.y); + const R = Math.abs(outsidePoint.x - insidePoint.x); + + if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) { + // Intersection is top or bottom of rect. + const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y; + r = (R * q) / Q; + const res = { + x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r, + y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q, + }; + + if (r === 0) { + res.x = outsidePoint.x; + res.y = outsidePoint.y; + } + if (R === 0) { + res.x = outsidePoint.x; + } + if (Q === 0) { + res.y = outsidePoint.y; + } + + log.debug(`abc89 topp/bott calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res); // cspell: disable-line + + return res; + } else { + // Intersection onn sides of rect + if (insidePoint.x < outsidePoint.x) { + r = outsidePoint.x - w - x; + } else { + // r = outsidePoint.x - w - x; + r = x - w - outsidePoint.x; + } + const q = (Q * r) / R; + // OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x + dx - w; + // OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r; + let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r; + // let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r; + let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q; + log.debug(`sides calc abc89, Q ${Q}, q ${q}, R ${R}, r ${r}`, { _x, _y }); + if (r === 0) { + _x = outsidePoint.x; + _y = outsidePoint.y; + } + if (R === 0) { + _x = outsidePoint.x; + } + if (Q === 0) { + _y = outsidePoint.y; + } + + return { x: _x, y: _y }; + } + }; + const outsideNode = ( + node: { x: any; y: any; width: number; height: number }, + point: { x: number; y: number } + ) => { + const x = node.x; + const y = node.y; + const dx = Math.abs(point.x - x); + const dy = Math.abs(point.y - y); + const w = node.width / 2; + const h = node.height / 2; + if (dx >= w || dy >= h) { + return true; + } + return false; + }; + /** + * This function will page a path and node where the last point(s) in the path is inside the node + * and return an update path ending by the border of the node. + */ + const cutPathAtIntersect = ( + _points: any[], + bounds: { x: any; y: any; width: any; height: any; padding: any }, + isDiamond: boolean + ) => { + log.debug('UIO cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond); + const points: any[] = []; + let lastPointOutside = _points[0]; + let isInside = false; + _points.forEach((point: any) => { + // const node = clusterDb[edge.toCluster].node; + log.debug(' checking point', point, bounds); + + // check if point is inside the boundary rect + if (!outsideNode(bounds, point) && !isInside) { + // First point inside the rect found + // Calc the intersection coord between the point anf the last point outside the rect + let inter; + + if (isDiamond) { + const inter2 = diamondIntersection(bounds, lastPointOutside, point); + const distance = Math.sqrt( + (lastPointOutside.x - inter2.x) ** 2 + (lastPointOutside.y - inter2.y) ** 2 + ); + if (distance > 1) { + inter = inter2; + } + } + if (!inter) { + inter = intersection(bounds, lastPointOutside, point); + } + + // Check case where the intersection is the same as the last point + let pointPresent = false; + points.forEach((p) => { + pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y); + }); + // if (!pointPresent) { + if (!points.some((e) => e.x === inter.x && e.y === inter.y)) { + points.push(inter); + } else { + log.debug('abc88 no intersect', inter, points); + } + // points.push(inter); + isInside = true; + } else { + // Outside + log.debug('abc88 outside', point, lastPointOutside, points); + lastPointOutside = point; + // points.push(point); + if (!isInside) { + points.push(point); + } + } + }); + log.debug('returning points', points); + return points; + }; + + // @ts-ignore - ELK is not typed const elk = new ELK(); // Add the arrowheads to the svg insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId); // Setup the graph with the layout options and the data for the layout - let elkGraph = { + let elkGraph: any = { id: 'root', layoutOptions: { 'elk.hierarchyHandling': 'INCLUDE_CHILDREN', @@ -489,7 +773,7 @@ export const render = async (data4Layout: LayoutData, svg, element, algorithm) = // Create the lookup db for the subgraphs and their children to used when creating // the tree structured graph - const parentLookupDb = addSubGraphs(data4Layout.nodes); + const parentLookupDb: any = addSubGraphs(data4Layout.nodes); // Add elements in the svg to be used to hold the subgraphs container // elements and the nodes @@ -510,7 +794,7 @@ export const render = async (data4Layout: LayoutData, svg, element, algorithm) = // Iterate through all nodes and add the top level nodes to the graph const nodes = data4Layout.nodes; - nodes.forEach((n) => { + nodes.forEach((n: { id: string | number }) => { const node = nodeDb[n.id]; // Subgraph @@ -544,7 +828,7 @@ export const render = async (data4Layout: LayoutData, svg, element, algorithm) = delete node.height; } }); - elkGraph.edges.forEach((edge) => { + elkGraph.edges.forEach((edge: any) => { const source = edge.sources[0]; const target = edge.targets[0]; @@ -560,438 +844,149 @@ export const render = async (data4Layout: LayoutData, svg, element, algorithm) = // debugger; await drawNodes(0, 0, g.children, svg, subGraphsEl, 0); - g.edges?.map((edge) => { - // (elem, edge, clusterDb, diagramType, graph, id) - const startNode = nodeDb[edge.sources[0]]; - const startCluster = parentLookupDb[edge.sources[0]]; - const endNode = nodeDb[edge.targets[0]]; - const sourceId = edge.start; - const targetId = edge.end; + g.edges?.map( + (edge: { + sources: (string | number)[]; + targets: (string | number)[]; + start: any; + end: any; + sections: { startPoint: any; endPoint: any; bendPoints: any }[]; + points: any[]; + x: any; + labels: { height: number; width: number; x: number; y: number }[]; + y: any; + }) => { + // (elem, edge, clusterDb, diagramType, graph, id) + const startNode = nodeDb[edge.sources[0]]; + const startCluster = parentLookupDb[edge.sources[0]]; + const endNode = nodeDb[edge.targets[0]]; + const sourceId = edge.start; + const targetId = edge.end; - const offset = calcOffset(sourceId, targetId, parentLookupDb); - log.debug( - 'offset', - offset, - sourceId, - ' ==> ', - targetId, - 'edge:', - edge, - 'cluster:', - startCluster, - startNode - ); - if (edge.sections) { - const src = edge.sections[0].startPoint; - const dest = edge.sections[0].endPoint; - const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : []; - - const segPoints = segments.map((segment) => { - return { x: segment.x + offset.x, y: segment.y + offset.y }; - }); - edge.points = [ - { x: src.x + offset.x, y: src.y + offset.y }, - ...segPoints, - { x: dest.x + offset.x, y: dest.y + offset.y }, - ]; - - let sw = startNode.width; - let ew = endNode.width; - if (startNode.isGroup) { - const bbox = startNode.domId.node().getBBox(); - // sw = Math.max(bbox.width, startNode.width, startNode.labels[0].width); - sw = Math.max(startNode.width, startNode.labels[0].width + startNode.padding); - // sw = startNode.width; - log.debug( - 'UIO width', - startNode.id, - startNode.with, - 'bbox.width=', - bbox.width, - 'lw=', - startNode.labels[0].width, - 'node:', - startNode.width, - 'SW = ', - sw - // 'HTML:', - // startNode.domId.node().innerHTML - ); - } - if (endNode.isGroup) { - const bbox = endNode.domId.node().getBBox(); - ew = Math.max(endNode.width, endNode.labels[0].width + endNode.padding); - - log.debug( - 'UIO width', - startNode.id, - startNode.with, - bbox.width, - 'EW = ', - ew, - 'HTML:', - startNode.innerHTML - ); - } - if (startNode.shape === 'diamond') { - edge.points.unshift({ - x: startNode.x + startNode.width / 2 + offset.x, - y: startNode.y + startNode.height / 2 + offset.y, - }); - } - if (endNode.shape === 'diamond') { - edge.points.push({ - x: endNode.x + endNode.width / 2 + offset.x, - y: endNode.y + endNode.height / 2 + offset.y, - }); - } - - edge.points = cutPathAtIntersect( - edge.points.reverse(), - { - x: startNode.x + startNode.width / 2 + offset.x, - y: startNode.y + startNode.height / 2 + offset.y, - width: sw, - height: startNode.height, - padding: startNode.padding, - }, - startNode.shape === 'diamond' - ).reverse(); - - edge.points = cutPathAtIntersect( - edge.points, - { - x: endNode.x + ew / 2 + endNode.offset.x, - y: endNode.y + endNode.height / 2 + endNode.offset.y, - width: ew, - height: endNode.height, - padding: endNode.padding, - }, - endNode.shape === 'diamond' - ); - - const paths = insertEdge( - edgesEl, + const offset = calcOffset(sourceId, targetId, parentLookupDb); + log.debug( + 'offset', + offset, + sourceId, + ' ==> ', + targetId, + 'edge:', edge, - clusterDb, - data4Layout.type, - startNode, - endNode, - data4Layout.diagramId + 'cluster:', + startCluster, + startNode ); - log.info('APA12 edge points after insert', JSON.stringify(edge.points)); + if (edge.sections) { + const src = edge.sections[0].startPoint; + const dest = edge.sections[0].endPoint; + const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : []; - edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2; - edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2; - positionEdgeLabel(edge, paths); - } - // const src = edge.sections[0].startPoint; - // const dest = edge.sections[0].endPoint; - // const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : []; + const segPoints = segments.map((segment: { x: any; y: any }) => { + return { x: segment.x + offset.x, y: segment.y + offset.y }; + }); + edge.points = [ + { x: src.x + offset.x, y: src.y + offset.y }, + ...segPoints, + { x: dest.x + offset.x, y: dest.y + offset.y }, + ]; - // const segPoints = segments.map((segment) => { - // return { x: segment.x + offset.x, y: segment.y + offset.y }; - // }); - // edge.points = [ - // { x: src.x + offset.x, y: src.y + offset.y }, - // ...segPoints, - // { x: dest.x + offset.x, y: dest.y + offset.y }, - // ]; - // const paths = insertEdge( - // edgesEl, - // edge, - // clusterDb, - // data4Layout.type, - // startNode, - // endNode, - // data4Layout.diagramId - // ); - // log.info('APA12 edge points after insert', JSON.stringify(edge.points)); - - // edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2; - // edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2; - // positionEdgeLabel(edge, paths); - }); -}; - -function intersectLine(p1, p2, q1, q2) { - log.debug('UIO intersectLine', p1, p2, q1, q2); - // Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994, - // p7 and p473. - - // let a1, a2, b1, b2, c1, c2; - // let r1, r2, r3, r4; - // let denom, offset, num; - // let x, y; - - // Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x + - // b1 y + c1 = 0. - const a1 = p2.y - p1.y; - const b1 = p1.x - p2.x; - const c1 = p2.x * p1.y - p1.x * p2.y; - - // Compute r3 and r4. - const r3 = a1 * q1.x + b1 * q1.y + c1; - const r4 = a1 * q2.x + b1 * q2.y + c1; - - // Check signs of r3 and r4. If both point 3 and point 4 lie on - // same side of line 1, the line segments do not intersect. - if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) { - return /*DON'T_INTERSECT*/; - } - - // Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0 - const a2 = q2.y - q1.y; - const b2 = q1.x - q2.x; - const c2 = q2.x * q1.y - q1.x * q2.y; - - // Compute r1 and r2 - const r1 = a2 * p1.x + b2 * p1.y + c2; - const r2 = a2 * p2.x + b2 * p2.y + c2; - - // Check signs of r1 and r2. If both point 1 and point 2 lie - // on same side of second line segment, the line segments do - // not intersect. - if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) { - return /*DON'T_INTERSECT*/; - } - - // Line segments intersect: compute intersection point. - const denom = a1 * b2 - a2 * b1; - if (denom === 0) { - return /*COLLINEAR*/; - } - - const offset = Math.abs(denom / 2); - - // The denom/2 is to get rounding instead of truncating. It - // is added or subtracted to the numerator, depending upon the - // sign of the numerator. - let num = b1 * c2 - b2 * c1; - const x = num < 0 ? (num - offset) / denom : (num + offset) / denom; - - num = a2 * c1 - a1 * c2; - const y = num < 0 ? (num - offset) / denom : (num + offset) / denom; - - return { x: x, y: y }; -} - -function sameSign(r1, r2) { - return r1 * r2 > 0; -} -const diamondIntersection = (bounds, outsidePoint, insidePoint) => { - const x1 = bounds.x; - const y1 = bounds.y; - - const w = bounds.width; //+ bounds.padding; - const h = bounds.height; // + bounds.padding; - - const polyPoints = [ - { x: x1, y: y1 - h / 2 }, - { x: x1 + w / 2, y: y1 }, - { x: x1, y: y1 + h / 2 }, - { x: x1 - w / 2, y: y1 }, - ]; - log.debug( - `UIO diamondIntersection calc abc89: - outsidePoint: ${JSON.stringify(outsidePoint)} - insidePoint : ${JSON.stringify(insidePoint)} - node : x:${bounds.x} y:${bounds.y} w:${bounds.width} h:${bounds.height}`, - polyPoints - ); - - const intersections = []; - - let minX = Number.POSITIVE_INFINITY; - let minY = Number.POSITIVE_INFINITY; - if (typeof polyPoints.forEach === 'function') { - polyPoints.forEach(function (entry) { - minX = Math.min(minX, entry.x); - minY = Math.min(minY, entry.y); - }); - } else { - minX = Math.min(minX, polyPoints.x); - minY = Math.min(minY, polyPoints.y); - } - - // const left = x1 - w / 2; - // const top = y1 + h / 2; - - for (let i = 0; i < polyPoints.length; i++) { - const p1 = polyPoints[i]; - const p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0]; - const intersect = intersectLine( - bounds, - outsidePoint, - { x: p1.x, y: p1.y }, - { x: p2.x, y: p2.y } - ); - - if (intersect) { - intersections.push(intersect); - } - } - - if (!intersections.length) { - return bounds; - } - - log.debug('UIO intersections', intersections); - - if (intersections.length > 1) { - // More intersections, find the one nearest to edge end point - intersections.sort(function (p, q) { - const pdx = p.x - outsidePoint.x; - const pdy = p.y - outsidePoint.y; - const distp = Math.sqrt(pdx * pdx + pdy * pdy); - - const qdx = q.x - outsidePoint.x; - const qdy = q.y - outsidePoint.y; - const distq = Math.sqrt(qdx * qdx + qdy * qdy); - - return distp < distq ? -1 : distp === distq ? 0 : 1; - }); - } - - return intersections[0]; -}; - -export const intersection = (node, outsidePoint, insidePoint) => { - log.debug(`intersection calc abc89: - outsidePoint: ${JSON.stringify(outsidePoint)} - insidePoint : ${JSON.stringify(insidePoint)} - node : x:${node.x} y:${node.y} w:${node.width} h:${node.height}`); - const x = node.x; - const y = node.y; - - const dx = Math.abs(x - insidePoint.x); - // const dy = Math.abs(y - insidePoint.y); - const w = node.width / 2; - let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx; - const h = node.height / 2; - - const Q = Math.abs(outsidePoint.y - insidePoint.y); - const R = Math.abs(outsidePoint.x - insidePoint.x); - - if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) { - // Intersection is top or bottom of rect. - const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y; - r = (R * q) / Q; - const res = { - x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r, - y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q, - }; - - if (r === 0) { - res.x = outsidePoint.x; - res.y = outsidePoint.y; - } - if (R === 0) { - res.x = outsidePoint.x; - } - if (Q === 0) { - res.y = outsidePoint.y; - } - - log.debug(`abc89 topp/bott calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res); // cspell: disable-line - - return res; - } else { - // Intersection onn sides of rect - if (insidePoint.x < outsidePoint.x) { - r = outsidePoint.x - w - x; - } else { - // r = outsidePoint.x - w - x; - r = x - w - outsidePoint.x; - } - const q = (Q * r) / R; - // OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x + dx - w; - // OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r; - let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r; - // let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r; - let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q; - log.debug(`sides calc abc89, Q ${Q}, q ${q}, R ${R}, r ${r}`, { _x, _y }); - if (r === 0) { - _x = outsidePoint.x; - _y = outsidePoint.y; - } - if (R === 0) { - _x = outsidePoint.x; - } - if (Q === 0) { - _y = outsidePoint.y; - } - - return { x: _x, y: _y }; - } -}; -const outsideNode = (node, point) => { - const x = node.x; - const y = node.y; - const dx = Math.abs(point.x - x); - const dy = Math.abs(point.y - y); - const w = node.width / 2; - const h = node.height / 2; - if (dx >= w || dy >= h) { - return true; - } - return false; -}; -/** - * This function will page a path and node where the last point(s) in the path is inside the node - * and return an update path ending by the border of the node. - */ -const cutPathAtIntersect = (_points, bounds, isDiamond: boolean) => { - log.debug('UIO cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond); - const points = []; - let lastPointOutside = _points[0]; - let isInside = false; - _points.forEach((point) => { - // const node = clusterDb[edge.toCluster].node; - log.debug(' checking point', point, bounds); - - // check if point is inside the boundary rect - if (!outsideNode(bounds, point) && !isInside) { - // First point inside the rect found - // Calc the intersection coord between the point anf the last point outside the rect - let inter; - - if (isDiamond) { - const inter2 = diamondIntersection(bounds, lastPointOutside, point); - const distance = Math.sqrt( - (lastPointOutside.x - inter2.x) ** 2 + (lastPointOutside.y - inter2.y) ** 2 - ); - if (distance > 1) { - inter = inter2; + let sw = startNode.width; + let ew = endNode.width; + if (startNode.isGroup) { + const bbox = startNode.domId.node().getBBox(); + // sw = Math.max(bbox.width, startNode.width, startNode.labels[0].width); + sw = Math.max(startNode.width, startNode.labels[0].width + startNode.padding); + // sw = startNode.width; + log.debug( + 'UIO width', + startNode.id, + startNode.with, + 'bbox.width=', + bbox.width, + 'lw=', + startNode.labels[0].width, + 'node:', + startNode.width, + 'SW = ', + sw + // 'HTML:', + // startNode.domId.node().innerHTML + ); } - } - if (!inter) { - inter = intersection(bounds, lastPointOutside, point); - } + if (endNode.isGroup) { + const bbox = endNode.domId.node().getBBox(); + ew = Math.max(endNode.width, endNode.labels[0].width + endNode.padding); - // Check case where the intersection is the same as the last point - let pointPresent = false; - points.forEach((p) => { - pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y); - }); - // if (!pointPresent) { - if (!points.some((e) => e.x === inter.x && e.y === inter.y)) { - points.push(inter); - } else { - log.debug('abc88 no intersect', inter, points); - } - // points.push(inter); - isInside = true; - } else { - // Outside - log.debug('abc88 outside', point, lastPointOutside, points); - lastPointOutside = point; - // points.push(point); - if (!isInside) { - points.push(point); + log.debug( + 'UIO width', + startNode.id, + startNode.with, + bbox.width, + 'EW = ', + ew, + 'HTML:', + startNode.innerHTML + ); + } + if (startNode.shape === 'diamond') { + edge.points.unshift({ + x: startNode.x + startNode.width / 2 + offset.x, + y: startNode.y + startNode.height / 2 + offset.y, + }); + } + if (endNode.shape === 'diamond') { + const x = endNode.x + endNode.width / 2 + offset.x; + // Add a point at the center of the diamond + if ( + Math.abs(edge.points[edge.points.length - 1].y - endNode.y - offset.y) > 0.001 || + Math.abs(edge.points[edge.points.length - 1].x - x) > 0.001 + ) { + edge.points.push({ + x: endNode.x + endNode.width / 2 + offset.x, + y: endNode.y + endNode.height / 2 + offset.y, + }); + } + } + + edge.points = cutPathAtIntersect( + edge.points.reverse(), + { + x: startNode.x + startNode.width / 2 + offset.x, + y: startNode.y + startNode.height / 2 + offset.y, + width: sw, + height: startNode.height, + padding: startNode.padding, + }, + startNode.shape === 'diamond' + ).reverse(); + + edge.points = cutPathAtIntersect( + edge.points, + { + x: endNode.x + ew / 2 + endNode.offset.x, + y: endNode.y + endNode.height / 2 + endNode.offset.y, + width: ew, + height: endNode.height, + padding: endNode.padding, + }, + endNode.shape === 'diamond' + ); + + const paths = insertEdge( + edgesEl, + edge, + clusterDb, + data4Layout.type, + startNode, + endNode, + data4Layout.diagramId + ); + log.info('APA12 edge points after insert', JSON.stringify(edge.points)); + + edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2; + edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2; + positionEdgeLabel(edge, paths); } } - }); - log.debug('returning points', points); - return points; + ); }; diff --git a/packages/mermaid/src/defaultConfig.ts b/packages/mermaid/src/defaultConfig.ts index 0fa897d11..97f3e0bb1 100644 --- a/packages/mermaid/src/defaultConfig.ts +++ b/packages/mermaid/src/defaultConfig.ts @@ -248,19 +248,6 @@ const config: RequiredDeep = { ...defaultConfigJson.requirement, useWidth: undefined, }, - gitGraph: { - ...defaultConfigJson.gitGraph, - // TODO: This is a temporary override for `gitGraph`, since every other - // diagram does have `useMaxWidth`, but instead sets it to `true`. - // Should we set this to `true` instead? - useMaxWidth: false, - }, - sankey: { - ...defaultConfigJson.sankey, - // this is false, unlike every other diagram (other than gitGraph) - // TODO: can we make this default to `true` instead? - useMaxWidth: false, - }, packet: { ...defaultConfigJson.packet, }, diff --git a/packages/mermaid/src/diagram-api/types.ts b/packages/mermaid/src/diagram-api/types.ts index 4556c1d6e..fdb175e52 100644 --- a/packages/mermaid/src/diagram-api/types.ts +++ b/packages/mermaid/src/diagram-api/types.ts @@ -129,6 +129,6 @@ export type HTML = d3.Selection; -export type Group = d3.Selection; +export type SVGGroup = d3.Selection; export type DiagramStylesProvider = (options?: any) => string; diff --git a/packages/mermaid/src/diagrams/common/svgDrawCommon.ts b/packages/mermaid/src/diagrams/common/svgDrawCommon.ts index 8d5ba3b7c..59c6d43cf 100644 --- a/packages/mermaid/src/diagrams/common/svgDrawCommon.ts +++ b/packages/mermaid/src/diagrams/common/svgDrawCommon.ts @@ -1,5 +1,6 @@ import { sanitizeUrl } from '@braintree/sanitize-url'; -import type { Group, SVG } from '../../diagram-api/types.js'; +import type { SVG, SVGGroup } from '../../diagram-api/types.js'; +import { lineBreakRegex } from './common.js'; import type { Bound, D3ImageElement, @@ -11,9 +12,8 @@ import type { TextData, TextObject, } from './commonTypes.js'; -import { lineBreakRegex } from './common.js'; -export const drawRect = (element: SVG | Group, rectData: RectData): D3RectElement => { +export const drawRect = (element: SVG | SVGGroup, rectData: RectData): D3RectElement => { const rectElement: D3RectElement = element.append('rect'); rectElement.attr('x', rectData.x); rectElement.attr('y', rectData.y); @@ -50,7 +50,7 @@ export const drawRect = (element: SVG | Group, rectData: RectData): D3RectElemen * @param element - Diagram (reference for bounds) * @param bounds - Shape of the rectangle */ -export const drawBackgroundRect = (element: SVG | Group, bounds: Bound): void => { +export const drawBackgroundRect = (element: SVG | SVGGroup, bounds: Bound): void => { const rectData: RectData = { x: bounds.startx, y: bounds.starty, @@ -64,7 +64,7 @@ export const drawBackgroundRect = (element: SVG | Group, bounds: Bound): void => rectElement.lower(); }; -export const drawText = (element: SVG | Group, textData: TextData): D3TextElement => { +export const drawText = (element: SVG | SVGGroup, textData: TextData): D3TextElement => { const nText: string = textData.text.replace(lineBreakRegex, ' '); const textElem: D3TextElement = element.append('text'); @@ -84,7 +84,7 @@ export const drawText = (element: SVG | Group, textData: TextData): D3TextElemen return textElem; }; -export const drawImage = (elem: SVG | Group, x: number, y: number, link: string): void => { +export const drawImage = (elem: SVG | SVGGroup, x: number, y: number, link: string): void => { const imageElement: D3ImageElement = elem.append('image'); imageElement.attr('x', x); imageElement.attr('y', y); @@ -93,7 +93,7 @@ export const drawImage = (elem: SVG | Group, x: number, y: number, link: string) }; export const drawEmbeddedImage = ( - element: SVG | Group, + element: SVG | SVGGroup, x: number, y: number, link: string diff --git a/packages/mermaid/src/diagrams/error/errorRenderer.ts b/packages/mermaid/src/diagrams/error/errorRenderer.ts index 1b3622c6d..a5f10acef 100644 --- a/packages/mermaid/src/diagrams/error/errorRenderer.ts +++ b/packages/mermaid/src/diagrams/error/errorRenderer.ts @@ -1,5 +1,5 @@ +import type { SVG, SVGGroup } from '../../diagram-api/types.js'; import { log } from '../../logger.js'; -import type { Group, SVG } from '../../diagram-api/types.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; @@ -13,7 +13,7 @@ import { configureSvgSize } from '../../setupGraphViewbox.js'; export const draw = (_text: string, id: string, version: string) => { log.debug('rendering svg for syntax error\n'); const svg: SVG = selectSvgElement(id); - const g: Group = svg.append('g'); + const g: SVGGroup = svg.append('g'); svg.attr('viewBox', '0 0 2412 512'); configureSvgSize(svg, 100, 512, true); diff --git a/packages/mermaid/src/diagrams/flowchart/elk/detector.ts b/packages/mermaid/src/diagrams/flowchart/elk/detector.ts index b476ff11b..6688ffd8c 100644 --- a/packages/mermaid/src/diagrams/flowchart/elk/detector.ts +++ b/packages/mermaid/src/diagrams/flowchart/elk/detector.ts @@ -1,34 +1,26 @@ import type { - ExternalDiagramDefinition, DiagramDetector, DiagramLoader, + ExternalDiagramDefinition, } from '../../../diagram-api/types.js'; -import { log } from '../../../logger.js'; const id = 'flowchart-elk'; -const detector: DiagramDetector = (txt, config): boolean => { +const detector: DiagramDetector = (txt, config = {}): boolean => { if ( // If diagram explicitly states flowchart-elk /^\s*flowchart-elk/.test(txt) || // If a flowchart/graph diagram has their default renderer set to elk (/^\s*flowchart|graph/.test(txt) && config?.flowchart?.defaultRenderer === 'elk') ) { - // This will log at the end, hopefully. - setTimeout( - () => - log.warn( - 'flowchart-elk was moved to an external package in Mermaid v11. Please refer [release notes](link) for more details. This diagram will be rendered using `dagre` layout as a fallback.' - ), - 500 - ); + config.layout = 'elk'; return true; } return false; }; const loader: DiagramLoader = async () => { - const { diagram } = await import('../flowDiagram-v2.js'); + const { diagram } = await import('../flowDiagram.js'); return { id, diagram }; }; diff --git a/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts b/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts index dda5a67f0..b66afe4bf 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts @@ -1,5 +1,8 @@ -import type { DiagramDetector, DiagramLoader } from '../../diagram-api/types.js'; -import type { ExternalDiagramDefinition } from '../../diagram-api/types.js'; +import type { + DiagramDetector, + DiagramLoader, + ExternalDiagramDefinition, +} from '../../diagram-api/types.js'; const id = 'flowchart-v2'; @@ -19,7 +22,7 @@ const detector: DiagramDetector = (txt, config) => { }; const loader: DiagramLoader = async () => { - const { diagram } = await import('./flowDiagram-v2.js'); + const { diagram } = await import('./flowDiagram.js'); return { id, diagram }; }; diff --git a/packages/mermaid/src/diagrams/flowchart/flowDiagram-v2.ts b/packages/mermaid/src/diagrams/flowchart/flowDiagram-v2.ts deleted file mode 100644 index 5b8012ede..000000000 --- a/packages/mermaid/src/diagrams/flowchart/flowDiagram-v2.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-ignore: JISON doesn't support types -import flowParser from './parser/flow.jison'; -import flowDb from './flowDb.js'; -import renderer from './flowRenderer-v3-unified.js'; -import flowStyles from './styles.js'; -import type { MermaidConfig } from '../../config.type.js'; -import { setConfig } from '../../diagram-api/diagramAPI.js'; - -export const diagram = { - parser: flowParser, - db: flowDb, - renderer, - styles: flowStyles, - init: (cnf: MermaidConfig) => { - if (!cnf.flowchart) { - cnf.flowchart = {}; - } - cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; - setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } }); - flowDb.clear(); - flowDb.setGen('gen-2'); - }, -}; diff --git a/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts b/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts index 5b8012ede..67cdf918f 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts @@ -1,10 +1,10 @@ -// @ts-ignore: JISON doesn't support types -import flowParser from './parser/flow.jison'; -import flowDb from './flowDb.js'; -import renderer from './flowRenderer-v3-unified.js'; -import flowStyles from './styles.js'; import type { MermaidConfig } from '../../config.type.js'; import { setConfig } from '../../diagram-api/diagramAPI.js'; +import flowDb from './flowDb.js'; +import renderer from './flowRenderer-v3-unified.js'; +// @ts-ignore: JISON doesn't support types +import flowParser from './parser/flow.jison'; +import flowStyles from './styles.js'; export const diagram = { parser: flowParser, @@ -15,6 +15,9 @@ export const diagram = { if (!cnf.flowchart) { cnf.flowchart = {}; } + if (cnf.layout) { + setConfig({ layout: cnf.layout }); + } cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } }); flowDb.clear(); diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts index 102662ee6..5d52b64e8 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts @@ -3,7 +3,7 @@ import { getConfig } from '../../diagram-api/diagramAPI.js'; import type { DiagramStyleClassDef } from '../../diagram-api/types.js'; import { log } from '../../logger.js'; import { getDiagramElements } from '../../rendering-util/insertElementsForSize.js'; -import { render } from '../../rendering-util/render.js'; +import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js'; import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; import type { LayoutData } from '../../rendering-util/types.js'; import utils from '../../utils.js'; @@ -40,7 +40,12 @@ export const draw = async function (text: string, id: string, _version: string, const direction = getDirection(); data4Layout.type = diag.type; - data4Layout.layoutAlgorithm = layout; + data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout); + if (data4Layout.layoutAlgorithm === 'dagre' && layout === 'elk') { + log.warn( + 'flowchart-elk was moved to an external package in Mermaid v11. Please refer [release notes](https://github.com/mermaid-js/mermaid/releases/tag/v11.0.0) for more details. This diagram will be rendered using `dagre` layout as a fallback.' + ); + } data4Layout.direction = direction; data4Layout.nodeSpacing = conf?.nodeSpacing || 50; data4Layout.rankSpacing = conf?.rankSpacing || 50; diff --git a/packages/mermaid/src/diagrams/info/infoRenderer.ts b/packages/mermaid/src/diagrams/info/infoRenderer.ts index 25ae72fce..a8314eb72 100644 --- a/packages/mermaid/src/diagrams/info/infoRenderer.ts +++ b/packages/mermaid/src/diagrams/info/infoRenderer.ts @@ -1,7 +1,7 @@ +import type { DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js'; import { log } from '../../logger.js'; -import { configureSvgSize } from '../../setupGraphViewbox.js'; -import type { DrawDefinition, Group, SVG } from '../../diagram-api/types.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; +import { configureSvgSize } from '../../setupGraphViewbox.js'; /** * Draws a an info picture in the tag with id: id based on the graph definition in text. @@ -16,7 +16,7 @@ const draw: DrawDefinition = (text, id, version) => { const svg: SVG = selectSvgElement(id); configureSvgSize(svg, 100, 400, true); - const group: Group = svg.append('g'); + const group: SVGGroup = svg.append('g'); group .append('text') .attr('x', 100) diff --git a/packages/mermaid/src/diagrams/packet/renderer.ts b/packages/mermaid/src/diagrams/packet/renderer.ts index c89e055cc..25445a228 100644 --- a/packages/mermaid/src/diagrams/packet/renderer.ts +++ b/packages/mermaid/src/diagrams/packet/renderer.ts @@ -1,6 +1,6 @@ import type { Diagram } from '../../Diagram.js'; import type { PacketDiagramConfig } from '../../config.type.js'; -import type { DiagramRenderer, DrawDefinition, Group, SVG } from '../../diagram-api/types.js'; +import type { DiagramRenderer, DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; import type { PacketDB, PacketWord } from './types.js'; @@ -39,7 +39,7 @@ const drawWord = ( rowNumber: number, { rowHeight, paddingX, paddingY, bitWidth, bitsPerRow, showBits }: Required ) => { - const group: Group = svg.append('g'); + const group: SVGGroup = svg.append('g'); const wordY = rowNumber * (rowHeight + paddingY) + paddingY; for (const block of word) { const blockX = (block.start % bitsPerRow) * bitWidth + 1; diff --git a/packages/mermaid/src/diagrams/pie/pieRenderer.ts b/packages/mermaid/src/diagrams/pie/pieRenderer.ts index 8f3b9cc5b..a0cdce3df 100644 --- a/packages/mermaid/src/diagrams/pie/pieRenderer.ts +++ b/packages/mermaid/src/diagrams/pie/pieRenderer.ts @@ -1,13 +1,13 @@ import type d3 from 'd3'; -import { scaleOrdinal, pie as d3pie, arc } from 'd3'; -import { log } from '../../logger.js'; -import { configureSvgSize } from '../../setupGraphViewbox.js'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; -import { cleanAndMerge, parseFontSize } from '../../utils.js'; -import type { DrawDefinition, Group, SVG } from '../../diagram-api/types.js'; -import type { D3Section, PieDB, Sections } from './pieTypes.js'; +import { arc, pie as d3pie, scaleOrdinal } from 'd3'; import type { MermaidConfig, PieDiagramConfig } from '../../config.type.js'; +import { getConfig } from '../../diagram-api/diagramAPI.js'; +import type { DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js'; +import { log } from '../../logger.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; +import { configureSvgSize } from '../../setupGraphViewbox.js'; +import { cleanAndMerge, parseFontSize } from '../../utils.js'; +import type { D3Section, PieDB, Sections } from './pieTypes.js'; const createPieArcs = (sections: Sections): d3.PieArcDatum[] => { // Compute the position of each group on the pie: @@ -46,7 +46,7 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => { const height = 450; const pieWidth: number = height; const svg: SVG = selectSvgElement(id); - const group: Group = svg.append('g'); + const group: SVGGroup = svg.append('g'); group.attr('transform', 'translate(' + pieWidth / 2 + ',' + height / 2 + ')'); const { themeVariables } = globalConfig; diff --git a/packages/mermaid/src/diagrams/requirement/requirementRenderer.js b/packages/mermaid/src/diagrams/requirement/requirementRenderer.js index 6b5143752..778dc36b1 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementRenderer.js +++ b/packages/mermaid/src/diagrams/requirement/requirementRenderer.js @@ -1,11 +1,11 @@ import { line, select } from 'd3'; import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js'; import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; +import { getConfig } from '../../diagram-api/diagramAPI.js'; import { log } from '../../logger.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; import common from '../common/common.js'; import markers from './requirementMarkers.js'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; let conf = {}; let relCnt = 0; diff --git a/packages/mermaid/src/diagrams/state/dataFetcher.js b/packages/mermaid/src/diagrams/state/dataFetcher.js index 35cde69ab..921544ff2 100644 --- a/packages/mermaid/src/diagrams/state/dataFetcher.js +++ b/packages/mermaid/src/diagrams/state/dataFetcher.js @@ -166,43 +166,11 @@ function insertOrUpdateNode(nodes, nodeData, classes) { * @returns {string} */ function getClassesFromDbInfo(dbInfoItem) { - if (dbInfoItem === undefined || dbInfoItem === null) { - return ''; - } else { - if (dbInfoItem.classes) { - let classStr = ''; - // for each class in classes, add it to the string as comma separated - for (let i = 0; i < dbInfoItem.classes.length; i++) { - //do not add comma for the last class - if (i === dbInfoItem.classes.length - 1) { - classStr += dbInfoItem.classes[i]; - } - //add comma for all other classes - else { - classStr += dbInfoItem.classes[i] + ' '; - } - } - return classStr; - } else { - return ''; - } - } + return dbInfoItem?.classes?.join(' ') ?? ''; } -/** - * Get classes from the db for the info item. - * If there aren't any or if dbInfoItem isn't defined, return an empty string. - * Else create 1 string from the list of classes found - */ + function getStylesFromDbInfo(dbInfoItem) { - if (dbInfoItem === undefined || dbInfoItem === null) { - return; - } else { - if (dbInfoItem.styles) { - return dbInfoItem.styles; - } else { - return []; - } - } + return dbInfoItem?.styles ?? []; } export const dataFetcher = ( @@ -224,10 +192,10 @@ export const dataFetcher = ( if (itemId !== 'root') { let shape = SHAPE_STATE; + // The if === true / false can be removed if we can guarantee that the parsedItem.start is always a boolean if (parsedItem.start === true) { shape = SHAPE_START; - } - if (parsedItem.start === false) { + } else if (parsedItem.start === false) { shape = SHAPE_END; } if (parsedItem.type !== DEFAULT_STATE_TYPE) { diff --git a/packages/mermaid/src/diagrams/timeline/parser/timeline.jison b/packages/mermaid/src/diagrams/timeline/parser/timeline.jison index 348c31fad..89bfd06f4 100644 --- a/packages/mermaid/src/diagrams/timeline/parser/timeline.jison +++ b/packages/mermaid/src/diagrams/timeline/parser/timeline.jison @@ -18,7 +18,7 @@ \#[^\n]* /* skip comments */ "timeline" return 'timeline'; -"title"\s[^#\n;]+ return 'title'; +"title"\s[^\n]+ return 'title'; accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; } (?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; } accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; } @@ -26,11 +26,11 @@ accDescr\s*":"\s* { this.begin("ac accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} [\}] { this.popState(); } [^\}]* return "acc_descr_multiline_value"; -"section"\s[^#:\n;]+ return 'section'; +"section"\s[^:\n]+ return 'section'; // event starting with "==>" keyword -":"\s[^#:\n;]+ return 'event'; -[^#:\n;]+ return 'period'; +":"\s[^:\n]+ return 'event'; +[^#:\n]+ return 'period'; <> return 'EOF'; diff --git a/packages/mermaid/src/diagrams/timeline/timeline.spec.js b/packages/mermaid/src/diagrams/timeline/timeline.spec.js index 69b9df1ba..a7005cada 100644 --- a/packages/mermaid/src/diagrams/timeline/timeline.spec.js +++ b/packages/mermaid/src/diagrams/timeline/timeline.spec.js @@ -1,6 +1,7 @@ +import { setLogLevel } from '../../diagram-api/diagramAPI.js'; +import * as commonDb from '../common/commonDb.js'; import { parser as timeline } from './parser/timeline.jison'; import * as timelineDB from './timelineDb.js'; -import { setLogLevel } from '../../diagram-api/diagramAPI.js'; describe('when parsing a timeline ', function () { beforeEach(function () { @@ -9,7 +10,7 @@ describe('when parsing a timeline ', function () { setLogLevel('trace'); }); describe('Timeline', function () { - it('TL-1 should handle a simple section definition abc-123', function () { + it('should handle a simple section definition abc-123', function () { let str = `timeline section abc-123`; @@ -17,7 +18,7 @@ describe('when parsing a timeline ', function () { expect(timelineDB.getSections()).to.deep.equal(['abc-123']); }); - it('TL-2 should handle a simple section and only two tasks', function () { + it('should handle a simple section and only two tasks', function () { let str = `timeline section abc-123 task1 @@ -29,7 +30,7 @@ describe('when parsing a timeline ', function () { }); }); - it('TL-3 should handle a two section and two coressponding tasks', function () { + it('should handle a two section and two coressponding tasks', function () { let str = `timeline section abc-123 task1 @@ -50,7 +51,7 @@ describe('when parsing a timeline ', function () { }); }); - it('TL-4 should handle a section, and task and its events', function () { + it('should handle a section, and task and its events', function () { let str = `timeline section abc-123 task1: event1 @@ -74,7 +75,7 @@ describe('when parsing a timeline ', function () { }); }); - it('TL-5 should handle a section, and task and its multi line events', function () { + it('should handle a section, and task and its multi line events', function () { let str = `timeline section abc-123 task1: event1 @@ -98,5 +99,42 @@ describe('when parsing a timeline ', function () { } }); }); + + it('should handle a title, section, task, and events with semicolons', function () { + let str = `timeline + title ;my;title; + section ;a;bc-123; + ;ta;sk1;: ;ev;ent1; : ;ev;ent2; : ;ev;ent3; + `; + timeline.parse(str); + expect(commonDb.getDiagramTitle()).equal(';my;title;'); + expect(timelineDB.getSections()).to.deep.equal([';a;bc-123;']); + expect(timelineDB.getTasks()[0].events).toMatchInlineSnapshot(` + [ + ";ev;ent1; ", + ";ev;ent2; ", + ";ev;ent3;", + ] + `); + }); + + it('should handle a title, section, task, and events with hashtags', function () { + let str = `timeline + title #my#title# + section #a#bc-123# + task1: #ev#ent1# : #ev#ent2# : #ev#ent3# + `; + timeline.parse(str); + expect(commonDb.getDiagramTitle()).equal('#my#title#'); + expect(timelineDB.getSections()).to.deep.equal(['#a#bc-123#']); + expect(timelineDB.getTasks()[0].task).equal('task1'); + expect(timelineDB.getTasks()[0].events).toMatchInlineSnapshot(` + [ + "#ev#ent1# ", + "#ev#ent2# ", + "#ev#ent3#", + ] + `); + }); }); }); diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/index.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/index.ts index cde0d6a93..a1ec492f6 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/index.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/axis/index.ts @@ -1,4 +1,4 @@ -import type { Group } from '../../../../../diagram-api/types.js'; +import type { SVGGroup } from '../../../../../diagram-api/types.js'; import type { AxisDataType, ChartComponent, @@ -25,7 +25,7 @@ export function getAxis( data: AxisDataType, axisConfig: XYChartAxisConfig, axisThemeConfig: XYChartAxisThemeConfig, - tmpSVGGroup: Group + tmpSVGGroup: SVGGroup ): Axis { const textDimensionCalculator = new TextDimensionCalculatorWithFont(tmpSVGGroup); if (isBandAxisData(data)) { diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/chartTitle.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/chartTitle.ts index bbab56bdc..5512df988 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/components/chartTitle.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/components/chartTitle.ts @@ -1,13 +1,13 @@ -import type { Group } from '../../../../diagram-api/types.js'; +import type { SVGGroup } from '../../../../diagram-api/types.js'; import type { BoundingRect, ChartComponent, Dimension, DrawableElem, Point, + XYChartConfig, XYChartData, XYChartThemeConfig, - XYChartConfig, } from '../interfaces.js'; import type { TextDimensionCalculator } from '../textDimensionCalculator.js'; import { TextDimensionCalculatorWithFont } from '../textDimensionCalculator.js'; @@ -84,7 +84,7 @@ export function getChartTitleComponent( chartConfig: XYChartConfig, chartData: XYChartData, chartThemeConfig: XYChartThemeConfig, - tmpSVGGroup: Group + tmpSVGGroup: SVGGroup ): ChartComponent { const textDimensionCalculator = new TextDimensionCalculatorWithFont(tmpSVGGroup); return new ChartTitle(textDimensionCalculator, chartConfig, chartData, chartThemeConfig); diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/index.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/index.ts index 192eb47f6..6a1b6ec3a 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/index.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/index.ts @@ -1,4 +1,4 @@ -import type { Group } from '../../../diagram-api/types.js'; +import type { SVGGroup } from '../../../diagram-api/types.js'; import type { DrawableElem, XYChartConfig, XYChartData, XYChartThemeConfig } from './interfaces.js'; import { Orchestrator } from './orchestrator.js'; @@ -7,7 +7,7 @@ export class XYChartBuilder { config: XYChartConfig, chartData: XYChartData, chartThemeConfig: XYChartThemeConfig, - tmpSVGGroup: Group + tmpSVGGroup: SVGGroup ): DrawableElem[] { const orchestrator = new Orchestrator(config, chartData, chartThemeConfig, tmpSVGGroup); return orchestrator.getDrawableElement(); diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/orchestrator.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/orchestrator.ts index 8160d1500..8809efe26 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/orchestrator.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/orchestrator.ts @@ -1,3 +1,9 @@ +import type { SVGGroup } from '../../../diagram-api/types.js'; +import type { Axis } from './components/axis/index.js'; +import { getAxis } from './components/axis/index.js'; +import { getChartTitleComponent } from './components/chartTitle.js'; +import type { Plot } from './components/plot/index.js'; +import { getPlotComponent } from './components/plot/index.js'; import type { ChartComponent, DrawableElem, @@ -6,12 +12,6 @@ import type { XYChartThemeConfig, } from './interfaces.js'; import { isBarPlot } from './interfaces.js'; -import type { Axis } from './components/axis/index.js'; -import { getAxis } from './components/axis/index.js'; -import { getChartTitleComponent } from './components/chartTitle.js'; -import type { Plot } from './components/plot/index.js'; -import { getPlotComponent } from './components/plot/index.js'; -import type { Group } from '../../../diagram-api/types.js'; export class Orchestrator { private componentStore: { @@ -24,7 +24,7 @@ export class Orchestrator { private chartConfig: XYChartConfig, private chartData: XYChartData, chartThemeConfig: XYChartThemeConfig, - tmpSVGGroup: Group + tmpSVGGroup: SVGGroup ) { this.componentStore = { title: getChartTitleComponent(chartConfig, chartData, chartThemeConfig, tmpSVGGroup), diff --git a/packages/mermaid/src/diagrams/xychart/chartBuilder/textDimensionCalculator.ts b/packages/mermaid/src/diagrams/xychart/chartBuilder/textDimensionCalculator.ts index 8049bf527..0f118fc92 100644 --- a/packages/mermaid/src/diagrams/xychart/chartBuilder/textDimensionCalculator.ts +++ b/packages/mermaid/src/diagrams/xychart/chartBuilder/textDimensionCalculator.ts @@ -1,13 +1,13 @@ -import type { Dimension } from './interfaces.js'; +import type { SVGGroup } from '../../../diagram-api/types.js'; import { computeDimensionOfText } from '../../../rendering-util/createText.js'; -import type { Group } from '../../../diagram-api/types.js'; +import type { Dimension } from './interfaces.js'; export interface TextDimensionCalculator { getMaxDimension(texts: string[], fontSize: number): Dimension; } export class TextDimensionCalculatorWithFont implements TextDimensionCalculator { - constructor(private parentGroup: Group) {} + constructor(private parentGroup: SVGGroup) {} getMaxDimension(texts: string[], fontSize: number): Dimension { if (!this.parentGroup) { return { diff --git a/packages/mermaid/src/diagrams/xychart/xychartDb.ts b/packages/mermaid/src/diagrams/xychart/xychartDb.ts index 23b90724c..fb2435df2 100644 --- a/packages/mermaid/src/diagrams/xychart/xychartDb.ts +++ b/packages/mermaid/src/diagrams/xychart/xychartDb.ts @@ -1,3 +1,9 @@ +import * as configApi from '../../config.js'; +import defaultConfig from '../../defaultConfig.js'; +import type { SVGGroup } from '../../diagram-api/types.js'; +import { getThemeVariables } from '../../themes/theme-default.js'; +import { cleanAndMerge } from '../../utils.js'; +import { sanitizeText } from '../common/common.js'; import { clear as commonClear, getAccDescription, @@ -7,11 +13,6 @@ import { setAccTitle, setDiagramTitle, } from '../common/commonDb.js'; -import * as configApi from '../../config.js'; -import defaultConfig from '../../defaultConfig.js'; -import { getThemeVariables } from '../../themes/theme-default.js'; -import { cleanAndMerge } from '../../utils.js'; -import { sanitizeText } from '../common/common.js'; import { XYChartBuilder } from './chartBuilder/index.js'; import type { DrawableElem, @@ -21,11 +22,10 @@ import type { XYChartThemeConfig, } from './chartBuilder/interfaces.js'; import { isBandAxisData, isLinearAxisData } from './chartBuilder/interfaces.js'; -import type { Group } from '../../diagram-api/types.js'; let plotIndex = 0; -let tmpSVGGroup: Group; +let tmpSVGGroup: SVGGroup; let xyChartConfig: XYChartConfig = getChartDefaultConfig(); let xyChartThemeConfig: XYChartThemeConfig = getChartDefaultThemeConfig(); @@ -75,7 +75,7 @@ function textSanitizer(text: string) { return sanitizeText(text.trim(), config); } -function setTmpSVGG(SVGG: Group) { +function setTmpSVGG(SVGG: SVGGroup) { tmpSVGGroup = SVGG; } function setOrientation(orientation: string) { diff --git a/packages/mermaid/src/docs/ecosystem/integrations-community.md b/packages/mermaid/src/docs/ecosystem/integrations-community.md index d77a82b44..2d5972f20 100644 --- a/packages/mermaid/src/docs/ecosystem/integrations-community.md +++ b/packages/mermaid/src/docs/ecosystem/integrations-community.md @@ -51,6 +51,7 @@ To add an integration to this list, see the [Integrations - create page](./integ - [SVG diagram generator](https://github.com/SimonKenyonShepard/mermaidjs-github-svg-generator) - [GitLab](https://docs.gitlab.com/ee/user/markdown.html#diagrams-and-flowcharts) ✅ - [Mermaid Plugin for JetBrains IDEs](https://plugins.jetbrains.com/plugin/20146-mermaid) +- [MonsterWriter](https://www.monsterwriter.com/) ✅ - [Joplin](https://joplinapp.org) ✅ - [LiveBook](https://livebook.dev) ✅ - [Tuleap](https://docs.tuleap.org/user-guide/writing-in-tuleap.html#graphs) ✅ diff --git a/packages/mermaid/src/internals.ts b/packages/mermaid/src/internals.ts index cebcd4ace..7cc058cb3 100644 --- a/packages/mermaid/src/internals.ts +++ b/packages/mermaid/src/internals.ts @@ -29,3 +29,5 @@ export const internalHelpers = { log, positionEdgeLabel, }; + +export type InternalHelpers = typeof internalHelpers; diff --git a/packages/mermaid/src/mermaid.ts b/packages/mermaid/src/mermaid.ts index f542b22f2..0cc94bcc2 100644 --- a/packages/mermaid/src/mermaid.ts +++ b/packages/mermaid/src/mermaid.ts @@ -4,38 +4,41 @@ */ import { dedent } from 'ts-dedent'; import type { MermaidConfig } from './config.type.js'; -import { log } from './logger.js'; -import utils from './utils.js'; -import type { ParseOptions, ParseResult, RenderResult } from './types.js'; -import { mermaidAPI } from './mermaidAPI.js'; -import { registerLazyLoadedDiagrams, detectType } from './diagram-api/detectType.js'; +import { detectType, registerLazyLoadedDiagrams } from './diagram-api/detectType.js'; +import { addDiagrams } from './diagram-api/diagram-orchestration.js'; import { loadRegisteredDiagrams } from './diagram-api/loadDiagram.js'; +import type { ExternalDiagramDefinition, SVG, SVGGroup } from './diagram-api/types.js'; import type { ParseErrorFunction } from './Diagram.js'; -import { isDetailedError } from './utils.js'; -import type { DetailedError } from './utils.js'; -import type { ExternalDiagramDefinition } from './diagram-api/types.js'; import type { UnknownDiagramError } from './errors.js'; +import type { InternalHelpers } from './internals.js'; +import { log } from './logger.js'; +import { mermaidAPI } from './mermaidAPI.js'; +import type { LayoutLoaderDefinition, RenderOptions } from './rendering-util/render.js'; +import { registerLayoutLoaders } from './rendering-util/render.js'; +import type { LayoutData } from './rendering-util/types.js'; +import type { ParseOptions, ParseResult, RenderResult } from './types.js'; +import type { DetailedError } from './utils.js'; +import utils, { isDetailedError } from './utils.js'; import type { IconLibrary, IconResolver } from './rendering-util/svgRegister.js'; import { createIcon } from './rendering-util/svgRegister.js'; -import { addDiagrams } from './diagram-api/diagram-orchestration.js'; -import { registerLayoutLoaders } from './rendering-util/render.js'; -import type { LayoutLoaderDefinition } from './rendering-util/render.js'; -import { internalHelpers } from './internals.js'; -import type { LayoutData } from './rendering-util/types.js'; export type { - MermaidConfig, DetailedError, ExternalDiagramDefinition, + InternalHelpers, + LayoutData, + LayoutLoaderDefinition, + MermaidConfig, ParseErrorFunction, - RenderResult, ParseOptions, ParseResult, + RenderOptions, + RenderResult, + SVG, + SVGGroup, UnknownDiagramError, IconLibrary, IconResolver, - LayoutLoaderDefinition, - LayoutData, }; export { createIcon }; @@ -438,11 +441,6 @@ export interface Mermaid { contentLoaded: typeof contentLoaded; setParseErrorHandler: typeof setParseErrorHandler; detectType: typeof detectType; - /** - * Internal helpers for mermaid - * @deprecated - This should not be used by external packages, as the definitions will change without notice. - */ - internalHelpers: typeof internalHelpers; } const mermaid: Mermaid = { @@ -459,7 +457,6 @@ const mermaid: Mermaid = { contentLoaded, setParseErrorHandler, detectType, - internalHelpers, }; export default mermaid; diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index 7fb8b8401..af2fbd04d 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -28,6 +28,7 @@ import { registerIcons } from './rendering-util/svgRegister.js'; import defaultIconLibrary from './rendering-util/svg/index.js'; import { toBase64 } from './utils/base64.js'; import type { D3Element, ParseOptions, ParseResult, RenderResult } from './types.js'; +import assignWithDepth from './assignWithDepth.js'; const MAX_TEXTLENGTH = 50_000; const MAX_TEXTLENGTH_EXCEEDED_MSG = @@ -476,9 +477,10 @@ const render = async function ( }; /** - * @param options - Initial Mermaid options + * @param userOptions - Initial Mermaid options */ -function initialize(options: MermaidConfig = {}) { +function initialize(userOptions: MermaidConfig = {}) { + const options: MermaidConfig = assignWithDepth({}, userOptions); // Handle legacy location of font-family configuration if (options?.fontFamily && !options.themeVariables?.fontFamily) { if (!options.themeVariables) { diff --git a/packages/mermaid/src/rendering-util/createText.ts b/packages/mermaid/src/rendering-util/createText.ts index 12d81715f..c6ce204ba 100644 --- a/packages/mermaid/src/rendering-util/createText.ts +++ b/packages/mermaid/src/rendering-util/createText.ts @@ -1,16 +1,16 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ // @ts-nocheck TODO: Fix types -import type { MermaidConfig } from '../config.type.js'; -import type { Group } from '../diagram-api/types.js'; +import { getConfig } from '$root/diagram-api/diagramAPI.js'; +import common, { hasKatex, renderKatex } from '$root/diagrams/common/common.js'; import { select } from 'd3'; +import type { MermaidConfig } from '../config.type.js'; +import type { SVGGroup } from '../diagram-api/types.js'; import type { D3TSpanElement, D3TextElement } from '../diagrams/common/commonTypes.js'; import { log } from '../logger.js'; import { markdownToHTML, markdownToLines } from '../rendering-util/handle-markdown-text.js'; import { decodeEntities } from '../utils.js'; import { splitLineToFitWidth } from './splitText.js'; import type { MarkdownLine, MarkdownWord } from './types.js'; -import common, { hasKatex, renderKatex } from '$root/diagrams/common/common.js'; -import { getConfig } from '$root/diagram-api/diagramAPI.js'; function applyStyle(dom, styleFn) { if (styleFn) { @@ -82,7 +82,7 @@ function computeWidthOfText(parentNode: any, lineHeight: number, line: MarkdownL } export function computeDimensionOfText( - parentNode: Group, + parentNode: SVGGroup, lineHeight: number, text: string ): DOMRect | undefined { diff --git a/packages/mermaid/src/rendering-util/render.ts b/packages/mermaid/src/rendering-util/render.ts index 442780c75..8b28fe82b 100644 --- a/packages/mermaid/src/rendering-util/render.ts +++ b/packages/mermaid/src/rendering-util/render.ts @@ -1,5 +1,21 @@ +import type { SVG } from '$root/diagram-api/types.js'; +import type { InternalHelpers } from '$root/internals.js'; +import { internalHelpers } from '$root/internals.js'; +import { log } from '$root/logger.js'; +import type { LayoutData } from './types.js'; + +export interface RenderOptions { + algorithm?: string; +} + export interface LayoutAlgorithm { - render(data4Layout: any, svg: any, element: any, algorithm?: string): any; + render( + layoutData: LayoutData, + svg: SVG, + element: any, + helpers: InternalHelpers, + options?: RenderOptions + ): Promise; } export type LayoutLoader = () => Promise; @@ -24,21 +40,33 @@ const registerDefaultLayoutLoaders = () => { name: 'dagre', loader: async () => await import('./layout-algorithms/dagre/index.js'), }, - // { - // name: 'elk', - // loader: async () => await import('../../../mermaid-layout-elk/src/render.js'), - // }, ]); }; registerDefaultLayoutLoaders(); -export const render = async (data4Layout: any, svg: any, element: any) => { +export const render = async (data4Layout: LayoutData, svg: SVG, element: any) => { if (!(data4Layout.layoutAlgorithm in layoutAlgorithms)) { throw new Error(`Unknown layout algorithm: ${data4Layout.layoutAlgorithm}`); } const layoutDefinition = layoutAlgorithms[data4Layout.layoutAlgorithm]; const layoutRenderer = await layoutDefinition.loader(); - return layoutRenderer.render(data4Layout, svg, element, layoutDefinition.algorithm); + return layoutRenderer.render(data4Layout, svg, element, internalHelpers, { + algorithm: layoutDefinition.algorithm, + }); +}; + +/** + * Get the registered layout algorithm. If the algorithm is not registered, use the fallback algorithm. + */ +export const getRegisteredLayoutAlgorithm = (algorithm = '', { fallback = 'dagre' } = {}) => { + if (algorithm in layoutAlgorithms) { + return algorithm; + } + if (fallback in layoutAlgorithms) { + log.warn(`Layout algorithm ${algorithm} is not registered. Using ${fallback} as fallback.`); + return fallback; + } + throw new Error(`Both layout algorithms ${algorithm} and ${fallback} are not registered.`); }; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js index 6d0c157dd..087fcf0be 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js @@ -538,6 +538,27 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod .attr('style', edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : ''); } + // DEBUG code, DO NOT REMOVE + // adds a red circle at each edge coordinate + // cornerPoints.forEach((point) => { + // elem + // .append('circle') + // .style('stroke', 'blue') + // .style('fill', 'blue') + // .attr('r', 3) + // .attr('cx', point.x) + // .attr('cy', point.y); + // }); + // lineData.forEach((point) => { + // elem + // .append('circle') + // .style('stroke', 'blue') + // .style('fill', 'blue') + // .attr('r', 3) + // .attr('cx', point.x) + // .attr('cy', point.y); + // }); + let url = ''; if (getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute) { url = diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 017060756..acaa9171e 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -119,7 +119,7 @@ properties: - NETWORK_SIMPLEX - LINEAR_SEGMENTS - BRANDES_KOEPF - default: SIMPLE + default: BRANDES_KOEPF darkMode: type: boolean default: false diff --git a/packages/mermaid/src/styles.ts b/packages/mermaid/src/styles.ts index d0eead51d..78b514c40 100644 --- a/packages/mermaid/src/styles.ts +++ b/packages/mermaid/src/styles.ts @@ -70,6 +70,9 @@ const getStyles = ( font-family: ${options.fontFamily}; font-size: ${options.fontSize}; } + & p { + margin: 0 + } ${diagramStyles} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0663f9c8..d7050b136 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -407,31 +407,6 @@ importers: specifier: ^5.0.5 version: 5.0.10 - packages/mermaid-flowchart-elk: - dependencies: - d3: - specifier: ^7.9.0 - version: 7.9.0 - dagre-d3-es: - specifier: 7.0.10 - version: 7.0.10 - elkjs: - specifier: ^0.9.2 - version: 0.9.3 - khroma: - specifier: ^2.1.0 - version: 2.1.0 - devDependencies: - concurrently: - specifier: ^8.2.2 - version: 8.2.2 - mermaid: - specifier: workspace:^ - version: link:../mermaid - rimraf: - specifier: ^5.0.5 - version: 5.0.10 - packages/mermaid-layout-elk: dependencies: d3: @@ -443,6 +418,10 @@ importers: mermaid: specifier: workspace:^ version: link:../mermaid + devDependencies: + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 packages/mermaid-zenuml: dependencies: