diff --git a/.changeset/angry-bags-brake.md b/.changeset/angry-bags-brake.md new file mode 100644 index 000000000..472e486ec --- /dev/null +++ b/.changeset/angry-bags-brake.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: architecture diagrams no longer grow to extreme heights due to conflicting alignments diff --git a/.changeset/chilly-hotels-mix.md b/.changeset/chilly-hotels-mix.md deleted file mode 100644 index 642ab6ecf..000000000 --- a/.changeset/chilly-hotels-mix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix: Jagged edge fix for icon shape diff --git a/.changeset/dry-students-act.md b/.changeset/dry-students-act.md deleted file mode 100644 index 43f439f2e..000000000 --- a/.changeset/dry-students-act.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -Add missing TypeScript dependencies diff --git a/.changeset/heavy-cats-mate.md b/.changeset/heavy-cats-mate.md deleted file mode 100644 index c903cc47e..000000000 --- a/.changeset/heavy-cats-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix: Icon color fix for colored icons. diff --git a/.changeset/kind-drinks-invent.md b/.changeset/kind-drinks-invent.md deleted file mode 100644 index 244be2bf6..000000000 --- a/.changeset/kind-drinks-invent.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': minor ---- - -Adding Kanban board, a new diagram type diff --git a/.changeset/thick-elephants-search.md b/.changeset/thick-elephants-search.md deleted file mode 100644 index 5e29c42d6..000000000 --- a/.changeset/thick-elephants-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix: error `mermaid.parse` on an invalid shape, so that it matches the errors thrown by `mermaid.render` diff --git a/cypress/integration/rendering/architecture.spec.ts b/cypress/integration/rendering/architecture.spec.ts index bc2276352..25326ff80 100644 --- a/cypress/integration/rendering/architecture.spec.ts +++ b/cypress/integration/rendering/architecture.spec.ts @@ -171,6 +171,58 @@ describe.skip('architecture diagram', () => { ` ); }); + + it('should render an architecture diagram with a resonable height', () => { + imgSnapshotTest( + `architecture-beta + group federated(cloud)[Federated Environment] + service server1(server)[System] in federated + service edge(server)[Edge Device] in federated + server1:R -- L:edge + + group on_prem(cloud)[Hub] + service firewall(server)[Firewall Device] in on_prem + service server(server)[Server] in on_prem + firewall:R -- L:server + + service db1(database)[db1] in on_prem + service db2(database)[db2] in on_prem + service db3(database)[db3] in on_prem + service db4(database)[db4] in on_prem + service db5(database)[db5] in on_prem + service db6(database)[db6] in on_prem + + junction mid in on_prem + server:B -- T:mid + + junction 1Leftofmid in on_prem + 1Leftofmid:R -- L:mid + 1Leftofmid:B -- T:db1 + + junction 2Leftofmid in on_prem + 2Leftofmid:R -- L:1Leftofmid + 2Leftofmid:B -- T:db2 + + junction 3Leftofmid in on_prem + 3Leftofmid:R -- L:2Leftofmid + 3Leftofmid:B -- T:db3 + + junction 1RightOfMid in on_prem + mid:R -- L:1RightOfMid + 1RightOfMid:B -- T:db4 + + junction 2RightOfMid in on_prem + 1RightOfMid:R -- L:2RightOfMid + 2RightOfMid:B -- T:db5 + + junction 3RightOfMid in on_prem + 2RightOfMid:R -- L:3RightOfMid + 3RightOfMid:B -- T:db6 + + edge:R -- L:firewall + ` + ); + }); }); // Skipped as the layout is not deterministic, and causes issues in E2E tests. diff --git a/cypress/integration/rendering/flowchart-elk.spec.js b/cypress/integration/rendering/flowchart-elk.spec.js index b5caef973..c3aba53ea 100644 --- a/cypress/integration/rendering/flowchart-elk.spec.js +++ b/cypress/integration/rendering/flowchart-elk.spec.js @@ -1,6 +1,6 @@ import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; -describe.skip('Flowchart ELK', () => { +describe('Flowchart ELK', () => { it('1-elk: should render a simple flowchart', () => { imgSnapshotTest( `flowchart-elk TD @@ -857,6 +857,49 @@ flowchart LR D --> E A["A"] +`, + { flowchart: { titleTopMargin: 0 } } + ); + }); + it('6080: should handle diamond shape intersections', () => { + imgSnapshotTest( + `--- +config: + layout: elk +--- +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} + `, { flowchart: { titleTopMargin: 0 } } ); diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index 1de071283..7ec666c1a 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -10,6 +10,10 @@ href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" rel="stylesheet" /> +
+---
+config:
+  layout: elk
+---
+flowchart LR
+ subgraph s1["Untitled subgraph"]
+        n1["Evaluate"]
+        n2["Option 1"]
+        n3["Option 2"]
+        n4["fa:fa-car Option 3"]
+  end
+    n1 -- One --> n2
+    n1 -- Two --> n3
+    n1 -- Three --> n4
+    n5
+    n1@{ shape: diam}
+    n2@{ shape: rect}
+    n3@{ shape: rect}
+    n4@{ shape: rect}
+    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"]
+
+
+    
+
+---
+config:
+  layout: elk
+---
+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}
+
+    
+
+---
+config:
+  layout: elk
+---
+flowchart LR
+ subgraph s1["Untitled subgraph"]
+        n1["Evaluate"]
+        n2["Option 1"]
+  end
+    n1 -- One --> n2
+
+
+
+
+    
+
+---
+config:
+  layout: elk
+---
+flowchart LR
+    A{A} --> B & C
+
+
+---
+config:
+  layout: elk
+---
+flowchart LR
+    n2@{ shape: rect}
+    n3@{ shape: rect}
+    n4@{ shape: rect}
+    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"]
+    %% C@{ shape: hexagon}
+
+
+    
+
+---
+config:
+  kanban:
+    ticketBaseUrl: 'https://github.com/your-repo/issues/#TICKET#'
+---
+kanban
+  Backlog
+    task1[๐Ÿ“ Define project requirements]@{ ticket: a101 }
+  To Do
+    task2[๐Ÿ” Research technologies]@{ ticket: a102 }
+  Review
+    task4[๐Ÿ” Code review for login feature]@{ ticket: a104 }
+  Done
+    task5[โœ… Deploy initial version]@{ ticket: a105 }
+  In Progress
+    task3[๐Ÿ’ป Develop login feature]@{ ticket: 103 }
+
+    
+
+flowchart LR
+nA[Default] --> A@{ icon: 'fa:bell', form: 'rounded' }
+
+    
+
+flowchart LR
+nA[Style] --> A@{ icon: 'fa:bell', form: 'rounded' }
+style A fill:#f9f,stroke:#333,stroke-width:4px
+    
+
+flowchart LR
+nA[Class] --> A@{ icon: 'fa:bell', form: 'rounded' }
+A:::AClass
+classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
+    
+
+flowchart LR
+  nA[Class] --> A@{ icon: 'logos:aws', form: 'rounded' }
+
+    
+
+flowchart LR
+nA[Default] --> A@{ icon: 'fa:bell', form: 'square' }
+
+    
+
+flowchart LR
+nA[Style] --> A@{ icon: 'fa:bell', form: 'square' }
+style A fill:#f9f,stroke:#333,stroke-width:4px
+    
+
+flowchart LR
+nA[Class] --> A@{ icon: 'fa:bell', form: 'square' }
+A:::AClass
+classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
+    
+
+flowchart LR
+  nA[Class] --> A@{ icon: 'logos:aws', form: 'square' }
+
+    
+
+flowchart LR
+nA[Default] --> A@{ icon: 'fa:bell', form: 'circle' }
+
+    
+
+flowchart LR
+nA[Style] --> A@{ icon: 'fa:bell', form: 'circle' }
+style A fill:#f9f,stroke:#333,stroke-width:4px
+    
+
+flowchart LR
+nA[Class] --> A@{ icon: 'fa:bell', form: 'circle' }
+A:::AClass
+classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
+    
+
+flowchart LR
+  nA[Class] --> A@{ icon: 'logos:aws', form: 'circle' }
+  A:::AClass
+  classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
+    
+
+flowchart LR
+  nA[Style] --> A@{ icon: 'logos:aws', form: 'circle' }
+  style A fill:#f9f,stroke:#333,stroke-width:4px
+    
+
 kanban
   id2[In progress]
     docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }
     
-
+    
 ---
 config:
   kanban:
@@ -118,6 +320,30 @@ kanban
     
 
@@ -41,7 +45,7 @@ onMounted(() => {
         >
           {{ taglines[index].label }}
           
         
       
diff --git a/packages/mermaid/src/docs/.vitepress/theme/index.ts b/packages/mermaid/src/docs/.vitepress/theme/index.ts
index 05f15ea6a..3ec200937 100644
--- a/packages/mermaid/src/docs/.vitepress/theme/index.ts
+++ b/packages/mermaid/src/docs/.vitepress/theme/index.ts
@@ -9,8 +9,6 @@ import Contributors from '../components/Contributors.vue';
 import HomePage from '../components/HomePage.vue';
 // @ts-ignore Type not available
 import TopBar from '../components/TopBar.vue';
-// @ts-ignore Type not available
-import ProductHuntBadge from '../components/ProductHuntBadge.vue';
 import { getRedirect } from './redirect.js';
 // @ts-ignore Type not available
 import 'uno.css';
@@ -25,7 +23,6 @@ export default {
     return h(Theme.Layout, null, {
       // Keeping this as comment as it took a lot of time to figure out how to add a component to the top bar.
       'home-hero-before': () => h(TopBar),
-      'home-hero-info-before': () => h(ProductHuntBadge),
       'home-features-after': () => h(HomePage),
       'doc-before': () => h(TopBar),
     });
diff --git a/packages/mermaid/src/docs/ecosystem/integrations-community.md b/packages/mermaid/src/docs/ecosystem/integrations-community.md
index 974cccc12..6cff12aac 100644
--- a/packages/mermaid/src/docs/ecosystem/integrations-community.md
+++ b/packages/mermaid/src/docs/ecosystem/integrations-community.md
@@ -180,8 +180,6 @@ Communication tools and platforms
   - [=Diagram block](https://github.com/zag/podlite/tree/main/packages/podlite-diagrams)
 - [Standard Notes](https://standardnotes.com/)
   - [Mermaid Extension](https://github.com/nienow/sn-mermaid)
-- [Sublime Text 3](https://sublimetext.com)
-  - [Mermaid Package](https://packagecontrol.io/packages/Mermaid)
 - [VS Code](https://code.visualstudio.com/)
   - [Mermaid Editor](https://marketplace.visualstudio.com/items?itemName=tomoyukim.vscode-mermaid-editor)
   - [Mermaid Export](https://marketplace.visualstudio.com/items?itemName=Gruntfuggly.mermaid-export)
diff --git a/packages/mermaid/src/docs/ecosystem/mermaid-chart.md b/packages/mermaid/src/docs/ecosystem/mermaid-chart.md
index 83695dab7..77a7020b7 100644
--- a/packages/mermaid/src/docs/ecosystem/mermaid-chart.md
+++ b/packages/mermaid/src/docs/ecosystem/mermaid-chart.md
@@ -6,7 +6,7 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
 
 
-Mermaid Whiteboard - Drag & Drop your Nodes with Mermaid's new Whiteboard! | Product Hunt +Mermaid Chart - A smarter way to create diagrams | Product Hunt ## About diff --git a/packages/mermaid/src/docs/news/blog.md b/packages/mermaid/src/docs/news/blog.md index 4f67758f3..d15f79cdc 100644 --- a/packages/mermaid/src/docs/news/blog.md +++ b/packages/mermaid/src/docs/news/blog.md @@ -1,5 +1,17 @@ # Blog +## [Mermaid 11.4 is out: New Features and Kanban Diagramming](https://www.mermaidchart.com/blog/posts/mermaid-11-4-is-out-new-features-and-kanban-diagramming) + +Mermaid 11.4 brings enhanced functionality with the introduction of Kanban diagrams, allowing users to create visual workflows with status columns and task details. + +October 31, 2024 ยท 2 mins + +## [How To Build an ER Diagram with Mermaid Chart](https://www.mermaidchart.com/blog/posts/how-to-build-an-er-diagram-with-mermaid-chart) + +An entity relationship (ER) diagram acts like a blueprint for your database. This makes ER diagrams effective tools for anyone dealing with complex databases, data modeling, and AI model training. + +October 24, 2024 ยท 4 mins + ## [Expanding the Horizons of Mermaid Flowcharts: Introducing 30 New Shapes!](https://www.mermaidchart.com/blog/posts/new-mermaid-flowchart-shapes/) 24 September 2024 ยท 5 mins diff --git a/packages/mermaid/src/docs/syntax/kanban.md b/packages/mermaid/src/docs/syntax/kanban.md index 4ef98fbac..c50eed7d8 100644 --- a/packages/mermaid/src/docs/syntax/kanban.md +++ b/packages/mermaid/src/docs/syntax/kanban.md @@ -64,7 +64,7 @@ todo[Todo] ## Configuration Options -You can customize the Kanban diagram using a configuration block at the beginning of your markdown file. This is useful for setting global settings like a base URL for tickets. Currently there is one configuration option for kanban diagrams tacketBaseUrl. This can be set as in the the following example: +You can customize the Kanban diagram using a configuration block at the beginning of your markdown file. This is useful for setting global settings like a base URL for tickets. Currently there is one configuration option for kanban diagrams `ticketBaseUrl`. This can be set as in the the following example: ```yaml --- diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index c44161a52..910ecb5e8 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -455,6 +455,7 @@ const render = async function ( svgCode = DOMPurify.sanitize(svgCode, { ADD_TAGS: DOMPURIFY_TAGS, ADD_ATTR: DOMPURIFY_ATTR, + HTML_INTEGRATION_POINTS: { foreignobject: true }, }); } diff --git a/packages/mermaid/src/rendering-util/handle-markdown-text.ts b/packages/mermaid/src/rendering-util/handle-markdown-text.ts index 1bff5a977..f898875cf 100644 --- a/packages/mermaid/src/rendering-util/handle-markdown-text.ts +++ b/packages/mermaid/src/rendering-util/handle-markdown-text.ts @@ -39,6 +39,7 @@ export function markdownToLines(markdown: string, config: MermaidConfig = {}): M lines.push([]); } textLine.split(' ').forEach((word) => { + word = word.replace(/'/g, `'`); if (word) { lines[currentLine].push({ content: word, type: parentType }); } diff --git a/packages/mermaid/src/rendering-util/rendering-elements/clusters.js b/packages/mermaid/src/rendering-util/rendering-elements/clusters.js index 3bd9c9dc7..1dd87d438 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/clusters.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/clusters.js @@ -471,6 +471,13 @@ const shapes = { let clusterElems = new Map(); +/** + * @typedef {keyof typeof shapes} ClusterShapeID + */ + +/** + * @param {import('../types.js').ClusterNode} node - Shape defaults to 'rect' + */ export const insertCluster = async (elem, node) => { const shape = node.shape || 'rect'; const cluster = await shapes[shape](elem, node); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/nodes.ts b/packages/mermaid/src/rendering-util/rendering-elements/nodes.ts index e2eea5e19..5af6cd17a 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/nodes.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/nodes.ts @@ -1,6 +1,6 @@ import { log } from '../../logger.js'; import { shapes } from './shapes.js'; -import type { Node, ShapeRenderOptions } from '../types.js'; +import type { Node, NonClusterNode, ShapeRenderOptions } from '../types.js'; import type { SVGGroup } from '../../mermaid.js'; import type { D3Selection } from '../../types.js'; import type { graphlib } from 'dagre-d3-es'; @@ -10,7 +10,11 @@ type NodeElement = D3Selection | Awaited>; const nodeElems = new Map(); -export async function insertNode(elem: SVGGroup, node: Node, renderOptions: ShapeRenderOptions) { +export async function insertNode( + elem: SVGGroup, + node: NonClusterNode, + renderOptions: ShapeRenderOptions +) { let newEl: NodeElement | undefined; let el; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts index 4f6459d85..dbfc93677 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts @@ -449,14 +449,6 @@ export const shapesDefs = [ aliases: ['lined-document'], handler: linedWaveEdgedRect, }, - { - semanticName: 'Class Box', - name: 'Class Box', - shortName: 'classBox', - description: 'Class Box', - aliases: ['class-box'], - handler: classBox, - }, ] as const satisfies ShapeDefinition[]; const generateShapeMap = () => { @@ -477,8 +469,13 @@ const generateShapeMap = () => { icon, iconRounded, imageSquare, - kanbanItem, anchor, + + // Kanban diagram + kanbanItem, + + // class diagram + classBox, } as const; const entries = [ diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconCircle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconCircle.ts index 313e5c7af..e8e16853a 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconCircle.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconCircle.ts @@ -26,16 +26,18 @@ export async function iconCircle( const topLabel = node.pos === 't'; - const { nodeBorder } = themeVariables; + const { nodeBorder, mainBkg } = themeVariables; const { stylesMap } = compileStyles(node); // @ts-expect-error -- Passing a D3.Selection seems to work for some reason const rc = rough.svg(shapeSvg); - const options = userNodeOverrides(node, { stroke: 'transparent' }); + const options = userNodeOverrides(node, {}); if (node.look !== 'handDrawn') { options.roughness = 0; options.fillStyle = 'solid'; } + const fill = stylesMap.get('fill'); + options.stroke = fill ?? mainBkg; const iconElem = shapeSvg.append('g'); if (node.icon) { diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconRounded.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconRounded.ts index ab778de71..40c427ef5 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconRounded.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconRounded.ts @@ -30,7 +30,7 @@ export async function iconRounded( const height = iconSize + halfPadding * 2; const width = iconSize + halfPadding * 2; - const { nodeBorder } = themeVariables; + const { nodeBorder, mainBkg } = themeVariables; const { stylesMap } = compileStyles(node); const x = -width / 2; @@ -40,12 +40,14 @@ export async function iconRounded( // @ts-expect-error -- Passing a D3.Selection seems to work for some reason const rc = rough.svg(shapeSvg); - const options = userNodeOverrides(node, { stroke: 'transparent' }); + const options = userNodeOverrides(node, {}); if (node.look !== 'handDrawn') { options.roughness = 0; options.fillStyle = 'solid'; } + const fill = stylesMap.get('fill'); + options.stroke = fill ?? mainBkg; const iconNode = rc.path(createRoundedRectPathD(x, y, width, height, 5), options); @@ -58,7 +60,7 @@ export async function iconRounded( stroke: 'none', }); - const iconShape = shapeSvg.insert(() => iconNode, ':first-child'); + const iconShape = shapeSvg.insert(() => iconNode, ':first-child').attr('class', 'icon-shape2'); const outerShape = shapeSvg.insert(() => outerNode); if (node.icon) { diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconSquare.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconSquare.ts index 8cbccb74d..e3536b89e 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconSquare.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconSquare.ts @@ -3,6 +3,7 @@ import { log } from '../../../logger.js'; import { getIconSVG } from '../../icons.js'; import type { Node, ShapeRenderOptions } from '../../types.js'; import intersect from '../intersect/index.js'; +import { createRoundedRectPathD } from './roundedRectPath.js'; import { compileStyles, styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; import { labelHelper, updateNodeBounds } from './util.js'; import type { D3Selection } from '../../../types.js'; @@ -29,7 +30,7 @@ export async function iconSquare( const height = iconSize + halfPadding * 2; const width = iconSize + halfPadding * 2; - const { nodeBorder } = themeVariables; + const { nodeBorder, mainBkg } = themeVariables; const { stylesMap } = compileStyles(node); const x = -width / 2; @@ -39,14 +40,16 @@ export async function iconSquare( // @ts-expect-error -- Passing a D3.Selection seems to work for some reason const rc = rough.svg(shapeSvg); - const options = userNodeOverrides(node, { stroke: 'transparent' }); + const options = userNodeOverrides(node, {}); if (node.look !== 'handDrawn') { options.roughness = 0; options.fillStyle = 'solid'; } + const fill = stylesMap.get('fill'); + options.stroke = fill ?? mainBkg; - const iconNode = rc.rectangle(x, y, width, height, options); + const iconNode = rc.path(createRoundedRectPathD(x, y, width, height, 0.1), options); const outerWidth = Math.max(width, bbox.width); const outerHeight = height + bbox.height + labelPadding; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/kanbanItem.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/kanbanItem.ts index 61dc3f85d..fba867a71 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/kanbanItem.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/kanbanItem.ts @@ -1,28 +1,33 @@ import { labelHelper, insertLabel, updateNodeBounds, getNodeClasses } from './util.js'; import intersect from '../intersect/index.js'; -import type { SVG } from '../../../diagram-api/types.js'; import type { Node, KanbanNode, ShapeRenderOptions } from '../../types.js'; import { createRoundedRectPathD } from './roundedRectPath.js'; import { userNodeOverrides, styles2String } from './handDrawnShapeStyles.js'; import rough from 'roughjs'; +import type { D3Selection } from '../../../types.js'; -const colorFromPriority = (priority: KanbanNode['priority']) => { +const colorFromPriority = (priority: NonNullable) => { switch (priority) { case 'Very High': return 'red'; case 'High': return 'orange'; + case 'Medium': + return null; // no stroke case 'Low': return 'blue'; case 'Very Low': return 'lightblue'; } }; -export const kanbanItem = async (parent: SVG, node: Node, { config }: ShapeRenderOptions) => { - const unknownNode = node as unknown; - const kanbanNode = unknownNode as KanbanNode; +export async function kanbanItem( + parent: D3Selection, + // Omit the 'shape' prop since otherwise, it causes a TypeScript circular dependency error + kanbanNode: Omit | Omit, + { config }: ShapeRenderOptions +) { const { labelStyles, nodeStyles } = styles2String(kanbanNode); - kanbanNode.labelStyle = labelStyles; + kanbanNode.labelStyle = labelStyles || ''; const labelPaddingX = 10; const orgWidth = kanbanNode.width; @@ -38,10 +43,10 @@ export const kanbanItem = async (parent: SVG, node: Node, { config }: ShapeRende let ticketUrl = ''; let link; - if (kanbanNode.ticket && config?.kanban?.ticketBaseUrl) { + if ('ticket' in kanbanNode && kanbanNode.ticket && config?.kanban?.ticketBaseUrl) { ticketUrl = config?.kanban?.ticketBaseUrl.replace('#TICKET#', kanbanNode.ticket); link = shapeSvg - .insert('svg:a', ':first-child') + .insert('svg:a', ':first-child') .attr('class', 'kanban-ticket-link') .attr('xlink:href', ticketUrl) .attr('target', '_blank'); @@ -49,21 +54,29 @@ export const kanbanItem = async (parent: SVG, node: Node, { config }: ShapeRende const options = { useHtmlLabels: kanbanNode.useHtmlLabels, - labelStyle: kanbanNode.labelStyle, + labelStyle: kanbanNode.labelStyle || '', width: kanbanNode.width, - icon: kanbanNode.icon, img: kanbanNode.img, - padding: kanbanNode.padding, + padding: kanbanNode.padding || 8, centerLabel: false, }; - const { label: labelEl, bbox: bbox2 } = await insertLabel( - link ? link : shapeSvg, - kanbanNode.ticket || '', - options - ); + let labelEl, bbox2; + if (link) { + ({ label: labelEl, bbox: bbox2 } = await insertLabel( + link, + ('ticket' in kanbanNode && kanbanNode.ticket) || '', + options + )); + } else { + ({ label: labelEl, bbox: bbox2 } = await insertLabel( + shapeSvg, + ('ticket' in kanbanNode && kanbanNode.ticket) || '', + options + )); + } const { label: labelElAssigned, bbox: bboxAssigned } = await insertLabel( shapeSvg, - kanbanNode.assigned || '', + ('assigned' in kanbanNode && kanbanNode.assigned) || '', options ); kanbanNode.width = orgWidth; @@ -107,21 +120,23 @@ export const kanbanItem = async (parent: SVG, node: Node, { config }: ShapeRende : rc.rectangle(x, y, totalWidth, totalHeight, options); rect = shapeSvg.insert(() => roughNode, ':first-child'); - rect.attr('class', 'basic label-container').attr('style', cssStyles); + rect.attr('class', 'basic label-container').attr('style', cssStyles ? cssStyles : null); } else { rect = shapeSvg.insert('rect', ':first-child'); rect .attr('class', 'basic label-container __APA__') .attr('style', nodeStyles) - .attr('rx', rx) - .attr('ry', ry) + .attr('rx', rx ?? 5) + .attr('ry', ry ?? 5) .attr('x', x) .attr('y', y) .attr('width', totalWidth) .attr('height', totalHeight); - if (kanbanNode.priority) { - const line = shapeSvg.append('line', ':first-child'); + + const priority = 'priority' in kanbanNode && kanbanNode.priority; + if (priority) { + const line = shapeSvg.append('line'); const lineX = x + 2; const y1 = y + Math.floor((rx ?? 0) / 2); @@ -133,7 +148,7 @@ export const kanbanItem = async (parent: SVG, node: Node, { config }: ShapeRende .attr('y2', y2) .attr('stroke-width', '4') - .attr('stroke', colorFromPriority(kanbanNode.priority)); + .attr('stroke', colorFromPriority(priority)); } } @@ -145,4 +160,4 @@ export const kanbanItem = async (parent: SVG, node: Node, { config }: ShapeRende }; return shapeSvg; -}; +} diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/note.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/note.ts index 403294783..4a7f66a87 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/note.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/note.ts @@ -4,6 +4,7 @@ import intersect from '../intersect/index.js'; import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js'; import type { D3Selection } from '../../../types.js'; +import { getConfig } from '../../../config.js'; export async function note( parent: D3Selection, @@ -12,16 +13,16 @@ export async function note( ) { const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; + const useHtmlLabels = node.useHtmlLabels || getConfig().flowchart?.htmlLabels !== false; + if (!useHtmlLabels) { + node.centerLabel = true; + } const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); const totalWidth = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); const totalHeight = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); const x = -totalWidth / 2; const y = -totalHeight / 2; const { cssStyles } = node; - const useHtmlLabels = node.useHtmlLabels; - if (!useHtmlLabels) { - node.centerLabel = true; - } // add the rect // @ts-ignore TODO: Fix rough typings diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/tiltedCylinder.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/tiltedCylinder.ts index f8a2fb52b..29f2c267f 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/tiltedCylinder.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/tiltedCylinder.ts @@ -125,7 +125,7 @@ export async function tiltedCylinder( ) { let x = rx * rx * (1 - (y * y) / (ry * ry)); if (x != 0) { - x = Math.sqrt(x); + x = Math.sqrt(Math.abs(x)); } x = rx - x; if (point.x - (node.x ?? 0) > 0) { diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.ts index 6a9db8c22..52471ecc0 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.ts @@ -10,11 +10,10 @@ import type { D3Selection, Point } from '../../../types.js'; export const labelHelper = async ( parent: D3Selection, node: Node, - _classes?: string, - _shapeSvg?: D3Selection + _classes?: string ) => { let cssClasses; - const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig()?.flowchart?.htmlLabels); + const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig()?.htmlLabels); if (!_classes) { cssClasses = 'node default'; } else { @@ -22,12 +21,10 @@ export const labelHelper = async ( } // Add outer g element - const shapeSvg = _shapeSvg - ? _shapeSvg - : parent - .insert('g') - .attr('class', cssClasses) - .attr('id', node.domId || node.id); + const shapeSvg = parent + .insert('g') + .attr('class', cssClasses) + .attr('id', node.domId || node.id); // Create the label and insert it after the rect const labelEl = shapeSvg @@ -55,7 +52,7 @@ export const labelHelper = async ( let bbox = text.getBBox(); const halfPadding = (node?.padding ?? 0) / 2; - if (evaluate(getConfig().flowchart?.htmlLabels)) { + if (useHtmlLabels) { const div = text.children[0]; const dv = select(text); @@ -119,7 +116,7 @@ export const labelHelper = async ( labelEl.insert('rect', ':first-child'); return { shapeSvg, bbox, halfPadding, label: labelEl }; }; -export const insertLabel = async ( +export const insertLabel = async ( parent: D3Selection, label: string, options: { @@ -136,7 +133,10 @@ export const insertLabel = async ( const useHtmlLabels = options.useHtmlLabels || evaluate(getConfig()?.flowchart?.htmlLabels); // Create the label and insert it after the rect - const labelEl = parent.insert('g').attr('class', 'label').attr('style', options.labelStyle); + const labelEl = parent + .insert('g') + .attr('class', 'label') + .attr('style', options.labelStyle || ''); const text = await createText(labelEl, sanitizeText(decodeEntities(label), getConfig()), { useHtmlLabels, diff --git a/packages/mermaid/src/rendering-util/types.ts b/packages/mermaid/src/rendering-util/types.ts index e49218f71..86cfd50b3 100644 --- a/packages/mermaid/src/rendering-util/types.ts +++ b/packages/mermaid/src/rendering-util/types.ts @@ -1,5 +1,6 @@ export type MarkdownWordType = 'normal' | 'strong' | 'em'; import type { MermaidConfig } from '../config.type.js'; +import type { ClusterShapeID } from './rendering-elements/clusters.js'; import type { ShapeID } from './rendering-elements/shapes.js'; export interface MarkdownWord { content: string; @@ -9,8 +10,7 @@ export type MarkdownLine = MarkdownWord[]; /** Returns `true` if the line fits a constraint (e.g. it's under ๐‘› chars) */ export type CheckFitFunction = (text: MarkdownLine) => boolean; -// Common properties for any node in the system -export interface Node { +interface BaseNode { id: string; label?: string; description?: string[]; @@ -38,7 +38,6 @@ export interface Node { linkTarget?: string; tooltip?: string; padding?: number; //REMOVE?, use from LayoutData.config - Keep, this could be shape specific - shape?: ShapeID; isGroup: boolean; width?: number; height?: number; @@ -75,6 +74,22 @@ export interface Node { constraint?: 'on' | 'off'; } +/** + * Group/cluster nodes, e.g. nodes that contain other nodes. + */ +export interface ClusterNode extends BaseNode { + shape?: ClusterShapeID; + isGroup: true; +} + +export interface NonClusterNode extends BaseNode { + shape?: ShapeID; + isGroup: false; +} + +// Common properties for any node in the system +export type Node = ClusterNode | NonClusterNode; + // Common properties for any edge in the system export interface Edge { id: string; @@ -118,9 +133,9 @@ export interface RectOptions { } // Extending the Node interface for specific types if needed -export interface ClassDiagramNode extends Node { +export type ClassDiagramNode = Node & { memberData: any; // Specific property for class diagram nodes -} +}; // Specific interfaces for layout and render data export interface LayoutData { @@ -154,11 +169,11 @@ export interface ShapeRenderOptions { dir?: Node['dir']; } -export interface KanbanNode extends Node { +export type KanbanNode = Node & { // Kanban specif data priority?: 'Very High' | 'High' | 'Medium' | 'Low' | 'Very Low'; ticket?: string; assigned?: string; icon?: string; level: number; -} +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a9bc7d75..7f3f4fa5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -226,9 +226,6 @@ importers: '@types/d3': specifier: ^7.4.3 version: 7.4.3 - '@types/dompurify': - specifier: ^3.0.5 - version: 3.0.5 cytoscape: specifier: ^3.29.2 version: 3.30.2 @@ -251,8 +248,8 @@ importers: specifier: ^1.11.10 version: 1.11.13 dompurify: - specifier: ^3.0.11 <3.1.7 - version: 3.1.6 + specifier: ^3.2.1 + version: 3.2.1 katex: specifier: ^0.16.9 version: 0.16.11 @@ -2768,9 +2765,6 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - '@types/dompurify@3.0.5': - resolution: {integrity: sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==} - '@types/estree@0.0.39': resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} @@ -4720,8 +4714,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.1.6: - resolution: {integrity: sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==} + dompurify@3.2.1: + resolution: {integrity: sha512-NBHEsc0/kzRYQd+AY6HR6B/IgsqzBABrqJbpCDQII/OK6h7B7LXzweZTDsqSW2LkTRpoxf18YUP+YjGySk6B3w==} domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} @@ -12125,10 +12119,6 @@ snapshots: dependencies: '@types/ms': 0.7.34 - '@types/dompurify@3.0.5': - dependencies: - '@types/trusted-types': 2.0.7 - '@types/estree@0.0.39': {} '@types/estree@1.0.6': {} @@ -12970,7 +12960,7 @@ snapshots: antlr4: 4.11.0 color-string: 1.9.1 dom-to-image-more: 2.16.0 - dompurify: 3.1.6 + dompurify: 3.2.1 file-saver: 2.0.5 highlight.js: 10.7.3 html-to-image: 1.11.11 @@ -14509,7 +14499,9 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.1.6: {} + dompurify@3.2.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 domutils@3.1.0: dependencies: diff --git a/scripts/tsc-check.ts b/scripts/tsc-check.ts index 7a5ff50a9..2e5b3016d 100644 --- a/scripts/tsc-check.ts +++ b/scripts/tsc-check.ts @@ -38,7 +38,6 @@ const SRC = { // to match the real `package.json` values 'type-fest': '*', '@types/d3': '^7.4.3', - '@types/dompurify': '^3.0.5', typescript: '*', }, },