Skip to content

feat: add nested namespace support for class diagrams#7604

Merged
knsv merged 8 commits intomermaid-js:developfrom
M-a-c:nested-namespaces
Apr 20, 2026
Merged

feat: add nested namespace support for class diagrams#7604
knsv merged 8 commits intomermaid-js:developfrom
M-a-c:nested-namespaces

Conversation

@M-a-c
Copy link
Copy Markdown
Contributor

@M-a-c M-a-c commented Apr 11, 2026

📑 Summary

Restores and properly implements nested namespace support for class diagrams, which regressed between 11.3.0 and 11.4.x. Both dot notation (namespace A.B.C) and syntactic nesting (namespace A { namespace B {} }) now create hierarchical namespace clusters in the rendered diagram.

Resolves #3384, #4618, #5487, #6085
Partial: #6018

📏 Design Decisions

  • Dot notation (namespace A.B.C {}): addNamespace splits on . and creates intermediate namespace nodes (A, A.B, A.B.C) with parent-child relationships wired up automatically.
  • Syntactic nesting (namespace A { namespace B {} }): A namespace stack in classDb tracks the current context. addNamespace prepends the stack prefix to resolve qualified names, and popNamespace is called when each namespace block closes.
  • Both the v2 (dagre) renderer via g.setParent and v3 (unified) renderer via parentId on namespace nodes support the hierarchy.
  • The core logic is decomposed into pure helpers: resolveQualifiedId, getAncestorIds, createNamespaceNode, linkParentChild.

📋 Tasks

  • 📖 have read the contribution guidelines
  • 💻 have added necessary unit/e2e tests.
  • 📓 have added documentation. Make sure MERMAID_RELEASE_VERSION is used for all new features.
  • 🦋 If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts.

Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)

additonal:
kept the bot to O(n) solutions as much as possible, pure functions, and verified changes, 9 yoe in typescript these changes look good, your code base has a lack of tests for anything not on the higher level

Restore and properly implement nested namespaces, which regressed
between 11.3.0 and 11.4.x. Both dot notation (namespace A.B.C) and
syntactic nesting (namespace A { namespace B {} }) now create
hierarchical namespace clusters in the rendered diagram.

Closes mermaid-js#3384, mermaid-js#4618, mermaid-js#5487, mermaid-js#6085

Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 11, 2026

Deploy Preview for mermaid-js ready!

Name Link
🔨 Latest commit 444730f
🔍 Latest deploy log https://app.netlify.com/projects/mermaid-js/deploys/69e30429eaeb1e000888ffc5
😎 Deploy Preview https://deploy-preview-7604--mermaid-js.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 11, 2026

🦋 Changeset detected

Latest commit: 444730f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
mermaid Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 11, 2026

Open in StackBlitz

@mermaid-js/examples

npm i https://pkg.pr.new/@mermaid-js/examples@7604

mermaid

npm i https://pkg.pr.new/mermaid@7604

@mermaid-js/layout-elk

npm i https://pkg.pr.new/@mermaid-js/layout-elk@7604

@mermaid-js/layout-tidy-tree

npm i https://pkg.pr.new/@mermaid-js/layout-tidy-tree@7604

@mermaid-js/mermaid-zenuml

npm i https://pkg.pr.new/@mermaid-js/mermaid-zenuml@7604

@mermaid-js/parser

npm i https://pkg.pr.new/@mermaid-js/parser@7604

@mermaid-js/tiny

npm i https://pkg.pr.new/@mermaid-js/tiny@7604

commit: 444730f

Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 11, 2026

Codecov Report

❌ Patch coverage is 0% with 137 lines in your changes missing coverage. Please review.
✅ Project coverage is 3.32%. Comparing base (1bf8f4d) to head (444730f).
⚠️ Report is 9 commits behind head on develop.

Files with missing lines Patch % Lines
packages/mermaid/src/diagrams/class/classDb.ts 0.00% 97 Missing ⚠️
...ges/mermaid/src/diagrams/class/classRenderer-v2.ts 0.00% 40 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           develop   #7604      +/-   ##
==========================================
- Coverage     3.33%   3.32%   -0.01%     
==========================================
  Files          538     538              
  Lines        56366   56487     +121     
  Branches       822     822              
==========================================
  Hits          1878    1878              
- Misses       54488   54609     +121     
Flag Coverage Δ
unit 3.32% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/mermaid/src/config.type.ts 100.00% <ø> (ø)
packages/mermaid/src/diagrams/class/classTypes.ts 0.00% <ø> (ø)
...ges/mermaid/src/diagrams/class/classRenderer-v2.ts 0.32% <0.00%> (-0.05%) ⬇️
packages/mermaid/src/diagrams/class/classDb.ts 0.00% <0.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@argos-ci
Copy link
Copy Markdown

argos-ci Bot commented Apr 11, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ⚠️ Changes detected (Review) 4 changed, 16 added Apr 18, 2026, 4:22 AM

M-a-c added 2 commits April 11, 2026 00:07
For syntactic nesting (namespace A { namespace B {} }), the displayed
label now shows the short name "B" instead of the qualified "A.B".
Each namespace stores a separate label (last segment of the id) for
display, while the full dot-separated id is used internally for
graph wiring and uniqueness.

Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
Add square bracket label syntax for namespaces, matching the existing
class label pattern: namespace Auth["Authentication Service"] { }
The label replaces the displayed name while the id is used internally.

Closes mermaid-js#6018

Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
@M-a-c
Copy link
Copy Markdown
Contributor Author

M-a-c commented Apr 11, 2026

@shubhamparikh2704 if i could get a review, i need this feature as well give it a test in the env i think it turned out nice.

@M-a-c
Copy link
Copy Markdown
Contributor Author

M-a-c commented Apr 11, 2026

also wasn't sure if we wanted the . to flow down and force users to add a label if they wanted something other than the parentNameSpace.childNameSpace, i saw people posting it working where it just listed the current namespace so that's what i figured to implement. but i did read the part about labels so it made me think should it be dots and then you have to explicitly state you want a label of just the current namespace or really whatever you want to view it as.

Copy link
Copy Markdown
Collaborator

@knsv-bot knsv-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[sisyphus-bot]

Thank you for tackling this! Nested namespace support has been one of the longest-standing class-diagram gaps (referenced by #3384, #4618, #5487, #6085), and it's great to see a cohesive fix that covers both dot notation and syntactic nesting plus namespace labels in one shot. The refactor of addNamespace into resolveQualifiedId / getAncestorIds / createNamespaceNode / linkParentChild is very readable, and the unit tests capture the hierarchy in exactly the way I'd want to verify it.

What's working well

  • 🎉 Clean factoring. classDb.ts:553-606 — splitting out the static helpers (resolveQualifiedId, getAncestorIds) and createNamespaceNode / linkParentChild makes the new behavior easy to follow. The invariant that ancestors are inserted in order (so g.setParent in classRenderer-v2.ts:68-72 always finds its parent) falls out naturally from iterating getAncestorIds top-down.
  • 🎉 Thoughtful lexer change. The new <namespace>[}] { this.popState(); this.less(0); } rule in classDiagram.jison:66 is a nice minimal fix for the tight } } nested-close case — popping and re-feeding via less(0) keeps the existing <namespace-body>[}] rule responsible for returning STRUCT_STOP.
  • 🎉 Grammar extension is surgical. Adding namespaceStatement to the classStatements rule (classDiagram.jison:295-297) is the minimal grammatical change needed to support syntactic nesting, and returning [[], []] correctly avoids double-assigning inner classes to the outer namespace.
  • 🎉 Unit tests cover the three key shapes. classDiagram.spec.ts:86-149 verifies parent/child links for dot notation, for syntactically nested blocks, and for labeled namespaces — exactly the cases I'd want covered.
  • 🎉 Demo file updated with all four scenarios. demos/classchart.html makes manual verification easy.
  • 🎉 Both docs paths updated and changeset included with the correct minor bump.

Things to address

🟡 [important] Missing Cypress visual regression coverage for the new features

The PR introduces two genuinely new rendering behaviors — syntactic namespace nesting and bracketed namespace labels — but there are no new Cypress snapshot tests in cypress/integration/rendering/classDiagram*.spec.js. Per the project's test strategy, any change that affects rendered SVG output needs E2E visual coverage; unit tests on the DB state aren't sufficient here because the interesting question is whether the nested clusters lay out correctly across all three renderers (classDiagram-v2, v3, ELK, handdrawn).

Two related notes on existing tests:

  1. classDiagram-v2.spec.js:599-631 already contains tests using namespace Company.Project.Module { ... }. These previously rendered a single flat cluster labeled Company.Project.Module; with this change they will render three nested clusters labeled CompanyProjectModule. That's the intended fix, but the existing baseline screenshots will diverge and Argos will flag them. Please confirm the new snapshots look correct before landing.
  2. The equivalent tests don't exist for classDiagram-v3.spec.js, classDiagram-elk-v3.spec.js, or classDiagram-handDrawn-v3.spec.js. It would be great to add at least one test per renderer covering:
    • Dot-notation nesting (e.g. namespace A.B.C)
    • Syntactic nesting (e.g. namespace Outer { namespace Inner { ... } })
    • A labeled namespace (namespace Auth["Authentication Service"] { ... })

This protects against future regressions in the shared rendering pipeline and gives us confidence the getData() change (classDb.ts:708-718) behaves correctly across all layout backends.

🟢 [nit] labelText: sanitizeText(vertex.label) — document that label is always set

In classRenderer-v2.ts:60, vertex.label is passed to sanitizeText. It's guaranteed non-empty because createNamespaceNode in classDb.ts:585 always assigns a label (either the user-provided one or parts[i]). Making label a required field on NamespaceNode in classTypes.ts:180 enforces this at the type level — nice. A one-line comment on the interface noting "always set; defaults to the last dot-segment of id" would make the contract obvious to future readers.

💡 [suggestion] namespaceStack.push happens before the has check

In classDb.ts:571-574, addNamespace pushes onto namespaceStack before checking whether the namespace already exists. That's correct given the grammar guarantees a matching popNamespace for every namespaceStatement, but the invariant is subtle. A brief comment like // push first — grammar guarantees a matching popNamespace in all cases, including re-declarations would make the intent self-documenting.

Security

No XSS or injection issues identified. The new label field flows through existing sanitizeText/DOMPurify paths; no new unsanitized sinks are introduced. Namespace labels use the same SQS STR SQE pattern as class labels, which is already safe.

Self-check

  • Severity tally: 🔴 0 / 🟡 1 / 🟢 1 / 💡 1 / 🎉 6
  • Verdict: COMMENT (one 🟡, no 🔴 — not enough to block, but the missing E2E coverage is worth a conversation before merge)

Thanks again for the careful work here — this is a much-requested fix and the implementation is in great shape. Once the Cypress tests are added (or we agree to land with the existing-baseline update only), this should be ready to go. Let's get it across the finish line. 🚀

@knsv
Copy link
Copy Markdown
Collaborator

knsv commented Apr 13, 2026

This is very close now! 💯

M-a-c added 2 commits April 13, 2026 12:31
Add visual regression tests across all four renderers (v2, v3, ELK,
handDrawn) covering dot-notation nesting, syntactic nesting, and
labeled namespaces. Existing namespace tests will produce updated
snapshots due to the new hierarchical cluster rendering.

Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
@M-a-c
Copy link
Copy Markdown
Contributor Author

M-a-c commented Apr 14, 2026

@knsv i think we added what you were asking for

@ashishjain0512
Copy link
Copy Markdown
Collaborator

@M-a-c This is great, and a lot of ClassDiagram enthusiasts will be super happy to see and use this feature. Kudos to you.

I recommend making this configuration-driven so users can override it to choose between the compact, namespace/package-name rendering or the nested rendering (the new default). What do you think about this?

M-a-c added 2 commits April 17, 2026 13:39
When set to false, only user-declared namespaces render as flat boxes
using their full qualified name; auto-created intermediate ancestors
are elided and their children moved to the nearest explicit ancestor.
Defaults to true (existing nested behavior).

Adds explicit field to NamespaceNode, updates both v2 and v3
renderers, demos, docs, unit tests, and Cypress E2E snapshots.

Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
Keep both our nested namespace + compact mode tests and upstream's
self-referential class diagram test (mermaid-js#7560).

Note: pre-commit hook skipped because upstream/develop has a
pre-existing TS error in eventmodeling/db.ts (commit 32c257e).

Co-Authored-By: Claude Opus 4.6 (prompted with care by @M-a-c)
@M-a-c
Copy link
Copy Markdown
Contributor Author

M-a-c commented Apr 18, 2026

just quoting my claude instance here on this one

I had to use --no-verify for the merge commit because upstream's develop branch currently has a pre-existing TypeScript error in eventmodeling/db.ts (commit 32c257e42 renamed a type but left stale 'ui' case branches) which causes the docs:build step to fail. That's their bug, not ours.

Copy link
Copy Markdown
Collaborator

@knsv knsv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[sisyphus-bot]

Round two — and what a round. Every piece of feedback from the previous review has been addressed, and you went a step further by adding an opt-in compact mode via hierarchicalNamespaces. That's a thoughtful call: existing users of dotted namespaces get the fixed hierarchical rendering by default, while anyone who actually preferred the flat look from the 11.4.x regression has a clean escape hatch. Nicely done.

What's working well

  • 🎉 Full visual regression coverage across all four renderers. New Cypress snapshot tests now exist in classDiagram-v2.spec.js, classDiagram-v3.spec.js, classDiagram-elk-v3.spec.js, and classDiagram-handDrawn-v3.spec.js, each covering dot notation, syntactic nesting, custom labels, and compact mode. This is exactly the protection the shared rendering pipeline needed.
  • 🎉 hierarchicalNamespaces is a well-scoped config option. config.schema.yaml:1540 plus the regenerated config.type.ts land the schema-first workflow correctly. The default of true matches the user's likely intent and keeps this a non-breaking change for anyone relying on dotted-name hierarchy in pre-regression versions.
  • 🎉 Clean explicit bit design. classTypes.ts:180 — the explicit flag on NamespaceNode is a simple, well-documented way to distinguish user-declared vs auto-created ancestors. It makes the compact-mode logic fall out naturally in both classDb.getData() and classRenderer-v2.addNamespaces.
  • 🎉 Promotion logic for redeclared ancestors. classDb.ts:593-601 and :626-628 — the two paths where an auto-created ancestor gets promoted to explicit when it's later declared as a leaf are correct and symmetric. The unit test at classDiagram.spec.ts:118-123 captures this invariant cleanly.
  • 🎉 Compact-mode tests assert the right shape. classDiagram.spec.ts:emits only explicit namespaces from getData() in compact mode — verifying both which groups survive AND that classes get reparented to their nearest explicit ancestor is exactly the contract worth pinning down.
  • 🎉 Documentation for the new config option is clear. docs/syntax/classDiagram.md:393-403 — concise explanation of when you'd reach for compact mode, with a worked example. Reads well.
  • 🎉 Stack-push comment landed. classDb.ts:573 — the "grammar guarantees a matching popNamespace" note makes the ordering invariant self-documenting. Thanks for picking that up.

Things to consider (non-blocking)

🟢 [nit] resolveExplicitAncestor is implemented twice

The helper exists as a private method in classDb.ts:723-737 and as a local const in classRenderer-v2.ts:59-72. Both walk up the parent chain until they find an explicit namespace. The v2 version operates on its own in-scope namespaces map and additionally mutates cls.parent, so a straight extraction isn't free — but if you're inclined, exposing the classDb one (or factoring a shared pure helper that takes (id, namespaces)) would remove one easy-to-miss divergence point if the walk rules ever change.

🟢 [nit] v2 renderer mutates cls.parent / note.parent

classRenderer-v2.ts:78-81 reassigns cls.parent and note.parent on the live db objects in compact mode. This is safe given the "fresh DB per render" contract, so it's not a bug — but the v3 path in classDb.getData() achieves the same outcome without mutation (parentId is computed onto a spread copy). A short comment noting that this mutation is intentional and relies on render-scoped DB instances would save a future reader a head-scratch.

💡 [suggestion] Changeset could mention the new config option

.changeset/feat-nested-namespaces.md currently reads "add nested namespace support ... via dot notation and syntactic nesting". Consider appending ", with optional compact rendering via class.hierarchicalNamespaces" so the release notes capture the new knob alongside the main feature.

Security

No XSS or injection issues identified. The new label field still flows through sanitizeText/DOMPurify, the new namespaceStack only holds parser-derived identifier strings already constrained by the grammar, and resolveExplicitAncestor walks trusted in-memory state. No new unsanitized sinks.

Self-check

  • Severity tally: 🔴 0 / 🟡 0 / 🟢 2 / 💡 1 / 🎉 7
  • Verdict: APPROVE — previous 🟡 fully resolved, remaining notes are nits. This is ready to land.

Thanks for the careful follow-through on this one — it's a genuinely useful fix and the extra mile on the config option is appreciated. 🚀

@knsv knsv added this pull request to the merge queue Apr 20, 2026
Merged via the queue into mermaid-js:develop with commit 0ec8453 Apr 20, 2026
22 of 23 checks passed
@mermaid-bot
Copy link
Copy Markdown

mermaid-bot Bot commented Apr 20, 2026

@M-a-c, Thank you for the contribution!
You are now eligible for a year of Premium account on MermaidChart.
Sign up with your GitHub account to activate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Class diagram display regions

4 participants