diff --git a/.build/common.ts b/.build/common.ts index efd0e3a85..2497d443f 100644 --- a/.build/common.ts +++ b/.build/common.ts @@ -33,6 +33,11 @@ export const packageOptions = { packageName: 'mermaid-layout-elk', file: 'layouts.ts', }, + 'mermaid-layout-tidy-tree': { + name: 'mermaid-layout-tidy-tree', + packageName: 'mermaid-layout-tidy-tree', + file: 'index.ts', + }, examples: { name: 'mermaid-examples', packageName: 'examples', diff --git a/.changeset/clean-wolves-turn.md b/.changeset/clean-wolves-turn.md new file mode 100644 index 000000000..7a44c1c16 --- /dev/null +++ b/.changeset/clean-wolves-turn.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: Render newlines as spaces in class diagrams diff --git a/.changeset/crazy-loops-matter.md b/.changeset/crazy-loops-matter.md new file mode 100644 index 000000000..e6377a9e5 --- /dev/null +++ b/.changeset/crazy-loops-matter.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: Handle arrows correctly when auto number is enabled diff --git a/.changeset/hungry-baths-glow.md b/.changeset/hungry-baths-glow.md new file mode 100644 index 000000000..b3084bcab --- /dev/null +++ b/.changeset/hungry-baths-glow.md @@ -0,0 +1,5 @@ +--- +'mermaid': minor +--- + +feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`. diff --git a/.changeset/hungry-guests-drive.md b/.changeset/hungry-guests-drive.md new file mode 100644 index 000000000..1b0e0a07b --- /dev/null +++ b/.changeset/hungry-guests-drive.md @@ -0,0 +1,7 @@ +--- +'mermaid': minor +'@mermaid-js/layout-tidy-tree': minor +'@mermaid-js/layout-elk': minor +--- + +feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes diff --git a/.cspell/mermaid-terms.txt b/.cspell/mermaid-terms.txt index b0cfa0a1d..6900c15b0 100644 --- a/.cspell/mermaid-terms.txt +++ b/.cspell/mermaid-terms.txt @@ -5,6 +5,7 @@ bmatrix braintree catmull compositTitleSize +cose curv doublecircle elems diff --git a/.cspell/misc-terms.txt b/.cspell/misc-terms.txt index 1820e3c86..2906a02fa 100644 --- a/.cspell/misc-terms.txt +++ b/.cspell/misc-terms.txt @@ -1,4 +1,5 @@ BRANDES +Buzan circo handDrawn KOEPF diff --git a/.github/workflows/e2e-applitools.yml b/.github/workflows/e2e-applitools.yml index dd97b49e1..6aaa91eb8 100644 --- a/.github/workflows/e2e-applitools.yml +++ b/.github/workflows/e2e-applitools.yml @@ -23,9 +23,6 @@ env: jobs: e2e-applitools: runs-on: ubuntu-latest - container: - image: cypress/browsers:node-20.11.0-chrome-121.0.6167.85-1-ff-120.0-edge-121.0.2277.83-1 - options: --user 1001 steps: - if: ${{ ! env.USE_APPLI }} name: Warn if not using Applitools diff --git a/.github/workflows/e2e-timings.yml b/.github/workflows/e2e-timings.yml index 2bbfa8412..21dbda293 100644 --- a/.github/workflows/e2e-timings.yml +++ b/.github/workflows/e2e-timings.yml @@ -58,7 +58,7 @@ jobs: echo "EOF" >> $GITHUB_OUTPUT - name: Commit and create pull request - uses: peter-evans/create-pull-request@cb4d3bfce175d44325c6b7697f81e0afe8a79bdf + uses: peter-evans/create-pull-request@18e469570b1cf0dfc11d60ec121099f8ff3e617a with: add-paths: | cypress/timings.json diff --git a/.github/workflows/validate-lockfile.yml b/.github/workflows/validate-lockfile.yml index 6eb0a63ca..59a6df96d 100644 --- a/.github/workflows/validate-lockfile.yml +++ b/.github/workflows/validate-lockfile.yml @@ -35,7 +35,7 @@ jobs: # 2) No unwanted vitepress paths if grep -qF 'packages/mermaid/src/vitepress' pnpm-lock.yaml; then - issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run `rm -rf packages/mermaid/src/vitepress && pnpm install` to regenerate.") + issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run \`rm -rf packages/mermaid/src/vitepress && pnpm install\` to regenerate.") fi # 3) Lockfile only changes when package.json changes diff --git a/.gitignore b/.gitignore index 7448f2a81..7eb55d5cb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules/ coverage/ .idea/ .pnpm-store/ +.instructions/ dist v8-compile-cache-0 diff --git a/cypress/integration/rendering/classDiagram.spec.js b/cypress/integration/rendering/classDiagram.spec.js index bd2a96b34..6cea402f8 100644 --- a/cypress/integration/rendering/classDiagram.spec.js +++ b/cypress/integration/rendering/classDiagram.spec.js @@ -524,5 +524,18 @@ describe('Class diagram', () => { `, {} ); + it('should handle an empty class body with empty braces', () => { + imgSnapshotTest( + ` classDiagram + class FooBase~T~ {} + class Bar { + +Zip + +Zap() + } + FooBase <|-- Ba + `, + { flowchart: { defaultRenderer: 'elk' } } + ); + }); }); }); diff --git a/cypress/integration/rendering/mindmap-tidy-tree.spec.js b/cypress/integration/rendering/mindmap-tidy-tree.spec.js new file mode 100644 index 000000000..e111c281a --- /dev/null +++ b/cypress/integration/rendering/mindmap-tidy-tree.spec.js @@ -0,0 +1,79 @@ +import { imgSnapshotTest } from '../../helpers/util.ts'; + +describe('Mindmap Tidy Tree', () => { + it('1-tidy-tree: should render a simple mindmap without children', () => { + imgSnapshotTest( + ` --- + config: + layout: tidy-tree + --- + mindmap + root((mindmap)) + A + B + ` + ); + }); + it('2-tidy-tree: should render a simple mindmap', () => { + imgSnapshotTest( + ` --- + config: + layout: tidy-tree + --- + mindmap + root((mindmap is a long thing)) + A + B + C + D + ` + ); + }); + it('3-tidy-tree: should render a mindmap with different shapes', () => { + imgSnapshotTest( + ` --- + config: + layout: tidy-tree + --- + mindmap + root((mindmap)) + Origins + Long history + ::icon(fa fa-book) + Popularisation + British popular psychology author Tony Buzan + Research + On effectiveness<br/>and features + On Automatic creation + Uses + Creative techniques + Strategic planning + Argument mapping + Tools + id)I am a cloud( + id))I am a bang(( + Tools + ` + ); + }); + it('4-tidy-tree: should render a mindmap with children', () => { + imgSnapshotTest( + ` --- + config: + layout: tidy-tree + --- + mindmap + ((This is a mindmap)) + child1 + grandchild 1 + grandchild 2 + child2 + grandchild 3 + grandchild 4 + child3 + grandchild 5 + grandchild 6 + ` + ); + }); +}); diff --git a/cypress/integration/rendering/mindmap.spec.ts b/cypress/integration/rendering/mindmap.spec.ts index d76e58c56..e0409ed46 100644 --- a/cypress/integration/rendering/mindmap.spec.ts +++ b/cypress/integration/rendering/mindmap.spec.ts @@ -159,12 +159,10 @@ root }); it('square shape', () => { imgSnapshotTest( - ` -mindmap + `mindmap root[ The root - ] - `, + ]`, {}, undefined, shouldHaveRoot @@ -172,12 +170,10 @@ mindmap }); it('rounded rect shape', () => { imgSnapshotTest( - ` -mindmap + `mindmap root(( The root - )) - `, + ))`, {}, undefined, shouldHaveRoot @@ -185,12 +181,10 @@ mindmap }); it('circle shape', () => { imgSnapshotTest( - ` -mindmap + `mindmap root( The root - ) - `, + )`, {}, undefined, shouldHaveRoot @@ -198,10 +192,8 @@ mindmap }); it('default shape', () => { imgSnapshotTest( - ` -mindmap - The root - `, + `mindmap + The root`, {}, undefined, shouldHaveRoot @@ -209,12 +201,10 @@ mindmap }); it('adding children', () => { imgSnapshotTest( - ` -mindmap + `mindmap The root child1 - child2 - `, + child2`, {}, undefined, shouldHaveRoot @@ -222,13 +212,11 @@ mindmap }); it('adding grand children', () => { imgSnapshotTest( - ` -mindmap + `mindmap The root child1 child2 - child3 - `, + child3`, {}, undefined, shouldHaveRoot @@ -240,25 +228,21 @@ mindmap `mindmap id1[\`**Start** with a second line 😎\`] - id2[\`The dog in **the** hog... a *very long text* about it -Word!\`] -` + id2[\`The dog in **the** hog... a *very long text* about it Word!\`]` ); }); }); describe('Include char sequence "graph" in text (#6795)', () => { it('has a label with char sequence "graph"', () => { imgSnapshotTest( - ` - mindmap + ` mindmap root Photograph Waterfall Landscape Geography Mountains - Rocks - `, + Rocks`, { flowchart: { defaultRenderer: 'elk' } } ); }); diff --git a/cypress/integration/rendering/sequencediagram-v2.spec.js b/cypress/integration/rendering/sequencediagram-v2.spec.js new file mode 100644 index 000000000..f1c2aafbd --- /dev/null +++ b/cypress/integration/rendering/sequencediagram-v2.spec.js @@ -0,0 +1,659 @@ +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; + +const looks = ['classic']; +const participantTypes = [ + { type: 'participant', display: 'participant' }, + { type: 'actor', display: 'actor' }, + { type: 'boundary', display: 'boundary' }, + { type: 'control', display: 'control' }, + { type: 'entity', display: 'entity' }, + { type: 'database', display: 'database' }, + { type: 'collections', display: 'collections' }, + { type: 'queue', display: 'queue' }, +]; + +const restrictedTypes = ['boundary', 'control', 'entity', 'database', 'collections', 'queue']; + +const interactionTypes = ['->>', '-->>', '->', '-->', '-x', '--x', '->>+', '-->>+']; + +const notePositions = ['left of', 'right of', 'over']; + +function getParticipantLine(name, type, alias) { + if (restrictedTypes.includes(type)) { + return ` participant ${name}@{ "type" : "${type}" }\n`; + } else if (alias) { + return ` participant ${name}@{ "type" : "${type}" } \n`; + } else { + return ` participant ${name}@{ "type" : "${type}" }\n`; + } +} + +looks.forEach((look) => { + describe(`Sequence Diagram Tests - ${look} look`, () => { + it('should render all participant types', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.forEach((pt, index) => { + const name = `${pt.display}${index}`; + diagramCode += getParticipantLine(name, pt.type); + }); + for (let i = 0; i < participantTypes.length - 1; i++) { + diagramCode += ` ${participantTypes[i].display}${i} ->> ${participantTypes[i + 1].display}${i + 1}: Message ${i}\n`; + } + imgSnapshotTest(diagramCode, { look, sequence: { diagramMarginX: 50, diagramMarginY: 10 } }); + }); + + it('should render all interaction types', () => { + let diagramCode = `sequenceDiagram\n`; + diagramCode += getParticipantLine('A', 'actor'); + diagramCode += getParticipantLine('B', 'boundary'); + interactionTypes.forEach((interaction, index) => { + diagramCode += ` A ${interaction} B: ${interaction} message ${index}\n`; + }); + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render participant creation and destruction', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.forEach((pt, index) => { + const name = `${pt.display}${index}`; + diagramCode += getParticipantLine('A', pt.type); + diagramCode += getParticipantLine('B', pt.type); + diagramCode += ` create participant ${name}@{ "type" : "${pt.type}" }\n`; + diagramCode += ` A ->> ${name}: Hello ${pt.display}\n`; + if (index % 2 === 0) { + diagramCode += ` destroy ${name}\n`; + } + }); + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render notes in all positions', () => { + let diagramCode = `sequenceDiagram\n`; + diagramCode += getParticipantLine('A', 'actor'); + diagramCode += getParticipantLine('B', 'boundary'); + notePositions.forEach((position, index) => { + diagramCode += ` Note ${position} A: Note ${position} ${index}\n`; + }); + diagramCode += ` A ->> B: Message with notes\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render parallel interactions', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.slice(0, 4).forEach((pt, index) => { + diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type); + }); + diagramCode += ` par Parallel actions\n`; + for (let i = 0; i < 3; i += 2) { + diagramCode += ` ${participantTypes[i].display}${i} ->> ${participantTypes[i + 1].display}${i + 1}: Message ${i}\n`; + if (i < participantTypes.length - 2) { + diagramCode += ` and\n`; + } + } + diagramCode += ` end\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render alternative flows', () => { + let diagramCode = `sequenceDiagram\n`; + diagramCode += getParticipantLine('A', 'actor'); + diagramCode += getParticipantLine('B', 'boundary'); + diagramCode += ` alt Successful case\n`; + diagramCode += ` A ->> B: Request\n`; + diagramCode += ` B -->> A: Success\n`; + diagramCode += ` else Failure case\n`; + diagramCode += ` A ->> B: Request\n`; + diagramCode += ` B --x A: Failure\n`; + diagramCode += ` end\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render loops', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.slice(0, 3).forEach((pt, index) => { + diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type); + }); + diagramCode += ` loop For each participant\n`; + for (let i = 0; i < 3; i++) { + diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[1].display}1: Message ${i}\n`; + } + diagramCode += ` end\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render boxes around groups', () => { + let diagramCode = `sequenceDiagram\n`; + diagramCode += ` box Group 1\n`; + participantTypes.slice(0, 3).forEach((pt, index) => { + diagramCode += ` ${getParticipantLine(`${pt.display}${index}`, pt.type)}`; + }); + diagramCode += ` end\n`; + diagramCode += ` box rgb(200,220,255) Group 2\n`; + participantTypes.slice(3, 6).forEach((pt, index) => { + diagramCode += ` ${getParticipantLine(`${pt.display}${index}`, pt.type)}`; + }); + diagramCode += ` end\n`; + diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[3].display}0: Cross-group message\n`; + imgSnapshotTest(diagramCode, { look }); + }); + + it('should render with different font settings', () => { + let diagramCode = `sequenceDiagram\n`; + participantTypes.slice(0, 3).forEach((pt, index) => { + diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type); + }); + diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[1].display}1: Regular message\n`; + diagramCode += ` Note right of ${participantTypes[1].display}1: Regular note\n`; + imgSnapshotTest(diagramCode, { + look, + sequence: { + actorFontFamily: 'courier', + actorFontSize: 14, + messageFontFamily: 'Arial', + messageFontSize: 12, + noteFontFamily: 'times', + noteFontSize: 16, + noteAlign: 'left', + }, + }); + }); + }); +}); + +// Additional tests for specific combinations +describe('Sequence Diagram Special Cases', () => { + it('should render complex sequence with all features', () => { + const diagramCode = ` + sequenceDiagram + box rgb(200,220,255) Authentication + actor User + participant LoginUI@{ "type": "boundary" } + participant AuthService@{ "type": "control" } + participant UserDB@{ "type": "database" } + end + + box rgb(200,255,220) Order Processing + participant Order@{ "type": "entity" } + participant OrderQueue@{ "type": "queue" } + participant AuditLogs@{ "type": "collections" } + end + + User ->> LoginUI: Enter credentials + LoginUI ->> AuthService: Validate + AuthService ->> UserDB: Query user + UserDB -->> AuthService: User data + alt Valid credentials + AuthService -->> LoginUI: Success + LoginUI -->> User: Welcome + + par Place order + User ->> Order: New order + Order ->> OrderQueue: Process + and + Order ->> AuditLogs: Record + end + + loop Until confirmed + OrderQueue ->> Order: Update status + Order -->> User: Notification + end + else Invalid credentials + AuthService --x LoginUI: Failure + LoginUI --x User: Retry + end + `; + imgSnapshotTest(diagramCode, {}); + }); + + it('should render with wrapped messages and notes', () => { + const diagramCode = ` + sequenceDiagram + participant A + participant B + + A ->> B: This is a very long message that should wrap properly in the diagram rendering + Note over A,B: This is a very long note that should also wrap properly when rendered in the diagram + + par Wrapped parallel + A ->> B: Parallel message 1
with explicit line break + and + B ->> A: Parallel message 2
with explicit line break + end + + loop Wrapped loop + Note right of B: This is a long note
in a loop + A ->> B: Message in loop + end + `; + imgSnapshotTest(diagramCode, { sequence: { wrap: true } }); + }); + describe('Sequence Diagram Rendering with Different Participant Types', () => { + it('should render a sequence diagram with various participant types', () => { + imgSnapshotTest( + ` + sequenceDiagram + participant User@{ "type": "actor" } + participant AuthService@{ "type": "control" } + participant UI@{ "type": "boundary" } + participant OrderController@{ "type": "control" } + participant Product@{ "type": "entity" } + participant MongoDB@{ "type": "database" } + participant Products@{ "type": "collections" } + participant OrderQueue@{ "type": "queue" } + User ->> UI: Login request + UI ->> AuthService: Validate credentials + AuthService -->> UI: Authentication token + UI ->> OrderController: Place order + OrderController ->> Product: Check availability + Product -->> OrderController: Available + OrderController ->> MongoDB: Save order + MongoDB -->> OrderController: Order saved + OrderController ->> OrderQueue: Process payment + OrderQueue -->> User: Order confirmation + ` + ); + }); + + it('should render participant creation and destruction with different types', () => { + imgSnapshotTest(` + sequenceDiagram + participant Alice@{ "type" : "boundary" } + Alice->>Bob: Hello Bob, how are you ? + Bob->>Alice: Fine, thank you. And you? + create participant Carl@{ "type" : "control" } + Alice->>Carl: Hi Carl! + create actor D as Donald + Carl->>D: Hi! + destroy Carl + Alice-xCarl: We are too many + destroy Bob + Bob->>Alice: I agree + `); + }); + + it('should handle complex interactions between different participant types', () => { + imgSnapshotTest( + ` + sequenceDiagram + box rgb(200,220,255) Authentication + participant User@{ "type": "actor" } + participant LoginUI@{ "type": "boundary" } + participant AuthService@{ "type": "control" } + participant UserDB@{ "type": "database" } + end + + box rgb(200,255,220) Order Processing + participant Order@{ "type": "entity" } + participant OrderQueue@{ "type": "queue" } + participant AuditLogs@{ "type": "collections" } + end + + User ->> LoginUI: Enter credentials + LoginUI ->> AuthService: Validate + AuthService ->> UserDB: Query user + UserDB -->> AuthService: User data + + alt Valid credentials + AuthService -->> LoginUI: Success + LoginUI -->> User: Welcome + + par Place order + User ->> Order: New order + Order ->> OrderQueue: Process + and + Order ->> AuditLogs: Record + end + + loop Until confirmed + OrderQueue ->> Order: Update status + Order -->> User: Notification + end + else Invalid credentials + AuthService --x LoginUI: Failure + LoginUI --x User: Retry + end + `, + { sequence: { useMaxWidth: false } } + ); + }); + + it('should render parallel processes with different participant types', () => { + imgSnapshotTest( + ` + sequenceDiagram + participant Customer@{ "type": "actor" } + participant Frontend@{ "type": "participant" } + participant PaymentService@{ "type": "boundary" } + participant InventoryManager@{ "type": "control" } + participant Order@{ "type": "entity" } + participant OrdersDB@{ "type": "database" } + participant NotificationQueue@{ "type": "queue" } + + Customer ->> Frontend: Place order + Frontend ->> Order: Create order + par Parallel Processing + Order ->> PaymentService: Process payment + and + Order ->> InventoryManager: Reserve items + end + PaymentService -->> Order: Payment confirmed + InventoryManager -->> Order: Items reserved + Order ->> OrdersDB: Save finalized order + OrdersDB -->> Order: Order saved + Order ->> NotificationQueue: Send confirmation + NotificationQueue -->> Customer: Order confirmation + ` + ); + }); + }); + it('should render different participant types with notes and loops', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Admin + participant Dashboard + participant AuthService@{ "type" : "boundary" } + participant UserManager@{ "type" : "control" } + participant UserProfile@{ "type" : "entity" } + participant UserDB@{ "type" : "database" } + participant Logs@{ "type" : "database" } + + Admin ->> Dashboard: Open user management + loop Authentication check + Dashboard ->> AuthService: Verify admin rights + AuthService ->> Dashboard: Access granted + end + Dashboard ->> UserManager: List users + UserManager ->> UserDB: Query users + UserDB ->> UserManager: Return user data + Note right of UserDB: Encrypted data
requires decryption + UserManager ->> UserProfile: Format profiles + UserProfile ->> UserManager: Formatted data + UserManager ->> Dashboard: Display users + Dashboard ->> Logs: Record access + Logs ->> Admin: Audit trail + ` + ); + }); + + it('should render different participant types with alternative flows', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Client + participant MobileApp + participant CloudService@{ "type" : "boundary" } + participant DataProcessor@{ "type" : "control" } + participant Transaction@{ "type" : "entity" } + participant TransactionsDB@{ "type" : "database" } + participant EventBus@{ "type" : "queue" } + + Client ->> MobileApp: Initiate transaction + MobileApp ->> CloudService: Authenticate + alt Authentication successful + CloudService -->> MobileApp: Auth token + MobileApp ->> DataProcessor: Process data + DataProcessor ->> Transaction: Create transaction + Transaction ->> TransactionsDB: Save record + TransactionsDB -->> Transaction: Confirmation + Transaction ->> EventBus: Publish event + EventBus -->> Client: Notification + else Authentication failed + CloudService -->> MobileApp: Error + MobileApp -->> Client: Show error + end + ` + ); + }); + + it('should render different participant types with wrapping text', () => { + imgSnapshotTest( + ` + sequenceDiagram + participant B@{ "type" : "boundary" } + participant C@{ "type" : "control" } + participant E@{ "type" : "entity" } + participant DB@{ "type" : "database" } + participant COL@{ "type" : "collections" } + participant Q@{ "type" : "queue" } + + FE ->> B: Another long message
with explicit
line breaks + B -->> FE: Response message that is also quite long and needs to wrap + FE ->> C: Process data + C ->> E: Validate + E -->> C: Validation result + C ->> DB: Save + DB -->> C: Save result + C ->> COL: Log + COL -->> Q: Forward + Q -->> LongNameUser: Final response with confirmation of all actions taken + `, + { sequence: { wrap: true } } + ); + }); + + describe('Sequence Diagram - New Participant Types with Long Notes and Messages', () => { + it('should render long notes left of boundary', () => { + imgSnapshotTest( + ` + sequenceDiagram + participant Alice@{ "type" : "boundary" } + actor Bob + Alice->>Bob: Hola + Note left of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render wrapped long notes left of control', () => { + imgSnapshotTest( + ` + sequenceDiagram + participant Alice@{ "type" : "control" } + actor Bob + Alice->>Bob: Hola + Note left of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render long notes right of entity', () => { + imgSnapshotTest( + ` + sequenceDiagram + participant Alice@{ "type" : "entity" } + actor Bob + Alice->>Bob: Hola + Note right of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render wrapped long notes right of database', () => { + imgSnapshotTest( + ` + sequenceDiagram + participant Alice@{ "type" : "database" } + actor Bob + Alice->>Bob: Hola + Note right of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render long notes over collections', () => { + imgSnapshotTest( + ` + sequenceDiagram + participant Alice@{ "type" : "collections" } + actor Bob + Alice->>Bob: Hola + Note over Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render wrapped long notes over queue', () => { + imgSnapshotTest( + ` + sequenceDiagram + participant Alice@{ "type" : "queue" } + actor Bob + Alice->>Bob: Hola + Note over Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render notes over actor and boundary', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Alice + participant Charlie@{ "type" : "boundary" } + note over Alice: Some note + note over Charlie: Other note + `, + {} + ); + }); + + it('should render long messages from database to collections', () => { + imgSnapshotTest( + ` + sequenceDiagram + participant Alice@{ "type" : "database" } + participant Bob@{ "type" : "collections" } + Alice->>Bob: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render wrapped long messages from control to entity', () => { + imgSnapshotTest( + ` + sequenceDiagram + participant Alice@{ "type" : "control" } + participant Bob@{ "type" : "entity" } + Alice->>Bob:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + Bob->>Alice: I'm short though + `, + {} + ); + }); + + it('should render long messages from queue to boundary', () => { + imgSnapshotTest( + ` + sequenceDiagram + participant Alice@{ "type" : "queue" } + participant Bob@{ "type" : "boundary" } + Alice->>Bob: I'm short + Bob->>Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + `, + {} + ); + }); + + it('should render wrapped long messages from actor to database', () => { + imgSnapshotTest( + ` + sequenceDiagram + actor Alice + participant Bob@{ "type" : "database" } + Alice->>Bob: I'm short + Bob->>Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be + `, + {} + ); + }); + }); + + describe('svg size', () => { + it('should render a sequence diagram when useMaxWidth is true (default)', () => { + renderGraph( + ` + sequenceDiagram + actor Alice + participant Bob@{ "type" : "boundary" } + participant John@{ "type" : "control" } + Alice ->> Bob: Hello Bob, how are you? + Bob-->>John: How about you John? + Bob--x Alice: I am good thanks! + Bob-x John: I am good thanks! + Note right of John: Bob thinks a long
long time, so long
that the text does
not fit on a row. + Bob-->Alice: Checking with John... + alt either this + Alice->>John: Yes + else or this + Alice->>John: No + else or this will happen + Alice->John: Maybe + end + par this happens in parallel + Alice -->> Bob: Parallel message 1 + and + Alice -->> John: Parallel message 2 + end + `, + { sequence: { useMaxWidth: true } } + ); + cy.get('svg').should((svg) => { + expect(svg).to.have.attr('width', '100%'); + const style = svg.attr('style'); + expect(style).to.match(/^max-width: [\d.]+px;$/); + const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join('')); + expect(maxWidthValue).to.be.within(820 * 0.95, 820 * 1.05); + }); + }); + + it('should render a sequence diagram when useMaxWidth is false', () => { + renderGraph( + ` + sequenceDiagram + actor Alice + participant Bob@{ "type" : "boundary" } + participant John@{ "type" : "control" } + Alice ->> Bob: Hello Bob, how are you? + Bob-->>John: How about you John? + Bob--x Alice: I am good thanks! + Bob-x John: I am good thanks! + Note right of John: Bob thinks a long
long time, so long
that the text does
not fit on a row. + Bob-->Alice: Checking with John... + alt either this + Alice->>John: Yes + else or this + Alice->>John: No + else or this will happen + Alice->John: Maybe + end + par this happens in parallel + Alice -->> Bob: Parallel message 1 + and + Alice -->> John: Parallel message 2 + end + `, + { sequence: { useMaxWidth: false } } + ); + cy.get('svg').should((svg) => { + const width = parseFloat(svg.attr('width')); + expect(width).to.be.within(820 * 0.95, 820 * 1.05); + expect(svg).to.not.have.attr('style'); + }); + }); + }); +}); diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index eb5528844..fc33a58b4 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -130,6 +130,76 @@ +
+      ---
+      config:
+        layout: tidy-tree
+      ---
+      mindmap
+      root((mindmap))
+        Origins
+          Long history
+          ::icon(fa fa-book)
+          Popularisation
+            British popular psychology author Tony Buzan
+        Research
+          On effectiveness
and features + On Automatic creation + Uses + Creative techniques + Strategic planning + Argument mapping + Tools + Pen and paper + Mermaid + +
+
+      ---
+      config:
+        layout: tidy-tree
+      ---
+      mindmap
+      root((mindmap is a long thing))
+        A
+        B
+        C
+        D
+      
+
+      ---
+      config:
+        layout: tidy-tree
+      ---
+      mindmap
+      root((mindmap))
+        A
+        B
+      
+
+      ---
+      config:
+        layout: tidy-tree
+      ---
+      mindmap
+      root((mindmap))
+        A
+          a
+            apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+            apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+          b
+          c
+          d
+        B
+            apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+        D
+          apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+          apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+
+    
+
 ---
 config:
@@ -191,8 +261,145 @@ treemap
           "Item B2": 25
     
+      ---
+      config:
+        layout: tidy-tree
+      ---
+      mindmap
+      root((mindmap))
+        a
+          apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+          apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+        b
+          apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+          apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+
+
+    
+
+      ---
+      config:
+        layout: tidy-tree
+      ---
+      flowchart TB
+          A --> n0["1"]
+          A --> n1["2"]
+          A --> n2["3"]
+          A --> n3["4"] --> Q & R & S & T
+    
+
+      ---
+      config:
+        layout: elk
+      ---
+      flowchart TB
+          A --> n0["1"]
+          A --> n1["2"]
+          A --> n2["3"]
+          A --> n3["4"] --> Q & R & S & T
+    
+
+      ---
+      config:
+        layout: dagre
+      ---
+      mindmap
+      root((mindmap is a long thing))
+        Origins
+          Long history
+          ::icon(fa fa-book)
+          Popularisation
+            British popular psychology author Tony Buzan
+        Research
+          On effectiveness<br/>and features
+          On Automatic creation
+            Uses
+                Creative techniques
+                Strategic planning
+                Argument mapping
+        Tools
+          Pen and paper
+          Mermaid
+    
+
+      ---
+      config:
+        layout: cose-bilkent
+      ---
+      mindmap
+      root((mindmap))
+        Origins
+          Long history
+          ::icon(fa fa-book)
+          Popularisation
+            British popular psychology author Tony Buzan
+        Research
+          On effectiveness<br/>and features
+          On Automatic creation
+            Uses
+                Creative techniques
+                Strategic planning
+                Argument mapping
+        Tools
+          Pen and paper
+          Mermaid
+    
+
+      ---
+      config:
+        layout: elk
+      ---
+      mindmap
+      root((mindmap))
+        Origins
+          Long history
+          ::icon(fa fa-book)
+          Popularisation
+            British popular psychology author Tony Buzan
+        Research
+          On effectiveness<br/>and features
+          On Automatic creation
+            Uses
+                Creative techniques
+                Strategic planning
+                Argument mapping
+        Tools
+          Pen and paper
+          Mermaid
+    
+
+      ---
+      config:
+        layout: cose-bilkent
+      ---
       flowchart LR
-        AB["apa@apa@"] --> B(("`apa@apa`"))
+      root{mindmap} --- Origins --- Europe
+      Origins --> Asia
+      root --- Background --- Rich
+      Background --- Poor
+      subgraph apa
+        Background
+        Poor
+      end
+
+
+
+
+    
+
+      ---
+      config:
+        layout: elk
+      ---
+      flowchart LR
+      root{mindmap} --- Origins --- Europe
+      Origins --> Asia
+      root --- Background --- Rich
+      Background --- Poor
+
+
+
+
     
       flowchart
@@ -274,6 +481,44 @@ config:
     
 ---
+config:
+  layout: elk
+---
+      flowchart LR
+      a
+      subgraph s0["APA"]
+      subgraph s8["APA"]
+      subgraph s1["APA"]
+        D{"X"}
+        E[Q]
+      end
+      subgraph s3["BAPA"]
+        F[Q]
+        I
+      end
+            D --> I
+            D --> I
+            D --> I
+
+      I{"X"}
+      end
+      end
+    
+
+---
+config:
+  layout: elk
+---
+      flowchart LR
+      a
+        D{"Use the editor"}
+
+      D -- Mermaid js --> I{"fa:fa-code Text"}
+      D-->I
+      D-->I
+    
+
+---
 config:
   layout: elk
 ---
diff --git a/cypress/platform/mindmap-layouts.html b/cypress/platform/mindmap-layouts.html
new file mode 100644
index 000000000..0aef65d42
--- /dev/null
+++ b/cypress/platform/mindmap-layouts.html
@@ -0,0 +1,376 @@
+
+
+  
+    
+    
+    Mermaid Quick Test Page
+    
+    
+  
+
+  
+    
+ ---
+      config:
+        layout: tidy-tree
+      ---
+      mindmap
+      root((mindmap))
+        A
+        B
+    
+
+ ---
+      config:
+        layout: dagre
+      ---
+      mindmap
+      root((mindmap))
+        A
+        B
+    
+
+ ---
+      config:
+        layout: elk
+      ---
+      mindmap
+      root((mindmap))
+        A
+        B
+    
+
+ ---
+      config:
+        layout: cose-bilkent
+      ---
+      mindmap
+      root((mindmap))
+        A
+        B
+    
+
+    ---
+      config:
+        layout: tidy-tree
+      ---
+      mindmap
+      root((mindmap is a long thing))
+        A
+        B
+        C
+        D
+    
+
+    ---
+      config:
+        layout: dagre
+      ---
+      mindmap
+      root((mindmap is a long thing))
+        A
+        B
+        C
+        D
+    
+
+    ---
+      config:
+        layout: elk
+      ---
+      mindmap
+      root((mindmap is a long thing))
+        A
+        B
+        C
+        D
+    
+
+    ---
+      config:
+        layout: cose-bilkent
+      ---
+      mindmap
+      root((mindmap is a long thing))
+        A
+        B
+        C
+        D
+    
+ +
+    ---
+      config:
+        layout: tidy-tree
+      ---
+      mindmap
+      root((mindmap))
+        Origins
+          Long history
+          ::icon(fa fa-book)
+          Popularisation
+            British popular psychology author Tony Buzan
+        Research
+          On effectiveness<br/>and features
+          On Automatic creation
+            Uses
+                Creative techniques
+                Strategic planning
+                Argument mapping
+        Tools
+              id)I am a cloud(
+                  id))I am a bang((
+                    Tools
+    
+
+    ---
+      config:
+        layout: dagre
+      ---
+      mindmap
+      root((mindmap))
+        Origins
+          Long history
+          ::icon(fa fa-book)
+          Popularisation
+            British popular psychology author Tony Buzan
+        Research
+          On effectiveness<br/>and features
+          On Automatic creation
+            Uses
+                Creative techniques
+                Strategic planning
+                Argument mapping
+        Tools
+              id)I am a cloud(
+                  id))I am a bang((
+                    Tools
+    
+
+    ---
+      config:
+        layout: elk
+      ---
+      mindmap
+      root((mindmap))
+        Origins
+          Long history
+          ::icon(fa fa-book)
+          Popularisation
+            British popular psychology author Tony Buzan
+        Research
+          On effectiveness<br/>and features
+          On Automatic creation
+            Uses
+                Creative techniques
+                Strategic planning
+                Argument mapping
+        Tools
+              id)I am a cloud(
+                  id))I am a bang((
+                    Tools
+    
+
+    ---
+      config:
+        layout: cose-bilkent
+      ---
+      mindmap
+      root((mindmap))
+        Origins
+          Long history
+          ::icon(fa fa-book)
+          Popularisation
+            British popular psychology author Tony Buzan
+        Research
+          On effectiveness<br/>and features
+          On Automatic creation
+            Uses
+                Creative techniques
+                Strategic planning
+                Argument mapping
+        Tools
+              id)I am a cloud(
+                  id))I am a bang((
+                    Tools
+    
+
+      ---
+      config:
+        layout: tidy-tree
+      ---
+      mindmap
+      root((mindmap))
+        A
+          a
+            apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+            apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+          b
+          c
+          d
+        B
+            apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+        D
+          apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+          apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+
+    
+
+      ---
+      config:
+        layout: dagre
+      ---
+      mindmap
+      root((mindmap))
+        A
+          a
+            apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+            apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+          b
+          c
+          d
+        B
+            apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+        D
+          apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+          apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+
+    
+
+      ---
+      config:
+        layout: elk
+      ---
+      mindmap
+      root((mindmap))
+        A
+          a
+            apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+            apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+          b
+          c
+          d
+        B
+            apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+        D
+          apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+          apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+
+    
+
+      ---
+      config:
+        layout: cose-bilkent
+      ---
+      mindmap
+      root((mindmap))
+        A
+          a
+            apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+            apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+          b
+          c
+          d
+        B
+            apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+        D
+          apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+          apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
+
+    
+ +
+ ---
+      config:
+        layout: tidy-tree
+      ---
+      mindmap
+      ((This is a mindmap))
+        child1
+         grandchild 1
+         grandchild 2
+        child2
+         grandchild 3
+         grandchild 4
+        child3
+         grandchild 5
+         grandchild 6
+      
+    
+ +
+ ---
+      config:
+        layout: dagre
+      ---
+      mindmap
+      ((This is a mindmap))
+        child1
+         grandchild 1
+         grandchild 2
+        child2
+         grandchild 3
+         grandchild 4
+        child3
+         grandchild 5
+         grandchild 6
+      
+    
+ +
+ ---
+      config:
+        layout: elk
+      ---
+      mindmap
+      ((This is a mindmap))
+        child1
+         grandchild 1
+         grandchild 2
+        child2
+         grandchild 3
+         grandchild 4
+        child3
+         grandchild 5
+         grandchild 6
+      
+    
+ +
+ ---
+      config:
+        layout: cose-bilkent
+      ---
+      mindmap
+      ((This is a mindmap))
+        child1
+         grandchild 1
+         grandchild 2
+        child2
+         grandchild 3
+         grandchild 4
+        child3
+         grandchild 5
+         grandchild 6
+      
+    
+ +
+ + + diff --git a/cypress/platform/viewer.js b/cypress/platform/viewer.js index 7ff95e163..de7dcafe8 100644 --- a/cypress/platform/viewer.js +++ b/cypress/platform/viewer.js @@ -1,5 +1,6 @@ import externalExample from './mermaid-example-diagram.esm.mjs'; import layouts from './mermaid-layout-elk.esm.mjs'; +import tidyTree from './mermaid-layout-tidy-tree.esm.mjs'; import zenUml from './mermaid-zenuml.esm.mjs'; import mermaid from './mermaid.esm.mjs'; @@ -65,6 +66,7 @@ const contentLoaded = async function () { await mermaid.registerExternalDiagrams([externalExample, zenUml]); mermaid.registerLayoutLoaders(layouts); + mermaid.registerLayoutLoaders(tidyTree); mermaid.initialize(graphObj.mermaid); /** * CC-BY-4.0 diff --git a/cypress/timings.json b/cypress/timings.json index 86d5b5222..8a6a4c00e 100644 --- a/cypress/timings.json +++ b/cypress/timings.json @@ -2,219 +2,227 @@ "durations": [ { "spec": "cypress/integration/other/configuration.spec.js", - "duration": 6297 + "duration": 5841 }, { "spec": "cypress/integration/other/external-diagrams.spec.js", - "duration": 2187 + "duration": 2138 }, { "spec": "cypress/integration/other/ghsa.spec.js", - "duration": 3509 + "duration": 3370 }, { "spec": "cypress/integration/other/iife.spec.js", - "duration": 2218 + "duration": 2052 }, { "spec": "cypress/integration/other/interaction.spec.js", - "duration": 12104 + "duration": 12243 }, { "spec": "cypress/integration/other/rerender.spec.js", - "duration": 2151 + "duration": 2065 }, { "spec": "cypress/integration/other/xss.spec.js", - "duration": 33064 + "duration": 31288 }, { "spec": "cypress/integration/rendering/appli.spec.js", - "duration": 3488 + "duration": 3421 }, { "spec": "cypress/integration/rendering/architecture.spec.ts", - "duration": 106 + "duration": 97 }, { "spec": "cypress/integration/rendering/block.spec.js", - "duration": 18317 + "duration": 18500 }, { "spec": "cypress/integration/rendering/c4.spec.js", - "duration": 5592 + "duration": 5793 }, { "spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js", - "duration": 39358 + "duration": 40966 }, { "spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js", - "duration": 37160 + "duration": 39176 }, { "spec": "cypress/integration/rendering/classDiagram-v2.spec.js", - "duration": 23660 + "duration": 23468 }, { "spec": "cypress/integration/rendering/classDiagram-v3.spec.js", - "duration": 36866 + "duration": 38291 }, { "spec": "cypress/integration/rendering/classDiagram.spec.js", - "duration": 17334 + "duration": 16949 }, { "spec": "cypress/integration/rendering/conf-and-directives.spec.js", - "duration": 9871 + "duration": 9480 }, { "spec": "cypress/integration/rendering/current.spec.js", - "duration": 2833 + "duration": 2753 }, { "spec": "cypress/integration/rendering/erDiagram-unified.spec.js", - "duration": 85321 + "duration": 88028 }, { "spec": "cypress/integration/rendering/erDiagram.spec.js", - "duration": 15673 + "duration": 15615 }, { "spec": "cypress/integration/rendering/errorDiagram.spec.js", - "duration": 3724 + "duration": 3706 }, { "spec": "cypress/integration/rendering/flowchart-elk.spec.js", - "duration": 41178 + "duration": 43905 }, { "spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js", - "duration": 29966 + "duration": 31217 }, { "spec": "cypress/integration/rendering/flowchart-icon.spec.js", - "duration": 7689 + "duration": 7531 }, { "spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts", - "duration": 24709 + "duration": 25423 }, { "spec": "cypress/integration/rendering/flowchart-v2.spec.js", - "duration": 45565 + "duration": 49664 }, { "spec": "cypress/integration/rendering/flowchart.spec.js", - "duration": 31144 + "duration": 32525 }, { "spec": "cypress/integration/rendering/gantt.spec.js", - "duration": 20808 + "duration": 20915 }, { "spec": "cypress/integration/rendering/gitGraph.spec.js", - "duration": 49985 + "duration": 53556 }, { "spec": "cypress/integration/rendering/iconShape.spec.ts", - "duration": 273272 + "duration": 283038 }, { "spec": "cypress/integration/rendering/imageShape.spec.ts", - "duration": 55880 + "duration": 59434 }, { "spec": "cypress/integration/rendering/info.spec.ts", - "duration": 3271 + "duration": 3101 }, { "spec": "cypress/integration/rendering/journey.spec.js", - "duration": 7293 + "duration": 7099 }, { "spec": "cypress/integration/rendering/kanban.spec.ts", - "duration": 7861 + "duration": 7567 }, { "spec": "cypress/integration/rendering/katex.spec.js", - "duration": 3922 + "duration": 3817 }, { "spec": "cypress/integration/rendering/marker_unique_id.spec.js", - "duration": 2726 + "duration": 2624 + }, + { + "spec": "cypress/integration/rendering/mindmap-tidy-tree.spec.js", + "duration": 4246 }, { "spec": "cypress/integration/rendering/mindmap.spec.ts", - "duration": 11670 + "duration": 11967 }, { "spec": "cypress/integration/rendering/newShapes.spec.ts", - "duration": 146020 + "duration": 151914 }, { "spec": "cypress/integration/rendering/oldShapes.spec.ts", - "duration": 114244 + "duration": 116698 }, { "spec": "cypress/integration/rendering/packet.spec.ts", - "duration": 5036 + "duration": 4967 }, { "spec": "cypress/integration/rendering/pie.spec.ts", - "duration": 6545 + "duration": 6700 }, { "spec": "cypress/integration/rendering/quadrantChart.spec.js", - "duration": 9097 + "duration": 8963 }, { "spec": "cypress/integration/rendering/radar.spec.js", - "duration": 5676 + "duration": 5540 }, { "spec": "cypress/integration/rendering/requirement.spec.js", - "duration": 2795 + "duration": 2782 }, { "spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js", - "duration": 51660 + "duration": 54797 }, { "spec": "cypress/integration/rendering/sankey.spec.ts", - "duration": 6957 + "duration": 6914 + }, + { + "spec": "cypress/integration/rendering/sequencediagram-v2.spec.js", + "duration": 20481 }, { "spec": "cypress/integration/rendering/sequencediagram.spec.js", - "duration": 36026 + "duration": 38490 }, { "spec": "cypress/integration/rendering/stateDiagram-v2.spec.js", - "duration": 29551 + "duration": 30766 }, { "spec": "cypress/integration/rendering/stateDiagram.spec.js", - "duration": 17364 + "duration": 16705 }, { "spec": "cypress/integration/rendering/theme.spec.js", - "duration": 30209 + "duration": 30928 }, { "spec": "cypress/integration/rendering/timeline.spec.ts", - "duration": 8699 + "duration": 8424 }, { "spec": "cypress/integration/rendering/treemap.spec.ts", - "duration": 12168 + "duration": 12533 }, { "spec": "cypress/integration/rendering/xyChart.spec.js", - "duration": 21453 + "duration": 21197 }, { "spec": "cypress/integration/rendering/zenuml.spec.js", - "duration": 3577 + "duration": 3455 } ] } diff --git a/docs/config/faq.md b/docs/config/faq.md index db775e438..6d27b658e 100644 --- a/docs/config/faq.md +++ b/docs/config/faq.md @@ -6,7 +6,7 @@ # Frequently Asked Questions -1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217) +1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/1433#issuecomment-1991554712) 2. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785) 3. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621) 4. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136) diff --git a/docs/config/layouts.md b/docs/config/layouts.md new file mode 100644 index 000000000..18e9c9423 --- /dev/null +++ b/docs/config/layouts.md @@ -0,0 +1,40 @@ +> **Warning** +> +> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. +> +> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/layouts.md](../../packages/mermaid/src/docs/config/layouts.md). + +# Layouts + +This page lists the available layout algorithms supported in Mermaid diagrams. + +## Supported Layouts + +- **elk**: [ELK (Eclipse Layout Kernel)](https://www.eclipse.org/elk/) +- **tidy-tree**: Tidy tree layout for hierarchical diagrams [Tidy Tree Configuration](/config/tidy-tree) +- **cose-bilkent**: Cose Bilkent layout for force-directed graphs +- **dagre**: Dagre layout for layered graphs + +## How to Use + +You can specify the layout in your diagram's YAML config or initialization options. For example: + +```mermaid-example +--- +config: + layout: elk +--- +graph TD; + A-->B; + B-->C; +``` + +```mermaid +--- +config: + layout: elk +--- +graph TD; + A-->B; + B-->C; +``` diff --git a/docs/config/setup/config/README.md b/docs/config/setup/config/README.md index 67fca78eb..c811c7b08 100644 --- a/docs/config/setup/config/README.md +++ b/docs/config/setup/config/README.md @@ -19,6 +19,7 @@ - [addDirective](functions/addDirective.md) - [getConfig](functions/getConfig.md) - [getSiteConfig](functions/getSiteConfig.md) +- [getUserDefinedConfig](functions/getUserDefinedConfig.md) - [reset](functions/reset.md) - [sanitize](functions/sanitize.md) - [saveConfigFromInitialize](functions/saveConfigFromInitialize.md) diff --git a/docs/config/setup/config/functions/getUserDefinedConfig.md b/docs/config/setup/config/functions/getUserDefinedConfig.md new file mode 100644 index 000000000..ed39f1337 --- /dev/null +++ b/docs/config/setup/config/functions/getUserDefinedConfig.md @@ -0,0 +1,19 @@ +> **Warning** +> +> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. +> +> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/config/functions/getUserDefinedConfig.md](../../../../../packages/mermaid/src/docs/config/setup/config/functions/getUserDefinedConfig.md). + +[**mermaid**](../../README.md) + +--- + +# Function: getUserDefinedConfig() + +> **getUserDefinedConfig**(): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md) + +Defined in: [packages/mermaid/src/config.ts:252](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L252) + +## Returns + +[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md) diff --git a/docs/config/setup/mermaid/README.md b/docs/config/setup/mermaid/README.md index 3e2cd7a28..653d90592 100644 --- a/docs/config/setup/mermaid/README.md +++ b/docs/config/setup/mermaid/README.md @@ -10,10 +10,6 @@ # mermaid -## Classes - -- [UnknownDiagramError](classes/UnknownDiagramError.md) - ## Interfaces - [DetailedError](interfaces/DetailedError.md) @@ -27,6 +23,7 @@ - [RenderOptions](interfaces/RenderOptions.md) - [RenderResult](interfaces/RenderResult.md) - [RunOptions](interfaces/RunOptions.md) +- [UnknownDiagramError](interfaces/UnknownDiagramError.md) ## Type Aliases diff --git a/docs/config/setup/mermaid/classes/UnknownDiagramError.md b/docs/config/setup/mermaid/classes/UnknownDiagramError.md deleted file mode 100644 index c077f0e34..000000000 --- a/docs/config/setup/mermaid/classes/UnknownDiagramError.md +++ /dev/null @@ -1,159 +0,0 @@ -> **Warning** -> -> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. -> -> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/mermaid/classes/UnknownDiagramError.md](../../../../../packages/mermaid/src/docs/config/setup/mermaid/classes/UnknownDiagramError.md). - -[**mermaid**](../../README.md) - ---- - -# Class: UnknownDiagramError - -Defined in: [packages/mermaid/src/errors.ts:1](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L1) - -## Extends - -- `Error` - -## Constructors - -### new UnknownDiagramError() - -> **new UnknownDiagramError**(`message`): [`UnknownDiagramError`](UnknownDiagramError.md) - -Defined in: [packages/mermaid/src/errors.ts:2](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L2) - -#### Parameters - -##### message - -`string` - -#### Returns - -[`UnknownDiagramError`](UnknownDiagramError.md) - -#### Overrides - -`Error.constructor` - -## Properties - -### cause? - -> `optional` **cause**: `unknown` - -Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es2022.error.d.ts:26 - -#### Inherited from - -`Error.cause` - ---- - -### message - -> **message**: `string` - -Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1077 - -#### Inherited from - -`Error.message` - ---- - -### name - -> **name**: `string` - -Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1076 - -#### Inherited from - -`Error.name` - ---- - -### stack? - -> `optional` **stack**: `string` - -Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1078 - -#### Inherited from - -`Error.stack` - ---- - -### prepareStackTrace()? - -> `static` `optional` **prepareStackTrace**: (`err`, `stackTraces`) => `any` - -Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:143 - -Optional override for formatting stack traces - -#### Parameters - -##### err - -`Error` - -##### stackTraces - -`CallSite`\[] - -#### Returns - -`any` - -#### See - - - -#### Inherited from - -`Error.prepareStackTrace` - ---- - -### stackTraceLimit - -> `static` **stackTraceLimit**: `number` - -Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:145 - -#### Inherited from - -`Error.stackTraceLimit` - -## Methods - -### captureStackTrace() - -> `static` **captureStackTrace**(`targetObject`, `constructorOpt`?): `void` - -Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:136 - -Create .stack property on a target object - -#### Parameters - -##### targetObject - -`object` - -##### constructorOpt? - -`Function` - -#### Returns - -`void` - -#### Inherited from - -`Error.captureStackTrace` diff --git a/docs/config/setup/mermaid/interfaces/LayoutData.md b/docs/config/setup/mermaid/interfaces/LayoutData.md index b4c88454e..32bef322c 100644 --- a/docs/config/setup/mermaid/interfaces/LayoutData.md +++ b/docs/config/setup/mermaid/interfaces/LayoutData.md @@ -10,7 +10,7 @@ # Interface: LayoutData -Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L145) +Defined in: [packages/mermaid/src/rendering-util/types.ts:168](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L168) ## Indexable @@ -22,7 +22,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.co > **config**: [`MermaidConfig`](MermaidConfig.md) -Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L148) +Defined in: [packages/mermaid/src/rendering-util/types.ts:171](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L171) --- @@ -30,7 +30,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.co > **edges**: `Edge`\[] -Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L147) +Defined in: [packages/mermaid/src/rendering-util/types.ts:170](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L170) --- @@ -38,4 +38,4 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.co > **nodes**: `Node`\[] -Defined in: [packages/mermaid/src/rendering-util/types.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L146) +Defined in: [packages/mermaid/src/rendering-util/types.ts:169](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L169) diff --git a/docs/config/setup/mermaid/interfaces/Mermaid.md b/docs/config/setup/mermaid/interfaces/Mermaid.md index fd15b306b..0c63d140a 100644 --- a/docs/config/setup/mermaid/interfaces/Mermaid.md +++ b/docs/config/setup/mermaid/interfaces/Mermaid.md @@ -32,7 +32,7 @@ page. ### detectType() -> **detectType**: (`text`, `config`?) => `string` +> **detectType**: (`text`, `config?`) => `string` Defined in: [packages/mermaid/src/mermaid.ts:449](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L449) @@ -105,7 +105,7 @@ An array of objects with the id of the diagram. ### ~~init()~~ -> **init**: (`config`?, `nodes`?, `callback`?) => `Promise`<`void`> +> **init**: (`config?`, `nodes?`, `callback?`) => `Promise`<`void`> Defined in: [packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L442) @@ -117,7 +117,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/ [`MermaidConfig`](MermaidConfig.md) -**Deprecated**, please set configuration in [initialize](Mermaid.md#initialize). +**Deprecated**, please set configuration in [initialize](#initialize). ##### nodes? @@ -141,13 +141,13 @@ Called once for each rendered diagram's id. #### Deprecated -Use [initialize](Mermaid.md#initialize) and [run](Mermaid.md#run) instead. +Use [initialize](#initialize) and [run](#run) instead. Renders the mermaid diagrams #### Deprecated -Use [initialize](Mermaid.md#initialize) and [run](Mermaid.md#run) instead. +Use [initialize](#initialize) and [run](#run) instead. --- @@ -176,7 +176,7 @@ Configuration object for mermaid. ### ~~mermaidAPI~~ -> **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](MermaidConfig.md); `getConfig`: () => [`MermaidConfig`](MermaidConfig.md); `getDiagramFromText`: (`text`, `metadata`) => `Promise`<`Diagram`>; `getSiteConfig`: () => [`MermaidConfig`](MermaidConfig.md); `globalReset`: () => `void`; `initialize`: (`userOptions`) => `void`; `parse`: (`text`, `parseOptions`) => `Promise`<`false` | [`ParseResult`](ParseResult.md)>(`text`, `parseOptions`?) => `Promise`<[`ParseResult`](ParseResult.md)>; `render`: (`id`, `text`, `svgContainingElement`?) => `Promise`<[`RenderResult`](RenderResult.md)>; `reset`: () => `void`; `setConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); `updateSiteConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); }> +> **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](MermaidConfig.md); `getConfig`: () => [`MermaidConfig`](MermaidConfig.md); `getDiagramFromText`: (`text`, `metadata`) => `Promise`<`Diagram`>; `getSiteConfig`: () => [`MermaidConfig`](MermaidConfig.md); `globalReset`: () => `void`; `initialize`: (`userOptions`) => `void`; `parse`: {(`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)>; (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)>; }; `render`: (`id`, `text`, `svgContainingElement?`) => `Promise`<[`RenderResult`](RenderResult.md)>; `reset`: () => `void`; `setConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); `updateSiteConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); }> Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L436) @@ -184,73 +184,81 @@ Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/ #### Deprecated -Use [parse](Mermaid.md#parse) and [render](Mermaid.md#render) instead. Please [open a discussion](https://github.com/mermaid-js/mermaid/discussions) if your use case does not fit the new API. +Use [parse](#parse) and [render](#render) instead. Please [open a discussion](https://github.com/mermaid-js/mermaid/discussions) if your use case does not fit the new API. --- ### parse() -> **parse**: (`text`, `parseOptions`) => `Promise`<`false` | [`ParseResult`](ParseResult.md)>(`text`, `parseOptions`?) => `Promise`<[`ParseResult`](ParseResult.md)> +> **parse**: {(`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)>; (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)>; } Defined in: [packages/mermaid/src/mermaid.ts:437](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L437) +#### Call Signature + +> (`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)> + Parse the text and validate the syntax. -#### Parameters +##### Parameters -##### text +###### text `string` The mermaid diagram definition. -##### parseOptions +###### parseOptions [`ParseOptions`](ParseOptions.md) & `object` Options for parsing. -#### Returns +##### Returns `Promise`<`false` | [`ParseResult`](ParseResult.md)> An object with the `diagramType` set to type of the diagram if valid. Otherwise `false` if parseOptions.suppressErrors is `true`. -#### See +##### See [ParseOptions](ParseOptions.md) -#### Throws +##### Throws Error if the diagram is invalid and parseOptions.suppressErrors is false or not set. +#### Call Signature + +> (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)> + Parse the text and validate the syntax. -#### Parameters +##### Parameters -##### text +###### text `string` The mermaid diagram definition. -##### parseOptions? +###### parseOptions? [`ParseOptions`](ParseOptions.md) Options for parsing. -#### Returns +##### Returns `Promise`<[`ParseResult`](ParseResult.md)> An object with the `diagramType` set to type of the diagram if valid. Otherwise `false` if parseOptions.suppressErrors is `true`. -#### See +##### See [ParseOptions](ParseOptions.md) -#### Throws +##### Throws Error if the diagram is invalid and parseOptions.suppressErrors is false or not set. @@ -332,7 +340,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:444](https://github.com/mermaid-js/ ### render() -> **render**: (`id`, `text`, `svgContainingElement`?) => `Promise`<[`RenderResult`](RenderResult.md)> +> **render**: (`id`, `text`, `svgContainingElement?`) => `Promise`<[`RenderResult`](RenderResult.md)> Defined in: [packages/mermaid/src/mermaid.ts:438](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L438) diff --git a/docs/config/setup/mermaid/interfaces/ParseOptions.md b/docs/config/setup/mermaid/interfaces/ParseOptions.md index ea96f2706..628da0da0 100644 --- a/docs/config/setup/mermaid/interfaces/ParseOptions.md +++ b/docs/config/setup/mermaid/interfaces/ParseOptions.md @@ -10,7 +10,7 @@ # Interface: ParseOptions -Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L72) +Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88) ## Properties @@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mer > `optional` **suppressErrors**: `boolean` -Defined in: [packages/mermaid/src/types.ts:77](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L77) +Defined in: [packages/mermaid/src/types.ts:93](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L93) If `true`, parse will return `false` instead of throwing error when the diagram is invalid. The `parseError` function will not be called. diff --git a/docs/config/setup/mermaid/interfaces/ParseResult.md b/docs/config/setup/mermaid/interfaces/ParseResult.md index 7a5990610..0e200aa95 100644 --- a/docs/config/setup/mermaid/interfaces/ParseResult.md +++ b/docs/config/setup/mermaid/interfaces/ParseResult.md @@ -10,7 +10,7 @@ # Interface: ParseResult -Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L80) +Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96) ## Properties @@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mer > **config**: [`MermaidConfig`](MermaidConfig.md) -Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88) +Defined in: [packages/mermaid/src/types.ts:104](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L104) The config passed as YAML frontmatter or directives @@ -28,6 +28,6 @@ The config passed as YAML frontmatter or directives > **diagramType**: `string` -Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84) +Defined in: [packages/mermaid/src/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L100) The diagram type, e.g. 'flowchart', 'sequence', etc. diff --git a/docs/config/setup/mermaid/interfaces/RenderResult.md b/docs/config/setup/mermaid/interfaces/RenderResult.md index fc5fac4f5..237c51de2 100644 --- a/docs/config/setup/mermaid/interfaces/RenderResult.md +++ b/docs/config/setup/mermaid/interfaces/RenderResult.md @@ -10,7 +10,7 @@ # Interface: RenderResult -Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L98) +Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114) ## Properties @@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mer > `optional` **bindFunctions**: (`element`) => `void` -Defined in: [packages/mermaid/src/types.ts:116](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L116) +Defined in: [packages/mermaid/src/types.ts:132](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L132) Bind function to be called after the svg has been inserted into the DOM. This is necessary for adding event listeners to the elements in the svg. @@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present. > **diagramType**: `string` -Defined in: [packages/mermaid/src/types.ts:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L106) +Defined in: [packages/mermaid/src/types.ts:122](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L122) The diagram type, e.g. 'flowchart', 'sequence', etc. @@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc. > **svg**: `string` -Defined in: [packages/mermaid/src/types.ts:102](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L102) +Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118) The svg code for the rendered graph. diff --git a/docs/config/setup/mermaid/interfaces/UnknownDiagramError.md b/docs/config/setup/mermaid/interfaces/UnknownDiagramError.md new file mode 100644 index 000000000..2415d77ee --- /dev/null +++ b/docs/config/setup/mermaid/interfaces/UnknownDiagramError.md @@ -0,0 +1,65 @@ +> **Warning** +> +> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. +> +> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/mermaid/interfaces/UnknownDiagramError.md](../../../../../packages/mermaid/src/docs/config/setup/mermaid/interfaces/UnknownDiagramError.md). + +[**mermaid**](../../README.md) + +--- + +# Interface: UnknownDiagramError + +Defined in: [packages/mermaid/src/errors.ts:1](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L1) + +## Extends + +- `Error` + +## Properties + +### cause? + +> `optional` **cause**: `unknown` + +Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es2022.error.d.ts:26 + +#### Inherited from + +`Error.cause` + +--- + +### message + +> **message**: `string` + +Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1077 + +#### Inherited from + +`Error.message` + +--- + +### name + +> **name**: `string` + +Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1076 + +#### Inherited from + +`Error.name` + +--- + +### stack? + +> `optional` **stack**: `string` + +Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1078 + +#### Inherited from + +`Error.stack` diff --git a/docs/config/setup/mermaid/type-aliases/InternalHelpers.md b/docs/config/setup/mermaid/type-aliases/InternalHelpers.md index 6baf786fe..bfaeabd12 100644 --- a/docs/config/setup/mermaid/type-aliases/InternalHelpers.md +++ b/docs/config/setup/mermaid/type-aliases/InternalHelpers.md @@ -10,6 +10,6 @@ # Type Alias: InternalHelpers -> **InternalHelpers**: _typeof_ `internalHelpers` +> **InternalHelpers** = _typeof_ `internalHelpers` Defined in: [packages/mermaid/src/internals.ts:33](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/internals.ts#L33) diff --git a/docs/config/setup/mermaid/type-aliases/ParseErrorFunction.md b/docs/config/setup/mermaid/type-aliases/ParseErrorFunction.md index 78f27854c..dd5938478 100644 --- a/docs/config/setup/mermaid/type-aliases/ParseErrorFunction.md +++ b/docs/config/setup/mermaid/type-aliases/ParseErrorFunction.md @@ -10,7 +10,7 @@ # Type Alias: ParseErrorFunction() -> **ParseErrorFunction**: (`err`, `hash`?) => `void` +> **ParseErrorFunction** = (`err`, `hash?`) => `void` Defined in: [packages/mermaid/src/Diagram.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/Diagram.ts#L10) diff --git a/docs/config/setup/mermaid/type-aliases/SVG.md b/docs/config/setup/mermaid/type-aliases/SVG.md index 8bfb7bda0..184f3e2cd 100644 --- a/docs/config/setup/mermaid/type-aliases/SVG.md +++ b/docs/config/setup/mermaid/type-aliases/SVG.md @@ -10,6 +10,6 @@ # Type Alias: SVG -> **SVG**: `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`> +> **SVG** = `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`> Defined in: [packages/mermaid/src/diagram-api/types.ts:126](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L126) diff --git a/docs/config/setup/mermaid/type-aliases/SVGGroup.md b/docs/config/setup/mermaid/type-aliases/SVGGroup.md index 5e53052fd..8d673aafb 100644 --- a/docs/config/setup/mermaid/type-aliases/SVGGroup.md +++ b/docs/config/setup/mermaid/type-aliases/SVGGroup.md @@ -10,6 +10,6 @@ # Type Alias: SVGGroup -> **SVGGroup**: `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`> +> **SVGGroup** = `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`> Defined in: [packages/mermaid/src/diagram-api/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L128) diff --git a/docs/config/tidy-tree.md b/docs/config/tidy-tree.md new file mode 100644 index 000000000..1d4227596 --- /dev/null +++ b/docs/config/tidy-tree.md @@ -0,0 +1,89 @@ +> **Warning** +> +> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. +> +> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/tidy-tree.md](../../packages/mermaid/src/docs/config/tidy-tree.md). + +# Tidy-tree Layout + +The **tidy-tree** layout arranges nodes in a hierarchical, tree-like structure. It is especially useful for diagrams where parent-child relationships are important, such as mindmaps. + +## Features + +- Organizes nodes in a tidy, non-overlapping tree +- Ideal for mindmaps and hierarchical data +- Automatically adjusts spacing for readability + +## Example Usage + +```mermaid-example +--- +config: + layout: tidy-tree +--- +mindmap +root((mindmap is a long thing)) + A + B + C + D +``` + +```mermaid +--- +config: + layout: tidy-tree +--- +mindmap +root((mindmap is a long thing)) + A + B + C + D +``` + +```mermaid-example +--- +config: + layout: tidy-tree +--- +mindmap +root((mindmap)) + Origins + Long history + ::icon(fa fa-book) + Popularisation + British popular psychology author Tony Buzan + Research + On effectiveness
and features + On Automatic creation + Uses + Creative techniques + Strategic planning + Argument mapping +``` + +```mermaid +--- +config: + layout: tidy-tree +--- +mindmap +root((mindmap)) + Origins + Long history + ::icon(fa fa-book) + Popularisation + British popular psychology author Tony Buzan + Research + On effectiveness
and features + On Automatic creation + Uses + Creative techniques + Strategic planning + Argument mapping +``` + +## Note + +- Currently, tidy-tree is primarily supported for mindmap diagrams. diff --git a/docs/syntax/flowchart.md b/docs/syntax/flowchart.md index daaa29581..23c34509c 100644 --- a/docs/syntax/flowchart.md +++ b/docs/syntax/flowchart.md @@ -326,7 +326,9 @@ Below is a comprehensive list of the newly introduced shapes and their correspon | **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** | | --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- | +| Bang | Bang | `bang` | Bang | `bang` | | Card | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` | +| Cloud | Cloud | `cloud` | cloud | `cloud` | | Collate | Hourglass | `hourglass` | Represents a collate operation | `collate`, `hourglass` | | Com Link | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` | | Comment | Curly Brace | `brace` | Adds a comment | `brace-l`, `comment` | @@ -983,11 +985,23 @@ flowchart TD - `b` - **w**: The width of the image. If not defined, this will default to the natural width of the image. - **h**: The height of the image. If not defined, this will default to the natural height of the image. -- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are: +- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the width (`w`) accordingly to the height (`h`). If not defined, this will default to `off` Possible values are: - `on` - `off` -These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging. +If you want to resize an image, but keep the same aspect ratio, set `h`, and set `constraint: on` to constrain the aspect ratio. E.g. + +```mermaid-example +flowchart TD + %% My image with a constrained aspect ratio + A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" } +``` + +```mermaid +flowchart TD + %% My image with a constrained aspect ratio + A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" } +``` ## Links between nodes diff --git a/docs/syntax/mindmap.md b/docs/syntax/mindmap.md index 1adaa2c49..844b3293c 100644 --- a/docs/syntax/mindmap.md +++ b/docs/syntax/mindmap.md @@ -314,3 +314,22 @@ You can also refer the [implementation in the live editor](https://github.com/me cspell:locale en,en-gb cspell:ignore Buzan ---> + +## Layouts + +Mermaid also supports a Tidy Tree layout for mindmaps. + +``` +--- +config: + layout: tidy-tree +--- +mindmap +root((mindmap is a long thing)) + A + B + C + D +``` + +Instructions to add and register tidy-tree layout are present in [Tidy Tree Configuration](/config/tidy-tree) diff --git a/docs/syntax/sequenceDiagram.md b/docs/syntax/sequenceDiagram.md index 84240a0cd..a8f502dcd 100644 --- a/docs/syntax/sequenceDiagram.md +++ b/docs/syntax/sequenceDiagram.md @@ -74,6 +74,126 @@ sequenceDiagram Bob->>Alice: Hi Alice ``` +### Boundary + +If you want to use the boundary symbol for a participant, use the JSON configuration syntax as shown below. + +```mermaid-example +sequenceDiagram + participant Alice@{ "type" : "boundary" } + participant Bob + Alice->>Bob: Request from boundary + Bob->>Alice: Response to boundary +``` + +```mermaid +sequenceDiagram + participant Alice@{ "type" : "boundary" } + participant Bob + Alice->>Bob: Request from boundary + Bob->>Alice: Response to boundary +``` + +### Control + +If you want to use the control symbol for a participant, use the JSON configuration syntax as shown below. + +```mermaid-example +sequenceDiagram + participant Alice@{ "type" : "control" } + participant Bob + Alice->>Bob: Control request + Bob->>Alice: Control response +``` + +```mermaid +sequenceDiagram + participant Alice@{ "type" : "control" } + participant Bob + Alice->>Bob: Control request + Bob->>Alice: Control response +``` + +### Entity + +If you want to use the entity symbol for a participant, use the JSON configuration syntax as shown below. + +```mermaid-example +sequenceDiagram + participant Alice@{ "type" : "entity" } + participant Bob + Alice->>Bob: Entity request + Bob->>Alice: Entity response +``` + +```mermaid +sequenceDiagram + participant Alice@{ "type" : "entity" } + participant Bob + Alice->>Bob: Entity request + Bob->>Alice: Entity response +``` + +### Database + +If you want to use the database symbol for a participant, use the JSON configuration syntax as shown below. + +```mermaid-example +sequenceDiagram + participant Alice@{ "type" : "database" } + participant Bob + Alice->>Bob: DB query + Bob->>Alice: DB result +``` + +```mermaid +sequenceDiagram + participant Alice@{ "type" : "database" } + participant Bob + Alice->>Bob: DB query + Bob->>Alice: DB result +``` + +### Collections + +If you want to use the collections symbol for a participant, use the JSON configuration syntax as shown below. + +```mermaid-example +sequenceDiagram + participant Alice@{ "type" : "collections" } + participant Bob + Alice->>Bob: Collections request + Bob->>Alice: Collections response +``` + +```mermaid +sequenceDiagram + participant Alice@{ "type" : "collections" } + participant Bob + Alice->>Bob: Collections request + Bob->>Alice: Collections response +``` + +### Queue + +If you want to use the queue symbol for a participant, use the JSON configuration syntax as shown below. + +```mermaid-example +sequenceDiagram + participant Alice@{ "type" : "queue" } + participant Bob + Alice->>Bob: Queue message + Bob->>Alice: Queue response +``` + +```mermaid +sequenceDiagram + participant Alice@{ "type" : "queue" } + participant Bob + Alice->>Bob: Queue message + Bob->>Alice: Queue response +``` + ### Aliases The actor can have a convenient identifier and a descriptive label. diff --git a/docs/syntax/xyChart.md b/docs/syntax/xyChart.md index dec16a518..742a4f18a 100644 --- a/docs/syntax/xyChart.md +++ b/docs/syntax/xyChart.md @@ -138,7 +138,7 @@ xychart ## Chart Theme Variables -Themes for xychart resides inside xychart attribute so to set the variables use this syntax: +Themes for xychart reside inside the `xychart` attribute, allowing customization through the following syntax: ```yaml --- @@ -163,6 +163,52 @@ config: | yAxisLineColor | Color of the y-axis line | | plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" | +### Setting Colors for Lines and Bars + +To set the color for lines and bars, use the `plotColorPalette` parameter. Colors in the palette will correspond sequentially to the elements in your chart (e.g., first bar/line will use the first color specified in the palette). + +```mermaid-example +--- +config: + themeVariables: + xyChart: + plotColorPalette: '#000000, #0000FF, #00FF00, #FF0000' +--- +xychart +title "Different Colors in xyChart" +x-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"] +y-axis "valuesY" 0 --> 50 +%% Black line +line [10,20,30,40] +%% Blue bar +bar [20,30,25,35] +%% Green bar +bar [15,25,20,30] +%% Red line +line [5,15,25,35] +``` + +```mermaid +--- +config: + themeVariables: + xyChart: + plotColorPalette: '#000000, #0000FF, #00FF00, #FF0000' +--- +xychart +title "Different Colors in xyChart" +x-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"] +y-axis "valuesY" 0 --> 50 +%% Black line +line [10,20,30,40] +%% Blue bar +bar [20,30,25,35] +%% Green bar +bar [15,25,20,30] +%% Red line +line [5,15,25,35] +``` + ## Example on config and theme ```mermaid-example diff --git a/eslint.config.js b/eslint.config.js index 7a144ee00..416fca2c6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,6 +17,7 @@ export default tseslint.config( ...tseslint.configs.stylisticTypeChecked, { ignores: [ + '**/*.d.ts', '**/dist/', '**/node_modules/', '.git/', diff --git a/packages/mermaid-layout-elk/README.md b/packages/mermaid-layout-elk/README.md index ab3289d6e..eec287263 100644 --- a/packages/mermaid-layout-elk/README.md +++ b/packages/mermaid-layout-elk/README.md @@ -2,7 +2,7 @@ This package provides a layout engine for Mermaid based on the [ELK](https://www.eclipse.org/elk/) layout engine. -> [!NOTE] +> [!NOTE] > The ELK Layout engine will not be available in all providers that support mermaid by default. > The websites will have to install the `@mermaid-js/layout-elk` package to use the ELK layout engine. @@ -69,4 +69,4 @@ mermaid.registerLayoutLoaders(elkLayouts); - `elk.mrtree`: Multi-root tree layout - `elk.sporeOverlap`: Spore overlap layout - + diff --git a/packages/mermaid-layout-elk/src/find-common-ancestor.d.ts b/packages/mermaid-layout-elk/src/find-common-ancestor.d.ts new file mode 100644 index 000000000..db94f42c9 --- /dev/null +++ b/packages/mermaid-layout-elk/src/find-common-ancestor.d.ts @@ -0,0 +1,9 @@ +export interface TreeData { + parentById: Record; + childrenById: Record; +} +export declare const findCommonAncestor: ( + id1: string, + id2: string, + { parentById }: TreeData +) => string; diff --git a/packages/mermaid-layout-elk/src/render.ts b/packages/mermaid-layout-elk/src/render.ts index d1c44b67f..d7d6974f5 100644 --- a/packages/mermaid-layout-elk/src/render.ts +++ b/packages/mermaid-layout-elk/src/render.ts @@ -4,7 +4,8 @@ import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from ' import { type TreeData, findCommonAncestor } from './find-common-ancestor.js'; type Node = LayoutData['nodes'][number]; - +// Used to calculate distances in order to avoid floating number rounding issues when comparing floating numbers +const epsilon = 0.0001; interface LabelData { width: number; height: number; @@ -13,11 +14,20 @@ interface LabelData { } interface NodeWithVertex extends Omit { - children?: unknown[]; + children?: LayoutData['nodes']; labelData?: LabelData; domId?: Node['domId'] | SVGGroup | d3.Selection; } - +interface Point { + x: number; + y: number; +} +function distance(p1?: Point, p2?: Point): number { + if (!p1 || !p2) { + return 0; + } + return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); +} export const render = async ( data4Layout: LayoutData, svg: SVG, @@ -51,15 +61,30 @@ export const render = async ( // Add the element to the DOM if (!node.isGroup) { + // Create a clean node object for ELK with only the properties it expects const child: NodeWithVertex = { - ...node, + id: node.id, + width: node.width, + height: node.height, + // Store the original node data for later use + label: node.label, + isGroup: node.isGroup, + shape: node.shape, + padding: node.padding, + cssClasses: node.cssClasses, + cssStyles: node.cssStyles, + look: node.look, + // Include parentId for subgraph processing + parentId: node.parentId, }; graph.children.push(child); nodeDb[node.id] = child; const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir }); const boundingBox = childNodeEl.node()!.getBBox(); + // Store the domId separately for rendering, not in the ELK graph child.domId = childNodeEl; + child.calcIntersect = node.calcIntersect; child.width = boundingBox.width; child.height = boundingBox.height; } else { @@ -459,302 +484,6 @@ export const render = async ( } } - function intersectLine( - p1: { y: number; x: number }, - p2: { y: number; x: number }, - q1: { x: any; y: any }, - q2: { x: any; y: any } - ) { - log.debug('UIO intersectLine', p1, p2, q1, q2); - // Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994, - // p7 and p473. - - // let a1, a2, b1, b2, c1, c2; - // let r1, r2, r3, r4; - // let denom, offset, num; - // let x, y; - - // Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x + - // b1 y + c1 = 0. - const a1 = p2.y - p1.y; - const b1 = p1.x - p2.x; - const c1 = p2.x * p1.y - p1.x * p2.y; - - // Compute r3 and r4. - const r3 = a1 * q1.x + b1 * q1.y + c1; - const r4 = a1 * q2.x + b1 * q2.y + c1; - - const epsilon = 1e-6; - - // Check signs of r3 and r4. If both point 3 and point 4 lie on - // same side of line 1, the line segments do not intersect. - if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) { - return /*DON'T_INTERSECT*/; - } - - // Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0 - const a2 = q2.y - q1.y; - const b2 = q1.x - q2.x; - const c2 = q2.x * q1.y - q1.x * q2.y; - - // Compute r1 and r2 - const r1 = a2 * p1.x + b2 * p1.y + c2; - const r2 = a2 * p2.x + b2 * p2.y + c2; - - // Check signs of r1 and r2. If both point 1 and point 2 lie - // on same side of second line segment, the line segments do - // not intersect. - if (Math.abs(r1) < epsilon && Math.abs(r2) < epsilon && sameSign(r1, r2)) { - return /*DON'T_INTERSECT*/; - } - - // Line segments intersect: compute intersection point. - const denom = a1 * b2 - a2 * b1; - if (denom === 0) { - return /*COLLINEAR*/; - } - - const offset = Math.abs(denom / 2); - - // The denom/2 is to get rounding instead of truncating. It - // is added or subtracted to the numerator, depending upon the - // sign of the numerator. - let num = b1 * c2 - b2 * c1; - const x = num < 0 ? (num - offset) / denom : (num + offset) / denom; - - num = a2 * c1 - a1 * c2; - const y = num < 0 ? (num - offset) / denom : (num + offset) / denom; - - return { x: x, y: y }; - } - - function sameSign(r1: number, r2: number) { - return r1 * r2 > 0; - } - const diamondIntersection = ( - bounds: { x: any; y: any; width: any; height: any }, - outsidePoint: { x: number; y: number }, - insidePoint: any - ) => { - const x1 = bounds.x; - const y1 = bounds.y; - - const w = bounds.width; //+ bounds.padding; - const h = bounds.height; // + bounds.padding; - - const polyPoints = [ - { x: x1, y: y1 - h / 2 }, - { x: x1 + w / 2, y: y1 }, - { x: x1, y: y1 + h / 2 }, - { x: x1 - w / 2, y: y1 }, - ]; - log.debug( - `APA16 diamondIntersection calc abc89: - outsidePoint: ${JSON.stringify(outsidePoint)} - insidePoint : ${JSON.stringify(insidePoint)} - node-bounds : x:${bounds.x} y:${bounds.y} w:${bounds.width} h:${bounds.height}`, - JSON.stringify(polyPoints) - ); - - const intersections = []; - - let minX = Number.POSITIVE_INFINITY; - let minY = Number.POSITIVE_INFINITY; - - polyPoints.forEach(function (entry) { - minX = Math.min(minX, entry.x); - minY = Math.min(minY, entry.y); - }); - - const left = x1 - w / 2 - minX; - const top = y1 - h / 2 - minY; - - for (let i = 0; i < polyPoints.length; i++) { - const p1 = polyPoints[i]; - const p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0]; - const intersect = intersectLine( - bounds, - outsidePoint, - { x: left + p1.x, y: top + p1.y }, - { x: left + p2.x, y: top + p2.y } - ); - - if (intersect) { - intersections.push(intersect); - } - } - - if (!intersections.length) { - return bounds; - } - - log.debug('UIO intersections', intersections); - - if (intersections.length > 1) { - // More intersections, find the one nearest to edge end point - intersections.sort(function (p, q) { - const pdx = p.x - outsidePoint.x; - const pdy = p.y - outsidePoint.y; - const distp = Math.sqrt(pdx * pdx + pdy * pdy); - - const qdx = q.x - outsidePoint.x; - const qdy = q.y - outsidePoint.y; - const distq = Math.sqrt(qdx * qdx + qdy * qdy); - - return distp < distq ? -1 : distp === distq ? 0 : 1; - }); - } - - return intersections[0]; - }; - - const intersection = ( - node: { x: any; y: any; width: number; height: number }, - outsidePoint: { x: number; y: number }, - insidePoint: { x: number; y: number } - ) => { - log.debug(`intersection calc abc89: - outsidePoint: ${JSON.stringify(outsidePoint)} - insidePoint : ${JSON.stringify(insidePoint)} - node : x:${node.x} y:${node.y} w:${node.width} h:${node.height}`); - const x = node.x; - const y = node.y; - - const dx = Math.abs(x - insidePoint.x); - // const dy = Math.abs(y - insidePoint.y); - const w = node.width / 2; - let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx; - const h = node.height / 2; - - const Q = Math.abs(outsidePoint.y - insidePoint.y); - const R = Math.abs(outsidePoint.x - insidePoint.x); - - if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) { - // Intersection is top or bottom of rect. - const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y; - r = (R * q) / Q; - const res = { - x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r, - y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q, - }; - - if (r === 0) { - res.x = outsidePoint.x; - res.y = outsidePoint.y; - } - if (R === 0) { - res.x = outsidePoint.x; - } - if (Q === 0) { - res.y = outsidePoint.y; - } - - log.debug(`abc89 topp/bott calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res); // cspell: disable-line - - return res; - } else { - // Intersection on sides of rect - if (insidePoint.x < outsidePoint.x) { - r = outsidePoint.x - w - x; - } else { - // r = outsidePoint.x - w - x; - r = x - w - outsidePoint.x; - } - const q = (Q * r) / R; - // OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x + dx - w; - // OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r; - let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r; - // let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r; - let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q; - log.debug(`sides calc abc89, Q ${Q}, q ${q}, R ${R}, r ${r}`, { _x, _y }); - if (r === 0) { - _x = outsidePoint.x; - _y = outsidePoint.y; - } - if (R === 0) { - _x = outsidePoint.x; - } - if (Q === 0) { - _y = outsidePoint.y; - } - - return { x: _x, y: _y }; - } - }; - const outsideNode = ( - node: { x: any; y: any; width: number; height: number }, - point: { x: number; y: number } - ) => { - const x = node.x; - const y = node.y; - const dx = Math.abs(point.x - x); - const dy = Math.abs(point.y - y); - const w = node.width / 2; - const h = node.height / 2; - if (dx >= w || dy >= h) { - return true; - } - return false; - }; - /** - * This function will page a path and node where the last point(s) in the path is inside the node - * and return an update path ending by the border of the node. - */ - const cutPathAtIntersect = ( - _points: any[], - bounds: { x: any; y: any; width: any; height: any; padding: any }, - isDiamond: boolean - ) => { - log.debug('APA18 cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond); - const points: any[] = []; - let lastPointOutside = _points[0]; - let isInside = false; - _points.forEach((point: any) => { - // check if point is inside the boundary rect - if (!outsideNode(bounds, point) && !isInside) { - // First point inside the rect found - // Calc the intersection coord between the point and the last point outside the rect - let inter; - - if (isDiamond) { - const inter2 = diamondIntersection(bounds, lastPointOutside, point); - const distance = Math.sqrt( - (lastPointOutside.x - inter2.x) ** 2 + (lastPointOutside.y - inter2.y) ** 2 - ); - if (distance > 1) { - inter = inter2; - } - } - if (!inter) { - inter = intersection(bounds, lastPointOutside, point); - } - - // Check case where the intersection is the same as the last point - let pointPresent = false; - points.forEach((p) => { - pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y); - }); - // if (!pointPresent) { - if (!points.some((e) => e.x === inter.x && e.y === inter.y)) { - points.push(inter); - } else { - log.debug('abc88 no intersect', inter, points); - } - // points.push(inter); - isInside = true; - } else { - // Outside - log.debug('abc88 outside', point, lastPointOutside, points); - lastPointOutside = point; - // points.push(point); - if (!isInside) { - points.push(point); - } - } - }); - return points; - }; - // @ts-ignore - ELK is not typed const elk = new ELK(); const element = svg.select('g'); @@ -869,11 +598,16 @@ export const render = async ( delete node.height; } }); - elkGraph.edges.forEach((edge: any) => { + log.debug('APA01 processing edges, count:', elkGraph.edges.length); + elkGraph.edges.forEach((edge: any, index: number) => { + log.debug('APA01 processing edge', index, ':', edge); const source = edge.sources[0]; const target = edge.targets[0]; + log.debug('APA01 source:', source, 'target:', target); + log.debug('APA01 nodeDb[source]:', nodeDb[source]); + log.debug('APA01 nodeDb[target]:', nodeDb[target]); - if (nodeDb[source].parentId !== nodeDb[target].parentId) { + if (nodeDb[source] && nodeDb[target] && nodeDb[source].parentId !== nodeDb[target].parentId) { const ancestorId = findCommonAncestor(source, target, parentLookupDb); // an edge that breaks a subgraph has been identified, set configuration accordingly setIncludeChildrenPolicy(source, ancestorId); @@ -881,7 +615,37 @@ export const render = async ( } }); - const g = await elk.layout(elkGraph); + log.debug('APA01 before'); + log.debug('APA01 elkGraph structure:', JSON.stringify(elkGraph, null, 2)); + log.debug('APA01 elkGraph.children length:', elkGraph.children?.length); + log.debug('APA01 elkGraph.edges length:', elkGraph.edges?.length); + + // Validate that all edge references exist as nodes + elkGraph.edges?.forEach((edge: any, index: number) => { + log.debug(`APA01 validating edge ${index}:`, edge); + if (edge.sources) { + edge.sources.forEach((sourceId: any) => { + const sourceExists = elkGraph.children?.some((child: any) => child.id === sourceId); + log.debug(`APA01 source ${sourceId} exists:`, sourceExists); + }); + } + if (edge.targets) { + edge.targets.forEach((targetId: any) => { + const targetExists = elkGraph.children?.some((child: any) => child.id === targetId); + log.debug(`APA01 target ${targetId} exists:`, targetExists); + }); + } + }); + + let g; + try { + g = await elk.layout(elkGraph); + log.debug('APA01 after - success'); + log.debug('APA01 layout result:', JSON.stringify(g, null, 2)); + } catch (error) { + log.error('APA01 ELK layout error:', error); + throw error; + } // debugger; await drawNodes(0, 0, g.children, svg, subGraphsEl, 0); @@ -969,42 +733,37 @@ export const render = async ( startNode.innerHTML ); } - if (startNode.shape === 'diamond' || startNode.shape === 'diam') { - edge.points.unshift({ - x: startNode.offset.posX + startNode.width / 2, - y: startNode.offset.posY + startNode.height / 2, - }); - } - if (endNode.shape === 'diamond' || endNode.shape === 'diam') { - edge.points.push({ - x: endNode.offset.posX + endNode.width / 2, - y: endNode.offset.posY + endNode.height / 2, - }); - } - edge.points = cutPathAtIntersect( - edge.points.reverse(), - { - x: startNode.offset.posX + startNode.width / 2, - y: startNode.offset.posY + startNode.height / 2, - width: sw, - height: startNode.height, - padding: startNode.padding, - }, - startNode.shape === 'diamond' || startNode.shape === 'diam' - ).reverse(); + if (startNode.calcIntersect) { + const intersection = startNode.calcIntersect( + { + x: startNode.offset.posX + startNode.width / 2, + y: startNode.offset.posY + startNode.height / 2, + width: startNode.width, + height: startNode.height, + }, + edge.points[0] + ); - edge.points = cutPathAtIntersect( - edge.points, - { - x: endNode.offset.posX + endNode.width / 2, - y: endNode.offset.posY + endNode.height / 2, - width: ew, - height: endNode.height, - padding: endNode.padding, - }, - endNode.shape === 'diamond' || endNode.shape === 'diam' - ); + if (distance(intersection, edge.points[0]) > epsilon) { + edge.points.unshift(intersection); + } + } + if (endNode.calcIntersect) { + const intersection = endNode.calcIntersect( + { + x: endNode.offset.posX + endNode.width / 2, + y: endNode.offset.posY + endNode.height / 2, + width: endNode.width, + height: endNode.height, + }, + edge.points[edge.points.length - 1] + ); + + if (distance(intersection, edge.points[edge.points.length - 1]) > epsilon) { + edge.points.push(intersection); + } + } const paths = insertEdge( edgesEl, @@ -1015,7 +774,6 @@ export const render = async ( endNode, data4Layout.diagramId ); - log.info('APA12 edge points after insert', JSON.stringify(edge.points)); edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2; edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2; diff --git a/packages/mermaid-layout-elk/tsconfig.json b/packages/mermaid-layout-elk/tsconfig.json index 0d701cede..8f83e2bad 100644 --- a/packages/mermaid-layout-elk/tsconfig.json +++ b/packages/mermaid-layout-elk/tsconfig.json @@ -5,6 +5,6 @@ "outDir": "./dist", "types": ["vitest/importMeta", "vitest/globals"] }, - "include": ["./src/**/*.ts"], + "include": ["./src/**/*.ts", "./src/**/*.d.ts"], "typeRoots": ["./src/types"] } diff --git a/packages/mermaid-layout-tidy-tree/README.md b/packages/mermaid-layout-tidy-tree/README.md new file mode 100644 index 000000000..e8ae05f4c --- /dev/null +++ b/packages/mermaid-layout-tidy-tree/README.md @@ -0,0 +1,59 @@ +# @mermaid-js/layout-tidy-tree + +This package provides a bidirectional tidy tree layout engine for Mermaid based on the non-layered-tidy-tree-layout algorithm. + +> [!NOTE] +> The Tidy Tree Layout engine will not be available in all providers that support mermaid by default. +> The websites will have to install the @mermaid-js/layout-tidy-tree package to use the Tidy Tree layout engine. + +## Usage + +``` +--- +config: + layout: tidy-tree +--- +mindmap +root((mindmap)) + A + B +``` + +### With bundlers + +```sh +npm install @mermaid-js/layout-tidy-tree +``` + +```ts +import mermaid from 'mermaid'; +import tidyTreeLayouts from '@mermaid-js/layout-tidy-tree'; + +mermaid.registerLayoutLoaders(tidyTreeLayouts); +``` + +### With CDN + +```html + +``` + +## Tidy Tree Layout Overview + +tidy-tree: The bidirectional tidy tree layout + +The bidirectional tidy tree layout algorithm creates two separate trees that grow horizontally in opposite directions from a central root node: +Left tree: grows horizontally to the left (children alternate: 1st, 3rd, 5th...) +Right tree: grows horizontally to the right (children alternate: 2nd, 4th, 6th...) + +This creates a balanced, symmetric layout that is ideal for mindmaps, organizational charts, and other tree-based diagrams. + +Layout Structure: +[Child 3] ← [Child 1] ← [Root] → [Child 2] → [Child 4] +↓ ↓ ↓ ↓ +[GrandChild] [GrandChild] [GrandChild] [GrandChild] diff --git a/packages/mermaid-layout-tidy-tree/package.json b/packages/mermaid-layout-tidy-tree/package.json new file mode 100644 index 000000000..d8c3ed965 --- /dev/null +++ b/packages/mermaid-layout-tidy-tree/package.json @@ -0,0 +1,46 @@ +{ + "name": "@mermaid-js/layout-tidy-tree", + "version": "0.1.0", + "description": "Tidy-tree layout engine for mermaid", + "module": "dist/mermaid-layout-tidy-tree.core.mjs", + "types": "dist/layouts.d.ts", + "type": "module", + "exports": { + ".": { + "import": "./dist/mermaid-layout-tidy-tree.core.mjs", + "types": "./dist/layouts.d.ts" + }, + "./": "./" + }, + "keywords": [ + "diagram", + "markdown", + "tidy-tree", + "mermaid", + "layout" + ], + "scripts": {}, + "repository": { + "type": "git", + "url": "https://github.com/mermaid-js/mermaid" + }, + "contributors": [ + "Knut Sveidqvist", + "Sidharth Vinod" + ], + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "non-layered-tidy-tree-layout": "^2.0.2" + }, + "devDependencies": { + "@types/d3": "^7.4.3", + "mermaid": "workspace:^" + }, + "peerDependencies": { + "mermaid": "^11.0.2" + }, + "files": [ + "dist" + ] +} diff --git a/packages/mermaid-layout-tidy-tree/src/index.ts b/packages/mermaid-layout-tidy-tree/src/index.ts new file mode 100644 index 000000000..2be1b59e6 --- /dev/null +++ b/packages/mermaid-layout-tidy-tree/src/index.ts @@ -0,0 +1,50 @@ +/** + * Bidirectional Tidy-Tree Layout Algorithm for Generic Diagrams + * + * This module provides a layout algorithm implementation using the + * non-layered-tidy-tree-layout algorithm for positioning nodes and edges + * in tree structures with a bidirectional approach. + * + * The algorithm creates two separate trees that grow horizontally in opposite + * directions from a central root node: + * - Left tree: grows horizontally to the left (children alternate: 1st, 3rd, 5th...) + * - Right tree: grows horizontally to the right (children alternate: 2nd, 4th, 6th...) + * + * This creates a balanced, symmetric layout that is ideal for mindmaps, + * organizational charts, and other tree-based diagrams. + * + * The algorithm follows the unified rendering pattern and can be used + * by any diagram type that provides compatible LayoutData. + */ + +/** + * Render function for the bidirectional tidy-tree layout algorithm + * + * This function follows the unified rendering pattern used by all layout algorithms. + * It takes LayoutData, inserts nodes into DOM, runs the bidirectional tidy-tree layout algorithm, + * and renders the positioned elements to the SVG. + * + * Features: + * - Alternates root children between left and right trees + * - Left tree grows horizontally to the left (rotated 90° counterclockwise) + * - Right tree grows horizontally to the right (rotated 90° clockwise) + * - Uses tidy-tree algorithm for optimal spacing within each tree + * - Creates symmetric, balanced layouts + * - Maintains proper edge connections between all nodes + * + * Layout Structure: + * ``` + * [Child 3] ← [Child 1] ← [Root] → [Child 2] → [Child 4] + * ↓ ↓ ↓ ↓ + * [GrandChild] [GrandChild] [GrandChild] [GrandChild] + * ``` + * + * @param layoutData - Layout data containing nodes, edges, and configuration + * @param svg - SVG element to render to + * @param helpers - Internal helper functions for rendering + * @param options - Rendering options + */ +export { default } from './layouts.js'; +export * from './types.js'; +export * from './layout.js'; +export { render } from './render.js'; diff --git a/packages/mermaid-layout-tidy-tree/src/layout.test.ts b/packages/mermaid-layout-tidy-tree/src/layout.test.ts new file mode 100644 index 000000000..2b3b79b37 --- /dev/null +++ b/packages/mermaid-layout-tidy-tree/src/layout.test.ts @@ -0,0 +1,409 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { executeTidyTreeLayout, validateLayoutData } from './layout.js'; +import type { LayoutResult } from './types.js'; +import type { LayoutData, MermaidConfig } from 'mermaid'; + +// Mock non-layered-tidy-tree-layout +vi.mock('non-layered-tidy-tree-layout', () => ({ + BoundingBox: vi.fn().mockImplementation(() => ({})), + Layout: vi.fn().mockImplementation(() => ({ + layout: vi.fn().mockImplementation((treeData) => { + const result = { ...treeData }; + + if (result.id?.toString().startsWith('virtual-root')) { + result.x = 0; + result.y = 0; + } else { + result.x = 100; + result.y = 50; + } + + if (result.children) { + result.children.forEach((child: any, index: number) => { + child.x = 50 + index * 100; + child.y = 100; + + if (child.children) { + child.children.forEach((grandchild: any, gIndex: number) => { + grandchild.x = 25 + gIndex * 50; + grandchild.y = 200; + }); + } + }); + } + + return { + result, + boundingBox: { + left: 0, + right: 200, + top: 0, + bottom: 250, + }, + }; + }), + })), +})); + +describe('Tidy-Tree Layout Algorithm', () => { + let mockConfig: MermaidConfig; + let mockLayoutData: LayoutData; + + beforeEach(() => { + mockConfig = { + theme: 'default', + } as MermaidConfig; + + mockLayoutData = { + nodes: [ + { + id: 'root', + label: 'Root', + isGroup: false, + shape: 'rect', + width: 100, + height: 50, + padding: 10, + x: 0, + y: 0, + cssClasses: '', + cssStyles: [], + look: 'default', + }, + { + id: 'child1', + label: 'Child 1', + isGroup: false, + shape: 'rect', + width: 80, + height: 40, + padding: 10, + x: 0, + y: 0, + cssClasses: '', + cssStyles: [], + look: 'default', + }, + { + id: 'child2', + label: 'Child 2', + isGroup: false, + shape: 'rect', + width: 80, + height: 40, + padding: 10, + x: 0, + y: 0, + cssClasses: '', + cssStyles: [], + look: 'default', + }, + { + id: 'child3', + label: 'Child 3', + isGroup: false, + shape: 'rect', + width: 80, + height: 40, + padding: 10, + x: 0, + y: 0, + cssClasses: '', + cssStyles: [], + look: 'default', + }, + { + id: 'child4', + label: 'Child 4', + isGroup: false, + shape: 'rect', + width: 80, + height: 40, + padding: 10, + x: 0, + y: 0, + cssClasses: '', + cssStyles: [], + look: 'default', + }, + ], + edges: [ + { + id: 'root_child1', + start: 'root', + end: 'child1', + type: 'edge', + classes: '', + style: [], + animate: false, + arrowTypeEnd: 'arrow_point', + arrowTypeStart: 'none', + }, + { + id: 'root_child2', + start: 'root', + end: 'child2', + type: 'edge', + classes: '', + style: [], + animate: false, + arrowTypeEnd: 'arrow_point', + arrowTypeStart: 'none', + }, + { + id: 'root_child3', + start: 'root', + end: 'child3', + type: 'edge', + classes: '', + style: [], + animate: false, + arrowTypeEnd: 'arrow_point', + arrowTypeStart: 'none', + }, + { + id: 'root_child4', + start: 'root', + end: 'child4', + type: 'edge', + classes: '', + style: [], + animate: false, + arrowTypeEnd: 'arrow_point', + arrowTypeStart: 'none', + }, + ], + config: mockConfig, + direction: 'TB', + type: 'test', + diagramId: 'test-diagram', + markers: [], + }; + }); + + describe('validateLayoutData', () => { + it('should validate correct layout data', () => { + expect(() => validateLayoutData(mockLayoutData)).not.toThrow(); + }); + + it('should throw error for missing data', () => { + expect(() => validateLayoutData(null as any)).toThrow('Layout data is required'); + }); + + it('should throw error for missing config', () => { + const invalidData = { ...mockLayoutData, config: null as any }; + expect(() => validateLayoutData(invalidData)).toThrow('Configuration is required'); + }); + + it('should throw error for invalid nodes array', () => { + const invalidData = { ...mockLayoutData, nodes: null as any }; + expect(() => validateLayoutData(invalidData)).toThrow('Nodes array is required'); + }); + + it('should throw error for invalid edges array', () => { + const invalidData = { ...mockLayoutData, edges: null as any }; + expect(() => validateLayoutData(invalidData)).toThrow('Edges array is required'); + }); + }); + + describe('executeTidyTreeLayout function', () => { + it('should execute layout algorithm successfully', async () => { + const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData); + + expect(result).toBeDefined(); + expect(result.nodes).toBeDefined(); + expect(result.edges).toBeDefined(); + expect(Array.isArray(result.nodes)).toBe(true); + expect(Array.isArray(result.edges)).toBe(true); + }); + + it('should return positioned nodes with coordinates', async () => { + const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData); + + expect(result.nodes.length).toBeGreaterThan(0); + result.nodes.forEach((node) => { + expect(node.x).toBeDefined(); + expect(node.y).toBeDefined(); + expect(typeof node.x).toBe('number'); + expect(typeof node.y).toBe('number'); + }); + }); + + it('should return positioned edges with coordinates', async () => { + const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData); + + expect(result.edges.length).toBeGreaterThan(0); + result.edges.forEach((edge) => { + expect(edge.startX).toBeDefined(); + expect(edge.startY).toBeDefined(); + expect(edge.midX).toBeDefined(); + expect(edge.midY).toBeDefined(); + expect(edge.endX).toBeDefined(); + expect(edge.endY).toBeDefined(); + }); + }); + + it('should handle empty layout data gracefully', async () => { + const emptyData: LayoutData = { + ...mockLayoutData, + nodes: [], + edges: [], + }; + + await expect(executeTidyTreeLayout(emptyData)).rejects.toThrow( + 'No nodes found in layout data' + ); + }); + + it('should throw error for missing nodes', async () => { + const invalidData = { ...mockLayoutData, nodes: [] }; + + await expect(executeTidyTreeLayout(invalidData)).rejects.toThrow( + 'No nodes found in layout data' + ); + }); + + it('should handle empty edges (single node tree)', async () => { + const singleNodeData = { + ...mockLayoutData, + edges: [], + nodes: [mockLayoutData.nodes[0]], + }; + + const result = await executeTidyTreeLayout(singleNodeData); + expect(result).toBeDefined(); + expect(result.nodes).toHaveLength(1); + expect(result.edges).toHaveLength(0); + }); + + it('should create bidirectional dual-tree layout with alternating left/right children', async () => { + const result = await executeTidyTreeLayout(mockLayoutData); + + expect(result).toBeDefined(); + expect(result.nodes).toHaveLength(5); + + const rootNode = result.nodes.find((node) => node.id === 'root'); + expect(rootNode).toBeDefined(); + expect(rootNode!.x).toBe(0); + expect(rootNode!.y).toBe(20); + + const child1 = result.nodes.find((node) => node.id === 'child1'); + const child2 = result.nodes.find((node) => node.id === 'child2'); + const child3 = result.nodes.find((node) => node.id === 'child3'); + const child4 = result.nodes.find((node) => node.id === 'child4'); + + expect(child1).toBeDefined(); + expect(child2).toBeDefined(); + expect(child3).toBeDefined(); + expect(child4).toBeDefined(); + + expect(child1!.x).toBeLessThan(rootNode!.x); + expect(child2!.x).toBeGreaterThan(rootNode!.x); + expect(child3!.x).toBeLessThan(rootNode!.x); + expect(child4!.x).toBeGreaterThan(rootNode!.x); + + expect(child1!.x).toBeLessThan(-100); + expect(child3!.x).toBeLessThan(-100); + + expect(child2!.x).toBeGreaterThan(100); + expect(child4!.x).toBeGreaterThan(100); + }); + + it('should correctly transpose coordinates to prevent high nodes from covering nodes above them', async () => { + const testData = { + ...mockLayoutData, + nodes: [ + { + id: 'root', + label: 'Root', + isGroup: false, + shape: 'rect' as const, + width: 100, + height: 50, + padding: 10, + x: 0, + y: 0, + cssClasses: '', + cssStyles: [], + look: 'default', + }, + { + id: 'tall-child', + label: 'Tall Child', + isGroup: false, + shape: 'rect' as const, + width: 80, + height: 120, + padding: 10, + x: 0, + y: 0, + cssClasses: '', + cssStyles: [], + look: 'default', + }, + { + id: 'short-child', + label: 'Short Child', + isGroup: false, + shape: 'rect' as const, + width: 80, + height: 30, + padding: 10, + x: 0, + y: 0, + cssClasses: '', + cssStyles: [], + look: 'default', + }, + ], + edges: [ + { + id: 'root_tall', + start: 'root', + end: 'tall-child', + type: 'edge', + classes: '', + style: [], + animate: false, + arrowTypeEnd: 'arrow_point', + arrowTypeStart: 'none', + }, + { + id: 'root_short', + start: 'root', + end: 'short-child', + type: 'edge', + classes: '', + style: [], + animate: false, + arrowTypeEnd: 'arrow_point', + arrowTypeStart: 'none', + }, + ], + }; + + const result = await executeTidyTreeLayout(testData); + + expect(result).toBeDefined(); + expect(result.nodes).toHaveLength(3); + + const rootNode = result.nodes.find((node) => node.id === 'root'); + const tallChild = result.nodes.find((node) => node.id === 'tall-child'); + const shortChild = result.nodes.find((node) => node.id === 'short-child'); + + expect(rootNode).toBeDefined(); + expect(tallChild).toBeDefined(); + expect(shortChild).toBeDefined(); + + expect(tallChild!.x).not.toBe(shortChild!.x); + + expect(tallChild!.width).toBe(80); + expect(tallChild!.height).toBe(120); + expect(shortChild!.width).toBe(80); + expect(shortChild!.height).toBe(30); + + const yDifference = Math.abs(tallChild!.y - shortChild!.y); + expect(yDifference).toBeGreaterThanOrEqual(0); + }); + }); +}); diff --git a/packages/mermaid-layout-tidy-tree/src/layout.ts b/packages/mermaid-layout-tidy-tree/src/layout.ts new file mode 100644 index 000000000..6cc06a9ab --- /dev/null +++ b/packages/mermaid-layout-tidy-tree/src/layout.ts @@ -0,0 +1,629 @@ +import type { LayoutData } from 'mermaid'; +import type { Bounds, Point } from 'mermaid/src/types.js'; +import { BoundingBox, Layout } from 'non-layered-tidy-tree-layout'; +import type { + Edge, + LayoutResult, + Node, + PositionedEdge, + PositionedNode, + TidyTreeNode, +} from './types.js'; + +/** + * Execute the tidy-tree layout algorithm on generic layout data + * + * This function takes layout data and uses the non-layered-tidy-tree-layout + * algorithm to calculate optimal node positions for tree structures. + * + * @param data - The layout data containing nodes, edges, and configuration + * @param config - Mermaid configuration object + * @returns Promise resolving to layout result with positioned nodes and edges + */ +export function executeTidyTreeLayout(data: LayoutData): Promise { + let intersectionShift = 50; + + return new Promise((resolve, reject) => { + try { + if (!data.nodes || !Array.isArray(data.nodes) || data.nodes.length === 0) { + throw new Error('No nodes found in layout data'); + } + + if (!data.edges || !Array.isArray(data.edges)) { + data.edges = []; + } + + const { leftTree, rightTree, rootNode } = convertToDualTreeFormat(data); + + const gap = 20; + const bottomPadding = 40; + intersectionShift = 30; + + const bb = new BoundingBox(gap, bottomPadding); + const layout = new Layout(bb); + + let leftResult = null; + let rightResult = null; + + if (leftTree) { + const leftLayoutResult = layout.layout(leftTree); + leftResult = leftLayoutResult.result; + } + + if (rightTree) { + const rightLayoutResult = layout.layout(rightTree); + rightResult = rightLayoutResult.result; + } + + const positionedNodes = combineAndPositionTrees(rootNode, leftResult, rightResult); + const positionedEdges = calculateEdgePositions( + data.edges, + positionedNodes, + intersectionShift + ); + resolve({ + nodes: positionedNodes, + edges: positionedEdges, + }); + } catch (error) { + reject(error); + } + }); +} + +/** + * Convert LayoutData to dual-tree format (left and right trees) + * + * This function builds two separate tree structures from the nodes and edges, + * alternating children between left and right trees. + */ +function convertToDualTreeFormat(data: LayoutData): { + leftTree: TidyTreeNode | null; + rightTree: TidyTreeNode | null; + rootNode: TidyTreeNode; +} { + const { nodes, edges } = data; + + const nodeMap = new Map(); + nodes.forEach((node) => nodeMap.set(node.id, node)); + + const children = new Map(); + const parents = new Map(); + + edges.forEach((edge) => { + const parentId = edge.start; + const childId = edge.end; + + if (parentId && childId) { + if (!children.has(parentId)) { + children.set(parentId, []); + } + children.get(parentId)!.push(childId); + parents.set(childId, parentId); + } + }); + + const rootNodeData = nodes.find((node) => !parents.has(node.id)); + if (!rootNodeData && nodes.length === 0) { + throw new Error('No nodes available to create tree'); + } + + const actualRoot = rootNodeData ?? nodes[0]; + + const rootNode: TidyTreeNode = { + id: actualRoot.id, + width: actualRoot.width ?? 100, + height: actualRoot.height ?? 50, + _originalNode: actualRoot, + }; + + const rootChildren = children.get(actualRoot.id) ?? []; + const leftChildren: string[] = []; + const rightChildren: string[] = []; + + rootChildren.forEach((childId, index) => { + if (index % 2 === 0) { + leftChildren.push(childId); + } else { + rightChildren.push(childId); + } + }); + + const leftTree = leftChildren.length > 0 ? buildSubTree(leftChildren, children, nodeMap) : null; + + const rightTree = + rightChildren.length > 0 ? buildSubTree(rightChildren, children, nodeMap) : null; + + return { leftTree, rightTree, rootNode }; +} + +/** + * Build a subtree from a list of root children + * For horizontal trees, we need to transpose width/height since the tree will be rotated 90° + */ +function buildSubTree( + rootChildren: string[], + children: Map, + nodeMap: Map +): TidyTreeNode { + const virtualRoot: TidyTreeNode = { + id: `virtual-root-${Math.random()}`, + width: 1, + height: 1, + children: rootChildren + .map((childId) => nodeMap.get(childId)) + .filter((child): child is Node => child !== undefined) + .map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap)), + }; + + return virtualRoot; +} + +/** + * Recursively convert a node and its children to tidy-tree format + * This version transposes width/height for horizontal tree layout + */ +function convertNodeToTidyTreeTransposed( + node: Node, + children: Map, + nodeMap: Map +): TidyTreeNode { + const childIds = children.get(node.id) ?? []; + const childNodes = childIds + .map((childId) => nodeMap.get(childId)) + .filter((child): child is Node => child !== undefined) + .map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap)); + + return { + id: node.id, + width: node.height ?? 50, + height: node.width ?? 100, + children: childNodes.length > 0 ? childNodes : undefined, + _originalNode: node, + }; +} +/** + * Combine and position the left and right trees around the root node + * Creates a bidirectional layout where left tree grows left and right tree grows right + */ +function combineAndPositionTrees( + rootNode: TidyTreeNode, + leftResult: TidyTreeNode | null, + rightResult: TidyTreeNode | null +): PositionedNode[] { + const positionedNodes: PositionedNode[] = []; + + const rootX = 0; + const rootY = 0; + + const treeSpacing = rootNode.width / 2 + 30; + const leftTreeNodes: PositionedNode[] = []; + const rightTreeNodes: PositionedNode[] = []; + + if (leftResult?.children) { + positionLeftTreeBidirectional(leftResult.children, leftTreeNodes, rootX - treeSpacing, rootY); + } + + if (rightResult?.children) { + positionRightTreeBidirectional( + rightResult.children, + rightTreeNodes, + rootX + treeSpacing, + rootY + ); + } + + let leftTreeCenterY = 0; + let rightTreeCenterY = 0; + + if (leftTreeNodes.length > 0) { + const leftTreeXPositions = [...new Set(leftTreeNodes.map((node) => node.x))].sort( + (a, b) => b - a + ); + const firstLevelLeftX = leftTreeXPositions[0]; + const firstLevelLeftNodes = leftTreeNodes.filter((node) => node.x === firstLevelLeftX); + + if (firstLevelLeftNodes.length > 0) { + const leftMinY = Math.min( + ...firstLevelLeftNodes.map((node) => node.y - (node.height ?? 50) / 2) + ); + const leftMaxY = Math.max( + ...firstLevelLeftNodes.map((node) => node.y + (node.height ?? 50) / 2) + ); + leftTreeCenterY = (leftMinY + leftMaxY) / 2; + } + } + + if (rightTreeNodes.length > 0) { + const rightTreeXPositions = [...new Set(rightTreeNodes.map((node) => node.x))].sort( + (a, b) => a - b + ); + const firstLevelRightX = rightTreeXPositions[0]; + const firstLevelRightNodes = rightTreeNodes.filter((node) => node.x === firstLevelRightX); + + if (firstLevelRightNodes.length > 0) { + const rightMinY = Math.min( + ...firstLevelRightNodes.map((node) => node.y - (node.height ?? 50) / 2) + ); + const rightMaxY = Math.max( + ...firstLevelRightNodes.map((node) => node.y + (node.height ?? 50) / 2) + ); + rightTreeCenterY = (rightMinY + rightMaxY) / 2; + } + } + + const leftTreeOffset = -leftTreeCenterY; + const rightTreeOffset = -rightTreeCenterY; + + positionedNodes.push({ + id: String(rootNode.id), + x: rootX, + y: rootY + 20, + section: 'root', + width: rootNode._originalNode?.width ?? rootNode.width, + height: rootNode._originalNode?.height ?? rootNode.height, + originalNode: rootNode._originalNode, + }); + + const leftTreeNodesWithOffset = leftTreeNodes.map((node) => ({ + id: node.id, + x: node.x - (node.width ?? 0) / 2, + y: node.y + leftTreeOffset + (node.height ?? 0) / 2, + section: 'left' as const, + width: node.width, + height: node.height, + originalNode: node.originalNode, + })); + + const rightTreeNodesWithOffset = rightTreeNodes.map((node) => ({ + id: node.id, + x: node.x + (node.width ?? 0) / 2, + y: node.y + rightTreeOffset + (node.height ?? 0) / 2, + section: 'right' as const, + width: node.width, + height: node.height, + originalNode: node.originalNode, + })); + + positionedNodes.push(...leftTreeNodesWithOffset); + positionedNodes.push(...rightTreeNodesWithOffset); + + return positionedNodes; +} + +/** + * Position nodes from the left tree in a bidirectional layout (grows to the left) + * Rotates the tree 90 degrees counterclockwise so it grows horizontally to the left + */ +function positionLeftTreeBidirectional( + nodes: TidyTreeNode[], + positionedNodes: PositionedNode[], + offsetX: number, + offsetY: number +): void { + nodes.forEach((node) => { + const distanceFromRoot = node.y ?? 0; + const verticalPosition = node.x ?? 0; + + const originalWidth = node._originalNode?.width ?? 100; + const originalHeight = node._originalNode?.height ?? 50; + + const adjustedY = offsetY + verticalPosition; + + positionedNodes.push({ + id: String(node.id), + x: offsetX - distanceFromRoot, + y: adjustedY, + width: originalWidth, + height: originalHeight, + originalNode: node._originalNode, + }); + + if (node.children) { + positionLeftTreeBidirectional(node.children, positionedNodes, offsetX, offsetY); + } + }); +} + +/** + * Position nodes from the right tree in a bidirectional layout (grows to the right) + * Rotates the tree 90 degrees clockwise so it grows horizontally to the right + */ +function positionRightTreeBidirectional( + nodes: TidyTreeNode[], + positionedNodes: PositionedNode[], + offsetX: number, + offsetY: number +): void { + nodes.forEach((node) => { + const distanceFromRoot = node.y ?? 0; + const verticalPosition = node.x ?? 0; + + const originalWidth = node._originalNode?.width ?? 100; + const originalHeight = node._originalNode?.height ?? 50; + + const adjustedY = offsetY + verticalPosition; + + positionedNodes.push({ + id: String(node.id), + x: offsetX + distanceFromRoot, + y: adjustedY, + width: originalWidth, + height: originalHeight, + originalNode: node._originalNode, + }); + + if (node.children) { + positionRightTreeBidirectional(node.children, positionedNodes, offsetX, offsetY); + } + }); +} + +/** + * Calculate the intersection point of a line with a circle + * @param circle - Circle coordinates and radius + * @param lineStart - Starting point of the line + * @param lineEnd - Ending point of the line + * @returns The intersection point + */ +function computeCircleEdgeIntersection(circle: Bounds, lineStart: Point, lineEnd: Point): Point { + const radius = Math.min(circle.width, circle.height) / 2; + + const dx = lineEnd.x - lineStart.x; + const dy = lineEnd.y - lineStart.y; + const length = Math.sqrt(dx * dx + dy * dy); + + if (length === 0) { + return lineStart; + } + + const nx = dx / length; + const ny = dy / length; + + return { + x: circle.x - nx * radius, + y: circle.y - ny * radius, + }; +} + +function intersection(node: PositionedNode, outsidePoint: Point, insidePoint: Point): Point { + const x = node.x; + const y = node.y; + + if (!node.width || !node.height) { + return { x: outsidePoint.x, y: outsidePoint.y }; + } + const dx = Math.abs(x - insidePoint.x); + const w = node?.width / 2; + let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx; + const h = node.height / 2; + + const Q = Math.abs(outsidePoint.y - insidePoint.y); + const R = Math.abs(outsidePoint.x - insidePoint.x); + + if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) { + // Intersection is top or bottom of rect. + const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y; + r = (R * q) / Q; + const res = { + x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r, + y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q, + }; + + if (r === 0) { + res.x = outsidePoint.x; + res.y = outsidePoint.y; + } + if (R === 0) { + res.x = outsidePoint.x; + } + if (Q === 0) { + res.y = outsidePoint.y; + } + + return res; + } else { + if (insidePoint.x < outsidePoint.x) { + r = outsidePoint.x - w - x; + } else { + r = x - w - outsidePoint.x; + } + const q = (Q * r) / R; + let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r; + let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q; + + if (r === 0) { + _x = outsidePoint.x; + _y = outsidePoint.y; + } + if (R === 0) { + _x = outsidePoint.x; + } + if (Q === 0) { + _y = outsidePoint.y; + } + + return { x: _x, y: _y }; + } +} + +/** + * Calculate edge positions based on positioned nodes + * Now includes tree membership and node dimensions for precise edge calculations + * Edges now stop at shape boundaries instead of extending to centers + */ +function calculateEdgePositions( + edges: Edge[], + positionedNodes: PositionedNode[], + intersectionShift: number +): PositionedEdge[] { + const nodeInfo = new Map(); + positionedNodes.forEach((node) => { + nodeInfo.set(node.id, node); + }); + + return edges.map((edge) => { + const sourceNode = nodeInfo.get(edge.start ?? ''); + const targetNode = nodeInfo.get(edge.end ?? ''); + + if (!sourceNode || !targetNode) { + return { + id: edge.id, + source: edge.start ?? '', + target: edge.end ?? '', + startX: 0, + startY: 0, + midX: 0, + midY: 0, + endX: 0, + endY: 0, + points: [{ x: 0, y: 0 }], + sourceSection: undefined, + targetSection: undefined, + sourceWidth: undefined, + sourceHeight: undefined, + targetWidth: undefined, + targetHeight: undefined, + }; + } + + const sourceCenter = { x: sourceNode.x, y: sourceNode.y }; + const targetCenter = { x: targetNode.x, y: targetNode.y }; + + const isSourceRound = ['circle', 'cloud', 'bang'].includes( + sourceNode.originalNode?.shape ?? '' + ); + const isTargetRound = ['circle', 'cloud', 'bang'].includes( + targetNode.originalNode?.shape ?? '' + ); + + let startPos = isSourceRound + ? computeCircleEdgeIntersection( + { + x: sourceNode.x, + y: sourceNode.y, + width: sourceNode.width ?? 100, + height: sourceNode.height ?? 100, + }, + targetCenter, + sourceCenter + ) + : intersection(sourceNode, sourceCenter, targetCenter); + + let endPos = isTargetRound + ? computeCircleEdgeIntersection( + { + x: targetNode.x, + y: targetNode.y, + width: targetNode.width ?? 100, + height: targetNode.height ?? 100, + }, + sourceCenter, + targetCenter + ) + : intersection(targetNode, targetCenter, sourceCenter); + + const midX = (startPos.x + endPos.x) / 2; + const midY = (startPos.y + endPos.y) / 2; + + const points = [startPos]; + if (sourceNode.section === 'left') { + points.push({ + x: sourceNode.x - (sourceNode.width ?? 0) / 2 - intersectionShift, + y: sourceNode.y, + }); + } else if (sourceNode.section === 'right') { + points.push({ + x: sourceNode.x + (sourceNode.width ?? 0) / 2 + intersectionShift, + y: sourceNode.y, + }); + } + if (targetNode.section === 'left') { + points.push({ + x: targetNode.x + (targetNode.width ?? 0) / 2 + intersectionShift, + y: targetNode.y, + }); + } else if (targetNode.section === 'right') { + points.push({ + x: targetNode.x - (targetNode.width ?? 0) / 2 - intersectionShift, + y: targetNode.y, + }); + } + + points.push(endPos); + + const secondPoint = points.length > 1 ? points[1] : targetCenter; + startPos = isSourceRound + ? computeCircleEdgeIntersection( + { + x: sourceNode.x, + y: sourceNode.y, + width: sourceNode.width ?? 100, + height: sourceNode.height ?? 100, + }, + secondPoint, + sourceCenter + ) + : intersection(sourceNode, secondPoint, sourceCenter); + points[0] = startPos; + + const secondLastPoint = points.length > 1 ? points[points.length - 2] : sourceCenter; + endPos = isTargetRound + ? computeCircleEdgeIntersection( + { + x: targetNode.x, + y: targetNode.y, + width: targetNode.width ?? 100, + height: targetNode.height ?? 100, + }, + secondLastPoint, + targetCenter + ) + : intersection(targetNode, secondLastPoint, targetCenter); + points[points.length - 1] = endPos; + + return { + id: edge.id, + source: edge.start ?? '', + target: edge.end ?? '', + startX: startPos.x, + startY: startPos.y, + midX, + midY, + endX: endPos.x, + endY: endPos.y, + points, + sourceSection: sourceNode?.section, + targetSection: targetNode?.section, + sourceWidth: sourceNode?.width, + sourceHeight: sourceNode?.height, + targetWidth: targetNode?.width, + targetHeight: targetNode?.height, + }; + }); +} + +/** + * Validate layout data structure + * @param data - The data to validate + * @returns True if data is valid, throws error otherwise + */ +export function validateLayoutData(data: LayoutData): boolean { + if (!data) { + throw new Error('Layout data is required'); + } + + if (!data.config) { + throw new Error('Configuration is required in layout data'); + } + + if (!Array.isArray(data.nodes)) { + throw new Error('Nodes array is required in layout data'); + } + + if (!Array.isArray(data.edges)) { + throw new Error('Edges array is required in layout data'); + } + + return true; +} diff --git a/packages/mermaid-layout-tidy-tree/src/layouts.ts b/packages/mermaid-layout-tidy-tree/src/layouts.ts new file mode 100644 index 000000000..d5eac8992 --- /dev/null +++ b/packages/mermaid-layout-tidy-tree/src/layouts.ts @@ -0,0 +1,13 @@ +import type { LayoutLoaderDefinition } from 'mermaid'; + +const loader = async () => await import(`./render.js`); + +const tidyTreeLayout: LayoutLoaderDefinition[] = [ + { + name: 'tidy-tree', + loader, + algorithm: 'tidy-tree', + }, +]; + +export default tidyTreeLayout; diff --git a/packages/mermaid-layout-tidy-tree/src/non-layered-tidy-tree-layout.d.ts b/packages/mermaid-layout-tidy-tree/src/non-layered-tidy-tree-layout.d.ts new file mode 100644 index 000000000..248b5c05f --- /dev/null +++ b/packages/mermaid-layout-tidy-tree/src/non-layered-tidy-tree-layout.d.ts @@ -0,0 +1,18 @@ +declare module 'non-layered-tidy-tree-layout' { + export class BoundingBox { + constructor(gap: number, bottomPadding: number); + } + + export class Layout { + constructor(boundingBox: BoundingBox); + layout(data: any): { + result: any; + boundingBox: { + left: number; + right: number; + top: number; + bottom: number; + }; + }; + } +} diff --git a/packages/mermaid-layout-tidy-tree/src/render.ts b/packages/mermaid-layout-tidy-tree/src/render.ts new file mode 100644 index 000000000..4ce5e1deb --- /dev/null +++ b/packages/mermaid-layout-tidy-tree/src/render.ts @@ -0,0 +1,180 @@ +import type { InternalHelpers, LayoutData, RenderOptions, SVG } from 'mermaid'; +import { executeTidyTreeLayout } from './layout.js'; + +interface NodeWithPosition { + id: string; + x?: number; + y?: number; + width?: number; + height?: number; + domId?: any; + [key: string]: any; +} + +/** + * Render function for bidirectional tidy-tree layout algorithm + * + * This follows the same pattern as ELK and dagre renderers: + * 1. Insert nodes into DOM to get their actual dimensions + * 2. Run the bidirectional tidy-tree layout algorithm to calculate positions + * 3. Position the nodes and edges based on layout results + * + * The bidirectional layout creates two trees that grow horizontally in opposite + * directions from a central root node: + * - Left tree: grows horizontally to the left (children: 1st, 3rd, 5th...) + * - Right tree: grows horizontally to the right (children: 2nd, 4th, 6th...) + */ +export const render = async ( + data4Layout: LayoutData, + svg: SVG, + { + insertCluster, + insertEdge, + insertEdgeLabel, + insertMarkers, + insertNode, + log, + positionEdgeLabel, + }: InternalHelpers, + { algorithm: _algorithm }: RenderOptions +) => { + const nodeDb: Record = {}; + const clusterDb: Record = {}; + + const element = svg.select('g'); + insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId); + + const subGraphsEl = element.insert('g').attr('class', 'subgraphs'); + const edgePaths = element.insert('g').attr('class', 'edgePaths'); + const edgeLabels = element.insert('g').attr('class', 'edgeLabels'); + const nodes = element.insert('g').attr('class', 'nodes'); + // Step 1: Insert nodes into DOM to get their actual dimensions + log.debug('Inserting nodes into DOM for dimension calculation'); + + await Promise.all( + data4Layout.nodes.map(async (node) => { + if (node.isGroup) { + const clusterNode: NodeWithPosition = { + ...node, + id: node.id, + width: node.width, + height: node.height, + }; + clusterDb[node.id] = clusterNode; + nodeDb[node.id] = clusterNode; + + await insertCluster(subGraphsEl, node); + } else { + const nodeWithPosition: NodeWithPosition = { + ...node, + id: node.id, + width: node.width, + height: node.height, + }; + nodeDb[node.id] = nodeWithPosition; + + const nodeEl = await insertNode(nodes, node, { + config: data4Layout.config, + dir: data4Layout.direction || 'TB', + }); + + const boundingBox = nodeEl.node()!.getBBox(); + nodeWithPosition.width = boundingBox.width; + nodeWithPosition.height = boundingBox.height; + nodeWithPosition.domId = nodeEl; + + log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`); + } + }) + ); + // Step 2: Run the bidirectional tidy-tree layout algorithm + log.debug('Running bidirectional tidy-tree layout algorithm'); + + const updatedLayoutData = { + ...data4Layout, + nodes: data4Layout.nodes.map((node) => { + const nodeWithDimensions = nodeDb[node.id]; + return { + ...node, + width: nodeWithDimensions.width ?? node.width ?? 100, + height: nodeWithDimensions.height ?? node.height ?? 50, + }; + }), + }; + + const layoutResult = await executeTidyTreeLayout(updatedLayoutData); + // Step 3: Position the nodes based on bidirectional layout results + log.debug('Positioning nodes based on bidirectional layout results'); + + layoutResult.nodes.forEach((positionedNode) => { + const node = nodeDb[positionedNode.id]; + if (node?.domId) { + // Position the node at the calculated coordinates from bidirectional layout + // The layout algorithm has already calculated positions for: + // - Root node at center (0, 0) + // - Left tree nodes with negative x coordinates (growing left) + // - Right tree nodes with positive x coordinates (growing right) + node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`); + // Store the final position + node.x = positionedNode.x; + node.y = positionedNode.y; + // Step 3: Position the nodes based on bidirectional layout results + log.debug(`Positioned node ${node.id} at (${positionedNode.x}, ${positionedNode.y})`); + } + }); + + log.debug('Inserting and positioning edges'); + + await Promise.all( + data4Layout.edges.map(async (edge) => { + await insertEdgeLabel(edgeLabels, edge); + + const startNode = nodeDb[edge.start ?? '']; + const endNode = nodeDb[edge.end ?? '']; + + if (startNode && endNode) { + const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id); + + if (positionedEdge) { + log.debug('APA01 positionedEdge', positionedEdge); + const edgeWithPath = { + ...edge, + points: positionedEdge.points, + }; + const paths = insertEdge( + edgePaths, + edgeWithPath, + clusterDb, + data4Layout.type, + startNode, + endNode, + data4Layout.diagramId + ); + + positionEdgeLabel(edgeWithPath, paths); + } else { + const edgeWithPath = { + ...edge, + points: [ + { x: startNode.x ?? 0, y: startNode.y ?? 0 }, + { x: endNode.x ?? 0, y: endNode.y ?? 0 }, + ], + }; + + const paths = insertEdge( + edgePaths, + edgeWithPath, + clusterDb, + data4Layout.type, + startNode, + endNode, + data4Layout.diagramId + ); + positionEdgeLabel(edgeWithPath, paths); + } + } + }) + ); + + log.debug('Bidirectional tidy-tree rendering completed'); +}; diff --git a/packages/mermaid-layout-tidy-tree/src/types.ts b/packages/mermaid-layout-tidy-tree/src/types.ts new file mode 100644 index 000000000..2015a4909 --- /dev/null +++ b/packages/mermaid-layout-tidy-tree/src/types.ts @@ -0,0 +1,69 @@ +import type { LayoutData } from 'mermaid'; + +export type Node = LayoutData['nodes'][number]; +export type Edge = LayoutData['edges'][number]; + +/** + * Positioned node after layout calculation + */ +export interface PositionedNode { + id: string; + x: number; + y: number; + section?: 'root' | 'left' | 'right'; + width?: number; + height?: number; + originalNode?: Node; + [key: string]: unknown; +} + +/** + * Positioned edge after layout calculation + */ +export interface PositionedEdge { + id: string; + source: string; + target: string; + startX: number; + startY: number; + midX: number; + midY: number; + endX: number; + endY: number; + sourceSection?: 'root' | 'left' | 'right'; + targetSection?: 'root' | 'left' | 'right'; + sourceWidth?: number; + sourceHeight?: number; + targetWidth?: number; + targetHeight?: number; + [key: string]: unknown; +} + +/** + * Result of layout algorithm execution + */ +export interface LayoutResult { + nodes: PositionedNode[]; + edges: PositionedEdge[]; +} + +/** + * Tidy-tree node structure compatible with non-layered-tidy-tree-layout + */ +export interface TidyTreeNode { + id: string | number; + width: number; + height: number; + x?: number; + y?: number; + children?: TidyTreeNode[]; + _originalNode?: Node; +} + +/** + * Tidy-tree layout configuration + */ +export interface TidyTreeLayoutConfig { + gap: number; + bottomPadding: number; +} diff --git a/packages/mermaid-layout-tidy-tree/tsconfig.json b/packages/mermaid-layout-tidy-tree/tsconfig.json new file mode 100644 index 000000000..8f83e2bad --- /dev/null +++ b/packages/mermaid-layout-tidy-tree/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "types": ["vitest/importMeta", "vitest/globals"] + }, + "include": ["./src/**/*.ts", "./src/**/*.d.ts"], + "typeRoots": ["./src/types"] +} diff --git a/packages/mermaid/CHANGELOG.md b/packages/mermaid/CHANGELOG.md index 12bcb2571..fc2f97fdf 100644 --- a/packages/mermaid/CHANGELOG.md +++ b/packages/mermaid/CHANGELOG.md @@ -1,11 +1,5 @@ # mermaid -## 11.10.1 - -### Patch Changes - -- [#6886](https://github.com/mermaid-js/mermaid/pull/6886) [`e0b45c2`](https://github.com/mermaid-js/mermaid/commit/e0b45c2d2b41c2a9038bf87646fa3ccd7560eb20) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Handle arrows correctly when auto number is enabled - ## 11.10.0 ### Minor Changes @@ -235,7 +229,6 @@ - [#5999](https://github.com/mermaid-js/mermaid/pull/5999) [`742ad7c`](https://github.com/mermaid-js/mermaid/commit/742ad7c130964df1fb5544e909d9556081285f68) Thanks [@knsv](https://github.com/knsv)! - Adding Kanban board, a new diagram type - [#5880](https://github.com/mermaid-js/mermaid/pull/5880) [`bdf145f`](https://github.com/mermaid-js/mermaid/commit/bdf145ffe362462176d9c1e68d5f3ff5c9d962b0) Thanks [@yari-dewalt](https://github.com/yari-dewalt)! - Class diagram changes: - - Updates the class diagram to the new unified way of rendering. - Includes a new "classBox" shape to be used in diagrams - Other updates such as: diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index c884b62e9..af3185bf5 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -1,6 +1,6 @@ { "name": "mermaid", - "version": "11.10.1", + "version": "11.10.0", "description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.", "type": "module", "module": "./dist/mermaid.core.mjs", @@ -68,7 +68,7 @@ }, "dependencies": { "@braintree/sanitize-url": "^7.0.4", - "@iconify/utils": "^2.1.33", + "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "workspace:^", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", @@ -82,7 +82,7 @@ "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.21", - "marked": "^16.0.0", + "marked": "^15.0.7", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", @@ -123,8 +123,8 @@ "rimraf": "^6.0.1", "start-server-and-test": "^2.0.10", "type-fest": "^4.35.0", - "typedoc": "^0.27.8", - "typedoc-plugin-markdown": "^4.4.2", + "typedoc": "^0.28.9", + "typedoc-plugin-markdown": "^4.8.0", "typescript": "~5.7.3", "unist-util-flatmap": "^1.0.0", "unist-util-visit": "^5.0.0", diff --git a/packages/mermaid/scripts/docs.spec.ts b/packages/mermaid/scripts/docs.spec.ts index 68677d4c9..70923e226 100644 --- a/packages/mermaid/scripts/docs.spec.ts +++ b/packages/mermaid/scripts/docs.spec.ts @@ -171,7 +171,9 @@ This Markdown should be kept. expect(buildShapeDoc()).toMatchInlineSnapshot(` "| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** | | --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- | + | Bang | Bang | \`bang\` | Bang | \`bang\` | | Card | Notched Rectangle | \`notch-rect\` | Represents a card | \`card\`, \`notched-rectangle\` | + | Cloud | Cloud | \`cloud\` | cloud | \`cloud\` | | Collate | Hourglass | \`hourglass\` | Represents a collate operation | \`collate\`, \`hourglass\` | | Com Link | Lightning Bolt | \`bolt\` | Communication link | \`com-link\`, \`lightning-bolt\` | | Comment | Curly Brace | \`brace\` | Adds a comment | \`brace-l\`, \`comment\` | diff --git a/packages/mermaid/src/config.spec.ts b/packages/mermaid/src/config.spec.ts index 000be1282..7fbae03af 100644 --- a/packages/mermaid/src/config.spec.ts +++ b/packages/mermaid/src/config.spec.ts @@ -78,3 +78,187 @@ describe('when working with site config', () => { expect(config_4.altFontFamily).toBeUndefined(); }); }); + +describe('getUserDefinedConfig', () => { + beforeEach(() => { + configApi.reset(); + }); + + it('should return empty object when no user config is defined', () => { + const userConfig = configApi.getUserDefinedConfig(); + expect(userConfig).toEqual({}); + }); + + it('should return config from initialize only', () => { + const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial' }; + configApi.saveConfigFromInitialize(initConfig); + + const userConfig = configApi.getUserDefinedConfig(); + expect(userConfig).toEqual(initConfig); + }); + + it('should return config from directives only', () => { + const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 }; + const directive2: MermaidConfig = { theme: 'forest' }; + + configApi.addDirective(directive1); + configApi.addDirective(directive2); + + expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(` + { + "fontFamily": "Arial", + "fontSize": 14, + "layout": "elk", + "theme": "forest", + } + `); + }); + + it('should combine initialize config and directives', () => { + const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial', layout: 'dagre' }; + const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 }; + const directive2: MermaidConfig = { theme: 'forest' }; + + configApi.saveConfigFromInitialize(initConfig); + configApi.addDirective(directive1); + configApi.addDirective(directive2); + + const userConfig = configApi.getUserDefinedConfig(); + expect(userConfig).toMatchInlineSnapshot(` + { + "fontFamily": "Arial", + "fontSize": 14, + "layout": "elk", + "theme": "forest", + } + `); + }); + + it('should handle nested config objects properly', () => { + const initConfig: MermaidConfig = { + flowchart: { nodeSpacing: 50, rankSpacing: 100 }, + theme: 'default', + }; + const directive: MermaidConfig = { + flowchart: { nodeSpacing: 75, curve: 'basis' }, + mindmap: { padding: 20 }, + }; + + configApi.saveConfigFromInitialize(initConfig); + configApi.addDirective(directive); + + const userConfig = configApi.getUserDefinedConfig(); + expect(userConfig).toMatchInlineSnapshot(` + { + "flowchart": { + "curve": "basis", + "nodeSpacing": 75, + "rankSpacing": 100, + }, + "mindmap": { + "padding": 20, + }, + "theme": "default", + } + `); + }); + + it('should handle complex nested overrides', () => { + const initConfig: MermaidConfig = { + flowchart: { + nodeSpacing: 50, + rankSpacing: 100, + curve: 'linear', + }, + theme: 'default', + }; + const directive1: MermaidConfig = { + flowchart: { + nodeSpacing: 75, + }, + fontSize: 12, + }; + const directive2: MermaidConfig = { + flowchart: { + curve: 'basis', + nodeSpacing: 100, + }, + mindmap: { + padding: 15, + }, + }; + + configApi.saveConfigFromInitialize(initConfig); + configApi.addDirective(directive1); + configApi.addDirective(directive2); + + const userConfig = configApi.getUserDefinedConfig(); + expect(userConfig).toMatchInlineSnapshot(` + { + "flowchart": { + "curve": "basis", + "nodeSpacing": 100, + "rankSpacing": 100, + }, + "fontSize": 12, + "mindmap": { + "padding": 15, + }, + "theme": "default", + } + `); + }); + + it('should return independent copies (not references)', () => { + const initConfig: MermaidConfig = { theme: 'dark', flowchart: { nodeSpacing: 50 } }; + configApi.saveConfigFromInitialize(initConfig); + + const userConfig1 = configApi.getUserDefinedConfig(); + const userConfig2 = configApi.getUserDefinedConfig(); + + userConfig1.theme = 'neutral'; + userConfig1.flowchart!.nodeSpacing = 999; + + expect(userConfig2).toMatchInlineSnapshot(` + { + "flowchart": { + "nodeSpacing": 50, + }, + "theme": "dark", + } + `); + }); + + it('should handle edge cases with undefined values', () => { + const initConfig: MermaidConfig = { theme: 'dark', layout: undefined }; + const directive: MermaidConfig = { fontSize: 14, fontFamily: undefined }; + + configApi.saveConfigFromInitialize(initConfig); + configApi.addDirective(directive); + + expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(` + { + "fontSize": 14, + "layout": undefined, + "theme": "dark", + } + `); + }); + + it('should retain config from initialize after reset', () => { + const initConfig: MermaidConfig = { theme: 'dark' }; + const directive: MermaidConfig = { layout: 'elk' }; + + configApi.saveConfigFromInitialize(initConfig); + configApi.addDirective(directive); + + expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(` + { + "layout": "elk", + "theme": "dark", + } + `); + + configApi.reset(); + }); +}); diff --git a/packages/mermaid/src/config.ts b/packages/mermaid/src/config.ts index 9468a3e46..4fcb3224d 100644 --- a/packages/mermaid/src/config.ts +++ b/packages/mermaid/src/config.ts @@ -248,3 +248,17 @@ const checkConfig = (config: MermaidConfig) => { issueWarning('LAZY_LOAD_DEPRECATED'); } }; + +export const getUserDefinedConfig = (): MermaidConfig => { + let userConfig: MermaidConfig = {}; + + if (configFromInitialize) { + userConfig = assignWithDepth(userConfig, configFromInitialize); + } + + for (const d of directives) { + userConfig = assignWithDepth(userConfig, d); + } + + return userConfig; +}; diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 70391f2e5..79fadd195 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -1075,6 +1075,10 @@ export interface ArchitectureDiagramConfig extends BaseDiagramConfig { export interface MindmapDiagramConfig extends BaseDiagramConfig { padding?: number; maxNodeWidth?: number; + /** + * Layout algorithm to use for positioning mindmap nodes + */ + layoutAlgorithm?: string; } /** * The object containing configurations specific for kanban diagrams diff --git a/packages/mermaid/src/diagram-api/comments.spec.ts b/packages/mermaid/src/diagram-api/comments.spec.ts index 57a7d4a34..febca83e9 100644 --- a/packages/mermaid/src/diagram-api/comments.spec.ts +++ b/packages/mermaid/src/diagram-api/comments.spec.ts @@ -1,5 +1,3 @@ -// tests to check that comments are removed - import { cleanupComments } from './comments.js'; import { describe, it, expect } from 'vitest'; @@ -10,12 +8,12 @@ describe('comments', () => { %% This is a comment %% This is another comment graph TD - A-->B + A-->B %% This is a comment `; expect(cleanupComments(text)).toMatchInlineSnapshot(` "graph TD - A-->B + A-->B " `); }); @@ -29,9 +27,9 @@ graph TD %%{ init: {'theme': 'space before init'}}%% %%{init: {'theme': 'space after ending'}}%% graph TD - A-->B + A-->B - B-->C + B-->C %% This is a comment `; expect(cleanupComments(text)).toMatchInlineSnapshot(` @@ -39,9 +37,9 @@ graph TD %%{ init: {'theme': 'space before init'}}%% %%{init: {'theme': 'space after ending'}}%% graph TD - A-->B + A-->B - B-->C + B-->C " `); }); @@ -50,14 +48,14 @@ graph TD const text = ` %% This is a comment graph TD - A-->B - %% This is a comment - C-->D + A-->B + %% This is a comment + C-->D `; expect(cleanupComments(text)).toMatchInlineSnapshot(` "graph TD - A-->B - C-->D + A-->B + C-->D " `); }); @@ -70,11 +68,11 @@ graph TD %% This is a comment graph TD - A-->B + A-->B `; expect(cleanupComments(text)).toMatchInlineSnapshot(` "graph TD - A-->B + A-->B " `); }); @@ -82,12 +80,12 @@ graph TD it('should remove comments at end of text with no newline', () => { const text = ` graph TD - A-->B + A-->B %% This is a comment`; expect(cleanupComments(text)).toMatchInlineSnapshot(` "graph TD - A-->B + A-->B " `); }); diff --git a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts index b29567236..608b11816 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts @@ -1,6 +1,5 @@ -import type { Position } from 'cytoscape'; +import type { LayoutOptions, Position } from 'cytoscape'; import cytoscape from 'cytoscape'; -import type { FcoseLayoutOptions } from 'cytoscape-fcose'; import fcose from 'cytoscape-fcose'; import { select } from 'd3'; import type { DrawDefinition, SVG } from '../../diagram-api/types.js'; @@ -41,7 +40,7 @@ registerIconPacks([ icons: architectureIcons, }, ]); -cytoscape.use(fcose); +cytoscape.use(fcose as any); function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) { services.forEach((service) => { @@ -429,7 +428,7 @@ function layoutArchitecture( }, alignmentConstraint, relativePlacementConstraint, - } as FcoseLayoutOptions); + } as LayoutOptions); // Once the diagram has been generated and the service's position cords are set, adjust the XY edges to have a 90deg bend layout.one('layoutstop', () => { diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts index 7c88f2e41..aa5e514e0 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts @@ -1070,6 +1070,14 @@ describe('given a class diagram with members and methods ', function () { parser.parse(str); }); + it('should handle an empty class body with {}', function () { + const str = 'classDiagram\nclass EmptyClass {}'; + parser.parse(str); + const actual = parser.yy.getClass('EmptyClass'); + expect(actual.label).toBe('EmptyClass'); + expect(actual.members.length).toBe(0); + expect(actual.methods.length).toBe(0); + }); }); }); diff --git a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison index 0f971c8b9..9a1f991a7 100644 --- a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison +++ b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison @@ -293,6 +293,7 @@ classStatement : classIdentifier | classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);} | classIdentifier STRUCT_START members STRUCT_STOP {yy.addMembers($1,$3);} + | classIdentifier STRUCT_START STRUCT_STOP {} | classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.setCssClass($1, $3);yy.addMembers($1,$5);} ; @@ -301,8 +302,15 @@ classIdentifier | CLASS className classLabel {$$=$2; yy.addClass($2);yy.setClassLabel($2, $3);} ; + +emptyBody + : + | SPACE emptyBody + | NEWLINE emptyBody + ; + annotationStatement - :ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); } + : ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); } ; members diff --git a/packages/mermaid/src/diagrams/mindmap/mindmapDb.getData.test.ts b/packages/mermaid/src/diagrams/mindmap/mindmapDb.getData.test.ts new file mode 100644 index 000000000..7c10c0104 --- /dev/null +++ b/packages/mermaid/src/diagrams/mindmap/mindmapDb.getData.test.ts @@ -0,0 +1,297 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { MindmapDB } from './mindmapDb.js'; +import type { MindmapLayoutNode, MindmapLayoutEdge } from './mindmapDb.js'; +import type { Edge } from '../../rendering-util/types.js'; + +// Mock the getConfig function +vi.mock('../../diagram-api/diagramAPI.js', () => ({ + getConfig: vi.fn(() => ({ + mindmap: { + layoutAlgorithm: 'cose-bilkent', + padding: 10, + maxNodeWidth: 200, + useMaxWidth: true, + }, + })), +})); + +describe('MindmapDb getData function', () => { + let db: MindmapDB; + + beforeEach(() => { + db = new MindmapDB(); + // Clear the database before each test + db.clear(); + }); + + describe('getData', () => { + it('should return empty data when no mindmap is set', () => { + const result = db.getData(); + + expect(result.nodes).toEqual([]); + expect(result.edges).toEqual([]); + expect(result.config).toBeDefined(); + expect(result.rootNode).toBeUndefined(); + }); + + it('should return structured data for simple mindmap', () => { + // Create a simple mindmap structure + db.addNode(0, 'root', 'Root Node', 0); + db.addNode(1, 'child1', 'Child 1', 0); + db.addNode(1, 'child2', 'Child 2', 0); + + const result = db.getData(); + + expect(result.nodes).toHaveLength(3); + expect(result.edges).toHaveLength(2); + expect(result.config).toBeDefined(); + expect(result.rootNode).toBeDefined(); + + // Check root node + const rootNode = (result.nodes as MindmapLayoutNode[]).find((n) => n.id === '0'); + expect(rootNode).toBeDefined(); + expect(rootNode?.label).toBe('Root Node'); + expect(rootNode?.level).toBe(0); + + // Check child nodes + const child1 = (result.nodes as MindmapLayoutNode[]).find((n) => n.id === '1'); + expect(child1).toBeDefined(); + expect(child1?.label).toBe('Child 1'); + expect(child1?.level).toBe(1); + + // Check edges + expect(result.edges).toContainEqual( + expect.objectContaining({ + start: '0', + end: '1', + depth: 0, + }) + ); + }); + + it('should return structured data for hierarchical mindmap', () => { + // Create a hierarchical mindmap structure + db.addNode(0, 'root', 'Root Node', 0); + db.addNode(1, 'child1', 'Child 1', 0); + db.addNode(2, 'grandchild1', 'Grandchild 1', 0); + db.addNode(2, 'grandchild2', 'Grandchild 2', 0); + db.addNode(1, 'child2', 'Child 2', 0); + + const result = db.getData(); + + expect(result.nodes).toHaveLength(5); + expect(result.edges).toHaveLength(4); + + // Check that all levels are represented + const levels = result.nodes.map((n) => (n as MindmapLayoutNode).level); + expect(levels).toContain(0); // root + expect(levels).toContain(1); // children + expect(levels).toContain(2); // grandchildren + + // Check edge relationships + const edgeRelations = result.edges.map( + (e) => `${(e as MindmapLayoutEdge).start}->${(e as MindmapLayoutEdge).end}` + ); + expect(edgeRelations).toContain('0->1'); // root to child1 + expect(edgeRelations).toContain('1->2'); // child1 to grandchild1 + expect(edgeRelations).toContain('1->3'); // child1 to grandchild2 + expect(edgeRelations).toContain('0->4'); // root to child2 + }); + + it('should preserve node properties in processed data', () => { + // Add a node with specific properties + db.addNode(0, 'root', 'Root Node', 2); // type 2 = rectangle + + // Set additional properties + const mindmap = db.getMindmap(); + if (mindmap) { + mindmap.width = 150; + mindmap.height = 75; + mindmap.padding = 15; + mindmap.section = 1; + mindmap.class = 'custom-class'; + mindmap.icon = 'star'; + } + + const result = db.getData(); + + expect(result.nodes).toHaveLength(1); + const node = result.nodes[0] as MindmapLayoutNode; + + expect(node.type).toBe(2); + expect(node.width).toBe(150); + expect(node.height).toBe(75); + expect(node.padding).toBe(15); + expect(node.section).toBeUndefined(); // Root node has undefined section + expect(node.cssClasses).toBe('mindmap-node section-root section--1 custom-class'); + expect(node.icon).toBe('star'); + }); + + it('should generate unique edge IDs', () => { + db.addNode(0, 'root', 'Root Node', 0); + db.addNode(1, 'child1', 'Child 1', 0); + db.addNode(1, 'child2', 'Child 2', 0); + db.addNode(1, 'child3', 'Child 3', 0); + + const result = db.getData(); + + const edgeIds = result.edges.map((e: Edge) => e.id); + const uniqueIds = new Set(edgeIds); + + expect(edgeIds).toHaveLength(3); + expect(uniqueIds.size).toBe(3); // All IDs should be unique + }); + + it('should handle nodes with missing optional properties', () => { + db.addNode(0, 'root', 'Root Node', 0); + + const result = db.getData(); + const node = result.nodes[0] as MindmapLayoutNode; + + // Should handle undefined/missing properties gracefully + expect(node.section).toBeUndefined(); // Root node has undefined section + expect(node.cssClasses).toBe('mindmap-node section-root section--1'); // Root node gets special classes + expect(node.icon).toBeUndefined(); + expect(node.x).toBeUndefined(); + expect(node.y).toBeUndefined(); + }); + + it('should assign correct section classes based on sibling position', () => { + // Create the example mindmap structure: + // A + // a0 + // aa0 + // a1 + // aaa + // a2 + db.addNode(0, 'A', 'A', 0); // Root + db.addNode(1, 'a0', 'a0', 0); // First child of root + db.addNode(2, 'aa0', 'aa0', 0); // Child of a0 + db.addNode(1, 'a1', 'a1', 0); // Second child of root + db.addNode(2, 'aaa', 'aaa', 0); // Child of a1 + db.addNode(1, 'a2', 'a2', 0); // Third child of root + + const result = db.getData(); + + // Find nodes by their labels + const nodeA = result.nodes.find((n) => n.label === 'A') as MindmapLayoutNode; + const nodeA0 = result.nodes.find((n) => n.label === 'a0') as MindmapLayoutNode; + const nodeAa0 = result.nodes.find((n) => n.label === 'aa0') as MindmapLayoutNode; + const nodeA1 = result.nodes.find((n) => n.label === 'a1') as MindmapLayoutNode; + const nodeAaa = result.nodes.find((n) => n.label === 'aaa') as MindmapLayoutNode; + const nodeA2 = result.nodes.find((n) => n.label === 'a2') as MindmapLayoutNode; + + // Check section assignments + expect(nodeA.section).toBeUndefined(); // Root has undefined section + expect(nodeA0.section).toBe(0); // First child of root + expect(nodeAa0.section).toBe(0); // Inherits from parent a0 + expect(nodeA1.section).toBe(1); // Second child of root + expect(nodeAaa.section).toBe(1); // Inherits from parent a1 + expect(nodeA2.section).toBe(2); // Third child of root + + // Check CSS classes + expect(nodeA.cssClasses).toBe('mindmap-node section-root section--1'); + expect(nodeA0.cssClasses).toBe('mindmap-node section-0'); + expect(nodeAa0.cssClasses).toBe('mindmap-node section-0'); + expect(nodeA1.cssClasses).toBe('mindmap-node section-1'); + expect(nodeAaa.cssClasses).toBe('mindmap-node section-1'); + expect(nodeA2.cssClasses).toBe('mindmap-node section-2'); + }); + + it('should preserve custom classes while adding section classes', () => { + db.addNode(0, 'root', 'Root Node', 0); + db.addNode(1, 'child', 'Child Node', 0); + + // Add custom classes to nodes + const mindmap = db.getMindmap(); + if (mindmap) { + mindmap.class = 'custom-root-class'; + if (mindmap.children?.[0]) { + mindmap.children[0].class = 'custom-child-class'; + } + } + + const result = db.getData(); + const rootNode = result.nodes.find((n) => n.label === 'Root Node') as MindmapLayoutNode; + const childNode = result.nodes.find((n) => n.label === 'Child Node') as MindmapLayoutNode; + + // Should include both section classes and custom classes + expect(rootNode.cssClasses).toBe('mindmap-node section-root section--1 custom-root-class'); + expect(childNode.cssClasses).toBe('mindmap-node section-0 custom-child-class'); + }); + + it('should not create any fake root nodes', () => { + // Create a simple mindmap + db.addNode(0, 'A', 'A', 0); + db.addNode(1, 'a0', 'a0', 0); + db.addNode(1, 'a1', 'a1', 0); + + const result = db.getData(); + + // Check that we only have the expected nodes + expect(result.nodes).toHaveLength(3); + expect(result.nodes.map((n) => n.label)).toEqual(['A', 'a0', 'a1']); + + // Check that there's no node with label "mindmap" or any other fake root + const mindmapNode = result.nodes.find((n) => n.label === 'mindmap'); + expect(mindmapNode).toBeUndefined(); + + // Verify the root node has the correct classes + const rootNode = result.nodes.find((n) => n.label === 'A') as MindmapLayoutNode; + expect(rootNode.cssClasses).toBe('mindmap-node section-root section--1'); + expect(rootNode.level).toBe(0); + }); + + it('should assign correct section classes to edges', () => { + // Create the example mindmap structure: + // A + // a0 + // aa0 + // a1 + // aaa + // a2 + db.addNode(0, 'A', 'A', 0); // Root + db.addNode(1, 'a0', 'a0', 0); // First child of root + db.addNode(2, 'aa0', 'aa0', 0); // Child of a0 + db.addNode(1, 'a1', 'a1', 0); // Second child of root + db.addNode(2, 'aaa', 'aaa', 0); // Child of a1 + db.addNode(1, 'a2', 'a2', 0); // Third child of root + + const result = db.getData(); + + // Should have 5 edges: A->a0, a0->aa0, A->a1, a1->aaa, A->a2 + expect(result.edges).toHaveLength(5); + + // Find edges by their start and end nodes + const edgeA_a0 = result.edges.find( + (e) => e.start === '0' && e.end === '1' + ) as MindmapLayoutEdge; + const edgeA0_aa0 = result.edges.find( + (e) => e.start === '1' && e.end === '2' + ) as MindmapLayoutEdge; + const edgeA_a1 = result.edges.find( + (e) => e.start === '0' && e.end === '3' + ) as MindmapLayoutEdge; + const edgeA1_aaa = result.edges.find( + (e) => e.start === '3' && e.end === '4' + ) as MindmapLayoutEdge; + const edgeA_a2 = result.edges.find( + (e) => e.start === '0' && e.end === '5' + ) as MindmapLayoutEdge; + + // Check edge classes + expect(edgeA_a0.classes).toBe('edge section-edge-0 edge-depth-1'); // A->a0: section-0, depth-1 + expect(edgeA0_aa0.classes).toBe('edge section-edge-0 edge-depth-2'); // a0->aa0: section-0, depth-2 + expect(edgeA_a1.classes).toBe('edge section-edge-1 edge-depth-1'); // A->a1: section-1, depth-1 + expect(edgeA1_aaa.classes).toBe('edge section-edge-1 edge-depth-2'); // a1->aaa: section-1, depth-2 + expect(edgeA_a2.classes).toBe('edge section-edge-2 edge-depth-1'); // A->a2: section-2, depth-1 + + // Check section assignments match the child nodes + expect(edgeA_a0.section).toBe(0); + expect(edgeA0_aa0.section).toBe(0); + expect(edgeA_a1.section).toBe(1); + expect(edgeA1_aaa.section).toBe(1); + expect(edgeA_a2.section).toBe(2); + }); + }); +}); diff --git a/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts b/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts index 703ba8434..aebdba71b 100644 --- a/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts +++ b/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts @@ -1,9 +1,26 @@ import { getConfig } from '../../diagram-api/diagramAPI.js'; +import { v4 } from 'uuid'; import type { D3Element } from '../../types.js'; import { sanitizeText } from '../../diagrams/common/common.js'; import { log } from '../../logger.js'; import type { MindmapNode } from './mindmapTypes.js'; import defaultConfig from '../../defaultConfig.js'; +import type { LayoutData, Node, Edge } from '../../rendering-util/types.js'; +import { getUserDefinedConfig } from '../../config.js'; + +// Extend Node type for mindmap-specific properties +export type MindmapLayoutNode = Node & { + level: number; + nodeId: string; + type: number; + section?: number; +}; + +// Extend Edge type for mindmap-specific properties +export type MindmapLayoutEdge = Edge & { + depth: number; + section?: number; +}; const nodeType = { DEFAULT: 0, @@ -27,7 +44,6 @@ export class MindmapDB { this.nodeType = nodeType; this.clear(); this.getType = this.getType.bind(this); - this.getMindmap = this.getMindmap.bind(this); this.getElementById = this.getElementById.bind(this); this.getParent = this.getParent.bind(this); this.getMindmap = this.getMindmap.bind(this); @@ -156,6 +172,223 @@ export class MindmapDB { } } + /** + * Assign section numbers to nodes based on their position relative to root + * @param node - The mindmap node to process + * @param sectionNumber - The section number to assign (undefined for root) + */ + public assignSections(node: MindmapNode, sectionNumber?: number): void { + // For root node, section should be undefined (not -1) + if (node.level === 0) { + node.section = undefined; + } else { + // For non-root nodes, assign the section number + node.section = sectionNumber; + } + // For root node's children, assign section numbers based on their index + // For other nodes, inherit parent's section number + if (node.children) { + for (const [index, child] of node.children.entries()) { + const childSectionNumber = node.level === 0 ? index : sectionNumber; + this.assignSections(child, childSectionNumber); + } + } + } + + /** + * Convert mindmap tree structure to flat array of nodes + * @param node - The mindmap node to process + * @param processedNodes - Array to collect processed nodes + */ + public flattenNodes(node: MindmapNode, processedNodes: MindmapLayoutNode[]): void { + // Build CSS classes for the node + const cssClasses = ['mindmap-node']; + + // Add section-specific classes + if (node.level === 0) { + // Root node gets special classes + cssClasses.push('section-root', 'section--1'); + } else if (node.section !== undefined) { + // Child nodes get section class based on their section number + cssClasses.push(`section-${node.section}`); + } + + // Add any custom classes from the node + if (node.class) { + cssClasses.push(node.class); + } + + const classes = cssClasses.join(' '); + + // Map mindmap node type to valid shape name + const getShapeFromType = (type: number) => { + switch (type) { + case nodeType.CIRCLE: + return 'mindmapCircle'; + case nodeType.RECT: + return 'rect'; + case nodeType.ROUNDED_RECT: + return 'rounded'; + case nodeType.CLOUD: + return 'cloud'; + case nodeType.BANG: + return 'bang'; + case nodeType.HEXAGON: + return 'hexagon'; + case nodeType.DEFAULT: + return 'defaultMindmapNode'; + case nodeType.NO_BORDER: + default: + return 'rect'; + } + }; + + const processedNode: MindmapLayoutNode = { + id: node.id.toString(), + domId: 'node_' + node.id.toString(), + label: node.descr, + isGroup: false, + shape: getShapeFromType(node.type), + width: node.width, + height: node.height ?? 0, + padding: node.padding, + cssClasses: classes, + cssStyles: [], + look: 'default', + icon: node.icon, + x: node.x, + y: node.y, + // Mindmap-specific properties + level: node.level, + nodeId: node.nodeId, + type: node.type, + section: node.section, + }; + + processedNodes.push(processedNode); + + // Recursively process children + if (node.children) { + for (const child of node.children) { + this.flattenNodes(child, processedNodes); + } + } + } + + /** + * Generate edges from parent-child relationships in mindmap tree + * @param node - The mindmap node to process + * @param edges - Array to collect edges + */ + public generateEdges(node: MindmapNode, edges: MindmapLayoutEdge[]): void { + if (!node.children) { + return; + } + for (const child of node.children) { + // Build CSS classes for the edge + let edgeClasses = 'edge'; + + // Add section-specific classes based on the child's section + if (child.section !== undefined) { + edgeClasses += ` section-edge-${child.section}`; + } + + // Add depth class based on the parent's level + 1 (depth of the edge) + const edgeDepth = node.level + 1; + edgeClasses += ` edge-depth-${edgeDepth}`; + + const edge: MindmapLayoutEdge = { + id: `edge_${node.id}_${child.id}`, + start: node.id.toString(), + end: child.id.toString(), + type: 'normal', + curve: 'basis', + thickness: 'normal', + look: 'default', + classes: edgeClasses, + // Store mindmap-specific data + depth: node.level, + section: child.section, + }; + + edges.push(edge); + + // Recursively process child edges + this.generateEdges(child, edges); + } + } + + /** + * Get structured data for layout algorithms + * Following the pattern established by ER diagrams + * @returns Structured data containing nodes, edges, and config + */ + public getData(): LayoutData { + const mindmapRoot = this.getMindmap(); + const config = getConfig(); + + const userDefinedConfig = getUserDefinedConfig(); + const hasUserDefinedLayout = userDefinedConfig.layout !== undefined; + + const finalConfig = config; + if (!hasUserDefinedLayout) { + finalConfig.layout = 'cose-bilkent'; + } + + if (!mindmapRoot) { + return { + nodes: [], + edges: [], + config: finalConfig, + }; + } + log.debug('getData: mindmapRoot', mindmapRoot, config); + + // Assign section numbers to all nodes based on their position relative to root + this.assignSections(mindmapRoot); + + // Convert tree structure to flat arrays + const processedNodes: MindmapLayoutNode[] = []; + const processedEdges: MindmapLayoutEdge[] = []; + + this.flattenNodes(mindmapRoot, processedNodes); + this.generateEdges(mindmapRoot, processedEdges); + + log.debug( + `getData: processed ${processedNodes.length} nodes and ${processedEdges.length} edges` + ); + + // Create shapes map for ELK compatibility + const shapes = new Map(); + for (const node of processedNodes) { + shapes.set(node.id, { + shape: node.shape, + width: node.width, + height: node.height, + padding: node.padding, + }); + } + + return { + nodes: processedNodes, + edges: processedEdges, + config: finalConfig, + // Store the root node for mindmap-specific layout algorithms + rootNode: mindmapRoot, + // Properties required by dagre layout algorithm + markers: ['point'], // Mindmaps don't use markers + direction: 'TB', // Top-to-bottom direction for mindmaps + nodeSpacing: 50, // Default spacing between nodes + rankSpacing: 50, // Default spacing between ranks + // Add shapes for ELK compatibility + shapes: Object.fromEntries(shapes), + // Additional properties that layout algorithms might expect + type: 'mindmap', + diagramId: 'mindmap-' + v4(), + }; + } + + // Expose logger to grammar public getLogger() { return log; } diff --git a/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts b/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts index ef9be0565..a962dc924 100644 --- a/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts +++ b/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts @@ -1,200 +1,83 @@ -import cytoscape from 'cytoscape'; -// @ts-expect-error No types available -import coseBilkent from 'cytoscape-cose-bilkent'; -import { select } from 'd3'; -import type { MermaidConfig } from '../../config.type.js'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; import type { DrawDefinition } from '../../diagram-api/types.js'; import { log } from '../../logger.js'; -import type { D3Element } from '../../types.js'; -import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; -import { setupGraphViewbox } from '../../setupGraphViewbox.js'; -import type { FilledMindMapNode, MindmapNode } from './mindmapTypes.js'; -import { drawNode, positionNode } from './svgDraw.js'; +import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js'; +import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js'; +import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; +import type { LayoutData } from '../../rendering-util/types.js'; +import type { FilledMindMapNode } from './mindmapTypes.js'; import defaultConfig from '../../defaultConfig.js'; import type { MindmapDB } from './mindmapDb.js'; -// Inject the layout algorithm into cytoscape -cytoscape.use(coseBilkent); -async function drawNodes( - db: MindmapDB, - svg: D3Element, - mindmap: FilledMindMapNode, - section: number, - conf: MermaidConfig -) { - await drawNode(db, svg, mindmap, section, conf); - if (mindmap.children) { - await Promise.all( - mindmap.children.map((child, index) => - drawNodes(db, svg, child, section < 0 ? index : section, conf) - ) - ); - } -} - -declare module 'cytoscape' { - interface EdgeSingular { - _private: { - bodyBounds: unknown; - rscratch: { - startX: number; - startY: number; - midX: number; - midY: number; - endX: number; - endY: number; - }; - }; - } -} - -function drawEdges(edgesEl: D3Element, cy: cytoscape.Core) { - cy.edges().map((edge, id) => { - const data = edge.data(); - if (edge[0]._private.bodyBounds) { - const bounds = edge[0]._private.rscratch; - log.trace('Edge: ', id, data); - edgesEl - .insert('path') - .attr( - 'd', - `M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} ` - ) - .attr('class', 'edge section-edge-' + data.section + ' edge-depth-' + data.depth); +/** + * Update the layout data with actual node dimensions after drawing + */ +function _updateNodeDimensions(data4Layout: LayoutData, mindmapRoot: FilledMindMapNode) { + const updateNode = (node: FilledMindMapNode) => { + // Find the corresponding node in the layout data + const layoutNode = data4Layout.nodes.find((n) => n.id === node.id.toString()); + if (layoutNode) { + // Update with the actual dimensions calculated by drawNode + layoutNode.width = node.width; + layoutNode.height = node.height; + log.debug('Updated node dimensions:', node.id, 'width:', node.width, 'height:', node.height); } - }); -} -function addNodes(mindmap: MindmapNode, cy: cytoscape.Core, conf: MermaidConfig, level: number) { - cy.add({ - group: 'nodes', - data: { - id: mindmap.id.toString(), - labelText: mindmap.descr, - height: mindmap.height, - width: mindmap.width, - level: level, - nodeId: mindmap.id, - padding: mindmap.padding, - type: mindmap.type, - }, - position: { - x: mindmap.x!, - y: mindmap.y!, - }, - }); - if (mindmap.children) { - mindmap.children.forEach((child) => { - addNodes(child, cy, conf, level + 1); - cy.add({ - group: 'edges', - data: { - id: `${mindmap.id}_${child.id}`, - source: mindmap.id, - target: child.id, - depth: level, - section: child.section, - }, - }); - }); - } -} + // Recursively update children + node.children?.forEach(updateNode); + }; -function layoutMindmap(node: MindmapNode, conf: MermaidConfig): Promise { - return new Promise((resolve) => { - // Add temporary render element - const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none'); - const cy = cytoscape({ - container: document.getElementById('cy'), // container to render in - style: [ - { - selector: 'edge', - style: { - 'curve-style': 'bezier', - }, - }, - ], - }); - // Remove element after layout - renderEl.remove(); - addNodes(node, cy, conf, 0); - - // Make cytoscape care about the dimensions of the nodes - cy.nodes().forEach(function (n) { - n.layoutDimensions = () => { - const data = n.data(); - return { w: data.width, h: data.height }; - }; - }); - - cy.layout({ - name: 'cose-bilkent', - // @ts-ignore Types for cose-bilkent are not correct? - quality: 'proof', - styleEnabled: false, - animate: false, - }).run(); - cy.ready((e) => { - log.info('Ready', e); - resolve(cy); - }); - }); -} - -function positionNodes(db: MindmapDB, cy: cytoscape.Core) { - cy.nodes().map((node, id) => { - const data = node.data(); - data.x = node.position().x; - data.y = node.position().y; - positionNode(db, data); - const el = db.getElementById(data.nodeId); - log.info('id:', id, 'Position: (', node.position().x, ', ', node.position().y, ')', data); - el.attr( - 'transform', - `translate(${node.position().x - data.width / 2}, ${node.position().y - data.height / 2})` - ); - el.attr('attr', `apa-${id})`); - }); + updateNode(mindmapRoot); } export const draw: DrawDefinition = async (text, id, _version, diagObj) => { log.debug('Rendering mindmap diagram\n' + text); + // Draw the nodes first to get their dimensions, then update the layout data const db = diagObj.db as MindmapDB; + + // The getData method provided in all supported diagrams is used to extract the data from the parsed structure + // into the Layout data format + const data4Layout = db.getData(); + + // Create the root SVG - the element is the div containing the SVG element + const svg = getDiagramElement(id, data4Layout.config.securityLevel); + + data4Layout.type = diagObj.type; + data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(data4Layout.config.layout, { + fallback: 'cose-bilkent', + }); + + data4Layout.diagramId = id; + const mm = db.getMindmap(); if (!mm) { return; } - const conf = getConfig(); - conf.htmlLabels = false; + data4Layout.nodes.forEach((node) => { + if (node.shape === 'rounded') { + node.radius = 15; + node.taper = 15; + node.stroke = 'none'; + node.width = 0; + node.padding = 15; + } else if (node.shape === 'circle') { + node.padding = 10; + } else if (node.shape === 'rect') { + node.width = 0; + node.padding = 10; + } + }); - const svg = selectSvgElement(id); + // Use the unified rendering system + await render(data4Layout, svg); - // Draw the graph and start with drawing the nodes without proper position - // this gives us the size of the nodes and we can set the positions later - - const edgesElem = svg.append('g'); - edgesElem.attr('class', 'mindmap-edges'); - const nodesElem = svg.append('g'); - nodesElem.attr('class', 'mindmap-nodes'); - await drawNodes(db, nodesElem, mm as FilledMindMapNode, -1, conf); - - // Next step is to layout the mindmap, giving each node a position - - const cy = await layoutMindmap(mm, conf); - - // After this we can draw, first the edges and the then nodes with the correct position - drawEdges(edgesElem, cy); - positionNodes(db, cy); - - // Setup the view box and size of the svg element - setupGraphViewbox( - undefined, + // Setup the view box and size of the svg element using config from data4Layout + setupViewPortForSVG( svg, - conf.mindmap?.padding ?? defaultConfig.mindmap.padding, - conf.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth + data4Layout.config.mindmap?.padding ?? defaultConfig.mindmap.padding, + 'mindmapDiagram', + data4Layout.config.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth ); }; diff --git a/packages/mermaid/src/diagrams/mindmap/styles.ts b/packages/mermaid/src/diagrams/mindmap/styles.ts index fffa6e4d9..8372bddf1 100644 --- a/packages/mermaid/src/diagrams/mindmap/styles.ts +++ b/packages/mermaid/src/diagrams/mindmap/styles.ts @@ -64,6 +64,12 @@ const getStyles: DiagramStylesProvider = (options) => .section-root text { fill: ${options.gitBranchLabel0}; } + .section-root span { + color: ${options.gitBranchLabel0}; + } + .section-2 span { + color: ${options.gitBranchLabel0}; + } .icon-container { height:100%; display: flex; diff --git a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison index 2dcadc0bb..13e63f3ae 100644 --- a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison +++ b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison @@ -14,7 +14,7 @@ // Special states for recognizing aliases // A special state for grabbing text up to the first comment/newline -%x ID ALIAS LINE +%x ID ALIAS LINE CONFIG CONFIG_DATA %x acc_title %x acc_descr @@ -28,6 +28,11 @@ \%%(?!\{)[^\n]* /* skip comments */ [^\}]\%\%[^\n]* /* skip comments */ [0-9]+(?=[ \n]+) return 'NUM'; +\@\{ { this.begin('CONFIG'); return 'CONFIG_START'; } +[^\}]+ { return 'CONFIG_CONTENT'; } +\} { this.popState(); this.popState(); return 'CONFIG_END'; } +[^\<->\->:\n,;@\s]+(?=\@\{) { yytext = yytext.trim(); return 'ACTOR'; } +[^\<->\->:\n,;@]+?([\-]*[^\<->\->:\n,;@]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; } "box" { this.begin('LINE'); return 'box'; } "participant" { this.begin('ID'); return 'participant'; } "actor" { this.begin('ID'); return 'participant_actor'; } @@ -231,6 +236,8 @@ participant_statement | 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;} | 'participant_actor' actor 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;} | 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;} + | 'participant' actor_with_config 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant'; $$=$2;} + ; note_statement @@ -301,6 +308,23 @@ signal { $$ = [$1,$3,{type: 'addMessage', from:$1.actor, to:$3.actor, signalType:$2, msg:$4}]} ; +actor_with_config + : ACTOR config_object + { + $$ = { + type: 'addParticipant', + actor: $1, + config: $2 + }; + } + ; + +config_object + : CONFIG_START CONFIG_CONTENT CONFIG_END + { + $$ = $2.trim(); + } + ; // actor // : actor_participant // | actor_actor @@ -313,7 +337,7 @@ signaltype : SOLID_OPEN_ARROW { $$ = yy.LINETYPE.SOLID_OPEN; } | DOTTED_OPEN_ARROW { $$ = yy.LINETYPE.DOTTED_OPEN; } | SOLID_ARROW { $$ = yy.LINETYPE.SOLID; } - | BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; } + | BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; } | DOTTED_ARROW { $$ = yy.LINETYPE.DOTTED; } | BIDIRECTIONAL_DOTTED_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_DOTTED; } | SOLID_CROSS { $$ = yy.LINETYPE.SOLID_CROSS; } diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDb.ts b/packages/mermaid/src/diagrams/sequence/sequenceDb.ts index c6b44dac0..67ae19de5 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDb.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceDb.ts @@ -1,4 +1,5 @@ import { getConfig } from '../../diagram-api/diagramAPI.js'; +import * as yaml from 'js-yaml'; import type { DiagramDB } from '../../diagram-api/types.js'; import { log } from '../../logger.js'; import { ImperativeState } from '../../utils/imperativeState.js'; @@ -13,6 +14,7 @@ import { setDiagramTitle, } from '../common/commonDb.js'; import type { Actor, AddMessageParams, Box, Message, Note } from './types.js'; +import type { ParticipantMetaData } from '../../types.js'; interface SequenceState { prevActor?: string; @@ -75,6 +77,17 @@ const PLACEMENT = { OVER: 2, } as const; +export const PARTICIPANT_TYPE = { + ACTOR: 'actor', + BOUNDARY: 'boundary', + COLLECTIONS: 'collections', + CONTROL: 'control', + DATABASE: 'database', + ENTITY: 'entity', + PARTICIPANT: 'participant', + QUEUE: 'queue', +} as const; + export class SequenceDB implements DiagramDB { private readonly state = new ImperativeState(() => ({ prevActor: undefined, @@ -119,9 +132,22 @@ export class SequenceDB implements DiagramDB { id: string, name: string, description: { text: string; wrap?: boolean | null; type: string }, - type: string + type: string, + metadata?: any ) { let assignedBox = this.state.records.currentBox; + let doc; + if (metadata !== undefined) { + let yamlData; + // detect if shapeData contains a newline character + if (!metadata.includes('\n')) { + yamlData = '{\n' + metadata + '\n}'; + } else { + yamlData = metadata + '\n'; + } + doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as ParticipantMetaData; + } + type = doc?.type ?? type; const old = this.state.records.actors.get(id); if (old) { // If already set and trying to set to a new one throw error @@ -518,7 +544,7 @@ export class SequenceDB implements DiagramDB { }); break; case 'addParticipant': - this.addActor(param.actor, param.actor, param.description, param.draw); + this.addActor(param.actor, param.actor, param.description, param.draw, param.config); break; case 'createParticipant': if (this.state.records.actors.has(param.actor)) { @@ -527,7 +553,7 @@ export class SequenceDB implements DiagramDB { ); } this.state.records.lastCreated = param.actor; - this.addActor(param.actor, param.actor, param.description, param.draw); + this.addActor(param.actor, param.actor, param.description, param.draw, param.config); this.state.records.createdActors.set(param.actor, this.state.records.messages.length); break; case 'destroyParticipant': diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js index 97833be8f..4e69fda7e 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js +++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js @@ -1368,7 +1368,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com it('should handle box without description', async () => { const diagram = await Diagram.fromText(` sequenceDiagram - box Aqua + box aqua participant a as Alice participant b as Bob end @@ -1384,7 +1384,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com const boxes = diagram.db.getBoxes(); expect(boxes[0].name).toBeFalsy(); expect(boxes[0].actorKeys).toEqual(['a', 'b']); - expect(boxes[0].fill).toEqual('Aqua'); + expect(boxes[0].fill).toEqual('aqua'); }); it('should handle simple actor creation', async () => { @@ -2058,4 +2058,272 @@ Bob->>Alice:Got it! expect(messages[0].from).toBe('Alice'); expect(messages[0].to).toBe('Bob'); }); + describe('when parsing extended participant syntax', () => { + it('should parse participants with different quote styles and whitespace', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice@{ "type" : "database" } + participant Bob@{ "type" : "database" } + participant Carl@{ type: "database" } + participant David@{ "type" : 'database' } + participant Eve@{ type: 'database' } + participant Favela@{ "type" : "database" } + Bob->>+Alice: Hi Alice + Alice->>+Bob: Hi Bob + `); + + const actors = diagram.db.getActors(); + + expect(actors.get('Alice').type).toBe('database'); + expect(actors.get('Alice').description).toBe('Alice'); + + expect(actors.get('Bob').type).toBe('database'); + expect(actors.get('Bob').description).toBe('Bob'); + + expect(actors.get('Carl').type).toBe('database'); + expect(actors.get('Carl').description).toBe('Carl'); + + expect(actors.get('David').type).toBe('database'); + expect(actors.get('David').description).toBe('David'); + + expect(actors.get('Eve').type).toBe('database'); + expect(actors.get('Eve').description).toBe('Eve'); + + expect(actors.get('Favela').type).toBe('database'); + expect(actors.get('Favela').description).toBe('Favela'); + + // Verify messages were parsed correctly + const messages = diagram.db.getMessages(); + expect(messages.length).toBe(4); // 2 messages + 2 activation messages + expect(messages[0].from).toBe('Bob'); + expect(messages[0].to).toBe('Alice'); + expect(messages[0].message).toBe('Hi Alice'); + expect(messages[2].from).toBe('Alice'); // Second message (index 2 due to activation) + expect(messages[2].to).toBe('Bob'); + expect(messages[2].message).toBe('Hi Bob'); + }); + + it('should parse mixed participant types with extended syntax', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant lead + participant dsa@{ "type" : "queue" } + API->>+Database: getUserb + Database-->>-API: userb + dsa --> Database: hello +`); + + // Verify actors were created + const actors = diagram.db.getActors(); + + expect(actors.get('lead').type).toBe('participant'); + expect(actors.get('lead').description).toBe('lead'); + + // Participant with extended syntax + expect(actors.get('dsa').type).toBe('queue'); + expect(actors.get('dsa').description).toBe('dsa'); + + // Implicitly created actors (from messages) + expect(actors.get('API').type).toBe('participant'); + expect(actors.get('API').description).toBe('API'); + + expect(actors.get('Database').type).toBe('participant'); + expect(actors.get('Database').description).toBe('Database'); + + // Verify messages were parsed correctly + const messages = diagram.db.getMessages(); + expect(messages.length).toBe(5); // 3 messages + 2 activation messages + + // First message with activation + expect(messages[0].from).toBe('API'); + expect(messages[0].to).toBe('Database'); + expect(messages[0].message).toBe('getUserb'); + expect(messages[0].activate).toBe(true); + + // Second message with deactivation + expect(messages[2].from).toBe('Database'); + expect(messages[2].to).toBe('API'); + expect(messages[2].message).toBe('userb'); + + // Third message + expect(messages[4].from).toBe('dsa'); + expect(messages[4].to).toBe('Database'); + expect(messages[4].message).toBe('hello'); + }); + + it('should fail for malformed JSON in participant definition', async () => { + const invalidDiagram = ` + sequenceDiagram + participant D@{ "type: "entity" } + participant E@{ "type": "dat + abase } + `; + + let error = false; + try { + await mermaidAPI.parse(invalidDiagram); + } catch (e) { + error = true; + } + expect(error).toBe(true); + }); + + it('should fail for missing colon separator', async () => { + const invalidDiagram = ` + sequenceDiagram + participant C@{ "type" "control" } + C ->> C: action + `; + + let error = false; + try { + await mermaidAPI.parse(invalidDiagram); + } catch (e) { + error = true; + } + expect(error).toBe(true); + }); + + it('should fail for missing closing brace', async () => { + const invalidDiagram = ` + sequenceDiagram + participant E@{ "type": "entity" + E ->> E: process + `; + + let error = false; + try { + await mermaidAPI.parse(invalidDiagram); + } catch (e) { + error = true; + } + expect(error).toBe(true); + }); + }); + describe('participant type parsing', () => { + it('should parse boundary participant', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant boundary@{ "type" : "boundary" } + boundary->boundary: test + `); + const actors = diagram.db.getActors(); + expect(actors.get('boundary').type).toBe('boundary'); + expect(actors.get('boundary').description).toBe('boundary'); + }); + + it('should parse control participant', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant C@{ "type" : "control" } + C->C: test + `); + const actors = diagram.db.getActors(); + expect(actors.get('C').type).toBe('control'); + expect(actors.get('C').description).toBe('C'); + }); + + it('should parse entity participant', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant E@{ "type" : "entity" } + E->E: test + `); + const actors = diagram.db.getActors(); + expect(actors.get('E').type).toBe('entity'); + expect(actors.get('E').description).toBe('E'); + }); + + it('should parse database participant', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant D@{ "type" : "database" } + D->D: test + `); + const actors = diagram.db.getActors(); + expect(actors.get('D').type).toBe('database'); + expect(actors.get('D').description).toBe('D'); + }); + + it('should parse collections participant', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant L@{ "type" : "collections" } + L->L: test + `); + const actors = diagram.db.getActors(); + expect(actors.get('L').type).toBe('collections'); + expect(actors.get('L').description).toBe('L'); + }); + + it('should parse queue participant', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Q@{ "type" : "queue" } + Q->Q: test + `); + const actors = diagram.db.getActors(); + expect(actors.get('Q').type).toBe('queue'); + expect(actors.get('Q').description).toBe('Q'); + }); + }); + + describe('participant type parsing', () => { + it('should parse actor participant', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant A@{ "type" : "queue" } + A->A: test + `); + const actors = diagram.db.getActors(); + expect(actors.get('A').type).toBe('queue'); + expect(actors.get('A').description).toBe('A'); + }); + + it('should parse participant participant', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant P@{ "type" : "database" } + P->P: test + `); + const actors = diagram.db.getActors(); + expect(actors.get('P').type).toBe('database'); + expect(actors.get('P').description).toBe('P'); + }); + + it('should parse boundary using actor keyword', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice@{ "type" : "collections" } + participant Bob@{ "type" : "control" } + Alice->>Bob: Hello Bob, how are you? + `); + const actors = diagram.db.getActors(); + expect(actors.get('Alice').type).toBe('collections'); + expect(actors.get('Bob').type).toBe('control'); + expect(actors.get('Bob').description).toBe('Bob'); + }); + + it('should parse control using participant keyword', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant C@{ "type" : "control" } + C->C: test + `); + const actors = diagram.db.getActors(); + expect(actors.get('C').type).toBe('control'); + expect(actors.get('C').description).toBe('C'); + }); + + it('should parse entity using actor keyword', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant E@{ "type" : "entity" } + E->E: test + `); + const actors = diagram.db.getActors(); + expect(actors.get('E').type).toBe('entity'); + expect(actors.get('E').description).toBe('E'); + }); + }); }); diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index 2c0d898ca..5fac3cf2d 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -10,6 +10,7 @@ import assignWithDepth from '../../assignWithDepth.js'; import utils from '../../utils.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; import type { Diagram } from '../../Diagram.js'; +import { PARTICIPANT_TYPE } from './sequenceDb.js'; let conf = {}; @@ -746,11 +747,19 @@ function adjustCreatedDestroyedData( msgModel.startx = msgModel.startx - adjustment; } } + const actorArray = [ + PARTICIPANT_TYPE.ACTOR, + PARTICIPANT_TYPE.CONTROL, + PARTICIPANT_TYPE.ENTITY, + PARTICIPANT_TYPE.DATABASE, + ]; // if it is a create message if (createdActors.get(msg.to) == index) { const actor = actors.get(msg.to); - const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3; + const adjustment = actorArray.includes(actor.type) + ? ACTOR_TYPE_WIDTH / 2 + 3 + : actor.width / 2 + 3; receiverAdjustment(actor, adjustment); actor.starty = lineStartY - actor.height / 2; bounds.bumpVerticalPos(actor.height / 2); @@ -759,7 +768,7 @@ function adjustCreatedDestroyedData( else if (destroyedActors.get(msg.from) == index) { const actor = actors.get(msg.from); if (conf.mirrorActors) { - const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2; + const adjustment = actorArray.includes(actor.type) ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2; senderAdjustment(actor, adjustment); } actor.stopy = lineStartY - actor.height / 2; @@ -769,7 +778,9 @@ function adjustCreatedDestroyedData( else if (destroyedActors.get(msg.to) == index) { const actor = actors.get(msg.to); if (conf.mirrorActors) { - const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3; + const adjustment = actorArray.includes(actor.type) + ? ACTOR_TYPE_WIDTH / 2 + 3 + : actor.width / 2 + 3; receiverAdjustment(actor, adjustment); } actor.stopy = lineStartY - actor.height / 2; @@ -1087,10 +1098,11 @@ export const draw = async function (_text: string, id: string, _version: string, for (const box of bounds.models.boxes) { box.height = bounds.getVerticalPos() - box.y; bounds.insert(box.x, box.y, box.x + box.width, box.height); - box.startx = box.x; - box.starty = box.y; - box.stopx = box.startx + box.width; - box.stopy = box.starty + box.height; + const boxPadding = conf.boxMargin * 2; + box.startx = box.x - boxPadding; + box.starty = box.y - boxPadding * 0.25; + box.stopx = box.startx + box.width + 2 * boxPadding; + box.stopy = box.starty + box.height + boxPadding * 0.75; box.stroke = 'rgb(0,0,0, 0.5)'; svgDraw.drawBox(diagram, box, conf); } @@ -1355,6 +1367,9 @@ async function calculateActorMargins( return (total += actors.get(aKey).width + (actors.get(aKey).margin || 0)); }, 0); + const standardBoxPadding = conf.boxMargin * 8; + totalWidth += standardBoxPadding; + totalWidth -= 2 * conf.boxTextMargin; if (box.wrap) { box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont); diff --git a/packages/mermaid/src/diagrams/sequence/styles.js b/packages/mermaid/src/diagrams/sequence/styles.js index 5c36b4ed1..3cee9d3dc 100644 --- a/packages/mermaid/src/diagrams/sequence/styles.js +++ b/packages/mermaid/src/diagrams/sequence/styles.js @@ -12,6 +12,11 @@ const getStyles = (options) => .actor-line { stroke: ${options.actorLineColor}; } + + .innerArc { + stroke-width: 1.5; + stroke-dasharray: none; + } .messageLine0 { stroke-width: 1.5; @@ -115,6 +120,7 @@ const getStyles = (options) => fill: ${options.actorBkg}; stroke-width: 2px; } + `; export default getStyles; diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js index 18fd2d034..1971082a8 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js @@ -415,6 +415,600 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) { return height; }; +/** + * Draws an actor in the diagram with the attached line + * + * @param {any} elem - The diagram we'll draw to. + * @param {any} actor - The actor to draw. + * @param {any} conf - DrawText implementation discriminator object + * @param {boolean} isFooter - If the actor is the footer one + */ +const drawActorTypeCollections = function (elem, actor, conf, isFooter) { + const actorY = isFooter ? actor.stopy : actor.starty; + const center = actor.x + actor.width / 2; + const centerY = actorY + actor.height; + + const boxplusLineGroup = elem.append('g').lower(); + var g = boxplusLineGroup; + + if (!isFooter) { + actorCnt++; + if (Object.keys(actor.links || {}).length && !conf.forceMenus) { + g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer'); + } + g.append('line') + .attr('id', 'actor' + actorCnt) + .attr('x1', center) + .attr('y1', centerY) + .attr('x2', center) + .attr('y2', 2000) + .attr('class', 'actor-line 200') + .attr('stroke-width', '0.5px') + .attr('stroke', '#999') + .attr('name', actor.name); + + g = boxplusLineGroup.append('g'); + actor.actorCnt = actorCnt; + + if (actor.links != null) { + g.attr('id', 'root-' + actorCnt); + } + } + + const rect = svgDrawCommon.getNoteRect(); + var cssclass = 'actor'; + if (actor.properties?.class) { + cssclass = actor.properties.class; + } else { + rect.fill = '#eaeaea'; + } + if (isFooter) { + cssclass += ` ${BOTTOM_ACTOR_CLASS}`; + } else { + cssclass += ` ${TOP_ACTOR_CLASS}`; + } + rect.x = actor.x; + rect.y = actorY; + rect.width = actor.width; + rect.height = actor.height; + rect.class = cssclass; + rect.name = actor.name; + + // DRAW STACKED RECTANGLES + const offset = 6; + const shadowRect = { + ...rect, + x: rect.x + (isFooter ? -offset : -offset), + y: rect.y + (isFooter ? +offset : +offset), + class: 'actor', + }; + const rectElem = drawRect(g, rect); // draw main rectangle on top + drawRect(g, shadowRect); + actor.rectData = rect; + + if (actor.properties?.icon) { + const iconSrc = actor.properties.icon.trim(); + if (iconSrc.charAt(0) === '@') { + svgDrawCommon.drawEmbeddedImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc.substr(1)); + } else { + svgDrawCommon.drawImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc); + } + } + + _drawTextCandidateFunc(conf, hasKatex(actor.description))( + actor.description, + g, + rect.x - offset, + rect.y + offset, + rect.width, + rect.height, + { class: `actor ${ACTOR_BOX_CLASS}` }, + conf + ); + + let height = actor.height; + if (rectElem.node) { + const bounds = rectElem.node().getBBox(); + actor.height = bounds.height; + height = bounds.height; + } + + return height; +}; + +const drawActorTypeQueue = function (elem, actor, conf, isFooter) { + const actorY = isFooter ? actor.stopy : actor.starty; + const center = actor.x + actor.width / 2; + const centerY = actorY + actor.height; + + const boxplusLineGroup = elem.append('g').lower(); + let g = boxplusLineGroup; + + if (!isFooter) { + actorCnt++; + if (Object.keys(actor.links || {}).length && !conf.forceMenus) { + g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer'); + } + g.append('line') + .attr('id', 'actor' + actorCnt) + .attr('x1', center) + .attr('y1', centerY) + .attr('x2', center) + .attr('y2', 2000) + .attr('class', 'actor-line 200') + .attr('stroke-width', '0.5px') + .attr('stroke', '#999') + .attr('name', actor.name); + + g = boxplusLineGroup.append('g'); + actor.actorCnt = actorCnt; + + if (actor.links != null) { + g.attr('id', 'root-' + actorCnt); + } + } + + const rect = svgDrawCommon.getNoteRect(); + let cssclass = 'actor'; + if (actor.properties?.class) { + cssclass = actor.properties.class; + } else { + rect.fill = '#eaeaea'; + } + + if (isFooter) { + cssclass += ` ${BOTTOM_ACTOR_CLASS}`; + } else { + cssclass += ` ${TOP_ACTOR_CLASS}`; + } + + rect.x = actor.x; + rect.y = actorY; + rect.width = actor.width; + rect.height = actor.height; + rect.class = cssclass; + rect.name = actor.name; + + // Cylinder dimensions + const ry = rect.height / 2; + const rx = ry / (2.5 + rect.height / 50); + + // Cylinder base group + const cylinderGroup = g.append('g'); + const cylinderArc = g.append('g'); + + // Main cylinder body + cylinderGroup + .append('path') + .attr( + 'd', + `M ${rect.x},${rect.y + ry} + a ${rx},${ry} 0 0 0 0,${rect.height} + h ${rect.width - 2 * rx} + a ${rx},${ry} 0 0 0 0,-${rect.height} + Z + ` + ) + .attr('class', cssclass); + cylinderArc + .append('path') + .attr( + 'd', + `M ${rect.x},${rect.y + ry} + a ${rx},${ry} 0 0 0 0,${rect.height}` + ) + .attr('stroke', '#666') + .attr('stroke-width', '1px') + .attr('class', cssclass); + + cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2)})`); + cylinderArc.attr('transform', `translate(${rect.width - rx}, ${-rect.height / 2})`); + + actor.rectData = rect; + + if (actor.properties?.icon) { + const iconSrc = actor.properties.icon.trim(); + const iconX = rect.x + rect.width - 20; + const iconY = rect.y + 10; + if (iconSrc.charAt(0) === '@') { + svgDrawCommon.drawEmbeddedImage(g, iconX, iconY, iconSrc.substr(1)); + } else { + svgDrawCommon.drawImage(g, iconX, iconY, iconSrc); + } + } + + _drawTextCandidateFunc(conf, hasKatex(actor.description))( + actor.description, + g, + rect.x, + rect.y, + rect.width, + rect.height, + { class: `actor ${ACTOR_BOX_CLASS}` }, + conf + ); + + let height = actor.height; + const lastPath = cylinderGroup.select('path:last-child'); + if (lastPath.node()) { + const bounds = lastPath.node().getBBox(); + actor.height = bounds.height; + height = bounds.height; + } + + return height; +}; + +const drawActorTypeControl = function (elem, actor, conf, isFooter) { + const actorY = isFooter ? actor.stopy : actor.starty; + const center = actor.x + actor.width / 2; + const centerY = actorY + 75; + + const line = elem.append('g').lower(); + + if (!isFooter) { + actorCnt++; + line + .append('line') + .attr('id', 'actor' + actorCnt) + .attr('x1', center) + .attr('y1', centerY) + .attr('x2', center) + .attr('y2', 2000) + .attr('class', 'actor-line 200') + .attr('stroke-width', '0.5px') + .attr('stroke', '#999') + .attr('name', actor.name); + + actor.actorCnt = actorCnt; + } + const actElem = elem.append('g'); + let cssClass = ACTOR_MAN_FIGURE_CLASS; + if (isFooter) { + cssClass += ` ${BOTTOM_ACTOR_CLASS}`; + } else { + cssClass += ` ${TOP_ACTOR_CLASS}`; + } + actElem.attr('class', cssClass); + actElem.attr('name', actor.name); + + const rect = svgDrawCommon.getNoteRect(); + rect.x = actor.x; + rect.y = actorY; + rect.fill = '#eaeaea'; + rect.width = actor.width; + rect.height = actor.height; + rect.class = 'actor'; + + const cx = actor.x + actor.width / 2; + const cy = actorY + 30; + const r = 18; + + actElem + .append('defs') + .append('marker') + .attr('id', 'filled-head-control') + .attr('refX', 11) + .attr('refY', 5.8) + .attr('markerWidth', 20) + .attr('markerHeight', 28) + .attr('orient', '172.5') + .append('path') + .attr('d', 'M 14.4 5.6 L 7.2 10.4 L 8.8 5.6 L 7.2 0.8 Z'); + + // Draw the base circle + actElem + .append('circle') + .attr('cx', cx) + .attr('cy', cy) + .attr('r', r) + .attr('fill', '#eaeaf7') + .attr('stroke', '#666') + .attr('stroke-width', 1.2); + + // Draw looping arrow as arc path + actElem + .append('line') + .attr('marker-end', 'url(#filled-head-control)') + .attr('transform', `translate(${cx}, ${cy - r})`); + + const bounds = actElem.node().getBBox(); + actor.height = bounds.height + 2 * (conf?.sequence?.labelBoxHeight ?? 0); + + _drawTextCandidateFunc(conf, hasKatex(actor.description))( + actor.description, + actElem, + rect.x, + rect.y + r + (isFooter ? 5 : 10), + rect.width, + rect.height, + { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` }, + conf + ); + + return actor.height; +}; + +const drawActorTypeEntity = function (elem, actor, conf, isFooter) { + const actorY = isFooter ? actor.stopy : actor.starty; + const center = actor.x + actor.width / 2; + const centerY = actorY + 75; + + const line = elem.append('g').lower(); + + const actElem = elem.append('g'); + let cssClass = ACTOR_MAN_FIGURE_CLASS; + if (isFooter) { + cssClass += ` ${BOTTOM_ACTOR_CLASS}`; + } else { + cssClass += ` ${TOP_ACTOR_CLASS}`; + } + actElem.attr('class', cssClass); + actElem.attr('name', actor.name); + + const rect = svgDrawCommon.getNoteRect(); + rect.x = actor.x; + rect.y = actorY; + rect.fill = '#eaeaea'; + rect.width = actor.width; + rect.height = actor.height; + rect.class = 'actor'; + + const cx = actor.x + actor.width / 2; + const cy = actorY + (!isFooter ? 25 : 10); + const r = 18; + + actElem + .append('circle') + .attr('cx', cx) + .attr('cy', cy) + .attr('r', r) + .attr('width', actor.width) + .attr('height', actor.height); + + actElem + .append('line') + .attr('x1', cx - r) + .attr('x2', cx + r) + .attr('y1', cy + r) + .attr('y2', cy + r) + .attr('stroke', '#333') + .attr('stroke-width', 2); + + const bounds = actElem.node().getBBox(); + actor.height = bounds.height + (conf?.sequence?.labelBoxHeight ?? 0); + + if (!isFooter) { + actorCnt++; + line + .append('line') + .attr('id', 'actor' + actorCnt) + .attr('x1', center) + .attr('y1', centerY) + .attr('x2', center) + .attr('y2', 2000) + .attr('class', 'actor-line 200') + .attr('stroke-width', '0.5px') + .attr('stroke', '#999') + .attr('name', actor.name); + + actor.actorCnt = actorCnt; + } + + _drawTextCandidateFunc(conf, hasKatex(actor.description))( + actor.description, + actElem, + rect.x, + rect.y + (!isFooter ? (cy + r - actorY) / 2 : (cy - actorY + r - 5) / 2), + rect.width, + rect.height, + { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` }, + conf + ); + + if (!isFooter) { + actElem.attr('transform', `translate(${0}, ${r / 2})`); + } else { + actElem.attr('transform', `translate(${0}, ${r / 2})`); + } + + return actor.height; +}; + +const drawActorTypeDatabase = function (elem, actor, conf, isFooter) { + const actorY = isFooter ? actor.stopy : actor.starty; + const center = actor.x + actor.width / 2; + const centerY = actorY + actor.height + 2 * conf.boxTextMargin; + + const boxplusLineGroup = elem.append('g').lower(); + let g = boxplusLineGroup; + + if (!isFooter) { + actorCnt++; + if (Object.keys(actor.links || {}).length && !conf.forceMenus) { + g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer'); + } + g.append('line') + .attr('id', 'actor' + actorCnt) + .attr('x1', center) + .attr('y1', centerY) + .attr('x2', center) + .attr('y2', 2000) + .attr('class', 'actor-line 200') + .attr('stroke-width', '0.5px') + .attr('stroke', '#999') + .attr('name', actor.name); + + g = boxplusLineGroup.append('g'); + actor.actorCnt = actorCnt; + + if (actor.links != null) { + g.attr('id', 'root-' + actorCnt); + } + } + + const rect = svgDrawCommon.getNoteRect(); + + let cssclass = 'actor'; + if (actor.properties?.class) { + cssclass = actor.properties.class; + } else { + rect.fill = '#eaeaea'; + } + + if (isFooter) { + cssclass += ` ${BOTTOM_ACTOR_CLASS}`; + } else { + cssclass += ` ${TOP_ACTOR_CLASS}`; + } + + rect.x = actor.x; + rect.y = actorY; + rect.width = actor.width; + rect.height = actor.height; + rect.class = cssclass; + rect.name = actor.name; + + // Cylinder dimensions + rect.x = actor.x; + rect.y = actorY; + const w = rect.width / 4; + const h = rect.width / 4; + const rx = w / 2; + const ry = rx / (2.5 + w / 50); + + // Cylinder base group + const cylinderGroup = g.append('g'); + + const d = ` + M ${rect.x},${rect.y + ry} + a ${rx},${ry} 0 0 0 ${w},0 + a ${rx},${ry} 0 0 0 -${w},0 + l 0,${h - 2 * ry} + a ${rx},${ry} 0 0 0 ${w},0 + l 0,-${h - 2 * ry} +`; + // Draw the main cylinder body + cylinderGroup + .append('path') + .attr('d', d) + .attr('fill', '#eaeaea') + .attr('stroke', '#000') + .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})`); + } + 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.width, + rect.height, + { class: `actor ${ACTOR_BOX_CLASS}` }, + conf + ); + + const lastPath = cylinderGroup.select('path:last-child'); + if (lastPath.node()) { + const bounds = lastPath.node().getBBox(); + actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0); + } + + return actor.height; +}; + +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 line = elem.append('g').lower(); + + if (!isFooter) { + actorCnt++; + line + .append('line') + .attr('id', 'actor' + actorCnt) + .attr('x1', center) + .attr('y1', centerY) + .attr('x2', center) + .attr('y2', 2000) + .attr('class', 'actor-line 200') + .attr('stroke-width', '0.5px') + .attr('stroke', '#999') + .attr('name', actor.name); + + actor.actorCnt = actorCnt; + } + const actElem = elem.append('g'); + let cssClass = ACTOR_MAN_FIGURE_CLASS; + if (isFooter) { + cssClass += ` ${BOTTOM_ACTOR_CLASS}`; + } else { + cssClass += ` ${TOP_ACTOR_CLASS}`; + } + actElem.attr('class', cssClass); + actElem.attr('name', actor.name); + + const rect = svgDrawCommon.getNoteRect(); + rect.x = actor.x; + rect.y = actorY; + rect.fill = '#eaeaea'; + rect.width = actor.width; + rect.height = actor.height; + rect.class = 'actor'; + + actElem + .append('line') + .attr('id', 'actor-man-torso' + actorCnt) + .attr('x1', actor.x + actor.width / 2 - radius * 2.5) + .attr('y1', actorY + 10) + .attr('x2', actor.x + actor.width / 2 - 15) + .attr('y2', actorY + 10); + + 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('x2', actor.x + actor.width / 2 - radius * 2.5) + .attr('y2', actorY + 20); // ending Y (26px long, adjust as needed) + + actElem + .append('circle') + .attr('cx', actor.x + actor.width / 2) + .attr('cy', actorY + 10) + .attr('r', radius); + + const bounds = actElem.node().getBBox(); + actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0); + + _drawTextCandidateFunc(conf, hasKatex(actor.description))( + actor.description, + actElem, + rect.x, + rect.y + (!isFooter ? radius / 2 + 3 : radius / 2 - 4), + rect.width, + rect.height, + { class: `actor ${ACTOR_MAN_FIGURE_CLASS}` }, + conf + ); + + if (!isFooter) { + actElem.attr('transform', `translate(0,${radius / 2 + 7})`); + } else { + actElem.attr('transform', `translate(0,${radius / 2 + 7})`); + } + + return actor.height; +}; + const drawActorTypeActor = function (elem, actor, conf, isFooter) { const actorY = isFooter ? actor.stopy : actor.starty; const center = actor.x + actor.width / 2; @@ -516,6 +1110,18 @@ export const drawActor = async function (elem, actor, conf, isFooter) { return await drawActorTypeActor(elem, actor, conf, isFooter); case 'participant': return await drawActorTypeParticipant(elem, actor, conf, isFooter); + case 'boundary': + return await drawActorTypeBoundary(elem, actor, conf, isFooter); + case 'control': + return await drawActorTypeControl(elem, actor, conf, isFooter); + case 'entity': + return await drawActorTypeEntity(elem, actor, conf, isFooter); + case 'database': + return await drawActorTypeDatabase(elem, actor, conf, isFooter); + case 'collections': + return await drawActorTypeCollections(elem, actor, conf, isFooter); + case 'queue': + return await drawActorTypeQueue(elem, actor, conf, isFooter); } }; diff --git a/packages/mermaid/src/docs/.vitepress/config.ts b/packages/mermaid/src/docs/.vitepress/config.ts index 1c41e7cba..066fde693 100644 --- a/packages/mermaid/src/docs/.vitepress/config.ts +++ b/packages/mermaid/src/docs/.vitepress/config.ts @@ -203,6 +203,7 @@ function sidebarConfig() { { text: 'Accessibility', link: '/config/accessibility' }, { text: 'Mermaid CLI', link: '/config/mermaidCLI' }, { text: 'FAQ', link: '/config/faq' }, + { text: 'Layouts', link: '/config/layouts' }, ], }, ]; diff --git a/packages/mermaid/src/docs/config/faq.md b/packages/mermaid/src/docs/config/faq.md index 6d1261fc1..4acf0c3d3 100644 --- a/packages/mermaid/src/docs/config/faq.md +++ b/packages/mermaid/src/docs/config/faq.md @@ -1,6 +1,6 @@ # Frequently Asked Questions -1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217) +1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/1433#issuecomment-1991554712) 1. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785) 1. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621) 1. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136) diff --git a/packages/mermaid/src/docs/config/layouts.md b/packages/mermaid/src/docs/config/layouts.md new file mode 100644 index 000000000..56f5072f6 --- /dev/null +++ b/packages/mermaid/src/docs/config/layouts.md @@ -0,0 +1,24 @@ +# Layouts + +This page lists the available layout algorithms supported in Mermaid diagrams. + +## Supported Layouts + +- **elk**: [ELK (Eclipse Layout Kernel)](https://www.eclipse.org/elk/) +- **tidy-tree**: Tidy tree layout for hierarchical diagrams [Tidy Tree Configuration](/config/tidy-tree) +- **cose-bilkent**: Cose Bilkent layout for force-directed graphs +- **dagre**: Dagre layout for layered graphs + +## How to Use + +You can specify the layout in your diagram's YAML config or initialization options. For example: + +```mermaid +--- +config: + layout: elk +--- +graph TD; + A-->B; + B-->C; +``` diff --git a/packages/mermaid/src/docs/config/tidy-tree.md b/packages/mermaid/src/docs/config/tidy-tree.md new file mode 100644 index 000000000..f98d36379 --- /dev/null +++ b/packages/mermaid/src/docs/config/tidy-tree.md @@ -0,0 +1,49 @@ +# Tidy-tree Layout + +The **tidy-tree** layout arranges nodes in a hierarchical, tree-like structure. It is especially useful for diagrams where parent-child relationships are important, such as mindmaps. + +## Features + +- Organizes nodes in a tidy, non-overlapping tree +- Ideal for mindmaps and hierarchical data +- Automatically adjusts spacing for readability + +## Example Usage + +```mermaid-example +--- +config: + layout: tidy-tree +--- +mindmap +root((mindmap is a long thing)) + A + B + C + D +``` + +```mermaid-example +--- +config: + layout: tidy-tree +--- +mindmap +root((mindmap)) + Origins + Long history + ::icon(fa fa-book) + Popularisation + British popular psychology author Tony Buzan + Research + On effectiveness
and features + On Automatic creation + Uses + Creative techniques + Strategic planning + Argument mapping +``` + +## Note + +- Currently, tidy-tree is primarily supported for mindmap diagrams. diff --git a/packages/mermaid/src/docs/package.json b/packages/mermaid/src/docs/package.json index 6ec43eb93..b5b95e7c0 100644 --- a/packages/mermaid/src/docs/package.json +++ b/packages/mermaid/src/docs/package.json @@ -31,7 +31,7 @@ "fast-glob": "^3.3.3", "https-localhost": "^4.7.1", "pathe": "^2.0.3", - "unocss": "^66.0.0", + "unocss": "^66.4.2", "unplugin-vue-components": "^28.4.0", "vite": "^6.1.1", "vite-plugin-pwa": "^1.0.0", diff --git a/packages/mermaid/src/docs/syntax/flowchart.md b/packages/mermaid/src/docs/syntax/flowchart.md index a19dcff21..341143c47 100644 --- a/packages/mermaid/src/docs/syntax/flowchart.md +++ b/packages/mermaid/src/docs/syntax/flowchart.md @@ -590,11 +590,17 @@ flowchart TD - `b` - **w**: The width of the image. If not defined, this will default to the natural width of the image. - **h**: The height of the image. If not defined, this will default to the natural height of the image. -- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are: +- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the width (`w`) accordingly to the height (`h`). If not defined, this will default to `off` Possible values are: - `on` - `off` -These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging. +If you want to resize an image, but keep the same aspect ratio, set `h`, and set `constraint: on` to constrain the aspect ratio. E.g. + +```mermaid +flowchart TD + %% My image with a constrained aspect ratio + A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" } +``` ## Links between nodes diff --git a/packages/mermaid/src/docs/syntax/mindmap.md b/packages/mermaid/src/docs/syntax/mindmap.md index 3dfbed2f6..41d736798 100644 --- a/packages/mermaid/src/docs/syntax/mindmap.md +++ b/packages/mermaid/src/docs/syntax/mindmap.md @@ -209,3 +209,22 @@ You can also refer the [implementation in the live editor](https://github.com/me cspell:locale en,en-gb cspell:ignore Buzan ---> + +## Layouts + +Mermaid also supports a Tidy Tree layout for mindmaps. + +``` +--- +config: + layout: tidy-tree +--- +mindmap +root((mindmap is a long thing)) + A + B + C + D +``` + +Instructions to add and register tidy-tree layout are present in [Tidy Tree Configuration](/config/tidy-tree) diff --git a/packages/mermaid/src/docs/syntax/sequenceDiagram.md b/packages/mermaid/src/docs/syntax/sequenceDiagram.md index 3087eb743..6e0ac87bf 100644 --- a/packages/mermaid/src/docs/syntax/sequenceDiagram.md +++ b/packages/mermaid/src/docs/syntax/sequenceDiagram.md @@ -46,6 +46,78 @@ sequenceDiagram Bob->>Alice: Hi Alice ``` +### Boundary + +If you want to use the boundary symbol for a participant, use the JSON configuration syntax as shown below. + +```mermaid-example +sequenceDiagram + participant Alice@{ "type" : "boundary" } + participant Bob + Alice->>Bob: Request from boundary + Bob->>Alice: Response to boundary +``` + +### Control + +If you want to use the control symbol for a participant, use the JSON configuration syntax as shown below. + +```mermaid-example +sequenceDiagram + participant Alice@{ "type" : "control" } + participant Bob + Alice->>Bob: Control request + Bob->>Alice: Control response +``` + +### Entity + +If you want to use the entity symbol for a participant, use the JSON configuration syntax as shown below. + +```mermaid-example +sequenceDiagram + participant Alice@{ "type" : "entity" } + participant Bob + Alice->>Bob: Entity request + Bob->>Alice: Entity response +``` + +### Database + +If you want to use the database symbol for a participant, use the JSON configuration syntax as shown below. + +```mermaid-example +sequenceDiagram + participant Alice@{ "type" : "database" } + participant Bob + Alice->>Bob: DB query + Bob->>Alice: DB result +``` + +### Collections + +If you want to use the collections symbol for a participant, use the JSON configuration syntax as shown below. + +```mermaid-example +sequenceDiagram + participant Alice@{ "type" : "collections" } + participant Bob + Alice->>Bob: Collections request + Bob->>Alice: Collections response +``` + +### Queue + +If you want to use the queue symbol for a participant, use the JSON configuration syntax as shown below. + +```mermaid-example +sequenceDiagram + participant Alice@{ "type" : "queue" } + participant Bob + Alice->>Bob: Queue message + Bob->>Alice: Queue response +``` + ### Aliases The actor can have a convenient identifier and a descriptive label. diff --git a/packages/mermaid/src/docs/syntax/xyChart.md b/packages/mermaid/src/docs/syntax/xyChart.md index 4154fb2f0..cfff201d3 100644 --- a/packages/mermaid/src/docs/syntax/xyChart.md +++ b/packages/mermaid/src/docs/syntax/xyChart.md @@ -126,7 +126,7 @@ xychart ## Chart Theme Variables -Themes for xychart resides inside xychart attribute so to set the variables use this syntax: +Themes for xychart reside inside the `xychart` attribute, allowing customization through the following syntax: ```yaml --- @@ -151,6 +151,31 @@ config: | yAxisLineColor | Color of the y-axis line | | plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" | +### Setting Colors for Lines and Bars + +To set the color for lines and bars, use the `plotColorPalette` parameter. Colors in the palette will correspond sequentially to the elements in your chart (e.g., first bar/line will use the first color specified in the palette). + +```mermaid-example +--- +config: + themeVariables: + xyChart: + plotColorPalette: '#000000, #0000FF, #00FF00, #FF0000' +--- +xychart +title "Different Colors in xyChart" +x-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"] +y-axis "valuesY" 0 --> 50 +%% Black line +line [10,20,30,40] +%% Blue bar +bar [20,30,25,35] +%% Green bar +bar [15,25,20,30] +%% Red line +line [5,15,25,35] +``` + ## Example on config and theme ```mermaid-example diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index b2d2d3cd3..ff794abb1 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -41,7 +41,6 @@ import { decodeEntities, encodeEntities } from './utils.js'; import { toBase64 } from './utils/base64.js'; import { StateDB } from './diagrams/state/stateDb.js'; import { ensureNodeFromSelector, jsdomIt } from './tests/util.js'; -import { select } from 'd3'; import { JSDOM } from 'jsdom'; /** @@ -50,7 +49,6 @@ import { JSDOM } from 'jsdom'; */ // ------------------------------------------------------------------------------------- - describe('mermaidAPI', () => { describe('encodeEntities', () => { it('removes the ending ; from style [text1]:[optional word]#[text2]; with ', () => { @@ -913,4 +911,241 @@ graph TD;A--x|text including URL space|B;`) expect(sequenceDiagram1.db.getActors()).not.toEqual(sequenceDiagram2.db.getActors()); }); }); + + describe('mermaidAPI config precedence', () => { + const id = 'mermaid-config-test'; + + beforeEach(() => { + mermaidAPI.globalReset(); + }); + + jsdomIt('renders with YAML config taking precedence over initialize config', async () => { + mermaid.initialize({ + theme: 'forest', + fontFamily: 'Arial', + themeVariables: { fontFamily: 'Arial', fontSize: '16px' }, + flowchart: { htmlLabels: false }, + }); + + const diagramText = `--- +config: + theme: base + fontFamily: Courier + themeVariables: + fontFamily: "Courier New" + fontSize: "20px" + flowchart: + htmlLabels: true +--- +flowchart TD + A --> B +`; + + const { svg } = await mermaidAPI.render('yaml-over-init', diagramText); + + const config = mermaidAPI.getConfig(); + expect(config.theme).toBe('base'); + expect(config.fontFamily).toBe('Courier'); + expect(config.themeVariables.fontFamily).toBe('Courier New'); + expect(config.themeVariables.fontSize).toBe('20px'); + expect(config.flowchart?.htmlLabels).toBe(true); + + const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document); + expect(svgNode).not.toBeNull(); + }); + + jsdomIt( + 'renders with YAML themeVariables fully overriding initialize themeVariables', + async () => { + mermaid.initialize({ + themeVariables: { fontFamily: 'Arial', fontSize: '16px' }, + }); + + const diagramText = `--- +config: + themeVariables: + fontFamily: "Courier New" + fontSize: "20px" +--- +flowchart TD + A --> B +`; + + const { svg } = await mermaidAPI.render(id, diagramText); + const config = mermaidAPI.getConfig(); + + expect(config.themeVariables.fontFamily).toBe('Courier New'); + expect(config.themeVariables.fontSize).toBe('20px'); + expect(config.themeVariables.fontFamily).not.toBe('Arial'); + expect(config.themeVariables.fontSize).not.toBe('16px'); + + const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document); + expect(svgNode).not.toBeNull(); + } + ); + + jsdomIt( + 'renders with YAML themeVariables overriding only provided keys and keeping others from initialize', + async () => { + mermaid.initialize({ + theme: 'forest', + fontFamily: 'Arial', + themeVariables: { fontFamily: 'Arial', fontSize: '16px', colorPrimary: '#ff0000' }, + }); + + const diagramText = `--- +config: + themeVariables: + fontFamily: "Courier New" +--- +flowchart TD + A --> B +`; + + const { svg } = await mermaidAPI.render(id, diagramText); + + const config = mermaidAPI.getConfig(); + expect(config.themeVariables.fontFamily).toBe('Courier New'); + expect(config.themeVariables.fontSize).toBe('16px'); + expect(config.themeVariables.colorPrimary).toBe('#ff0000'); + + const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document); + expect(svgNode).not.toBeNull(); + } + ); + + jsdomIt( + 'renders with YAML config (no themeVariables) and falls back to initialize themeVariables', + async () => { + mermaid.initialize({ + themeVariables: { fontFamily: 'Arial', fontSize: '16px' }, + }); + + const diagramText = `--- +config: + theme: base +--- +flowchart TD + A --> B +`; + + const { svg } = await mermaidAPI.render(id, diagramText); + + const config = mermaidAPI.getConfig(); + expect(config.themeVariables.fontFamily).toBe('Arial'); + expect(config.themeVariables.fontSize).toBe('16px'); + expect(config.theme).toBe('base'); + + const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document); + expect(svgNode).not.toBeNull(); + } + ); + + jsdomIt( + 'renders with full YAML config block taking full precedence over initialize config', + async () => { + mermaid.initialize({ + theme: 'forest', + fontFamily: 'Arial', + themeVariables: { fontFamily: 'Arial', fontSize: '16px' }, + flowchart: { htmlLabels: false }, + }); + + const diagramText = `--- +config: + theme: base + fontFamily: Courier + themeVariables: + fontFamily: "Courier New" + fontSize: "20px" + flowchart: + htmlLabels: true +--- +flowchart TD + A --> B +`; + + const { svg } = await mermaidAPI.render('yaml-over-init', diagramText); + + const config = mermaidAPI.getConfig(); + expect(config.theme).toBe('base'); + expect(config.fontFamily).toBe('Courier'); + expect(config.themeVariables.fontFamily).toBe('Courier New'); + expect(config.themeVariables.fontSize).toBe('20px'); + expect(config.flowchart?.htmlLabels).toBe(true); + + const svgNode = ensureNodeFromSelector('svg', new JSDOM(svg).window.document); + expect(svgNode).not.toBeNull(); + } + ); + + jsdomIt( + 'renders with YAML config (no themeVariables) and falls back to initialize themeVariables (duplicate scenario)', + async () => { + mermaid.initialize({ + themeVariables: { fontFamily: 'Arial', fontSize: '16px' }, + }); + + const diagramText = `--- +config: + theme: base +--- +flowchart TD + A --> B +`; + + await mermaidAPI.render(id, diagramText); + + const config = mermaidAPI.getConfig(); + expect(config.themeVariables.fontFamily).toBe('Arial'); + expect(config.themeVariables.fontSize).toBe('16px'); + expect(config.theme).toBe('base'); + } + ); + + jsdomIt('renders with no YAML config so initialize config is fully applied', async () => { + mermaid.initialize({ + theme: 'forest', + fontFamily: 'Arial', + themeVariables: { fontFamily: 'Arial', fontSize: '16px' }, + }); + + const diagramText = ` +flowchart TD + A --> B +`; + + await mermaidAPI.render(id, diagramText); + + const config = mermaidAPI.getConfig(); + expect(config.theme).toBe('forest'); + expect(config.fontFamily).toBe('Arial'); + expect(config.themeVariables.fontFamily).toBe('Arial'); + expect(config.themeVariables.fontSize).toBe('16px'); + }); + + jsdomIt( + 'renders with empty YAML config block and falls back to initialize config', + async () => { + mermaid.initialize({ + theme: 'dark', + themeVariables: { fontFamily: 'Times', fontSize: '14px' }, + }); + + const diagramText = `--- +config: {} +--- +flowchart TD + A --> B +`; + + await mermaidAPI.render(id, diagramText); + + const config = mermaidAPI.getConfig(); + expect(config.theme).toBe('dark'); + expect(config.themeVariables.fontFamily).toBe('Times'); + expect(config.themeVariables.fontSize).toBe('14px'); + } + ); + }); }); diff --git a/packages/mermaid/src/preprocess.ts b/packages/mermaid/src/preprocess.ts index a62326070..2334ff0b1 100644 --- a/packages/mermaid/src/preprocess.ts +++ b/packages/mermaid/src/preprocess.ts @@ -26,6 +26,7 @@ const processFrontmatter = (code: string) => { } config.gantt.displayMode = displayMode; } + return { title, config, text }; }; diff --git a/packages/mermaid/src/rendering-util/createGraph.ts b/packages/mermaid/src/rendering-util/createGraph.ts new file mode 100644 index 000000000..b08a3aae0 --- /dev/null +++ b/packages/mermaid/src/rendering-util/createGraph.ts @@ -0,0 +1,148 @@ +import { insertNode } from './rendering-elements/nodes.js'; +import type { LayoutData, NonClusterNode } from './types.ts'; +import type { Selection } from 'd3'; +import { getConfig } from '../diagram-api/diagramAPI.js'; +import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; + +// Update type: +type D3Selection = Selection< + T, + unknown, + Element | null, + unknown +>; + +/** + * Creates a graph by merging the graph construction and DOM element insertion. + * + * This function creates the graph, inserts the SVG groups (clusters, edgePaths, edgeLabels, nodes) + * into the provided element, and uses `insertNode` to add nodes to the diagram. Node dimensions + * are computed using each node's bounding box. + * + * @param element - The D3 selection in which the SVG groups are inserted. + * @param data4Layout - The layout data containing nodes and edges. + * @returns A promise resolving to an object containing the Graphology graph and the inserted groups. + */ +export async function createGraphWithElements( + element: D3Selection, + data4Layout: LayoutData +): Promise<{ + graph: graphlib.Graph; + groups: { + clusters: D3Selection; + edgePaths: D3Selection; + edgeLabels: D3Selection; + nodes: D3Selection; + rootGroups: D3Selection; + }; + nodeElements: Map>; +}> { + // Create a directed, multi graph. + const graph = new graphlib.Graph({ + multigraph: true, + compound: true, + }); + const edgesToProcess = [...data4Layout.edges]; + const config = getConfig(); + // Create groups for clusters, edge paths, edge labels, and nodes. + const clusters = element.insert('g').attr('class', 'clusters'); + const edgePaths = element.insert('g').attr('class', 'edges edgePath'); + const edgeLabels = element.insert('g').attr('class', 'edgeLabels'); + const nodesGroup = element.insert('g').attr('class', 'nodes'); + const rootGroups = element.insert('g').attr('class', 'root'); + + const nodeElements = new Map>(); + + // Insert nodes into the DOM and add them to the graph. + await Promise.all( + data4Layout.nodes.map(async (node) => { + if (node.isGroup) { + graph.setNode(node.id, { ...node }); + } else { + const childNodeEl = await insertNode(nodesGroup, node, { config, dir: node.dir }); + const boundingBox = childNodeEl.node()?.getBBox() ?? { width: 0, height: 0 }; + nodeElements.set(node.id, childNodeEl as D3Selection); + node.width = boundingBox.width; + node.height = boundingBox.height; + graph.setNode(node.id, { ...node }); + } + }) + ); + // Add edges to the graph. + for (const edge of edgesToProcess) { + if (edge.label && edge.label?.length > 0) { + // Create a label node for the edge + const labelNodeId = `edge-label-${edge.start}-${edge.end}-${edge.id}`; + const labelNode = { + id: labelNodeId, + label: edge.label, + edgeStart: edge.start, + edgeEnd: edge.end, + shape: 'labelRect', + width: 0, // Will be updated after insertion + height: 0, // Will be updated after insertion + isEdgeLabel: true, + isDummy: true, + isGroup: false, + parentId: edge.parentId, + ...(edge.dir ? { dir: edge.dir } : {}), + } as NonClusterNode; + + // Insert the label node into the DOM + const labelNodeEl = await insertNode(nodesGroup, labelNode, { config, dir: edge.dir }); + const boundingBox = labelNodeEl.node()?.getBBox() ?? { width: 0, height: 0 }; + + // Update node dimensions + labelNode.width = boundingBox.width; + labelNode.height = boundingBox.height; + + // Add to graph and tracking maps + graph.setNode(labelNodeId, { ...labelNode }); + nodeElements.set(labelNodeId, labelNodeEl as D3Selection); + data4Layout.nodes.push(labelNode); + + // Create two edges to replace the original one + const edgeToLabel = { + ...edge, + id: `${edge.id}-to-label`, + end: labelNodeId, + label: undefined, + isLabelEdge: true, + arrowTypeEnd: 'none', + arrowTypeStart: 'none', + }; + const edgeFromLabel = { + ...edge, + id: `${edge.id}-from-label`, + start: labelNodeId, + end: edge.end, + label: undefined, + isLabelEdge: true, + arrowTypeStart: 'none', + arrowTypeEnd: 'arrow_point', + }; + graph.setEdge(edgeToLabel.id, edgeToLabel.start, edgeToLabel.end, { ...edgeToLabel }); + graph.setEdge(edgeFromLabel.id, edgeFromLabel.start, edgeFromLabel.end, { ...edgeFromLabel }); + data4Layout.edges.push(edgeToLabel, edgeFromLabel); + const edgeIdToRemove = edge.id; + data4Layout.edges = data4Layout.edges.filter((edge) => edge.id !== edgeIdToRemove); + const indexInOriginal = data4Layout.edges.findIndex((e) => e.id === edge.id); + if (indexInOriginal !== -1) { + data4Layout.edges.splice(indexInOriginal, 1); + } + } else { + // Regular edge without label + graph.setEdge(edge.id, edge.start, edge.end, { ...edge }); + const edgeExists = data4Layout.edges.some((existingEdge) => existingEdge.id === edge.id); + if (!edgeExists) { + data4Layout.edges.push(edge); + } + } + } + + return { + graph, + groups: { clusters, edgePaths, edgeLabels, nodes: nodesGroup, rootGroups }, + nodeElements, + }; +} diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/cytoscape-setup.test.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/cytoscape-setup.test.ts new file mode 100644 index 000000000..707b031f4 --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/cytoscape-setup.test.ts @@ -0,0 +1,265 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { + addNodes, + addEdges, + extractPositionedNodes, + extractPositionedEdges, +} from './cytoscape-setup.js'; +import type { Node, Edge } from '../../types.js'; + +// Mock cytoscape +const mockCy = { + add: vi.fn(), + nodes: vi.fn(), + edges: vi.fn(), +}; + +vi.mock('cytoscape', () => { + const mockCytoscape = vi.fn(() => mockCy) as any; + mockCytoscape.use = vi.fn(); + return { + default: mockCytoscape, + }; +}); + +describe('Cytoscape Setup', () => { + let mockNodes: Node[]; + let mockEdges: Edge[]; + + beforeEach(() => { + vi.clearAllMocks(); + + mockNodes = [ + { + id: '1', + label: 'Root', + isGroup: false, + shape: 'rect', + width: 100, + height: 50, + padding: 10, + x: 100, + y: 100, + cssClasses: '', + cssStyles: [], + look: 'default', + }, + { + id: '2', + label: 'Child 1', + isGroup: false, + shape: 'rect', + width: 80, + height: 40, + padding: 10, + x: 150, + y: 150, + cssClasses: '', + cssStyles: [], + look: 'default', + }, + ]; + + mockEdges = [ + { + id: '1_2', + start: '1', + end: '2', + type: 'edge', + classes: '', + style: [], + animate: false, + arrowTypeEnd: 'arrow_point', + arrowTypeStart: 'none', + }, + ]; + }); + + describe('addNodes', () => { + it('should add nodes to cytoscape', () => { + addNodes([mockNodes[0]], mockCy as unknown as any); + + expect(mockCy.add).toHaveBeenCalledWith({ + group: 'nodes', + data: { + id: '1', + labelText: 'Root', + height: 50, + width: 100, + padding: 10, + isGroup: false, + shape: 'rect', + cssClasses: '', + cssStyles: [], + look: 'default', + }, + position: { + x: 100, + y: 100, + }, + }); + }); + + it('should add multiple nodes to cytoscape', () => { + addNodes(mockNodes, mockCy as unknown as any); + + expect(mockCy.add).toHaveBeenCalledTimes(2); + + expect(mockCy.add).toHaveBeenCalledWith({ + group: 'nodes', + data: { + id: '1', + labelText: 'Root', + height: 50, + width: 100, + padding: 10, + isGroup: false, + shape: 'rect', + cssClasses: '', + cssStyles: [], + look: 'default', + }, + position: { + x: 100, + y: 100, + }, + }); + + expect(mockCy.add).toHaveBeenCalledWith({ + group: 'nodes', + data: { + id: '2', + labelText: 'Child 1', + height: 40, + width: 80, + padding: 10, + isGroup: false, + shape: 'rect', + cssClasses: '', + cssStyles: [], + look: 'default', + }, + position: { + x: 150, + y: 150, + }, + }); + }); + }); + + describe('addEdges', () => { + it('should add edges to cytoscape', () => { + addEdges(mockEdges, mockCy as unknown as any); + + expect(mockCy.add).toHaveBeenCalledWith({ + group: 'edges', + data: { + id: '1_2', + source: '1', + target: '2', + type: 'edge', + classes: '', + style: [], + animate: false, + arrowTypeEnd: 'arrow_point', + arrowTypeStart: 'none', + }, + }); + }); + }); + + describe('extractPositionedNodes', () => { + it('should extract positioned nodes from cytoscape', () => { + const mockCytoscapeNodes = [ + { + data: () => ({ + id: '1', + labelText: 'Root', + width: 100, + height: 50, + padding: 10, + isGroup: false, + shape: 'rect', + }), + position: () => ({ x: 100, y: 100 }), + }, + { + data: () => ({ + id: '2', + labelText: 'Child 1', + width: 80, + height: 40, + padding: 10, + isGroup: false, + shape: 'rect', + }), + position: () => ({ x: 150, y: 150 }), + }, + ]; + + mockCy.nodes.mockReturnValue({ + map: (fn: unknown) => mockCytoscapeNodes.map(fn as any), + }); + + const result = extractPositionedNodes(mockCy as unknown as any); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + id: '1', + x: 100, + y: 100, + labelText: 'Root', + width: 100, + height: 50, + padding: 10, + isGroup: false, + shape: 'rect', + }); + }); + }); + + describe('extractPositionedEdges', () => { + it('should extract positioned edges from cytoscape', () => { + const mockCytoscapeEdges = [ + { + data: () => ({ + id: '1_2', + source: '1', + target: '2', + type: 'edge', + }), + _private: { + rscratch: { + startX: 100, + startY: 100, + midX: 125, + midY: 125, + endX: 150, + endY: 150, + }, + }, + }, + ]; + + mockCy.edges.mockReturnValue({ + map: (fn: unknown) => mockCytoscapeEdges.map(fn as any), + }); + + const result = extractPositionedEdges(mockCy as unknown as any); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + id: '1_2', + source: '1', + target: '2', + type: 'edge', + startX: 100, + startY: 100, + midX: 125, + midY: 125, + endX: 150, + endY: 150, + }); + }); + }); +}); diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/cytoscape-setup.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/cytoscape-setup.ts new file mode 100644 index 000000000..8fb9b2599 --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/cytoscape-setup.ts @@ -0,0 +1,207 @@ +import cytoscape from 'cytoscape'; +import coseBilkent from 'cytoscape-cose-bilkent'; +import { select } from 'd3'; +import { log } from '../../../logger.js'; +import type { LayoutData, Node, Edge } from '../../types.js'; +import type { CytoscapeLayoutConfig, PositionedNode, PositionedEdge } from './types.js'; + +// Inject the layout algorithm into cytoscape +cytoscape.use(coseBilkent); + +/** + * Declare module augmentation for cytoscape edge types + */ +declare module 'cytoscape' { + interface EdgeSingular { + _private: { + bodyBounds: unknown; + rscratch: { + startX: number; + startY: number; + midX: number; + midY: number; + endX: number; + endY: number; + }; + }; + } +} + +/** + * Add nodes to cytoscape instance from provided node array + * This function processes only the nodes provided in the data structure + * @param nodes - Array of nodes to add + * @param cy - The cytoscape instance + */ +export function addNodes(nodes: Node[], cy: cytoscape.Core): void { + nodes.forEach((node) => { + const nodeData: Record = { + id: node.id, + labelText: node.label, + height: node.height, + width: node.width, + padding: node.padding ?? 0, + }; + + // Add any additional properties from the node + Object.keys(node).forEach((key) => { + if (!['id', 'label', 'height', 'width', 'padding', 'x', 'y'].includes(key)) { + nodeData[key] = (node as unknown as Record)[key]; + } + }); + + cy.add({ + group: 'nodes', + data: nodeData, + position: { + x: node.x ?? 0, + y: node.y ?? 0, + }, + }); + }); +} + +/** + * Add edges to cytoscape instance from provided edge array + * This function processes only the edges provided in the data structure + * @param edges - Array of edges to add + * @param cy - The cytoscape instance + */ +export function addEdges(edges: Edge[], cy: cytoscape.Core): void { + edges.forEach((edge) => { + const edgeData: Record = { + id: edge.id, + source: edge.start, + target: edge.end, + }; + + // Add any additional properties from the edge + Object.keys(edge).forEach((key) => { + if (!['id', 'start', 'end'].includes(key)) { + edgeData[key] = (edge as unknown as Record)[key]; + } + }); + + cy.add({ + group: 'edges', + data: edgeData, + }); + }); +} + +/** + * Create and configure cytoscape instance + * @param data - Layout data containing nodes and edges + * @returns Promise resolving to configured cytoscape instance + */ +export function createCytoscapeInstance(data: LayoutData): Promise { + return new Promise((resolve) => { + // Add temporary render element + const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none'); + + const cy = cytoscape({ + container: document.getElementById('cy'), // container to render in + style: [ + { + selector: 'edge', + style: { + 'curve-style': 'bezier', + }, + }, + ], + }); + + // Remove element after layout + renderEl.remove(); + + // Add all nodes and edges to cytoscape using the generic functions + addNodes(data.nodes, cy); + addEdges(data.edges, cy); + + // Make cytoscape care about the dimensions of the nodes + cy.nodes().forEach(function (n) { + n.layoutDimensions = () => { + const nodeData = n.data(); + return { w: nodeData.width, h: nodeData.height }; + }; + }); + + // Configure and run the cose-bilkent layout + const layoutConfig: CytoscapeLayoutConfig = { + name: 'cose-bilkent', + // @ts-ignore Types for cose-bilkent are not correct? + quality: 'proof', + styleEnabled: false, + animate: false, + }; + + cy.layout(layoutConfig).run(); + + cy.ready((e) => { + log.info('Cytoscape ready', e); + resolve(cy); + }); + }); +} + +/** + * Extract positioned nodes from cytoscape instance + * @param cy - The cytoscape instance after layout + * @returns Array of positioned nodes + */ +export function extractPositionedNodes(cy: cytoscape.Core): PositionedNode[] { + return cy.nodes().map((node) => { + const data = node.data(); + const position = node.position(); + + // Create a positioned node with all original data plus position + const positionedNode: PositionedNode = { + id: data.id, + x: position.x, + y: position.y, + }; + + // Add all other properties from the original data + Object.keys(data).forEach((key) => { + if (key !== 'id') { + positionedNode[key] = data[key]; + } + }); + + return positionedNode; + }); +} + +/** + * Extract positioned edges from cytoscape instance + * @param cy - The cytoscape instance after layout + * @returns Array of positioned edges + */ +export function extractPositionedEdges(cy: cytoscape.Core): PositionedEdge[] { + return cy.edges().map((edge) => { + const data = edge.data(); + const rscratch = edge._private.rscratch; + + // Create a positioned edge with all original data plus position + const positionedEdge: PositionedEdge = { + id: data.id, + source: data.source, + target: data.target, + startX: rscratch.startX, + startY: rscratch.startY, + midX: rscratch.midX, + midY: rscratch.midY, + endX: rscratch.endX, + endY: rscratch.endY, + }; + + // Add all other properties from the original data + Object.keys(data).forEach((key) => { + if (!['id', 'source', 'target'].includes(key)) { + positionedEdge[key] = data[key]; + } + }); + + return positionedEdge; + }); +} diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts new file mode 100644 index 000000000..9e12d38a7 --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts @@ -0,0 +1,25 @@ +import { render as renderWithCoseBilkent } from './render.js'; + +/** + * Cose-Bilkent Layout Algorithm for Generic Diagrams + * + * This module provides a layout algorithm implementation using Cytoscape + * with the cose-bilkent algorithm for positioning nodes and edges. + * + * The algorithm follows the unified rendering pattern and can be used + * by any diagram type that provides compatible LayoutData. + */ + +/** + * Render function for the cose-bilkent layout algorithm + * + * This function follows the unified rendering pattern used by all layout algorithms. + * It takes LayoutData, inserts nodes into DOM, runs the cose-bilkent layout algorithm, + * and renders the positioned elements to the SVG. + * + * @param layoutData - Layout data containing nodes, edges, and configuration + * @param svg - SVG element to render to + * @param helpers - Internal helper functions for rendering + * @param options - Rendering options + */ +export const render = renderWithCoseBilkent; diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/layout.test.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/layout.test.ts new file mode 100644 index 000000000..f5650d3e8 --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/layout.test.ts @@ -0,0 +1,236 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { validateLayoutData, executeCoseBilkentLayout } from './layout.js'; +import type { LayoutResult } from './types.js'; +import type { MindmapNode } from '../../../diagrams/mindmap/mindmapTypes.js'; +import type { MermaidConfig } from '../../../config.type.js'; +import type { LayoutData } from '../../types.js'; + +// Mock cytoscape and cytoscape-cose-bilkent before importing the modules + +vi.mock('cytoscape', () => { + const mockCy = { + add: vi.fn(), + nodes: vi.fn(() => ({ + forEach: vi.fn(), + map: vi.fn((fn) => [ + fn({ + data: () => ({ + id: '1', + nodeId: '1', + labelText: 'Root', + level: 0, + type: 0, + width: 100, + height: 50, + padding: 10, + }), + position: () => ({ x: 100, y: 100 }), + }), + ]), + })), + edges: vi.fn(() => ({ + map: vi.fn((fn) => [ + fn({ + data: () => ({ + id: '1_2', + source: '1', + target: '2', + depth: 0, + }), + _private: { + rscratch: { + startX: 100, + startY: 100, + midX: 150, + midY: 150, + endX: 200, + endY: 200, + }, + }, + }), + ]), + })), + layout: vi.fn(() => ({ + run: vi.fn(), + })), + ready: vi.fn((callback) => callback({})), + }; + + const mockCytoscape = vi.fn(() => mockCy); + (mockCytoscape as any).use = vi.fn(); + + return { + default: mockCytoscape, + }; +}); + +describe('Cose-Bilkent Layout Algorithm', () => { + let mockConfig: MermaidConfig; + let mockRootNode: MindmapNode; + let mockLayoutData: LayoutData; + + beforeEach(() => { + mockConfig = { + mindmap: { + layoutAlgorithm: 'cose-bilkent', + padding: 10, + maxNodeWidth: 200, + useMaxWidth: true, + }, + } as MermaidConfig; + + mockRootNode = { + id: 1, + nodeId: '1', + level: 0, + descr: 'Root', + type: 0, + width: 100, + height: 50, + padding: 10, + x: 0, + y: 0, + children: [ + { + id: 2, + nodeId: '2', + level: 1, + descr: 'Child 1', + type: 0, + width: 80, + height: 40, + padding: 10, + x: 0, + y: 0, + }, + ], + } as MindmapNode; + + mockLayoutData = { + nodes: [ + { + id: '1', + nodeId: '1', + level: 0, + descr: 'Root', + type: 0, + width: 100, + height: 50, + padding: 10, + isGroup: false, + }, + { + id: '2', + nodeId: '2', + level: 1, + descr: 'Child 1', + type: 0, + width: 80, + height: 40, + padding: 10, + isGroup: false, + }, + ], + edges: [ + { + id: '1_2', + source: '1', + target: '2', + depth: 0, + }, + ], + config: mockConfig, + rootNode: mockRootNode, + }; + }); + + describe('validateLayoutData', () => { + it('should validate correct layout data', () => { + expect(() => validateLayoutData(mockLayoutData)).not.toThrow(); + }); + + it('should throw error for missing data', () => { + expect(() => validateLayoutData(null as any)).toThrow('Layout data is required'); + }); + + it('should throw error for missing root node', () => { + const invalidData = { ...mockLayoutData, rootNode: null as any }; + expect(() => validateLayoutData(invalidData)).toThrow('Root node is required'); + }); + + it('should throw error for missing config', () => { + const invalidData = { ...mockLayoutData, config: null as any }; + expect(() => validateLayoutData(invalidData)).toThrow('Configuration is required'); + }); + + it('should throw error for invalid nodes array', () => { + const invalidData = { ...mockLayoutData, nodes: null as any }; + expect(() => validateLayoutData(invalidData)).toThrow('No nodes found in layout data'); + }); + + it('should throw error for invalid edges array', () => { + const invalidData = { ...mockLayoutData, edges: null as any }; + expect(() => validateLayoutData(invalidData)).toThrow('Edges array is required'); + }); + }); + + describe('layout function', () => { + it('should execute layout algorithm successfully', async () => { + const result: LayoutResult = await executeCoseBilkentLayout(mockLayoutData, mockConfig); + + expect(result).toBeDefined(); + expect(result.nodes).toBeDefined(); + expect(result.edges).toBeDefined(); + expect(Array.isArray(result.nodes)).toBe(true); + expect(Array.isArray(result.edges)).toBe(true); + }); + + it('should return positioned nodes with coordinates', async () => { + const result: LayoutResult = await executeCoseBilkentLayout(mockLayoutData, mockConfig); + + expect(result.nodes.length).toBeGreaterThan(0); + result.nodes.forEach((node) => { + expect(node.x).toBeDefined(); + expect(node.y).toBeDefined(); + expect(typeof node.x).toBe('number'); + expect(typeof node.y).toBe('number'); + }); + }); + + it('should return positioned edges with coordinates', async () => { + const result: LayoutResult = await executeCoseBilkentLayout(mockLayoutData, mockConfig); + + expect(result.edges.length).toBeGreaterThan(0); + result.edges.forEach((edge) => { + expect(edge.startX).toBeDefined(); + expect(edge.startY).toBeDefined(); + expect(edge.midX).toBeDefined(); + expect(edge.midY).toBeDefined(); + expect(edge.endX).toBeDefined(); + expect(edge.endY).toBeDefined(); + }); + }); + + it('should handle empty mindmap data gracefully', async () => { + const emptyData: LayoutData = { + nodes: [], + edges: [], + config: mockConfig, + rootNode: mockRootNode, + }; + + const result: LayoutResult = await executeCoseBilkentLayout(emptyData, mockConfig); + expect(result).toBeDefined(); + expect(result.nodes).toBeDefined(); + expect(result.edges).toBeDefined(); + expect(Array.isArray(result.nodes)).toBe(true); + expect(Array.isArray(result.edges)).toBe(true); + }); + + it('should throw error for invalid data', async () => { + const invalidData = { ...mockLayoutData, rootNode: null as any }; + + await expect(executeCoseBilkentLayout(invalidData, mockConfig)).rejects.toThrow(); + }); + }); +}); diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/layout.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/layout.ts new file mode 100644 index 000000000..433723259 --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/layout.ts @@ -0,0 +1,77 @@ +import type { MermaidConfig } from '../../../config.type.js'; +import { log } from '../../../logger.js'; +import type { LayoutData } from '../../types.js'; +import type { LayoutResult } from './types.js'; +import { + createCytoscapeInstance, + extractPositionedNodes, + extractPositionedEdges, +} from './cytoscape-setup.js'; + +/** + * Execute the cose-bilkent layout algorithm on generic layout data + * + * This function takes layout data and uses Cytoscape with the cose-bilkent + * algorithm to calculate optimal node positions and edge paths. + * + * @param data - The layout data containing nodes, edges, and configuration + * @param config - Mermaid configuration object + * @returns Promise resolving to layout result with positioned nodes and edges + */ +export async function executeCoseBilkentLayout( + data: LayoutData, + _config: MermaidConfig +): Promise { + log.debug('Starting cose-bilkent layout algorithm'); + + try { + // Validate layout data structure + validateLayoutData(data); + + // Create and configure cytoscape instance + const cy = await createCytoscapeInstance(data); + + // Extract positioned nodes and edges after layout + const positionedNodes = extractPositionedNodes(cy); + const positionedEdges = extractPositionedEdges(cy); + + log.debug(`Layout completed: ${positionedNodes.length} nodes, ${positionedEdges.length} edges`); + + return { + nodes: positionedNodes, + edges: positionedEdges, + }; + } catch (error) { + log.error('Error in cose-bilkent layout algorithm:', error); + throw error; + } +} + +/** + * Validate layout data structure + * @param data - The data to validate + * @returns True if data is valid, throws error otherwise + */ +export function validateLayoutData(data: LayoutData): boolean { + if (!data) { + throw new Error('Layout data is required'); + } + + if (!data.config) { + throw new Error('Configuration is required in layout data'); + } + + if (!data.rootNode) { + throw new Error('Root node is required'); + } + + if (!data.nodes || !Array.isArray(data.nodes)) { + throw new Error('No nodes found in layout data'); + } + + if (!Array.isArray(data.edges)) { + throw new Error('Edges array is required in layout data'); + } + + return true; +} diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts new file mode 100644 index 000000000..2dbbf5d7e --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts @@ -0,0 +1,197 @@ +import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid'; +import { executeCoseBilkentLayout } from './layout.js'; +import type { D3Selection } from '../../../types.js'; + +type Node = Record; + +interface NodeWithPosition extends Node { + x?: number; + y?: number; + domId?: string | SVGGroup | D3Selection; + width?: number; + height?: number; + id?: string; +} + +/** + * Render function for cose-bilkent layout algorithm + * + * This follows the same pattern as ELK and dagre renderers: + * 1. Insert nodes into DOM to get their actual dimensions + * 2. Run the layout algorithm to calculate positions + * 3. Position the nodes and edges based on layout results + */ +export const render = async ( + data4Layout: LayoutData, + svg: SVG, + { + insertCluster, + insertEdge, + insertEdgeLabel, + insertMarkers, + insertNode, + log, + positionEdgeLabel, + }: InternalHelpers, + { algorithm: _algorithm }: RenderOptions +) => { + const nodeDb: Record = {}; + const clusterDb: Record = {}; + + // Insert markers for edges + const element = svg.select('g'); + insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId); + + // Create container groups + const subGraphsEl = element.insert('g').attr('class', 'subgraphs'); + const edgePaths = element.insert('g').attr('class', 'edgePaths'); + const edgeLabels = element.insert('g').attr('class', 'edgeLabels'); + const nodes = element.insert('g').attr('class', 'nodes'); + + // Step 1: Insert nodes into DOM to get their actual dimensions + log.debug('Inserting nodes into DOM for dimension calculation'); + + await Promise.all( + data4Layout.nodes.map(async (node) => { + if (node.isGroup) { + // Handle subgraphs/clusters + const clusterNode: NodeWithPosition = { ...node }; + clusterDb[node.id] = clusterNode; + nodeDb[node.id] = clusterNode; + + // Insert cluster to get dimensions + await insertCluster(subGraphsEl, node); + } else { + // Handle regular nodes + const nodeWithPosition: NodeWithPosition = { ...node }; + nodeDb[node.id] = nodeWithPosition; + + // Insert node to get actual dimensions + const nodeEl = await insertNode(nodes, node, { + config: data4Layout.config, + dir: data4Layout.direction || 'TB', + }); + + // Get the actual bounding box after insertion + const boundingBox = nodeEl.node()!.getBBox(); + nodeWithPosition.width = boundingBox.width; + nodeWithPosition.height = boundingBox.height; + nodeWithPosition.domId = nodeEl; + + log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`); + } + }) + ); + + // Step 2: Run the cose-bilkent layout algorithm + log.debug('Running cose-bilkent layout algorithm'); + + // Update the layout data with actual dimensions + const updatedLayoutData = { + ...data4Layout, + nodes: data4Layout.nodes.map((node) => { + const nodeWithDimensions = nodeDb[node.id]; + return { + ...node, + width: nodeWithDimensions.width, + height: nodeWithDimensions.height, + }; + }), + }; + + const layoutResult = await executeCoseBilkentLayout(updatedLayoutData, data4Layout.config); + + // Step 3: Position the nodes based on layout results + log.debug('Positioning nodes based on layout results'); + + layoutResult.nodes.forEach((positionedNode) => { + const node = nodeDb[positionedNode.id]; + if (node?.domId) { + // Position the node at the calculated coordinates + // The positionedNode.x/y represents the center of the node, so use directly + (node.domId as D3Selection).attr( + 'transform', + `translate(${positionedNode.x}, ${positionedNode.y})` + ); + + // Store the final position + node.x = positionedNode.x; + node.y = positionedNode.y; + + log.debug(`Positioned node ${node.id} at center (${positionedNode.x}, ${positionedNode.y})`); + } + }); + + layoutResult.edges.forEach((positionedEdge) => { + const edge = data4Layout.edges.find((e) => e.id === positionedEdge.id); + if (edge) { + // Update the edge data with positioned coordinates + edge.points = [ + { x: positionedEdge.startX, y: positionedEdge.startY }, + { x: positionedEdge.midX, y: positionedEdge.midY }, + { x: positionedEdge.endX, y: positionedEdge.endY }, + ]; + } + }); + + // Step 4: Insert and position edges + log.debug('Inserting and positioning edges'); + + await Promise.all( + data4Layout.edges.map(async (edge) => { + // Insert edge label first + const _edgeLabel = await insertEdgeLabel(edgeLabels, edge); + + // Get start and end nodes + const startNode = nodeDb[edge.start ?? '']; + const endNode = nodeDb[edge.end ?? '']; + + if (startNode && endNode) { + // Find the positioned edge data + const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id); + + if (positionedEdge) { + log.debug('APA01 positionedEdge', positionedEdge); + // Create edge path with positioned coordinates + const edgeWithPath = { ...edge }; + + // Insert the edge path + const paths = insertEdge( + edgePaths, + edgeWithPath, + clusterDb, + data4Layout.type, + startNode, + endNode, + data4Layout.diagramId + ); + + // Position the edge label + positionEdgeLabel(edgeWithPath, paths); + } else { + // Fallback: create a simple straight line between nodes + const edgeWithPath = { + ...edge, + points: [ + { x: startNode.x || 0, y: startNode.y || 0 }, + { x: endNode.x || 0, y: endNode.y || 0 }, + ], + }; + + const paths = insertEdge( + edgePaths, + edgeWithPath, + clusterDb, + data4Layout.type, + startNode, + endNode, + data4Layout.diagramId + ); + positionEdgeLabel(edgeWithPath, paths); + } + } + }) + ); + + log.debug('Cose-bilkent rendering completed'); +}; diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/types.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/types.ts new file mode 100644 index 000000000..fade24682 --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/types.ts @@ -0,0 +1,43 @@ +/** + * Positioned node after layout calculation + */ +export interface PositionedNode { + id: string; + x: number; + y: number; + [key: string]: unknown; // Allow additional properties +} + +/** + * Positioned edge after layout calculation + */ +export interface PositionedEdge { + id: string; + source: string; + target: string; + startX: number; + startY: number; + midX: number; + midY: number; + endX: number; + endY: number; + [key: string]: unknown; // Allow additional properties +} + +/** + * Result of layout algorithm execution + */ +export interface LayoutResult { + nodes: PositionedNode[]; + edges: PositionedEdge[]; +} + +/** + * Cytoscape layout configuration + */ +export interface CytoscapeLayoutConfig { + name: 'cose-bilkent'; + quality: 'proof'; + styleEnabled: boolean; + animate: boolean; +} diff --git a/packages/mermaid/src/rendering-util/render.ts b/packages/mermaid/src/rendering-util/render.ts index b975e7bf9..ff07510b3 100644 --- a/packages/mermaid/src/rendering-util/render.ts +++ b/packages/mermaid/src/rendering-util/render.ts @@ -39,6 +39,14 @@ const registerDefaultLayoutLoaders = () => { name: 'dagre', loader: async () => await import('./layout-algorithms/dagre/index.js'), }, + ...(includeLargeFeatures + ? [ + { + name: 'cose-bilkent', + loader: async () => await import('./layout-algorithms/cose-bilkent/index.js'), + }, + ] + : []), ]); }; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js index db48e313c..3292b3811 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js @@ -438,7 +438,6 @@ const fixCorners = function (lineData) { } return newLineData; }; - export const insertEdge = function (elem, edge, clusterDb, diagramType, startNode, endNode, id) { const { handDrawnSeed } = getConfig(); let points = edge.points; @@ -622,9 +621,9 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod // lineData.forEach((point) => { // elem // .append('circle') - // .style('stroke', 'blue') - // .style('fill', 'blue') - // .attr('r', 3) + // .style('stroke', 'red') + // .style('fill', 'red') + // .attr('r', 1) // .attr('cx', point.x) // .attr('cy', point.y); // }); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/intersect/intersect-line.js b/packages/mermaid/src/rendering-util/rendering-elements/intersect/intersect-line.js index bd3eb497f..6d476fac9 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/intersect/intersect-line.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/intersect/intersect-line.js @@ -2,64 +2,63 @@ * Returns the point at which two lines, p and q, intersect or returns undefined if they do not intersect. */ function intersectLine(p1, p2, q1, q2) { - // Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994, - // p7 and p473. + { + // Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994, + // p7 and p473. - var a1, a2, b1, b2, c1, c2; - var r1, r2, r3, r4; - var denom, offset, num; - var x, y; + // Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x + + // b1 y + c1 = 0. + const a1 = p2.y - p1.y; + const b1 = p1.x - p2.x; + const c1 = p2.x * p1.y - p1.x * p2.y; - // Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x + - // b1 y + c1 = 0. - a1 = p2.y - p1.y; - b1 = p1.x - p2.x; - c1 = p2.x * p1.y - p1.x * p2.y; + // Compute r3 and r4. + const r3 = a1 * q1.x + b1 * q1.y + c1; + const r4 = a1 * q2.x + b1 * q2.y + c1; - // Compute r3 and r4. - r3 = a1 * q1.x + b1 * q1.y + c1; - r4 = a1 * q2.x + b1 * q2.y + c1; + const epsilon = 1e-6; - // Check signs of r3 and r4. If both point 3 and point 4 lie on - // same side of line 1, the line segments do not intersect. - if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) { - return /*DON'T_INTERSECT*/; + // Check signs of r3 and r4. If both point 3 and point 4 lie on + // same side of line 1, the line segments do not intersect. + if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) { + return /*DON'T_INTERSECT*/; + } + + // Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0 + const a2 = q2.y - q1.y; + const b2 = q1.x - q2.x; + const c2 = q2.x * q1.y - q1.x * q2.y; + + // Compute r1 and r2 + const r1 = a2 * p1.x + b2 * p1.y + c2; + const r2 = a2 * p2.x + b2 * p2.y + c2; + + // Check signs of r1 and r2. If both point 1 and point 2 lie + // on same side of second line segment, the line segments do + // not intersect. + if (Math.abs(r1) < epsilon && Math.abs(r2) < epsilon && sameSign(r1, r2)) { + return /*DON'T_INTERSECT*/; + } + + // Line segments intersect: compute intersection point. + const denom = a1 * b2 - a2 * b1; + if (denom === 0) { + return /*COLLINEAR*/; + } + + const offset = Math.abs(denom / 2); + + // The denom/2 is to get rounding instead of truncating. It + // is added or subtracted to the numerator, depending upon the + // sign of the numerator. + let num = b1 * c2 - b2 * c1; + const x = num < 0 ? (num - offset) / denom : (num + offset) / denom; + + num = a2 * c1 - a1 * c2; + const y = num < 0 ? (num - offset) / denom : (num + offset) / denom; + + return { x: x, y: y }; } - - // Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0 - a2 = q2.y - q1.y; - b2 = q1.x - q2.x; - c2 = q2.x * q1.y - q1.x * q2.y; - - // Compute r1 and r2 - r1 = a2 * p1.x + b2 * p1.y + c2; - r2 = a2 * p2.x + b2 * p2.y + c2; - - // Check signs of r1 and r2. If both point 1 and point 2 lie - // on same side of second line segment, the line segments do - // not intersect. - if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) { - return /*DON'T_INTERSECT*/; - } - - // Line segments intersect: compute intersection point. - denom = a1 * b2 - a2 * b1; - if (denom === 0) { - return /*COLLINEAR*/; - } - - offset = Math.abs(denom / 2); - - // The denom/2 is to get rounding instead of truncating. It - // is added or subtracted to the numerator, depending upon the - // sign of the numerator. - num = b1 * c2 - b2 * c1; - x = num < 0 ? (num - offset) / denom : (num + offset) / denom; - - num = a2 * c1 - a1 * c2; - y = num < 0 ? (num - offset) / denom : (num + offset) / denom; - - return { x: x, y: y }; } function sameSign(r1, r2) { diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts index 829f89a8f..2509dead4 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts @@ -61,6 +61,10 @@ import { erBox } from './shapes/erBox.js'; import { classBox } from './shapes/classBox.js'; import { requirementBox } from './shapes/requirementBox.js'; import { kanbanItem } from './shapes/kanbanItem.js'; +import { bang } from './shapes/bang.js'; +import { cloud } from './shapes/cloud.js'; +import { defaultMindmapNode } from './shapes/defaultMindmapNode.js'; +import { mindmapCircle } from './shapes/mindmapCircle.js'; type ShapeHandler = ( parent: D3Selection, @@ -135,6 +139,22 @@ export const shapesDefs = [ aliases: ['circ'], handler: circle, }, + { + semanticName: 'Bang', + name: 'Bang', + shortName: 'bang', + description: 'Bang', + aliases: ['bang'], + handler: bang, + }, + { + semanticName: 'Cloud', + name: 'Cloud', + shortName: 'cloud', + description: 'cloud', + aliases: ['cloud'], + handler: cloud, + }, { semanticName: 'Decision', name: 'Diamond', @@ -476,6 +496,9 @@ const generateShapeMap = () => { // Kanban diagram kanbanItem, + //Mindmap diagram + mindmapCircle, + defaultMindmapNode, // class diagram classBox, diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/bang.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/bang.ts new file mode 100644 index 000000000..bfc8896a5 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/bang.ts @@ -0,0 +1,81 @@ +import { log } from '../../../logger.js'; +import { labelHelper, updateNodeBounds, getNodeClasses } 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 { handleUndefinedAttr } from '../../../utils.js'; +import type { Bounds, Point } from '../../../types.js'; + +export async function bang(parent: D3Selection, node: Node) { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, halfPadding, label } = await labelHelper( + parent, + node, + getNodeClasses(node) + ); + + const w = bbox.width + 10 * halfPadding; + const h = bbox.height + 8 * halfPadding; + const r = 0.15 * w; + const { cssStyles } = node; + + const minWidth = bbox.width + 20; + const minHeight = bbox.height + 20; + const effectiveWidth = Math.max(w, minWidth); + const effectiveHeight = Math.max(h, minHeight); + + label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`); + + let bangElem; + const path = `M0 0 + a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${-1 * effectiveHeight * 0.1} + a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${0} + a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${0} + a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${effectiveHeight * 0.1} + + a${r},${r} 1 0,0 ${effectiveWidth * 0.15},${effectiveHeight * 0.33} + a${r * 0.8},${r * 0.8} 1 0,0 0,${effectiveHeight * 0.34} + a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.15},${effectiveHeight * 0.33} + + a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},${effectiveHeight * 0.15} + a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},0 + a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},0 + a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},${-1 * effectiveHeight * 0.15} + + a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.1},${-1 * effectiveHeight * 0.33} + a${r * 0.8},${r * 0.8} 1 0,0 0,${-1 * effectiveHeight * 0.34} + a${r},${r} 1 0,0 ${effectiveWidth * 0.1},${-1 * effectiveHeight * 0.33} + H0 V0 Z`; + + 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 roughNode = rc.path(path, options); + bangElem = shapeSvg.insert(() => roughNode, ':first-child'); + bangElem.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles)); + } else { + bangElem = shapeSvg + .insert('path', ':first-child') + .attr('class', 'basic label-container') + .attr('style', nodeStyles) + .attr('d', path); + } + + // Translate the path (center the shape) + bangElem.attr('transform', `translate(${-effectiveWidth / 2}, ${-effectiveHeight / 2})`); + + updateNodeBounds(node, bangElem); + node.calcIntersect = function (bounds: Bounds, point: Point) { + return intersect.rect(bounds, point); + }; + node.intersect = function (point) { + log.info('Bang intersect', node, point); + return intersect.rect(node, point); + }; + + return shapeSvg; +} diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts index 6b3be6765..57f72fd2d 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts @@ -1,18 +1,22 @@ -import { log } from '../../../logger.js'; -import { labelHelper, updateNodeBounds, getNodeClasses } 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 { log } from '../../../logger.js'; +import type { Bounds, D3Selection, Point } from '../../../types.js'; import { handleUndefinedAttr } from '../../../utils.js'; +import type { MindmapOptions, Node, ShapeRenderOptions } from '../../types.js'; +import intersect from '../intersect/index.js'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js'; -export async function circle(parent: D3Selection, node: Node) { +export async function circle( + parent: D3Selection, + node: Node, + options?: MindmapOptions | ShapeRenderOptions +) { const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, getNodeClasses(node)); - - const radius = bbox.width / 2 + halfPadding; + const padding = options?.padding ?? halfPadding; + const radius = bbox.width / 2 + padding; let circleElem; const { cssStyles } = node; @@ -35,7 +39,10 @@ export async function circle(parent: D3Selection(parent: D3Selection, node: Node) { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + + const { shapeSvg, bbox, halfPadding, label } = await labelHelper( + parent, + node, + getNodeClasses(node) + ); + + const w = bbox.width + 2 * halfPadding; + const h = bbox.height + 2 * halfPadding; + + // Cloud radii + const r1 = 0.15 * w; + const r2 = 0.25 * w; + const r3 = 0.35 * w; + const r4 = 0.2 * w; + + const { cssStyles } = node; + let cloudElem; + + // Cloud path + const path = `M0 0 + a${r1},${r1} 0 0,1 ${w * 0.25},${-1 * w * 0.1} + a${r3},${r3} 1 0,1 ${w * 0.4},${-1 * w * 0.1} + a${r2},${r2} 1 0,1 ${w * 0.35},${w * 0.2} + + a${r1},${r1} 1 0,1 ${w * 0.15},${h * 0.35} + a${r4},${r4} 1 0,1 ${-1 * w * 0.15},${h * 0.65} + + a${r2},${r1} 1 0,1 ${-1 * w * 0.25},${w * 0.15} + a${r3},${r3} 1 0,1 ${-1 * w * 0.5},0 + a${r1},${r1} 1 0,1 ${-1 * w * 0.25},${-1 * w * 0.15} + + a${r1},${r1} 1 0,1 ${-1 * w * 0.1},${-1 * h * 0.35} + a${r4},${r4} 1 0,1 ${w * 0.1},${-1 * h * 0.65} + H0 V0 Z`; + + 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 roughNode = rc.path(path, options); + cloudElem = shapeSvg.insert(() => roughNode, ':first-child'); + cloudElem.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles)); + } else { + cloudElem = shapeSvg + .insert('path', ':first-child') + .attr('class', 'basic label-container') + .attr('style', nodeStyles) + .attr('d', path); + } + + label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`); + + // Center the shape + cloudElem.attr('transform', `translate(${-w / 2}, ${-h / 2})`); + + updateNodeBounds(node, cloudElem); + + node.calcIntersect = function (bounds: Bounds, point: Point) { + return intersect.rect(bounds, point); + }; + node.intersect = function (point) { + log.info('Cloud intersect', node, point); + return intersect.rect(node, point); + }; + + return shapeSvg; +} diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/defaultMindmapNode.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/defaultMindmapNode.ts new file mode 100644 index 000000000..f30c80844 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/defaultMindmapNode.ts @@ -0,0 +1,64 @@ +import type { Bounds, D3Selection, Point } from '../../../types.js'; +import type { Node } from '../../types.js'; +import intersect from '../intersect/index.js'; +import { styles2String } from './handDrawnShapeStyles.js'; +import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js'; + +export async function defaultMindmapNode( + parent: D3Selection, + node: Node +) { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + + const { shapeSvg, bbox, halfPadding, label } = await labelHelper( + parent, + node, + getNodeClasses(node) + ); + + const w = bbox.width + 8 * halfPadding; + const h = bbox.height + 2 * halfPadding; + const rd = 5; + + const rectPath = ` + M${-w / 2} ${h / 2 - rd} + v${-h + 2 * rd} + q0,-${rd} ${rd},-${rd} + h${w - 2 * rd} + q${rd},0 ${rd},${rd} + v${h - 2 * rd} + q0,${rd} -${rd},${rd} + h${-w + 2 * rd} + q-${rd},0 -${rd},-${rd} + Z + `; + + const bg = shapeSvg + .append('path') + .attr('id', 'node-' + node.id) + .attr('class', 'node-bkg node-' + node.type) + .attr('style', nodeStyles) + .attr('d', rectPath); + + shapeSvg + .append('line') + .attr('class', 'node-line-') + .attr('x1', -w / 2) + .attr('y1', h / 2) + .attr('x2', w / 2) + .attr('y2', h / 2); + + label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`); + shapeSvg.append(() => label.node()); + + updateNodeBounds(node, bg); + node.calcIntersect = function (bounds: Bounds, point: Point) { + return intersect.rect(bounds, point); + }; + node.intersect = function (point) { + return intersect.rect(node, point); + }; + + return shapeSvg; +} diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/drawRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/drawRect.ts index 707aed2c7..8f70f82fc 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/drawRect.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/drawRect.ts @@ -6,6 +6,7 @@ import { userNodeOverrides, styles2String } from './handDrawnShapeStyles.js'; import rough from 'roughjs'; import type { D3Selection } from '../../../types.js'; import { handleUndefinedAttr } from '../../../utils.js'; +import type { Bounds, Point } from '../../../types.js'; export async function drawRect( parent: D3Selection, @@ -62,6 +63,10 @@ export async function drawRect( updateNodeBounds(node, rect); + node.calcIntersect = function (bounds: Bounds, point: Point) { + return intersect.rect(bounds, point); + }; + node.intersect = function (point) { return intersect.rect(node, point); }; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/mindmapCircle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/mindmapCircle.ts new file mode 100644 index 000000000..5b9dab3fd --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/mindmapCircle.ts @@ -0,0 +1,13 @@ +import { circle } from './circle.js'; +import type { Node, MindmapOptions } from '../../types.js'; +import type { D3Selection } from '../../../types.js'; + +export async function mindmapCircle( + parent: D3Selection, + node: Node +) { + const options = { + padding: node.padding ?? 0, + } as MindmapOptions; + return circle(parent, node, options); +} diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts index 24c811b85..87adc4814 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts @@ -1,4 +1,3 @@ -import { log } from '../../../logger.js'; import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; import intersect from '../intersect/index.js'; import type { Node } from '../../types.js'; @@ -6,6 +5,7 @@ import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; import rough from 'roughjs'; import { insertPolygonShape } from './insertPolygonShape.js'; import type { D3Selection } from '../../../types.js'; +import type { Bounds, Point } from '../../../types.js'; export const createDecisionBoxPathD = (x: number, y: number, size: number): string => { return [ @@ -61,17 +61,26 @@ export async function question(parent: D3Selection } updateNodeBounds(node, polygon); + node.calcIntersect = function (bounds: Bounds, point: Point) { + const s = bounds.width; + + // Define polygon points + const points = [ + { x: s / 2, y: 0 }, + { x: s, y: -s / 2 }, + { x: s / 2, y: -s }, + { x: 0, y: -s / 2 }, + ]; + + // Calculate the intersection point + const res = intersect.polygon(bounds, points, point); + + return { x: res.x - 0.5, y: res.y - 0.5 }; // Adjusted result + }; node.intersect = function (point) { - log.debug( - 'APA12 Intersect called SPLIT\npoint:', - point, - '\nnode:\n', - node, - '\nres:', - intersect.polygon(node, points, point) - ); - return intersect.polygon(node, points, point); + // @ts-ignore TODO fix this (KNSV) + return this.calcIntersect(node as Bounds, point); }; return shapeSvg; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/roundedRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/roundedRect.ts index 40d71429c..2b8f03d92 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/roundedRect.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/roundedRect.ts @@ -98,18 +98,19 @@ export async function roundedRect( const w = (node?.width ? node?.width : bbox.width) + labelPaddingX * 2; const h = (node?.height ? node?.height : bbox.height) + labelPaddingY * 2; - const radius = 5; - const taper = 5; // Taper width for the rounded corners + 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) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/squareRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/squareRect.ts index af72a798f..0e5ec730e 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/squareRect.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/squareRect.ts @@ -7,7 +7,7 @@ export async function squareRect(parent: D3Selecti rx: 0, ry: 0, classes: '', - labelPaddingX: (node?.padding || 0) * 2, + labelPaddingX: node.labelPaddingX ?? (node?.padding || 0) * 2, labelPaddingY: (node?.padding || 0) * 1, } as RectOptions; return drawRect(parent, node, options); diff --git a/packages/mermaid/src/rendering-util/types.ts b/packages/mermaid/src/rendering-util/types.ts index b11d2f314..c8439b534 100644 --- a/packages/mermaid/src/rendering-util/types.ts +++ b/packages/mermaid/src/rendering-util/types.ts @@ -2,6 +2,7 @@ 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'; +import type { Bounds, Point } from '../types.js'; export interface MarkdownWord { content: string; type: MarkdownWordType; @@ -38,11 +39,12 @@ interface BaseNode { linkTarget?: string; tooltip?: string; padding?: number; //REMOVE?, use from LayoutData.config - Keep, this could be shape specific - isGroup: boolean; + isGroup?: boolean; width?: number; height?: number; // Specific properties for State Diagram nodes TODO remove and use generic properties intersect?: (point: any) => any; + calcIntersect?: (bounds: Bounds, point: Point) => any; // Non-generic properties rx?: number; // Used for rounded corners in Rect, Ellipse, etc.Maybe it to specialized RectNode, EllipseNode, etc. @@ -58,6 +60,8 @@ interface BaseNode { borderStyle?: string; borderWidth?: number; labelTextColor?: string; + labelPaddingX?: number; + labelPaddingY?: number; // Flowchart specific properties x?: number; @@ -72,16 +76,25 @@ interface BaseNode { defaultWidth?: number; imageAspectRatio?: number; constraint?: 'on' | 'off'; + children?: NodeChildren; + nodeId?: string; + level?: number; + descr?: string; + type?: number; + radius?: number; + taper?: number; + stroke?: string; } /** * Group/cluster nodes, e.g. nodes that contain other nodes. */ +export type NodeChildren = Node[]; + export interface ClusterNode extends BaseNode { shape?: ClusterShapeID; isGroup: true; } - export interface NonClusterNode extends BaseNode { shape?: ShapeID; isGroup: false; @@ -113,7 +126,7 @@ export interface Edge { start?: string; stroke?: string; text?: string; - type: string; + type?: string; // Class Diagram specific properties startLabelRight?: string; endLabelLeft?: string; @@ -126,6 +139,12 @@ export interface Edge { thickness?: 'normal' | 'thick' | 'invisible' | 'dotted'; look?: string; isUserDefinedId?: boolean; + points?: Point[]; + parentId?: string; + dir?: string; + source?: string; + target?: string; + depth?: number; } export interface RectOptions { @@ -136,6 +155,10 @@ export interface RectOptions { classes: string; } +export interface MindmapOptions { + padding: number; +} + // Extending the Node interface for specific types if needed export type ClassDiagramNode = Node & { memberData: any; // Specific property for class diagram nodes @@ -171,6 +194,7 @@ export interface ShapeRenderOptions { config: MermaidConfig; /** Some shapes render differently if a diagram has a direction `LR` */ dir?: Node['dir']; + padding?: number; } export type KanbanNode = Node & { diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 0ff385c61..4b75c9704 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -977,6 +977,7 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) - useMaxWidth - padding - maxNodeWidth + - layoutAlgorithm properties: padding: type: number @@ -984,6 +985,10 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) maxNodeWidth: type: number default: 200 + layoutAlgorithm: + description: Layout algorithm to use for positioning mindmap nodes + type: string + default: 'cose-bilkent' KanbanDiagramConfig: title: Kanban Diagram Config diff --git a/packages/mermaid/src/types.ts b/packages/mermaid/src/types.ts index d1394e71b..727b6bb3a 100644 --- a/packages/mermaid/src/types.ts +++ b/packages/mermaid/src/types.ts @@ -13,6 +13,18 @@ export interface NodeMetaData { ticket?: string; } +export interface ParticipantMetaData { + type?: + | 'actor' + | 'participant' + | 'boundary' + | 'control' + | 'entity' + | 'database' + | 'collections' + | 'queue'; +} + export interface EdgeMetaData { animation?: 'fast' | 'slow'; animate?: boolean; @@ -36,6 +48,10 @@ export interface Point { x: number; y: number; } +export interface Bounds extends Point { + width: number; + height: number; +} export interface TextDimensionConfig { fontSize?: number; diff --git a/packages/mermaid/src/types/cytoscape-cose-bilkent.d.ts b/packages/mermaid/src/types/cytoscape-cose-bilkent.d.ts new file mode 100644 index 000000000..6e2930a47 --- /dev/null +++ b/packages/mermaid/src/types/cytoscape-cose-bilkent.d.ts @@ -0,0 +1,4 @@ +declare module 'cytoscape-cose-bilkent' { + const coseBilkent: any; + export default coseBilkent; +} diff --git a/packages/mermaid/src/utils/lineWithOffset.ts b/packages/mermaid/src/utils/lineWithOffset.ts index 800a5ffaf..057944325 100644 --- a/packages/mermaid/src/utils/lineWithOffset.ts +++ b/packages/mermaid/src/utils/lineWithOffset.ts @@ -3,7 +3,7 @@ import type { EdgeData, Point } from '../types.js'; // We need to draw the lines a bit shorter to avoid drawing // under any transparent markers. // The offsets are calculated from the markers' dimensions. -const markerOffsets = { +export const markerOffsets = { aggregation: 18, extension: 18, composition: 18, @@ -104,7 +104,6 @@ export const getLineFunctionsWithOffset = ( adjustment *= DIRECTION === 'right' ? -1 : 1; offset += adjustment; } - return pointTransformer(d).x + offset; }, y: function ( diff --git a/packages/parser/README.md b/packages/parser/README.md index 0a1ef04ed..07ef5a5b4 100644 --- a/packages/parser/README.md +++ b/packages/parser/README.md @@ -59,5 +59,4 @@ ValueConverter -->> Package: Return AST - To insert or modify attributes that can't be parsed. - When to override `ValueConverter`? - - To modify the returned value from the parser. diff --git a/packages/tiny/CHANGELOG.md b/packages/tiny/CHANGELOG.md index 12bcb2571..fc2f97fdf 100644 --- a/packages/tiny/CHANGELOG.md +++ b/packages/tiny/CHANGELOG.md @@ -1,11 +1,5 @@ # mermaid -## 11.10.1 - -### Patch Changes - -- [#6886](https://github.com/mermaid-js/mermaid/pull/6886) [`e0b45c2`](https://github.com/mermaid-js/mermaid/commit/e0b45c2d2b41c2a9038bf87646fa3ccd7560eb20) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Handle arrows correctly when auto number is enabled - ## 11.10.0 ### Minor Changes @@ -235,7 +229,6 @@ - [#5999](https://github.com/mermaid-js/mermaid/pull/5999) [`742ad7c`](https://github.com/mermaid-js/mermaid/commit/742ad7c130964df1fb5544e909d9556081285f68) Thanks [@knsv](https://github.com/knsv)! - Adding Kanban board, a new diagram type - [#5880](https://github.com/mermaid-js/mermaid/pull/5880) [`bdf145f`](https://github.com/mermaid-js/mermaid/commit/bdf145ffe362462176d9c1e68d5f3ff5c9d962b0) Thanks [@yari-dewalt](https://github.com/yari-dewalt)! - Class diagram changes: - - Updates the class diagram to the new unified way of rendering. - Includes a new "classBox" shape to be used in diagrams - Other updates such as: diff --git a/packages/tiny/package.json b/packages/tiny/package.json index b378ad423..3d7b307c9 100644 --- a/packages/tiny/package.json +++ b/packages/tiny/package.json @@ -1,6 +1,6 @@ { "name": "@mermaid-js/tiny", - "version": "11.10.1", + "version": "11.10.0", "description": "Tiny version of mermaid", "type": "commonjs", "main": "./dist/mermaid.tiny.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8be2d5aca..ab2670281 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -227,8 +227,8 @@ importers: specifier: ^7.0.4 version: 7.1.0 '@iconify/utils': - specifier: ^2.1.33 - version: 2.3.0 + specifier: ^3.0.1 + version: 3.0.1 '@mermaid-js/parser': specifier: workspace:^ version: link:../parser @@ -269,8 +269,8 @@ importers: specifier: ^4.17.21 version: 4.17.21 marked: - specifier: ^16.0.0 - version: 16.0.0 + specifier: ^15.0.7 + version: 15.0.12 roughjs: specifier: ^4.6.6 version: 4.6.6(patch_hash=3543d47108cb41b68ec6a671c0e1f9d0cfe2ce524fea5b0992511ae84c3c6b64) @@ -387,11 +387,11 @@ importers: specifier: ^4.35.0 version: 4.35.0 typedoc: - specifier: ^0.27.8 - version: 0.27.8(typescript@5.7.3) + specifier: ^0.28.9 + version: 0.28.11(typescript@5.7.3) typedoc-plugin-markdown: - specifier: ^4.4.2 - version: 4.4.2(typedoc@0.27.8(typescript@5.7.3)) + specifier: ^4.8.0 + version: 4.8.1(typedoc@0.28.11(typescript@5.7.3)) typescript: specifier: ~5.7.3 version: 5.7.3 @@ -446,6 +446,22 @@ importers: specifier: workspace:^ version: link:../mermaid + packages/mermaid-layout-tidy-tree: + dependencies: + d3: + specifier: ^7.9.0 + version: 7.9.0 + non-layered-tidy-tree-layout: + specifier: ^2.0.2 + version: 2.0.2 + devDependencies: + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 + mermaid: + specifier: workspace:^ + version: link:../mermaid + packages/mermaid-zenuml: dependencies: '@zenuml/core': @@ -499,8 +515,8 @@ importers: specifier: ^2.0.3 version: 2.0.3 unocss: - specifier: ^66.0.0 - version: 66.0.0(postcss@8.5.6)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3)) + specifier: ^66.4.2 + version: 66.4.2(postcss@8.5.6)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) unplugin-vue-components: specifier: ^28.4.0 version: 28.4.0(@babel/parser@7.28.0)(vue@3.5.13(typescript@5.7.3)) @@ -647,11 +663,11 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@antfu/install-pkg@1.0.0': - resolution: {integrity: sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==} + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@antfu/utils@8.1.1': - resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} + '@antfu/utils@9.2.0': + resolution: {integrity: sha512-Oq1d9BGZakE/FyoEtcNeSwM7MpDO2vUBi11RWBZXf75zPsbUVWmUs03EqkRFrcgbXyKTas0BdZWC1wcuSoqSAw==} '@apideck/better-ajv-errors@0.3.6': resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} @@ -2409,8 +2425,8 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} - '@gerrit0/mini-shiki@1.27.2': - resolution: {integrity: sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==} + '@gerrit0/mini-shiki@3.12.0': + resolution: {integrity: sha512-CF1vkfe2ViPtmoFEvtUWilEc4dOCiFzV8+J7/vEISSsslKQ97FjeTPNMCqUhZEiKySmKRgK3UO/CxtkyOp7DvA==} '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} @@ -2460,8 +2476,8 @@ packages: '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - '@iconify/utils@2.3.0': - resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} + '@iconify/utils@3.0.1': + resolution: {integrity: sha512-A78CUEnFGX8I/WlILxJCuIJXloL0j/OJ9PSchPAfCargEIKmUBWvvEMmKWB5oONwiUqlNt+5eRufdkLxeHIWYw==} '@img/sharp-darwin-arm64@0.33.5': resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} @@ -2740,6 +2756,9 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@quansync/fs@0.1.4': + resolution: {integrity: sha512-vy/41FCdnIalPTQCb2Wl0ic1caMdzGus4ktDp+gpZesQNydXcx8nhh8qB3qMPbGkictOTaXgXEUUfQEm8DQYoA==} + '@react-aria/focus@3.21.0': resolution: {integrity: sha512-7NEGtTPsBy52EZ/ToVKCu0HSelE3kq9qeis+2eEq90XSuJOMaDHUQrA7RC2Y89tlEwQB31bud/kKRi9Qme1dkA==} peerDependencies: @@ -3043,27 +3062,33 @@ packages: '@shikijs/engine-javascript@2.5.0': resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} - '@shikijs/engine-oniguruma@1.29.2': - resolution: {integrity: sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==} - '@shikijs/engine-oniguruma@2.5.0': resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} + '@shikijs/engine-oniguruma@3.12.0': + resolution: {integrity: sha512-IfDl3oXPbJ/Jr2K8mLeQVpnF+FxjAc7ZPDkgr38uEw/Bg3u638neSrpwqOTnTHXt1aU0Fk1/J+/RBdst1kVqLg==} + '@shikijs/langs@2.5.0': resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} + '@shikijs/langs@3.12.0': + resolution: {integrity: sha512-HIca0daEySJ8zuy9bdrtcBPhcYBo8wR1dyHk1vKrOuwDsITtZuQeGhEkcEfWc6IDyTcom7LRFCH6P7ljGSCEiQ==} + '@shikijs/themes@2.5.0': resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} + '@shikijs/themes@3.12.0': + resolution: {integrity: sha512-/lxvQxSI5s4qZLV/AuFaA4Wt61t/0Oka/P9Lmpr1UV+HydNCczO3DMHOC/CsXCCpbv4Zq8sMD0cDa7mvaVoj0Q==} + '@shikijs/transformers@2.5.0': resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==} - '@shikijs/types@1.29.2': - resolution: {integrity: sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==} - '@shikijs/types@2.5.0': resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} + '@shikijs/types@3.12.0': + resolution: {integrity: sha512-jsFzm8hCeTINC3OCmTZdhR9DOl/foJWplH2Px0bTi4m8z59fnsueLsweX82oGcjRQ7mfQAluQYKGoH2VzsWY4A==} + '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -3538,88 +3563,94 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@unocss/astro@66.0.0': - resolution: {integrity: sha512-GBhXT6JPqXjDXoJZTXhySk83NgOt0UigChqrUUdG4x7Z+DVYkDBION8vZUJjw0OdIaxNQ4euGWu4GDsMF6gQQg==} + '@unocss/astro@66.4.2': + resolution: {integrity: sha512-En3AKHwkiPxtZT95vkVrNiRYrB+DFVCikew6/dMMCWDWVKK0+5tEVUTzR1ak3+YnzAXl0NpWj8D4zHb0PxOs/A==} peerDependencies: - vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 peerDependenciesMeta: vite: optional: true - '@unocss/cli@66.0.0': - resolution: {integrity: sha512-KVQiskoOjVkLVpNaG6WpLa4grPplrZROYZJVIUYSTqZyZRFNSvjttHcsCwpoWUEUdEombPtVZl8FrXePjY5IiQ==} + '@unocss/cli@66.4.2': + resolution: {integrity: sha512-WsXzrB0SHbSt2nOHtD5QM91VN8j38+wObqyGcoIhtBSugqzsc+t7AdPkxV/ZaYgtPAz87bR0WFEVKcbiBRnmJw==} engines: {node: '>=14'} hasBin: true - '@unocss/config@66.0.0': - resolution: {integrity: sha512-nFRGop/guBa4jLkrgXjaRDm5JPz4x3YpP10m5IQkHpHwlnHUVn1L9smyPl04ohYWhYn9ZcAHgR28Ih2jwta8hw==} + '@unocss/config@66.4.2': + resolution: {integrity: sha512-plji1gNGSzlWjuV2Uh0q6Dt5ZlNkOKCHpgxekW9J458WghGAMBeXgB9uNpWg6flilqP1g0GJQv+XvJcSkYRGpQ==} engines: {node: '>=14'} - '@unocss/core@66.0.0': - resolution: {integrity: sha512-PdVbSMHNDDkr++9nkqzsZRAkaU84gxMTEgYbqI7dt2p1DXp/5tomVtmMsr2/whXGYKRiUc0xZ3p4Pzraz8TcXA==} + '@unocss/core@66.4.2': + resolution: {integrity: sha512-cYgMQrLhB9nRekv5c+yPDDa+5dzlMkA2UMQRil0s5D9Lb5n7NsCMcr6+nfxkcSYVLy92SbwDV45c6T7vIxFTOA==} - '@unocss/extractor-arbitrary-variants@66.0.0': - resolution: {integrity: sha512-vlkOIOuwBfaFBJcN6o7+obXjigjOlzVFN/jT6pG1WXbQDTRZ021jeF3i9INdb9D/0cQHSeDvNgi1TJ5oUxfiow==} + '@unocss/extractor-arbitrary-variants@66.4.2': + resolution: {integrity: sha512-T/eSeodfAp7HaWnQGqVLOsW4PbKUAvuybNRyvFWThMneM2qo+dOo3kFnA5my9ULAmRSFsAlyB1DnupD3qv5Klg==} - '@unocss/inspector@66.0.0': - resolution: {integrity: sha512-mkIxieVm0kMOKw+E4ABpIerihYMdjgq9A92RD5h2+W/ebpxTEw5lTTK1xcMLiAlmOrVYMQKjpgPeu3vQmDyGZQ==} + '@unocss/inspector@66.4.2': + resolution: {integrity: sha512-ugcJK8r2ypM4eIdgetVn8RhfKrbA3AF3OQ/RohK5PPk2UPDAScqabzYpfdNW4eYQsBOZOgoiqWtnfc8weqo8LQ==} - '@unocss/postcss@66.0.0': - resolution: {integrity: sha512-6bi+ujzh8I1PJwtmHX71LH8z/H9+vPxeYD4XgFihyU1k4Y6MVhjr7giGjLX4yP27IP+NsVyotD22V7by/dBVEA==} + '@unocss/postcss@66.4.2': + resolution: {integrity: sha512-tu4lnh6K27pIAuaQHlFlhXin8korwC0r1kQl00YMmF3THiX7orXkTP6xWGcQwnkbx4uQz1dw+tBimYxeaAMrhA==} engines: {node: '>=14'} peerDependencies: postcss: ^8.4.21 - '@unocss/preset-attributify@66.0.0': - resolution: {integrity: sha512-eYsOgmcDoiIgGAepIwRX+DKGYxc/wm0r4JnDuZdz29AB+A6oY/FGHS1BVt4rq9ny4B5PofP4p6Rty+vwD9rigw==} + '@unocss/preset-attributify@66.4.2': + resolution: {integrity: sha512-DwFJJkkawmHpjo3pGQE8FyoPsvhbxh+QMvvaAdYpo+iZ5HRkeDml9SOj7u6SGTcmbNyI+QR61s0KM8fxx6HcVQ==} - '@unocss/preset-icons@66.0.0': - resolution: {integrity: sha512-6ObwTvEGuPBbKWRoMMiDioHtwwQTFI5oojFLJ32Y8tW6TdXvBLkO88d7qpgQxEjgVt4nJrqF1WEfR4niRgBm0Q==} + '@unocss/preset-icons@66.4.2': + resolution: {integrity: sha512-qJx9gmesrvrmoTe9Mqoidihad8hm2MSD4QAezhfDSAyllioJOgyT0Bev/IEWAbehe9jtqYIh8v1oCerBPbGn6Q==} - '@unocss/preset-mini@66.0.0': - resolution: {integrity: sha512-d62eACnuKtR0dwCFOQXgvw5VLh5YSyK56xCzpHkh0j0GstgfDLfKTys0T/XVAAvdSvAy/8A8vhSNJ4PlIc9V2A==} + '@unocss/preset-mini@66.4.2': + resolution: {integrity: sha512-Ry+5hM+XLmT8HrEb182mUfcZuyrZ8xR+TBe72DBcliJ1DhOV3K67TCxwQucfb0zHbGV71HNWdPmHsLKxPDgweQ==} - '@unocss/preset-tagify@66.0.0': - resolution: {integrity: sha512-GGYGyWxaevh0jN0NoATVO1Qe7DFXM3ykLxchlXmG6/zy963pZxItg/njrKnxE9la4seCdxpFH7wQBa68imwwdA==} + '@unocss/preset-tagify@66.4.2': + resolution: {integrity: sha512-dECS09LqWJY4sYpgPUH2OAUftWU/tiZPR2XDRoTngeGU37GxSN+1sWtSmB7vwDm3C7opsdVUN20he8F1LUNubw==} - '@unocss/preset-typography@66.0.0': - resolution: {integrity: sha512-apjckP5nPU5mtaHTCzz5u/dK9KJWwJ2kOFCVk0+a/KhUWmnqnzmjRYZlEuWxxr5QxTdCW+9cIoRDSA0lYZS5tg==} + '@unocss/preset-typography@66.4.2': + resolution: {integrity: sha512-ZOKRuR5+V0r30QTVq04/6ZoIw75me3V25v2dU2YWJXIzwpMKmQ9TUN/M1yeiEUFfXjOaruWX6Ad6CvAw2MlCew==} - '@unocss/preset-uno@66.0.0': - resolution: {integrity: sha512-qgoZ/hzTI32bQvcyjcwvv1X/dbPlmQNehzgjUaL7QFT0q0/CN/SRpysfzoQ8DLl2se9T+YCOS9POx3KrpIiYSQ==} + '@unocss/preset-uno@66.4.2': + resolution: {integrity: sha512-1MFtPivGcpqRQFWdjtP40Enop1y3XDb3tlZXoMQUX0IGLG8HJOT+lfQx/Xl9t73ShJ8aAJ/l6qTxC43ZGNACzA==} - '@unocss/preset-web-fonts@66.0.0': - resolution: {integrity: sha512-9MzfDc6AJILN4Kq7Z91FfFbizBOYgw3lJd2UwqIs3PDYWG5iH5Zv5zhx6jelZVqEW5uWcIARYEEg2m4stZO1ZA==} + '@unocss/preset-web-fonts@66.4.2': + resolution: {integrity: sha512-4FYmleeRoM8r2DqGl6dfIjnX57tepcfZCvVfeCqYnk7475Yddmv1OYkoMjkWMnkK9MzdSxsFwHMU6CIUTmFTzQ==} - '@unocss/preset-wind3@66.0.0': - resolution: {integrity: sha512-WAGRmpi1sb2skvYn9DBQUvhfqrJ+VmQmn5ZGsT2ewvsk7HFCvVLAMzZeKrrTQepeNBRhg6HzFDDi8yg6yB5c9g==} + '@unocss/preset-wind3@66.4.2': + resolution: {integrity: sha512-0Aye/PaT08M/cQhPnGKn93iEVoRJbym0/1eomMvXoL+8oc7DVry35ws06r5CLu5h1sXI6UmS6sejoePFlSkLJQ==} - '@unocss/preset-wind@66.0.0': - resolution: {integrity: sha512-FtvGpHnGC7FiyKJavPnn5y9lsaoWRhXlujCqlT5Bw63kKhMNr0ogKySBpenUhJOhWhVM0OQXn2nZ3GZRxW2qpw==} + '@unocss/preset-wind4@66.4.2': + resolution: {integrity: sha512-F4RZsDqIpnSevD9hY353+Tw5gxpJuHA5HwdKjLnC/TnT9VKKVmV7qUEZ6M0jEuAk1kz2x3/ngnQ9Ftw+C2L84A==} + + '@unocss/preset-wind@66.4.2': + resolution: {integrity: sha512-z/rFYFINNqmBtl3Dh+7UCKpPnPkxM7IIUGszMnvdntky9uhLauJ11dt/Puir73sM2cAfywfgvnHyZ00m0pg7rA==} '@unocss/reset@66.0.0': resolution: {integrity: sha512-YLFz/5yT7mFJC8JSmIUA5+bS3CBCJbtztOw+8rWzjQr/BEVSGuihWUUpI2Df6VVxXIXxKanZR6mIl59yvf+GEA==} - '@unocss/rule-utils@66.0.0': - resolution: {integrity: sha512-UJ51YHbwxYTGyj35ugsPlOT4gaa7tCbXdywZ3m5Nn0JgywwIqGmBFyiN9ZjHBHfJuDxmmPd6lxojoBscih/WMQ==} + '@unocss/reset@66.4.2': + resolution: {integrity: sha512-s3Kq4Q6a/d3/jYe6HTCfXUx7zYAYufetId5n66DZHzQxpeu6CoBS83+b37STTKsw27SOgV28cPJlJtZ6/D6Bhw==} + + '@unocss/rule-utils@66.4.2': + resolution: {integrity: sha512-7z3IuajwXhy2cx3E0IGOFXIiuKC79/jzm4Tt56TC68nXLh/etlH0fKhxVwkZ/HbcQRpVwWyDRNcbh29pmA3DwQ==} engines: {node: '>=14'} - '@unocss/transformer-attributify-jsx@66.0.0': - resolution: {integrity: sha512-jS7szFXXC6RjTv9wo0NACskf618w981bkbyQ5izRO7Ha47sNpHhHDpaltnG7SR9qV4cCtGalOw4onVMHsRKwRg==} + '@unocss/transformer-attributify-jsx@66.4.2': + resolution: {integrity: sha512-de6LzoyW1tkdOftlCrj6z8wEb4j6l1sqmOU1nYKkYHw7luLFGxRUELC7iujlI9KmylbM02bcKfLETAfJy/je2w==} - '@unocss/transformer-compile-class@66.0.0': - resolution: {integrity: sha512-ytUIE0nAcHRMACuTXkHp8auZ483DXrOZw99jk3FJ+aFjpD/pVSFmX14AWJ7bqPFObxb4SLFs6KhQma30ESC22A==} + '@unocss/transformer-compile-class@66.4.2': + resolution: {integrity: sha512-+oiIrV8c3T7qiJdICr6YsEWik5sjbWirXF0mlpcBvZu2HyV559hvHjzuWKr/fl7xYYZKDL9FvddbqWo3DOXh3Q==} - '@unocss/transformer-directives@66.0.0': - resolution: {integrity: sha512-utcg7m2Foi7uHrU5WHadNuJ0a3qWG8tZNkQMi+m0DQpX6KWfuDtDn0zDZ1X+z5lmiB3WGSJERRrsvZbj1q50Mw==} + '@unocss/transformer-directives@66.4.2': + resolution: {integrity: sha512-7m/dTrCUkBkZeSRKPxPEo65Rav239orQSLq6sztwZhoA4x/6H8r58xCkAK0qC9VEalyerpCpyarU3sKN4+ehNg==} - '@unocss/transformer-variant-group@66.0.0': - resolution: {integrity: sha512-1BLjNWtAnR1JAcQGw0TS+nGrVoB9aznzvVZRoTx23dtRr3btvgKPHb8LrD48eD/p8Dtw9j3WfuxMDKXKegKDLg==} + '@unocss/transformer-variant-group@66.4.2': + resolution: {integrity: sha512-SbPDbZUrhQyL4CpvnpvUfrr1DFq8AKf8ofPGbMJDm5S2TInQ34vFaIrhNroGR0szntMZRH5Zlkq6LtVUKDRs5g==} - '@unocss/vite@66.0.0': - resolution: {integrity: sha512-IVcPX8xL+2edyXKt4tp9yu5A6gcbPVCsspfcL0XgziCr01kS+4qSoZ90F3IUs3hXc/AyO5eCpRtGFMPLpOjXQg==} + '@unocss/vite@66.4.2': + resolution: {integrity: sha512-7eON9iPF3qWzuI+M6u0kq7K3y9nEbimZlLj01nGoqrgSGxEsyJpP01QQQsmT7FPRiZzRMJv7BiKMEyDQSuRRCA==} peerDependencies: - vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -4779,12 +4810,15 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + connect-history-api-fallback@2.0.0: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} engines: {node: '>=0.8'} - consola@3.4.0: - resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} console.table@0.10.0: @@ -5852,6 +5886,9 @@ packages: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -7276,6 +7313,10 @@ packages: resolution: {integrity: sha512-bbgPw/wmroJsil/GgL4qjDzs5YLTBMQ99weRsok1XCDccQeehbHA/I1oRvk2NPtr7KGZgT/Y5tPRnAtMqeG2Kg==} engines: {node: '>=14'} + local-pkg@1.1.1: + resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} + engines: {node: '>=14'} + locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -7404,9 +7445,9 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} - marked@16.0.0: - resolution: {integrity: sha512-MUKMXDjsD/eptB7GPzxo4xcnLS6oo7/RHimUMHEDRhUooPwmN9BEpMl7AEOJv3bmso169wHI2wUF9VQgL7zfmA==} - engines: {node: '>= 20'} + marked@15.0.12: + resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} + engines: {node: '>= 18'} hasBin: true marked@4.3.0: @@ -7830,6 +7871,9 @@ packages: resolution: {integrity: sha512-fiVbT7BqxiQqjlR9U3FDGOSERFCKoXVCdxV2FwZuNN7/cmJ42iQx35nUFOAFDcyvemu9Adp+IlsCGlKQYLmBKw==} deprecated: Package no longer supported. Contact support@npmjs.com for more info. + non-layered-tidy-tree-layout@2.0.2: + resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -8040,6 +8084,9 @@ packages: package-manager-detector@0.2.9: resolution: {integrity: sha512-+vYvA/Y31l8Zk8dwxHhL3JfTuHPm6tlxM2A3GeQyl7ovYnSp1+mzAxClxaOr0qO1TtPxbQxetI7v5XqKLJZk7Q==} + package-manager-detector@1.3.0: + resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} @@ -8223,6 +8270,9 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + pkg-types@2.2.0: + resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} + plist@3.1.0: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} @@ -8405,6 +8455,9 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -8996,6 +9049,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} @@ -9327,6 +9381,9 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyglobby@0.2.12: resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} engines: {node: '>=12.0.0'} @@ -9504,18 +9561,18 @@ packages: typedarray-to-buffer@3.1.5: resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} - typedoc-plugin-markdown@4.4.2: - resolution: {integrity: sha512-kJVkU2Wd+AXQpyL6DlYXXRrfNrHrEIUgiABWH8Z+2Lz5Sq6an4dQ/hfvP75bbokjNDUskOdFlEEm/0fSVyC7eg==} + typedoc-plugin-markdown@4.8.1: + resolution: {integrity: sha512-ug7fc4j0SiJxSwBGLncpSo8tLvrT9VONvPUQqQDTKPxCoFQBADLli832RGPtj6sfSVJebNSrHZQRUdEryYH/7g==} engines: {node: '>= 18'} peerDependencies: - typedoc: 0.27.x + typedoc: 0.28.x - typedoc@0.27.8: - resolution: {integrity: sha512-q0/2TUunNEDmWkn23ULKGXieK8cgGuAmBUXC/HcZ/rgzMI9Yr4Nq3in1K1vT1NZ9zx6M78yTk3kmIPbwJgK5KA==} - engines: {node: '>= 18'} + typedoc@0.28.11: + resolution: {integrity: sha512-1FqgrrUYGNuE3kImAiEDgAVVVacxdO4ZVTKbiOVDGkoeSB4sNwQaDpa8mta+Lw5TEzBFmGXzsg0I1NLRIoaSFw==} + engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: - typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x typescript-eslint@8.38.0: resolution: {integrity: sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==} @@ -9547,8 +9604,8 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - unconfig@7.0.0: - resolution: {integrity: sha512-G5CJSoG6ZTxgzCJblEfgpdRK2tos9+UdD2WtecDUVfImzQ0hFjwpH5RVvGMhP4pRpC9ML7NrC4qBsBl0Ttj35A==} + unconfig@7.3.2: + resolution: {integrity: sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg==} underscore@1.1.7: resolution: {integrity: sha512-w4QtCHoLBXw1mjofIDoMyexaEdWGMedWNDhlWTtT1V1lCRqi65Pnoygkh6+WRdr+Bm8ldkBNkNeCsXGMlQS9HQ==} @@ -9625,12 +9682,12 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - unocss@66.0.0: - resolution: {integrity: sha512-SHstiv1s7zGPSjzOsADzlwRhQM+6817+OqQE3Fv+N/nn2QLNx1bi3WXybFfz5tWkzBtyTZlwdPmeecsIs1yOCA==} + unocss@66.4.2: + resolution: {integrity: sha512-PsZ+4XF/ekiParR7PZEM7AchvHJ78EIfOXlqTPflTOXCYgZ77kG9NaIaIf4lHRevY+rRTyrHrjxdg1Ern2j8qw==} engines: {node: '>=14'} peerDependencies: - '@unocss/webpack': 66.0.0 - vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 + '@unocss/webpack': 66.4.2 + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 peerDependenciesMeta: '@unocss/webpack': optional: true @@ -9974,10 +10031,8 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - vue-flow-layout@0.1.1: - resolution: {integrity: sha512-JdgRRUVrN0Y2GosA0M68DEbKlXMqJ7FQgsK8CjQD2vxvNSqAU6PZEpi4cfcTVtfM2GVOMjHo7GKKLbXxOBqDqA==} - peerDependencies: - vue: ^3.4.37 + vue-flow-layout@0.2.0: + resolution: {integrity: sha512-zKgsWWkXq0xrus7H4Mc+uFs1ESrmdTXlO0YNbR6wMdPaFvosL3fMB8N7uTV308UhGy9UvTrGhIY7mVz9eN+L0Q==} vue@3.5.13: resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} @@ -10294,11 +10349,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml@2.7.0: - resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} - engines: {node: '>= 14'} - hasBin: true - yaml@2.8.0: resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} engines: {node: '>= 14.6'} @@ -10498,12 +10548,12 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/install-pkg@1.0.0': + '@antfu/install-pkg@1.1.0': dependencies: - package-manager-detector: 0.2.9 - tinyexec: 0.3.2 + package-manager-detector: 1.3.0 + tinyexec: 1.0.1 - '@antfu/utils@8.1.1': {} + '@antfu/utils@9.2.0': {} '@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)': dependencies: @@ -10856,7 +10906,7 @@ snapshots: '@babel/generator@7.27.1': dependencies: - '@babel/parser': 7.27.2 + '@babel/parser': 7.28.0 '@babel/types': 7.27.1 '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 @@ -10955,7 +11005,7 @@ snapshots: '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.27.1 + '@babel/traverse': 7.28.0 '@babel/types': 7.27.1 transitivePeerDependencies: - supports-color @@ -10965,7 +11015,7 @@ snapshots: '@babel/core': 7.27.1 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.27.1 + '@babel/traverse': 7.28.0 transitivePeerDependencies: - supports-color @@ -12080,14 +12130,14 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.27.2 + '@babel/parser': 7.28.0 '@babel/types': 7.27.1 '@babel/traverse@7.27.1': dependencies: '@babel/code-frame': 7.27.1 '@babel/generator': 7.27.1 - '@babel/parser': 7.27.2 + '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/types': 7.27.1 debug: 4.4.1(supports-color@8.1.1) @@ -12673,7 +12723,7 @@ snapshots: '@babel/preset-env': 7.27.2(@babel/core@7.27.1) babel-loader: 9.2.1(@babel/core@7.27.1)(webpack@5.95.0(esbuild@0.25.0)) bluebird: 3.7.1 - debug: 4.4.0 + debug: 4.4.1(supports-color@8.1.1) lodash: 4.17.21 webpack: 5.95.0(esbuild@0.25.0) transitivePeerDependencies: @@ -13062,10 +13112,12 @@ snapshots: '@floating-ui/utils@0.2.9': {} - '@gerrit0/mini-shiki@1.27.2': + '@gerrit0/mini-shiki@3.12.0': dependencies: - '@shikijs/engine-oniguruma': 1.29.2 - '@shikijs/types': 1.29.2 + '@shikijs/engine-oniguruma': 3.12.0 + '@shikijs/langs': 3.12.0 + '@shikijs/themes': 3.12.0 + '@shikijs/types': 3.12.0 '@shikijs/vscode-textmate': 10.0.2 '@hapi/hoek@9.3.0': {} @@ -13111,15 +13163,15 @@ snapshots: '@iconify/types@2.0.0': {} - '@iconify/utils@2.3.0': + '@iconify/utils@3.0.1': dependencies: - '@antfu/install-pkg': 1.0.0 - '@antfu/utils': 8.1.1 + '@antfu/install-pkg': 1.1.0 + '@antfu/utils': 9.2.0 '@iconify/types': 2.0.0 - debug: 4.4.0 + debug: 4.4.1(supports-color@8.1.1) globals: 15.15.0 kolorist: 1.8.0 - local-pkg: 1.0.0 + local-pkg: 1.1.1 mlly: 1.7.4 transitivePeerDependencies: - supports-color @@ -13501,6 +13553,10 @@ snapshots: '@polka/url@1.0.0-next.28': {} + '@quansync/fs@0.1.4': + dependencies: + quansync: 0.2.10 + '@react-aria/focus@3.21.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@react-aria/interactions': 3.25.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -13751,35 +13807,43 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 3.1.1 - '@shikijs/engine-oniguruma@1.29.2': - dependencies: - '@shikijs/types': 1.29.2 - '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/engine-oniguruma@2.5.0': dependencies: '@shikijs/types': 2.5.0 '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/engine-oniguruma@3.12.0': + dependencies: + '@shikijs/types': 3.12.0 + '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/langs@2.5.0': dependencies: '@shikijs/types': 2.5.0 + '@shikijs/langs@3.12.0': + dependencies: + '@shikijs/types': 3.12.0 + '@shikijs/themes@2.5.0': dependencies: '@shikijs/types': 2.5.0 + '@shikijs/themes@3.12.0': + dependencies: + '@shikijs/types': 3.12.0 + '@shikijs/transformers@2.5.0': dependencies: '@shikijs/core': 2.5.0 '@shikijs/types': 2.5.0 - '@shikijs/types@1.29.2': + '@shikijs/types@2.5.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - '@shikijs/types@2.5.0': + '@shikijs/types@3.12.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -13840,7 +13904,7 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.27.2 + '@babel/parser': 7.28.0 '@babel/types': 7.27.1 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 @@ -13852,7 +13916,7 @@ snapshots: '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.27.2 + '@babel/parser': 7.28.0 '@babel/types': 7.27.1 '@types/babel__traverse@7.20.6': @@ -14358,150 +14422,178 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@unocss/astro@66.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3))': + '@unocss/astro@66.4.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))': dependencies: - '@unocss/core': 66.0.0 - '@unocss/reset': 66.0.0 - '@unocss/vite': 66.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3)) + '@unocss/core': 66.4.2 + '@unocss/reset': 66.4.2 + '@unocss/vite': 66.4.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) optionalDependencies: vite: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) - transitivePeerDependencies: - - vue - '@unocss/cli@66.0.0': + '@unocss/astro@66.4.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))': + dependencies: + '@unocss/core': 66.4.2 + '@unocss/reset': 66.4.2 + '@unocss/vite': 66.4.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + optionalDependencies: + vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + + '@unocss/cli@66.4.2': dependencies: '@ampproject/remapping': 2.3.0 - '@unocss/config': 66.0.0 - '@unocss/core': 66.0.0 - '@unocss/preset-uno': 66.0.0 + '@unocss/config': 66.4.2 + '@unocss/core': 66.4.2 + '@unocss/preset-uno': 66.4.2 cac: 6.7.14 chokidar: 3.6.0 colorette: 2.0.20 - consola: 3.4.0 + consola: 3.4.2 magic-string: 0.30.17 pathe: 2.0.3 perfect-debounce: 1.0.0 - tinyglobby: 0.2.12 + tinyglobby: 0.2.14 unplugin-utils: 0.2.4 - '@unocss/config@66.0.0': + '@unocss/config@66.4.2': dependencies: - '@unocss/core': 66.0.0 - unconfig: 7.0.0 + '@unocss/core': 66.4.2 + unconfig: 7.3.2 - '@unocss/core@66.0.0': {} + '@unocss/core@66.4.2': {} - '@unocss/extractor-arbitrary-variants@66.0.0': + '@unocss/extractor-arbitrary-variants@66.4.2': dependencies: - '@unocss/core': 66.0.0 + '@unocss/core': 66.4.2 - '@unocss/inspector@66.0.0(vue@3.5.13(typescript@5.7.3))': + '@unocss/inspector@66.4.2': dependencies: - '@unocss/core': 66.0.0 - '@unocss/rule-utils': 66.0.0 + '@unocss/core': 66.4.2 + '@unocss/rule-utils': 66.4.2 colorette: 2.0.20 gzip-size: 6.0.0 sirv: 3.0.1 - vue-flow-layout: 0.1.1(vue@3.5.13(typescript@5.7.3)) - transitivePeerDependencies: - - vue + vue-flow-layout: 0.2.0 - '@unocss/postcss@66.0.0(postcss@8.5.6)': + '@unocss/postcss@66.4.2(postcss@8.5.6)': dependencies: - '@unocss/config': 66.0.0 - '@unocss/core': 66.0.0 - '@unocss/rule-utils': 66.0.0 + '@unocss/config': 66.4.2 + '@unocss/core': 66.4.2 + '@unocss/rule-utils': 66.4.2 css-tree: 3.1.0 postcss: 8.5.6 - tinyglobby: 0.2.12 + tinyglobby: 0.2.14 - '@unocss/preset-attributify@66.0.0': + '@unocss/preset-attributify@66.4.2': dependencies: - '@unocss/core': 66.0.0 + '@unocss/core': 66.4.2 - '@unocss/preset-icons@66.0.0': + '@unocss/preset-icons@66.4.2': dependencies: - '@iconify/utils': 2.3.0 - '@unocss/core': 66.0.0 + '@iconify/utils': 3.0.1 + '@unocss/core': 66.4.2 ofetch: 1.4.1 transitivePeerDependencies: - supports-color - '@unocss/preset-mini@66.0.0': + '@unocss/preset-mini@66.4.2': dependencies: - '@unocss/core': 66.0.0 - '@unocss/extractor-arbitrary-variants': 66.0.0 - '@unocss/rule-utils': 66.0.0 + '@unocss/core': 66.4.2 + '@unocss/extractor-arbitrary-variants': 66.4.2 + '@unocss/rule-utils': 66.4.2 - '@unocss/preset-tagify@66.0.0': + '@unocss/preset-tagify@66.4.2': dependencies: - '@unocss/core': 66.0.0 + '@unocss/core': 66.4.2 - '@unocss/preset-typography@66.0.0': + '@unocss/preset-typography@66.4.2': dependencies: - '@unocss/core': 66.0.0 - '@unocss/preset-mini': 66.0.0 - '@unocss/rule-utils': 66.0.0 + '@unocss/core': 66.4.2 + '@unocss/preset-mini': 66.4.2 + '@unocss/rule-utils': 66.4.2 - '@unocss/preset-uno@66.0.0': + '@unocss/preset-uno@66.4.2': dependencies: - '@unocss/core': 66.0.0 - '@unocss/preset-wind3': 66.0.0 + '@unocss/core': 66.4.2 + '@unocss/preset-wind3': 66.4.2 - '@unocss/preset-web-fonts@66.0.0': + '@unocss/preset-web-fonts@66.4.2': dependencies: - '@unocss/core': 66.0.0 + '@unocss/core': 66.4.2 ofetch: 1.4.1 - '@unocss/preset-wind3@66.0.0': + '@unocss/preset-wind3@66.4.2': dependencies: - '@unocss/core': 66.0.0 - '@unocss/preset-mini': 66.0.0 - '@unocss/rule-utils': 66.0.0 + '@unocss/core': 66.4.2 + '@unocss/preset-mini': 66.4.2 + '@unocss/rule-utils': 66.4.2 - '@unocss/preset-wind@66.0.0': + '@unocss/preset-wind4@66.4.2': dependencies: - '@unocss/core': 66.0.0 - '@unocss/preset-wind3': 66.0.0 + '@unocss/core': 66.4.2 + '@unocss/extractor-arbitrary-variants': 66.4.2 + '@unocss/rule-utils': 66.4.2 + + '@unocss/preset-wind@66.4.2': + dependencies: + '@unocss/core': 66.4.2 + '@unocss/preset-wind3': 66.4.2 '@unocss/reset@66.0.0': {} - '@unocss/rule-utils@66.0.0': + '@unocss/reset@66.4.2': {} + + '@unocss/rule-utils@66.4.2': dependencies: - '@unocss/core': 66.0.0 + '@unocss/core': 66.4.2 magic-string: 0.30.17 - '@unocss/transformer-attributify-jsx@66.0.0': + '@unocss/transformer-attributify-jsx@66.4.2': dependencies: - '@unocss/core': 66.0.0 + '@babel/parser': 7.28.0 + '@babel/traverse': 7.28.0 + '@unocss/core': 66.4.2 + transitivePeerDependencies: + - supports-color - '@unocss/transformer-compile-class@66.0.0': + '@unocss/transformer-compile-class@66.4.2': dependencies: - '@unocss/core': 66.0.0 + '@unocss/core': 66.4.2 - '@unocss/transformer-directives@66.0.0': + '@unocss/transformer-directives@66.4.2': dependencies: - '@unocss/core': 66.0.0 - '@unocss/rule-utils': 66.0.0 + '@unocss/core': 66.4.2 + '@unocss/rule-utils': 66.4.2 css-tree: 3.1.0 - '@unocss/transformer-variant-group@66.0.0': + '@unocss/transformer-variant-group@66.4.2': dependencies: - '@unocss/core': 66.0.0 + '@unocss/core': 66.4.2 - '@unocss/vite@66.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3))': + '@unocss/vite@66.4.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))': dependencies: '@ampproject/remapping': 2.3.0 - '@unocss/config': 66.0.0 - '@unocss/core': 66.0.0 - '@unocss/inspector': 66.0.0(vue@3.5.13(typescript@5.7.3)) + '@unocss/config': 66.4.2 + '@unocss/core': 66.4.2 + '@unocss/inspector': 66.4.2 chokidar: 3.6.0 magic-string: 0.30.17 - tinyglobby: 0.2.12 + pathe: 2.0.3 + tinyglobby: 0.2.14 unplugin-utils: 0.2.4 vite: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) - transitivePeerDependencies: - - vue + + '@unocss/vite@66.4.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@unocss/config': 66.4.2 + '@unocss/core': 66.4.2 + '@unocss/inspector': 66.4.2 + chokidar: 3.6.0 + magic-string: 0.30.17 + pathe: 2.0.3 + tinyglobby: 0.2.14 + unplugin-utils: 0.2.4 + vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -14566,6 +14658,10 @@ snapshots: dependencies: vite-plugin-pwa: 1.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0) + '@vite-pwa/vitepress@1.0.0(vite-plugin-pwa@1.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0))': + dependencies: + vite-plugin-pwa: 1.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0) + '@vitejs/plugin-vue@5.2.1(vite@5.4.19(@types/node@22.13.5)(terser@5.39.0))(vue@3.5.13(typescript@5.7.3))': dependencies: vite: 5.4.19(@types/node@22.13.5)(terser@5.39.0) @@ -14577,6 +14673,12 @@ snapshots: vite: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) vue: 3.5.13(typescript@5.7.3) + '@vitejs/plugin-vue@6.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.19 + vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + vue: 3.5.13(typescript@5.7.3) + '@vitest/coverage-v8@3.0.6(vitest@3.0.6)': dependencies: '@ampproject/remapping': 2.3.0 @@ -15771,9 +15873,11 @@ snapshots: confbox@0.1.8: {} + confbox@0.2.2: {} + connect-history-api-fallback@2.0.0: {} - consola@3.4.0: {} + consola@3.4.2: {} console.table@0.10.0: dependencies: @@ -17197,6 +17301,8 @@ snapshots: transitivePeerDependencies: - supports-color + exsolve@1.0.7: {} + extend@3.0.2: {} extendable-error@0.1.7: {} @@ -17402,7 +17508,7 @@ snapshots: '@actions/core': 1.11.1 arg: 5.0.2 console.table: 0.10.0 - debug: 4.4.0 + debug: 4.4.1(supports-color@8.1.1) find-test-names: 1.29.5(@babel/core@7.27.1) globby: 11.1.0 minimatch: 3.1.2 @@ -18303,7 +18409,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.4.0 + debug: 4.4.1(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -18936,6 +19042,12 @@ snapshots: mlly: 1.7.4 pkg-types: 1.3.1 + local-pkg@1.1.1: + dependencies: + mlly: 1.7.4 + pkg-types: 2.2.0 + quansync: 0.2.10 + locate-path@3.0.0: dependencies: p-locate: 3.0.0 @@ -19064,7 +19176,7 @@ snapshots: markdown-table@3.0.4: {} - marked@16.0.0: {} + marked@15.0.12: {} marked@4.3.0: {} @@ -19615,13 +19727,15 @@ snapshots: node-source-walk@7.0.0: dependencies: - '@babel/parser': 7.27.2 + '@babel/parser': 7.28.0 nomnom@1.5.2: dependencies: colors: 0.5.1 underscore: 1.1.7 + non-layered-tidy-tree-layout@2.0.2: {} + normalize-path@3.0.0: {} normalize-url@6.1.0: {} @@ -19873,6 +19987,8 @@ snapshots: package-manager-detector@0.2.9: {} + package-manager-detector@1.3.0: {} + pako@1.0.11: {} pako@2.1.0: {} @@ -20047,6 +20163,12 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 + pkg-types@2.2.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + plist@3.1.0: dependencies: '@xmldom/xmldom': 0.8.10 @@ -20228,6 +20350,8 @@ snapshots: dependencies: side-channel: 1.1.0 + quansync@0.2.10: {} + queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} @@ -21017,7 +21141,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.4.0 + debug: 4.4.1(supports-color@8.1.1) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -21034,7 +21158,7 @@ snapshots: deep-equal: 2.2.3 dependency-tree: 11.0.1 lazy-ass: 2.0.3 - tinyglobby: 0.2.12 + tinyglobby: 0.2.14 transitivePeerDependencies: - supports-color @@ -21397,6 +21521,8 @@ snapshots: tinyexec@0.3.2: {} + tinyexec@1.0.1: {} + tinyglobby@0.2.12: dependencies: fdir: 6.4.3(picomatch@4.0.2) @@ -21558,18 +21684,18 @@ snapshots: dependencies: is-typedarray: 1.0.0 - typedoc-plugin-markdown@4.4.2(typedoc@0.27.8(typescript@5.7.3)): + typedoc-plugin-markdown@4.8.1(typedoc@0.28.11(typescript@5.7.3)): dependencies: - typedoc: 0.27.8(typescript@5.7.3) + typedoc: 0.28.11(typescript@5.7.3) - typedoc@0.27.8(typescript@5.7.3): + typedoc@0.28.11(typescript@5.7.3): dependencies: - '@gerrit0/mini-shiki': 1.27.2 + '@gerrit0/mini-shiki': 3.12.0 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 typescript: 5.7.3 - yaml: 2.7.0 + yaml: 2.8.0 typescript-eslint@8.38.0(eslint@9.26.0(jiti@2.4.2))(typescript@5.7.3): dependencies: @@ -21599,11 +21725,12 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - unconfig@7.0.0: + unconfig@7.3.2: dependencies: - '@antfu/utils': 8.1.1 + '@quansync/fs': 0.1.4 defu: 6.1.4 jiti: 2.4.2 + quansync: 0.2.10 underscore@1.1.7: {} @@ -21689,32 +21816,59 @@ snapshots: universalify@2.0.1: {} - unocss@66.0.0(postcss@8.5.6)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3)): + unocss@66.4.2(postcss@8.5.6)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)): dependencies: - '@unocss/astro': 66.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3)) - '@unocss/cli': 66.0.0 - '@unocss/core': 66.0.0 - '@unocss/postcss': 66.0.0(postcss@8.5.6) - '@unocss/preset-attributify': 66.0.0 - '@unocss/preset-icons': 66.0.0 - '@unocss/preset-mini': 66.0.0 - '@unocss/preset-tagify': 66.0.0 - '@unocss/preset-typography': 66.0.0 - '@unocss/preset-uno': 66.0.0 - '@unocss/preset-web-fonts': 66.0.0 - '@unocss/preset-wind': 66.0.0 - '@unocss/preset-wind3': 66.0.0 - '@unocss/transformer-attributify-jsx': 66.0.0 - '@unocss/transformer-compile-class': 66.0.0 - '@unocss/transformer-directives': 66.0.0 - '@unocss/transformer-variant-group': 66.0.0 - '@unocss/vite': 66.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.13(typescript@5.7.3)) + '@unocss/astro': 66.4.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + '@unocss/cli': 66.4.2 + '@unocss/core': 66.4.2 + '@unocss/postcss': 66.4.2(postcss@8.5.6) + '@unocss/preset-attributify': 66.4.2 + '@unocss/preset-icons': 66.4.2 + '@unocss/preset-mini': 66.4.2 + '@unocss/preset-tagify': 66.4.2 + '@unocss/preset-typography': 66.4.2 + '@unocss/preset-uno': 66.4.2 + '@unocss/preset-web-fonts': 66.4.2 + '@unocss/preset-wind': 66.4.2 + '@unocss/preset-wind3': 66.4.2 + '@unocss/preset-wind4': 66.4.2 + '@unocss/transformer-attributify-jsx': 66.4.2 + '@unocss/transformer-compile-class': 66.4.2 + '@unocss/transformer-directives': 66.4.2 + '@unocss/transformer-variant-group': 66.4.2 + '@unocss/vite': 66.4.2(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) optionalDependencies: vite: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) transitivePeerDependencies: - postcss - supports-color - - vue + + unocss@66.4.2(postcss@8.5.6)(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)): + dependencies: + '@unocss/astro': 66.4.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + '@unocss/cli': 66.4.2 + '@unocss/core': 66.4.2 + '@unocss/postcss': 66.4.2(postcss@8.5.6) + '@unocss/preset-attributify': 66.4.2 + '@unocss/preset-icons': 66.4.2 + '@unocss/preset-mini': 66.4.2 + '@unocss/preset-tagify': 66.4.2 + '@unocss/preset-typography': 66.4.2 + '@unocss/preset-uno': 66.4.2 + '@unocss/preset-web-fonts': 66.4.2 + '@unocss/preset-wind': 66.4.2 + '@unocss/preset-wind3': 66.4.2 + '@unocss/preset-wind4': 66.4.2 + '@unocss/transformer-attributify-jsx': 66.4.2 + '@unocss/transformer-compile-class': 66.4.2 + '@unocss/transformer-directives': 66.4.2 + '@unocss/transformer-variant-group': 66.4.2 + '@unocss/vite': 66.4.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + optionalDependencies: + vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + transitivePeerDependencies: + - postcss + - supports-color unpipe@1.0.0: {} @@ -21870,6 +22024,17 @@ snapshots: transitivePeerDependencies: - supports-color + vite-plugin-pwa@1.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0): + dependencies: + debug: 4.4.0 + pretty-bytes: 6.1.1 + tinyglobby: 0.2.12 + vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + workbox-build: 7.1.1(@types/babel__core@7.20.5) + workbox-window: 7.3.0 + transitivePeerDependencies: + - supports-color + vite@5.4.19(@types/node@22.13.5)(terser@5.39.0): dependencies: esbuild: 0.21.5 @@ -22051,9 +22216,7 @@ snapshots: vscode-uri@3.1.0: {} - vue-flow-layout@0.1.1(vue@3.5.13(typescript@5.7.3)): - dependencies: - vue: 3.5.13(typescript@5.7.3) + vue-flow-layout@0.2.0: {} vue@3.5.13(typescript@5.7.3): dependencies: @@ -22542,8 +22705,6 @@ snapshots: yallist@3.1.1: {} - yaml@2.7.0: {} - yaml@2.8.0: {} yargs-parser@18.1.3: