Compare commits

..

19 Commits

Author SHA1 Message Date
omkarht
f2982cbcc3 Merge branch 'develop' into fix/sequence-diagram-autonumber-arrow-positioning 2025-11-11 12:35:06 +05:30
omkarht
0d1f6af383 fix: adjust line start position for bidirectional arrows
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-11-11 12:29:10 +05:30
Shubham P
6b9f26dac8 Merge pull request #7080 from mermaid-js/7079-c4context-componentqueue-ext-lexical-error
7079: add missing support for ComponentQueue_Ext in C4Context diagrams
2025-11-11 04:07:06 +00:00
Shubham P
ea590cdafe Merge pull request #7075 from mermaid-js/6889-fix-escaped-p-tags-in-sandbox-mode
6889: Fix escaped <p> tags in labels when securityLevel is set to "sandbox"
2025-11-11 04:05:37 +00:00
Shubham P
f3769c70bc Merge branch 'develop' into 7079-c4context-componentqueue-ext-lexical-error 2025-11-11 09:24:14 +05:30
Shubham P
4cf4d15197 Merge branch 'develop' into 6889-fix-escaped-p-tags-in-sandbox-mode 2025-11-11 09:23:23 +05:30
omkarht
e7c78dec87 fix : autonumber positioning for all types of arrows and central connections 2025-11-10 16:14:23 +05:30
Shubham P
c02cf92656 Merge pull request #7149 from mermaid-js/renovate/patch-dompurify
fix(deps): update dependency dompurify to ^3.3.0
2025-11-10 10:15:31 +00:00
Shubham P
3a1266892d Merge pull request #7148 from mermaid-js/renovate/patch-all-patch
fix(deps): update all patch dependencies (patch)
2025-11-10 10:15:16 +00:00
renovate[bot]
67e81de557 fix(deps): update dependency dompurify to ^3.3.0 2025-11-10 02:54:45 +00:00
renovate[bot]
847b3aa24e fix(deps): update all patch dependencies 2025-11-10 02:54:19 +00:00
Sidharth Vinod
85a13da40f Merge pull request #7138 from mermaid-js/update-timings
Update E2E Timings
2025-11-07 21:42:11 +09:00
darshanr0107
9ec0e8f932 Merge branch 'develop' of https://github.com/mermaid-js/mermaid into 6889-fix-escaped-p-tags-in-sandbox-mode 2025-11-07 11:58:38 +05:30
darshanr0107
9585ee7533 chore:add e2e test
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-11-07 11:46:41 +05:30
github-actions[bot]
1269486124 chore: update E2E timings 2025-11-07 04:15:01 +00:00
darshanr0107
983120d945 fix: add test case for C4Context diagram with ComponentQueue_Ext
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-11-05 17:18:34 +05:30
darshanr0107
835de0012d fix:ComponentQueue_Ext throws lexical error
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-14 19:00:17 +05:30
darshanr0107
96a766dcdb chore: added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-13 13:42:41 +05:30
darshanr0107
39d7ebd32e fix: escaped p tags in sandbox mode
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-10-13 13:16:58 +05:30
19 changed files with 1711 additions and 444 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Prevent HTML tags from being escaped in sandbox label rendering

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Support ComponentQueue_Ext to prevent parsing error

View File

@@ -98,12 +98,21 @@ export const openURLAndVerifyRendering = (
cy.visit(url);
cy.window().should('have.property', 'rendered', true);
cy.get('svg').should('be.visible');
// cspell:ignore viewbox
cy.get('svg').should('not.have.attr', 'viewbox');
if (validation) {
cy.get('svg').should(validation);
// Handle sandbox mode where SVG is inside an iframe
if (options.securityLevel === 'sandbox') {
cy.get('iframe').should('be.visible');
if (validation) {
cy.get('iframe').should(validation);
}
} else {
cy.get('svg').should('be.visible');
// cspell:ignore viewbox
cy.get('svg').should('not.have.attr', 'viewbox');
if (validation) {
cy.get('svg').should(validation);
}
}
if (screenshot) {

View File

@@ -114,4 +114,28 @@ describe('C4 diagram', () => {
{}
);
});
it('C4.6 should render C4Context diagram with ComponentQueue_Ext', () => {
imgSnapshotTest(
`
C4Context
title System Context diagram with ComponentQueue_Ext
Enterprise_Boundary(b0, "BankBoundary0") {
Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.")
System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.")
Enterprise_Boundary(b1, "BankBoundary") {
ComponentQueue_Ext(msgQueue, "Message Queue", "RabbitMQ", "External message queue system for processing banking transactions")
System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.")
}
}
BiRel(customerA, SystemAA, "Uses")
Rel(SystemAA, msgQueue, "Sends messages to")
Rel(SystemAA, SystemC, "Sends e-mails", "SMTP")
`,
{}
);
});
});

View File

@@ -79,6 +79,18 @@ describe('Flowchart v2', () => {
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
);
});
it('6a: should render complex HTML in labels with sandbox security', () => {
imgSnapshotTest(
`flowchart TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car]
`,
{ securityLevel: 'sandbox', flowchart: { htmlLabels: true } }
);
});
it('7: should render a flowchart when useMaxWidth is true (default)', () => {
renderGraph(
`flowchart TD

View File

@@ -776,5 +776,834 @@ describe('Sequence Diagram Special Cases', () => {
);
});
});
describe('Sequence Diagram Rendering with Autonumber and All Arrow Types', () => {
describe('Autonumber with All Arrow Types', () => {
it('should render all arrow types with autonumbering', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
Alice->>Bob: Solid arrow (->>)
Bob-->>Alice: Dotted arrow (-->>)
Alice->Charlie: Solid open arrow (->)
Charlie-->Dave: Dotted open arrow (-->)
Alice-xBob: Solid cross (-x)
Bob--xAlice: Dotted cross (--x)
Alice-)Charlie: Solid point async (-)
Charlie--)Dave: Dotted point async (--)
Alice<<->>Bob: Bidirectional solid (<<->>)
Charlie<<-->>Dave: Bidirectional dotted (<<-->>)
Alice-|\\Bob: Solid top half (-|\\)
Bob-|/Alice: Solid bottom half (-|/)
Alice-\\\\Charlie: Stick top half (-\\\\)
Charlie-//Dave: Stick bottom half (-//)
Dave/|-Charlie: Solid top reverse (/|-)
Charlie\\|-Bob: Solid bottom reverse (\\|-)
Bob//-Alice: Stick top reverse (//-)
Alice\\\\-Bob: Stick bottom reverse (\\\\-)
Alice--|\\Bob: Dotted solid top (--|\\)
Bob--|/Alice: Dotted solid bottom (--|/)
Alice--\\\\Charlie: Dotted stick top (--\\\\)
Charlie--//Dave: Dotted stick bottom (--//)
Dave/|--Charlie: Dotted solid top reverse (/|--)
Charlie\\|--Bob: Dotted solid bottom reverse (\\|--)
Bob//--Alice: Dotted stick top reverse (//--)
Alice\\\\--Bob: Dotted stick bottom reverse (\\\\--)
Alice->>()Bob: Solid with central connection
Bob()-->>Alice: Dotted with reverse central
Alice()->>()Charlie: Dual central connections`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render all arrow types with autonumbering - left to right only', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
Alice->>Bob: Solid arrow (->>)
Alice-->>Bob: Dotted arrow (-->>)
Alice->Bob: Solid open arrow (->)
Alice-->Bob: Dotted open arrow (-->)
Alice-xBob: Solid cross (-x)
Alice--xBob: Dotted cross (--x)
Alice-)Bob: Solid point async (-)
Alice--)Bob: Dotted point async (--)
Alice<<->>Bob: Bidirectional solid (<<->>)
Alice<<-->>Bob: Bidirectional dotted (<<-->>)
Alice-|\\Bob: Solid top half (-|\\)
Alice-|/Bob: Solid bottom half (-|/)
Alice-\\\\Bob: Stick top half (-\\\\)
Alice-//Bob: Stick bottom half (-//)
Bob/|-Alice: Solid top reverse (/|-)
Bob\\|-Alice: Solid bottom reverse (\\|-)
Bob//-Alice: Stick top reverse (//-)
Bob\\\\-Alice: Stick bottom reverse (\\\\-)
Alice--|\\Bob: Dotted solid top (--|\\)
Alice--|/Bob: Dotted solid bottom (--|/)
Alice--\\\\Bob: Dotted stick top (--\\\\)
Alice--//Bob: Dotted stick bottom (--//)
Bob/|--Alice: Dotted solid top reverse (/|--)
Bob\\|--Alice: Dotted solid bottom reverse (\\|--)
Bob//--Alice: Dotted stick top reverse (//--)
Bob\\\\--Alice: Dotted stick bottom reverse (\\\\--)
Alice->>()Bob: Solid with central connection
Alice()-->>Bob: Dotted with reverse central
Alice()->>()Bob: Dual central connections`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render central connections with autonumbering', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice->>()Bob: Central connection at destination
Bob()->>Alice: Reverse central at source
Alice()->>()Bob: Dual central connections
Bob->>()Charlie: Another central connection
Charlie()-->>Alice: Reverse central dotted
Alice()<<-->>()Bob: Dual central bidirectional`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render bidirectional arrows with autonumbering', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice<<->>Bob: Bidirectional solid left to right
Bob<<->>Alice: Bidirectional solid right to left
Alice<<-->>Charlie: Bidirectional dotted left to right
Charlie<<-->>Alice: Bidirectional dotted right to left
Bob<<->>Charlie: Bidirectional solid
Charlie<<-->>Bob: Bidirectional dotted`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render reverse arrows with autonumbering', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Bob/|-Alice: Solid top reverse left to right
Alice/|-Bob: Solid top reverse right to left
Bob\\|-Alice: Solid bottom reverse left to right
Alice\\|-Bob: Solid bottom reverse right to left
Bob//-Alice: Stick top reverse left to right
Alice//-Bob: Stick top reverse right to left
Bob\\\\-Alice: Stick bottom reverse left to right
Alice\\\\-Bob: Stick bottom reverse right to left
Bob/|--Alice: Dotted solid top reverse
Alice\\|--Bob: Dotted solid bottom reverse
Bob//--Alice: Dotted stick top reverse
Alice\\\\--Bob: Dotted stick bottom reverse`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
});
describe('Central Connections with Autonumber - Comprehensive Coverage', () => {
it('should render CENTRAL_CONNECTION with normal arrows - left to right', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice->>()Bob: Solid arrow with circle at destination
Alice-->>()Bob: Dotted arrow with circle at destination
Alice->()Bob: Open arrow with circle at destination
Alice--x()Bob: Cross arrow with circle at destination
Alice--)()Bob: Close arrow with circle at destination`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render CENTRAL_CONNECTION with normal arrows - right to left', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Bob->>()Alice: Solid arrow with circle at destination (RTL)
Charlie-->>()Bob: Dotted arrow with circle at destination (RTL)
Bob->()Alice: Open arrow with circle at destination (RTL)
Charlie--x()Alice: Cross arrow with circle at destination (RTL)
Bob--)()Alice: Close arrow with circle at destination (RTL)`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render CENTRAL_CONNECTION with reverse arrows - left to right', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Bob/|-()Alice: Solid top reverse with circle (LTR)
Bob\\|-()Alice: Solid bottom reverse with circle (LTR)
Bob//-()Alice: Stick top reverse with circle (LTR)
Bob\\\\-()Alice: Stick bottom reverse with circle (LTR)
Bob/|--()Alice: Dotted solid top reverse with circle (LTR)
Bob\\|--()Alice: Dotted solid bottom reverse with circle (LTR)
Bob//--()Alice: Dotted stick top reverse with circle (LTR)
Bob\\\\--()Alice: Dotted stick bottom reverse with circle (LTR)`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render CENTRAL_CONNECTION with reverse arrows - right to left', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice/|-()Bob: Solid top reverse with circle (RTL)
Alice\\|-()Bob: Solid bottom reverse with circle (RTL)
Alice//-()Bob: Stick top reverse with circle (RTL)
Alice\\\\-()Bob: Stick bottom reverse with circle (RTL)
Alice/|--()Bob: Dotted solid top reverse with circle (RTL)
Alice\\|--()Bob: Dotted solid bottom reverse with circle (RTL)
Alice//--()Bob: Dotted stick top reverse with circle (RTL)
Alice\\\\--()Bob: Dotted stick bottom reverse with circle (RTL)`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render Central_Connection_REVERSE ()->> normal LTR', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice()->>Bob: Circle at source with solid arrow
Alice()-->>Bob: Circle at source with dotted arrow
Alice()->Bob: Circle at source with open arrow
Alice()--xBob: Circle at source with cross arrow
Alice()--)Bob: Circle at source with close arrow`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render Central_Connection_REVERSE ()->> normal RTL', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Bob()->>Alice: Circle at source with solid arrow (RTL)
Charlie()-->>Bob: Circle at source with dotted arrow (RTL)
Bob()->Alice: Circle at source with open arrow (RTL)
Charlie()--xAlice: Circle at source with cross arrow (RTL)
Bob()--)Alice: Circle at source with close arrow (RTL)`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render Central_Connection_REVERSE ()->> reverse LTR', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Bob()/|-Alice: Circle at source with solid top reverse (LTR)
Bob()\\|-Alice: Circle at source with solid bottom reverse (LTR)
Bob()//-Alice: Circle at source with stick top reverse (LTR)
Bob()\\\\-Alice: Circle at source with stick bottom reverse (LTR)
Bob()/|--Alice: Circle at source with dotted solid top reverse (LTR)
Bob()\\|--Alice: Circle at source with dotted solid bottom reverse (LTR)
Bob()//--Alice: Circle at source with dotted stick top reverse (LTR)
Bob()\\\\--Alice: Circle at source with dotted stick bottom reverse (LTR)`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render Central_Connection_REVERSE ()->> reverse RTL', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice()/|-Bob: Circle at source with solid top reverse (RTL)
Alice()\\|-Bob: Circle at source with solid bottom reverse (RTL)
Alice()//-Bob: Circle at source with stick top reverse (RTL)
Alice()\\\\-Bob: Circle at source with stick bottom reverse (RTL)
Alice()/|--Bob: Circle at source with dotted solid top reverse (RTL)
Alice()\\|--Bob: Circle at source with dotted solid bottom reverse (RTL)
Alice()//--Bob: Circle at source with dotted stick top reverse (RTL)
Alice()\\\\--Bob: Circle at source with dotted stick bottom reverse (RTL)`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render Central_Connection_DUAL ()->>() normal LTR', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice()->>()Bob: Circles at both ends with solid arrow
Alice()-->>()Bob: Circles at both ends with dotted arrow
Alice()->()Bob: Circles at both ends with open arrow
Alice()--x()Bob: Circles at both ends with cross arrow
Alice()--)()Bob: Circles at both ends with close arrow`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render Central_Connection_DUAL ()->>() normal RTL', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Bob()->>()Alice: Circles at both ends with solid arrow (RTL)
Charlie()-->>()Bob: Circles at both ends with dotted arrow (RTL)
Bob()->()Alice: Circles at both ends with open arrow (RTL)
Charlie()--x()Alice: Circles at both ends with cross arrow (RTL)
Bob()--)()Alice: Circles at both ends with close arrow (RTL)`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render Central_Connection_DUAL ()->>() reverse LTR', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Bob()/|-()Alice: Circles at both ends with solid top reverse (LTR)
Bob()\\|-()Alice: Circles at both ends with solid bottom reverse (LTR)
Bob()//-()Alice: Circles at both ends with stick top reverse (LTR)
Bob()\\\\-()Alice: Circles at both ends with stick bottom reverse (LTR)
Bob()/|--()Alice: Circles at both ends with dotted solid top reverse (LTR)
Bob()\\|--()Alice: Circles at both ends with dotted solid bottom reverse (LTR)
Bob()//--()Alice: Circles at both ends with dotted stick top reverse (LTR)
Bob()\\\\--()Alice: Circles at both ends with dotted stick bottom reverse (LTR)`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render Central_Connection_DUAL ()->>() reverse RTL', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice()/|-()Bob: Circles at both ends with solid top reverse (RTL)
Alice()\\|-()Bob: Circles at both ends with solid bottom reverse (RTL)
Alice()//-()Bob: Circles at both ends with stick top reverse (RTL)
Alice()\\\\-()Bob: Circles at both ends with stick bottom reverse (RTL)
Alice()/|--()Bob: Circles at both ends with dotted solid top reverse (RTL)
Alice()\\|--()Bob: Circles at both ends with dotted solid bottom reverse (RTL)
Alice()//--()Bob: Circles at both ends with dotted stick top reverse (RTL)
Alice()\\\\--()Bob: Circles at both ends with dotted stick bottom reverse (RTL)`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render mixed central connections with autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
participant David
Note over Alice,David: Normal arrows with central connections
Alice->>()Bob: CENTRAL_CONNECTION LTR
Bob->>()Alice: CENTRAL_CONNECTION RTL
Alice()->>Bob: CENTRAL_CONNECTION_REVERSE LTR
Bob()->>Alice: CENTRAL_CONNECTION_REVERSE RTL
Alice()->>()Bob: CENTRAL_CONNECTION_DUAL LTR
Bob()->>()Alice: CENTRAL_CONNECTION_DUAL RTL
Note over Alice,David: Reverse arrows with central connections
Bob/|-()Alice: Reverse with CENTRAL_CONNECTION LTR
Alice/|-()Bob: Reverse with CENTRAL_CONNECTION RTL
Bob()/|-Alice: Reverse with CENTRAL_CONNECTION_REVERSE LTR
Alice()/|-Bob: Reverse with CENTRAL_CONNECTION_REVERSE RTL
Bob()/|-()Alice: Reverse with CENTRAL_CONNECTION_DUAL LTR
Alice()/|-()Bob: Reverse with CENTRAL_CONNECTION_DUAL RTL
Note over Alice,David: Mixed with different participants
Alice->>()Charlie: Skip participant
Charlie()->>Alice: Back skip
Bob()->>()David: Another skip
David()->>()Bob: Return skip`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render central connections with bidirectional arrows and autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice()<<->>()Bob: Dual central with bidirectional solid LTR
Bob()<<->>()Alice: Dual central with bidirectional solid RTL
Alice()<<-->>()Bob: Dual central with bidirectional dotted LTR
Bob()<<-->>()Alice: Dual central with bidirectional dotted RTL
Alice<<->>()Bob: Central at end with bidirectional LTR
Bob()<<->>Alice: Central at start with bidirectional RTL`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
});
describe('Self-Reference Arrows - Comprehensive Coverage', () => {
it('should render self-reference with normal arrows - without autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
participant Alice
participant Bob
participant Charlie
Alice->>Alice: Solid arrow self-reference
Bob-->>Bob: Dotted arrow self-reference
Charlie->Charlie: Open arrow self-reference
Alice-->Alice: Dotted open arrow self-reference
Bob-xBob: Cross arrow self-reference
Charlie--xCharlie: Dotted cross self-reference`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference with normal arrows - with autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice->>Alice: Solid arrow self-reference
Bob-->>Bob: Dotted arrow self-reference
Charlie->Charlie: Open arrow self-reference
Alice-->Alice: Dotted open arrow self-reference
Bob-xBob: Cross arrow self-reference
Charlie--xCharlie: Dotted cross self-reference`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference with reverse arrows - without autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
participant Alice
participant Bob
participant Charlie
Alice/|-Alice: Solid top reverse self-reference
Bob\\|-Bob: Solid bottom reverse self-reference
Charlie//-Charlie: Stick top reverse self-reference
Alice\\\\-Alice: Stick bottom reverse self-reference
Bob/|--Bob: Dotted solid top reverse self-reference
Charlie\\|--Charlie: Dotted solid bottom reverse self-reference
Alice//--Alice: Dotted stick top reverse self-reference
Bob\\\\--Bob: Dotted stick bottom reverse self-reference`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference with reverse arrows - with autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice/|-Alice: Solid top reverse self-reference
Bob\\|-Bob: Solid bottom reverse self-reference
Charlie//-Charlie: Stick top reverse self-reference
Alice\\\\-Alice: Stick bottom reverse self-reference
Bob/|--Bob: Dotted solid top reverse self-reference
Charlie\\|--Charlie: Dotted solid bottom reverse self-reference
Alice//--Alice: Dotted stick top reverse self-reference
Bob\\\\--Bob: Dotted stick bottom reverse self-reference`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference with bidirectional arrows - without autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
participant Alice
participant Bob
participant Charlie
Alice<<->>Alice: Bidirectional solid self-reference
Bob<<-->>Bob: Bidirectional dotted self-reference
Charlie<<->>Charlie: Another bidirectional solid
Alice<<-->>Alice: Another bidirectional dotted`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference with bidirectional arrows - with autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice<<->>Alice: Bidirectional solid self-reference
Bob<<-->>Bob: Bidirectional dotted self-reference
Charlie<<->>Charlie: Another bidirectional solid
Alice<<-->>Alice: Another bidirectional dotted`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference with CENTRAL_CONNECTION - without autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
participant Alice
participant Bob
participant Charlie
Alice->>()Alice: Solid arrow with circle at destination
Bob-->>()Bob: Dotted arrow with circle at destination
Charlie->()Charlie: Open arrow with circle at destination
Alice--x()Alice: Cross arrow with circle at destination
Bob--)()Bob: Close arrow with circle at destination`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference with CENTRAL_CONNECTION - with autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice->>()Alice: Solid arrow with circle at destination
Bob-->>()Bob: Dotted arrow with circle at destination
Charlie->()Charlie: Open arrow with circle at destination
Alice--x()Alice: Cross arrow with circle at destination
Bob--)()Bob: Close arrow with circle at destination`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference with CENTRAL_CONNECTION_REVERSE - without autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
participant Alice
participant Bob
participant Charlie
Alice()->>Alice: Circle at source with solid arrow
Bob()-->>Bob: Circle at source with dotted arrow
Charlie()->Charlie: Circle at source with open arrow
Alice()--xAlice: Circle at source with cross arrow
Bob()--)Bob: Circle at source with close arrow`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference with CENTRAL_CONNECTION_REVERSE - with autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice()->>Alice: Circle at source with solid arrow
Bob()-->>Bob: Circle at source with dotted arrow
Charlie()->Charlie: Circle at source with open arrow
Alice()--xAlice: Circle at source with cross arrow
Bob()--)Bob: Circle at source with close arrow`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference with CENTRAL_CONNECTION_DUAL - without autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
participant Alice
participant Bob
participant Charlie
Alice()->>()Alice: Circles at both ends with solid arrow
Bob()-->>()Bob: Circles at both ends with dotted arrow
Charlie()->()Charlie: Circles at both ends with open arrow
Alice()--x()Alice: Circles at both ends with cross arrow
Bob()--)()Bob: Circles at both ends with close arrow`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference with CENTRAL_CONNECTION_DUAL - with autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice()->>()Alice: Circles at both ends with solid arrow
Bob()-->>()Bob: Circles at both ends with dotted arrow
Charlie()->()Charlie: Circles at both ends with open arrow
Alice()--x()Alice: Circles at both ends with cross arrow
Bob()--)()Bob: Circles at both ends with close arrow`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference with reverse arrows and CENTRAL_CONNECTION', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
participant Alice
participant Bob
participant Charlie
Alice/|-()Alice: Solid top reverse with circle at destination
Bob\\|-()Bob: Solid bottom reverse with circle at destination
Charlie//-()Charlie: Stick top reverse with circle at destination
Alice\\\\-()Alice: Stick bottom reverse with circle at destination
Bob/|--()Bob: Dotted solid top reverse with circle at destination
Charlie\\|--()Charlie: Dotted solid bottom reverse with circle at destination
Alice//--()Alice: Dotted stick top reverse with circle at destination
Bob\\\\--()Bob: Dotted stick bottom reverse with circle at destination`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference with reverse arrows and CENTRAL_CONNECTION - with autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice/|-()Alice: Solid top reverse with circle at destination
Bob\\|-()Bob: Solid bottom reverse with circle at destination
Charlie//-()Charlie: Stick top reverse with circle at destination
Alice\\\\-()Alice: Stick bottom reverse with circle at destination
Bob/|--()Bob: Dotted solid top reverse with circle at destination
Charlie\\|--()Charlie: Dotted solid bottom reverse with circle at destination
Alice//--()Alice: Dotted stick top reverse with circle at destination
Bob\\\\--()Bob: Dotted stick bottom reverse with circle at destination`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-ref reverse Central_Connection_REVERSE no-autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
participant Alice
participant Bob
participant Charlie
Alice()/|-Alice: Circle at source with solid top reverse
Bob()\\|-Bob: Circle at source with solid bottom reverse
Charlie()//-Charlie: Circle at source with stick top reverse
Alice()\\\\-Alice: Circle at source with stick bottom reverse
Bob()/|--Bob: Circle at source with dotted solid top reverse
Charlie()\\|--Charlie: Circle at source with dotted solid bottom reverse
Alice()//--Alice: Circle at source with dotted stick top reverse
Bob()\\\\--Bob: Circle at source with dotted stick bottom reverse`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-ref reverse Central_Connection_REVERSE autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice()/|-Alice: Circle at source with solid top reverse
Bob()\\|-Bob: Circle at source with solid bottom reverse
Charlie()//-Charlie: Circle at source with stick top reverse
Alice()\\\\-Alice: Circle at source with stick bottom reverse
Bob()/|--Bob: Circle at source with dotted solid top reverse
Charlie()\\|--Charlie: Circle at source with dotted solid bottom reverse
Alice()//--Alice: Circle at source with dotted stick top reverse
Bob()\\\\--Bob: Circle at source with dotted stick bottom reverse`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-ref reverse Central_Connection_DUAL no-autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
participant Alice
participant Bob
participant Charlie
Alice()/|-()Alice: Circles at both ends with solid top reverse
Bob()\\|-()Bob: Circles at both ends with solid bottom reverse
Charlie()//-()Charlie: Circles at both ends with stick top reverse
Alice()\\\\-()Alice: Circles at both ends with stick bottom reverse
Bob()/|--()Bob: Circles at both ends with dotted solid top reverse
Charlie()\\|--()Charlie: Circles at both ends with dotted solid bottom reverse
Alice()//--()Alice: Circles at both ends with dotted stick top reverse
Bob()\\\\--()Bob: Circles at both ends with dotted stick bottom reverse`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-references, reverse arrows & dual central connection (autonumber).', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice()/|-()Alice: Circles at both ends with solid top reverse
Bob()\\|-()Bob: Circles at both ends with solid bottom reverse
Charlie()//-()Charlie: Circles at both ends with stick top reverse
Alice()\\\\-()Alice: Circles at both ends with stick bottom reverse
Bob()/|--()Bob: Circles at both ends with dotted solid top reverse
Charlie()\\|--()Charlie: Circles at both ends with dotted solid bottom reverse
Alice()//--()Alice: Circles at both ends with dotted stick top reverse
Bob()\\\\--()Bob: Circles at both ends with dotted stick bottom reverse`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('Render self-references with bidirectional central connections (no autonumber).', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
participant Alice
participant Bob
participant Charlie
Alice()<<->>()Alice: Dual central with bidirectional solid
Bob()<<-->>()Bob: Dual central with bidirectional dotted
Charlie<<->>()Alice: Central at end with bidirectional
Bob()<<->>Bob: Central at start with bidirectional`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference with bidirectional and central connections - with autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice()<<->>()Alice: Dual central with bidirectional solid
Bob()<<-->>()Bob: Dual central with bidirectional dotted
Charlie<<->>()Charlie: Central at end with bidirectional
Bob()<<->>Bob: Central at start with bidirectional`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render comprehensive self-reference scenario - all arrow types mixed', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Note over Alice,Charlie: Normal arrows
Alice->>Alice: Normal solid
Bob-->>Bob: Normal dotted
Charlie->Charlie: Normal open
Note over Alice,Charlie: Reverse arrows
Alice/|-Alice: Reverse solid top
Bob\\|-Bob: Reverse solid bottom
Note over Alice,Charlie: Bidirectional arrows
Charlie<<->>Charlie: Bidirectional solid
Alice<<-->>Alice: Bidirectional dotted
Note over Alice,Charlie: Central connections
Bob->>()Bob: Central at destination
Charlie()->>Charlie: Central at source
Alice()->>()Alice: Dual central
Note over Alice,Charlie: Reverse with central
Bob()/|-()Bob: Reverse with dual central
Charlie/|-()Charlie: Reverse with central at destination
Note over Alice,Charlie: Bidirectional with central
Alice()<<->>()Alice: Bidirectional with dual central`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
it('should render self-reference mixed with regular messages and autonumber', () => {
imgSnapshotTest(
`%%{init: {'theme':'base'}}%%
sequenceDiagram
autonumber
participant Alice
participant Bob
participant Charlie
Alice->>Bob: Regular message
Bob->>Bob: Self-reference solid
Bob-->>Charlie: Regular dotted
Charlie()->>()Charlie: Self-ref dual central
Charlie->>Alice: Regular back
Alice<<->>Alice: Self-ref bidirectional
Alice()->>Bob: Regular with central
Bob()/|-()Bob: Self-ref reverse dual central
Bob-->>Alice: Regular dotted back`,
{ sequence: { diagramMarginX: 50, diagramMarginY: 10 } }
);
});
});
});
});
});

View File

@@ -2,227 +2,227 @@
"durations": [
{
"spec": "cypress/integration/other/configuration.spec.js",
"duration": 5841
"duration": 6099
},
{
"spec": "cypress/integration/other/external-diagrams.spec.js",
"duration": 2138
"duration": 2236
},
{
"spec": "cypress/integration/other/ghsa.spec.js",
"duration": 3370
"duration": 3405
},
{
"spec": "cypress/integration/other/iife.spec.js",
"duration": 2052
"duration": 2176
},
{
"spec": "cypress/integration/other/interaction.spec.js",
"duration": 12243
"duration": 12300
},
{
"spec": "cypress/integration/other/rerender.spec.js",
"duration": 2065
"duration": 2089
},
{
"spec": "cypress/integration/other/xss.spec.js",
"duration": 31288
"duration": 32033
},
{
"spec": "cypress/integration/rendering/appli.spec.js",
"duration": 3421
"duration": 3672
},
{
"spec": "cypress/integration/rendering/architecture.spec.ts",
"duration": 97
"duration": 103
},
{
"spec": "cypress/integration/rendering/block.spec.js",
"duration": 18500
"duration": 18135
},
{
"spec": "cypress/integration/rendering/c4.spec.js",
"duration": 5793
"duration": 5661
},
{
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
"duration": 40966
"duration": 41456
},
{
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
"duration": 39176
"duration": 38910
},
{
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
"duration": 23468
"duration": 24120
},
{
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
"duration": 38291
"duration": 38454
},
{
"spec": "cypress/integration/rendering/classDiagram.spec.js",
"duration": 16949
"duration": 17099
},
{
"spec": "cypress/integration/rendering/conf-and-directives.spec.js",
"duration": 9480
"duration": 9844
},
{
"spec": "cypress/integration/rendering/current.spec.js",
"duration": 2753
"duration": 2951
},
{
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
"duration": 88028
"duration": 90081
},
{
"spec": "cypress/integration/rendering/erDiagram.spec.js",
"duration": 15615
"duration": 19496
},
{
"spec": "cypress/integration/rendering/errorDiagram.spec.js",
"duration": 3706
"duration": 3829
},
{
"spec": "cypress/integration/rendering/flowchart-elk.spec.js",
"duration": 43905
"duration": 42517
},
{
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
"duration": 31217
"duration": 31541
},
{
"spec": "cypress/integration/rendering/flowchart-icon.spec.js",
"duration": 7531
"duration": 7749
},
{
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
"duration": 25423
"duration": 25230
},
{
"spec": "cypress/integration/rendering/flowchart-v2.spec.js",
"duration": 49664
"duration": 49359
},
{
"spec": "cypress/integration/rendering/flowchart.spec.js",
"duration": 32525
"duration": 33028
},
{
"spec": "cypress/integration/rendering/gantt.spec.js",
"duration": 20915
"duration": 22271
},
{
"spec": "cypress/integration/rendering/gitGraph.spec.js",
"duration": 53556
"duration": 51837
},
{
"spec": "cypress/integration/rendering/iconShape.spec.ts",
"duration": 283038
"duration": 285060
},
{
"spec": "cypress/integration/rendering/imageShape.spec.ts",
"duration": 59434
"duration": 59517
},
{
"spec": "cypress/integration/rendering/info.spec.ts",
"duration": 3101
"duration": 3501
},
{
"spec": "cypress/integration/rendering/journey.spec.js",
"duration": 7099
"duration": 7405
},
{
"spec": "cypress/integration/rendering/kanban.spec.ts",
"duration": 7567
"duration": 7975
},
{
"spec": "cypress/integration/rendering/katex.spec.js",
"duration": 3817
"duration": 4312
},
{
"spec": "cypress/integration/rendering/marker_unique_id.spec.js",
"duration": 2624
"duration": 2630
},
{
"spec": "cypress/integration/rendering/mindmap-tidy-tree.spec.js",
"duration": 4246
"duration": 4541
},
{
"spec": "cypress/integration/rendering/mindmap.spec.ts",
"duration": 11967
"duration": 12134
},
{
"spec": "cypress/integration/rendering/newShapes.spec.ts",
"duration": 151914
"duration": 151160
},
{
"spec": "cypress/integration/rendering/oldShapes.spec.ts",
"duration": 116698
"duration": 118044
},
{
"spec": "cypress/integration/rendering/packet.spec.ts",
"duration": 4967
"duration": 5166
},
{
"spec": "cypress/integration/rendering/pie.spec.ts",
"duration": 6700
"duration": 7074
},
{
"spec": "cypress/integration/rendering/quadrantChart.spec.js",
"duration": 8963
"duration": 9518
},
{
"spec": "cypress/integration/rendering/radar.spec.js",
"duration": 5540
"duration": 5846
},
{
"spec": "cypress/integration/rendering/requirement.spec.js",
"duration": 2782
"duration": 3089
},
{
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
"duration": 54797
"duration": 55361
},
{
"spec": "cypress/integration/rendering/sankey.spec.ts",
"duration": 6914
"duration": 7236
},
{
"spec": "cypress/integration/rendering/sequencediagram-v2.spec.js",
"duration": 20481
"duration": 26057
},
{
"spec": "cypress/integration/rendering/sequencediagram.spec.js",
"duration": 38490
"duration": 48401
},
{
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
"duration": 30766
"duration": 30364
},
{
"spec": "cypress/integration/rendering/stateDiagram.spec.js",
"duration": 16705
"duration": 16862
},
{
"spec": "cypress/integration/rendering/theme.spec.js",
"duration": 30928
"duration": 30553
},
{
"spec": "cypress/integration/rendering/timeline.spec.ts",
"duration": 8424
"duration": 8962
},
{
"spec": "cypress/integration/rendering/treemap.spec.ts",
"duration": 12533
"duration": 12486
},
{
"spec": "cypress/integration/rendering/xyChart.spec.js",
"duration": 21197
"duration": 21718
},
{
"spec": "cypress/integration/rendering/zenuml.spec.js",
"duration": 3455
"duration": 3882
}
]
}

View File

@@ -63,8 +63,8 @@
]
},
"devDependencies": {
"@applitools/eyes-cypress": "^3.55.4",
"@argos-ci/cypress": "^6.1.5",
"@applitools/eyes-cypress": "^3.56.3",
"@argos-ci/cypress": "^6.2.1",
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.29.7",
"@cspell/eslint-plugin": "^9.3.0",
@@ -77,7 +77,7 @@
"@types/jsdom": "^21.1.7",
"@types/lodash": "^4.17.20",
"@types/mdast": "^4.0.4",
"@types/node": "^22.18.13",
"@types/node": "^22.19.0",
"@types/rollup-plugin-visualizer": "^5.0.3",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/spy": "^3.2.4",
@@ -88,7 +88,7 @@
"cors": "^2.8.5",
"cpy-cli": "^5.0.0",
"cross-env": "^7.0.3",
"cspell": "^9.2.2",
"cspell": "^9.3.0",
"cypress": "^14.5.4",
"cypress-image-snapshot": "^4.0.1",
"cypress-split": "^1.24.25",
@@ -127,7 +127,7 @@
"tsx": "^4.20.6",
"typescript": "~5.7.3",
"typescript-eslint": "^8.38.0",
"vite": "^7.0.7",
"vite": "^7.0.8",
"vite-plugin-istanbul": "^7.0.0",
"vitest": "^3.2.4"
},

View File

@@ -78,7 +78,7 @@
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.13",
"dayjs": "^1.11.19",
"dompurify": "^3.2.7",
"dompurify": "^3.3.0",
"katex": "^0.16.25",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",

View File

@@ -0,0 +1,58 @@
import c4Db from '../c4Db.js';
import c4 from './c4Diagram.jison';
import { setConfig } from '../../../config.js';
setConfig({
securityLevel: 'strict',
});
describe.each([
['Component', 'component'],
['ComponentDb', 'component_db'],
['ComponentQueue', 'component_queue'],
['Component_Ext', 'external_component'],
['ComponentDb_Ext', 'external_component_db'],
['ComponentQueue_Ext', 'external_component_queue'],
])('parsing a C4 %s', function (macroName, elementName) {
beforeEach(function () {
c4.parser.yy = c4Db;
c4.parser.yy.clear();
});
it('should parse a C4 diagram with one Component correctly', function () {
c4.parser.parse(`C4Component
title Component diagram for Internet Banking Component
${macroName}(ComponentAA, "Internet Banking Component", "Technology", "Allows customers to view information about their bank accounts, and make payments.")`);
const yy = c4.parser.yy;
const shapes = yy.getC4ShapeArray();
expect(shapes.length).toBe(1);
const onlyShape = shapes[0];
expect(onlyShape).toMatchObject({
alias: 'ComponentAA',
descr: {
text: 'Allows customers to view information about their bank accounts, and make payments.',
},
label: {
text: 'Internet Banking Component',
},
techn: {
text: 'Technology',
},
typeC4Shape: {
text: elementName,
},
});
});
it('should handle a trailing whitespaces after Component', function () {
const whitespace = ' ';
const rendered = c4.parser.parse(`C4Component${whitespace}
title Component diagram for Internet Banking Component${whitespace}
${macroName}(ComponentAA, "Internet Banking Component", "Technology", "Allows customers to view information about their bank accounts, and make payments.")${whitespace}`);
expect(rendered).toBe(true);
});
});

View File

@@ -158,10 +158,10 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
"UpdateRelStyle" { this.begin("update_rel_style"); return 'UPDATE_REL_STYLE';}
"UpdateLayoutConfig" { this.begin("update_layout_config"); return 'UPDATE_LAYOUT_CONFIG';}
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config><<EOF>> return "EOF_IN_STRUCT";
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(][ ]*[,] { this.begin("attribute"); return "ATTRIBUTE_EMPTY";}
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(] { this.begin("attribute"); }
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config,attribute>[)] { this.popState();this.popState();}
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext_queue,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config><<EOF>> return "EOF_IN_STRUCT";
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext_queue,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(][ ]*[,] { this.begin("attribute"); return "ATTRIBUTE_EMPTY";}
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext_queue,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(] { this.begin("attribute"); }
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext_queue,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config,attribute>[)] { this.popState();this.popState();}
<attribute>",," { return 'ATTRIBUTE_EMPTY';}
<attribute>"," { }

View File

@@ -70,6 +70,31 @@ describe('Sanitize text', () => {
});
expect(result).not.toContain('javascript:alert(1)');
});
it('should allow HTML tags in sandbox mode', () => {
const htmlStr = '<p>This is a <strong>bold</strong> text</p>';
const result = sanitizeText(htmlStr, {
securityLevel: 'sandbox',
flowchart: { htmlLabels: true },
});
expect(result).toContain('<p>');
expect(result).toContain('<strong>');
expect(result).toContain('</strong>');
expect(result).toContain('</p>');
});
it('should remove script tags in sandbox mode', () => {
const maliciousStr = '<p>Hello <script>alert(1)</script> world</p>';
const result = sanitizeText(maliciousStr, {
securityLevel: 'sandbox',
flowchart: { htmlLabels: true },
});
expect(result).not.toContain('<script>');
expect(result).not.toContain('alert(1)');
expect(result).toContain('<p>');
expect(result).toContain('Hello');
expect(result).toContain('world');
});
});
describe('generic parser', () => {

View File

@@ -66,7 +66,7 @@ export const removeScript = (txt: string): string => {
const sanitizeMore = (text: string, config: MermaidConfig) => {
if (config.flowchart?.htmlLabels !== false) {
const level = config.securityLevel;
if (level === 'antiscript' || level === 'strict') {
if (level === 'antiscript' || level === 'strict' || level === 'sandbox') {
text = removeScript(text);
} else if (level !== 'loose') {
text = breakToPlaceholder(text);

View File

@@ -294,11 +294,25 @@ const drawCentralConnection = function (
const actors = diagObj.db.getActors();
const fromActor = actors.get(msg.from);
const toActor = actors.get(msg.to);
const fromCenter = fromActor.x + fromActor.width / 2;
const toCenter = toActor.x + toActor.width / 2;
const isAutoNumberOn = msgModel.sequenceVisible;
let fromCenter = fromActor.x + fromActor.width / 2;
let toCenter = toActor.x + toActor.width / 2;
// Determine arrow direction: left-to-right or right-to-left
const isLeftToRight = fromCenter <= toCenter;
const isReverse = isReverseArrowType(msg, diagObj);
const g = elem.append('g');
const CENTRAL_CONNECTION_CIRCLE_OFFSET = 16.5;
const getCircleOffset = (isLeftToRight: boolean, isReverse: boolean) => {
const baseOffset = isLeftToRight
? CENTRAL_CONNECTION_CIRCLE_OFFSET
: -CENTRAL_CONNECTION_CIRCLE_OFFSET;
return isReverse ? -baseOffset : baseOffset;
};
const drawCircle = (cx: number) => {
g.append('circle')
.attr('cx', cx)
@@ -311,6 +325,37 @@ const drawCentralConnection = function (
const { CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL } =
diagObj.db.LINETYPE;
// Calculate circle position adjustments when autonumber is enabled
if (isAutoNumberOn) {
switch (msg.centralConnection) {
case CENTRAL_CONNECTION:
// Pattern: actor ->>() actor - circle at destination
if (isReverse) {
toCenter += getCircleOffset(isLeftToRight, true);
}
// No adjustment for normal arrows
break;
case CENTRAL_CONNECTION_REVERSE:
// Pattern: actor ()->> actor - circle at source
if (!isReverse) {
fromCenter += getCircleOffset(isLeftToRight, false);
}
// No adjustment for reverse arrows
break;
case CENTRAL_CONNECTION_DUAL:
// Pattern: actor ()->>() actor - circles at both ends
if (isReverse) {
toCenter += getCircleOffset(isLeftToRight, true);
} else {
fromCenter += getCircleOffset(isLeftToRight, false);
}
break;
}
}
// Draw circles based on central connection type
switch (msg.centralConnection) {
case CENTRAL_CONNECTION:
drawCircle(toCenter);
@@ -438,12 +483,17 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
let line;
if (startx === stopx) {
const isAutoNumberOn = sequenceVisible || conf.showSequenceNumbers;
const isReverse = isReverseArrowType(msg, diagObj);
const isBidirectional = isBidirectionalArrowType(msg, diagObj);
const lineStartX = startx + (isAutoNumberOn && (isReverse || isBidirectional) ? 10 : 0);
if (conf.rightAngles) {
line = diagram
.append('path')
.attr(
'd',
`M ${startx},${lineStartY} H ${
`M ${lineStartX},${lineStartY} H ${
startx + common.getMax(conf.width / 2, textWidth / 2)
} V ${lineStartY + 25} H ${startx}`
);
@@ -453,11 +503,11 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
.attr(
'd',
'M ' +
startx +
lineStartX +
',' +
lineStartY +
' C ' +
(startx + 60) +
(lineStartX + 60) +
',' +
(lineStartY - 10) +
' ' +
@@ -590,38 +640,75 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE ||
type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED;
let x = 0;
if (isBidirectional || isReverseArrowType) {
const SEQUENCE_NUMBER_RADIUS = 6;
const SEQUENCE_NUMBER_RADIUS = 6;
const hasCentralConn = hasCentralConnection(msg, diagObj);
let lineStartX = startx;
let lineStopX = stopx;
if (isBidirectional) {
// For bidirectional arrows, adjust the start position
if (startx < stopx) {
line.attr('x1', startx + 2 * SEQUENCE_NUMBER_RADIUS);
lineStartX = startx + SEQUENCE_NUMBER_RADIUS * 2;
} else {
line.attr('x1', startx + SEQUENCE_NUMBER_RADIUS);
lineStartX = startx - SEQUENCE_NUMBER_RADIUS + (hasCentralConn ? -5 : 0);
lineStartX +=
msg?.centralConnection === diagObj.db.LINETYPE.CENTRAL_CONNECTION_DUAL ||
msg?.centralConnection === diagObj.db.LINETYPE.CENTRAL_CONNECTION_REVERSE
? -7.5
: 0;
}
x = 3.5;
line.attr('x1', lineStartX);
} else if (isReverseArrowType) {
// For reverse arrows, adjust the stop position (where the arrowhead is)
if (stopx > startx) {
lineStopX = stopx - 2 * SEQUENCE_NUMBER_RADIUS;
} else {
lineStopX = stopx - SEQUENCE_NUMBER_RADIUS;
lineStartX +=
msg?.centralConnection === diagObj.db.LINETYPE.CENTRAL_CONNECTION_DUAL ||
msg?.centralConnection === diagObj.db.LINETYPE.CENTRAL_CONNECTION_REVERSE
? -7.5
: 0;
}
lineStopX += hasCentralConn ? 15 : 0;
line.attr('x2', lineStopX);
line.attr('x1', lineStartX);
} else {
line.attr('x1', startx + SEQUENCE_NUMBER_RADIUS);
}
// Calculate autonumber X position
let autonumberX = 0;
const isSelfMessage = startx === stopx;
const isLeftToRight = startx <= stopx;
if (isSelfMessage) {
autonumberX = msgModel.fromBounds + 1;
} else if (isReverseArrowType) {
autonumberX = isLeftToRight ? msgModel.toBounds - 1 : msgModel.fromBounds + 1;
} else {
autonumberX = isLeftToRight ? msgModel.fromBounds + 1 : msgModel.toBounds - 1;
}
diagram
.append('line')
.attr('x1', startx)
.attr('x1', autonumberX)
.attr('y1', lineStartY)
.attr('x2', startx)
.attr('x2', autonumberX)
.attr('y2', lineStartY)
.attr('stroke-width', 0)
.attr('marker-start', 'url(' + url + '#sequencenumber)')
.attr('transform', `translate(-${x}, 0)`);
.attr('marker-start', 'url(' + url + '#sequencenumber)');
diagram
.append('text')
.attr('x', startx)
.attr('x', autonumberX)
.attr('y', lineStartY + 4)
.attr('font-family', 'sans-serif')
.attr('font-size', '12px')
.attr('text-anchor', 'middle')
.attr('class', 'sequenceNumber')
.text(sequenceIndex)
.attr('transform', `translate(-${x}, 0)`);
.text(sequenceIndex);
}
};
@@ -1648,7 +1735,8 @@ const calculateCentralConnectionOffset = function (msg, diagObj, isArrowToRight)
}
if (
msg.centralConnection === CENTRAL_CONNECTION_DUAL &&
(msg.centralConnection === CENTRAL_CONNECTION_REVERSE ||
msg.centralConnection === CENTRAL_CONNECTION_DUAL) &&
(msg.type === BIDIRECTIONAL_SOLID || msg.type === BIDIRECTIONAL_DOTTED)
) {
offset += isArrowToRight ? 0 : -CENTRAL_CONNECTION_BIDIRECTIONAL_OFFSET;
@@ -1657,6 +1745,47 @@ const calculateCentralConnectionOffset = function (msg, diagObj, isArrowToRight)
return offset;
};
/**
* Check if a message is a reverse arrow type
* @param msg - The message object
* @param diagObj - The diagram object containing LINETYPE constants
* @returns True if the message is a reverse arrow type
*/
const isReverseArrowType = function (msg, diagObj) {
const {
SOLID_ARROW_TOP_REVERSE,
SOLID_ARROW_TOP_REVERSE_DOTTED,
SOLID_ARROW_BOTTOM_REVERSE,
SOLID_ARROW_BOTTOM_REVERSE_DOTTED,
STICK_ARROW_TOP_REVERSE,
STICK_ARROW_TOP_REVERSE_DOTTED,
STICK_ARROW_BOTTOM_REVERSE,
STICK_ARROW_BOTTOM_REVERSE_DOTTED,
} = diagObj.db.LINETYPE;
return [
SOLID_ARROW_TOP_REVERSE,
SOLID_ARROW_TOP_REVERSE_DOTTED,
SOLID_ARROW_BOTTOM_REVERSE,
SOLID_ARROW_BOTTOM_REVERSE_DOTTED,
STICK_ARROW_TOP_REVERSE,
STICK_ARROW_TOP_REVERSE_DOTTED,
STICK_ARROW_BOTTOM_REVERSE,
STICK_ARROW_BOTTOM_REVERSE_DOTTED,
].includes(msg.type);
};
/**
* Check if a message is a bidirectional arrow type
* @param msg - The message object
* @param diagObj - The diagram object containing LINETYPE constants
* @returns True if the message is a bidirectional arrow type
*/
const isBidirectionalArrowType = function (msg, diagObj) {
const { BIDIRECTIONAL_SOLID, BIDIRECTIONAL_DOTTED } = diagObj.db.LINETYPE;
return [BIDIRECTIONAL_SOLID, BIDIRECTIONAL_DOTTED].includes(msg.type);
};
const buildMessageModel = function (msg, actors, diagObj) {
if (
![

View File

@@ -681,8 +681,8 @@ const drawActorTypeControl = function (elem, actor, conf, isFooter) {
rect.class = 'actor';
const cx = actor.x + actor.width / 2;
const cy = actorY + 30;
const r = 18;
const cy = actorY + 32;
const r = 22;
actElem
.append('defs')
@@ -719,7 +719,7 @@ const drawActorTypeControl = function (elem, actor, conf, isFooter) {
actor.description,
actElem,
rect.x,
rect.y + r + (isFooter ? 5 : 10),
rect.y + r + (!isFooter ? 12 : 5),
rect.width,
rect.height,
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
@@ -737,7 +737,7 @@ const drawActorTypeEntity = function (elem, actor, conf, isFooter) {
const line = elem.append('g').lower();
const actElem = elem.append('g');
let cssClass = ACTOR_MAN_FIGURE_CLASS;
let cssClass = 'actor';
if (isFooter) {
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
} else {
@@ -756,7 +756,7 @@ const drawActorTypeEntity = function (elem, actor, conf, isFooter) {
const cx = actor.x + actor.width / 2;
const cy = actorY + (!isFooter ? 25 : 10);
const r = 18;
const r = 22;
actElem
.append('circle')
@@ -772,7 +772,6 @@ const drawActorTypeEntity = function (elem, actor, conf, isFooter) {
.attr('x2', cx + r)
.attr('y1', cy + r)
.attr('y2', cy + r)
.attr('stroke', '#333')
.attr('stroke-width', 2);
const bounds = actElem.node().getBBox();
@@ -799,7 +798,7 @@ const drawActorTypeEntity = function (elem, actor, conf, isFooter) {
actor.description,
actElem,
rect.x,
rect.y + (!isFooter ? (cy + r - actorY) / 2 : (cy - actorY + r - 5) / 2),
rect.y + (!isFooter ? 30 : 15),
rect.width,
rect.height,
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
@@ -807,9 +806,9 @@ const drawActorTypeEntity = function (elem, actor, conf, isFooter) {
);
if (!isFooter) {
actElem.attr('transform', `translate(${0}, ${r / 2})`);
actElem.attr('transform', `translate(${0}, ${r / 2 - 5})`);
} else {
actElem.attr('transform', `translate(${0}, ${r / 2})`);
actElem.attr('transform', `translate(${0}, ${r})`);
}
return actor.height;
@@ -872,8 +871,8 @@ const drawActorTypeDatabase = function (elem, actor, conf, isFooter) {
// Cylinder dimensions
rect.x = actor.x;
rect.y = actorY;
const w = rect.width / 4;
const h = rect.width / 4;
const w = rect.width / 3;
const h = rect.width / 3;
const rx = w / 2;
const ry = rx / (2.5 + w / 50);
@@ -897,17 +896,14 @@ const drawActorTypeDatabase = function (elem, actor, conf, isFooter) {
.attr('stroke-width', 1)
.attr('class', cssclass);
if (!isFooter) {
cylinderGroup.attr('transform', `translate(${w * 1.5}, ${(rect.height + ry) / 4})`);
} else {
cylinderGroup.attr('transform', `translate(${w * 1.5}, ${rect.height / 4 - 2 * ry})`);
}
cylinderGroup.attr('transform', `translate(${w}, ${ry})`);
actor.rectData = rect;
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
actor.description,
g,
rect.x,
rect.y + (!isFooter ? (rect.height + ry) / 2 : (rect.height + h) / 4),
rect.y + 35,
rect.width,
rect.height,
{ class: `actor ${ACTOR_BOX_CLASS}` },
@@ -927,7 +923,7 @@ const drawActorTypeBoundary = function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
const centerY = actorY + 80;
const radius = 30;
const radius = 22;
const line = elem.append('g').lower();
if (!isFooter) {
@@ -968,22 +964,22 @@ const drawActorTypeBoundary = function (elem, actor, conf, isFooter) {
.append('line')
.attr('id', 'actor-man-torso' + actorCnt)
.attr('x1', actor.x + actor.width / 2 - radius * 2.5)
.attr('y1', actorY + 10)
.attr('y1', actorY + 12)
.attr('x2', actor.x + actor.width / 2 - 15)
.attr('y2', actorY + 10);
.attr('y2', actorY + 12);
actElem
.append('line')
.attr('id', 'actor-man-arms' + actorCnt)
.attr('x1', actor.x + actor.width / 2 - radius * 2.5)
.attr('y1', actorY + 0) // starting Y
.attr('y1', actorY + 2) // starting Y
.attr('x2', actor.x + actor.width / 2 - radius * 2.5)
.attr('y2', actorY + 20); // ending Y (26px long, adjust as needed)
.attr('y2', actorY + 22); // ending Y (26px long, adjust as needed)
actElem
.append('circle')
.attr('cx', actor.x + actor.width / 2)
.attr('cy', actorY + 10)
.attr('cy', actorY + 12)
.attr('r', radius);
const bounds = actElem.node().getBBox();
@@ -993,7 +989,7 @@ const drawActorTypeBoundary = function (elem, actor, conf, isFooter) {
actor.description,
actElem,
rect.x,
rect.y + (!isFooter ? radius / 2 + 3 : radius / 2 - 4),
rect.y + 15,
rect.width,
rect.height,
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
@@ -1001,9 +997,9 @@ const drawActorTypeBoundary = function (elem, actor, conf, isFooter) {
);
if (!isFooter) {
actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
actElem.attr('transform', `translate(0,${radius / 2 + 10})`);
} else {
actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
actElem.attr('transform', `translate(0,${radius / 2 + 10})`);
}
return actor.height;

View File

@@ -21,19 +21,19 @@
"font-awesome": "^4.7.0",
"jiti": "^2.4.2",
"mermaid": "workspace:^",
"vue": "^3.5.23"
"vue": "^3.5.24"
},
"devDependencies": {
"@iconify-json/carbon": "^1.2.14",
"@unocss/reset": "^66.5.4",
"@unocss/reset": "^66.5.5",
"@vite-pwa/vitepress": "^1.0.1",
"@vitejs/plugin-vue": "^6.0.1",
"fast-glob": "^3.3.3",
"https-localhost": "^4.7.1",
"pathe": "^2.0.3",
"unocss": "^66.5.4",
"unocss": "^66.5.5",
"unplugin-vue-components": "^28.8.0",
"vite": "^7.0.7",
"vite": "^7.0.8",
"vite-plugin-pwa": "^1.0.3",
"vitepress": "1.6.4",
"workbox-window": "^7.3.0"

View File

@@ -1,9 +1,8 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import { insertPolygonShape } from './insertPolygonShape.js';
import type { D3Selection } from '../../../types.js';
export const createHexagonPathD = (
@@ -29,42 +28,50 @@ export async function hexagon<T extends SVGGraphicsElement>(parent: D3Selection<
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const f = 4;
const h = bbox.height + node.padding;
const m = h / f;
const w = 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 },
];
let polygon: D3Selection<SVGGElement> | Awaited<ReturnType<typeof insertPolygonShape>>;
const h = bbox.height + (node.padding ?? 0);
const w = bbox.width + (node.padding ?? 0) * 2.5;
const { cssStyles } = node;
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look === 'handDrawn') {
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const pathData = createHexagonPathD(0, 0, w, h, m);
const roughNode = rc.path(pathData, options);
polygon = shapeSvg
.insert(() => roughNode, ':first-child')
.attr('transform', `translate(${-w / 2}, ${h / 2})`);
if (cssStyles) {
polygon.attr('style', cssStyles);
}
} else {
polygon = insertPolygonShape(shapeSvg, w, h, points);
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
if (nodeStyles) {
polygon.attr('style', nodeStyles);
let halfWidth = w / 2;
const m = halfWidth / 6; // Margin for label
halfWidth = halfWidth + m; // Adjusted half width for hexagon
const halfHeight = h / 2;
const fixedLength = halfHeight / 2;
const deducedWidth = halfWidth - fixedLength;
const points = [
{ x: -deducedWidth, y: -halfHeight },
{ x: 0, y: -halfHeight },
{ x: deducedWidth, y: -halfHeight },
{ x: halfWidth, y: 0 },
{ x: deducedWidth, y: halfHeight },
{ x: 0, y: halfHeight },
{ x: -deducedWidth, y: halfHeight },
{ x: -halfWidth, y: 0 },
];
const pathData = createPathFromPoints(points);
const shapeNode = rc.path(pathData, options);
const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
polygon.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', nodeStyles);
}
node.width = w;

View File

@@ -1,18 +1,161 @@
import type { Node, RectOptions } from '../../types.js';
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import type { D3Selection } from '../../../types.js';
import { drawRect } from './drawRect.js';
/**
* Generates evenly spaced points along an elliptical arc connecting two points.
*
* @param x1 - x-coordinate of the start point of the arc
* @param y1 - y-coordinate of the start point of the arc
* @param x2 - x-coordinate of the end point of the arc
* @param y2 - y-coordinate of the end point of the arc
* @param rx - horizontal radius of the ellipse
* @param ry - vertical radius of the ellipse
* @param clockwise - direction of the arc; true for clockwise, false for counterclockwise
* @returns Array of points `{ x, y }` along the elliptical arc
*
* @throws Error if the given radii are too small to draw an arc between the points
*/
export function generateArcPoints(
x1: number,
y1: number,
x2: number,
y2: number,
rx: number,
ry: number,
clockwise: boolean
) {
const numPoints = 20;
// Calculate midpoint
const midX = (x1 + x2) / 2;
const midY = (y1 + y2) / 2;
// Calculate the angle of the line connecting the points
const angle = Math.atan2(y2 - y1, x2 - x1);
// Calculate transformed coordinates for the ellipse
const dx = (x2 - x1) / 2;
const dy = (y2 - y1) / 2;
// Scale to unit circle
const transformedX = dx / rx;
const transformedY = dy / ry;
// Calculate the distance between points on the unit circle
const distance = Math.sqrt(transformedX ** 2 + transformedY ** 2);
// Check if the ellipse can be drawn with the given radii
if (distance > 1) {
throw new Error('The given radii are too small to create an arc between the points.');
}
// Calculate the distance from the midpoint to the center of the ellipse
const scaledCenterDistance = Math.sqrt(1 - distance ** 2);
// Calculate the center of the ellipse
const centerX = midX + scaledCenterDistance * ry * Math.sin(angle) * (clockwise ? -1 : 1);
const centerY = midY - scaledCenterDistance * rx * Math.cos(angle) * (clockwise ? -1 : 1);
// Calculate the start and end angles on the ellipse
const startAngle = Math.atan2((y1 - centerY) / ry, (x1 - centerX) / rx);
const endAngle = Math.atan2((y2 - centerY) / ry, (x2 - centerX) / rx);
// Adjust angles for clockwise/counterclockwise
let angleRange = endAngle - startAngle;
if (clockwise && angleRange < 0) {
angleRange += 2 * Math.PI;
}
if (!clockwise && angleRange > 0) {
angleRange -= 2 * Math.PI;
}
// Generate points
const points = [];
for (let i = 0; i < numPoints; i++) {
const t = i / (numPoints - 1);
const angle = startAngle + t * angleRange;
const x = centerX + rx * Math.cos(angle);
const y = centerY + ry * Math.sin(angle);
points.push({ x, y });
}
return points;
}
export async function roundedRect<T extends SVGGraphicsElement>(
parent: D3Selection<T>,
node: Node
) {
const options = {
rx: 5,
ry: 5,
classes: '',
labelPaddingX: (node?.padding || 0) * 1,
labelPaddingY: (node?.padding || 0) * 1,
} as RectOptions;
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
return drawRect(parent, node, options);
const labelPaddingX = node?.padding ?? 0;
const labelPaddingY = node?.padding ?? 0;
const w = (node?.width ? node?.width : bbox.width) + labelPaddingX * 2;
const h = (node?.height ? node?.height : bbox.height) + labelPaddingY * 2;
const radius = node.radius || 5;
const taper = node.taper || 5; // Taper width for the rounded corners
const { cssStyles } = node;
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.stroke) {
options.stroke = node.stroke;
}
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const points = [
// Top edge (left to right)
{ x: -w / 2 + taper, y: -h / 2 }, // Top-left corner start (1)
{ x: w / 2 - taper, y: -h / 2 }, // Top-right corner start (2)
...generateArcPoints(w / 2 - taper, -h / 2, w / 2, -h / 2 + taper, radius, radius, true), // Top-left arc (2 to 3)
// Right edge (top to bottom)
{ x: w / 2, y: -h / 2 + taper }, // Top-right taper point (3)
{ x: w / 2, y: h / 2 - taper }, // Bottom-right taper point (4)
...generateArcPoints(w / 2, h / 2 - taper, w / 2 - taper, h / 2, radius, radius, true), // Top-left arc (4 to 5)
// Bottom edge (right to left)
{ x: w / 2 - taper, y: h / 2 }, // Bottom-right corner start (5)
{ x: -w / 2 + taper, y: h / 2 }, // Bottom-left corner start (6)
...generateArcPoints(-w / 2 + taper, h / 2, -w / 2, h / 2 - taper, radius, radius, true), // Top-left arc (4 to 5)
// Left edge (bottom to top)
{ x: -w / 2, y: h / 2 - taper }, // Bottom-left taper point (7)
{ x: -w / 2, y: -h / 2 + taper }, // Top-left taper point (8)
...generateArcPoints(-w / 2, -h / 2 + taper, -w / 2 + taper, -h / 2, radius, radius, true), // Top-left arc (4 to 5)
];
const pathData = createPathFromPoints(points);
const shapeNode = rc.path(pathData, options);
const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
polygon.attr('class', 'basic label-container outer-path');
if (cssStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', nodeStyles);
}
updateNodeBounds(node, polygon);
node.intersect = function (point) {
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
}

583
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff