Skip to content

[pull] develop from mermaid-js:develop - #229

Merged
pull[bot] merged 12 commits into
code:developfrom
mermaid-js:develop
Aug 29, 2026
Merged

[pull] develop from mermaid-js:develop#229
pull[bot] merged 12 commits into
code:developfrom
mermaid-js:develop

Conversation

@pull

@pull pull Bot commented Aug 29, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

ashishjain0512 and others added 12 commits August 25, 2026 11:44
getBuildConfig built the core build's `external` list from `dependencies`
only. mermaid is a *peer* dependency of the layout plugins, so it was not
external, and a runtime (non-type) import of it resolved through the
package `exports` to dist/mermaid.core.mjs — which esbuild then inlined
wholesale.

That stayed invisible while mermaid-layout-elk imported mermaid type-only
(erased at compile time). Once src/render.ts began importing values
(`import mermaid, { createCommonLayoutRenderer } from 'mermaid'`), the
latent misconfiguration turned on: the published core entry went from
3 files / ~36 KB to 106 files / 6.6 MB, bundling every diagram renderer
plus katex and a second, version-skewed mermaid.

The duplicate matters beyond size. Module-level singletons are duplicated
with it, so the plugin renders against its own inlined mermaid rather than
the host's — meaning a mermaid rendering fix does not reach the ELK layout
path until layout-elk is itself rebuilt and republished. That is how the
`edgePaths` -> `edgePath` edge-container rename (#8124) stayed broken for
ELK consumers after the mermaid-side fix.

Push peerDependencies into `external` alongside dependencies. Scope is
narrow: only the `if (core)` branch changes, so the self-contained `esm`
entry still inlines mermaid for standalone and dev-server use, and
mermaid itself declares no peerDependencies, so its own build is
untouched. Verified: elk's core entry returns to 3 files / ~41 KB with
mermaid as a bare external, and a consumer bundle drops from two copies
of mermaid to one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… colour themes

Until now only ER, sequence, git and requirement diagrams read the colour
themes' `borderColorArray` / `bkgColorArray`. Class boxes and flowchart subgraph
containers rendered in a single uniform colour under `redux-color` and
`redux-dark-color`.

Class diagrams: each class gets its own border and fill from the palette,
cycling every 12, exactly as ER entities do. A class box is the structural twin
of an ER entity -- a titled box with member rows naming one distinct participant
-- so the same index-based palette applies. Namespaces and notes stay outside the
cycle: a namespace is a container, and a note carries the theme's fixed note
colour.

Flowchart subgraphs: each container gets its own border and fill. The nodes
inside are deliberately left uniform -- a flowchart node is a step in a flow
rather than a distinct participant, and node colour is already how `classDef` /
`style` convey meaning.

`clusters.js` is shared by every diagram that has subgraphs, so it stamps
`data-color-id` unconditionally and each diagram opts in by emitting the matching
rules in its own stylesheet. State, block and class-namespace containers get an
inert attribute and no colour.

Subgraph colours key off the declaration index rather than a running counter:
`getData()` walks subgraphs in reverse and skips ones hidden inside a collapsed
ancestor, so a counter would hand out colours in reverse reading order and
reshuffle them whenever a subgraph was collapsed.

Explicit user styling still wins. `classDef` / `style` declarations are applied
as inline `style` attributes and none of the new rules are `!important`, so
`style MySubgraph fill:#00ff00` keeps painting the container green -- there is a
test for that.
Extract the shared gate. `COLOR_THEMES` existed in five copies and the stamping
block was duplicated verbatim between `clusters.js` and `classBox.ts`; two
idioms for the same gate had already diverged (`er/styles.ts` keys off the theme
name, `requirement/styles.js` off the array being non-empty). New
`diagrams/common/colorThemeGate.ts` owns `COLOR_THEMES`, `isColorTheme`,
`hasPalette`, `colorSlotCount`, `safeLook` and `stampColorSlot`; the four call
sites import it. The spec was already sitting at the path that implied this
module.

Harden `look` before it reaches a CSS selector. `look` is a top-level config
key, so it is settable from diagram text, and `config.sanitize` only drops values
containing `<`, `>` or `url(data:` -- braces and quotes survive, which is enough
to close the attribute selector early and escape the `#svgId` scoping stylis
applies. Confirmed by probe: the value lands verbatim in the emitted rule.
`safeLook` rejects anything that is not a bare word. NOTE: this pattern is
pre-existing in `er/styles.ts` and `requirement/styles.js` on develop, so it
affects released versions; those are deliberately left for a coordinated fix and
can now import `safeLook` rather than adding a third copy.

Close the collapsedGroup seam. A collapsed subgraph rendered uncoloured beside
tinted siblings. It renders as `.node .collapsed-group` rather than `.cluster`,
so the flowchart stylesheet gained that selector; its own colours are
presentation attributes, which the rules outrank while still losing to user
inline style. Verified by render: First/Second(collapsed)/Third take slots
0/1/2, so collapsing does not reshuffle siblings.

Cover the join. The unit tests checked each end -- the db hands out slots, the
stylesheet emits rules -- but not that the stamped attribute meets the emitted
selector on a real element. Added the two colour themes to
`classDiagram-neo.spec.js` (42 tests) and a new
`flowchart-redux-color-subgraphs.spec.ts` (16) covering nesting, collapse and
user-styled precedence.

Also: derive `PLAIN_THEMES` from the theme registry so a new theme is covered
automatically; use `colorSlotCount` in both stylesheets so they agree when
`THEME_COLOR_LIMIT` is absent; `hasPalette` uses `Array.isArray` so a string
cannot pass as a palette; reconcile the `clusters.js` comment with the code,
which is conditional on the theme rather than unconditional.

Changeset shortened.
Review feedback: cut to one paragraph, matching the norm of the existing
changesets on develop.
…se two seams

The namespace test could not fail. `addClassesToNamespace` early-returns when the
namespace does not exist, and the test never called `addNamespace` -- so no
namespace node was created and the assertion passed on absence rather than on
behaviour. Verified the reviewer's check: giving namespaces `colorIndex: 99`, the
exact thing the test forbids, left all three passing. It now creates the
namespace and attaches a class, and fails on that regression.

Subgraph colour slots did not follow source order for nested subgraphs, contrary
to what the code, the test name and the description all claimed. `addSubGraph`
runs when a subgraph *closes*, so `subGraphs` holds nested ones before their
parent: `Outer { InnerOne, InnerTwo }, Sibling` arrives as [InnerOne, InnerTwo,
Outer, Sibling], and taking the array index handed Outer slot 2 while its own
children took 0 and 1. A pre-order walk of the containment forest recovers source
order, assigned once so the collapsed and expanded branches cannot drift.

The existing test could not have caught it -- three flat subgraphs, where close
order and source order are identical. Replaced with a nested fixture, and
confirmed it is the only test in the file that fails against the old
`colorIndex: i`.

Collapsed markers kept their default colour. The palette reached
`.collapsed-group` but not `.collapsed-indicator` or `.collapsed-separator`,
which take `clusterBorder` further down -- so a collapsed subgraph rendered a
tinted container with default-coloured dots and separator, the same seam one
level down from the one this PR set out to close.

Those selectors also now cover `rough-node`. `collapsedGroup.ts` goes through
`getNodeClasses`, which returns `rough-node` rather than `node` under the
handDrawn look, so a `.node`-only selector left handDrawn collapsed containers
uncoloured. Each descendant is appended to both prefixes separately: writing
`SLOT.node, SLOT.rough-node .thing` would attach the descendant to the last item
of the list only and silently match nothing under the classic look. Checked the
emitted CSS rather than assuming. Clusters were never affected -- `clusters.js`
sets the `cluster` class directly.

Corrected `colorSlotCount`'s docstring: it claimed to clamp to what the palette
can supply, but it never sees a palette -- it only floors a non-numeric
THEME_COLOR_LIMIT. The clamping is `stampColorSlot`'s, via `% palette.length`.
`colorSlotCount` returned `THEME_COLOR_LIMIT`, but `stampColorSlot` wraps at
`palette.length`. The two disagree whenever a palette is longer than the limit:
its tail gets stamped `color-N` with no rule emitted for that slot, and those
items render uncoloured beside their neighbours. Confirmed with a limit of 3 and
a five-entry palette -- the stylesheet emitted slots 0..2 while the fifth item
was stamped `color-4`.

The count now covers the palette as well as the limit, so every slot that can be
stamped has a rule. A shorter palette still emits the full limit and cycles, as
before.

No change for either shipped colour theme: both carry exactly THEME_COLOR_LIMIT
entries, so the two counts already agreed -- which is what hid this. It takes a
`themeVariables` override to reach, the same shape as the length mismatches this
PR's shared gate exists to prevent.

The new assertions name the slots that lost their rule rather than counting
them, and were checked against the old `return limit` -- three fail, listing
slots 3..19.
The stylesheets pass this straight into a `for` condition, so it has to be a
value a loop can finish on. `typeof x === 'number' && x > 0` was not: `Infinity`
satisfies both, and the class and flowchart generators then loop forever instead
of rendering something merely wrong.

Reachable from diagram text, not only from site config, which is worse than the
review put it: `THEME_COLOR_LIMIT: .inf` in front matter parses to `Infinity`
under the `JSON_SCHEMA` mermaid loads YAML with -- confirmed against the pinned
js-yaml. A large finite integer such as `1e9` wedges generation just as
effectively while passing every previous check.

Now accepts only a positive integer up to `MAX_COLOR_SLOTS`, and falls back to
the default otherwise. The cap bounds the loop rather than expressing a design
limit -- every shipped palette has twelve entries, so 64 is generous.

Verified in both directions: the pre-fix bound for `Infinity` is `Infinity` and a
loop on it was still running after 121 million iterations, while class stylesheet
generation with `THEME_COLOR_LIMIT: Infinity` now terminates and emits exactly
twelve slots.
Three rounds of review have now found the same bug in different clothes, because
two places decided the slot space independently: `stampColorSlot` assigns
`colorIndex % palette.length`, while the stylesheets emitted `0 .. limit - 1`.
Every fix kept both deciders and tried to keep them lined up.

  - a limit longer than the palette: tail stamped, no rule emitted
  - a palette longer than the limit (fe5fec1): same, other direction
  - a palette longer than MAX_COLOR_SLOTS: the cap I added to stop `Infinity`
    hanging the loop reintroduced it a third time

There is only one correct count, and it is not a policy choice. `stampColorSlot`
can produce exactly the ids `0 .. palette.length - 1` -- verified by enumerating
500 indices: a two-entry palette yields only slots 0 and 1, a seventy-entry
palette reaches slot 69. So the count is `palette.length`; anything less leaves
items stamped with no rule, anything more is dead CSS. `paletteSlotCount` is now
the single source of truth and both sides read it, which makes the two impossible
to disagree rather than something a bound has to police.

`THEME_COLOR_LIMIT` still governs callers that emit slots without stamping --
timeline numbers `.section-N` classes rather than palette slots -- and stays
bounded there, because a loop runs on it directly and `Infinity` is reachable
from front matter. `MAX_COLOR_SLOTS` now applies only to that path.

The missing test is the point. Nothing asserted the two sides agree, only that
each behaved as its author expected, which is why each round passed its own tests
and failed the next review. `emitted slots and stampable slots agree` compares
the two sets across palette lengths from 1 to MAX+50 and limits including
`Infinity` and `undefined`, per stylesheet as well as per function. Replaying all
three historical forms against it: 21, 13 and 10 assertions fail respectively.

Two of the corrected expectations were fe5fec1's -- "still emits the full limit
for a palette shorter than it" and "keeps the limit when the palette is shorter,
so the cycle still repeats". Both are wrong on the merits: with a two-entry
palette, slots 2..11 can never be stamped, so those were rules nothing could
match. The cycling happens in `stampColorSlot`, not in the rule count. One of the
corrected expectations was my own cap, which was the bug this round found.
…wchart-palette

feat(themes): colour class boxes and flowchart subgraphs in the redux colour themes
…-core-build

fix(build): externalize peerDependencies in core builds
@pull pull Bot locked and limited conversation to collaborators Aug 29, 2026
@pull pull Bot added the ⤵️ pull label Aug 29, 2026
@pull
pull Bot merged commit a8bff7b into code:develop Aug 29, 2026
11 of 12 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant