diff --git a/.changeset/class-default-unified-renderer.md b/.changeset/class-default-unified-renderer.md new file mode 100644 index 00000000000..882824f8f27 --- /dev/null +++ b/.changeset/class-default-unified-renderer.md @@ -0,0 +1,12 @@ +--- +'mermaid': minor +--- + +feat: route `classDiagram` to the unified (v2) renderer by default + +`class.defaultRenderer` now defaults to `dagre-wrapper`, so plain `classDiagram` +documents (without the `-v2` suffix) render through the unified renderer and the +shared dagre layout/paint pipeline instead of the legacy class renderer. Set +`class: { defaultRenderer: 'dagre-d3' }` in the config to restore the legacy +renderer. Self-referential edges in class and ER diagrams now render as compact +merged self-loops. diff --git a/.fallowrc.json b/.fallowrc.json index ecd6748273b..5649028a7c4 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -2,7 +2,11 @@ "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json", "entry": [ "packages/mermaid/src/rendering-util/layout-algorithms/**/*.ts", - "packages/mermaid/src/rendering-util/layout-algorithms/**/*.js" + "packages/mermaid/src/rendering-util/layout-algorithms/**/*.js", + "packages/mermaid/src/diagrams/block/**/*.ts", + "packages/mermaid/src/diagrams/block/**/*.js", + "packages/mermaid/src/rendering-util/rendering-elements/**/*.ts", + "packages/mermaid/src/rendering-util/rendering-elements/**/*.js" ], "ignorePatterns": [ "packages/parser/**", @@ -27,7 +31,39 @@ "scripts/**", "packages/mermaid/scripts/**", "packages/mermaid/src/dagre-wrapper/**", - "packages/mermaid/src/diagrams/**", + "packages/mermaid/src/diagrams/architecture/**", + "packages/mermaid/src/diagrams/c4/**", + "packages/mermaid/src/diagrams/class/**", + "packages/mermaid/src/diagrams/common/**", + "packages/mermaid/src/diagrams/cynefin/**", + "packages/mermaid/src/diagrams/er/**", + "packages/mermaid/src/diagrams/error/**", + "packages/mermaid/src/diagrams/eventmodeling/**", + "packages/mermaid/src/diagrams/flowchart/**", + "packages/mermaid/src/diagrams/gantt/**", + "packages/mermaid/src/diagrams/git/**", + "packages/mermaid/src/diagrams/globalStyles.ts", + "packages/mermaid/src/diagrams/info/**", + "packages/mermaid/src/diagrams/ishikawa/**", + "packages/mermaid/src/diagrams/kanban/**", + "packages/mermaid/src/diagrams/mindmap/**", + "packages/mermaid/src/diagrams/packet/**", + "packages/mermaid/src/diagrams/pie/**", + "packages/mermaid/src/diagrams/quadrant-chart/**", + "packages/mermaid/src/diagrams/radar/**", + "packages/mermaid/src/diagrams/railroad/**", + "packages/mermaid/src/diagrams/requirement/**", + "packages/mermaid/src/diagrams/sankey/**", + "packages/mermaid/src/diagrams/sequence/**", + "packages/mermaid/src/diagrams/state/**", + "packages/mermaid/src/diagrams/swimlanes/**", + "packages/mermaid/src/diagrams/timeline/**", + "packages/mermaid/src/diagrams/treeView/**", + "packages/mermaid/src/diagrams/treemap/**", + "packages/mermaid/src/diagrams/user-journey/**", + "packages/mermaid/src/diagrams/venn/**", + "packages/mermaid/src/diagrams/wardley/**", + "packages/mermaid/src/diagrams/xychart/**", "packages/mermaid/src/diagram-api/**", "packages/mermaid/src/docs/**", "packages/mermaid/src/schemas/**", @@ -36,7 +72,6 @@ "packages/mermaid/src/types/**", "packages/mermaid/src/utils/**", "packages/mermaid/src/__mocks__/**", - "packages/mermaid/src/rendering-util/rendering-elements/**", ".lintstagedrc.mjs", "cypress.config.ts", "eslint.config.js", diff --git a/cypress/helpers/util.ts b/cypress/helpers/util.ts index c02435c0b1d..9e95e935c7e 100644 --- a/cypress/helpers/util.ts +++ b/cypress/helpers/util.ts @@ -103,7 +103,7 @@ export const openURLAndVerifyRendering = ( const name: string = (options.name ?? cy.state('runnable').fullTitle()).replace(/\s+/g, '-'); cy.visit(url); - cy.window().should('have.property', 'rendered', true); + cy.window({ timeout: 15000 }).should('have.property', 'rendered', true); // Handle sandbox mode where SVG is inside an iframe if (options.securityLevel === 'sandbox') { diff --git a/cypress/integration/rendering/class/classDiagram-handDrawn-v3.spec.js b/cypress/integration/rendering/class/classDiagram-handDrawn-v3.spec.js index d4009d518bb..f67fd219b36 100644 --- a/cypress/integration/rendering/class/classDiagram-handDrawn-v3.spec.js +++ b/cypress/integration/rendering/class/classDiagram-handDrawn-v3.spec.js @@ -1137,4 +1137,19 @@ class C13["With Città foreign language"] { class: { hierarchicalNamespaces: false } } ); }); + + it('HD: should render a self-referential class diagram with multiplicity labels', () => { + imgSnapshotTest( + ` + classDiagram + class SelfReferential{ + +int id + +int self_referential_id + +SelfReferential referenced + } + SelfReferential "1" --> "0..1" SelfReferential : referenced + `, + { logLevel: 1, htmlLabels: true, look: 'handDrawn' } + ); + }); }); diff --git a/cypress/platform/dev-diagrams/layout-tests/block-diagrams/test1.mmd b/cypress/platform/dev-diagrams/layout-tests/block-diagrams/test1.mmd new file mode 100644 index 00000000000..6b49a3c1fb2 --- /dev/null +++ b/cypress/platform/dev-diagrams/layout-tests/block-diagrams/test1.mmd @@ -0,0 +1,4 @@ +block-beta + columns 5 + PA["Paid proceeds (actual) $613"]:1 DEF["Deficit $5,155"]:4 + CA["Cash back (actual) $128"]:1 SPO["Spoilage (35 unsold) $5,640"]:4 diff --git a/cypress/platform/dev-diagrams/layout-tests/block-diagrams/test2.mmd b/cypress/platform/dev-diagrams/layout-tests/block-diagrams/test2.mmd new file mode 100644 index 00000000000..c1fd6d1ab78 --- /dev/null +++ b/cypress/platform/dev-diagrams/layout-tests/block-diagrams/test2.mmd @@ -0,0 +1,4 @@ +block + A>"rect_left_inv_arrow"] + B{"diamond"} + C{{"hexagon"}} diff --git a/cypress/platform/dev-diagrams/layout-tests/block-diagrams/test3.mmd b/cypress/platform/dev-diagrams/layout-tests/block-diagrams/test3.mmd new file mode 100644 index 00000000000..e763c580125 --- /dev/null +++ b/cypress/platform/dev-diagrams/layout-tests/block-diagrams/test3.mmd @@ -0,0 +1,6 @@ +block + A + B + classDef blue fill:#66f,stroke:#333,stroke-width:2px; + class A blue + style B fill:#f9F,stroke:#333,stroke-width:4px diff --git a/cypress/platform/dev-diagrams/layout-tests/block-diagrams/test4.mmd b/cypress/platform/dev-diagrams/layout-tests/block-diagrams/test4.mmd new file mode 100644 index 00000000000..617b07e00d1 --- /dev/null +++ b/cypress/platform/dev-diagrams/layout-tests/block-diagrams/test4.mmd @@ -0,0 +1,6 @@ +%% BL21 +block + A[/"lean right"/] + B[\"lean left"\] + C[/"trapezoid"\] + D[\"trapezoid alt"/] diff --git a/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/4813-title-and-arrow-styling.mmd b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/4813-title-and-arrow-styling.mmd new file mode 100644 index 00000000000..b396d70ee10 --- /dev/null +++ b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/4813-title-and-arrow-styling.mmd @@ -0,0 +1,6 @@ +--- +title: My title +--- +flowchart LR + A-->B + A-->C diff --git a/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/Render-stadium-shape.mmd b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/Render-stadium-shape.mmd new file mode 100644 index 00000000000..1ad9294470e --- /dev/null +++ b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/Render-stadium-shape.mmd @@ -0,0 +1,20 @@ +--- +config: + flowchart: + htmlLabels: false + fontFamily: 'courier' + elk: + nodePlacementAlignment: NONE +--- +flowchart-elk TD + A([stadium shape test]) + A -->|Get money| B([Go shopping]) + B --> C([Let me think...
Do I want something for work,
something to spend every free second with,
or something to get around?]) + C -->|One| D([Laptop]) + C -->|Two| E([iPhone]) + C -->|Three| F([Car
wroom wroom]) + click A "index.html#link-clicked" "link test" + click B testClick "click test" + classDef someclass fill:#f96; + class A someclass; + class C someclass; diff --git a/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/diamond-intersections.mmd b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/diamond-intersections.mmd new file mode 100644 index 00000000000..583c05843b4 --- /dev/null +++ b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/diamond-intersections.mmd @@ -0,0 +1,38 @@ +--- +config: + layout: elk + elk: + nodePlacementAlignment: NONE +--- +flowchart LR + subgraph s1["Untitled subgraph"] + n1["Evaluate"] + n2["Option 1"] + n3["Option 2"] + n4["fa:fa-car Option 3"] + end + subgraph s2["Untitled subgraph"] + n5["Evaluate"] + n6["Option 1"] + n7["Option 2"] + n8["fa:fa-car Option 3"] + end + A["Start"] -- Some text --> B("Continue") + B --> C{"Evaluate"} + C -- One --> D["Option 1"] + C -- Two --> E["Option 2"] + C -- Three --> F["fa:fa-car Option 3"] + n1 -- One --> n2 + n1 -- Two --> n3 + n1 -- Three --> n4 + n5 -- One --> n6 + n5 -- Two --> n7 + n5 -- Three --> n8 + n1@{ shape: diam} + n2@{ shape: rect} + n3@{ shape: rect} + n4@{ shape: rect} + n5@{ shape: diam} + n6@{ shape: rect} + n7@{ shape: rect} + n8@{ shape: rect} diff --git a/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/full-class-diagram.mmd b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/full-class-diagram.mmd new file mode 100644 index 00000000000..b2a8a7206ac --- /dev/null +++ b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/full-class-diagram.mmd @@ -0,0 +1,32 @@ +--- +config: + elk: + nodePlacementAlignment: BALANCED +--- +classDiagram + note "I love this diagram!\nDo you love it?" + Class01 <|-- AveryLongClass : Cool + <> Class01 + Class03 "1" *-- "*" Class04 + Class05 "1" o-- "many" Class06 + Class07 "1" .. "*" Class08 + Class09 "1" --> "*" C2 : Where am i? + Class09 "*" --* "*" C3 + Class09 "1" --|> "1" Class07 + Class12 <|.. Class08 + Class11 ..>Class12 + Class07 : equals() + Class07 : Object[] elementData + Class01 : size() + Class01 : int chimp + Class01 : int gorilla + Class01 : -int privateChimp + Class01 : +int publicGorilla + Class01 : #int protectedMarmoset + Class08 <--> C2: Cool label + class Class10 { + <> + int id + test() + } + note for Class10 "Cool class\nI said it's very cool class!" diff --git a/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/non-rect-intersections.mmd b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/non-rect-intersections.mmd index bef0db45d14..c2c1d40afb6 100644 --- a/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/non-rect-intersections.mmd +++ b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/non-rect-intersections.mmd @@ -1,4 +1,9 @@ -%% Validate ELK edge endpoints against non-rectangular node shapes. +--- +config: + elk: + nodePlacementAlignment: BALANCED +--- + flowchart TD Start([Start]) --> Split{Ready to Process?} Split -->|yes| T1[Task A] diff --git a/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/right-angles-not-curves.mmd b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/right-angles-not-curves.mmd new file mode 100644 index 00000000000..673a6d25e6d --- /dev/null +++ b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/right-angles-not-curves.mmd @@ -0,0 +1,23 @@ +--- +config: + elk: + nodePlacementAlignment: NONE +--- +flowchart LR + subgraph G1 + N00 + N11 + N12 + N13 + end + subgraph G2 + N21 + N22 + end + N00 --- N01 & N02 & N03 & N04 & N05 + N00 --- N11 & N12 & N13 & N22 + N11 --- N22 + N11 --- N22 + N11 --- N22 + N11 --- N22 + N11 --- N22 diff --git a/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/should-render-edge-labels-correctly-when-flowchart-htmlLabels-is-false.mmd b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/should-render-edge-labels-correctly-when-flowchart-htmlLabels-is-false.mmd new file mode 100644 index 00000000000..9c532988a7f --- /dev/null +++ b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/should-render-edge-labels-correctly-when-flowchart-htmlLabels-is-false.mmd @@ -0,0 +1,35 @@ +--- +config: + flowchart: + htmlLabels: false + fontFamily: 'courier' +--- +requirementDiagram + requirement test_req { + id: 1 + text: the test text. + risk: high + verifymethod: test + } + + functionalRequirement test_req2 { + id: 1.1 + text: the second test text. + risk: low + verifymethod: inspection + } + + element test_entity { + type: simulation + } + + element test_entity2 { + type: word doc + docRef: reqs/test_entity + } + + test_entity - satisfies -> test_req2 + test_req - traces -> test_req2 + test_req - contains -> test_req2 + test_entity2 - verifies -> test_req + test_req <- copies - test_entity2 diff --git a/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/styled-edge-labels.mmd b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/styled-edge-labels.mmd new file mode 100644 index 00000000000..2fa0f0ea889 --- /dev/null +++ b/cypress/platform/dev-diagrams/layout-tests/elk-edge-cases/styled-edge-labels.mmd @@ -0,0 +1,15 @@ +flowchart TD + A[Christmas] ==> D + A[Christmas] -->|Get money| B(Go shopping) + A[Christmas] ==> C + subgraph T ["Test"] + A + B + C + end + + classDef Test fill:#F84E68,stroke:#333,color:white; + class A,T Test + classDef TestSub fill:green; + class T TestSub + linkStyle 0,1 color:orange, stroke: orange; diff --git a/cypress/platform/viewer.js b/cypress/platform/viewer.js index c36d39b08d9..ef0e62d55f4 100644 --- a/cypress/platform/viewer.js +++ b/cypress/platform/viewer.js @@ -15,6 +15,13 @@ function markRendered() { } } +function usesLegacyFontAwesomeSyntax(code) { + const legacyFontAwesomePattern = /fa[bklrs]?:fa-[\w-]+|::icon\(\s*fa\b/; + return Array.isArray(code) + ? code.some((diagramCode) => usesLegacyFontAwesomeSyntax(diagramCode)) + : legacyFontAwesomePattern.test(code); +} + function loadFontAwesomeCSS() { const link = document.createElement('link'); link.rel = 'stylesheet'; @@ -34,14 +41,16 @@ function loadFontAwesomeCSS() { * page. */ const contentLoaded = async function () { - await loadFontAwesomeCSS(); - await Promise.all(Array.from(document.fonts, (font) => font.load())); - let pos = document.location.href.indexOf('?graph='); if (pos > 0) { pos = pos + 7; const graphBase64 = document.location.href.substr(pos); const graphObj = JSON.parse(b64ToUtf8(graphBase64)); + if (usesLegacyFontAwesomeSyntax(graphObj.code)) { + await loadFontAwesomeCSS(); + } + await Promise.all(Array.from(document.fonts, (font) => font.load())); + const darkThemes = ['dark', 'neo-dark', 'redux-dark', 'redux-dark-color']; if (graphObj.mermaid && darkThemes.includes(graphObj.mermaid.theme)) { document.body.style.background = '#3f3f3f'; diff --git a/docs/config/setup/defaultConfig/variables/configKeys.md b/docs/config/setup/defaultConfig/variables/configKeys.md index 281c7c7df97..3582384af6a 100644 --- a/docs/config/setup/defaultConfig/variables/configKeys.md +++ b/docs/config/setup/defaultConfig/variables/configKeys.md @@ -12,4 +12,4 @@ > `const` **configKeys**: `Set`<`string`> -Defined in: [packages/mermaid/src/defaultConfig.ts:336](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L336) +Defined in: [packages/mermaid/src/defaultConfig.ts:340](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L340) diff --git a/docs/config/setup/mermaid/functions/clearLayoutRenderState.md b/docs/config/setup/mermaid/functions/clearLayoutRenderState.md index ce3c3278b65..420fe112b03 100644 --- a/docs/config/setup/mermaid/functions/clearLayoutRenderState.md +++ b/docs/config/setup/mermaid/functions/clearLayoutRenderState.md @@ -12,7 +12,7 @@ > **clearLayoutRenderState**(): `void` -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:164](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L164) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:183](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L183) ## Returns diff --git a/docs/config/setup/mermaid/functions/createCommonLayoutRenderer.md b/docs/config/setup/mermaid/functions/createCommonLayoutRenderer.md index 78e3022cf00..da870c43e88 100644 --- a/docs/config/setup/mermaid/functions/createCommonLayoutRenderer.md +++ b/docs/config/setup/mermaid/functions/createCommonLayoutRenderer.md @@ -12,7 +12,7 @@ > **createCommonLayoutRenderer**<`CoreResult`, `PreparedLayout`, `MeasureResult`>(`__namedParameters`): (`data4Layout`, `svg`, `helpers?`, `options?`) => `Promise`<`void`> -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:87](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L87) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L106) ## Type Parameters diff --git a/docs/config/setup/mermaid/functions/defaultMeasureLayout.md b/docs/config/setup/mermaid/functions/defaultMeasureLayout.md index f592ecdd7f4..83a4031cf3f 100644 --- a/docs/config/setup/mermaid/functions/defaultMeasureLayout.md +++ b/docs/config/setup/mermaid/functions/defaultMeasureLayout.md @@ -12,7 +12,7 @@ > **defaultMeasureLayout**(`data4Layout`, `__namedParameters`): `Promise`<{ `graph`: `Graph`; `groups`: { `clusters`: `D3Selection`<`SVGGElement`>; `edgeLabels`: `D3Selection`<`SVGGElement`>; `edgePaths`: `D3Selection`<`SVGGElement`>; `nodes`: `D3Selection`<`SVGGElement`>; `rootGroups`: `D3Selection`<`SVGGElement`>; }; `nodeElements`: `Map`<`string`, `D3Selection`<`SVGElement` | `SVGGElement`>>; }> -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:171](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L171) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:190](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L190) ## Parameters diff --git a/docs/config/setup/mermaid/functions/paintLayoutData.md b/docs/config/setup/mermaid/functions/paintLayoutData.md index e701e30fa38..a5a528f2df2 100644 --- a/docs/config/setup/mermaid/functions/paintLayoutData.md +++ b/docs/config/setup/mermaid/functions/paintLayoutData.md @@ -10,9 +10,9 @@ # Function: paintLayoutData() -> **paintLayoutData**(`data4Layout`, `__namedParameters`, `options`): `Promise`<`void`> +> **paintLayoutData**(`data4Layout`, `context`, `options`): `Promise`<`void`> -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:178](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L178) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:197](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L197) ## Parameters @@ -20,7 +20,7 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. [`LayoutData`](../interfaces/LayoutData.md) -### \_\_namedParameters +### context [`CommonLayoutPaintContext`](../interfaces/CommonLayoutPaintContext.md)<`unknown`, { `graph`: `Graph`; `groups`: { `clusters`: `D3Selection`<`SVGGElement`>; `edgeLabels`: `D3Selection`<`SVGGElement`>; `edgePaths`: `D3Selection`<`SVGGElement`>; `nodes`: `D3Selection`<`SVGGElement`>; `rootGroups`: `D3Selection`<`SVGGElement`>; }; `nodeElements`: `Map`<`string`, `D3Selection`<`SVGElement` | `SVGGElement`>>; }> diff --git a/docs/config/setup/mermaid/interfaces/CommonLayoutPaintContext.md b/docs/config/setup/mermaid/interfaces/CommonLayoutPaintContext.md index 11d321050c4..bee69e5a24a 100644 --- a/docs/config/setup/mermaid/interfaces/CommonLayoutPaintContext.md +++ b/docs/config/setup/mermaid/interfaces/CommonLayoutPaintContext.md @@ -10,7 +10,7 @@ # Interface: CommonLayoutPaintContext\ -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:45](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L45) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:46](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L46) ## Extends @@ -32,7 +32,7 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. > **element**: `D3Selection`<`SVGElement`> -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:39](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L39) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:40](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L40) #### Inherited from @@ -44,7 +44,7 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. > `optional` **helpers**: `object` -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:40](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L40) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:41](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L41) #### common @@ -492,7 +492,7 @@ The curve factory to use > **measure**: `MeasureResult` -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:49](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L49) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:50](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L50) --- @@ -500,7 +500,7 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. > `optional` **options**: [`RenderOptions`](RenderOptions.md) -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:41](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L41) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:42](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L42) #### Inherited from @@ -512,7 +512,7 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. > `optional` **preparedLayout**: `PreparedLayout` -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:42](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L42) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:43](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L43) #### Inherited from diff --git a/docs/config/setup/mermaid/interfaces/CommonLayoutPaintOptions.md b/docs/config/setup/mermaid/interfaces/CommonLayoutPaintOptions.md index 120b6d68fb7..7976917bf56 100644 --- a/docs/config/setup/mermaid/interfaces/CommonLayoutPaintOptions.md +++ b/docs/config/setup/mermaid/interfaces/CommonLayoutPaintOptions.md @@ -10,15 +10,93 @@ # Interface: CommonLayoutPaintOptions -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:52](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L52) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:53](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L53) ## Properties +### clusterDb? + +> `optional` **clusterDb**: `ClusterDb` + +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:54](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L54) + +--- + +### getEdgeNode()? + +> `optional` **getEdgeNode**: (`id`, `edge`, `context`) => `object` | `Node` | `undefined` + +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:59](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L59) + +#### Parameters + +##### id + +`string` | `undefined` + +##### edge + +`Edge` + +##### context + +[`CommonLayoutPaintContext`](CommonLayoutPaintContext.md)<`unknown`, { `graph`: `Graph`; `groups`: { `clusters`: `D3Selection`<`SVGGElement`>; `edgeLabels`: `D3Selection`<`SVGGElement`>; `edgePaths`: `D3Selection`<`SVGGElement`>; `nodes`: `D3Selection`<`SVGGElement`>; `rootGroups`: `D3Selection`<`SVGGElement`>; }; `nodeElements`: `Map`<`string`, `D3Selection`<`SVGElement` | `SVGGElement`>>; }> + +#### Returns + +`object` | `Node` | `undefined` + +--- + +### getNodes()? + +> `optional` **getNodes**: (`data4Layout`, `context`) => `Iterable`<`Node`> + +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:55](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L55) + +#### Parameters + +##### data4Layout + +[`LayoutData`](LayoutData.md) + +##### context + +[`CommonLayoutPaintContext`](CommonLayoutPaintContext.md)<`unknown`, { `graph`: `Graph`; `groups`: { `clusters`: `D3Selection`<`SVGGElement`>; `edgeLabels`: `D3Selection`<`SVGGElement`>; `edgePaths`: `D3Selection`<`SVGGElement`>; `nodes`: `D3Selection`<`SVGGElement`>; `rootGroups`: `D3Selection`<`SVGGElement`>; }; `nodeElements`: `Map`<`string`, `D3Selection`<`SVGElement` | `SVGGElement`>>; }> + +#### Returns + +`Iterable`<`Node`> + +--- + +### isCluster()? + +> `optional` **isCluster**: (`node`, `context`) => `boolean` + +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:68](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L68) + +#### Parameters + +##### node + +`Node` + +##### context + +[`CommonLayoutPaintContext`](CommonLayoutPaintContext.md)<`unknown`, { `graph`: `Graph`; `groups`: { `clusters`: `D3Selection`<`SVGGElement`>; `edgeLabels`: `D3Selection`<`SVGGElement`>; `edgePaths`: `D3Selection`<`SVGGElement`>; `nodes`: `D3Selection`<`SVGGElement`>; `rootGroups`: `D3Selection`<`SVGGElement`>; }; `nodeElements`: `Map`<`string`, `D3Selection`<`SVGElement` | `SVGGElement`>>; }> + +#### Returns + +`boolean` + +--- + ### skipEdge()? > `optional` **skipEdge**: (`edge`) => `boolean` -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:53](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L53) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:72](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L72) #### Parameters @@ -36,4 +114,26 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. > `optional` **skipIntersect**: `boolean` | (`edge`) => `boolean` -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:54](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L54) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:73](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L73) + +--- + +### skipNode()? + +> `optional` **skipNode**: (`node`, `context`) => `boolean` + +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:64](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L64) + +#### Parameters + +##### node + +`Node` + +##### context + +[`CommonLayoutPaintContext`](CommonLayoutPaintContext.md)<`unknown`, { `graph`: `Graph`; `groups`: { `clusters`: `D3Selection`<`SVGGElement`>; `edgeLabels`: `D3Selection`<`SVGGElement`>; `edgePaths`: `D3Selection`<`SVGGElement`>; `nodes`: `D3Selection`<`SVGGElement`>; `rootGroups`: `D3Selection`<`SVGGElement`>; }; `nodeElements`: `Map`<`string`, `D3Selection`<`SVGElement` | `SVGGElement`>>; }> + +#### Returns + +`boolean` diff --git a/docs/config/setup/mermaid/interfaces/CommonLayoutRenderContext.md b/docs/config/setup/mermaid/interfaces/CommonLayoutRenderContext.md index 461901b7a73..3ef74298922 100644 --- a/docs/config/setup/mermaid/interfaces/CommonLayoutRenderContext.md +++ b/docs/config/setup/mermaid/interfaces/CommonLayoutRenderContext.md @@ -10,7 +10,7 @@ # Interface: CommonLayoutRenderContext\ -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:38](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L38) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:39](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L39) ## Extended by @@ -28,7 +28,7 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. > **element**: `D3Selection`<`SVGElement`> -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:39](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L39) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:40](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L40) --- @@ -36,7 +36,7 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. > `optional` **helpers**: `object` -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:40](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L40) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:41](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L41) #### common @@ -480,7 +480,7 @@ The curve factory to use > `optional` **options**: [`RenderOptions`](RenderOptions.md) -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:41](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L41) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:42](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L42) --- @@ -488,4 +488,4 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. > `optional` **preparedLayout**: `PreparedLayout` -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:42](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L42) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:43](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L43) diff --git a/docs/config/setup/mermaid/interfaces/CommonLayoutRendererDefinition.md b/docs/config/setup/mermaid/interfaces/CommonLayoutRendererDefinition.md index eacdb1f930a..451c95b4310 100644 --- a/docs/config/setup/mermaid/interfaces/CommonLayoutRendererDefinition.md +++ b/docs/config/setup/mermaid/interfaces/CommonLayoutRendererDefinition.md @@ -10,7 +10,7 @@ # Interface: CommonLayoutRendererDefinition\ -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:57](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L57) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:76](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L76) ## Type Parameters @@ -32,7 +32,7 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. > `optional` **afterPaint**: (`data4Layout`, `context`, `coreResult`) => `void` | `Promise`<`void`> -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:79](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L79) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L98) #### Parameters @@ -58,7 +58,7 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. > `optional` **measureLayout**: (`data4Layout`, `context`) => `Promise`<`MeasureResult`> -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:66](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L66) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:85](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L85) #### Parameters @@ -80,7 +80,7 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. > `optional` **paintLayout**: (`data4Layout`, `context`, `coreResult`) => `void` | `Promise`<`void`> -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:74](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L74) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:93](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L93) #### Parameters @@ -106,7 +106,7 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. > `optional` **paintOptions**: [`CommonLayoutPaintOptions`](CommonLayoutPaintOptions.md) -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L84) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:103](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L103) --- @@ -114,7 +114,7 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. > `optional` **prepareLayout**: (`data4Layout`, `context`) => `PreparedLayout` | `Promise`<`PreparedLayout`> -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:62](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L62) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:81](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L81) #### Parameters @@ -136,7 +136,7 @@ Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index. > **runLayoutCore**: (`data4Layout`, `context`) => `CoreResult` | `Promise`<`CoreResult`> -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:70](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L70) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L89) #### Parameters diff --git a/packages/mermaid/src/config.spec.ts b/packages/mermaid/src/config.spec.ts index 408cea9cc18..7a3b6bae186 100644 --- a/packages/mermaid/src/config.spec.ts +++ b/packages/mermaid/src/config.spec.ts @@ -63,6 +63,13 @@ describe('when working with site config', () => { expect(config.elk?.nodePlacementAlignment).toBe('NONE'); }); + it('should default class diagrams to dagre-wrapper without forcing class padding', () => { + const config = configApi.getConfig(); + + expect(config.class?.defaultRenderer).toBe('dagre-wrapper'); + expect(config.class?.padding).toBeUndefined(); + }); + it('should retain railroad directives after sanitization', () => { configApi.saveConfigFromInitialize({}); configApi.addDirective({ diff --git a/packages/mermaid/src/dagre-wrapper/GraphObjects.md b/packages/mermaid/src/dagre-wrapper/GraphObjects.md deleted file mode 100644 index b90ce69312e..00000000000 --- a/packages/mermaid/src/dagre-wrapper/GraphObjects.md +++ /dev/null @@ -1,140 +0,0 @@ -# Cluster handling - -Dagre does not support edges between nodes and clusters or between clusters to other clusters. In order to remedy this shortcoming the dagre wrapper implements a few workarounds. - -In the diagram below there are two clusters and there are no edges to nodes outside the own cluster. - -```mermaid -flowchart - subgraph C1 - a --> b - end - subgraph C2 - c - end - C1 --> C2 -``` - -In this case the dagre-wrapper will transform the graph to the graph below. - -```mermaid -flowchart - C1 --> C2 -``` - -The new nodes C1 and C2 are a special type of nodes, clusterNodes. ClusterNodes have the nodes in the cluster including the cluster attached in a graph object. - -When rendering this diagram it is being rendered recursively. The diagram is rendered by the dagre-mermaid:render function which in turn will be used to render the node C1 and the node C2. The result of those renderings will be inserted as nodes in the "root" diagram. With this recursive approach it would be possible to have different layout direction for each cluster. - -``` -{ clusterNode: true, graph } -``` - -_Data for a clusterNode_ - -When a cluster has edges to or from some of its nodes leading outside the cluster the approach of recursive rendering cannot be used as the layout of the graph needs to take responsibility for nodes outside of the cluster. - -```mermaid -flowchart - subgraph C1 - a - end - subgraph C2 - b - end - a --> C2 -``` - -To handle this case a special type of edge is inserted. The edge to/from the cluster is replaced with an edge to/from a node in the cluster which is tagged with toCluster/fromCluster. When rendering this edge the intersection between the edge and the border of the cluster is calculated making the edge start/stop there. In practice this renders like an edge to/from the cluster. - -In the diagram above the root diagram would be rendered with C1 whereas C2 would be rendered recursively. - -Of these two approaches the top one renders better and is used when possible. When this is not possible, ie an edge is added crossing the border the non recursive approach is used. - -# Graph objects and their properties - -Explains the representation of various objects used to render the flow charts and what the properties mean. This is from the perspective of the dagre-wrapper. - -## node - -Sample object: - -```json -{ - "shape": "rect", - "labelText": "Test", - "rx": 0, - "ry": 0, - "class": "default", - "style": "", - "id": "Test", - "type": "group", - "padding": 15 -} -``` - -This is set by the renderer of the diagram and insert the data that the wrapper needs for rendering. - -| property | description | -| ---------- | ------------------------------------------------------------------------------------------------ | -| labelStyle | Css styles for the label. User for instance for styling the labels for clusters | -| shape | The shape of the node. | -| labelText | The text on the label | -| rx | The corner radius - maybe part of the shape instead? Used for rects. | -| ry | The corner radius - maybe part of the shape instead? Used for rects. | -| classes | Classes to be set for the shape. Not used | -| style | Css styles for the actual shape | -| id | id of the shape | -| type | if set to group then this node indicates _a cluster_. | -| padding | Padding. Passed from the render as this might differ between different diagrams. Maybe obsolete. | -| data | Non-generic data specific to the shape. | - -# edge - -arrowType sets the type of arrows to use. The following arrow types are currently supported: - -arrow_cross -double_arrow_cross -arrow_point -double_arrow_point -arrow_circle -double_arrow_circle - -Lets try to make these types semantic free so that diagram type semantics does not find its way into this more generic layer. - -Required edgeData for proper rendering: - -| property | description | -| ---------- | -------------------------------------------------------------------- | -| id | ID of the edge | -| arrowHead | overlap between arrowHead and arrowType? | -| arrowType | overlap between arrowHead and arrowType? | -| style | | -| labelStyle | | -| label | overlap between label and labelText? | -| labelPos | | -| labelType | overlap between label and labelText? | -| thickness | Sets the thickness of the edge. Can be \['normal', 'thick'\] | -| pattern | Sets the pattern of the edge. Can be \['solid', 'dotted', 'dashed'\] | - -# Markers - -Define what markers that should be included in the diagram with the insert markers function. The function takes two arguments, first the element in which the markers should be included and a list of the markers that should be added. - -Ex: -insertMarkers(el, \['point', 'circle'\]) - -The example above adds the markers point and cross. This means that edges with the arrowTypes arrow_cross, double_arrow_cross, arrow_point and double_arrow_cross will get the corresponding markers but arrowType arrow_cross will have no impact. - -Current markers: - -- point - the standard arrow from flowcharts -- circle - Arrows ending with circle -- cross - arrows starting and ending with a cross - -// Todo - in case of common renderer - -# Common functions used by the renderer to be implemented by the Db - -getDirection -getClasses diff --git a/packages/mermaid/src/dagre-wrapper/blockArrowHelper.ts b/packages/mermaid/src/dagre-wrapper/blockArrowHelper.ts deleted file mode 100644 index 57a28c23785..00000000000 --- a/packages/mermaid/src/dagre-wrapper/blockArrowHelper.ts +++ /dev/null @@ -1,247 +0,0 @@ -import type { Direction } from '../../src/diagrams/block/blockTypes.js'; - -const expandAndDeduplicateDirections = (directions: Direction[]) => { - const uniqueDirections = new Set(); - - for (const direction of directions) { - switch (direction) { - case 'x': - uniqueDirections.add('right'); - uniqueDirections.add('left'); - break; - case 'y': - uniqueDirections.add('up'); - uniqueDirections.add('down'); - break; - default: - uniqueDirections.add(direction); - break; - } - } - - return uniqueDirections; -}; -export const getArrowPoints = ( - duplicatedDirections: Direction[], - bbox: { width: number; height: number }, - node: any, - totalWidth?: number -) => { - // Expand and deduplicate the provided directions. - // for instance: x, right => right, left - const directions = expandAndDeduplicateDirections(duplicatedDirections); - - // Factor to divide height for some calculations. - const f = 2; - - // Calculated height of the bounding box, accounting for node padding. - const height = bbox.height + 2 * node.padding; - // Midpoint calculation based on height. - const midpoint = height / f; - // Calculated width of the bounding box, accounting for additional width and node padding. - // When totalWidth is provided - const width = totalWidth ?? bbox.width + 2 * midpoint + node.padding; - // Padding to use, half of the node padding. - const padding = node.padding / 2; - - if ( - directions.has('right') && - directions.has('left') && - directions.has('up') && - directions.has('down') - ) { - // SQUARE - return [ - // Bottom - { x: 0, y: 0 }, - { x: midpoint, y: 0 }, - { x: width / 2, y: 2 * padding }, - { x: width - midpoint, y: 0 }, - { x: width, y: 0 }, - - // Right - { x: width, y: -height / 3 }, - { x: width + 2 * padding, y: -height / 2 }, - { x: width, y: (-2 * height) / 3 }, - { x: width, y: -height }, - - // Top - { x: width - midpoint, y: -height }, - { x: width / 2, y: -height - 2 * padding }, - { x: midpoint, y: -height }, - - // Left - { x: 0, y: -height }, - { x: 0, y: (-2 * height) / 3 }, - { x: -2 * padding, y: -height / 2 }, - { x: 0, y: -height / 3 }, - ]; - } - if (directions.has('right') && directions.has('left') && directions.has('up')) { - // RECTANGLE_VERTICAL (Top Open) - return [ - { x: midpoint, y: 0 }, - { x: width - midpoint, y: 0 }, - { x: width, y: -height / 2 }, - { x: width - midpoint, y: -height }, - { x: midpoint, y: -height }, - { x: 0, y: -height / 2 }, - ]; - } - if (directions.has('right') && directions.has('left') && directions.has('down')) { - // RECTANGLE_VERTICAL (Bottom Open) - return [ - { x: 0, y: 0 }, - { x: midpoint, y: -height }, - { x: width - midpoint, y: -height }, - { x: width, y: 0 }, - ]; - } - if (directions.has('right') && directions.has('up') && directions.has('down')) { - // RECTANGLE_HORIZONTAL (Right Open) - return [ - { x: 0, y: 0 }, - { x: width, y: -midpoint }, - { x: width, y: -height + midpoint }, - { x: 0, y: -height }, - ]; - } - if (directions.has('left') && directions.has('up') && directions.has('down')) { - // RECTANGLE_HORIZONTAL (Left Open) - return [ - { x: width, y: 0 }, - { x: 0, y: -midpoint }, - { x: 0, y: -height + midpoint }, - { x: width, y: -height }, - ]; - } - if (directions.has('right') && directions.has('left')) { - // HORIZONTAL_LINE - return [ - { x: midpoint, y: 0 }, - { x: midpoint, y: -padding }, - { x: width - midpoint, y: -padding }, - { x: width - midpoint, y: 0 }, - { x: width, y: -height / 2 }, - { x: width - midpoint, y: -height }, - { x: width - midpoint, y: -height + padding }, - { x: midpoint, y: -height + padding }, - { x: midpoint, y: -height }, - { x: 0, y: -height / 2 }, - ]; - } - if (directions.has('up') && directions.has('down')) { - // VERTICAL_LINE - return [ - // Bottom center - { x: width / 2, y: 0 }, - // Left point of bottom arrow - { x: 0, y: -padding }, - { x: midpoint, y: -padding }, - // Left top over vertical section - { x: midpoint, y: -height + padding }, - { x: 0, y: -height + padding }, - // Top of arrow - { x: width / 2, y: -height }, - { x: width, y: -height + padding }, - // Top of right vertical bar - { x: width - midpoint, y: -height + padding }, - { x: width - midpoint, y: -padding }, - { x: width, y: -padding }, - ]; - } - if (directions.has('right') && directions.has('up')) { - // ANGLE_RT - return [ - { x: 0, y: 0 }, - { x: width, y: -midpoint }, - { x: 0, y: -height }, - ]; - } - if (directions.has('right') && directions.has('down')) { - // ANGLE_RB - return [ - { x: 0, y: 0 }, - { x: width, y: 0 }, - { x: 0, y: -height }, - ]; - } - if (directions.has('left') && directions.has('up')) { - // ANGLE_LT - return [ - { x: width, y: 0 }, - { x: 0, y: -midpoint }, - { x: width, y: -height }, - ]; - } - if (directions.has('left') && directions.has('down')) { - // ANGLE_LB - return [ - { x: width, y: 0 }, - { x: 0, y: 0 }, - { x: width, y: -height }, - ]; - } - if (directions.has('right')) { - // ARROW_RIGHT - return [ - { x: midpoint, y: -padding }, - { x: midpoint, y: -padding }, - { x: width - midpoint, y: -padding }, - { x: width - midpoint, y: 0 }, - { x: width, y: -height / 2 }, - { x: width - midpoint, y: -height }, - { x: width - midpoint, y: -height + padding }, - // top left corner of arrow - { x: midpoint, y: -height + padding }, - { x: midpoint, y: -height + padding }, - ]; - } - if (directions.has('left')) { - // ARROW_LEFT - return [ - { x: midpoint, y: 0 }, - { x: midpoint, y: -padding }, - // Two points, the right corners - { x: width - midpoint, y: -padding }, - { x: width - midpoint, y: -height + padding }, - { x: midpoint, y: -height + padding }, - { x: midpoint, y: -height }, - { x: 0, y: -height / 2 }, - ]; - } - if (directions.has('up')) { - // ARROW_TOP - return [ - // Bottom center - { x: midpoint, y: -padding }, - // Left top over vertical section - { x: midpoint, y: -height + padding }, - { x: 0, y: -height + padding }, - // Top of arrow - { x: width / 2, y: -height }, - { x: width, y: -height + padding }, - // Top of right vertical bar - { x: width - midpoint, y: -height + padding }, - { x: width - midpoint, y: -padding }, - ]; - } - if (directions.has('down')) { - // ARROW_BOTTOM - return [ - // Bottom center - { x: width / 2, y: 0 }, - // Left point of bottom arrow - { x: 0, y: -padding }, - { x: midpoint, y: -padding }, - // Left top over vertical section - { x: midpoint, y: -height + padding }, - { x: width - midpoint, y: -height + padding }, - { x: width - midpoint, y: -padding }, - { x: width, y: -padding }, - ]; - } - - // POINT - return [{ x: 0, y: 0 }]; -}; diff --git a/packages/mermaid/src/dagre-wrapper/clusters.js b/packages/mermaid/src/dagre-wrapper/clusters.js deleted file mode 100644 index c7313e121c4..00000000000 --- a/packages/mermaid/src/dagre-wrapper/clusters.js +++ /dev/null @@ -1,256 +0,0 @@ -import intersectRect from './intersect/intersect-rect.js'; -import { log } from '../logger.js'; -import createLabel from './createLabel.js'; -import { createText } from '../rendering-util/createText.js'; -import { select } from 'd3'; -import { getConfig } from '../diagram-api/diagramAPI.js'; -import { getEffectiveHtmlLabels } from '../config.js'; -import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js'; - -const rect = async (parent, node) => { - log.info('Creating subgraph rect for ', node.id, node); - const siteConfig = getConfig(); - - // Add outer g element - const shapeSvg = parent - .insert('g') - .attr('class', 'cluster' + (node.class ? ' ' + node.class : '')) - .attr('id', node.id); - - // add the rect - const rect = shapeSvg.insert('rect', ':first-child'); - - const useHtmlLabels = getEffectiveHtmlLabels(siteConfig); - - // Create the label and insert it after the rect - const label = shapeSvg.insert('g').attr('class', 'cluster-label'); - - // TODO: the createText function returns a `Promise`, so I'm guessing it never runs? - const text = - node.labelType === 'markdown' - ? createText(label, node.labelText, { style: node.labelStyle, useHtmlLabels }, siteConfig) - : await createLabel(label, node.labelText, node.labelStyle, undefined, true); - - // Get the size of the label - let bbox = text.getBBox(); - - if (getEffectiveHtmlLabels(siteConfig)) { - const div = text.children[0]; - const dv = select(text); - bbox = div.getBoundingClientRect(); - dv.attr('width', bbox.width); - dv.attr('height', bbox.height); - } - - const padding = 0 * node.padding; - const halfPadding = padding / 2; - - const width = node.width <= bbox.width + padding ? bbox.width + padding : node.width; - if (node.width <= bbox.width + padding) { - node.diff = (bbox.width - node.width) / 2 - node.padding / 2; - } else { - node.diff = -node.padding / 2; - } - - log.trace('Data ', node, JSON.stringify(node)); - // center the rect around its coordinate - rect - .attr('style', node.style) - .attr('rx', node.rx) - .attr('ry', node.ry) - .attr('x', node.x - width / 2) - .attr('y', node.y - node.height / 2 - halfPadding) - .attr('width', width) - .attr('height', node.height + padding); - - const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig); - if (useHtmlLabels) { - label.attr( - 'transform', - // This puts the label on top of the box instead of inside it - `translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})` - ); - } else { - label.attr( - 'transform', - // This puts the label on top of the box instead of inside it - `translate(${node.x}, ${node.y - node.height / 2 + subGraphTitleTopMargin})` - ); - } - // Center the label - - const rectBox = rect.node().getBBox(); - node.width = rectBox.width; - node.height = rectBox.height; - - node.intersect = function (point) { - return intersectRect(node, point); - }; - - return shapeSvg; -}; - -/** - * Non visible cluster where the note is group with its - * - * @param {any} parent - * @param {any} node - * @returns {any} ShapeSvg - */ -const noteGroup = (parent, node) => { - // Add outer g element - const shapeSvg = parent.insert('g').attr('class', 'note-cluster').attr('id', node.id); - - // add the rect - const rect = shapeSvg.insert('rect', ':first-child'); - - const padding = 0 * node.padding; - const halfPadding = padding / 2; - - // center the rect around its coordinate - rect - .attr('rx', node.rx) - .attr('ry', node.ry) - .attr('x', node.x - node.width / 2 - halfPadding) - .attr('y', node.y - node.height / 2 - halfPadding) - .attr('width', node.width + padding) - .attr('height', node.height + padding) - .attr('fill', 'none'); - - const rectBox = rect.node().getBBox(); - node.width = rectBox.width; - node.height = rectBox.height; - - node.intersect = function (point) { - return intersectRect(node, point); - }; - - return shapeSvg; -}; -const roundedWithTitle = async (parent, node) => { - const siteConfig = getConfig(); - - // Add outer g element - const shapeSvg = parent.insert('g').attr('class', node.classes).attr('id', node.id); - - // add the rect - const rect = shapeSvg.insert('rect', ':first-child'); - - // Create the label and insert it after the rect - const label = shapeSvg.insert('g').attr('class', 'cluster-label'); - const innerRect = shapeSvg.append('rect'); - - const text = await createLabel(label, node.labelText, node.labelStyle, undefined, true); - - // Get the size of the label - let bbox = text.getBBox(); - if (getEffectiveHtmlLabels(siteConfig)) { - const div = text.children[0]; - const dv = select(text); - bbox = div.getBoundingClientRect(); - dv.attr('width', bbox.width); - dv.attr('height', bbox.height); - } - bbox = text.getBBox(); - const padding = 0 * node.padding; - const halfPadding = padding / 2; - - const width = node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width; - if (node.width <= bbox.width + node.padding) { - node.diff = (bbox.width + node.padding * 0 - node.width) / 2; - } else { - node.diff = -node.padding / 2; - } - - // center the rect around its coordinate - rect - .attr('class', 'outer') - .attr('x', node.x - width / 2 - halfPadding) - .attr('y', node.y - node.height / 2 - halfPadding) - .attr('width', width + padding) - .attr('height', node.height + padding); - innerRect - .attr('class', 'inner') - .attr('x', node.x - width / 2 - halfPadding) - .attr('y', node.y - node.height / 2 - halfPadding + bbox.height - 1) - .attr('width', width + padding) - .attr('height', node.height + padding - bbox.height - 3); - - const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig); - // Center the label - label.attr( - 'transform', - `translate(${node.x - bbox.width / 2}, ${ - node.y - - node.height / 2 - - node.padding / 3 + - (getEffectiveHtmlLabels(siteConfig) ? 5 : 3) + - subGraphTitleTopMargin - })` - ); - - const rectBox = rect.node().getBBox(); - node.height = rectBox.height; - - node.intersect = function (point) { - return intersectRect(node, point); - }; - - return shapeSvg; -}; - -const divider = (parent, node) => { - // Add outer g element - const shapeSvg = parent.insert('g').attr('class', node.classes).attr('id', node.id); - - // add the rect - const rect = shapeSvg.insert('rect', ':first-child'); - - const padding = 0 * node.padding; - const halfPadding = padding / 2; - - // center the rect around its coordinate - rect - .attr('class', 'divider') - .attr('x', node.x - node.width / 2 - halfPadding) - .attr('y', node.y - node.height / 2) - .attr('width', node.width + padding) - .attr('height', node.height + padding); - - const rectBox = rect.node().getBBox(); - node.width = rectBox.width; - node.height = rectBox.height; - node.diff = -node.padding / 2; - node.intersect = function (point) { - return intersectRect(node, point); - }; - - return shapeSvg; -}; - -const shapes = { rect, roundedWithTitle, noteGroup, divider }; - -let clusterElems = {}; - -export const insertCluster = async (elem, node) => { - log.trace('Inserting cluster'); - const shape = node.shape || 'rect'; - clusterElems[node.id] = await shapes[shape](elem, node); -}; -export const getClusterTitleWidth = async (elem, node) => { - const label = await createLabel(elem, node.labelText, node.labelStyle, undefined, true); - const width = label.getBBox().width; - elem.node().removeChild(label); - return width; -}; - -export const clear = () => { - clusterElems = {}; -}; - -export const positionCluster = (node) => { - log.info('Position cluster (' + node.id + ', ' + node.x + ', ' + node.y + ')'); - const el = clusterElems[node.id]; - - el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')'); -}; diff --git a/packages/mermaid/src/dagre-wrapper/createLabel.js b/packages/mermaid/src/dagre-wrapper/createLabel.js deleted file mode 100644 index 24f726aceee..00000000000 --- a/packages/mermaid/src/dagre-wrapper/createLabel.js +++ /dev/null @@ -1,55 +0,0 @@ -import { getEffectiveHtmlLabels } from '../config.js'; -import { getConfig } from '../diagram-api/diagramAPI.js'; -import { createText } from '../rendering-util/createText.js'; - -/** - * @param {import('../types.js').D3Selection} element - The parent element to which the label will be appended. - * @param {string | [string] | undefined} _vertexText - The text content of the label. - * @param {string} style - * @param {boolean} [isTitle] - If `true`, style this as a title label, else as a normal label. - * @param {boolean} [isNode] - If `true`, style this as a node label, else as an edge label. - * @deprecated svg-util/createText instead - * - * @example - * - * If `getEffectiveHtmlLabels(getConfig())` is `true`, you must reset the width - * and height of the created label after creation, like this: - * - * ```js - * const labelElement = await createLabel(parent, ... ); - * let slBox = labelElement.getBBox(); - * if (useHtmlLabels) { - * const div = labelElement.children[0]; - * const dv = select(labelElement); - * slBox = div.getBoundingClientRect(); - * dv.attr('width', slBox.width); - * dv.attr('height', slBox.height); - * } - * parent.attr('transform', 'translate(' + -slBox.width / 2 + ', ' + -slBox.height / 2 + ')'); - * ``` - */ -const createLabel = async (element, _vertexText, style, isTitle = false, isNode = false) => { - let vertexText = _vertexText || ''; - if (typeof vertexText === 'object') { - vertexText = vertexText[0]; - } - - const config = getConfig(); - const useHtmlLabels = getEffectiveHtmlLabels(config); - - return await createText( - element, - vertexText, - { - style, - isTitle, - useHtmlLabels, - markdown: false, - isNode, - width: Number.POSITIVE_INFINITY, - }, - config - ); -}; - -export default createLabel; diff --git a/packages/mermaid/src/dagre-wrapper/edgeMarker.spec.ts b/packages/mermaid/src/dagre-wrapper/edgeMarker.spec.ts deleted file mode 100644 index 6fb43955269..00000000000 --- a/packages/mermaid/src/dagre-wrapper/edgeMarker.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* eslint-disable @typescript-eslint/unbound-method */ -import type { Mocked } from 'vitest'; -import type { SVG } from '../diagram-api/types.js'; -import { addEdgeMarkers } from './edgeMarker.js'; - -describe('addEdgeMarker', () => { - const svgPath = { - attr: vitest.fn(), - } as unknown as Mocked; - const url = 'http://example.com'; - const id = 'test'; - const diagramType = 'test'; - - beforeEach(() => { - svgPath.attr.mockReset(); - }); - - it('should add markers for arrow_cross:arrow_point', () => { - const arrowTypeStart = 'arrow_cross'; - const arrowTypeEnd = 'arrow_point'; - addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType); - expect(svgPath.attr).toHaveBeenCalledWith( - 'marker-start', - `url(${url}#${id}_${diagramType}-crossStart)` - ); - expect(svgPath.attr).toHaveBeenCalledWith( - 'marker-end', - `url(${url}#${id}_${diagramType}-pointEnd)` - ); - }); - - it('should add markers for aggregation:arrow_point', () => { - const arrowTypeStart = 'aggregation'; - const arrowTypeEnd = 'arrow_point'; - addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType); - expect(svgPath.attr).toHaveBeenCalledWith( - 'marker-start', - `url(${url}#${id}_${diagramType}-aggregationStart)` - ); - expect(svgPath.attr).toHaveBeenCalledWith( - 'marker-end', - `url(${url}#${id}_${diagramType}-pointEnd)` - ); - }); - - it('should add markers for arrow_point:aggregation', () => { - const arrowTypeStart = 'arrow_point'; - const arrowTypeEnd = 'aggregation'; - addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType); - expect(svgPath.attr).toHaveBeenCalledWith( - 'marker-start', - `url(${url}#${id}_${diagramType}-pointStart)` - ); - expect(svgPath.attr).toHaveBeenCalledWith( - 'marker-end', - `url(${url}#${id}_${diagramType}-aggregationEnd)` - ); - }); - - it('should add markers for aggregation:composition', () => { - const arrowTypeStart = 'aggregation'; - const arrowTypeEnd = 'composition'; - addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType); - expect(svgPath.attr).toHaveBeenCalledWith( - 'marker-start', - `url(${url}#${id}_${diagramType}-aggregationStart)` - ); - expect(svgPath.attr).toHaveBeenCalledWith( - 'marker-end', - `url(${url}#${id}_${diagramType}-compositionEnd)` - ); - }); - - it('should not add invalid markers', () => { - const arrowTypeStart = 'this is an invalid marker'; - const arrowTypeEnd = ') url(https://my-malicious-site.example)'; - addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType); - expect(svgPath.attr).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/mermaid/src/dagre-wrapper/edgeMarker.ts b/packages/mermaid/src/dagre-wrapper/edgeMarker.ts deleted file mode 100644 index 778a7708d8b..00000000000 --- a/packages/mermaid/src/dagre-wrapper/edgeMarker.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { SVG } from '../diagram-api/types.js'; -import { log } from '../logger.js'; -import type { EdgeData } from '../types.js'; -/** - * Adds SVG markers to a path element based on the arrow types specified in the edge. - * - * @param svgPath - The SVG path element to add markers to. - * @param edge - The edge data object containing the arrow types. - * @param url - The URL of the SVG marker definitions. - * @param id - The ID prefix for the SVG marker definitions. - * @param diagramType - The type of diagram being rendered. - */ -export const addEdgeMarkers = ( - svgPath: SVG, - edge: Pick, - url: string, - id: string, - diagramType: string -) => { - if (edge.arrowTypeStart) { - addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType); - } - if (edge.arrowTypeEnd) { - addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType); - } -}; - -const arrowTypesMap = { - arrow_cross: 'cross', - arrow_point: 'point', - arrow_barb: 'barb', - arrow_circle: 'circle', - aggregation: 'aggregation', - extension: 'extension', - composition: 'composition', - dependency: 'dependency', - lollipop: 'lollipop', -} as const; - -const addEdgeMarker = ( - svgPath: SVG, - position: 'start' | 'end', - arrowType: string, - url: string, - id: string, - diagramType: string -) => { - const endMarkerType = arrowTypesMap[arrowType as keyof typeof arrowTypesMap]; - - if (!endMarkerType) { - log.warn(`Unknown arrow type: ${arrowType}`); - return; // unknown arrow type, ignore - } - - const suffix = position === 'start' ? 'Start' : 'End'; - svgPath.attr(`marker-${position}`, `url(${url}#${id}_${diagramType}-${endMarkerType}${suffix})`); -}; diff --git a/packages/mermaid/src/dagre-wrapper/edges.js b/packages/mermaid/src/dagre-wrapper/edges.js deleted file mode 100644 index 7c6de6ce43e..00000000000 --- a/packages/mermaid/src/dagre-wrapper/edges.js +++ /dev/null @@ -1,494 +0,0 @@ -import { log } from '../logger.js'; -import createLabel from './createLabel.js'; -import { createText } from '../rendering-util/createText.js'; -import { computeLabelTransform } from '../rendering-util/labelTransform.js'; -import { line, curveBasis, select } from 'd3'; -import { getConfig } from '../diagram-api/diagramAPI.js'; -import { getEffectiveHtmlLabels } from '../config.js'; -import utils from '../utils.js'; -import { getUrl } from '../diagrams/common/common.js'; -import { getLineFunctionsWithOffset } from '../utils/lineWithOffset.js'; -import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js'; -import { addEdgeMarkers } from './edgeMarker.js'; - -let edgeLabels = {}; -let terminalLabels = {}; - -export const clear = () => { - edgeLabels = {}; - terminalLabels = {}; -}; - -export const insertEdgeLabel = async (elem, edge) => { - const config = getConfig(); - const useHtmlLabels = getEffectiveHtmlLabels(config); - - // Create outer g, edgeLabel, this will be positioned after graph layout - const edgeLabel = elem.insert('g').attr('class', 'edgeLabel'); - - // Create inner g, label, this will be positioned now for centering the text - const label = edgeLabel.insert('g').attr('class', 'label'); - - // Create the actual text element - const isMarkdown = edge.labelType === 'markdown'; - const labelElement = await createText( - elem, - edge.label, - { - style: edge.labelStyle, - useHtmlLabels, - // TODO: The old code only set addSvgBackground when using markdown, but - // this function is only used by block diagrams which never use markdown. - addSvgBackground: isMarkdown, - isNode: false, - markdown: isMarkdown, - // If using markdown, wrap using default width - width: isMarkdown ? undefined : Number.POSITIVE_INFINITY, - }, - config - ); - - label.node().appendChild(labelElement); - - let bbox = labelElement.getBBox(); - let transformBbox = bbox; - if (useHtmlLabels) { - const div = labelElement.children[0]; - const dv = select(labelElement); - bbox = div.getBoundingClientRect(); - transformBbox = bbox; - dv.attr('width', bbox.width); - dv.attr('height', bbox.height); - } else { - const textEl = select(labelElement).select('text').node(); - if (textEl && typeof textEl.getBBox === 'function') { - transformBbox = textEl.getBBox(); - } - } - label.attr('transform', computeLabelTransform(transformBbox, useHtmlLabels)); - - // Make element accessible by id for positioning - edgeLabels[edge.id] = edgeLabel; - - // Update the abstract data of the edge with the new information about its width and height - edge.width = bbox.width; - edge.height = bbox.height; - - let fo; - if (edge.startLabelLeft) { - // Create the actual text element - const startEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals'); - const inner = startEdgeLabelLeft.insert('g').attr('class', 'inner'); - const startLabelElement = await createLabel(inner, edge.startLabelLeft, edge.labelStyle); - fo = startLabelElement; - let slBox = startLabelElement.getBBox(); - if (useHtmlLabels) { - const div = startLabelElement.children[0]; - const dv = select(startLabelElement); - slBox = div.getBoundingClientRect(); - dv.attr('width', slBox.width); - dv.attr('height', slBox.height); - } - inner.attr('transform', computeLabelTransform(slBox, useHtmlLabels)); - if (!terminalLabels[edge.id]) { - terminalLabels[edge.id] = {}; - } - terminalLabels[edge.id].startLeft = startEdgeLabelLeft; - setTerminalWidth(fo, edge.startLabelLeft); - } - if (edge.startLabelRight) { - const startEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals'); - const inner = startEdgeLabelRight.insert('g').attr('class', 'inner'); - const startLabelElement = await createLabel(inner, edge.startLabelRight, edge.labelStyle); - fo = startLabelElement; - let slBox = startLabelElement.getBBox(); - if (useHtmlLabels) { - const div = startLabelElement.children[0]; - const dv = select(startLabelElement); - slBox = div.getBoundingClientRect(); - dv.attr('width', slBox.width); - dv.attr('height', slBox.height); - } - inner.attr('transform', computeLabelTransform(slBox, useHtmlLabels)); - - if (!terminalLabels[edge.id]) { - terminalLabels[edge.id] = {}; - } - terminalLabels[edge.id].startRight = startEdgeLabelRight; - setTerminalWidth(fo, edge.startLabelRight); - } - if (edge.endLabelLeft) { - const endEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals'); - // TODO: Remove? `inner` is not used - const inner = endEdgeLabelLeft.insert('g').attr('class', 'inner'); - const endLabelElement = await createLabel(endEdgeLabelLeft, edge.endLabelLeft, edge.labelStyle); - fo = endLabelElement; - let slBox = endLabelElement.getBBox(); - if (useHtmlLabels) { - const div = endLabelElement.children[0]; - const dv = select(endLabelElement); - slBox = div.getBoundingClientRect(); - dv.attr('width', slBox.width); - dv.attr('height', slBox.height); - } - inner.attr('transform', computeLabelTransform(slBox, useHtmlLabels)); - - if (!terminalLabels[edge.id]) { - terminalLabels[edge.id] = {}; - } - terminalLabels[edge.id].endLeft = endEdgeLabelLeft; - setTerminalWidth(fo, edge.endLabelLeft); - } - if (edge.endLabelRight) { - const endEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals'); - // TODO: Remove? `inner` is not used - const inner = endEdgeLabelRight.insert('g').attr('class', 'inner'); - const endLabelElement = await createLabel( - endEdgeLabelRight, - edge.endLabelRight, - edge.labelStyle - ); - fo = endLabelElement; - let slBox = endLabelElement.getBBox(); - if (useHtmlLabels) { - const div = endLabelElement.children[0]; - const dv = select(endLabelElement); - slBox = div.getBoundingClientRect(); - dv.attr('width', slBox.width); - dv.attr('height', slBox.height); - } - inner.attr('transform', computeLabelTransform(slBox, useHtmlLabels)); - - if (!terminalLabels[edge.id]) { - terminalLabels[edge.id] = {}; - } - terminalLabels[edge.id].endRight = endEdgeLabelRight; - setTerminalWidth(fo, edge.endLabelRight); - } - return labelElement; -}; - -/** - * @param {any} fo - * @param {any} value - */ -function setTerminalWidth(fo, value) { - if (getEffectiveHtmlLabels(getConfig()) && fo) { - fo.style.width = value.length * 9 + 'px'; - fo.style.height = '12px'; - } -} - -export const positionEdgeLabel = (edge, paths) => { - log.debug('Moving label abc88 ', edge.id, edge.label, edgeLabels[edge.id], paths); - let path = paths.updatedPath ? paths.updatedPath : paths.originalPath; - const siteConfig = getConfig(); - const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig); - if (edge.label) { - const el = edgeLabels[edge.id]; - let x = edge.x; - let y = edge.y; - if (path) { - // // debugger; - const pos = utils.calcLabelPosition(path); - log.debug( - 'Moving label ' + edge.label + ' from (', - x, - ',', - y, - ') to (', - pos.x, - ',', - pos.y, - ') abc88' - ); - if (paths.updatedPath) { - x = pos.x; - y = pos.y; - } - } - el.attr('transform', `translate(${x}, ${y + subGraphTitleTotalMargin / 2})`); - } - - //let path = paths.updatedPath ? paths.updatedPath : paths.originalPath; - if (edge.startLabelLeft) { - const el = terminalLabels[edge.id].startLeft; - let x = edge.x; - let y = edge.y; - if (path) { - // debugger; - const pos = utils.calcTerminalLabelPosition(edge.arrowTypeStart ? 10 : 0, 'start_left', path); - x = pos.x; - y = pos.y; - } - el.attr('transform', `translate(${x}, ${y})`); - } - if (edge.startLabelRight) { - const el = terminalLabels[edge.id].startRight; - let x = edge.x; - let y = edge.y; - if (path) { - // debugger; - const pos = utils.calcTerminalLabelPosition( - edge.arrowTypeStart ? 10 : 0, - 'start_right', - path - ); - x = pos.x; - y = pos.y; - } - el.attr('transform', `translate(${x}, ${y})`); - } - if (edge.endLabelLeft) { - const el = terminalLabels[edge.id].endLeft; - let x = edge.x; - let y = edge.y; - if (path) { - // debugger; - const pos = utils.calcTerminalLabelPosition(edge.arrowTypeEnd ? 10 : 0, 'end_left', path); - x = pos.x; - y = pos.y; - } - el.attr('transform', `translate(${x}, ${y})`); - } - if (edge.endLabelRight) { - const el = terminalLabels[edge.id].endRight; - let x = edge.x; - let y = edge.y; - if (path) { - // debugger; - const pos = utils.calcTerminalLabelPosition(edge.arrowTypeEnd ? 10 : 0, 'end_right', path); - x = pos.x; - y = pos.y; - } - el.attr('transform', `translate(${x}, ${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; -}; - -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. - let 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 on sides of rect - if (insidePoint.x < outsidePoint.x) { - r = outsidePoint.x - w - x; - } else { - // r = outsidePoint.x - w - x; - r = x - w - outsidePoint.x; - } - let 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 }; - } -}; -/** - * 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. - * - * @param {Array} _points - * @param {any} boundaryNode - * @returns {Array} Points - */ -const cutPathAtIntersect = (_points, boundaryNode) => { - log.debug('abc88 cutPathAtIntersect', _points, boundaryNode); - let points = []; - let lastPointOutside = _points[0]; - let isInside = false; - _points.forEach((point) => { - // check if point is inside the boundary rect - if (!outsideNode(boundaryNode, point) && !isInside) { - // First point inside the rect found - // Calc the intersection coord between the point and the last point outside the rect - const inter = intersection(boundaryNode, 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); - } - - isInside = true; - } else { - // Outside - lastPointOutside = point; - // points.push(point); - if (!isInside) { - points.push(point); - } - } - }); - return points; -}; - -export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph, id) { - let points = edge.points; - log.debug('abc88 InsertEdge: edge=', edge, 'e=', e); - let pointsHasChanged = false; - const tail = graph.node(e.v); - var head = graph.node(e.w); - - if (head?.intersect && tail?.intersect) { - points = points.slice(1, edge.points.length - 1); - points.unshift(tail.intersect(points[0])); - points.push(head.intersect(points[points.length - 1])); - } - - if (edge.toCluster) { - log.debug('to cluster abc88', clusterDb[edge.toCluster]); - points = cutPathAtIntersect(edge.points, clusterDb[edge.toCluster].node); - - pointsHasChanged = true; - } - - if (edge.fromCluster) { - log.debug('from cluster abc88', clusterDb[edge.fromCluster]); - points = cutPathAtIntersect(points.reverse(), clusterDb[edge.fromCluster].node).reverse(); - - pointsHasChanged = true; - } - - // The data for our line - const lineData = points.filter((p) => !Number.isNaN(p.y)); - - // This is the accessor function we talked about above - let curve = curveBasis; - // Currently only flowcharts get the curve from the settings, perhaps this should - // be expanded to a common setting? Restricting it for now in order not to cause side-effects that - // have not been thought through - if (edge.curve && (diagramType === 'graph' || diagramType === 'flowchart')) { - curve = edge.curve; - } - - const { x, y } = getLineFunctionsWithOffset(edge); - const lineFunction = line().x(x).y(y).curve(curve); - - // Construct stroke classes based on properties - let strokeClasses; - switch (edge.thickness) { - case 'normal': - strokeClasses = 'edge-thickness-normal'; - break; - case 'thick': - strokeClasses = 'edge-thickness-thick'; - break; - case 'invisible': - strokeClasses = 'edge-thickness-thick'; - break; - default: - strokeClasses = ''; - } - switch (edge.pattern) { - case 'solid': - strokeClasses += ' edge-pattern-solid'; - break; - case 'dotted': - strokeClasses += ' edge-pattern-dotted'; - break; - case 'dashed': - strokeClasses += ' edge-pattern-dashed'; - break; - } - - const svgPath = elem - .append('path') - .attr('d', lineFunction(lineData)) - .attr('id', edge.id) - .attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '')) - .attr('style', edge.style); - - // DEBUG code, adds a red circle at each edge coordinate - // edge.points.forEach((point) => { - // elem - // .append('circle') - // .style('stroke', 'red') - // .style('fill', 'red') - // .attr('r', 1) - // .attr('cx', point.x) - // .attr('cy', point.y); - // }); - - let url = ''; - // // TODO: Can we load this config only from the rendered graph type? - if (getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute) { - url = getUrl(true); - } - - addEdgeMarkers(svgPath, edge, url, id, diagramType); - - let paths = {}; - if (pointsHasChanged) { - paths.updatedPath = points; - } - paths.originalPath = edge.points; - return paths; -}; diff --git a/packages/mermaid/src/dagre-wrapper/edges.spec.js b/packages/mermaid/src/dagre-wrapper/edges.spec.js deleted file mode 100644 index dfe4c790171..00000000000 --- a/packages/mermaid/src/dagre-wrapper/edges.spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import { intersection } from './edges.js'; -import { setLogLevel } from '../logger.js'; - -describe('Graphlib decorations', () => { - let node; - beforeEach(function () { - setLogLevel(1); - node = { x: 171, y: 100, width: 210, height: 184 }; - }); - - describe('intersection', function () { - it('case 1 - intersection on left edge of box', function () { - const o = { x: 31, y: 143.2257070163421 }; - const i = { x: 99.3359375, y: 100 }; - const int = intersection(node, o, i); - expect(int.x).toBe(66); - expect(int.y).toBeCloseTo(122.139); - }); - - it('case 2 - intersection on left edge of box', function () { - const o = { x: 310.2578125, y: 169.88002060631462 }; - const i = { x: 127.96875, y: 100 }; - const node2 = { - height: 337.5, - width: 184.4609375, - x: 100.23046875, - y: 176.75, - }; - const int = intersection(node2, o, i); - expect(int.x).toBeCloseTo(192.4609375); - expect(int.y).toBeCloseTo(145.15711441743503); - }); - it('case 3 - intersection on top of box outside point greater than inside point', function () { - const o = { x: 157, y: 39 }; - const i = { x: 104, y: 105 }; - const node2 = { - width: 212, - x: 114, - y: 164, - height: 176, - }; - const int = intersection(node2, o, i); - expect(int.x).toBeCloseTo(133.71); - expect(int.y).toBeCloseTo(76); - // expect(int.y).toBeCloseTo(67.833) - }); - it('case 4 - intersection on top of box inside point greater than inside point', function () { - const o = { x: 144, y: 38 }; - const i = { x: 198, y: 105 }; - const node2 = { - width: 212, - x: 114, - y: 164, - height: 176, - }; - const int = intersection(node2, o, i); - expect(int.x).toBeCloseTo(174.626); - expect(int.y).toBeCloseTo(76); - // expect(int.y).toBeCloseTo(67.833) - }); - }); -}); diff --git a/packages/mermaid/src/dagre-wrapper/index.js b/packages/mermaid/src/dagre-wrapper/index.js deleted file mode 100644 index f000dbe01f9..00000000000 --- a/packages/mermaid/src/dagre-wrapper/index.js +++ /dev/null @@ -1,196 +0,0 @@ -import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js'; -import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js'; -import insertMarkers from './markers.js'; -import { updateNodeBounds } from './shapes/util.js'; -import { - clear as clearGraphlib, - clusterDb, - adjustClustersAndEdges, - findNonClusterChild, - sortNodesByHierarchy, -} from './mermaid-graphlib.js'; -import { insertNode, positionNode, clear as clearNodes, setNodeElem } from './nodes.js'; -import { insertCluster, clear as clearClusters } from './clusters.js'; -import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges.js'; -import { log } from '../logger.js'; -import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js'; -import { getConfig } from '../diagram-api/diagramAPI.js'; - -const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, siteConfig) => { - log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster); - const dir = graph.graph().rankdir; - log.trace('Dir in recursive render - dir:', dir); - - const elem = _elem.insert('g').attr('class', 'root'); - if (!graph.nodes()) { - log.info('No nodes found for', graph); - } else { - log.info('Recursive render XXX', graph.nodes()); - } - if (graph.edges().length > 0) { - log.trace('Recursive edges', graph.edge(graph.edges()[0])); - } - const clusters = elem.insert('g').attr('class', 'clusters'); - const edgePaths = elem.insert('g').attr('class', 'edgePaths'); - const edgeLabels = elem.insert('g').attr('class', 'edgeLabels'); - const nodes = elem.insert('g').attr('class', 'nodes'); - - // Insert nodes, this will insert them into the dom and each node will get a size. The size is updated - // to the abstract node and is later used by dagre for the layout - await Promise.all( - graph.nodes().map(async function (v) { - const node = graph.node(v); - if (parentCluster !== undefined) { - const data = JSON.parse(JSON.stringify(parentCluster.clusterData)); - // data.clusterPositioning = true; - log.info('Setting data for cluster XXX (', v, ') ', data, parentCluster); - graph.setNode(parentCluster.id, data); - if (!graph.parent(v)) { - log.trace('Setting parent', v, parentCluster.id); - graph.setParent(v, parentCluster.id, data); - } - } - log.info('(Insert) Node XXX' + v + ': ' + JSON.stringify(graph.node(v))); - if (node?.clusterNode) { - // const children = graph.children(v); - log.info('Cluster identified', v, node.width, graph.node(v)); - // `node.graph.setGraph` applies the graph configurations such as nodeSpacing to subgraphs as without this the default values would be used - // We override only the `ranksep` and `nodesep` configurations to allow for setting subgraph spacing while avoiding overriding other properties - const { ranksep, nodesep } = graph.graph(); - node.graph.setGraph({ - ...node.graph.graph(), - ranksep, - nodesep, - }); - const o = await recursiveRender( - nodes, - node.graph, - diagramType, - id, - graph.node(v), - siteConfig - ); - const newEl = o.elem; - updateNodeBounds(node, newEl); - node.diff = o.diff || 0; - log.info('Node bounds (abc123)', v, node, node.width, node.x, node.y); - setNodeElem(newEl, node); - - log.warn('Recursive render complete ', newEl, node); - } else { - if (graph.children(v).length > 0) { - // This is a cluster but not to be rendered recursively - // Render as before - log.info('Cluster - the non recursive path XXX', v, node.id, node, graph); - log.info(findNonClusterChild(node.id, graph)); - clusterDb[node.id] = { id: findNonClusterChild(node.id, graph), node }; - // insertCluster(clusters, graph.node(v)); - } else { - log.info('Node - the non recursive path', v, node.id, node); - await insertNode(nodes, graph.node(v), { config: siteConfig, dir }); - } - } - }) - ); - - // Insert labels, this will insert them into the dom so that the width can be calculated - // Also figure out which edges point to/from clusters and adjust them accordingly - // Edges from/to clusters really points to the first child in the cluster. - // TODO: pick optimal child in the cluster to us as link anchor - graph.edges().forEach(async function (e) { - const edge = graph.edge(e.v, e.w, e.name); - log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e)); - log.info('Edge ' + e.v + ' -> ' + e.w + ': ', e, ' ', JSON.stringify(graph.edge(e))); - - // Check if link is either from or to a cluster - log.info('Fix', clusterDb, 'ids:', e.v, e.w, 'Translating: ', clusterDb[e.v], clusterDb[e.w]); - await insertEdgeLabel(edgeLabels, edge); - }); - - graph.edges().forEach(function (e) { - log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e)); - }); - log.info('Graph before layout:', JSON.stringify(graphlibJson.write(graph))); - log.info('#############################################'); - log.info('### Layout ###'); - log.info('#############################################'); - log.info(graph); - dagreLayout(graph); - log.info('Graph after layout:', JSON.stringify(graphlibJson.write(graph))); - // Move the nodes to the correct place - let diff = 0; - const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig); - for (const v of sortNodesByHierarchy(graph)) { - const node = graph.node(v); - log.info('Position ' + v + ': ' + JSON.stringify(graph.node(v))); - log.info( - 'Position ' + v + ': (' + node.x, - ',' + node.y, - ') width: ', - node.width, - ' height: ', - node.height - ); - if (node?.clusterNode) { - // clusterDb[node.id].node = node; - node.y += subGraphTitleTotalMargin; - positionNode(node); - } else { - // Non cluster node - if (graph.children(v).length > 0) { - // A cluster in the non-recursive way - // positionCluster(node); - node.height += subGraphTitleTotalMargin; - await insertCluster(clusters, node); - clusterDb[node.id].node = node; - } else { - node.y += subGraphTitleTotalMargin / 2; - positionNode(node); - } - } - } - - // Move the edge labels to the correct place after layout - graph.edges().forEach(function (e) { - const edge = graph.edge(e); - log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge); - - edge.points.forEach((point) => (point.y += subGraphTitleTotalMargin / 2)); - const paths = insertEdge(edgePaths, e, edge, clusterDb, diagramType, graph, id); - positionEdgeLabel(edge, paths); - }); - - graph.nodes().forEach(function (v) { - const n = graph.node(v); - log.info(v, n.type, n.diff); - if (n.type === 'group') { - diff = n.diff; - } - }); - return { elem, diff }; -}; - -export const render = async (elem, graph, markers, diagramType, id) => { - insertMarkers(elem, markers, diagramType, id); - clearNodes(); - clearEdges(); - clearClusters(); - clearGraphlib(); - - log.warn('Graph at first:', JSON.stringify(graphlibJson.write(graph))); - adjustClustersAndEdges(graph); - log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph))); - // log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph)); - const siteConfig = getConfig(); - await recursiveRender(elem, graph, diagramType, id, undefined, siteConfig); -}; - -// const shapeDefinitions = {}; -// export const addShape = ({ shapeType: fun }) => { -// shapeDefinitions[shapeType] = fun; -// }; - -// const arrowDefinitions = {}; -// export const addArrow = ({ arrowType: fun }) => { -// arrowDefinitions[arrowType] = fun; -// }; diff --git a/packages/mermaid/src/dagre-wrapper/intersect/index.js b/packages/mermaid/src/dagre-wrapper/intersect/index.js deleted file mode 100644 index 47f71747db1..00000000000 --- a/packages/mermaid/src/dagre-wrapper/intersect/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Borrowed with love from dagre-d3. Many thanks to cpettitt! - */ - -import node from './intersect-node.js'; -import circle from './intersect-circle.js'; -import ellipse from './intersect-ellipse.js'; -import polygon from './intersect-polygon.js'; -import rect from './intersect-rect.js'; - -export default { - node, - circle, - ellipse, - polygon, - rect, -}; diff --git a/packages/mermaid/src/dagre-wrapper/intersect/intersect-circle.js b/packages/mermaid/src/dagre-wrapper/intersect/intersect-circle.js deleted file mode 100644 index 8f5ba72dfc1..00000000000 --- a/packages/mermaid/src/dagre-wrapper/intersect/intersect-circle.js +++ /dev/null @@ -1,12 +0,0 @@ -import intersectEllipse from './intersect-ellipse.js'; - -/** - * @param node - * @param rx - * @param point - */ -function intersectCircle(node, rx, point) { - return intersectEllipse(node, rx, rx, point); -} - -export default intersectCircle; diff --git a/packages/mermaid/src/dagre-wrapper/intersect/intersect-ellipse.js b/packages/mermaid/src/dagre-wrapper/intersect/intersect-ellipse.js deleted file mode 100644 index 96f4a166e6a..00000000000 --- a/packages/mermaid/src/dagre-wrapper/intersect/intersect-ellipse.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @param node - * @param rx - * @param ry - * @param point - */ -function intersectEllipse(node, rx, ry, point) { - // Formulae from: https://mathworld.wolfram.com/Ellipse-LineIntersection.html - - var cx = node.x; - var cy = node.y; - - var px = cx - point.x; - var py = cy - point.y; - - var det = Math.sqrt(rx * rx * py * py + ry * ry * px * px); - - var dx = Math.abs((rx * ry * px) / det); - if (point.x < cx) { - dx = -dx; - } - var dy = Math.abs((rx * ry * py) / det); - if (point.y < cy) { - dy = -dy; - } - - return { x: cx + dx, y: cy + dy }; -} - -export default intersectEllipse; diff --git a/packages/mermaid/src/dagre-wrapper/intersect/intersect-line.js b/packages/mermaid/src/dagre-wrapper/intersect/intersect-line.js deleted file mode 100644 index e97ae6f0dc6..00000000000 --- a/packages/mermaid/src/dagre-wrapper/intersect/intersect-line.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Returns the point at which two lines, p and q, intersect or returns undefined if they do not intersect. - * - * @param p1 - * @param p2 - * @param q1 - * @param q2 - */ -function intersectLine(p1, p2, q1, q2) { - // Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994, - // p7 and p473. - - var a1, a2, b1, b2, c1, c2; - var r1, r2, r3, r4; - var denom, offset, num; - var x, y; - - // Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x + - // b1 y + c1 = 0. - a1 = p2.y - p1.y; - b1 = p1.x - p2.x; - c1 = p2.x * p1.y - p1.x * p2.y; - - // Compute r3 and r4. - r3 = a1 * q1.x + b1 * q1.y + c1; - 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 - a2 = q2.y - q1.y; - b2 = q1.x - q2.x; - c2 = q2.x * q1.y - q1.x * q2.y; - - // Compute r1 and r2 - r1 = a2 * p1.x + b2 * p1.y + c2; - 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. - denom = a1 * b2 - a2 * b1; - if (denom === 0) { - return /*COLLINEAR*/; - } - - 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. - num = b1 * c2 - b2 * c1; - x = num < 0 ? (num - offset) / denom : (num + offset) / denom; - - num = a2 * c1 - a1 * c2; - y = num < 0 ? (num - offset) / denom : (num + offset) / denom; - - return { x: x, y: y }; -} - -/** - * @param r1 - * @param r2 - */ -function sameSign(r1, r2) { - return r1 * r2 > 0; -} - -export default intersectLine; diff --git a/packages/mermaid/src/dagre-wrapper/intersect/intersect-node.js b/packages/mermaid/src/dagre-wrapper/intersect/intersect-node.js deleted file mode 100644 index 54a88ba6175..00000000000 --- a/packages/mermaid/src/dagre-wrapper/intersect/intersect-node.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @param node - * @param point - */ -function intersectNode(node, point) { - // console.info('Intersect Node'); - return node.intersect(point); -} - -export default intersectNode; diff --git a/packages/mermaid/src/dagre-wrapper/intersect/intersect-polygon.js b/packages/mermaid/src/dagre-wrapper/intersect/intersect-polygon.js deleted file mode 100644 index 6941372c7d6..00000000000 --- a/packages/mermaid/src/dagre-wrapper/intersect/intersect-polygon.js +++ /dev/null @@ -1,70 +0,0 @@ -/* eslint "no-console": off */ - -import intersectLine from './intersect-line.js'; - -export default intersectPolygon; - -/** - * Returns the point ({x, y}) at which the point argument intersects with the node argument assuming - * that it has the shape specified by polygon. - * - * @param node - * @param polyPoints - * @param point - */ -function intersectPolygon(node, polyPoints, point) { - var x1 = node.x; - var y1 = node.y; - - var intersections = []; - - var minX = Number.POSITIVE_INFINITY; - var 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); - } - - var left = x1 - node.width / 2 - minX; - var top = y1 - node.height / 2 - minY; - - for (var i = 0; i < polyPoints.length; i++) { - var p1 = polyPoints[i]; - var p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0]; - var intersect = intersectLine( - node, - point, - { x: left + p1.x, y: top + p1.y }, - { x: left + p2.x, y: top + p2.y } - ); - if (intersect) { - intersections.push(intersect); - } - } - - if (!intersections.length) { - // console.log('NO INTERSECTION FOUND, RETURN NODE CENTER', node); - return node; - } - - if (intersections.length > 1) { - // More intersections, find the one nearest to edge end point - intersections.sort(function (p, q) { - var pdx = p.x - point.x; - var pdy = p.y - point.y; - var distp = Math.sqrt(pdx * pdx + pdy * pdy); - - var qdx = q.x - point.x; - var qdy = q.y - point.y; - var distq = Math.sqrt(qdx * qdx + qdy * qdy); - - return distp < distq ? -1 : distp === distq ? 0 : 1; - }); - } - return intersections[0]; -} diff --git a/packages/mermaid/src/dagre-wrapper/intersect/intersect-rect.js b/packages/mermaid/src/dagre-wrapper/intersect/intersect-rect.js deleted file mode 100644 index daf6b5eea00..00000000000 --- a/packages/mermaid/src/dagre-wrapper/intersect/intersect-rect.js +++ /dev/null @@ -1,32 +0,0 @@ -const intersectRect = (node, point) => { - var x = node.x; - var y = node.y; - - // Rectangle intersection algorithm from: - // https://math.stackexchange.com/questions/108113/find-edge-between-two-boxes - var dx = point.x - x; - var dy = point.y - y; - var w = node.width / 2; - var h = node.height / 2; - - var sx, sy; - if (Math.abs(dy) * w > Math.abs(dx) * h) { - // Intersection is top or bottom of rect. - if (dy < 0) { - h = -h; - } - sx = dy === 0 ? 0 : (h * dx) / dy; - sy = h; - } else { - // Intersection is left or right of rect. - if (dx < 0) { - w = -w; - } - sx = w; - sy = dx === 0 ? 0 : (w * dy) / dx; - } - - return { x: x + sx, y: y + sy }; -}; - -export default intersectRect; diff --git a/packages/mermaid/src/dagre-wrapper/markers.js b/packages/mermaid/src/dagre-wrapper/markers.js deleted file mode 100644 index 91f7ecc80ac..00000000000 --- a/packages/mermaid/src/dagre-wrapper/markers.js +++ /dev/null @@ -1,294 +0,0 @@ -/** Setup arrow head and define the marker. The result is appended to the svg. */ - -import { log } from '../logger.js'; - -// Only add the number of markers that the diagram needs -const insertMarkers = (elem, markerArray, type, id) => { - markerArray.forEach((markerName) => { - markers[markerName](elem, type, id); - }); -}; - -const extension = (elem, type, id) => { - log.trace('Making markers for ', id); - elem - .append('defs') - .append('marker') - .attr('id', id + '_' + type + '-extensionStart') - .attr('class', 'marker extension ' + type) - .attr('refX', 18) - .attr('refY', 7) - .attr('markerWidth', 190) - .attr('markerHeight', 240) - .attr('orient', 'auto') - .append('path') - .attr('d', 'M 1,7 L18,13 V 1 Z'); - - elem - .append('defs') - .append('marker') - .attr('id', id + '_' + type + '-extensionEnd') - .attr('class', 'marker extension ' + type) - .attr('refX', 1) - .attr('refY', 7) - .attr('markerWidth', 20) - .attr('markerHeight', 28) - .attr('orient', 'auto') - .append('path') - .attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead -}; - -const composition = (elem, type, id) => { - elem - .append('defs') - .append('marker') - .attr('id', id + '_' + type + '-compositionStart') - .attr('class', 'marker composition ' + type) - .attr('refX', 18) - .attr('refY', 7) - .attr('markerWidth', 190) - .attr('markerHeight', 240) - .attr('orient', 'auto') - .append('path') - .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); - - elem - .append('defs') - .append('marker') - .attr('id', id + '_' + type + '-compositionEnd') - .attr('class', 'marker composition ' + type) - .attr('refX', 1) - .attr('refY', 7) - .attr('markerWidth', 20) - .attr('markerHeight', 28) - .attr('orient', 'auto') - .append('path') - .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); -}; -const aggregation = (elem, type, id) => { - elem - .append('defs') - .append('marker') - .attr('id', id + '_' + type + '-aggregationStart') - .attr('class', 'marker aggregation ' + type) - .attr('refX', 18) - .attr('refY', 7) - .attr('markerWidth', 190) - .attr('markerHeight', 240) - .attr('orient', 'auto') - .append('path') - .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); - - elem - .append('defs') - .append('marker') - .attr('id', id + '_' + type + '-aggregationEnd') - .attr('class', 'marker aggregation ' + type) - .attr('refX', 1) - .attr('refY', 7) - .attr('markerWidth', 20) - .attr('markerHeight', 28) - .attr('orient', 'auto') - .append('path') - .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); -}; -const dependency = (elem, type, id) => { - elem - .append('defs') - .append('marker') - .attr('id', id + '_' + type + '-dependencyStart') - .attr('class', 'marker dependency ' + type) - .attr('refX', 6) - .attr('refY', 7) - .attr('markerWidth', 190) - .attr('markerHeight', 240) - .attr('orient', 'auto') - .append('path') - .attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z'); - - elem - .append('defs') - .append('marker') - .attr('id', id + '_' + type + '-dependencyEnd') - .attr('class', 'marker dependency ' + type) - .attr('refX', 13) - .attr('refY', 7) - .attr('markerWidth', 20) - .attr('markerHeight', 28) - .attr('orient', 'auto') - .append('path') - .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z'); -}; -const lollipop = (elem, type, id) => { - elem - .append('defs') - .append('marker') - .attr('id', id + '_' + type + '-lollipopStart') - .attr('class', 'marker lollipop ' + type) - .attr('refX', 13) - .attr('refY', 7) - .attr('markerWidth', 190) - .attr('markerHeight', 240) - .attr('orient', 'auto') - .append('circle') - .attr('stroke', 'black') - .attr('fill', 'transparent') - .attr('cx', 7) - .attr('cy', 7) - .attr('r', 6); - - elem - .append('defs') - .append('marker') - .attr('id', id + '_' + type + '-lollipopEnd') - .attr('class', 'marker lollipop ' + type) - .attr('refX', 1) - .attr('refY', 7) - .attr('markerWidth', 190) - .attr('markerHeight', 240) - .attr('orient', 'auto') - .append('circle') - .attr('stroke', 'black') - .attr('fill', 'transparent') - .attr('cx', 7) - .attr('cy', 7) - .attr('r', 6); -}; -const point = (elem, type, id) => { - elem - .append('marker') - .attr('id', id + '_' + type + '-pointEnd') - .attr('class', 'marker ' + type) - .attr('viewBox', '0 0 10 10') - .attr('refX', 6) - .attr('refY', 5) - .attr('markerUnits', 'userSpaceOnUse') - .attr('markerWidth', 12) - .attr('markerHeight', 12) - .attr('orient', 'auto') - .append('path') - .attr('d', 'M 0 0 L 10 5 L 0 10 z') - .attr('class', 'arrowMarkerPath') - .style('stroke-width', 1) - .style('stroke-dasharray', '1,0'); - elem - .append('marker') - .attr('id', id + '_' + type + '-pointStart') - .attr('class', 'marker ' + type) - .attr('viewBox', '0 0 10 10') - .attr('refX', 4.5) - .attr('refY', 5) - .attr('markerUnits', 'userSpaceOnUse') - .attr('markerWidth', 12) - .attr('markerHeight', 12) - .attr('orient', 'auto') - .append('path') - .attr('d', 'M 0 5 L 10 10 L 10 0 z') - .attr('class', 'arrowMarkerPath') - .style('stroke-width', 1) - .style('stroke-dasharray', '1,0'); -}; -const circle = (elem, type, id) => { - elem - .append('marker') - .attr('id', id + '_' + type + '-circleEnd') - .attr('class', 'marker ' + type) - .attr('viewBox', '0 0 10 10') - .attr('refX', 11) - .attr('refY', 5) - .attr('markerUnits', 'userSpaceOnUse') - .attr('markerWidth', 11) - .attr('markerHeight', 11) - .attr('orient', 'auto') - .append('circle') - .attr('cx', '5') - .attr('cy', '5') - .attr('r', '5') - .attr('class', 'arrowMarkerPath') - .style('stroke-width', 1) - .style('stroke-dasharray', '1,0'); - - elem - .append('marker') - .attr('id', id + '_' + type + '-circleStart') - .attr('class', 'marker ' + type) - .attr('viewBox', '0 0 10 10') - .attr('refX', -1) - .attr('refY', 5) - .attr('markerUnits', 'userSpaceOnUse') - .attr('markerWidth', 11) - .attr('markerHeight', 11) - .attr('orient', 'auto') - .append('circle') - .attr('cx', '5') - .attr('cy', '5') - .attr('r', '5') - .attr('class', 'arrowMarkerPath') - .style('stroke-width', 1) - .style('stroke-dasharray', '1,0'); -}; -const cross = (elem, type, id) => { - elem - .append('marker') - .attr('id', id + '_' + type + '-crossEnd') - .attr('class', 'marker cross ' + type) - .attr('viewBox', '0 0 11 11') - .attr('refX', 12) - .attr('refY', 5.2) - .attr('markerUnits', 'userSpaceOnUse') - .attr('markerWidth', 11) - .attr('markerHeight', 11) - .attr('orient', 'auto') - .append('path') - // .attr('stroke', 'black') - .attr('d', 'M 1,1 l 9,9 M 10,1 l -9,9') - .attr('class', 'arrowMarkerPath') - .style('stroke-width', 2) - .style('stroke-dasharray', '1,0'); - - elem - .append('marker') - .attr('id', id + '_' + type + '-crossStart') - .attr('class', 'marker cross ' + type) - .attr('viewBox', '0 0 11 11') - .attr('refX', -1) - .attr('refY', 5.2) - .attr('markerUnits', 'userSpaceOnUse') - .attr('markerWidth', 11) - .attr('markerHeight', 11) - .attr('orient', 'auto') - .append('path') - // .attr('stroke', 'black') - .attr('d', 'M 1,1 l 9,9 M 10,1 l -9,9') - .attr('class', 'arrowMarkerPath') - .style('stroke-width', 2) - .style('stroke-dasharray', '1,0'); -}; -const barb = (elem, type, id) => { - elem - .append('defs') - .append('marker') - .attr('id', id + '_' + type + '-barbEnd') - .attr('refX', 19) - .attr('refY', 7) - .attr('markerWidth', 20) - .attr('markerHeight', 14) - .attr('markerUnits', 'strokeWidth') - .attr('orient', 'auto') - .append('path') - .attr('d', 'M 19,7 L9,13 L14,7 L9,1 Z'); -}; - -// TODO rename the class diagram markers to something shape descriptive and semantic free -const markers = { - extension, - composition, - aggregation, - dependency, - lollipop, - point, - circle, - cross, - barb, -}; -export default insertMarkers; diff --git a/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js b/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js deleted file mode 100644 index 7e5c5f582ff..00000000000 --- a/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.js +++ /dev/null @@ -1,484 +0,0 @@ -/** Decorates with functions required by mermaids dagre-wrapper. */ -import { log } from '../logger.js'; -import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js'; -import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; - -export let clusterDb = {}; -let descendants = {}; -let parents = {}; - -export const clear = () => { - descendants = {}; - parents = {}; - clusterDb = {}; -}; - -const isDescendant = (id, ancestorId) => { - // if (id === ancestorId) return true; - - log.trace('In isDescendant', ancestorId, ' ', id, ' = ', descendants[ancestorId].includes(id)); - if (descendants[ancestorId].includes(id)) { - return true; - } - - return false; -}; - -const edgeInCluster = (edge, clusterId) => { - log.info('Descendants of ', clusterId, ' is ', descendants[clusterId]); - log.info('Edge is ', edge); - // Edges to/from the cluster is not in the cluster, they are in the parent - if (edge.v === clusterId) { - return false; - } - if (edge.w === clusterId) { - return false; - } - - if (!descendants[clusterId]) { - log.debug('Tilt, ', clusterId, ',not in descendants'); - return false; - } - return ( - descendants[clusterId].includes(edge.v) || - isDescendant(edge.v, clusterId) || - isDescendant(edge.w, clusterId) || - descendants[clusterId].includes(edge.w) - ); -}; - -const copy = (clusterId, graph, newGraph, rootId) => { - log.warn( - 'Copying children of ', - clusterId, - 'root', - rootId, - 'data', - graph.node(clusterId), - rootId - ); - const nodes = graph.children(clusterId) || []; - - // Include cluster node if it is not the root - if (clusterId !== rootId) { - nodes.push(clusterId); - } - - log.warn('Copying (nodes) clusterId', clusterId, 'nodes', nodes); - - nodes.forEach((node) => { - if (graph.children(node).length > 0) { - copy(node, graph, newGraph, rootId); - } else { - const data = graph.node(node); - log.info('cp ', node, ' to ', rootId, ' with parent ', clusterId); //,node, data, ' parent is ', clusterId); - newGraph.setNode(node, data); - if (rootId !== graph.parent(node)) { - log.warn('Setting parent', node, graph.parent(node)); - newGraph.setParent(node, graph.parent(node)); - } - - if (clusterId !== rootId && node !== clusterId) { - log.debug('Setting parent', node, clusterId); - newGraph.setParent(node, clusterId); - } else { - log.info('In copy ', clusterId, 'root', rootId, 'data', graph.node(clusterId), rootId); - log.debug( - 'Not Setting parent for node=', - node, - 'cluster!==rootId', - clusterId !== rootId, - 'node!==clusterId', - node !== clusterId - ); - } - const edges = graph.edges(node); - log.debug('Copying Edges', edges); - edges.forEach((edge) => { - log.info('Edge', edge); - const data = graph.edge(edge.v, edge.w, edge.name); - log.info('Edge data', data, rootId); - try { - // Do not copy edges in and out of the root cluster, they belong to the parent graph - if (edgeInCluster(edge, rootId)) { - log.info('Copying as ', edge.v, edge.w, data, edge.name); - newGraph.setEdge(edge.v, edge.w, data, edge.name); - log.info('newGraph edges ', newGraph.edges(), newGraph.edge(newGraph.edges()[0])); - } else { - log.info( - 'Skipping copy of edge ', - edge.v, - '-->', - edge.w, - ' rootId: ', - rootId, - ' clusterId:', - clusterId - ); - } - } catch (e) { - log.error(e); - } - }); - } - log.debug('Removing node', node); - graph.removeNode(node); - }); -}; -export const extractDescendants = (id, graph) => { - // log.debug('Extracting ', id); - const children = graph.children(id); - let res = [...children]; - - for (const child of children) { - parents[child] = id; - res = [...res, ...extractDescendants(child, graph)]; - } - - return res; -}; - -/** - * Validates the graph, checking that all parent child relation points to existing nodes and that - * edges between nodes also ia correct. When not correct the function logs the discrepancies. - * - * @param graph - */ -export const validate = (graph) => { - const edges = graph.edges(); - log.trace('Edges: ', edges); - for (const edge of edges) { - if (graph.children(edge.v).length > 0) { - log.trace('The node ', edge.v, ' is part of and edge even though it has children'); - return false; - } - if (graph.children(edge.w).length > 0) { - log.trace('The node ', edge.w, ' is part of and edge even though it has children'); - return false; - } - } - return true; -}; - -/** - * Finds a child that is not a cluster. When faking an edge between a node and a cluster. - * - * @param id - * @param {any} graph - */ -export const findNonClusterChild = (id, graph) => { - // const node = graph.node(id); - log.trace('Searching', id); - // const children = graph.children(id).reverse(); - const children = graph.children(id); //.reverse(); - log.trace('Searching children of id ', id, children); - if (children.length < 1) { - log.trace('This is a valid node', id); - return id; - } - for (const child of children) { - const _id = findNonClusterChild(child, graph); - if (_id) { - log.trace('Found replacement for', id, ' => ', _id); - return _id; - } - } -}; - -const getAnchorId = (id) => { - if (!clusterDb[id]) { - return id; - } - // If the cluster has no external connections - if (!clusterDb[id].externalConnections) { - return id; - } - - // Return the replacement node - if (clusterDb[id]) { - return clusterDb[id].id; - } - return id; -}; - -export const adjustClustersAndEdges = (graph, depth) => { - if (!graph || depth > 10) { - log.debug('Opting out, no graph '); - return; - } else { - log.debug('Opting in, graph '); - } - // Go through the nodes and for each cluster found, save a replacement node, this can be used when - // faking a link to a cluster - graph.nodes().forEach(function (id) { - const children = graph.children(id); - if (children.length > 0) { - log.warn( - 'Cluster identified', - id, - ' Replacement id in edges: ', - findNonClusterChild(id, graph) - ); - descendants[id] = extractDescendants(id, graph); - clusterDb[id] = { id: findNonClusterChild(id, graph), clusterData: graph.node(id) }; - } - }); - - // Check incoming and outgoing edges for each cluster - graph.nodes().forEach(function (id) { - const children = graph.children(id); - const edges = graph.edges(); - if (children.length > 0) { - log.debug('Cluster identified', id, descendants); - edges.forEach((edge) => { - // log.debug('Edge, descendants: ', edge, descendants[id]); - - // Check if any edge leaves the cluster (not the actual cluster, that's a link from the box) - if (edge.v !== id && edge.w !== id) { - // Any edge where either the one of the nodes is descending to the cluster but not the other - // if (descendants[id].indexOf(edge.v) < 0 && descendants[id].indexOf(edge.w) < 0) { - - const d1 = isDescendant(edge.v, id); - const d2 = isDescendant(edge.w, id); - - // d1 xor d2 - if either d1 is true and d2 is false or the other way around - if (d1 ^ d2) { - log.warn('Edge: ', edge, ' leaves cluster ', id); - log.warn('Descendants of XXX ', id, ': ', descendants[id]); - clusterDb[id].externalConnections = true; - } - } - }); - } else { - log.debug('Not a cluster ', id, descendants); - } - }); - - for (let id of Object.keys(clusterDb)) { - const nonClusterChild = clusterDb[id].id; - const parent = graph.parent(nonClusterChild); - - // Change replacement node of id to parent of current replacement node if valid - if (parent !== id && clusterDb[parent] && !clusterDb[parent].externalConnections) { - clusterDb[id].id = parent; - } - } - - // For clusters with incoming and/or outgoing edges translate those edges to a real node - // in the cluster in order to fake the edge - graph.edges().forEach(function (e) { - const edge = graph.edge(e); - log.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e)); - log.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e))); - - let v = e.v; - let w = e.w; - // Check if link is either from or to a cluster - log.warn( - 'Fix XXX', - clusterDb, - 'ids:', - e.v, - e.w, - 'Translating: ', - clusterDb[e.v], - ' --- ', - clusterDb[e.w] - ); - if (clusterDb[e.v] && clusterDb[e.w] && clusterDb[e.v] === clusterDb[e.w]) { - // cspell:ignore trixing - log.warn('Fixing and trixing link to self - removing XXX', e.v, e.w, e.name); - log.warn('Fixing and trixing - removing XXX', e.v, e.w, e.name); - v = getAnchorId(e.v); - w = getAnchorId(e.w); - graph.removeEdge(e.v, e.w, e.name); - const specialId = e.w + '---' + e.v; - graph.setNode(specialId, { - domId: specialId, - id: specialId, - labelStyle: '', - labelText: edge.label, - padding: 0, - shape: 'labelRect', - style: '', - }); - const edge1 = structuredClone(edge); - const edge2 = structuredClone(edge); - edge1.label = ''; - edge1.arrowTypeEnd = 'none'; - // Clear multiplicity labels on edge1 (start->mid segment) - keep startLabelRight if needed, clear others - edge1.startLabelLeft = ''; - edge1.endLabelRight = ''; - edge1.endLabelLeft = ''; - edge2.label = ''; - edge2.arrowTypeStart = 'none'; - // Clear multiplicity labels on edge2 (mid->end segment) - keep endLabelLeft if needed, clear others - edge2.startLabelRight = ''; - edge2.startLabelLeft = ''; - edge2.endLabelRight = ''; - edge1.fromCluster = e.v; - edge2.toCluster = e.v; - - graph.setEdge(v, specialId, edge1, e.name + '-cyclic-special'); - graph.setEdge(specialId, w, edge2, e.name + '-cyclic-special'); - } else if (clusterDb[e.v] || clusterDb[e.w]) { - log.warn('Fixing and trixing - removing XXX', e.v, e.w, e.name); - v = getAnchorId(e.v); - w = getAnchorId(e.w); - graph.removeEdge(e.v, e.w, e.name); - if (v !== e.v) { - const parent = graph.parent(v); - clusterDb[parent].externalConnections = true; - edge.fromCluster = e.v; - } - if (w !== e.w) { - const parent = graph.parent(w); - clusterDb[parent].externalConnections = true; - edge.toCluster = e.w; - } - log.warn('Fix Replacing with XXX', v, w, e.name); - graph.setEdge(v, w, edge, e.name); - } - }); - log.warn('Adjusted Graph', graphlibJson.write(graph)); - extractor(graph, 0); - - log.trace(clusterDb); - - // Remove references to extracted cluster - // graph.edges().forEach(edge => { - // if (isDescendant(edge.v, clusterId) || isDescendant(edge.w, clusterId)) { - // graph.removeEdge(edge); - // } - // }); -}; - -export const extractor = (graph, depth) => { - log.warn('extractor - ', depth, graphlibJson.write(graph), graph.children('D')); - if (depth > 10) { - log.error('Bailing out'); - return; - } - // For clusters without incoming and/or outgoing edges, create a new cluster-node - // containing the nodes and edges in the cluster in a new graph - // for (let i = 0;) - let nodes = graph.nodes(); - let hasChildren = false; - for (const node of nodes) { - const children = graph.children(node); - hasChildren = hasChildren || children.length > 0; - } - - if (!hasChildren) { - log.debug('Done, no node has children', graph.nodes()); - return; - } - // const clusters = Object.keys(clusterDb); - // clusters.forEach(clusterId => { - log.debug('Nodes = ', nodes, depth); - for (const node of nodes) { - log.debug( - 'Extracting node', - node, - clusterDb, - clusterDb[node] && !clusterDb[node].externalConnections, - !graph.parent(node), - graph.node(node), - graph.children('D'), - ' Depth ', - depth - ); - // Note that the node might have been removed after the Object.keys call so better check - // that it still is in the game - if (!clusterDb[node]) { - // Skip if the node is not a cluster - log.debug('Not a cluster', node, depth); - // break; - } else if ( - !clusterDb[node].externalConnections && - // !graph.parent(node) && - graph.children(node) && - graph.children(node).length > 0 - ) { - log.warn( - 'Cluster without external connections, without a parent and with children', - node, - depth - ); - - const graphSettings = graph.graph(); - let dir = graphSettings.rankdir === 'TB' ? 'LR' : 'TB'; - if (clusterDb[node]?.clusterData?.dir) { - dir = clusterDb[node].clusterData.dir; - log.warn('Fixing dir', clusterDb[node].clusterData.dir, dir); - } - - const clusterGraph = new graphlib.Graph({ - multigraph: true, - compound: true, - }) - .setGraph({ - rankdir: dir, // Todo: set proper spacing - nodesep: 50, - ranksep: 50, - marginx: 8, - marginy: 8, - }) - .setDefaultEdgeLabel(function () { - return {}; - }); - - log.warn('Old graph before copy', graphlibJson.write(graph)); - copy(node, graph, clusterGraph, node); - graph.setNode(node, { - clusterNode: true, - id: node, - clusterData: clusterDb[node].clusterData, - labelText: clusterDb[node].labelText, - graph: clusterGraph, - }); - log.warn('New graph after copy node: (', node, ')', graphlibJson.write(clusterGraph)); - log.debug('Old graph after copy', graphlibJson.write(graph)); - } else { - log.warn( - 'Cluster ** ', - node, - ' **not meeting the criteria !externalConnections:', - !clusterDb[node].externalConnections, - ' no parent: ', - !graph.parent(node), - ' children ', - graph.children(node) && graph.children(node).length > 0, - graph.children('D'), - depth - ); - log.debug(clusterDb); - } - } - - nodes = graph.nodes(); - log.warn('New list of nodes', nodes); - for (const node of nodes) { - const data = graph.node(node); - log.warn(' Now next level', node, data); - if (data.clusterNode) { - extractor(data.graph, depth + 1); - } - } -}; - -const sorter = (graph, nodes) => { - if (nodes.length === 0) { - return []; - } - let result = Object.assign(nodes); - nodes.forEach((node) => { - const children = graph.children(node); - const sorted = sorter(graph, children); - result = [...result, ...sorted]; - }); - - return result; -}; - -export const sortNodesByHierarchy = (graph) => sorter(graph, graph.children()); diff --git a/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.spec.js b/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.spec.js deleted file mode 100644 index d44e5439110..00000000000 --- a/packages/mermaid/src/dagre-wrapper/mermaid-graphlib.spec.js +++ /dev/null @@ -1,508 +0,0 @@ -import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js'; -import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; -import { - validate, - adjustClustersAndEdges, - extractDescendants, - sortNodesByHierarchy, -} from './mermaid-graphlib.js'; -import { setLogLevel, log } from '../logger.js'; - -describe('Graphlib decorations', () => { - let g; - beforeEach(function () { - setLogLevel(1); - g = new graphlib.Graph({ - multigraph: true, - compound: true, - }); - g.setGraph({ - rankdir: 'TB', - nodesep: 10, - ranksep: 10, - marginx: 8, - marginy: 8, - }); - g.setDefaultEdgeLabel(function () { - return {}; - }); - }); - - describe('validate', function () { - it('Validate should detect edges between clusters', function () { - /* - subgraph C1 - a --> b - end - subgraph C2 - c - end - C1 --> C2 - */ - g.setNode('a', { data: 1 }); - g.setNode('b', { data: 2 }); - g.setNode('c', { data: 3 }); - g.setParent('a', 'C1'); - g.setParent('b', 'C1'); - g.setParent('c', 'C2'); - g.setEdge('a', 'b'); - g.setEdge('C1', 'C2'); - - expect(validate(g)).toBe(false); - }); - it('Validate should not detect edges between clusters after adjustment', function () { - /* - subgraph C1 - a --> b - end - subgraph C2 - c - end - C1 --> C2 - */ - g.setNode('a', {}); - g.setNode('b', {}); - g.setNode('c', {}); - g.setParent('a', 'C1'); - g.setParent('b', 'C1'); - g.setParent('c', 'C2'); - g.setEdge('a', 'b'); - g.setEdge('C1', 'C2'); - - adjustClustersAndEdges(g); - log.info(g.edges()); - expect(validate(g)).toBe(true); - }); - - it('Validate should detect edges between clusters and transform clusters GLB4', function () { - /* - a --> b - subgraph C1 - subgraph C2 - a - end - b - end - C1 --> c - */ - g.setNode('a', { data: 1 }); - g.setNode('b', { data: 2 }); - g.setNode('c', { data: 3 }); - g.setNode('C1', { data: 4 }); - g.setNode('C2', { data: 5 }); - g.setParent('a', 'C2'); - g.setParent('b', 'C1'); - g.setParent('C2', 'C1'); - g.setEdge('a', 'b', { name: 'C1-internal-link' }); - g.setEdge('C1', 'c', { name: 'C1-external-link' }); - - adjustClustersAndEdges(g); - log.info(g.nodes()); - expect(g.nodes().length).toBe(2); - expect(validate(g)).toBe(true); - }); - it('Validate should detect edges between clusters and transform clusters GLB5', function () { - /* - a --> b - subgraph C1 - a - end - subgraph C2 - b - end - C1 --> - */ - g.setNode('a', { data: 1 }); - g.setNode('b', { data: 2 }); - g.setParent('a', 'C1'); - g.setParent('b', 'C2'); - // g.setEdge('a', 'b', { name: 'C1-internal-link' }); - g.setEdge('C1', 'C2', { name: 'C1-external-link' }); - - log.info(g.nodes()); - adjustClustersAndEdges(g); - log.info(g.nodes()); - expect(g.nodes().length).toBe(2); - expect(validate(g)).toBe(true); - }); - it('adjustClustersAndEdges GLB6', function () { - /* - subgraph C1 - a - end - C1 --> b - */ - g.setNode('a', { data: 1 }); - g.setNode('b', { data: 2 }); - g.setNode('C1', { data: 3 }); - g.setParent('a', 'C1'); - g.setEdge('C1', 'b', { data: 'link1' }, '1'); - - // log.info(g.edges()) - adjustClustersAndEdges(g); - log.info(g.edges()); - expect(g.nodes()).toEqual(['b', 'C1']); - expect(g.edges().length).toBe(1); - expect(validate(g)).toBe(true); - expect(g.node('C1').clusterNode).toBe(true); - - const C1Graph = g.node('C1').graph; - expect(C1Graph.nodes()).toEqual(['a']); - }); - it('adjustClustersAndEdges GLB7', function () { - /* - subgraph C1 - a - end - C1 --> b - C1 --> c - */ - g.setNode('a', { data: 1 }); - g.setNode('b', { data: 2 }); - g.setNode('c', { data: 3 }); - g.setParent('a', 'C1'); - g.setNode('C1', { data: 4 }); - g.setEdge('C1', 'b', { data: 'link1' }, '1'); - g.setEdge('C1', 'c', { data: 'link2' }, '2'); - - log.info(g.node('C1')); - adjustClustersAndEdges(g); - log.info(g.edges()); - expect(g.nodes()).toEqual(['b', 'c', 'C1']); - expect(g.nodes().length).toBe(3); - expect(g.edges().length).toBe(2); - - expect(g.edges().length).toBe(2); - const edgeData = g.edge(g.edges()[1]); - expect(edgeData.data).toBe('link2'); - expect(validate(g)).toBe(true); - - const C1Graph = g.node('C1').graph; - expect(C1Graph.nodes()).toEqual(['a']); - }); - it('adjustClustersAndEdges GLB8', function () { - /* - subgraph A - a - end - subgraph B - b - end - subgraph C - c - end - A --> B - A --> C - */ - g.setNode('a', { data: 1 }); - g.setNode('b', { data: 2 }); - g.setNode('c', { data: 3 }); - g.setParent('a', 'A'); - g.setParent('b', 'B'); - g.setParent('c', 'C'); - g.setEdge('A', 'B', { data: 'link1' }, '1'); - g.setEdge('A', 'C', { data: 'link2' }, '2'); - - // log.info(g.edges()) - adjustClustersAndEdges(g); - expect(g.nodes()).toEqual(['A', 'B', 'C']); - expect(g.edges().length).toBe(2); - - expect(g.edges().length).toBe(2); - const edgeData = g.edge(g.edges()[1]); - expect(edgeData.data).toBe('link2'); - expect(validate(g)).toBe(true); - - const CGraph = g.node('C').graph; - expect(CGraph.nodes()).toEqual(['c']); - }); - - it('adjustClustersAndEdges the extracted graphs shall contain the correct data GLB10', function () { - /* - subgraph C - subgraph D - d - end - end - */ - - g.setNode('C', { data: 1 }); - g.setNode('D', { data: 2 }); - g.setNode('d', { data: 3 }); - g.setParent('d', 'D'); - g.setParent('D', 'C'); - - // log.info('Graph before', g.node('D')) - // log.info('Graph before', graphlibJson.write(g)) - adjustClustersAndEdges(g); - // log.info('Graph after', graphlibJson.write(g), g.node('C').graph) - - const CGraph = g.node('C').graph; - const DGraph = CGraph.node('D').graph; - - expect(CGraph.nodes()).toEqual(['D']); - expect(DGraph.nodes()).toEqual(['d']); - - expect(g.nodes()).toEqual(['C']); - expect(g.nodes().length).toBe(1); - }); - - it('adjustClustersAndEdges the extracted graphs shall contain the correct data GLB11', function () { - /* - subgraph A - a - end - subgraph B - b - end - subgraph C - subgraph D - d - end - end - A --> B - A --> C - */ - - g.setNode('C', { data: 1 }); - g.setNode('D', { data: 2 }); - g.setNode('d', { data: 3 }); - g.setNode('B', { data: 4 }); - g.setNode('b', { data: 5 }); - g.setNode('A', { data: 6 }); - g.setNode('a', { data: 7 }); - g.setParent('a', 'A'); - g.setParent('b', 'B'); - g.setParent('d', 'D'); - g.setParent('D', 'C'); - g.setEdge('A', 'B', { data: 'link1' }, '1'); - g.setEdge('A', 'C', { data: 'link2' }, '2'); - - log.info('Graph before', g.node('D')); - log.info('Graph before', graphlibJson.write(g)); - adjustClustersAndEdges(g); - log.trace('Graph after', graphlibJson.write(g)); - expect(g.nodes()).toEqual(['C', 'B', 'A']); - expect(g.nodes().length).toBe(3); - expect(g.edges().length).toBe(2); - - const AGraph = g.node('A').graph; - const BGraph = g.node('B').graph; - const CGraph = g.node('C').graph; - // log.info(CGraph.nodes()); - const DGraph = CGraph.node('D').graph; - // log.info('DG', CGraph.children('D')); - - log.info('A', AGraph.nodes()); - expect(AGraph.nodes().length).toBe(1); - expect(AGraph.nodes()).toEqual(['a']); - log.trace('Nodes', BGraph.nodes()); - expect(BGraph.nodes().length).toBe(1); - expect(BGraph.nodes()).toEqual(['b']); - expect(CGraph.nodes()).toEqual(['D']); - expect(CGraph.nodes().length).toEqual(1); - - expect(AGraph.edges().length).toBe(0); - expect(BGraph.edges().length).toBe(0); - expect(CGraph.edges().length).toBe(0); - expect(DGraph.nodes()).toEqual(['d']); - expect(DGraph.edges().length).toBe(0); - // expect(CGraph.node('D')).toEqual({ data: 2 }); - expect(g.edges().length).toBe(2); - - // expect(g.edges().length).toBe(2); - // const edgeData = g.edge(g.edges()[1]); - // expect(edgeData.data).toBe('link2'); - // expect(validate(g)).toBe(true); - }); - it('adjustClustersAndEdges the extracted graphs shall contain the correct links GLB20', function () { - /* - a --> b - subgraph b [Test] - c --> d -->e - end - */ - g.setNode('a', { data: 1 }); - g.setNode('b', { data: 2 }); - g.setNode('c', { data: 3 }); - g.setNode('d', { data: 3 }); - g.setNode('e', { data: 3 }); - g.setParent('c', 'b'); - g.setParent('d', 'b'); - g.setParent('e', 'b'); - g.setEdge('a', 'b', { data: 'link1' }, '1'); - g.setEdge('c', 'd', { data: 'link2' }, '2'); - g.setEdge('d', 'e', { data: 'link2' }, '2'); - - log.info('Graph before', graphlibJson.write(g)); - adjustClustersAndEdges(g); - const bGraph = g.node('b').graph; - // log.trace('Graph after', graphlibJson.write(g)) - log.info('Graph after', graphlibJson.write(bGraph)); - expect(bGraph.nodes().length).toBe(3); - expect(bGraph.edges().length).toBe(2); - }); - it('adjustClustersAndEdges the extracted graphs shall contain the correct links GLB21', function () { - /* - state a { - state b { - state c { - e - } - } - } - */ - g.setNode('a', { data: 1 }); - g.setNode('b', { data: 2 }); - g.setNode('c', { data: 3 }); - g.setNode('e', { data: 3 }); - g.setParent('b', 'a'); - g.setParent('c', 'b'); - g.setParent('e', 'c'); - - log.info('Graph before', graphlibJson.write(g)); - adjustClustersAndEdges(g); - const aGraph = g.node('a').graph; - const bGraph = aGraph.node('b').graph; - log.info('Graph after', graphlibJson.write(aGraph)); - const cGraph = bGraph.node('c').graph; - // log.trace('Graph after', graphlibJson.write(g)) - expect(aGraph.nodes().length).toBe(1); - expect(bGraph.nodes().length).toBe(1); - expect(cGraph.nodes().length).toBe(1); - expect(bGraph.edges().length).toBe(0); - }); - }); - it('adjustClustersAndEdges should handle nesting GLB77', function () { - /* -flowchart TB - subgraph A - b-->B - a-->c - end - subgraph B - c - end - */ - - const exportedGraph = JSON.parse( - '{"options":{"directed":true,"multigraph":true,"compound":true},"nodes":[{"v":"A","value":{"labelStyle":"","shape":"rect","labelText":"A","rx":0,"ry":0,"class":"default","style":"","id":"A","width":500,"type":"group","padding":15}},{"v":"B","value":{"labelStyle":"","shape":"rect","labelText":"B","rx":0,"ry":0,"class":"default","style":"","id":"B","width":500,"type":"group","padding":15},"parent":"A"},{"v":"b","value":{"labelStyle":"","shape":"rect","labelText":"b","rx":0,"ry":0,"class":"default","style":"","id":"b","padding":15},"parent":"A"},{"v":"c","value":{"labelStyle":"","shape":"rect","labelText":"c","rx":0,"ry":0,"class":"default","style":"","id":"c","padding":15},"parent":"B"},{"v":"a","value":{"labelStyle":"","shape":"rect","labelText":"a","rx":0,"ry":0,"class":"default","style":"","id":"a","padding":15},"parent":"A"}],"edges":[{"v":"b","w":"B","name":"1","value":{"minlen":1,"arrowhead":"normal","arrowTypeStart":"arrow_open","arrowTypeEnd":"arrow_point","thickness":"normal","pattern":"solid","style":"fill:none","labelStyle":"","arrowheadStyle":"fill: #333","labelpos":"c","labelType":"text","label":"","id":"L-b-B","classes":"flowchart-link LS-b LE-B"}},{"v":"a","w":"c","name":"2","value":{"minlen":1,"arrowhead":"normal","arrowTypeStart":"arrow_open","arrowTypeEnd":"arrow_point","thickness":"normal","pattern":"solid","style":"fill:none","labelStyle":"","arrowheadStyle":"fill: #333","labelpos":"c","labelType":"text","label":"","id":"L-a-c","classes":"flowchart-link LS-a LE-c"}}],"value":{"rankdir":"TB","nodesep":50,"ranksep":50,"marginx":8,"marginy":8}}' - ); - const gr = graphlibJson.read(exportedGraph); - - log.info('Graph before', graphlibJson.write(gr)); - adjustClustersAndEdges(gr); - const aGraph = gr.node('A').graph; - const bGraph = aGraph.node('B').graph; - log.info('Graph after', graphlibJson.write(aGraph)); - // log.trace('Graph after', graphlibJson.write(g)) - expect(aGraph.parent('c')).toBe('B'); - expect(aGraph.parent('B')).toBe(undefined); - }); -}); -describe('extractDescendants', function () { - let g; - beforeEach(function () { - setLogLevel(1); - g = new graphlib.Graph({ - multigraph: true, - compound: true, - }); - g.setGraph({ - rankdir: 'TB', - nodesep: 10, - ranksep: 10, - marginx: 8, - marginy: 8, - }); - g.setDefaultEdgeLabel(function () { - return {}; - }); - }); - it('Simple case of one level descendants GLB9', function () { - /* - subgraph A - a - end - subgraph B - b - end - subgraph C - c - end - A --> B - A --> C - */ - g.setNode('a', { data: 1 }); - g.setNode('b', { data: 2 }); - g.setNode('c', { data: 3 }); - g.setParent('a', 'A'); - g.setParent('b', 'B'); - g.setParent('c', 'C'); - g.setEdge('A', 'B', { data: 'link1' }, '1'); - g.setEdge('A', 'C', { data: 'link2' }, '2'); - - // log.info(g.edges()) - const d1 = extractDescendants('A', g); - const d2 = extractDescendants('B', g); - const d3 = extractDescendants('C', g); - - expect(d1).toEqual(['a']); - expect(d2).toEqual(['b']); - expect(d3).toEqual(['c']); - }); -}); -describe('sortNodesByHierarchy', function () { - let g; - beforeEach(function () { - setLogLevel(1); - g = new graphlib.Graph({ - multigraph: true, - compound: true, - }); - g.setGraph({ - rankdir: 'TB', - nodesep: 10, - ranksep: 10, - marginx: 8, - marginy: 8, - }); - g.setDefaultEdgeLabel(function () { - return {}; - }); - }); - it('should sort proper en nodes are in reverse order', function () { - /* - a -->b - subgraph B - b - end - subgraph A - B - end - */ - g.setNode('a', { data: 1 }); - g.setNode('b', { data: 2 }); - g.setParent('b', 'B'); - g.setParent('B', 'A'); - g.setEdge('a', 'b', '1'); - expect(sortNodesByHierarchy(g)).toEqual(['a', 'A', 'B', 'b']); - }); - it('should sort proper en nodes are in correct order', function () { - /* - a -->b - subgraph B - b - end - subgraph A - B - end - */ - g.setNode('a', { data: 1 }); - g.setParent('B', 'A'); - g.setParent('b', 'B'); - g.setNode('b', { data: 2 }); - g.setEdge('a', 'b', '1'); - expect(sortNodesByHierarchy(g)).toEqual(['a', 'A', 'B', 'b']); - }); -}); diff --git a/packages/mermaid/src/dagre-wrapper/nodes.js b/packages/mermaid/src/dagre-wrapper/nodes.js deleted file mode 100644 index 8d173319f30..00000000000 --- a/packages/mermaid/src/dagre-wrapper/nodes.js +++ /dev/null @@ -1,1203 +0,0 @@ -import { select } from 'd3'; -import { getConfig } from '../diagram-api/diagramAPI.js'; -import { getEffectiveHtmlLabels } from '../config.js'; -import { log } from '../logger.js'; -import { getArrowPoints } from './blockArrowHelper.js'; -import createLabel from './createLabel.js'; -import intersect from './intersect/index.js'; -import note from './shapes/note.js'; -import { insertPolygonShape, labelHelper, updateNodeBounds } from './shapes/util.js'; - -const formatClass = (str) => { - if (str) { - return ' ' + str; - } - return ''; -}; -const getClassesFromNode = (node, otherClasses) => { - return `${otherClasses ? otherClasses : 'node default'}${formatClass(node.classes)} ${formatClass( - node.class - )}`; -}; - -const question = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper( - parent, - node, - getClassesFromNode(node, undefined), - true - ); - - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; - const s = w + h; - - const points = [ - { x: s / 2, y: 0 }, - { x: s, y: -s / 2 }, - { x: s / 2, y: -s }, - { x: 0, y: -s / 2 }, - ]; - - log.info('Question main (Circle)'); - - const questionElem = insertPolygonShape(shapeSvg, s, s, points); - questionElem.attr('style', node.style); - updateNodeBounds(node, questionElem); - - node.intersect = function (point) { - log.warn('Intersect called'); - return intersect.polygon(node, points, point); - }; - - return shapeSvg; -}; - -const choice = (parent, node) => { - const shapeSvg = parent - .insert('g') - .attr('class', 'node default') - .attr('id', node.domId || node.id); - - const s = 28; - const points = [ - { x: 0, y: s / 2 }, - { x: s / 2, y: 0 }, - { x: 0, y: -s / 2 }, - { x: -s / 2, y: 0 }, - ]; - - const choice = shapeSvg.insert('polygon', ':first-child').attr( - 'points', - points - .map(function (d) { - return d.x + ',' + d.y; - }) - .join(' ') - ); - // center the circle around its coordinate - choice.attr('class', 'state-start').attr('r', 7).attr('width', 28).attr('height', 28); - node.width = 28; - node.height = 28; - - node.intersect = function (point) { - return intersect.circle(node, 14, point); - }; - - return shapeSvg; -}; - -const hexagon = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper( - parent, - node, - getClassesFromNode(node, undefined), - true - ); - - const f = 4; - const h = node.positioned ? node.height : bbox.height + node.padding; - const m = h / f; - const w = node.positioned ? node.width : bbox.width + 2 * m + node.padding; - const points = [ - { x: m, y: 0 }, - { x: w - m, y: 0 }, - { x: w, y: -h / 2 }, - { x: w - m, y: -h }, - { x: m, y: -h }, - { x: 0, y: -h / 2 }, - ]; - - const hex = insertPolygonShape(shapeSvg, w, h, points); - hex.attr('style', node.style); - updateNodeBounds(node, hex); - - node.intersect = function (point) { - return intersect.polygon(node, points, point); - }; - - return shapeSvg; -}; - -const block_arrow = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true); - - const f = 2; - const h = bbox.height + 2 * node.padding; - const m = h / f; - const naturalW = bbox.width + 2 * m + node.padding; - // Only use the layout-computed width when the block explicitly spans multiple columns - const isSpanning = node.positioned && (node.widthInColumns ?? 1) > 1 && node.width > naturalW; - const w = isSpanning ? node.width : naturalW; - - const points = getArrowPoints(node.directions, bbox, node, w); - - const blockArrow = insertPolygonShape(shapeSvg, w, h, points); - blockArrow.attr('style', node.style); - updateNodeBounds(node, blockArrow); - - node.intersect = function (point) { - return intersect.polygon(node, points, point); - }; - - return shapeSvg; -}; - -const rect_left_inv_arrow = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper( - parent, - node, - getClassesFromNode(node, undefined), - true - ); - - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; - const points = [ - { x: -h / 2, y: 0 }, - { x: w, y: 0 }, - { x: w, y: -h }, - { x: -h / 2, y: -h }, - { x: 0, y: -h / 2 }, - ]; - - const el = insertPolygonShape(shapeSvg, w, h, points); - el.attr('style', node.style); - - node.width = w + h; - node.height = h; - - node.intersect = function (point) { - return intersect.polygon(node, points, point); - }; - - return shapeSvg; -}; - -const lean_right = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper(parent, node, getClassesFromNode(node), true); - - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; - const points = [ - { x: (-2 * h) / 6, y: 0 }, - { x: w - h / 6, y: 0 }, - { x: w + (2 * h) / 6, y: -h }, - { x: h / 6, y: -h }, - ]; - - const el = insertPolygonShape(shapeSvg, w, h, points); - el.attr('style', node.style); - updateNodeBounds(node, el); - - node.intersect = function (point) { - return intersect.polygon(node, points, point); - }; - - return shapeSvg; -}; - -const lean_left = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper( - parent, - node, - getClassesFromNode(node, undefined), - true - ); - - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; - const points = [ - { x: (2 * h) / 6, y: 0 }, - { x: w + h / 6, y: 0 }, - { x: w - (2 * h) / 6, y: -h }, - { x: -h / 6, y: -h }, - ]; - - const el = insertPolygonShape(shapeSvg, w, h, points); - el.attr('style', node.style); - updateNodeBounds(node, el); - - node.intersect = function (point) { - return intersect.polygon(node, points, point); - }; - - return shapeSvg; -}; - -const trapezoid = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper( - parent, - node, - getClassesFromNode(node, undefined), - true - ); - - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; - const points = [ - { x: (-2 * h) / 6, y: 0 }, - { x: w + (2 * h) / 6, y: 0 }, - { x: w - h / 6, y: -h }, - { x: h / 6, y: -h }, - ]; - - const el = insertPolygonShape(shapeSvg, w, h, points); - el.attr('style', node.style); - updateNodeBounds(node, el); - - node.intersect = function (point) { - return intersect.polygon(node, points, point); - }; - - return shapeSvg; -}; - -const inv_trapezoid = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper( - parent, - node, - getClassesFromNode(node, undefined), - true - ); - - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; - const points = [ - { x: h / 6, y: 0 }, - { x: w - h / 6, y: 0 }, - { x: w + (2 * h) / 6, y: -h }, - { x: (-2 * h) / 6, y: -h }, - ]; - - const el = insertPolygonShape(shapeSvg, w, h, points); - el.attr('style', node.style); - updateNodeBounds(node, el); - - node.intersect = function (point) { - return intersect.polygon(node, points, point); - }; - - return shapeSvg; -}; - -const rect_right_inv_arrow = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper( - parent, - node, - getClassesFromNode(node, undefined), - true - ); - - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; - const points = [ - { x: 0, y: 0 }, - { x: w + h / 2, y: 0 }, - { x: w, y: -h / 2 }, - { x: w + h / 2, y: -h }, - { x: 0, y: -h }, - ]; - - const el = insertPolygonShape(shapeSvg, w, h, points); - el.attr('style', node.style); - updateNodeBounds(node, el); - - node.intersect = function (point) { - return intersect.polygon(node, points, point); - }; - - return shapeSvg; -}; - -const cylinder = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper( - parent, - node, - getClassesFromNode(node, undefined), - true - ); - - const w = bbox.width + node.padding; - const rx = w / 2; - const ry = rx / (2.5 + w / 50); - const h = bbox.height + ry + node.padding; - - const shape = - 'M 0,' + - ry + - ' a ' + - rx + - ',' + - ry + - ' 0,0,0 ' + - w + - ' 0 a ' + - rx + - ',' + - ry + - ' 0,0,0 ' + - -w + - ' 0 l 0,' + - h + - ' a ' + - rx + - ',' + - ry + - ' 0,0,0 ' + - w + - ' 0 l 0,' + - -h; - - const el = shapeSvg - .attr('label-offset-y', ry) - .insert('path', ':first-child') - .attr('style', node.style) - .attr('d', shape) - .attr('transform', 'translate(' + -w / 2 + ',' + -(h / 2 + ry) + ')'); - - updateNodeBounds(node, el); - - node.intersect = function (point) { - const pos = intersect.rect(node, point); - const x = pos.x - node.x; - - if ( - rx != 0 && - (Math.abs(x) < node.width / 2 || - (Math.abs(x) == node.width / 2 && Math.abs(pos.y - node.y) > node.height / 2 - ry)) - ) { - // ellipsis equation: x*x / a*a + y*y / b*b = 1 - // solve for y to get adjusted value for pos.y - let y = ry * ry * (1 - (x * x) / (rx * rx)); - if (y != 0) { - y = Math.sqrt(y); - } - y = ry - y; - if (point.y - node.y > 0) { - y = -y; - } - - pos.y += y; - } - - return pos; - }; - - return shapeSvg; -}; - -const rect = async (parent, node) => { - const { shapeSvg, bbox, halfPadding } = await labelHelper( - parent, - node, - 'node ' + node.classes + ' ' + node.class, - true - ); - - // add the rect - const rect = shapeSvg.insert('rect', ':first-child'); - - // console.log('Rect node:', node, 'bbox:', bbox, 'halfPadding:', halfPadding, 'node.padding:', node.padding); - // const totalWidth = bbox.width + node.padding * 2; - // const totalHeight = bbox.height + node.padding * 2; - const totalWidth = node.positioned ? node.width : bbox.width + node.padding; - const totalHeight = node.positioned ? node.height : bbox.height + node.padding; - const x = node.positioned ? -totalWidth / 2 : -bbox.width / 2 - halfPadding; - const y = node.positioned ? -totalHeight / 2 : -bbox.height / 2 - halfPadding; - rect - .attr('class', 'basic label-container') - .attr('style', node.style) - .attr('rx', node.rx) - .attr('ry', node.ry) - .attr('x', x) - .attr('y', y) - .attr('width', totalWidth) - .attr('height', totalHeight); - - if (node.props) { - const propKeys = new Set(Object.keys(node.props)); - if (node.props.borders) { - applyNodePropertyBorders(rect, node.props.borders, totalWidth, totalHeight); - propKeys.delete('borders'); - } - propKeys.forEach((propKey) => { - log.warn(`Unknown node property ${propKey}`); - }); - } - - updateNodeBounds(node, rect); - - node.intersect = function (point) { - return intersect.rect(node, point); - }; - - return shapeSvg; -}; - -const composite = async (parent, node) => { - const { shapeSvg, bbox, halfPadding } = await labelHelper( - parent, - node, - 'node ' + node.classes, - true - ); - - // add the rect - const rect = shapeSvg.insert('rect', ':first-child'); - - // const totalWidth = bbox.width + node.padding * 2; - // const totalHeight = bbox.height + node.padding * 2; - const totalWidth = node.positioned ? node.width : bbox.width + node.padding; - const totalHeight = node.positioned ? node.height : bbox.height + node.padding; - const x = node.positioned ? -totalWidth / 2 : -bbox.width / 2 - halfPadding; - const y = node.positioned ? -totalHeight / 2 : -bbox.height / 2 - halfPadding; - rect - .attr('class', 'basic cluster composite label-container') - .attr('style', node.style) - .attr('rx', node.rx) - .attr('ry', node.ry) - .attr('x', x) - .attr('y', y) - .attr('width', totalWidth) - .attr('height', totalHeight); - - if (node.props) { - const propKeys = new Set(Object.keys(node.props)); - if (node.props.borders) { - applyNodePropertyBorders(rect, node.props.borders, totalWidth, totalHeight); - propKeys.delete('borders'); - } - propKeys.forEach((propKey) => { - log.warn(`Unknown node property ${propKey}`); - }); - } - - updateNodeBounds(node, rect); - - node.intersect = function (point) { - return intersect.rect(node, point); - }; - - return shapeSvg; -}; - -const labelRect = async (parent, node) => { - const { shapeSvg } = await labelHelper(parent, node, 'label', true); - - log.trace('Classes = ', node.class); - // add the rect - const rect = shapeSvg.insert('rect', ':first-child'); - - // Hide the rect we are only after the label - const totalWidth = 0; - const totalHeight = 0; - rect.attr('width', totalWidth).attr('height', totalHeight); - shapeSvg.attr('class', 'label edgeLabel'); - - if (node.props) { - const propKeys = new Set(Object.keys(node.props)); - if (node.props.borders) { - applyNodePropertyBorders(rect, node.props.borders, totalWidth, totalHeight); - propKeys.delete('borders'); - } - propKeys.forEach((propKey) => { - log.warn(`Unknown node property ${propKey}`); - }); - } - - updateNodeBounds(node, rect); - - node.intersect = function (point) { - return intersect.rect(node, point); - }; - - return shapeSvg; -}; - -/** - * @param rect - * @param borders - * @param totalWidth - * @param totalHeight - */ -function applyNodePropertyBorders(rect, borders, totalWidth, totalHeight) { - const strokeDashArray = []; - const addBorder = (length) => { - strokeDashArray.push(length, 0); - }; - const skipBorder = (length) => { - strokeDashArray.push(0, length); - }; - if (borders.includes('t')) { - log.debug('add top border'); - addBorder(totalWidth); - } else { - skipBorder(totalWidth); - } - if (borders.includes('r')) { - log.debug('add right border'); - addBorder(totalHeight); - } else { - skipBorder(totalHeight); - } - if (borders.includes('b')) { - log.debug('add bottom border'); - addBorder(totalWidth); - } else { - skipBorder(totalWidth); - } - if (borders.includes('l')) { - log.debug('add left border'); - addBorder(totalHeight); - } else { - skipBorder(totalHeight); - } - rect.attr('stroke-dasharray', strokeDashArray.join(' ')); -} - -const rectWithTitle = async (parent, node) => { - // const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes); - - let classes; - if (!node.classes) { - classes = 'node default'; - } else { - classes = 'node ' + node.classes; - } - // Add outer g element - const shapeSvg = parent - .insert('g') - .attr('class', classes) - .attr('id', node.domId || node.id); - - // Create the title label and insert it after the rect - const rect = shapeSvg.insert('rect', ':first-child'); - // const innerRect = shapeSvg.insert('rect'); - const innerLine = shapeSvg.insert('line'); - - const label = shapeSvg.insert('g').attr('class', 'label'); - - const text2 = node.labelText.flat ? node.labelText.flat() : node.labelText; - // const text2 = typeof text2prim === 'object' ? text2prim[0] : text2prim; - - let title = ''; - if (typeof text2 === 'object') { - title = text2[0]; - } else { - title = text2; - } - log.info('Label text abc79', title, text2, typeof text2 === 'object'); - - const text = await createLabel(label, title, node.labelStyle, true, true); - let bbox = { width: 0, height: 0 }; - if (getEffectiveHtmlLabels(getConfig())) { - const div = text.children[0]; - const dv = select(text); - bbox = div.getBoundingClientRect(); - dv.attr('width', bbox.width); - dv.attr('height', bbox.height); - } - log.info('Text 2', text2); - const textRows = text2.slice(1, text2.length); - let titleBox = text.getBBox(); - const descr = await createLabel( - label, - textRows.join ? textRows.join('
') : textRows, - node.labelStyle, - true, - true - ); - - if (getEffectiveHtmlLabels(getConfig())) { - const div = descr.children[0]; - const dv = select(descr); - bbox = div.getBoundingClientRect(); - dv.attr('width', bbox.width); - dv.attr('height', bbox.height); - } - // bbox = label.getBBox(); - // log.info(descr); - const halfPadding = node.padding / 2; - select(descr).attr( - 'transform', - 'translate( ' + - // (titleBox.width - bbox.width) / 2 + - (bbox.width > titleBox.width ? 0 : (titleBox.width - bbox.width) / 2) + - ', ' + - (titleBox.height + halfPadding + 5) + - ')' - ); - select(text).attr( - 'transform', - 'translate( ' + - // (titleBox.width - bbox.width) / 2 + - (bbox.width < titleBox.width ? 0 : -(titleBox.width - bbox.width) / 2) + - ', ' + - 0 + - ')' - ); - // Get the size of the label - - // Bounding box for title and text - bbox = label.node().getBBox(); - - // Center the label - label.attr( - 'transform', - 'translate(' + -bbox.width / 2 + ', ' + (-bbox.height / 2 - halfPadding + 3) + ')' - ); - - rect - .attr('class', 'outer title-state') - .attr('x', -bbox.width / 2 - halfPadding) - .attr('y', -bbox.height / 2 - halfPadding) - .attr('width', bbox.width + node.padding) - .attr('height', bbox.height + node.padding); - - innerLine - .attr('class', 'divider') - .attr('x1', -bbox.width / 2 - halfPadding) - .attr('x2', bbox.width / 2 + halfPadding) - .attr('y1', -bbox.height / 2 - halfPadding + titleBox.height + halfPadding) - .attr('y2', -bbox.height / 2 - halfPadding + titleBox.height + halfPadding); - - updateNodeBounds(node, rect); - - node.intersect = function (point) { - return intersect.rect(node, point); - }; - - return shapeSvg; -}; - -const stadium = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper( - parent, - node, - getClassesFromNode(node, undefined), - true - ); - - const h = bbox.height + node.padding; - const w = bbox.width + h / 4 + node.padding; - - // add the rect - const rect = shapeSvg - .insert('rect', ':first-child') - .attr('style', node.style) - .attr('rx', h / 2) - .attr('ry', h / 2) - .attr('x', -w / 2) - .attr('y', -h / 2) - .attr('width', w) - .attr('height', h); - - updateNodeBounds(node, rect); - - node.intersect = function (point) { - return intersect.rect(node, point); - }; - - return shapeSvg; -}; - -const circle = async (parent, node) => { - const { shapeSvg, bbox, halfPadding } = await labelHelper( - parent, - node, - getClassesFromNode(node, undefined), - true - ); - const circle = shapeSvg.insert('circle', ':first-child'); - - // center the circle around its coordinate - circle - .attr('style', node.style) - .attr('rx', node.rx) - .attr('ry', node.ry) - .attr('r', bbox.width / 2 + halfPadding) - .attr('width', bbox.width + node.padding) - .attr('height', bbox.height + node.padding); - - log.info('Circle main'); - - updateNodeBounds(node, circle); - - node.intersect = function (point) { - log.info('Circle intersect', node, bbox.width / 2 + halfPadding, point); - return intersect.circle(node, bbox.width / 2 + halfPadding, point); - }; - - return shapeSvg; -}; - -const doublecircle = async (parent, node) => { - const { shapeSvg, bbox, halfPadding } = await labelHelper( - parent, - node, - getClassesFromNode(node, undefined), - true - ); - const gap = 5; - const circleGroup = shapeSvg.insert('g', ':first-child'); - const outerCircle = circleGroup.insert('circle'); - const innerCircle = circleGroup.insert('circle'); - - circleGroup.attr('class', node.class); - - // center the circle around its coordinate - outerCircle - .attr('style', node.style) - .attr('rx', node.rx) - .attr('ry', node.ry) - .attr('r', bbox.width / 2 + halfPadding + gap) - .attr('width', bbox.width + node.padding + gap * 2) - .attr('height', bbox.height + node.padding + gap * 2); - - innerCircle - .attr('style', node.style) - .attr('rx', node.rx) - .attr('ry', node.ry) - .attr('r', bbox.width / 2 + halfPadding) - .attr('width', bbox.width + node.padding) - .attr('height', bbox.height + node.padding); - - log.info('DoubleCircle main'); - - updateNodeBounds(node, outerCircle); - - node.intersect = function (point) { - log.info('DoubleCircle intersect', node, bbox.width / 2 + halfPadding + gap, point); - return intersect.circle(node, bbox.width / 2 + halfPadding + gap, point); - }; - - return shapeSvg; -}; - -const subroutine = async (parent, node) => { - const { shapeSvg, bbox } = await labelHelper( - parent, - node, - getClassesFromNode(node, undefined), - true - ); - - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; - const points = [ - { x: 0, y: 0 }, - { x: w, y: 0 }, - { x: w, y: -h }, - { x: 0, y: -h }, - { x: 0, y: 0 }, - { x: -8, y: 0 }, - { x: w + 8, y: 0 }, - { x: w + 8, y: -h }, - { x: -8, y: -h }, - { x: -8, y: 0 }, - ]; - - const el = insertPolygonShape(shapeSvg, w, h, points); - el.attr('style', node.style); - updateNodeBounds(node, el); - - node.intersect = function (point) { - return intersect.polygon(node, points, point); - }; - - return shapeSvg; -}; - -const start = (parent, node) => { - const shapeSvg = parent - .insert('g') - .attr('class', 'node default') - .attr('id', node.domId || node.id); - const circle = shapeSvg.insert('circle', ':first-child'); - - // center the circle around its coordinate - circle.attr('class', 'state-start').attr('r', 7).attr('width', 14).attr('height', 14); - - updateNodeBounds(node, circle); - - node.intersect = function (point) { - return intersect.circle(node, 7, point); - }; - - return shapeSvg; -}; - -const forkJoin = (parent, node, dir) => { - const shapeSvg = parent - .insert('g') - .attr('class', 'node default') - .attr('id', node.domId || node.id); - - let width = 70; - let height = 10; - - if (dir === 'LR') { - width = 10; - height = 70; - } - - const shape = shapeSvg - .append('rect') - .attr('x', (-1 * width) / 2) - .attr('y', (-1 * height) / 2) - .attr('width', width) - .attr('height', height) - .attr('class', 'fork-join'); - - updateNodeBounds(node, shape); - node.height = node.height + node.padding / 2; - node.width = node.width + node.padding / 2; - node.intersect = function (point) { - return intersect.rect(node, point); - }; - - return shapeSvg; -}; - -const end = (parent, node) => { - const shapeSvg = parent - .insert('g') - .attr('class', 'node default') - .attr('id', node.domId || node.id); - const innerCircle = shapeSvg.insert('circle', ':first-child'); - const circle = shapeSvg.insert('circle', ':first-child'); - - circle.attr('class', 'state-start').attr('r', 7).attr('width', 14).attr('height', 14); - - innerCircle.attr('class', 'state-end').attr('r', 5).attr('width', 10).attr('height', 10); - - updateNodeBounds(node, circle); - - node.intersect = function (point) { - return intersect.circle(node, 7, point); - }; - - return shapeSvg; -}; - -const class_box = async (parent, node) => { - const halfPadding = node.padding / 2; - const rowPadding = 4; - const lineHeight = 8; - - let classes; - if (!node.classes) { - classes = 'node default'; - } else { - classes = 'node ' + node.classes; - } - // Add outer g element - const shapeSvg = parent - .insert('g') - .attr('class', classes) - .attr('id', node.domId || node.id); - - // Create the title label and insert it after the rect - const rect = shapeSvg.insert('rect', ':first-child'); - const topLine = shapeSvg.insert('line'); - const bottomLine = shapeSvg.insert('line'); - let maxWidth = 0; - let maxHeight = rowPadding; - - const labelContainer = shapeSvg.insert('g').attr('class', 'label'); - let verticalPos = 0; - const hasInterface = node.classData.annotations?.[0]; - - // 1. Create the labels - const interfaceLabelText = node.classData.annotations[0] - ? '«' + node.classData.annotations[0] + '»' - : ''; - const interfaceLabel = await createLabel( - labelContainer, - interfaceLabelText, - node.labelStyle, - true, - true - ); - let interfaceBBox = interfaceLabel.getBBox(); - if (getEffectiveHtmlLabels(getConfig())) { - const div = interfaceLabel.children[0]; - const dv = select(interfaceLabel); - interfaceBBox = div.getBoundingClientRect(); - dv.attr('width', interfaceBBox.width); - dv.attr('height', interfaceBBox.height); - } - if (node.classData.annotations[0]) { - maxHeight += interfaceBBox.height + rowPadding; - maxWidth += interfaceBBox.width; - } - - let classTitleString = node.classData.label; - - if (node.classData.type !== undefined && node.classData.type !== '') { - if (getEffectiveHtmlLabels(getConfig())) { - classTitleString += '<' + node.classData.type + '>'; - } else { - classTitleString += '<' + node.classData.type + '>'; - } - } - const classTitleLabel = await createLabel( - labelContainer, - classTitleString, - node.labelStyle, - true, - true - ); - select(classTitleLabel).attr('class', 'classTitle'); - let classTitleBBox = classTitleLabel.getBBox(); - if (getEffectiveHtmlLabels(getConfig())) { - const div = classTitleLabel.children[0]; - const dv = select(classTitleLabel); - classTitleBBox = div.getBoundingClientRect(); - dv.attr('width', classTitleBBox.width); - dv.attr('height', classTitleBBox.height); - } - maxHeight += classTitleBBox.height + rowPadding; - if (classTitleBBox.width > maxWidth) { - maxWidth = classTitleBBox.width; - } - const classAttributes = []; - node.classData.members.forEach(async (member) => { - const parsedInfo = member.getDisplayDetails(); - let parsedText = parsedInfo.displayText; - if (getEffectiveHtmlLabels(getConfig())) { - parsedText = parsedText.replace(//g, '>'); - } - const lbl = await createLabel( - labelContainer, - parsedText, - parsedInfo.cssStyle ? parsedInfo.cssStyle : node.labelStyle, - true, - true - ); - let bbox = lbl.getBBox(); - if (getEffectiveHtmlLabels(getConfig())) { - const div = lbl.children[0]; - const dv = select(lbl); - bbox = div.getBoundingClientRect(); - dv.attr('width', bbox.width); - dv.attr('height', bbox.height); - } - if (bbox.width > maxWidth) { - maxWidth = bbox.width; - } - maxHeight += bbox.height + rowPadding; - classAttributes.push(lbl); - }); - - maxHeight += lineHeight; - - const classMethods = []; - node.classData.methods.forEach(async (member) => { - const parsedInfo = member.getDisplayDetails(); - let displayText = parsedInfo.displayText; - if (getEffectiveHtmlLabels(getConfig())) { - displayText = displayText.replace(//g, '>'); - } - const lbl = await createLabel( - labelContainer, - displayText, - parsedInfo.cssStyle ? parsedInfo.cssStyle : node.labelStyle, - true, - true - ); - let bbox = lbl.getBBox(); - if (getEffectiveHtmlLabels(getConfig())) { - const div = lbl.children[0]; - const dv = select(lbl); - bbox = div.getBoundingClientRect(); - dv.attr('width', bbox.width); - dv.attr('height', bbox.height); - } - if (bbox.width > maxWidth) { - maxWidth = bbox.width; - } - maxHeight += bbox.height + rowPadding; - - classMethods.push(lbl); - }); - - maxHeight += lineHeight; - - // 2. Position the labels - - // position the interface label - if (hasInterface) { - let diffX = (maxWidth - interfaceBBox.width) / 2; - select(interfaceLabel).attr( - 'transform', - 'translate( ' + ((-1 * maxWidth) / 2 + diffX) + ', ' + (-1 * maxHeight) / 2 + ')' - ); - verticalPos = interfaceBBox.height + rowPadding; - } - // Position the class title label - let diffX = (maxWidth - classTitleBBox.width) / 2; - select(classTitleLabel).attr( - 'transform', - 'translate( ' + - ((-1 * maxWidth) / 2 + diffX) + - ', ' + - ((-1 * maxHeight) / 2 + verticalPos) + - ')' - ); - verticalPos += classTitleBBox.height + rowPadding; - - topLine - .attr('class', 'divider') - .attr('x1', -maxWidth / 2 - halfPadding) - .attr('x2', maxWidth / 2 + halfPadding) - .attr('y1', -maxHeight / 2 - halfPadding + lineHeight + verticalPos) - .attr('y2', -maxHeight / 2 - halfPadding + lineHeight + verticalPos); - - verticalPos += lineHeight; - - classAttributes.forEach((lbl) => { - select(lbl).attr( - 'transform', - 'translate( ' + - -maxWidth / 2 + - ', ' + - ((-1 * maxHeight) / 2 + verticalPos + lineHeight / 2) + - ')' - ); - //get the height of the bounding box of each member if exists - const memberBBox = lbl?.getBBox(); - verticalPos += (memberBBox?.height ?? 0) + rowPadding; - }); - - verticalPos += lineHeight; - bottomLine - .attr('class', 'divider') - .attr('x1', -maxWidth / 2 - halfPadding) - .attr('x2', maxWidth / 2 + halfPadding) - .attr('y1', -maxHeight / 2 - halfPadding + lineHeight + verticalPos) - .attr('y2', -maxHeight / 2 - halfPadding + lineHeight + verticalPos); - - verticalPos += lineHeight; - - classMethods.forEach((lbl) => { - select(lbl).attr( - 'transform', - 'translate( ' + -maxWidth / 2 + ', ' + ((-1 * maxHeight) / 2 + verticalPos) + ')' - ); - const memberBBox = lbl?.getBBox(); - verticalPos += (memberBBox?.height ?? 0) + rowPadding; - }); - - rect - .attr('style', node.style) - .attr('class', 'outer title-state') - .attr('x', -maxWidth / 2 - halfPadding) - .attr('y', -(maxHeight / 2) - halfPadding) - .attr('width', maxWidth + node.padding) - .attr('height', maxHeight + node.padding); - - updateNodeBounds(node, rect); - - node.intersect = function (point) { - return intersect.rect(node, point); - }; - - return shapeSvg; -}; - -const shapes = { - rhombus: question, - composite, - question, - rect, - labelRect, - rectWithTitle, - choice, - circle, - doublecircle, - stadium, - hexagon, - block_arrow, - rect_left_inv_arrow, - lean_right, - lean_left, - trapezoid, - inv_trapezoid, - rect_right_inv_arrow, - cylinder, - start, - end, - note, - subroutine, - fork: forkJoin, - join: forkJoin, - class_box, -}; - -let nodeElems = {}; - -export const insertNode = async (elem, node, renderOptions) => { - let newEl; - let el; - - // Add link when appropriate - if (node.link) { - let target; - if (getConfig().securityLevel === 'sandbox') { - target = '_top'; - } else if (node.linkTarget) { - target = node.linkTarget || '_blank'; - } - newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target); - el = await shapes[node.shape](newEl, node, renderOptions); - } else { - el = await shapes[node.shape](elem, node, renderOptions); - newEl = el; - } - if (node.tooltip) { - el.attr('title', node.tooltip); - } - if (node.class) { - el.attr('class', 'node default ' + node.class); - } - - nodeElems[node.id] = newEl; - - if (node.haveCallback) { - nodeElems[node.id].attr('class', nodeElems[node.id].attr('class') + ' clickable'); - } - return newEl; -}; -export const setNodeElem = (elem, node) => { - nodeElems[node.id] = elem; -}; -export const clear = () => { - nodeElems = {}; -}; - -export const positionNode = (node) => { - const el = nodeElems[node.id]; - log.trace( - 'Transforming node', - node.diff, - node, - 'translate(' + (node.x - node.width / 2 - 5) + ', ' + node.width / 2 + ')' - ); - const padding = 8; - const diff = node.diff || 0; - if (node.clusterNode) { - el.attr( - 'transform', - 'translate(' + - (node.x + diff - node.width / 2) + - ', ' + - (node.y - node.height / 2 - padding) + - ')' - ); - } else { - el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')'); - } - return diff; -}; diff --git a/packages/mermaid/src/dagre-wrapper/shapes/note.js b/packages/mermaid/src/dagre-wrapper/shapes/note.js deleted file mode 100644 index 5fa6077e315..00000000000 --- a/packages/mermaid/src/dagre-wrapper/shapes/note.js +++ /dev/null @@ -1,40 +0,0 @@ -import { updateNodeBounds, labelHelper } from './util.js'; -import { log } from '../../logger.js'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; -import { getEffectiveHtmlLabels } from '../../config.js'; -import intersect from '../intersect/index.js'; - -const note = async (parent, node) => { - const useHtmlLabels = node.useHtmlLabels || getEffectiveHtmlLabels(getConfig()); - if (!useHtmlLabels) { - node.centerLabel = true; - } - const { shapeSvg, bbox, halfPadding } = await labelHelper( - parent, - node, - 'node ' + node.classes, - true - ); - - log.info('Classes = ', node.classes); - // add the rect - const rect = shapeSvg.insert('rect', ':first-child'); - - rect - .attr('rx', node.rx) - .attr('ry', node.ry) - .attr('x', -bbox.width / 2 - halfPadding) - .attr('y', -bbox.height / 2 - halfPadding) - .attr('width', bbox.width + node.padding) - .attr('height', bbox.height + node.padding); - - updateNodeBounds(node, rect); - - node.intersect = function (point) { - return intersect.rect(node, point); - }; - - return shapeSvg; -}; - -export default note; diff --git a/packages/mermaid/src/dagre-wrapper/shapes/util.js b/packages/mermaid/src/dagre-wrapper/shapes/util.js deleted file mode 100644 index c3e0b9774df..00000000000 --- a/packages/mermaid/src/dagre-wrapper/shapes/util.js +++ /dev/null @@ -1,114 +0,0 @@ -import createLabel from '../createLabel.js'; -import { createText } from '../../rendering-util/createText.js'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; -import { getEffectiveHtmlLabels } from '../../config.js'; -import { select } from 'd3'; -import { sanitizeText } from '../../diagrams/common/common.js'; -import { decodeEntities } from '../../utils.js'; -import { configureLabelImages } from '../../rendering-util/rendering-elements/shapes/labelImageUtils.js'; - -export const labelHelper = async (parent, node, _classes, isNode) => { - const config = getConfig(); - let classes; - const useHtmlLabels = node.useHtmlLabels || getEffectiveHtmlLabels(config); - if (!_classes) { - classes = 'node default'; - } else { - classes = _classes; - } - - // Add outer g element - const shapeSvg = parent - .insert('g') - .attr('class', classes) - .attr('id', node.domId || node.id); - - // Create the label and insert it after the rect - const label = shapeSvg.insert('g').attr('class', 'label').attr('style', node.labelStyle); - - // Replace labelText with default value if undefined - let labelText; - if (node.labelText === undefined) { - labelText = ''; - } else { - labelText = typeof node.labelText === 'string' ? node.labelText : node.labelText[0]; - } - - let text; - if (node.labelType === 'markdown') { - // text = textNode; - text = createText( - label, - sanitizeText(decodeEntities(labelText), config), - { - useHtmlLabels, - width: node.width || config.flowchart.wrappingWidth, - classes: 'markdown-node-label', - }, - config - ); - } else { - text = await createLabel( - label, - sanitizeText(decodeEntities(labelText), config), - node.labelStyle, - false, - isNode - ); - } - // Get the size of the label - let bbox = text.getBBox(); - const halfPadding = node.padding / 2; - - if (getEffectiveHtmlLabels(config)) { - const div = text.children[0]; - const dv = select(text); - - // if there are images, need to wait for them to load before getting the bounding box - await configureLabelImages(div); - - bbox = div.getBoundingClientRect(); - dv.attr('width', bbox.width); - dv.attr('height', bbox.height); - } - - // Center the label - if (useHtmlLabels) { - label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')'); - } else { - label.attr('transform', 'translate(' + 0 + ', ' + -bbox.height / 2 + ')'); - } - if (node.centerLabel) { - label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')'); - } - label.insert('rect', ':first-child'); - - return { shapeSvg, bbox, halfPadding, label }; -}; - -export const updateNodeBounds = (node, element) => { - const bbox = element.node().getBBox(); - node.width = bbox.width; - node.height = bbox.height; -}; - -/** - * @param parent - * @param w - * @param h - * @param points - */ -export function insertPolygonShape(parent, w, h, points) { - return parent - .insert('polygon', ':first-child') - .attr( - 'points', - points - .map(function (d) { - return d.x + ',' + d.y; - }) - .join(' ') - ) - .attr('class', 'label-container') - .attr('transform', 'translate(' + -w / 2 + ',' + h / 2 + ')'); -} diff --git a/packages/mermaid/src/defaultConfig.ts b/packages/mermaid/src/defaultConfig.ts index 62d706d885c..18efe851eb6 100644 --- a/packages/mermaid/src/defaultConfig.ts +++ b/packages/mermaid/src/defaultConfig.ts @@ -58,8 +58,12 @@ const config: RequiredDeep = { }, }, class: { + defaultRenderer: 'dagre-wrapper', hideEmptyMembersBox: false, hierarchicalNamespaces: true, + // `padding` is intentionally left undefined so the unified (v2) renderer keeps + // its own node sizing — setting the schema default of 5 here would change class + // node dimensions. }, gantt: { ...defaultConfigJson.gantt, diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts index 652a5926cd0..317dbb12015 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts @@ -76,6 +76,12 @@ describe('diagram-orchestration', () => { ); }); + it('should detect class diagram v2 when dagre-wrapper is the class renderer', () => { + expect(detectType('classDiagram', { class: { defaultRenderer: 'dagre-wrapper' } })).toBe( + 'classDiagram' + ); + }); + it('should not detect flowchart if pie contains flowchart', () => { expect( detectType(`pie title: "flowchart" diff --git a/packages/mermaid/src/diagrams/block/blockRenderer.ts b/packages/mermaid/src/diagrams/block/blockRenderer.ts index 21931cb8ac8..1dbaa484f3f 100644 --- a/packages/mermaid/src/diagrams/block/blockRenderer.ts +++ b/packages/mermaid/src/diagrams/block/blockRenderer.ts @@ -1,8 +1,8 @@ import { select as d3select } from 'd3'; import type { Diagram } from '../../Diagram.js'; import * as configApi from '../../config.js'; -import insertMarkers from '../../dagre-wrapper/markers.js'; import { log } from '../../logger.js'; +import insertMarkers from '../../rendering-util/rendering-elements/markers.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; import type { BlockDB } from './blockDB.js'; import { layout } from './layout.js'; @@ -53,8 +53,14 @@ export const draw = async function ( // Establish svg dimensions and get width and height // Why, oh why ???? - if (bounds) { - const bounds2 = bounds; + const renderedBounds = nodes.node()?.getBBox(); + const bounds2 = + renderedBounds && + Number.isFinite(renderedBounds.width) && + Number.isFinite(renderedBounds.height) + ? renderedBounds + : bounds; + if (bounds2) { const magicFactor = Math.max(1, Math.round(0.125 * (bounds2.width / bounds2.height))); const height = bounds2.height + magicFactor + 10; const width = bounds2.width + 10; diff --git a/packages/mermaid/src/diagrams/block/renderHelpers.ts b/packages/mermaid/src/diagrams/block/renderHelpers.ts index 72f2e64411d..3c948ef79d1 100644 --- a/packages/mermaid/src/diagrams/block/renderHelpers.ts +++ b/packages/mermaid/src/diagrams/block/renderHelpers.ts @@ -1,7 +1,12 @@ import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; import { getConfig } from '../../config.js'; -import { insertEdge, insertEdgeLabel, positionEdgeLabel } from '../../dagre-wrapper/edges.js'; -import { insertNode, positionNode } from '../../dagre-wrapper/nodes.js'; +import { + insertEdge, + insertEdgeLabel, + positionEdgeLabel, +} from '../../rendering-util/rendering-elements/edges.js'; +import { insertNode, positionNode } from '../../rendering-util/rendering-elements/nodes.js'; +import type { ShapeID } from '../../rendering-util/rendering-elements/shapes.js'; import { getStylesFromArray } from '../../utils.js'; import type { BlockDB } from './blockDB.js'; import type { Block } from './blockTypes.js'; @@ -14,10 +19,13 @@ function getNodeFromBlock(block: Block, db: BlockDB, positioned = false) { classStr = (vertex?.classes ?? []).join(' '); } classStr = classStr + ' flowchart-label'; + const cssCompiledStyles = (vertex?.classes ?? []).flatMap( + (className) => db.getClasses().get(className)?.styles ?? [] + ); // We create a SVG label, either by delegating to addHtmlLabel or manually let radius = 0; - let shape = ''; + let shape: ShapeID = 'rect'; let padding; // Set the shape based parameters switch (vertex.type) { @@ -64,6 +72,7 @@ function getNodeFromBlock(block: Block, db: BlockDB, positioned = false) { shape = 'circle'; break; case 'ellipse': + // @ts-expect-error -- Ellipses are broken, see https://github.com/mermaid-js/mermaid/issues/5976 shape = 'ellipse'; break; case 'stadium': @@ -95,22 +104,26 @@ function getNodeFromBlock(block: Block, db: BlockDB, positioned = false) { // Add the node const node = { labelStyle: styles.labelStyle, - shape: shape, + shape, + label: vertexText, labelText: vertexText, rx: radius, ry: radius, class: classStr, + cssClasses: classStr, + cssStyles: vertex?.styles ?? [], + cssCompiledStyles, style: styles.style, id: vertex.id, domId: dbDiagramId ? `${dbDiagramId}-${vertex.id}` : vertex.id, + isGroup: false as const, directions: vertex.directions, - width: bounds.width, - height: bounds.height, + width: bounds.width || undefined, + height: bounds.height || undefined, x: bounds.x, y: bounds.y, positioned, intersect: undefined, - type: vertex.type, padding: padding ?? getConfig()?.block?.padding ?? 0, widthInColumns: vertex.widthInColumns ?? 1, }; @@ -122,14 +135,14 @@ async function calculateBlockSize( db: any ) { const node = getNodeFromBlock(block, db, false); - if (node.type === 'group') { + if (block.type === 'group') { return; } // Add the element to the DOM to size it const config = getConfig(); const nodeEl = await insertNode(elem, node, { config }); - const boundingBox = nodeEl.node().getBBox(); + const boundingBox = nodeEl.node()?.getBBox() ?? { width: 0, height: 0 }; const obj = db.getBlock(node.id); obj.size = { width: boundingBox.width, height: boundingBox.height, x: 0, y: 0, node: nodeEl }; db.setBlock(obj); @@ -228,7 +241,6 @@ export async function insertEdges( insertEdge( elem, - { v: edge.start, w: edge.end, name: prefixedEdgeId }, { ...edge, id: prefixedEdgeId, @@ -237,9 +249,10 @@ export async function insertEdges( points, classes: dynamicClasses, }, - undefined, + {}, 'block', - g, + g.node(edge.start), + g.node(edge.end), id ); if (edge.label) { diff --git a/packages/mermaid/src/diagrams/class/classRenderer-v2.ts b/packages/mermaid/src/diagrams/class/classRenderer-v2.ts deleted file mode 100644 index 4cc054066cb..00000000000 --- a/packages/mermaid/src/diagrams/class/classRenderer-v2.ts +++ /dev/null @@ -1,473 +0,0 @@ -// @ts-nocheck - don't check until handle it -import { select, curveLinear } from 'd3'; -import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; -import { log } from '../../logger.js'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; -import { getEffectiveHtmlLabels } from '../../config.js'; -import { render } from '../../dagre-wrapper/index.js'; -import utils, { getEdgeId } from '../../utils.js'; -import { interpolateToCurve, getStylesFromArray } from '../../utils.js'; -import { setupGraphViewbox } from '../../setupGraphViewbox.js'; -import common from '../common/common.js'; -import type { ClassRelation, ClassMap, ClassNoteMap, NamespaceMap } from './classTypes.js'; -import type { EdgeData } from '../../types.js'; - -const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig()); - -let conf = { - dividerMargin: 10, - padding: 5, - textHeight: 10, - curve: undefined, -}; - -interface RectParameters { - id: string; - shape: 'rect'; - labelStyle: string; - domId: string; - labelText: string; - padding: number | undefined; - style?: string; -} - -/** - * Function that adds the vertices found during parsing to the graph to be rendered. - * - * @param namespaces - Object containing the vertices. - * @param g - The graph that is to be drawn. - * @param _id - id of the graph - * @param diagObj - The diagram object - */ -export const addNamespaces = function ( - namespaces: NamespaceMap, - g: graphlib.Graph, - _id: string, - diagObj: any -) { - log.info('keys:', [...namespaces.keys()]); - log.info(namespaces); - - const hierarchical = getConfig().class?.hierarchicalNamespaces ?? true; - const classes: ClassMap = diagObj.db.getClasses(); - const relations: ClassRelation[] = diagObj.db.getRelations(); - const notes: ClassNoteMap = diagObj.db.getNotes(); - - // Walks up the namespace chain to the nearest ancestor (or self) flagged as - // user-declared. Used in compact mode to reassign descendants of implicit - // intermediate namespaces onto the nearest explicit box. - const resolveExplicitAncestor = (parentId?: string): string | undefined => { - let current = parentId; - while (current) { - const ns = namespaces.get(current); - if (!ns) { - return undefined; - } - if (ns.explicit) { - return current; - } - current = ns.parent; - } - return undefined; - }; - - // In compact mode, collapse each child's parent to its nearest explicit ancestor - // so addClasses/addNotes' filter-by-parent logic naturally groups them. - if (!hierarchical) { - classes.forEach((cls) => { - cls.parent = resolveExplicitAncestor(cls.parent); - }); - notes.forEach((note) => { - note.parent = resolveExplicitAncestor(note.parent); - }); - } - - // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition - namespaces.forEach(function (vertex) { - // Compact mode skips auto-created intermediate namespaces entirely - if (!hierarchical && !vertex.explicit) { - return; - } - - // parent node must be one of [rect, roundedWithTitle, noteGroup, divider] - const shape = 'rect'; - - const node: RectParameters = { - shape: shape, - id: vertex.id, - domId: vertex.domId, - labelText: sanitizeText(hierarchical ? vertex.label : vertex.id), - labelStyle: '', - style: 'fill: none; stroke: black', - // TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release - padding: getConfig().flowchart?.padding ?? getConfig().class?.padding, - }; - - g.setNode(vertex.id, node); - - if (hierarchical && vertex.parent && g.hasNode(vertex.parent)) { - g.setParent(vertex.id, vertex.parent); - } - - if (hierarchical) { - addClasses(vertex.classes, g, _id, diagObj, vertex.id); - addNotes(vertex.notes, g, relations.length + 1, classes, vertex.id); - } else { - // Classes/notes have already been reassigned above; use the global maps so - // descendants of implicit ancestors get picked up here. - addClasses(classes, g, _id, diagObj, vertex.id); - addNotes(notes, g, relations.length + 1, classes, vertex.id); - } - - log.info('setNode', node); - }); -}; - -/** - * Function that adds the vertices found during parsing to the graph to be rendered. - * - * @param classes - Object containing the vertices. - * @param g - The graph that is to be drawn. - * @param _id - id of the graph - * @param diagObj - The diagram object - * @param parent - id of the parent namespace, if it exists - */ -export const addClasses = function ( - classes: ClassMap, - g: graphlib.Graph, - _id: string, - diagObj: any, - parent?: string -) { - log.info('keys:', [...classes.keys()]); - log.info(classes); - - // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition - [...classes.values()] - .filter((vertex) => vertex.parent === parent) - .forEach(function (vertex) { - /** - * Variable for storing the classes for the vertex - */ - const cssClassStr = vertex.cssClasses.join(' '); - - const styles = getStylesFromArray(vertex.styles); - - // Use vertex id as text in the box if no text is provided by the graph definition - const vertexText = vertex.label ?? vertex.id; - const radius = 0; - const shape = 'class_box'; - - // Add the node - const node = { - labelStyle: styles.labelStyle, - shape: shape, - labelText: sanitizeText(vertexText), - classData: vertex, - rx: radius, - ry: radius, - class: cssClassStr, - style: styles.style, - id: vertex.id, - domId: vertex.domId, - tooltip: diagObj.db.getTooltip(vertex.id, parent) || '', - haveCallback: vertex.haveCallback, - link: vertex.link, - width: vertex.type === 'group' ? 500 : undefined, - type: vertex.type, - // TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release - padding: getConfig().flowchart?.padding ?? getConfig().class?.padding, - }; - g.setNode(vertex.id, node); - - if (parent) { - g.setParent(vertex.id, parent); - } - - log.info('setNode', node); - }); -}; - -/** - * Function that adds the additional vertices (notes) found during parsing to the graph to be rendered. - * - * @param notes - Object containing the additional vertices (notes). - * @param g - The graph that is to be drawn. - * @param startEdgeId - starting index for note edge - * @param classes - Classes - */ -export const addNotes = function ( - notes: ClassNoteMap, - g: graphlib.Graph, - startEdgeId: number, - classes: ClassMap, - parent?: string -) { - log.info(notes); - - [...notes.values()] - .filter((note) => note.parent === parent) - .forEach(function (vertex) { - const cssNoteStr = ''; - - const styles = { labelStyle: '', style: '' }; - - const vertexText = vertex.text; - - const radius = 0; - const shape = 'note'; - const node = { - labelStyle: styles.labelStyle, - shape: shape, - labelText: sanitizeText(vertexText), - noteData: vertex, - rx: radius, - ry: radius, - class: cssNoteStr, - style: styles.style, - id: vertex.id, - domId: vertex.id, - tooltip: '', - type: 'note', - // TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release - padding: getConfig().flowchart?.padding ?? getConfig().class?.padding, - }; - g.setNode(vertex.id, node); - log.info('setNode', node); - - if (parent) { - g.setParent(vertex.id, parent); - } - - if (!vertex.class || !classes.has(vertex.class)) { - return; - } - const edgeId = startEdgeId + vertex.index; - - const edgeData: EdgeData = { - id: `edgeNote${edgeId}`, - //Set relationship style and line type - classes: 'relation', - pattern: 'dotted', - // Set link type for rendering - arrowhead: 'none', - //Set edge extra labels - startLabelRight: '', - endLabelLeft: '', - //Set relation arrow types - arrowTypeStart: 'none', - arrowTypeEnd: 'none', - style: 'fill:none', - labelStyle: '', - curve: interpolateToCurve(conf.curve, curveLinear), - }; - - // Add the edge to the graph - g.setEdge(vertex.id, vertex.class, edgeData, edgeId); - }); -}; - -/** - * Add edges to graph based on parsed graph definition - * - * @param relations - - * @param g - The graph object - */ -export const addRelations = function (relations: ClassRelation[], g: graphlib.Graph) { - const conf = getConfig().flowchart; - let cnt = 0; - - relations.forEach(function (edge) { - cnt++; - const edgeData: EdgeData = { - //Set relationship style and line type - classes: 'relation', - pattern: edge.relation.lineType == 1 ? 'dashed' : 'solid', - id: getEdgeId(edge.id1, edge.id2, { - prefix: 'id', - counter: cnt, - }), - // Set link type for rendering - arrowhead: edge.type === 'arrow_open' ? 'none' : 'normal', - //Set edge extra labels - startLabelRight: edge.relationTitle1 === 'none' ? '' : edge.relationTitle1, - endLabelLeft: edge.relationTitle2 === 'none' ? '' : edge.relationTitle2, - //Set relation arrow types - arrowTypeStart: getArrowMarker(edge.relation.type1), - arrowTypeEnd: getArrowMarker(edge.relation.type2), - style: 'fill:none', - labelStyle: '', - curve: interpolateToCurve(conf?.curve, curveLinear), - }; - - log.info(edgeData, edge); - - if (edge.style !== undefined) { - const styles = getStylesFromArray(edge.style); - edgeData.style = styles.style; - edgeData.labelStyle = styles.labelStyle; - } - - edge.text = edge.title; - if (edge.text === undefined) { - if (edge.style !== undefined) { - edgeData.arrowheadStyle = 'fill: #333'; - } - } else { - edgeData.arrowheadStyle = 'fill: #333'; - edgeData.labelpos = 'c'; - - if (getEffectiveHtmlLabels(getConfig())) { - edgeData.labelType = 'html'; - edgeData.label = '' + edge.text + ''; - } else { - edgeData.labelType = 'text'; - 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:'); - } - } - // Add the edge to the graph - g.setEdge(edge.id1, edge.id2, edgeData, cnt); - }); -}; - -/** - * Merges the value of `conf` with the passed `cnf` - * - * @param cnf - Config to merge - */ -export const setConf = function (cnf: any) { - conf = { - ...conf, - ...cnf, - }; -}; - -/** - * Draws a class diagram in the tag with id: id based on the definition in text. - * - * @param text - - * @param id - - * @param _version - - * @param diagObj - - */ -export const draw = async function (text: string, id: string, _version: string, diagObj: any) { - log.info('Drawing class - ', id); - - // TODO V10: Why flowchart? Might be a mistake when copying. - const conf = getConfig().flowchart ?? getConfig().class; - const securityLevel = getConfig().securityLevel; - log.info('config:', conf); - const nodeSpacing = conf?.nodeSpacing ?? 50; - const rankSpacing = conf?.rankSpacing ?? 50; - - // Create the input mermaid.graph - const g: graphlib.Graph = new graphlib.Graph({ - multigraph: true, - compound: true, - }) - .setGraph({ - rankdir: diagObj.db.getDirection(), - nodesep: nodeSpacing, - ranksep: rankSpacing, - marginx: 8, - marginy: 8, - }) - .setDefaultEdgeLabel(function () { - return {}; - }); - - // Fetch the vertices/nodes and edges/links from the parsed graph definition - const namespaces: NamespaceMap = diagObj.db.getNamespaces(); - const classes: ClassMap = diagObj.db.getClasses(); - const relations: ClassRelation[] = diagObj.db.getRelations(); - const notes: ClassNoteMap = diagObj.db.getNotes(); - log.info(relations); - addNamespaces(namespaces, g, id, diagObj); - addClasses(classes, g, id, diagObj); - addRelations(relations, g); - addNotes(notes, g, relations.length + 1, classes); - - // Set up an SVG group so that we can translate the final graph. - let sandboxElement; - if (securityLevel === 'sandbox') { - sandboxElement = select('#i' + id); - } - const root = - securityLevel === 'sandbox' - ? select(sandboxElement.nodes()[0]!.contentDocument.body) - : select('body'); - const svg = root.select(`[id="${id}"]`); - - // Run the renderer. This is what draws the final graph. - const element = root.select('#' + id + ' g'); - await render( - element, - g, - ['aggregation', 'extension', 'composition', 'dependency', 'lollipop'], - 'classDiagram', - id - ); - - utils.insertTitle(svg, 'classTitleText', conf?.titleTopMargin ?? 5, diagObj.db.getDiagramTitle()); - - setupGraphViewbox(g, svg, conf?.diagramPadding, conf?.useMaxWidth); - - // Add label rects for non html labels - if (!conf?.htmlLabels) { - const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0]!.contentDocument : document; - const labels = doc.querySelectorAll('[id="' + id + '"] .edgeLabel .label'); - for (const label of labels) { - // Get dimensions of label - const dim = label.getBBox(); - - const rect = doc.createElementNS('http://www.w3.org/2000/svg', 'rect'); - rect.setAttribute('rx', 0); - rect.setAttribute('ry', 0); - rect.setAttribute('width', dim.width); - rect.setAttribute('height', dim.height); - - label.insertBefore(rect, label.firstChild); - } - } -}; - -/** - * Gets the arrow marker for a type index - * - * @param type - The type to look for - * @returns The arrow marker - */ -function getArrowMarker(type: number) { - let marker; - switch (type) { - case 0: - marker = 'aggregation'; - break; - case 1: - marker = 'extension'; - break; - case 2: - marker = 'composition'; - break; - case 3: - marker = 'dependency'; - break; - case 4: - marker = 'lollipop'; - break; - default: - marker = 'none'; - } - return marker; -} - -export default { - setConf, - draw, -}; diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index ffcf69e1733..a24b98ee942 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -846,7 +846,7 @@ graph TD;A--x|text including URL space|B;`) // We have to have both the specific textDiagramType and the expected type name because the expected type may be slightly different from what is put in the diagram text (ex: in -v2 diagrams) const diagramTypesAndExpectations = [ // { textDiagramType: 'C4Context', expectedType: 'c4' }, TODO : setAccTitle not called in C4 jison parser - { textDiagramType: 'classDiagram', expectedType: 'class' }, + { textDiagramType: 'classDiagram', expectedType: 'classDiagram' }, { textDiagramType: 'classDiagram-v2', expectedType: 'classDiagram' }, { textDiagramType: 'erDiagram', expectedType: 'er' }, { textDiagramType: 'graph', expectedType: 'flowchart-v2' }, diff --git a/packages/mermaid/src/rendering-util/createGraph.ts b/packages/mermaid/src/rendering-util/createGraph.ts index cf7b4b424c2..f0723fcc878 100644 --- a/packages/mermaid/src/rendering-util/createGraph.ts +++ b/packages/mermaid/src/rendering-util/createGraph.ts @@ -1,6 +1,6 @@ import type { Selection } from 'd3'; import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; -import type { LayoutData } from './types.js'; +import type { ClusterNode, LayoutData, NonClusterNode, ShapeRenderOptions } from './types.js'; import { getConfig } from '../diagram-api/diagramAPI.js'; import { hasEdgeLabel, insertEdgeLabel } from './rendering-elements/edges.js'; import { insertNode } from './rendering-elements/nodes.js'; @@ -14,6 +14,56 @@ type D3Selection = Selection< unknown >; +interface LayoutElementGroups { + clusters: D3Selection; + edgePaths: D3Selection; + edgeLabels: D3Selection; + nodes: D3Selection; + rootGroups: D3Selection; +} + +export interface CreateLayoutElementGroupsOptions { + edgePathsClass?: string; +} + +export function createLayoutElementGroups( + element: D3Selection, + { edgePathsClass = 'edges edgePath' }: CreateLayoutElementGroupsOptions = {} +): LayoutElementGroups { + const rootGroups = element.insert('g').attr('class', 'root'); + const clusters = rootGroups.insert('g').attr('class', 'clusters'); + const edgePaths = rootGroups.insert('g').attr('class', edgePathsClass); + const edgeLabels = rootGroups.insert('g').attr('class', 'edgeLabels'); + const nodes = rootGroups.insert('g').attr('class', 'nodes'); + + return { clusters, edgePaths, edgeLabels, nodes, rootGroups }; +} + +export async function measureGroupLabel( + nodesGroup: D3Selection, + node: ClusterNode +): Promise { + if (node.label) { + const { shapeSvg, bbox } = await labelHelper(nodesGroup, node); + node.labelBBox = { width: bbox.width, height: bbox.height }; + shapeSvg.remove(); + } else { + node.labelBBox = { width: 0, height: 0 }; + } +} + +export async function insertMeasuredNode( + nodesGroup: D3Selection, + node: NonClusterNode, + renderOptions: ShapeRenderOptions +): Promise> { + const childNodeEl = await insertNode(nodesGroup, node, renderOptions); + const boundingBox = childNodeEl.node()?.getBBox() ?? { width: 0, height: 0 }; + node.width = boundingBox.width; + node.height = boundingBox.height; + return childNodeEl as D3Selection; +} + /** * Creates a graph by merging the graph construction and DOM element insertion. * @@ -46,12 +96,8 @@ export async function createGraphWithElements( }); const edgesToProcess = [...data4Layout.edges]; const config = getConfig(); - // Create groups for clusters, edge paths, edge labels, and nodes. - const rootGroups = element.insert('g').attr('class', 'root'); - const clusters = rootGroups.insert('g').attr('class', 'clusters'); - const edgePaths = rootGroups.insert('g').attr('class', 'edges edgePath'); - const edgeLabels = rootGroups.insert('g').attr('class', 'edgeLabels'); - const nodesGroup = rootGroups.insert('g').attr('class', 'nodes'); + const groups = createLayoutElementGroups(element); + const { edgeLabels, nodes: nodesGroup } = groups; const nodeElements = new Map>(); @@ -67,22 +113,16 @@ export async function createGraphWithElements( data4Layout.nodes.map(async (node) => { if (node.isGroup) { if (hasDom) { - if (node.label) { - const { shapeSvg, bbox } = await labelHelper(nodesGroup, node); - node.labelBBox = { width: bbox.width, height: bbox.height }; - shapeSvg.remove(); - } else { - node.labelBBox = { width: 0, height: 0 }; - } + await measureGroupLabel(nodesGroup, node); } graph.setNode(node.id, { ...node }); } else { if (hasDom) { - const childNodeEl = await insertNode(nodesGroup, node, { config, dir: node.dir }); - const boundingBox = childNodeEl.node()?.getBBox() ?? { width: 0, height: 0 }; - nodeElements.set(node.id, childNodeEl as D3Selection); - node.width = boundingBox.width; - node.height = boundingBox.height; + const childNodeEl = await insertMeasuredNode(nodesGroup, node, { + config, + dir: node.dir, + }); + nodeElements.set(node.id, childNodeEl); } graph.setNode(node.id, { ...node }); } @@ -114,7 +154,7 @@ export async function createGraphWithElements( return { graph, - groups: { clusters, edgePaths, edgeLabels, nodes: nodesGroup, rootGroups }, + groups, nodeElements, }; } diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/common/index.spec.ts b/packages/mermaid/src/rendering-util/layout-algorithms/common/index.spec.ts index d2cbfa5d35e..29bf4bb6ed0 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/common/index.spec.ts +++ b/packages/mermaid/src/rendering-util/layout-algorithms/common/index.spec.ts @@ -308,8 +308,10 @@ describe('paintLayoutData', () => { edges: [layoutOnlyEdge, skippedEdge, renderEdge], }); const measured = measure(); + const clusterDb = new Map([[group.id, { node: group }]]); await paintLayoutData(data, { measure: measured } as never, { + clusterDb, skipEdge: (candidate) => candidate.id === skippedEdge.id, skipIntersect: (candidate) => candidate.id === renderEdge.id, }); @@ -321,7 +323,7 @@ describe('paintLayoutData', () => { expect(mocks.insertEdge).toHaveBeenCalledWith( measured.groups.edgePaths, { ...renderEdge }, - {}, + clusterDb, data.type, nodeA, nodeB, @@ -330,6 +332,87 @@ describe('paintLayoutData', () => { ); }); + it('positions pre-rendered cluster nodes without inserting another cluster', async () => { + const { paintLayoutData } = await import('./index.js'); + const clusterNode = node('G', { clusterNode: true, isGroup: true } as never); + const data = layout({ + nodes: [clusterNode], + edges: [], + }); + const measured = measure(); + + await paintLayoutData(data, { measure: measured } as never); + + expect(mocks.insertCluster).not.toHaveBeenCalled(); + expect(mocks.positionNode).toHaveBeenCalledWith(clusterNode); + }); + + it('lets layout algorithms decide which group nodes are clusters', async () => { + const { paintLayoutData } = await import('./index.js'); + const leafGroup = node('G', { isGroup: true, shape: 'roundedWithTitle' }); + const data = layout({ + nodes: [leafGroup], + edges: [], + }); + const measured = measure(); + + await paintLayoutData(data, { measure: measured } as never, { + isCluster: () => false, + }); + + expect(mocks.insertCluster).not.toHaveBeenCalled(); + expect(mocks.positionNode).toHaveBeenCalledWith(leafGroup); + }); + + it('lets layout algorithms skip nodes that were already painted elsewhere', async () => { + const { paintLayoutData } = await import('./index.js'); + const rootNode = node('root'); + const extractedNode = node('nested'); + const data = layout({ + nodes: [rootNode, extractedNode], + edges: [], + }); + const measured = measure(); + + await paintLayoutData(data, { measure: measured } as never, { + skipNode: (candidate) => candidate.id === extractedNode.id, + }); + + expect(mocks.positionNode).toHaveBeenCalledTimes(1); + expect(mocks.positionNode).toHaveBeenCalledWith(rootNode); + expect(mocks.positionNode).not.toHaveBeenCalledWith(extractedNode); + }); + + it('lets layout algorithms provide graph-backed paint nodes and edge endpoints', async () => { + const { paintLayoutData } = await import('./index.js'); + const nodeB = node('B'); + const graphOnlyNode = node('dummy'); + const renderEdge = edge('render', { start: graphOnlyNode.id, end: nodeB.id }); + const data = layout({ + nodes: [nodeB], + edges: [renderEdge], + }); + const measured = measure(); + + await paintLayoutData(data, { measure: measured } as never, { + getNodes: () => [graphOnlyNode, nodeB], + getEdgeNode: (id) => (id === graphOnlyNode.id ? graphOnlyNode : undefined), + }); + + expect(mocks.positionNode).toHaveBeenNthCalledWith(1, graphOnlyNode); + expect(mocks.positionNode).toHaveBeenNthCalledWith(2, nodeB); + expect(mocks.insertEdge).toHaveBeenCalledWith( + measured.groups.edgePaths, + { ...renderEdge }, + expect.any(Map), + data.type, + graphOnlyNode, + nodeB, + data.diagramId, + false + ); + }); + it('inserts and positions edge labels after drawing labelled edges', async () => { const { paintLayoutData } = await import('./index.js'); const labelledEdge = edge('labelled', { label: 'Yes', x: 0, y: 0 } as Partial); diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts b/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts index aaa4eed770a..c772ae48ce5 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts +++ b/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts @@ -17,7 +17,7 @@ import { } from '../../rendering-elements/edges.js'; import insertMarkers from '../../rendering-elements/markers.js'; import { clear as clearNodes, positionNode } from '../../rendering-elements/nodes.js'; -import type { LayoutData, Edge } from '../../types.js'; +import type { LayoutData, Edge, ClusterNode } from '../../types.js'; import type { RenderOptions } from '../../render.js'; import { clear as clearGraphlib } from '../dagre/mermaid-graphlib.js'; @@ -29,6 +29,7 @@ type RenderedEdge = Edge & { endLabelRight?: string; }; type EdgeRenderPath = Parameters[0]; +type ClusterDb = Map>; interface EdgeRenderPaths { originalPath?: EdgeRenderPath; @@ -50,6 +51,24 @@ export interface CommonLayoutPaintContext< } export interface CommonLayoutPaintOptions { + clusterDb?: ClusterDb; + getNodes?: ( + data4Layout: LayoutData, + context: CommonLayoutPaintContext + ) => Iterable; + getEdgeNode?: ( + id: string | undefined, + edge: Edge, + context: CommonLayoutPaintContext + ) => LayoutData['nodes'][number] | object | undefined; + skipNode?: ( + node: LayoutData['nodes'][number], + context: CommonLayoutPaintContext + ) => boolean; + isCluster?: ( + node: LayoutData['nodes'][number], + context: CommonLayoutPaintContext + ) => boolean; skipEdge?: (edge: Edge) => boolean; skipIntersect?: boolean | ((edge: Edge) => boolean); } @@ -177,14 +196,18 @@ export async function defaultMeasureLayout( export async function paintLayoutData( data4Layout: LayoutData, - { measure }: CommonLayoutPaintContext, + context: CommonLayoutPaintContext, options: CommonLayoutPaintOptions = {} ): Promise { + const { measure } = context; const { groups } = measure; // Render clusters and position nodes; this also populates node.intersect on shapes. - for (const node of data4Layout.nodes) { - await paintLayoutNode(groups, node); + for (const node of options.getNodes?.(data4Layout, context) ?? data4Layout.nodes) { + if (options.skipNode?.(node, context)) { + continue; + } + await paintLayoutNode(groups, node, context, options); } const nodeById = buildNodeLookup(data4Layout.nodes); @@ -194,21 +217,33 @@ export async function paintLayoutData( continue; } - await paintLayoutEdge(groups, edge, nodeById, data4Layout, options); + await paintLayoutEdge(groups, edge, nodeById, data4Layout, options, context); } } async function paintLayoutNode( groups: CommonLayoutMeasure['groups'], - node: LayoutData['nodes'][number] + node: LayoutData['nodes'][number], + context: CommonLayoutPaintContext, + options: CommonLayoutPaintOptions ): Promise { - if (node.isGroup) { + if ((node as { clusterNode?: boolean }).clusterNode) { + positionNode(node); + } else if (shouldPaintAsCluster(node, context, options)) { await insertCluster(groups.clusters, node); } else { positionNode(node); } } +function shouldPaintAsCluster( + node: LayoutData['nodes'][number], + context: CommonLayoutPaintContext, + options: CommonLayoutPaintOptions +): node is ClusterNode { + return node.isGroup === true && (options.isCluster?.(node, context) ?? true); +} + function buildNodeLookup(nodes: LayoutData['nodes']): Map { const nodeById = new Map(); for (const node of nodes) { @@ -228,15 +263,16 @@ async function paintLayoutEdge( edge: Edge, nodeById: Map, data4Layout: LayoutData, - options: CommonLayoutPaintOptions + options: CommonLayoutPaintOptions, + context: CommonLayoutPaintContext ): Promise { const paths = insertEdge( groups.edgePaths, { ...edge }, - {}, + options.clusterDb ?? new Map(), data4Layout.type, - getRenderedNode(edge.start, nodeById), - getRenderedNode(edge.end, nodeById), + getRenderedNode(edge.start, edge, nodeById, context, options), + getRenderedNode(edge.end, edge, nodeById, context, options), data4Layout.diagramId, shouldSkipIntersect(edge, options) ) as EdgeRenderPaths | undefined; @@ -251,9 +287,12 @@ async function paintLayoutEdge( function getRenderedNode( id: string | undefined, - nodeById: Map + edge: Edge, + nodeById: Map, + context: CommonLayoutPaintContext, + options: CommonLayoutPaintOptions ): LayoutData['nodes'][number] | object { - return id ? (nodeById.get(id) ?? {}) : {}; + return options.getEdgeNode?.(id, edge, context) ?? (id ? (nodeById.get(id) ?? {}) : {}); } function shouldSkipIntersect(edge: Edge, options: CommonLayoutPaintOptions): boolean { diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js index 52ad710cee0..072e2e30dae 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js +++ b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js @@ -1,6 +1,7 @@ import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js'; import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js'; import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; +import { createLayoutElementGroups, insertMeasuredNode } from '../../createGraph.js'; import { createCommonLayoutRenderer } from '../common/index.js'; import { updateNodeBounds } from '../../rendering-elements/shapes/util.js'; import { @@ -9,7 +10,7 @@ import { findNonClusterChild, sortNodesByHierarchy, } from './mermaid-graphlib.js'; -import { insertNode, positionNode, setNodeElem } from '../../rendering-elements/nodes.js'; +import { positionNode, setNodeElem } from '../../rendering-elements/nodes.js'; import { insertCluster } from '../../rendering-elements/clusters.js'; import { insertEdgeLabel, positionEdgeLabel, insertEdge } from '../../rendering-elements/edges.js'; import { log } from '../../../logger.js'; @@ -32,10 +33,28 @@ const getDefaultSelfLoopSide = (rankdir = 'TB') => { } }; -// Class diagrams also use dagre, but self-referential multiplicity labels rely on -// the existing segmented self-loop rendering path for terminal label placement. const shouldMergeSelfLoopSegments = (diagramType) => - diagramType === 'flowchart' || diagramType === 'flowchart-v2' || diagramType === 'stateDiagram'; + diagramType === 'flowchart' || + diagramType === 'flowchart-v2' || + diagramType === 'stateDiagram' || + diagramType === 'er' || + diagramType === 'classDiagram'; + +// Keep this a fixed literal allowlist — never derive keys from graph data, since node ids and +// other graph keys are user-controlled and copying them wholesale onto LayoutData would open a +// property-injection surface. `intersect`/`calcIntersect` are function references installed by +// the shape handlers; the painter needs them alongside the geometry. +const DAGRE_NODE_LAYOUT_PROPERTIES = [ + 'x', + 'y', + 'width', + 'height', + 'labelBBox', + 'intersect', + 'calcIntersect', + 'diff', + 'clusterNode', +]; // Use dagre's dummy self-loop placement as a hint, so loops are not always forced above the node. const getSelfLoopSide = (graph, node, segments, originalNodeId, rankdir) => { @@ -236,19 +255,27 @@ export const getEdgesToRender = (graph, yOffset = 0, { mergeSelfLoops = true } = return edgesToRender; }; -const measureAndRunDagreLayoutCore = async ( - _elem, +const measureDagreGraph = async ({ + element: _elem, graph, diagramType, id, parentCluster, - siteConfig -) => { + siteConfig, +}) => { log.warn('Graph in recursive render:XAX', graphlibJson.write(graph), parentCluster); const dir = graph.graph().rankdir; log.trace('Dir in recursive render - dir:', dir); - const elem = _elem.insert('g').attr('class', 'root'); + const { + clusters, + edgePaths, + edgeLabels, + nodes, + rootGroups: elem, + } = createLayoutElementGroups(_elem, { + edgePathsClass: 'edgePaths', + }); if (!graph.nodes()) { log.info('No nodes found for', graph); } else { @@ -257,10 +284,6 @@ const measureAndRunDagreLayoutCore = async ( if (graph.edges().length > 0) { log.info('Recursive edges', graph.edge(graph.edges()[0])); } - const clusters = elem.insert('g').attr('class', 'clusters'); - const edgePaths = elem.insert('g').attr('class', 'edgePaths'); - const edgeLabels = elem.insert('g').attr('class', 'edgeLabels'); - const nodes = elem.insert('g').attr('class', 'nodes'); const mergeSelfLoops = shouldMergeSelfLoopSegments(diagramType); // Insert nodes, this will insert them into the dom and each node will get a size. The size is updated @@ -300,14 +323,14 @@ const measureAndRunDagreLayoutCore = async ( }); // "o" will contain the full cluster not just the children - const o = await recursiveRender( - nodes, - node.graph, + const o = await renderDagreSubgraph({ + element: nodes, + graph: node.graph, diagramType, id, - graph.node(v), - siteConfig - ); + parentCluster: graph.node(v), + siteConfig, + }); const newEl = o.elem; updateNodeBounds(node, newEl); // node.height = o.diff; @@ -342,7 +365,7 @@ const measureAndRunDagreLayoutCore = async ( // insertCluster(clusters, graph.node(v)); } else { log.trace('Node - the non recursive path XAX', v, nodes, graph.node(v), dir); - await insertNode(nodes, graph.node(v), { config: siteConfig, dir }); + await insertMeasuredNode(nodes, graph.node(v), { config: siteConfig, dir }); } } }) @@ -370,10 +393,19 @@ const measureAndRunDagreLayoutCore = async ( if (edge.selfLoop.order !== 1) { return; } - const segmentId = edge.id; - edge.id = edge.selfLoop.id; - await insertEdgeLabel(edgeLabels, edge); - edge.id = segmentId; + const labelEdge = { + ...edge.originalEdge, + ...edge, + id: edge.selfLoop.id, + startLabelLeft: edge.originalEdge?.startLabelLeft ?? edge.startLabelLeft, + startLabelRight: edge.originalEdge?.startLabelRight ?? edge.startLabelRight, + endLabelLeft: edge.originalEdge?.endLabelLeft ?? edge.endLabelLeft, + endLabelRight: edge.originalEdge?.endLabelRight ?? edge.endLabelRight, + }; + await insertEdgeLabel(edgeLabels, labelEdge); + edge.width = labelEdge.width; + edge.height = labelEdge.height; + edge.labelStyle = labelEdge.labelStyle; return; } await insertEdgeLabel(edgeLabels, edge); @@ -384,6 +416,19 @@ const measureAndRunDagreLayoutCore = async ( await processEdges(); + const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig); + return { + elem, + graph, + groups: { clusters, edgePaths, edgeLabels, nodes, rootGroups: elem }, + diagramType, + id, + mergeSelfLoops, + subGraphTitleTotalMargin, + }; +}; + +const runDagreGraphLayout = (graph) => { log.info('Graph before layout:', JSON.stringify(graphlibJson.write(graph))); log.info('############################################# XXX'); @@ -393,17 +438,81 @@ const measureAndRunDagreLayoutCore = async ( dagreLayout(graph); log.info('Graph after layout:', JSON.stringify(graphlibJson.write(graph))); +}; - const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig); - return { - elem, - graph, - groups: { clusters, edgePaths }, - diagramType, - id, - mergeSelfLoops, - subGraphTitleTotalMargin, - }; +const normalizeDagreNode = (graph, nodeId, subGraphTitleTotalMargin) => { + const node = graph.node(nodeId); + if (!node) { + return undefined; + } + + const normalizedNode = { ...node }; + if (node?.clusterNode) { + normalizedNode.y = (node.y ?? 0) + subGraphTitleTotalMargin; + } else if (graph.children(nodeId).length > 0) { + normalizedNode.height = (node.height ?? 0) + subGraphTitleTotalMargin; + } else { + normalizedNode.y = (node.y ?? 0) + subGraphTitleTotalMargin / 2; + } + return normalizedNode; +}; + +const applyDagreNodeLayout = (targetNode, dagreNode) => { + DAGRE_NODE_LAYOUT_PROPERTIES.forEach((property) => { + if (dagreNode[property] !== undefined) { + targetNode[property] = dagreNode[property]; + } + }); +}; + +const normalizeDagreEdge = (edge, start, end, edgeOffsetY) => ({ + ...edge, + start: edge.start ?? start, + end: edge.end ?? end, + points: (edge.points ?? []).map((point) => ({ + ...point, + y: typeof point.y === 'number' ? point.y + edgeOffsetY : point.y, + })), +}); + +/** + * Copy the Dagre layout results back onto LayoutData. + * + * Known limitations — the normalization only covers the top-level graph: + * - Recursive clusters: `adjustClustersAndEdges` moves cluster children and intra-cluster edges + * into the nested `node.graph`, where they are laid out and painted during the recursive + * measure pass in their own coordinate space. Those nodes keep their pre-layout LayoutData + * values, and intra-cluster edges are absent from the normalized `data4Layout.edges`. + * - Self-loops on diagram types where segments are not merged: the three `*-cyclic-special-*` + * layout segments replace the original self-edge in `data4Layout.edges`, since they are what + * is actually rendered. + * Consumers of the normalized LayoutData must not assume it is a complete description of the + * rendered output for cluster diagrams — the recursive paint path owns the nested content. + */ +export const applyDagreLayoutResult = (data4Layout, measuredLayout) => { + const { graph, mergeSelfLoops, subGraphTitleTotalMargin = 0 } = measuredLayout; + const nodeById = new Map(data4Layout.nodes.map((node) => [node.id, node])); + + sortNodesByHierarchy(graph).forEach((nodeId) => { + const dagreNode = normalizeDagreNode(graph, nodeId, subGraphTitleTotalMargin); + if (!dagreNode) { + return; + } + + applyDagreNodeLayout(graph.node(nodeId), dagreNode); + + const targetNode = nodeById.get(nodeId); + if (targetNode) { + applyDagreNodeLayout(targetNode, dagreNode); + } + }); + + const edgeOffsetY = subGraphTitleTotalMargin / 2; + data4Layout.edges = getEdgesToRender(graph, edgeOffsetY, { mergeSelfLoops }).map( + ({ edge, start, end }) => normalizeDagreEdge(edge, start, end, edgeOffsetY) + ); + + return data4Layout; }; const paintDagreLayoutCore = async ({ @@ -519,8 +628,11 @@ const paintDagreLayoutCore = async ({ return { elem, diff }; }; -const recursiveRender = async (...args) => - await paintDagreLayoutCore(await measureAndRunDagreLayoutCore(...args)); +const renderDagreSubgraph = async (options) => { + const measuredLayout = await measureDagreGraph(options); + runDagreGraphLayout(measuredLayout.graph); + return await paintDagreLayoutCore(measuredLayout); +}; export const prepareLayoutForDagre = (data4Layout) => { const graph = new graphlib.Graph({ @@ -637,28 +749,51 @@ export const prepareLayoutForDagre = (data4Layout) => { return { graph }; }; -export const runDagreLayoutCore = async (data4Layout, { element, preparedLayout }) => { - const { graph } = preparedLayout ?? prepareLayoutForDagre(data4Layout); +export const measureDagreLayout = async (data4Layout, { element, preparedLayout }) => { + const prepared = preparedLayout ?? prepareLayoutForDagre(data4Layout); const siteConfig = getConfig(); - return await measureAndRunDagreLayoutCore( + const measuredLayout = await measureDagreGraph({ element, - graph, - data4Layout.type, - data4Layout.diagramId, - undefined, - siteConfig - ); + graph: prepared.graph, + diagramType: data4Layout.type, + id: data4Layout.diagramId, + parentCluster: undefined, + siteConfig, + }); + prepared.measuredLayout = measuredLayout; + return measuredLayout; }; -export const paintDagreLayout = async (_data4Layout, _context, coreResult) => { - await paintDagreLayoutCore(coreResult); +export const runDagreLayoutCore = (data4Layout, context) => { + const measuredLayout = context.preparedLayout?.measuredLayout; + + if (!measuredLayout) { + throw new Error('runDagreLayoutCore requires measureDagreLayout to run first'); + } + + runDagreGraphLayout(measuredLayout.graph); + applyDagreLayoutResult(data4Layout, measuredLayout); + return measuredLayout; }; -const measureDagreLayout = () => Promise.resolve(undefined); +const getDagrePaintNodes = (_data4Layout, { measure }) => + sortNodesByHierarchy(measure.graph) + .map((nodeId) => measure.graph.node(nodeId)) + .filter(Boolean); + +const getDagreEdgeNode = (nodeId, _edge, { measure }) => + nodeId ? measure.graph.node(nodeId) : undefined; export const render = createCommonLayoutRenderer({ prepareLayout: prepareLayoutForDagre, measureLayout: measureDagreLayout, runLayoutCore: runDagreLayoutCore, - paintLayout: paintDagreLayout, + paintOptions: { + clusterDb, + getNodes: getDagrePaintNodes, + getEdgeNode: getDagreEdgeNode, + skipNode: (node, { measure }) => !measure.graph.hasNode(node.id), + isCluster: (node, { measure }) => + measure.graph.hasNode(node.id) && (measure.graph.children(node.id) ?? []).length > 0, + }, }); diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.spec.js b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.spec.js index 7f2de9454b6..b9e70fb20a7 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.spec.js +++ b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.spec.js @@ -1,7 +1,14 @@ import { JSDOM } from 'jsdom'; import { describe, it, expect, beforeAll } from 'vitest'; +import { select } from 'd3'; import { Graph } from 'dagre-d3-es/src/graphlib/index.js'; -import { getEdgesToRender } from './index.js'; +import { + applyDagreLayoutResult, + getEdgesToRender, + measureDagreLayout, + prepareLayoutForDagre, + runDagreLayoutCore, +} from './index.js'; import mermaid from '../../../mermaid.js'; import { mermaidAPI } from '../../../mermaidAPI.js'; @@ -10,6 +17,7 @@ const setupDom = () => { const oldDocument = globalThis.document; const oldMutationObserver = globalThis.MutationObserver; const dom = new JSDOM('
', { + url: 'http://localhost/', resources: 'usable', beforeParse(window) { window.Element.prototype.getBBox = () => ({ @@ -33,6 +41,19 @@ const setupDom = () => { }; }; +const getCyclicPaths = (document) => + [...document.querySelectorAll('.edgePaths path')].filter((path) => + path.getAttribute('data-id')?.includes('cyclic-special') + ); + +const expectFinitePaths = (paths) => { + for (const path of paths) { + const pathData = path.getAttribute('d'); + expect(pathData).toBeTruthy(); + expect(pathData).not.toMatch(/NaN|undefined/); + } +}; + describe('getEdgesToRender', () => { beforeAll(async () => { await mermaid.registerExternalDiagrams([]); @@ -44,6 +65,180 @@ describe('getEdgesToRender', () => { }); }); + it('copies DAGRE node and edge layout back onto LayoutData', () => { + const graph = new Graph({ multigraph: true, compound: true }); + graph.setGraph({ rankdir: 'TB' }); + graph.setNode('A', { id: 'A', x: 10, y: 20, width: 30, height: 40 }); + graph.setNode('B', { id: 'B', x: 100, y: 120, width: 30, height: 40 }); + graph.setEdge( + 'A', + 'B', + { + id: 'A-B', + start: 'A', + end: 'B', + points: [ + { x: 10, y: 20 }, + { x: 100, y: 120 }, + ], + x: 55, + y: 70, + }, + 'A-B' + ); + const data4Layout = { + nodes: [ + { id: 'A', isGroup: false }, + { id: 'B', isGroup: false }, + ], + edges: [{ id: 'A-B', start: 'A', end: 'B' }], + }; + + applyDagreLayoutResult(data4Layout, { + graph, + mergeSelfLoops: true, + subGraphTitleTotalMargin: 10, + }); + + expect(data4Layout.nodes[0]).toMatchObject({ x: 10, y: 25, width: 30, height: 40 }); + expect(data4Layout.nodes[1]).toMatchObject({ x: 100, y: 125, width: 30, height: 40 }); + expect(graph.node('A')).toMatchObject({ x: 10, y: 25, width: 30, height: 40 }); + expect(graph.node('B')).toMatchObject({ x: 100, y: 125, width: 30, height: 40 }); + expect(data4Layout.edges).toEqual([ + expect.objectContaining({ + id: 'A-B', + start: 'A', + end: 'B', + points: [ + { x: 10, y: 25 }, + { x: 100, y: 125 }, + ], + x: 55, + y: 70, + }), + ]); + }); + + it('normalizes merged self-loops back onto LayoutData', () => { + const graph = new Graph({ multigraph: true, compound: true }); + graph.setNode('A', { id: 'A', x: 10, y: 10, width: 20, height: 20 }); + graph.setNode('A---A---1', { id: 'A---A---1' }); + graph.setNode('A---A---2', { id: 'A---A---2' }); + + const originalEdge = { + id: 'A-A', + start: 'A', + end: 'A', + label: 'loop', + }; + const segment1 = { + ...originalEdge, + id: 'A-cyclic-special-1', + selfLoop: { id: 'A-A', order: 0 }, + originalEdge, + points: [], + }; + const segmentMid = { + ...originalEdge, + id: 'A-cyclic-special-mid', + selfLoop: { id: 'A-A', order: 1 }, + originalEdge, + points: [], + }; + const segment2 = { + ...originalEdge, + id: 'A-cyclic-special-2', + selfLoop: { id: 'A-A', order: 2 }, + originalEdge, + points: [], + }; + + graph.setEdge('A', 'A---A---1', segment1, 'A-cyclic-special-0'); + graph.setEdge('A---A---1', 'A---A---2', segmentMid, 'A-cyclic-special-1'); + graph.setEdge('A---A---2', 'A', segment2, 'A-cyclic-special-2'); + const data4Layout = { + nodes: [{ id: 'A', isGroup: false }], + edges: [originalEdge], + }; + + applyDagreLayoutResult(data4Layout, { + graph, + mergeSelfLoops: true, + subGraphTitleTotalMargin: 0, + }); + + expect(data4Layout.edges).toHaveLength(1); + expect(data4Layout.edges[0]).toMatchObject({ + id: 'A-A', + start: 'A', + end: 'A', + label: 'loop', + points: [ + { x: -8, y: 0 }, + { x: -8, y: -24 }, + { x: 28, y: -24 }, + { x: 28, y: 0 }, + ], + }); + expect(data4Layout.edges[0].selfLoop).toBeUndefined(); + }); + + // Documents a known limitation: normalization only covers the top-level graph. Cluster + // children and intra-cluster edges live in the nested cluster graph (own coordinate space, + // rendered by the recursive paint path) and are not written back onto LayoutData. + it('leaves recursive cluster children untouched and drops intra-cluster edges from LayoutData', () => { + const graph = new Graph({ multigraph: true, compound: true }); + graph.setGraph({ rankdir: 'TB' }); + // Top-level graph after cluster extraction: the cluster is a single clusterNode whose + // children were moved into its nested graph. + graph.setNode('cluster1', { + id: 'cluster1', + clusterNode: true, + x: 50, + y: 60, + width: 80, + height: 70, + }); + graph.setNode('X', { id: 'X', x: 50, y: 200, width: 30, height: 40 }); + graph.setEdge( + 'X', + 'cluster1', + { + id: 'X-cluster1', + start: 'X', + end: 'cluster1', + points: [{ x: 50, y: 150 }], + }, + 'X-cluster1' + ); + const data4Layout = { + nodes: [ + { id: 'cluster1', isGroup: true }, + { id: 'X', isGroup: false }, + { id: 'child1', isGroup: false, parentId: 'cluster1' }, + ], + edges: [ + { id: 'X-cluster1', start: 'X', end: 'cluster1' }, + { id: 'child1-child2', start: 'child1', end: 'child2' }, + ], + }; + + applyDagreLayoutResult(data4Layout, { + graph, + mergeSelfLoops: true, + subGraphTitleTotalMargin: 10, + }); + + // Top-level nodes are normalized (cluster gets the full margin, regular nodes half). + expect(data4Layout.nodes[0]).toMatchObject({ x: 50, y: 70, width: 80, height: 70 }); + expect(data4Layout.nodes[1]).toMatchObject({ x: 50, y: 205, width: 30, height: 40 }); + // The cluster child is not in the top-level graph — it keeps its pre-layout values. + expect(data4Layout.nodes[2].x).toBeUndefined(); + expect(data4Layout.nodes[2].y).toBeUndefined(); + // Intra-cluster edges are absent from the normalized edges. + expect(data4Layout.edges.map((edge) => edge.id)).toEqual(['X-cluster1']); + }); + it('creates one compact render edge from self-loop layout segments', () => { const graph = new Graph({ multigraph: true, compound: true }); graph.setNode('A', { id: 'A', x: 10, y: 10, width: 20, height: 20 }); @@ -229,6 +424,85 @@ describe('getEdgesToRender', () => { ]); }); + it('measures DAGRE DOM separately from running the layout core', async () => { + const restoreDom = setupDom(); + + try { + const data4Layout = { + type: 'flowchart', + diagramId: 'dagre-phase-test', + direction: 'TB', + config: { + flowchart: { + htmlLabels: false, + nodeSpacing: 50, + rankSpacing: 50, + }, + }, + nodes: [ + { + id: 'A', + domId: 'dagre-phase-test-A', + label: 'A', + shape: 'rect', + isGroup: false, + padding: 0, + }, + { + id: 'B', + domId: 'dagre-phase-test-B', + label: 'B', + shape: 'rect', + isGroup: false, + padding: 0, + }, + ], + edges: [ + { + id: 'A-B', + start: 'A', + end: 'B', + label: '', + arrowTypeStart: 'none', + arrowTypeEnd: 'arrow_point', + }, + ], + }; + const preparedLayout = prepareLayoutForDagre(data4Layout); + const element = select(document.querySelector('#container')).append('svg').append('g'); + + const measuredLayout = await measureDagreLayout(data4Layout, { element, preparedLayout }); + + expect(measuredLayout.graph.node('A').width).toBeGreaterThan(0); + expect(measuredLayout.graph.node('A').x).toBeUndefined(); + + runDagreLayoutCore(data4Layout, { element, preparedLayout }); + + expect(measuredLayout.graph.node('A').x).toEqual(expect.any(Number)); + expect(measuredLayout.graph.node('B').y).toEqual(expect.any(Number)); + expect(data4Layout.nodes[0].x).toEqual(expect.any(Number)); + expect(data4Layout.nodes[1].y).toEqual(expect.any(Number)); + expect(data4Layout.edges[0].points).toEqual(expect.any(Array)); + } finally { + restoreDom(); + } + }); + + it('requires DAGRE measurement before running the layout core', () => { + const data4Layout = { + type: 'flowchart', + diagramId: 'dagre-phase-contract-test', + direction: 'TB', + config: {}, + nodes: [], + edges: [], + }; + + expect(() => runDagreLayoutCore(data4Layout, {})).toThrow( + 'runDagreLayoutCore requires measureDagreLayout to run first' + ); + }); + it('renders a flowchart self-loop as one SVG path', async () => { const restoreDom = setupDom(); @@ -255,4 +529,266 @@ C --> C` restoreDom(); } }); + + it('keeps reverse-order nested flowchart subgraphs visible through shared paint', async () => { + const restoreDom = setupDom(); + + try { + const { svg } = await mermaidAPI.render( + 'reverse-order-subgraph-test', + `flowchart LR + a -->b + subgraph A + B + end + subgraph B + b + end` + ); + const dom = new JSDOM(svg); + const clusters = [...dom.window.document.querySelectorAll('.cluster')]; + + expect(clusters).toHaveLength(2); + expect(clusters.map((cluster) => cluster.id)).toEqual( + expect.arrayContaining([ + expect.stringContaining('reverse-order-subgraph-test-A'), + expect.stringContaining('reverse-order-subgraph-test-B'), + ]) + ); + } finally { + restoreDom(); + } + }); + + it('renders a recursive ER relationship as one logical self-loop path', async () => { + const restoreDom = setupDom(); + + try { + const { svg } = await mermaidAPI.render( + 'er-recursive-self-loop-test', + `erDiagram + CUSTOMER ||..o{ CUSTOMER : refers` + ); + const dom = new JSDOM(svg); + const cyclicPaths = getCyclicPaths(dom.window.document); + const relationshipPaths = dom.window.document.querySelectorAll( + '.edgePaths path.relationshipLine' + ); + + expect(cyclicPaths).toHaveLength(0); + expect(relationshipPaths).toHaveLength(1); + expect(relationshipPaths[0].getAttribute('data-id')).not.toContain('cyclic-special'); + expect(relationshipPaths[0].getAttribute('id')).toContain('entity-CUSTOMER-0'); + expect(relationshipPaths[0].getAttribute('id')).not.toContain('cyclic-special'); + expectFinitePaths(relationshipPaths); + } finally { + restoreDom(); + } + }); + + it('preserves class self-loop multiplicity terminal labels when merging', () => { + const graph = new Graph({ multigraph: true, compound: true }); + graph.setNode('SelfReferential', { + id: 'SelfReferential', + x: 50, + y: 50, + width: 100, + height: 60, + }); + graph.setNode('SelfReferential---SelfReferential---1', { + id: 'SelfReferential---SelfReferential---1', + x: 120, + y: 50, + }); + graph.setNode('SelfReferential---SelfReferential---2', { + id: 'SelfReferential---SelfReferential---2', + x: 120, + y: 90, + }); + + const originalEdge = { + id: 'SelfReferential-SelfReferential', + start: 'SelfReferential', + end: 'SelfReferential', + label: 'referenced', + arrowTypeStart: 'none', + arrowTypeEnd: 'extension', + startLabelRight: '1', + endLabelLeft: '0..1', + }; + const segment1 = { + ...originalEdge, + id: 'SelfReferential-cyclic-special-1', + selfLoop: { id: originalEdge.id, order: 0 }, + originalEdge, + points: [], + }; + const segmentMid = { + ...originalEdge, + id: 'SelfReferential-cyclic-special-mid', + selfLoop: { id: originalEdge.id, order: 1 }, + originalEdge, + points: [], + width: 52, + height: 14, + labelStyle: 'label-style', + }; + const segment2 = { + ...originalEdge, + id: 'SelfReferential-cyclic-special-2', + selfLoop: { id: originalEdge.id, order: 2 }, + originalEdge, + points: [], + }; + + graph.setEdge( + 'SelfReferential', + 'SelfReferential---SelfReferential---1', + segment1, + 'SelfReferential-cyclic-special-0' + ); + graph.setEdge( + 'SelfReferential---SelfReferential---1', + 'SelfReferential---SelfReferential---2', + segmentMid, + 'SelfReferential-cyclic-special-1' + ); + graph.setEdge( + 'SelfReferential---SelfReferential---2', + 'SelfReferential', + segment2, + 'SelfReferential-cyclic-special-2' + ); + + const edgesToRender = getEdgesToRender(graph); + + expect(edgesToRender).toHaveLength(1); + expect(edgesToRender[0].edge).toMatchObject({ + id: 'SelfReferential-SelfReferential', + start: 'SelfReferential', + end: 'SelfReferential', + label: 'referenced', + arrowTypeStart: 'none', + arrowTypeEnd: 'extension', + startLabelRight: '1', + endLabelLeft: '0..1', + labelStyle: 'label-style', + }); + expect(edgesToRender[0].edge.selfLoop).toBeUndefined(); + expect(edgesToRender[0].edge.originalEdge).toBeUndefined(); + }); + + it('renders a recursive class relationship as one path with multiplicity labels', async () => { + const restoreDom = setupDom(); + + try { + const { svg } = await mermaidAPI.render( + 'class-self-loop-shared-paint-test', + `classDiagram +class SelfReferential{ + +int id + +int self_referential_id + +SelfReferential referenced +} +SelfReferential "1" --> "0..1" SelfReferential : referenced` + ); + const dom = new JSDOM(svg); + const document = dom.window.document; + const cyclicPaths = getCyclicPaths(document); + const edgePaths = document.querySelectorAll('.edgePaths path[data-edge="true"]'); + const terminalLabels = [...document.querySelectorAll('.edgeTerminals')].map((label) => + label.textContent?.trim() + ); + + expect(cyclicPaths).toHaveLength(0); + expect(edgePaths).toHaveLength(1); + expect(terminalLabels).toContain('1'); + expect(terminalLabels).toContain('0..1'); + expectFinitePaths(edgePaths); + } finally { + restoreDom(); + } + }); + + it('renders a flowchart subgraph through the shared paint path', async () => { + const restoreDom = setupDom(); + + try { + const { svg } = await mermaidAPI.render( + 'shared-paint-subgraph-test', + `flowchart TD +subgraph clusterA[Cluster A] +A --> B +end +clusterA --> C` + ); + const dom = new JSDOM(svg); + + expect(dom.window.document.querySelectorAll('.cluster')).toHaveLength(1); + expect(dom.window.document.querySelectorAll('.node')).toHaveLength(3); + expect(dom.window.document.querySelectorAll('.edgePaths path.flowchart-link')).toHaveLength( + 2 + ); + } finally { + restoreDom(); + } + }); + + it('renders hand-drawn class diagrams with nested namespaces', async () => { + const restoreDom = setupDom(); + + try { + const dotNotation = await mermaidAPI.render( + 'class-nested-namespace-dot-test', + `%%{init: {"look": "handDrawn", "htmlLabels": true}}%% +classDiagram +namespace Company.Engineering.Backend { + class Developer { + +writeCode() + } +} +namespace Company.Engineering.Frontend { + class Designer { + +createMockup() + } +} +namespace Company.Engineering { + class TechLead { + +planSprint() + } +} +TechLead --> Developer : leads +TechLead --> Designer : leads` + ); + const syntactic = await mermaidAPI.render( + 'class-nested-namespace-syntax-test', + `%%{init: {"look": "handDrawn", "htmlLabels": true}}%% +classDiagram +namespace Platform { + namespace Auth { + class UserService { + +login() + +logout() + } + } + namespace Data { + class Repository { + +find() + +save() + } + } + class Gateway { + +route() + } +} +Gateway --> UserService : delegates +Gateway --> Repository : delegates` + ); + + expect(new JSDOM(dotNotation.svg).window.document.querySelector('svg')).toBeTruthy(); + expect(new JSDOM(syntactic.svg).window.document.querySelector('svg')).toBeTruthy(); + } finally { + restoreDom(); + } + }); }); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts index c21bf45da3a..6fbafa769e8 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts @@ -3,9 +3,11 @@ import type { D3Selection, MaybePromise } from '../../types.js'; import type { Node, ShapeRenderOptions } from '../types.js'; import { anchor } from './shapes/anchor.js'; import { bowTieRect } from './shapes/bowTieRect.js'; +import { block_arrow } from './shapes/blockArrow.js'; import { card } from './shapes/card.js'; import { choice } from './shapes/choice.js'; import { circle } from './shapes/circle.js'; +import { composite } from './shapes/composite.js'; import { crossedCircle } from './shapes/crossedCircle.js'; import { curlyBraceLeft } from './shapes/curlyBraceLeft.js'; import { curlyBraceRight } from './shapes/curlyBraceRight.js'; @@ -491,8 +493,10 @@ const generateShapeMap = () => { note, // Rectangles + composite, rectWithTitle, labelRect, + block_arrow, // Icons iconSquare, diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/blockArrow.spec.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/blockArrow.spec.ts new file mode 100644 index 00000000000..aa1f36cd8cc --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/blockArrow.spec.ts @@ -0,0 +1,197 @@ +import { describe, expect, it } from 'vitest'; +import type { Direction } from '../../../diagrams/block/blockTypes.js'; +import type { Point } from '../../../types.js'; +import type { Node } from '../../types.js'; +import { getArrowPoints } from './blockArrow.js'; + +const bbox = { width: 100, height: 20 }; +const node = { padding: 10 } satisfies Pick; +const width = 140; +const height = 40; +const midpoint = 20; +const padding = 5; + +const p = (x: number, y: number): Point => ({ x, y }); + +const allDirectionsPoints = [ + p(0, 0), + p(midpoint, 0), + p(width / 2, 2 * padding), + p(width - midpoint, 0), + p(width, 0), + p(width, -height / 3), + p(width + 2 * padding, -height / 2), + p(width, (-2 * height) / 3), + p(width, -height), + p(width - midpoint, -height), + p(width / 2, -height - 2 * padding), + p(midpoint, -height), + p(0, -height), + p(0, (-2 * height) / 3), + p(-2 * padding, -height / 2), + p(0, -height / 3), +]; + +const cases: { name: string; directions: Direction[]; points: Point[] }[] = [ + { + name: 'four-way arrow', + directions: ['right', 'left', 'up', 'down'], + points: allDirectionsPoints, + }, + { + name: 'right-left-up arrow', + directions: ['right', 'left', 'up'], + points: [ + p(midpoint, 0), + p(width - midpoint, 0), + p(width, -height / 2), + p(width - midpoint, -height), + p(midpoint, -height), + p(0, -height / 2), + ], + }, + { + name: 'right-left-down arrow', + directions: ['right', 'left', 'down'], + points: [p(0, 0), p(midpoint, -height), p(width - midpoint, -height), p(width, 0)], + }, + { + name: 'right-up-down arrow', + directions: ['right', 'up', 'down'], + points: [p(0, 0), p(width, -midpoint), p(width, -height + midpoint), p(0, -height)], + }, + { + name: 'left-up-down arrow', + directions: ['left', 'up', 'down'], + points: [p(width, 0), p(0, -midpoint), p(0, -height + midpoint), p(width, -height)], + }, + { + name: 'horizontal arrow', + directions: ['right', 'left'], + points: [ + p(midpoint, 0), + p(midpoint, -padding), + p(width - midpoint, -padding), + p(width - midpoint, 0), + p(width, -height / 2), + p(width - midpoint, -height), + p(width - midpoint, -height + padding), + p(midpoint, -height + padding), + p(midpoint, -height), + p(0, -height / 2), + ], + }, + { + name: 'vertical arrow', + directions: ['up', 'down'], + points: [ + p(width / 2, 0), + p(0, -padding), + p(midpoint, -padding), + p(midpoint, -height + padding), + p(0, -height + padding), + p(width / 2, -height), + p(width, -height + padding), + p(width - midpoint, -height + padding), + p(width - midpoint, -padding), + p(width, -padding), + ], + }, + { + name: 'right-up angle', + directions: ['right', 'up'], + points: [p(0, 0), p(width, -midpoint), p(0, -height)], + }, + { + name: 'right-down angle', + directions: ['right', 'down'], + points: [p(0, 0), p(width, 0), p(0, -height)], + }, + { + name: 'left-up angle', + directions: ['left', 'up'], + points: [p(width, 0), p(0, -midpoint), p(width, -height)], + }, + { + name: 'left-down angle', + directions: ['left', 'down'], + points: [p(width, 0), p(0, 0), p(width, -height)], + }, + { + name: 'right arrow', + directions: ['right'], + points: [ + p(midpoint, -padding), + p(midpoint, -padding), + p(width - midpoint, -padding), + p(width - midpoint, 0), + p(width, -height / 2), + p(width - midpoint, -height), + p(width - midpoint, -height + padding), + p(midpoint, -height + padding), + p(midpoint, -height + padding), + ], + }, + { + name: 'left arrow', + directions: ['left'], + points: [ + p(midpoint, 0), + p(midpoint, -padding), + p(width - midpoint, -padding), + p(width - midpoint, -height + padding), + p(midpoint, -height + padding), + p(midpoint, -height), + p(0, -height / 2), + ], + }, + { + name: 'up arrow', + directions: ['up'], + points: [ + p(midpoint, -padding), + p(midpoint, -height + padding), + p(0, -height + padding), + p(width / 2, -height), + p(width, -height + padding), + p(width - midpoint, -height + padding), + p(width - midpoint, -padding), + ], + }, + { + name: 'down arrow', + directions: ['down'], + points: [ + p(width / 2, 0), + p(0, -padding), + p(midpoint, -padding), + p(midpoint, -height + padding), + p(width - midpoint, -height + padding), + p(width - midpoint, -padding), + p(width, -padding), + ], + }, + { + name: 'point fallback', + directions: [], + points: [p(0, 0)], + }, +]; + +describe('block arrow points', () => { + for (const { name, directions, points } of cases) { + it(`returns ${name} geometry`, () => { + expect(getArrowPoints(directions, bbox, node, width)).toEqual(points); + }); + } + + it('expands axis shorthands and deduplicates directions', () => { + expect(getArrowPoints(['x', 'right', 'y', 'up'], bbox, node, width)).toEqual( + allDirectionsPoints + ); + }); + + it('uses the natural width when total width is omitted', () => { + expect(getArrowPoints(['right', 'down'], bbox, node)).toEqual([p(0, 0), p(150, 0), p(0, -40)]); + }); +}); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/blockArrow.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/blockArrow.ts new file mode 100644 index 00000000000..e6c2860aa1d --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/blockArrow.ts @@ -0,0 +1,225 @@ +import type { Direction } from '../../../diagrams/block/blockTypes.js'; +import type { D3Selection, Point } from '../../../types.js'; +import type { Node } from '../../types.js'; +import intersect from '../intersect/index.js'; +import { insertPolygonShape } from './insertPolygonShape.js'; +import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js'; + +type BlockArrowNode = Node & { + directions?: Direction[]; + positioned?: boolean; + style?: string; + widthInColumns?: number; +}; + +type DirectionName = Exclude; +interface ArrowMetrics { + height: number; + midpoint: number; + padding: number; + width: number; +} + +type ArrowPointsFactory = (metrics: ArrowMetrics) => Point[]; + +const DIRECTION_ORDER: DirectionName[] = ['right', 'left', 'up', 'down']; +const POINT_KEY = 'point'; + +const expandAndDeduplicateDirections = (directions: Direction[]): Set => { + const uniqueDirections = new Set(); + + for (const direction of directions) { + switch (direction) { + case 'x': + uniqueDirections.add('right'); + uniqueDirections.add('left'); + break; + case 'y': + uniqueDirections.add('up'); + uniqueDirections.add('down'); + break; + default: + uniqueDirections.add(direction); + break; + } + } + + return uniqueDirections; +}; + +const getDirectionKey = (directions: Set) => + DIRECTION_ORDER.filter((direction) => directions.has(direction)).join('|') || POINT_KEY; + +const arrowPointFactories: Record = { + 'right|left|up|down': ({ height, midpoint, padding, width }) => [ + { x: 0, y: 0 }, + { x: midpoint, y: 0 }, + { x: width / 2, y: 2 * padding }, + { x: width - midpoint, y: 0 }, + { x: width, y: 0 }, + { x: width, y: -height / 3 }, + { x: width + 2 * padding, y: -height / 2 }, + { x: width, y: (-2 * height) / 3 }, + { x: width, y: -height }, + { x: width - midpoint, y: -height }, + { x: width / 2, y: -height - 2 * padding }, + { x: midpoint, y: -height }, + { x: 0, y: -height }, + { x: 0, y: (-2 * height) / 3 }, + { x: -2 * padding, y: -height / 2 }, + { x: 0, y: -height / 3 }, + ], + 'right|left|up': ({ height, midpoint, width }) => [ + { x: midpoint, y: 0 }, + { x: width - midpoint, y: 0 }, + { x: width, y: -height / 2 }, + { x: width - midpoint, y: -height }, + { x: midpoint, y: -height }, + { x: 0, y: -height / 2 }, + ], + 'right|left|down': ({ height, midpoint, width }) => [ + { x: 0, y: 0 }, + { x: midpoint, y: -height }, + { x: width - midpoint, y: -height }, + { x: width, y: 0 }, + ], + 'right|up|down': ({ height, midpoint, width }) => [ + { x: 0, y: 0 }, + { x: width, y: -midpoint }, + { x: width, y: -height + midpoint }, + { x: 0, y: -height }, + ], + 'left|up|down': ({ height, midpoint, width }) => [ + { x: width, y: 0 }, + { x: 0, y: -midpoint }, + { x: 0, y: -height + midpoint }, + { x: width, y: -height }, + ], + 'right|left': ({ height, midpoint, padding, width }) => [ + { x: midpoint, y: 0 }, + { x: midpoint, y: -padding }, + { x: width - midpoint, y: -padding }, + { x: width - midpoint, y: 0 }, + { x: width, y: -height / 2 }, + { x: width - midpoint, y: -height }, + { x: width - midpoint, y: -height + padding }, + { x: midpoint, y: -height + padding }, + { x: midpoint, y: -height }, + { x: 0, y: -height / 2 }, + ], + 'up|down': ({ height, midpoint, padding, width }) => [ + { x: width / 2, y: 0 }, + { x: 0, y: -padding }, + { x: midpoint, y: -padding }, + { x: midpoint, y: -height + padding }, + { x: 0, y: -height + padding }, + { x: width / 2, y: -height }, + { x: width, y: -height + padding }, + { x: width - midpoint, y: -height + padding }, + { x: width - midpoint, y: -padding }, + { x: width, y: -padding }, + ], + 'right|up': ({ height, midpoint, width }) => [ + { x: 0, y: 0 }, + { x: width, y: -midpoint }, + { x: 0, y: -height }, + ], + 'right|down': ({ height, width }) => [ + { x: 0, y: 0 }, + { x: width, y: 0 }, + { x: 0, y: -height }, + ], + 'left|up': ({ height, midpoint, width }) => [ + { x: width, y: 0 }, + { x: 0, y: -midpoint }, + { x: width, y: -height }, + ], + 'left|down': ({ height, width }) => [ + { x: width, y: 0 }, + { x: 0, y: 0 }, + { x: width, y: -height }, + ], + right: ({ height, midpoint, padding, width }) => [ + { x: midpoint, y: -padding }, + { x: midpoint, y: -padding }, + { x: width - midpoint, y: -padding }, + { x: width - midpoint, y: 0 }, + { x: width, y: -height / 2 }, + { x: width - midpoint, y: -height }, + { x: width - midpoint, y: -height + padding }, + { x: midpoint, y: -height + padding }, + { x: midpoint, y: -height + padding }, + ], + left: ({ height, midpoint, padding, width }) => [ + { x: midpoint, y: 0 }, + { x: midpoint, y: -padding }, + { x: width - midpoint, y: -padding }, + { x: width - midpoint, y: -height + padding }, + { x: midpoint, y: -height + padding }, + { x: midpoint, y: -height }, + { x: 0, y: -height / 2 }, + ], + up: ({ height, midpoint, padding, width }) => [ + { x: midpoint, y: -padding }, + { x: midpoint, y: -height + padding }, + { x: 0, y: -height + padding }, + { x: width / 2, y: -height }, + { x: width, y: -height + padding }, + { x: width - midpoint, y: -height + padding }, + { x: width - midpoint, y: -padding }, + ], + down: ({ height, midpoint, padding, width }) => [ + { x: width / 2, y: 0 }, + { x: 0, y: -padding }, + { x: midpoint, y: -padding }, + { x: midpoint, y: -height + padding }, + { x: width - midpoint, y: -height + padding }, + { x: width - midpoint, y: -padding }, + { x: width, y: -padding }, + ], + [POINT_KEY]: () => [{ x: 0, y: 0 }], +}; + +export const getArrowPoints = ( + duplicatedDirections: Direction[], + bbox: { width: number; height: number }, + node: Pick, + totalWidth?: number +): Point[] => { + const directions = expandAndDeduplicateDirections(duplicatedDirections); + const padding = (node.padding ?? 0) / 2; + const height = bbox.height + 4 * padding; + const midpoint = height / 2; + const width = totalWidth ?? bbox.width + 2 * midpoint + 2 * padding; + const key = getDirectionKey(directions); + const pointFactory = arrowPointFactories[key] ?? arrowPointFactories[POINT_KEY]; + + return pointFactory({ height, midpoint, padding, width }); +}; + +export async function block_arrow( + parent: D3Selection, + node: Node +) { + const blockNode = node as BlockArrowNode; + const { shapeSvg, bbox } = await labelHelper(parent, blockNode, getNodeClasses(blockNode)); + const nodePadding = blockNode.padding ?? 0; + const height = bbox.height + 2 * nodePadding; + const midpoint = height / 2; + const naturalWidth = bbox.width + 2 * midpoint + nodePadding; + const nodeWidth = blockNode.width ?? 0; + const isSpanning = + blockNode.positioned && (blockNode.widthInColumns ?? 1) > 1 && nodeWidth > naturalWidth; + const width = isSpanning ? nodeWidth : naturalWidth; + const points = getArrowPoints(blockNode.directions ?? [], bbox, blockNode, width); + const blockArrow = insertPolygonShape(shapeSvg, width, height, points); + + blockArrow.attr('style', blockNode.style ?? null); + updateNodeBounds(blockNode, blockArrow); + + blockNode.intersect = function (point) { + return intersect.polygon(blockNode, points, point); + }; + + return shapeSvg; +} diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/composite.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/composite.ts new file mode 100644 index 00000000000..417594f50f5 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/composite.ts @@ -0,0 +1,44 @@ +import type { D3Selection } from '../../../types.js'; +import type { Node } from '../../types.js'; +import intersect from '../intersect/index.js'; +import { labelHelper, updateNodeBounds } from './util.js'; + +type CompositeNode = Node & { + class?: string; + positioned?: boolean; + style?: string; +}; + +export async function composite(parent: D3Selection, node: Node) { + const compositeNode = node as CompositeNode; + const classes = ['node', compositeNode.cssClasses, compositeNode.class].filter(Boolean).join(' '); + const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, compositeNode, classes); + const rect = shapeSvg.insert('rect', ':first-child'); + const nodePadding = compositeNode.padding ?? 0; + const totalWidth = compositeNode.positioned + ? (compositeNode.width ?? 0) + : bbox.width + nodePadding; + const totalHeight = compositeNode.positioned + ? (compositeNode.height ?? 0) + : bbox.height + nodePadding; + const x = compositeNode.positioned ? -totalWidth / 2 : -bbox.width / 2 - halfPadding; + const y = compositeNode.positioned ? -totalHeight / 2 : -bbox.height / 2 - halfPadding; + + rect + .attr('class', 'basic cluster composite label-container') + .attr('style', compositeNode.style ?? null) + .attr('rx', compositeNode.rx ?? null) + .attr('ry', compositeNode.ry ?? null) + .attr('x', x) + .attr('y', y) + .attr('width', totalWidth) + .attr('height', totalHeight); + + updateNodeBounds(compositeNode, rect); + + compositeNode.intersect = function (point) { + return intersect.rect(compositeNode, point); + }; + + return shapeSvg; +} diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.spec.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.spec.ts new file mode 100644 index 00000000000..44ff8e1325e --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.spec.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import type { Node } from '../../types.js'; +import { styles2String } from './handDrawnShapeStyles.js'; + +describe('hand drawn shape styles', () => { + it('normalizes string label styles before compiling node styles', () => { + const styles = styles2String({ + id: 'styled-node', + isGroup: false, + cssStyles: ['fill:#f9F', 'stroke:#333'], + labelStyle: 'color:#fff;text-align:center;', + } as Node); + + expect(styles.nodeStyles).toContain('fill:#f9F !important'); + expect(styles.nodeStyles).toContain('stroke:#333 !important'); + expect(styles.labelStyles).toContain('color:#fff !important'); + expect(styles.labelStyles).toContain('text-align:center !important'); + }); +}); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts index 25ddff46b64..808e3e4326a 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts @@ -15,6 +15,19 @@ export const solidStateFill = (color: string) => { }; }; +const normalizeStyleList = (styles: string | string[] | undefined): string[] => { + if (Array.isArray(styles)) { + return styles; + } + if (!styles) { + return []; + } + return styles + .split(';') + .map((style) => style.trim()) + .filter(Boolean); +}; + export const compileStyles = (node: Node) => { // node.cssCompiledStyles is an array of strings in the form of 'key: value' where key is the css property and value is the value // the array is the styles of node from the classes it is using @@ -23,7 +36,7 @@ export const compileStyles = (node: Node) => { const stylesMap = styles2Map([ ...(node.cssCompiledStyles || []), ...(node.cssStyles || []), - ...(node.labelStyle || []), + ...normalizeStyleList(node.labelStyle), ]); return { stylesMap, stylesArray: [...stylesMap] }; }; @@ -36,6 +49,7 @@ export const styles2Map = (styles: string[]) => { }); return styleMap; }; + export const isLabelStyle = (key: string) => { return ( key === 'color' || diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/invertedTrapezoid.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/invertedTrapezoid.ts index 7935faeb24a..737a3fee5da 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/invertedTrapezoid.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/invertedTrapezoid.ts @@ -18,8 +18,8 @@ export async function inv_trapezoid( const labelPaddingX = node.look === 'neo' ? nodePadding * 2 : nodePadding; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const w = Math.max(bbox.width + (labelPaddingX ?? 0) * 2, node?.width ?? 0); - const h = Math.max(bbox.height + (labelPaddingY ?? 0) * 2, node?.height ?? 0); + const h = Math.max(bbox.height + labelPaddingY * 2, node.height ?? 0); + const w = Math.max(bbox.width + labelPaddingX * 2, (node.width ?? 0) - h); const points = [ { x: 0, y: 0 }, diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanLeft.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanLeft.ts index 383bcf962f5..b68a7aa2679 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanLeft.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanLeft.ts @@ -13,8 +13,8 @@ export async function lean_left(parent: D3Selectio const labelPaddingY = nodePadding; const labelPaddingX = node.look === 'neo' ? nodePadding * 2 : nodePadding; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const h = (node?.height ?? bbox.height) + labelPaddingY; - const w = (node?.width ?? bbox.width) + labelPaddingX; + const h = Math.max(bbox.height + labelPaddingY, node.height ?? 0); + const w = Math.max(bbox.width + labelPaddingX, (node.width ?? 0) - h); const points = [ { x: 0, y: 0 }, { x: w + (3 * h) / 6, y: 0 }, diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanRight.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanRight.ts index 16acd155e37..8f40f2ee589 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanRight.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanRight.ts @@ -13,8 +13,8 @@ export async function lean_right(parent: D3Selecti const labelPaddingY = nodePadding; const labelPaddingX = node.look === 'neo' ? nodePadding * 2 : nodePadding; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const h = (node?.height ?? bbox.height) + labelPaddingY; - const w = (node?.width ?? bbox.width) + labelPaddingX; + const h = Math.max(bbox.height + labelPaddingY, node.height ?? 0); + const w = Math.max(bbox.width + labelPaddingX, (node.width ?? 0) - h); const points = [ { x: (-3 * h) / 6, y: 0 }, diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/rectLeftInvArrow.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/rectLeftInvArrow.ts index 888dd89ca9f..1d785d84f38 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/rectLeftInvArrow.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/rectLeftInvArrow.ts @@ -16,9 +16,13 @@ export async function rect_left_inv_arrow( const labelPaddingY = node.look === 'neo' ? 12 : (nodePadding ?? 0); const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); - const w = (node?.width ?? bbox.width) + (node.look === 'neo' ? labelPaddingX * 2 : labelPaddingX); - const h = - (node?.height ?? bbox.height) + (node.look === 'neo' ? labelPaddingY * 2 : labelPaddingY); + const labelWidth = bbox.width + (node.look === 'neo' ? labelPaddingX * 2 : labelPaddingX); + const h = Math.max( + bbox.height + (node.look === 'neo' ? labelPaddingY * 2 : labelPaddingY), + node.height ?? 0 + ); + const notchWidth = h / 4; + const w = Math.max(labelWidth, (node.width ?? 0) - notchWidth); const x = -w / 2; const y = -h / 2; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/subroutine.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/subroutine.ts index e64fc589c52..37b1dd107ed 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/subroutine.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/subroutine.ts @@ -46,8 +46,8 @@ export async function subroutine(parent: D3Selecti const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const totalWidth = (node?.width ?? bbox.width) + 2 * FRAME_WIDTH + labelPaddingX; - const totalHeight = (node?.height ?? bbox.height) + labelPaddingY; + const totalWidth = Math.max(bbox.width + 2 * FRAME_WIDTH + labelPaddingX, node.width ?? 0); + const totalHeight = Math.max(bbox.height + labelPaddingY, node.height ?? 0); const w = totalWidth - 2 * FRAME_WIDTH; const h = totalHeight; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoid.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoid.ts index c25f710d02e..efa2e6cd670 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoid.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoid.ts @@ -13,8 +13,8 @@ export async function trapezoid(parent: D3Selectio const labelPaddingY = node.look === 'neo' ? nodePadding : nodePadding; const labelPaddingX = node.look === 'neo' ? nodePadding * 2 : nodePadding; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const h = (node?.height ?? bbox.height) + labelPaddingY; - const w = (node?.width ?? bbox.width) + labelPaddingX; + const h = Math.max(bbox.height + labelPaddingY, node.height ?? 0); + const w = Math.max(bbox.width + labelPaddingX, (node.width ?? 0) - h); const points = [ { x: (-3 * h) / 6, y: 0 }, diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 9164beefe87..95f024eddd5 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -1553,13 +1553,9 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) nodeSpacing: type: integer minimum: 0 - # should the default value be 50? - # see https://github.com/mermaid-js/mermaid/blob/7647ae317a7b2130e32777248d25a9c4d24b8f9f/packages/mermaid/src/diagrams/class/classRenderer-v2.ts#L258 rankSpacing: type: integer minimum: 0 - # should the default value be 50? - # see https://github.com/mermaid-js/mermaid/blob/7647ae317a7b2130e32777248d25a9c4d24b8f9f/packages/mermaid/src/diagrams/class/classRenderer-v2.ts#L259 forkWidth: type: number default: 70 @@ -1637,13 +1633,9 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) nodeSpacing: type: integer minimum: 0 - # should the default value be 50? - # see https://github.com/mermaid-js/mermaid/blob/7647ae317a7b2130e32777248d25a9c4d24b8f9f/packages/mermaid/src/diagrams/class/classRenderer-v2.ts#L258 rankSpacing: type: integer minimum: 0 - # should the default value be 50? - # see https://github.com/mermaid-js/mermaid/blob/7647ae317a7b2130e32777248d25a9c4d24b8f9f/packages/mermaid/src/diagrams/class/classRenderer-v2.ts#L259 diagramPadding: $ref: '#/$defs/ErDiagramConfig/properties/diagramPadding' htmlLabels: