diff --git a/.changeset/cold-pillows-judge.md b/.changeset/cold-pillows-judge.md
new file mode 100644
index 00000000000..828a68f3397
--- /dev/null
+++ b/.changeset/cold-pillows-judge.md
@@ -0,0 +1,8 @@
+---
+'mermaid': patch
+---
+
+fix: handle CSS sibling combinators in compileCSS
+
+author: aloisklink
+commit: 12d472c9ed43f94814b110da8d7a9ae6dd5266ed
diff --git a/.changeset/curly-lands-taste.md b/.changeset/curly-lands-taste.md
new file mode 100644
index 00000000000..8194cb614e4
--- /dev/null
+++ b/.changeset/curly-lands-taste.md
@@ -0,0 +1,12 @@
+---
+'mermaid': patch
+---
+
+fix: increase protections against prototype pollution
+
+User-controlled input already has protections against prototype pollution.
+
+Fixes: GHSA-c4c3-pg64-4m4v
+
+commit: 2cd6dcf735533b323507e3e889ffdea870540b43
+author: @aloisklink
diff --git a/.changeset/dry-ducks-agree.md b/.changeset/dry-ducks-agree.md
new file mode 100644
index 00000000000..ea69e7f619d
--- /dev/null
+++ b/.changeset/dry-ducks-agree.md
@@ -0,0 +1,11 @@
+---
+'mermaid': patch
+---
+
+fix(architecture): use `Map`s and `Set`s to store groups/services
+
+Services are now rendered in the order they are defined and more service IDs
+are now supported.
+
+author: aloisklink
+commit: 99af3fc35ef0a9a9c8c6314521344d67523ddccf
diff --git a/.changeset/proud-breads-stick.md b/.changeset/proud-breads-stick.md
new file mode 100644
index 00000000000..627f0a7ae74
--- /dev/null
+++ b/.changeset/proud-breads-stick.md
@@ -0,0 +1,8 @@
+---
+'mermaid': minor
+---
+
+deprecate: Deprecate the `mermaidAPI.setConfig()` function
+
+commit: 2cd6dcf735533b323507e3e889ffdea870540b43
+author: @aloisklink
diff --git a/.changeset/solid-clubs-care.md b/.changeset/solid-clubs-care.md
new file mode 100644
index 00000000000..5fcbda063ee
--- /dev/null
+++ b/.changeset/solid-clubs-care.md
@@ -0,0 +1,8 @@
+---
+'mermaid': patch
+---
+
+fix(xychart): support zero-width x-axis ranges
+
+author: aloisklink
+commit: 630aa7e5dd417e1f56bff2a1ce8df2c5ad08d289
diff --git a/.changeset/wet-emus-sit.md b/.changeset/wet-emus-sit.md
new file mode 100644
index 00000000000..160bcb90f0f
--- /dev/null
+++ b/.changeset/wet-emus-sit.md
@@ -0,0 +1,9 @@
+---
+'mermaid': patch
+---
+
+fix(radar): limit number of ticks to 32
+
+Setting a ticks value higher than this would only show 32 ticks.
+
+commit: 59b22fad2b3bb04f87a476c84a8a2b24679e607e
diff --git a/.cspell/misc-terms.txt b/.cspell/misc-terms.txt
index ff21dd8b6f6..1bee84991b5 100644
--- a/.cspell/misc-terms.txt
+++ b/.cspell/misc-terms.txt
@@ -2,6 +2,7 @@ BRANDES
browserlist
Buzan
circo
+GHSA
handDrawn
KOEPF
MMLU
diff --git a/cypress/integration/rendering/xychart/xyChart.spec.js b/cypress/integration/rendering/xychart/xyChart.spec.js
index e4eb01b9ed2..0736a02025c 100644
--- a/cypress/integration/rendering/xychart/xyChart.spec.js
+++ b/cypress/integration/rendering/xychart/xyChart.spec.js
@@ -102,6 +102,20 @@ describe('XY Chart', () => {
{}
);
});
+ it('x-axis range with same values is supported', () => {
+ imgSnapshotTest(
+ `---
+title: Dirac delta function
+---
+xychart-beta
+ x-axis "Only 0 matters" 0 --> 0
+ line "dirac" [0, 0.5, 1]
+`,
+ {}
+ );
+
+ cy.get('g.plot g.line-plot-0 path').should('exist');
+ });
it('Render spark line with "plotReservedSpacePercent"', () => {
imgSnapshotTest(
`
diff --git a/demos/dev/example.html b/demos/dev/example.html
index cc49ddffb73..487ed70e60a 100644
--- a/demos/dev/example.html
+++ b/demos/dev/example.html
@@ -1,5 +1,4 @@
-
-
+
Mermaid development page
@@ -21,13 +20,16 @@
-
+
+
graph TB
a --> b
a --> c
b --> d
c --> d
-
+
+
info
+
Type code to view diagram:
@@ -35,22 +37,56 @@
-
info
+
+
+
+
+
Error
+
+
+
+
+
+
Render time
+
+
+
+
+
+
diff --git a/docs/config/setup/config/README.md b/docs/config/setup/config/README.md
index fa87267edf5..e84f71a9e8c 100644
--- a/docs/config/setup/config/README.md
+++ b/docs/config/setup/config/README.md
@@ -25,7 +25,6 @@
- [reset](functions/reset.md)
- [sanitize](functions/sanitize.md)
- [saveConfigFromInitialize](functions/saveConfigFromInitialize.md)
-- [setConfig](functions/setConfig.md)
+- [~~setConfig~~](functions/setConfig.md)
- [setSiteConfig](functions/setSiteConfig.md)
-- [updateCurrentConfig](functions/updateCurrentConfig.md)
- [updateSiteConfig](functions/updateSiteConfig.md)
diff --git a/docs/config/setup/config/functions/addDirective.md b/docs/config/setup/config/functions/addDirective.md
index e5a0f3a70f0..804f797b3fa 100644
--- a/docs/config/setup/config/functions/addDirective.md
+++ b/docs/config/setup/config/functions/addDirective.md
@@ -12,7 +12,7 @@
> **addDirective**(`directive`): `void`
-Defined in: [packages/mermaid/src/config.ts:197](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L197)
+Defined in: [packages/mermaid/src/config.ts:173](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L173)
Pushes in a directive to the configuration
diff --git a/docs/config/setup/config/functions/getConfig.md b/docs/config/setup/config/functions/getConfig.md
index 4be63b52ed3..bb76243817d 100644
--- a/docs/config/setup/config/functions/getConfig.md
+++ b/docs/config/setup/config/functions/getConfig.md
@@ -12,18 +12,17 @@
> **getConfig**(): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
-Defined in: [packages/mermaid/src/config.ts:140](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L140)
+Defined in: [packages/mermaid/src/config.ts:120](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L120)
-## getConfig
-
-| Function | Description | Type | Return Values |
-| --------- | ------------------------- | ----------- | ------------------------------ |
-| getConfig | Obtains the currentConfig | Get Request | Any Values from current Config |
-
-**Notes**: Avoid calling this function repeatedly. Instead, store the result in a variable and use it, and pass it down to function calls.
+Returns a copy of the `currentConfig`.
## Returns
[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
The currentConfig
+
+## Remarks
+
+Avoid calling this function repeatedly.
+Instead, store the result in a variable and use it, and pass it down to function calls.
diff --git a/docs/config/setup/config/functions/getEffectiveHtmlLabels.md b/docs/config/setup/config/functions/getEffectiveHtmlLabels.md
index 689fe7afc16..eb1534ae7a9 100644
--- a/docs/config/setup/config/functions/getEffectiveHtmlLabels.md
+++ b/docs/config/setup/config/functions/getEffectiveHtmlLabels.md
@@ -12,7 +12,7 @@
> **getEffectiveHtmlLabels**(`config`): `boolean`
-Defined in: [packages/mermaid/src/config.ts:282](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L282)
+Defined in: [packages/mermaid/src/config.ts:246](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L246)
Helper function to handle deprecated flowchart.htmlLabels
diff --git a/docs/config/setup/config/functions/getSiteConfig.md b/docs/config/setup/config/functions/getSiteConfig.md
index 342eaae7e5b..4c740905835 100644
--- a/docs/config/setup/config/functions/getSiteConfig.md
+++ b/docs/config/setup/config/functions/getSiteConfig.md
@@ -12,15 +12,9 @@
> **getSiteConfig**(): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
-Defined in: [packages/mermaid/src/config.ts:105](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L105)
+Defined in: [packages/mermaid/src/config.ts:94](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L94)
-## getSiteConfig
-
-| Function | Description | Type | Values |
-| ------------- | ------------------------------------------------- | ----------- | -------------------------------- |
-| setSiteConfig | Returns the current siteConfig base configuration | Get Request | Returns Any Values in siteConfig |
-
-**Notes**: Returns **any** values in siteConfig.
+Returns a copy of the current `siteConfig` base configuration.
## Returns
diff --git a/docs/config/setup/config/functions/getUserDefinedConfig.md b/docs/config/setup/config/functions/getUserDefinedConfig.md
index 2510b57b387..73e8094592c 100644
--- a/docs/config/setup/config/functions/getUserDefinedConfig.md
+++ b/docs/config/setup/config/functions/getUserDefinedConfig.md
@@ -12,7 +12,7 @@
> **getUserDefinedConfig**(): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
-Defined in: [packages/mermaid/src/config.ts:263](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L263)
+Defined in: [packages/mermaid/src/config.ts:227](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L227)
## Returns
diff --git a/docs/config/setup/config/functions/reset.md b/docs/config/setup/config/functions/reset.md
index fcd4ea109e1..9873e47bfc5 100644
--- a/docs/config/setup/config/functions/reset.md
+++ b/docs/config/setup/config/functions/reset.md
@@ -12,21 +12,9 @@
> **reset**(`config`): `void`
-Defined in: [packages/mermaid/src/config.ts:230](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L230)
+Defined in: [packages/mermaid/src/config.ts:194](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L194)
-## reset
-
-| Function | Description | Type | Required | Values |
-| -------- | ---------------------------- | ----------- | -------- | ------ |
-| reset | Resets currentConfig to conf | Put Request | Required | None |
-
-## conf
-
-| Parameter | Description | Type | Required | Values |
-| --------- | -------------------------------------------------------------- | ---------- | -------- | -------------------------------------------- |
-| conf | base set of values, which currentConfig could be **reset** to. | Dictionary | Required | Any Values, with respect to the secure Array |
-
-**Notes**: (default: current siteConfig ) (optional, default `getSiteConfig()`)
+Resets the current config and applied directives to the provided config.
## Parameters
@@ -34,8 +22,8 @@ Defined in: [packages/mermaid/src/config.ts:230](https://github.com/mermaid-js/m
[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md) = `siteConfig`
-base set of values, which currentConfig could be **reset** to.
-Defaults to the current siteConfig (e.g returned by [getSiteConfig](getSiteConfig.md)).
+the value to reset the `currentConfig` to.
+Defaults to the current `siteConfig` (e.g the value returned by [getSiteConfig](getSiteConfig.md)).
## Returns
diff --git a/docs/config/setup/config/functions/sanitize.md b/docs/config/setup/config/functions/sanitize.md
index 4eada26ced2..24e49d3c943 100644
--- a/docs/config/setup/config/functions/sanitize.md
+++ b/docs/config/setup/config/functions/sanitize.md
@@ -12,16 +12,9 @@
> **sanitize**(`options`): `void`
-Defined in: [packages/mermaid/src/config.ts:155](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L155)
+Defined in: [packages/mermaid/src/config.ts:131](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L131)
-## sanitize
-
-| Function | Description | Type | Values |
-| -------- | -------------------------------------- | ----------- | ------ |
-| sanitize | Sets the siteConfig to desired values. | Put Request | None |
-
-Ensures options parameter does not attempt to override siteConfig secure keys **Notes**: modifies
-options in-place
+Ensures options parameter does not attempt to override `siteConfig` secure keys.
## Parameters
@@ -29,8 +22,12 @@ options in-place
`any`
-The potential setConfig parameter
+The potential `setConfig` parameter
## Returns
`void`
+
+## Remarks
+
+Modifies options in-place.
diff --git a/docs/config/setup/config/functions/saveConfigFromInitialize.md b/docs/config/setup/config/functions/saveConfigFromInitialize.md
index 08f5528f6c0..209bbabb63f 100644
--- a/docs/config/setup/config/functions/saveConfigFromInitialize.md
+++ b/docs/config/setup/config/functions/saveConfigFromInitialize.md
@@ -12,7 +12,7 @@
> **saveConfigFromInitialize**(`conf`): `void`
-Defined in: [packages/mermaid/src/config.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L84)
+Defined in: [packages/mermaid/src/config.ts:78](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L78)
## Parameters
diff --git a/docs/config/setup/config/functions/setConfig.md b/docs/config/setup/config/functions/setConfig.md
index aab8d946dde..348954dc645 100644
--- a/docs/config/setup/config/functions/setConfig.md
+++ b/docs/config/setup/config/functions/setConfig.md
@@ -8,21 +8,13 @@
---
-# Function: setConfig()
+# ~~Function: setConfig()~~
> **setConfig**(`conf`): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
-Defined in: [packages/mermaid/src/config.ts:122](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L122)
+Defined in: [packages/mermaid/src/config.ts:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L106)
-## setConfig
-
-| Function | Description | Type | Values |
-| ------------- | ------------------------------------- | ----------- | --------------------------------------- |
-| setSiteConfig | Sets the siteConfig to desired values | Put Request | Any Values, except ones in secure array |
-
-**Notes**: Sets the currentConfig. The parameter conf is sanitized based on the siteConfig.secure
-keys. Any values found in conf with key found in siteConfig.secure will be replaced with the
-corresponding siteConfig value.
+Updates the `currentConfig` with the provided `conf` after sanitization.
## Parameters
@@ -37,3 +29,8 @@ The potential currentConfig
[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
The currentConfig merged with the sanitized conf
+
+## Deprecated
+
+Any changes to the `currentConfig` would be overwritten by the
+next call to [addDirective](addDirective.md) or [reset](reset.md).
diff --git a/docs/config/setup/config/functions/setSiteConfig.md b/docs/config/setup/config/functions/setSiteConfig.md
index fb5001aac88..1ea582ec2d1 100644
--- a/docs/config/setup/config/functions/setSiteConfig.md
+++ b/docs/config/setup/config/functions/setSiteConfig.md
@@ -12,18 +12,12 @@
> **setSiteConfig**(`conf`): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
-Defined in: [packages/mermaid/src/config.ts:70](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L70)
+Defined in: [packages/mermaid/src/config.ts:64](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L64)
-## setSiteConfig
+Sets the `siteConfig` to the desired values.
-| Function | Description | Type | Values |
-| ------------- | ------------------------------------- | ----------- | --------------------------------------- |
-| setSiteConfig | Sets the siteConfig to desired values | Put Request | Any Values, except ones in secure array |
-
-**Notes:** Sets the siteConfig. The siteConfig is a protected configuration for repeat use. Calls
-to reset() will reset the currentConfig to siteConfig. Calls to reset(configApi.defaultConfig)
-will reset siteConfig and currentConfig to the defaultConfig Note: currentConfig is set in this
-function _Default value: At default, will mirror Global Config_
+The `siteConfig` is a protected configuration for repeat use. Calls
+to [reset](reset.md) will reset the `currentConfig` to `siteConfig`.
## Parameters
@@ -31,7 +25,7 @@ function _Default value: At default, will mirror Global Config_
[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
-The base currentConfig to use as siteConfig
+The config to use as `siteConfig`. This will be merged with the `defaultConfig`.
## Returns
diff --git a/docs/config/setup/config/functions/updateCurrentConfig.md b/docs/config/setup/config/functions/updateCurrentConfig.md
deleted file mode 100644
index 6a69dcce5e8..00000000000
--- a/docs/config/setup/config/functions/updateCurrentConfig.md
+++ /dev/null
@@ -1,29 +0,0 @@
-> **Warning**
->
-> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
->
-> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/config/functions/updateCurrentConfig.md](../../../../../packages/mermaid/src/docs/config/setup/config/functions/updateCurrentConfig.md).
-
-[**mermaid**](../../README.md)
-
----
-
-# Function: updateCurrentConfig()
-
-> **updateCurrentConfig**(`siteCfg`, `_directives`): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
-
-Defined in: [packages/mermaid/src/config.ts:24](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L24)
-
-## Parameters
-
-### siteCfg
-
-[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
-
-### \_directives
-
-[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)\[]
-
-## Returns
-
-[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
diff --git a/docs/config/setup/config/functions/updateSiteConfig.md b/docs/config/setup/config/functions/updateSiteConfig.md
index 1ce4720d65a..865beb7c801 100644
--- a/docs/config/setup/config/functions/updateSiteConfig.md
+++ b/docs/config/setup/config/functions/updateSiteConfig.md
@@ -12,7 +12,7 @@
> **updateSiteConfig**(`conf`): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
-Defined in: [packages/mermaid/src/config.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L88)
+Defined in: [packages/mermaid/src/config.ts:82](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L82)
## Parameters
diff --git a/packages/mermaid/src/assignWithDepth.ts b/packages/mermaid/src/assignWithDepth.ts
index b04c38b0eb1..a88690088e7 100644
--- a/packages/mermaid/src/assignWithDepth.ts
+++ b/packages/mermaid/src/assignWithDepth.ts
@@ -1,12 +1,14 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-
/**
- * assignWithDepth Extends the functionality of {@link Object.assign} with the
- * ability to merge arbitrary-depth objects For each key in src with path `k` (recursively)
- * performs an Object.assign(dst[`k`], src[`k`]) with a slight change from the typical handling of
- * undefined for dst[`k`]: instead of raising an error, dst[`k`] is auto-initialized to `{}` and
- * effectively merged with src[`k`]
Additionally, dissimilar types will not clobber unless the
- * config.clobber parameter === true. Example:
+ * Extends the functionality of {@link Object.assign} with the
+ * ability to merge arbitrary-depth objects.
+ *
+ * For each key in `src` with path `k` (recursively)
+ * performs an `Object.assign(dst['k'], src['k'])` with a slight change from the
+ * typical handling of `undefined` for `dst['k']`:
+ * instead of raising an error, `dst['k']` is auto-initialized to `{}` and
+ * effectively merged with `src['k']`
+ *
+ * Additionally, dissimilar types will not clobber. Example:
*
* ```
* const config_0 = { foo: { bar: 'bar' }, bar: 'foo' };
@@ -16,21 +18,22 @@
* //-> result: { foo: { bar: 'bar' }, bar: 'bar' }
* ```
*
- * Traditional Object.assign would have clobbered foo in config_0 with foo in config_1. If src is a
- * destructured array of objects and dst is not an array, assignWithDepth will apply each element
- * of src to dst in order.
+ * Traditional {@link Object.assign} would have clobbered `foo` in `config_0` with `foo` in `config_1`.
+ * If `src` is a destructured array of objects and `dst` is not an array,
+ * `assignWithDepth` will apply each element of `src` to `dst` in order.
+ *
* @param dst - The destination of the merge
* @param src - The source object(s) to merge into destination
* @param config -
* * depth: depth to traverse within src and dst for merging
- * * clobber: should dissimilar types clobber
*/
const assignWithDepth = (
- dst: any,
- src: any,
- { depth = 2, clobber = false }: { depth?: number; clobber?: boolean } = {}
-): any => {
- const config: { depth: number; clobber: boolean } = { depth, clobber };
+ dst: unknown,
+ src: unknown,
+ { depth = 2 }: { depth?: number } = {}
+): // eslint-disable-next-line @typescript-eslint/no-explicit-any
+any => {
+ const config = { depth };
if (Array.isArray(src) && !Array.isArray(dst)) {
src.forEach((s) => assignWithDepth(dst, s, config));
return dst;
@@ -42,26 +45,45 @@ const assignWithDepth = (
});
return dst;
}
- if (dst === undefined || depth <= 0) {
+ if (dst === undefined || dst === null || depth <= 0) {
if (dst !== undefined && dst !== null && typeof dst === 'object' && typeof src === 'object') {
return Object.assign(dst, src);
} else {
return src;
}
}
- if (src !== undefined && typeof dst === 'object' && typeof src === 'object') {
- Object.keys(src).forEach((key) => {
- if (
- typeof src[key] === 'object' &&
- src[key] !== null &&
- (dst[key] === undefined || typeof dst[key] === 'object')
- ) {
- if (dst[key] === undefined) {
- dst[key] = Array.isArray(src[key]) ? [] : {};
+ if (src !== undefined && src !== null && typeof dst === 'object' && typeof src === 'object') {
+ const dstWithKeys = dst as typeof dst & Record;
+ Object.entries(src).forEach(([key, srcValue]) => {
+ if (typeof srcValue === 'object') {
+ if (srcValue === null) {
+ return;
+ }
+ if (!Object.hasOwn(dst, key)) {
+ Object.defineProperty(dst, key, {
+ value: undefined,
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ });
+ }
+ if (dstWithKeys[key] === undefined) {
+ dstWithKeys[key] = Array.isArray(srcValue) ? [] : {};
+ }
+ if (typeof dstWithKeys[key] === 'object') {
+ dstWithKeys[key] = assignWithDepth(dstWithKeys[key], srcValue, { depth: depth - 1 });
+ }
+ } else if (typeof dstWithKeys[key] !== 'object') {
+ if (Object.hasOwn(dst, key)) {
+ dstWithKeys[key] = srcValue;
+ } else {
+ Object.defineProperty(dst, key, {
+ value: srcValue,
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ });
}
- dst[key] = assignWithDepth(dst[key], src[key], { depth: depth - 1, clobber });
- } else if (clobber || (typeof dst[key] !== 'object' && typeof src[key] !== 'object')) {
- dst[key] = src[key];
}
});
}
diff --git a/packages/mermaid/src/config.spec.ts b/packages/mermaid/src/config.spec.ts
index 61327affac1..0c55c52f4ca 100644
--- a/packages/mermaid/src/config.spec.ts
+++ b/packages/mermaid/src/config.spec.ts
@@ -4,6 +4,7 @@ import type { MermaidConfig } from './config.type.js';
describe('when working with site config', () => {
beforeEach(() => {
// Resets the site config to default config
+ configApi.reset();
configApi.setSiteConfig({});
});
it('should set site config and config properly', () => {
@@ -29,7 +30,8 @@ describe('when working with site config', () => {
fontSize: 54321,
securityLevel: 'loose',
};
- const cfg: MermaidConfig = configApi.updateCurrentConfig(config_0, [directive]);
+ configApi.addDirective(directive);
+ const cfg: MermaidConfig = configApi.getConfig();
expect(cfg.fontFamily).toEqual(directive.fontFamily);
expect(cfg.fontSize).toBe(config_0.fontSize);
expect(cfg.securityLevel).toBe(config_0.securityLevel);
diff --git a/packages/mermaid/src/config.ts b/packages/mermaid/src/config.ts
index a898b78a6a4..67b771a08a0 100644
--- a/packages/mermaid/src/config.ts
+++ b/packages/mermaid/src/config.ts
@@ -21,7 +21,7 @@ let configFromInitialize: MermaidConfig;
let directives: MermaidConfig[] = [];
let currentConfig: MermaidConfig = assignWithDepth({}, defaultConfig);
-export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: MermaidConfig[]) => {
+const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: MermaidConfig[]) => {
// start with config being the siteConfig
let cfg: MermaidConfig = assignWithDepth({}, siteCfg);
// let sCfg = assignWithDepth(defaultConfig, siteConfigDelta);
@@ -53,18 +53,12 @@ export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: Mermaid
};
/**
- * ## setSiteConfig
+ * Sets the `siteConfig` to the desired values.
*
- * | Function | Description | Type | Values |
- * | ------------- | ------------------------------------- | ----------- | --------------------------------------- |
- * | setSiteConfig | Sets the siteConfig to desired values | Put Request | Any Values, except ones in secure array |
+ * The `siteConfig` is a protected configuration for repeat use. Calls
+ * to {@link reset} will reset the `currentConfig` to `siteConfig`.
*
- * **Notes:** Sets the siteConfig. The siteConfig is a protected configuration for repeat use. Calls
- * to reset() will reset the currentConfig to siteConfig. Calls to reset(configApi.defaultConfig)
- * will reset siteConfig and currentConfig to the defaultConfig Note: currentConfig is set in this
- * function _Default value: At default, will mirror Global Config_
- *
- * @param conf - The base currentConfig to use as siteConfig
+ * @param conf - The config to use as `siteConfig`. This will be merged with the `defaultConfig`.
* @returns The new siteConfig
*/
export const setSiteConfig = (conf: MermaidConfig): MermaidConfig => {
@@ -91,66 +85,48 @@ export const updateSiteConfig = (conf: MermaidConfig): MermaidConfig => {
return siteConfig;
};
+
/**
- * ## getSiteConfig
- *
- * | Function | Description | Type | Values |
- * | ------------- | ------------------------------------------------- | ----------- | -------------------------------- |
- * | setSiteConfig | Returns the current siteConfig base configuration | Get Request | Returns Any Values in siteConfig |
- *
- * **Notes**: Returns **any** values in siteConfig.
+ * Returns a copy of the current `siteConfig` base configuration.
*
* @returns The siteConfig
*/
export const getSiteConfig = (): MermaidConfig => {
return assignWithDepth({}, siteConfig);
};
+
/**
- * ## setConfig
- *
- * | Function | Description | Type | Values |
- * | ------------- | ------------------------------------- | ----------- | --------------------------------------- |
- * | setSiteConfig | Sets the siteConfig to desired values | Put Request | Any Values, except ones in secure array |
- *
- * **Notes**: Sets the currentConfig. The parameter conf is sanitized based on the siteConfig.secure
- * keys. Any values found in conf with key found in siteConfig.secure will be replaced with the
- * corresponding siteConfig value.
+ * Updates the `currentConfig` with the provided `conf` after sanitization.
*
+ * @deprecated Any changes to the `currentConfig` would be overwritten by the
+ * next call to {@link addDirective} or {@link reset}.
* @param conf - The potential currentConfig
* @returns The currentConfig merged with the sanitized conf
*/
export const setConfig = (conf: MermaidConfig): MermaidConfig => {
- checkConfig(conf);
- assignWithDepth(currentConfig, conf);
+ updateCurrentConfig(currentConfig, [conf]);
return getConfig();
};
/**
- * ## getConfig
+ * Returns a copy of the `currentConfig`.
*
- * | Function | Description | Type | Return Values |
- * | --------- | ------------------------- | ----------- | ------------------------------ |
- * | getConfig | Obtains the currentConfig | Get Request | Any Values from current Config |
- *
- * **Notes**: Avoid calling this function repeatedly. Instead, store the result in a variable and use it, and pass it down to function calls.
+ * @remarks Avoid calling this function repeatedly.
+ * Instead, store the result in a variable and use it, and pass it down to function calls.
*
* @returns The currentConfig
*/
export const getConfig = (): MermaidConfig => {
return assignWithDepth({}, currentConfig);
};
+
/**
- * ## sanitize
+ * Ensures options parameter does not attempt to override `siteConfig` secure keys.
*
- * | Function | Description | Type | Values |
- * | -------- | -------------------------------------- | ----------- | ------ |
- * | sanitize | Sets the siteConfig to desired values. | Put Request | None |
+ * @remarks Modifies options in-place.
*
- * Ensures options parameter does not attempt to override siteConfig secure keys **Notes**: modifies
- * options in-place
- *
- * @param options - The potential setConfig parameter
+ * @param options - The potential `setConfig` parameter
*/
export const sanitize = (options: any) => {
if (!options) {
@@ -210,22 +186,10 @@ export const addDirective = (directive: MermaidConfig) => {
};
/**
- * ## reset
- *
- * | Function | Description | Type | Required | Values |
- * | -------- | ---------------------------- | ----------- | -------- | ------ |
- * | reset | Resets currentConfig to conf | Put Request | Required | None |
- *
- * ## conf
- *
- * | Parameter | Description | Type | Required | Values |
- * | --------- | -------------------------------------------------------------- | ---------- | -------- | -------------------------------------------- |
- * | conf | base set of values, which currentConfig could be **reset** to. | Dictionary | Required | Any Values, with respect to the secure Array |
- *
- * **Notes**: (default: current siteConfig ) (optional, default `getSiteConfig()`)
+ * Resets the current config and applied directives to the provided config.
*
- * @param config - base set of values, which currentConfig could be **reset** to.
- * Defaults to the current siteConfig (e.g returned by {@link getSiteConfig}).
+ * @param config - the value to reset the `currentConfig` to.
+ * Defaults to the current `siteConfig` (e.g the value returned by {@link getSiteConfig}).
*/
export const reset = (config = siteConfig): void => {
// Replace current config with siteConfig
diff --git a/packages/mermaid/src/diagrams/architecture/architecture.spec.ts b/packages/mermaid/src/diagrams/architecture/architecture.spec.ts
index 21787da64ae..d3b58b0d3ca 100644
--- a/packages/mermaid/src/diagrams/architecture/architecture.spec.ts
+++ b/packages/mermaid/src/diagrams/architecture/architecture.spec.ts
@@ -195,6 +195,51 @@ describe('architecture diagrams', () => {
});
});
+ it('should throw when missing a group', async () => {
+ const str = `architecture-beta
+ group mermaidPrototypePollutionMarker(cloud)[Marker]
+ service a(server)[A] in __proto__
+ service b(server)[B] in mermaidPrototypePollutionMarker
+ a:R -- L:b`;
+ // __proto__ group does not exist.
+ await expect(parser.parse(str)).rejects.toThrow(/The service \[a]'s parent does not exist./);
+ });
+
+ it('should allow __proto__ as a group name', async () => {
+ const str = `architecture-beta
+ group __proto__(cloud)[Marker]
+ service a(server)[A] in __proto__
+ service b(server)[B] in __proto__
+ a:R -- L:b`;
+ await expect(parser.parse(str)).resolves.not.toThrow();
+ expect(db.getGroups().map((g) => g.id)).toContain('__proto__');
+ });
+
+ it('should block proto pollution via service id', async () => {
+ const str = `architecture-beta
+ group __proto__(cloud)[P]
+ group myPrototypePollutionKey(cloud)[Real]
+ service a(server)[A] in __proto__
+ service b(server)[B] in myPrototypePollutionKey
+ a:R -- L:b`;
+
+ await expect(parser.parse(str)).resolves.not.toThrow();
+ // GHSA-3rrr-jr9j-h3q3 happened in this function
+ const structures = db.getDataStructures();
+ expect(structures.groupAlignments.get(`"__proto__"-"myPrototypePollutionKey"`)).toBeDefined();
+ expect(Object.prototype).not.toHaveProperty('myPrototypePollutionKey');
+ });
+
+ it('should store services in the order they were added', async () => {
+ const str = `architecture-beta
+ service a(server)[A]
+ service 20(server)[B]
+ service 10(server)[C]`;
+ await expect(parser.parse(str)).resolves.not.toThrow();
+ // Old object iteration order rules sorts numeric keys first
+ expect(db.getServices().map((s) => s.id)).toEqual(['a', '20', '10']);
+ });
+
describe('align directive', () => {
it('should parse a row alignment and expose it via getLayoutHints', async () => {
const str = `architecture-beta
diff --git a/packages/mermaid/src/diagrams/architecture/architectureDb.ts b/packages/mermaid/src/diagrams/architecture/architectureDb.ts
index 05379b39fca..43d260cddd2 100644
--- a/packages/mermaid/src/diagrams/architecture/architectureDb.ts
+++ b/packages/mermaid/src/diagrams/architecture/architectureDb.ts
@@ -14,11 +14,11 @@ import {
setDiagramTitle,
} from '../common/commonDb.js';
import type {
- ArchitectureAlignment,
- ArchitectureDirectionPair,
+ ArchitectureAdjacencyList,
ArchitectureDirectionPairMap,
ArchitectureEdge,
ArchitectureGroup,
+ ArchitectureGroupAlignments,
ArchitectureJunction,
ArchitectureLayoutHint,
ArchitectureNode,
@@ -27,6 +27,7 @@ import type {
ArchitectureState,
} from './architectureTypes.js';
import {
+ architectureGroupAlignmentKey,
getArchitectureDirectionAlignment,
getArchitectureDirectionPair,
isArchitectureDirection,
@@ -38,13 +39,13 @@ import {
const DEFAULT_ARCHITECTURE_CONFIG: Required =
DEFAULT_CONFIG.architecture;
export class ArchitectureDB implements DiagramDB {
- private nodes: Record = {};
- private groups: Record = {};
+ private nodes = new Map();
+ private groups = new Map();
private edges: ArchitectureEdge[] = [];
private layoutHints: ArchitectureLayoutHint[] = [];
- private registeredIds: Record = {};
+ private registeredIds = new Map();
private dataStructures?: ArchitectureState['dataStructures'];
- private elements: Record = {};
+ private elements = new Map();
private diagramId = '';
constructor() {
@@ -60,13 +61,13 @@ export class ArchitectureDB implements DiagramDB {
}
public clear(): void {
- this.nodes = {};
- this.groups = {};
+ this.nodes = new Map();
+ this.groups = new Map();
this.edges = [];
this.layoutHints = [];
- this.registeredIds = {};
+ this.registeredIds = new Map();
this.dataStructures = undefined;
- this.elements = {};
+ this.elements = new Map();
this.diagramId = '';
commonClear();
}
@@ -78,28 +79,28 @@ export class ArchitectureDB implements DiagramDB {
title,
iconText,
}: Omit): void {
- if (this.registeredIds[id] !== undefined) {
+ if (this.registeredIds.has(id)) {
throw new Error(
- `The service id [${id}] is already in use by another ${this.registeredIds[id]}`
+ `The service id [${id}] is already in use by another ${this.registeredIds.get(id)}`
);
}
if (parent !== undefined) {
if (id === parent) {
throw new Error(`The service [${id}] cannot be placed within itself`);
}
- if (this.registeredIds[parent] === undefined) {
+ if (!this.registeredIds.has(parent)) {
throw new Error(
`The service [${id}]'s parent does not exist. Please make sure the parent is created before this service`
);
}
- if (this.registeredIds[parent] === 'node') {
+ if (this.registeredIds.get(parent) === 'node') {
throw new Error(`The service [${id}]'s parent is not a group`);
}
}
- this.registeredIds[id] = 'node';
+ this.registeredIds.set(id, 'node');
- this.nodes[id] = {
+ this.nodes.set(id, {
id,
type: 'service',
icon,
@@ -107,86 +108,86 @@ export class ArchitectureDB implements DiagramDB {
title,
edges: [],
in: parent,
- };
+ });
}
public getServices(): ArchitectureService[] {
- return Object.values(this.nodes).filter(isArchitectureService);
+ return [...this.nodes.values()].filter(isArchitectureService);
}
public addJunction({ id, in: parent }: Omit): void {
- if (this.registeredIds[id] !== undefined) {
+ if (this.registeredIds.has(id)) {
throw new Error(
- `The junction id [${id}] is already in use by another ${this.registeredIds[id]}`
+ `The junction id [${id}] is already in use by another ${this.registeredIds.get(id)}`
);
}
if (parent !== undefined) {
if (id === parent) {
throw new Error(`The junction [${id}] cannot be placed within itself`);
}
- if (this.registeredIds[parent] === undefined) {
+ if (!this.registeredIds.has(parent)) {
throw new Error(
`The junction [${id}]'s parent does not exist. Please make sure the parent is created before this junction`
);
}
- if (this.registeredIds[parent] === 'node') {
+ if (this.registeredIds.get(parent) === 'node') {
throw new Error(`The junction [${id}]'s parent is not a group`);
}
}
- this.registeredIds[id] = 'node';
+ this.registeredIds.set(id, 'node');
- this.nodes[id] = {
+ this.nodes.set(id, {
id,
type: 'junction',
edges: [],
in: parent,
- };
+ });
}
public getJunctions(): ArchitectureJunction[] {
- return Object.values(this.nodes).filter(isArchitectureJunction);
+ return [...this.nodes.values()].filter(isArchitectureJunction);
}
public getNodes(): ArchitectureNode[] {
- return Object.values(this.nodes);
+ return [...this.nodes.values()];
}
public getNode(id: string): ArchitectureNode | null {
- return this.nodes[id] ?? null;
+ return this.nodes.get(id) ?? null;
}
public addGroup({ id, icon, in: parent, title }: ArchitectureGroup): void {
- if (this.registeredIds?.[id] !== undefined) {
+ if (this.registeredIds.has(id)) {
throw new Error(
- `The group id [${id}] is already in use by another ${this.registeredIds[id]}`
+ `The group id [${id}] is already in use by another ${this.registeredIds.get(id)}`
);
}
if (parent !== undefined) {
if (id === parent) {
throw new Error(`The group [${id}] cannot be placed within itself`);
}
- if (this.registeredIds?.[parent] === undefined) {
+ if (!this.registeredIds.has(parent)) {
throw new Error(
`The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`
);
}
- if (this.registeredIds?.[parent] === 'node') {
+ if (this.registeredIds.get(parent) === 'node') {
throw new Error(`The group [${id}]'s parent is not a group`);
}
}
- this.registeredIds[id] = 'group';
+ this.registeredIds.set(id, 'group');
- this.groups[id] = {
+ this.groups.set(id, {
id,
icon,
title,
in: parent,
- };
+ });
}
public getGroups(): ArchitectureGroup[] {
- return Object.values(this.groups);
+ return [...this.groups.values()];
}
public addEdge({
lhsId,
@@ -210,19 +211,19 @@ export class ArchitectureDB implements DiagramDB {
);
}
- if (this.nodes[lhsId] === undefined && this.groups[lhsId] === undefined) {
+ if (!this.nodes.has(lhsId) && !this.groups.has(lhsId)) {
throw new Error(
`The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
);
}
- if (this.nodes[rhsId] === undefined && this.groups[rhsId] === undefined) {
+ if (!this.nodes.has(rhsId) && !this.groups.has(rhsId)) {
throw new Error(
`The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
);
}
- const lhsGroupId = this.nodes[lhsId].in;
- const rhsGroupId = this.nodes[rhsId].in;
+ const lhsGroupId = this.nodes.get(lhsId)!.in;
+ const rhsGroupId = this.nodes.get(rhsId)!.in;
if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
throw new Error(
`The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
@@ -247,9 +248,11 @@ export class ArchitectureDB implements DiagramDB {
};
this.edges.push(edge);
- if (this.nodes[lhsId] && this.nodes[rhsId]) {
- this.nodes[lhsId].edges.push(this.edges[this.edges.length - 1]);
- this.nodes[rhsId].edges.push(this.edges[this.edges.length - 1]);
+ const lhsNode = this.nodes.get(lhsId);
+ const rhsNode = this.nodes.get(rhsId);
+ if (lhsNode && rhsNode) {
+ lhsNode.edges.push(this.edges[this.edges.length - 1]);
+ rhsNode.edges.push(this.edges[this.edges.length - 1]);
}
}
@@ -265,7 +268,7 @@ export class ArchitectureDB implements DiagramDB {
}
const seen = new Set();
hint.members.forEach((id) => {
- if (this.registeredIds[id] !== 'node') {
+ if (this.registeredIds.get(id) !== 'node') {
throw new Error(
`align ${hint.direction} references [${id}], which is not a service or junction`
);
@@ -290,28 +293,22 @@ export class ArchitectureDB implements DiagramDB {
public getDataStructures() {
if (this.dataStructures === undefined) {
// Tracks how groups are aligned with one another. Generated while creating the adj list
- const groupAlignments: Record<
- string,
- Record>
- > = {};
+ const groupAlignments: ArchitectureGroupAlignments = new Map();
// Create an adjacency list of the diagram to perform BFS on
- // Outer reduce applied on all services
- // Inner reduce applied on the edges for a service
- const adjList = Object.entries(this.nodes).reduce<
- Record
- >((prevOuter, [id, service]) => {
- prevOuter[id] = service.edges.reduce((prevInner, edge) => {
+ // Outer Map applied on all services
+ // Inner Map applied on the edges for a service
+ const adjList: ArchitectureAdjacencyList = new Map();
+ for (const [id, service] of this.nodes.entries()) {
+ const directionMap: ArchitectureDirectionPairMap = new Map();
+ for (const edge of service.edges) {
// track the direction groups connect to one another
const lhsGroupId = this.getNode(edge.lhsId)?.in;
const rhsGroupId = this.getNode(edge.rhsId)?.in;
if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) {
const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir);
if (alignment !== 'bend') {
- groupAlignments[lhsGroupId] ??= {};
- groupAlignments[lhsGroupId][rhsGroupId] = alignment;
- groupAlignments[rhsGroupId] ??= {};
- groupAlignments[rhsGroupId][lhsGroupId] = alignment;
+ groupAlignments.set(architectureGroupAlignmentKey(lhsGroupId, rhsGroupId), alignment);
}
}
@@ -319,46 +316,49 @@ export class ArchitectureDB implements DiagramDB {
// source is LHS
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
if (pair) {
- prevInner[pair] = edge.rhsId;
+ directionMap.set(pair, edge.rhsId);
}
} else {
// source is RHS
const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir);
if (pair) {
- prevInner[pair] = edge.lhsId;
+ directionMap.set(pair, edge.lhsId);
}
}
- return prevInner;
- }, {});
- return prevOuter;
- }, {});
-
- // Configuration for the initial pass of BFS
- const firstId = Object.keys(adjList)[0];
- const visited = { [firstId]: 1 };
- // If a key is present in this object, it has not been visited
- const notVisited = Object.keys(adjList).reduce(
- (prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),
- {} as Record
- );
+ }
+
+ adjList.set(id, directionMap);
+ }
+
+ const visited = new Set();
+ // If a key is present in this set, it has not been visited
+ const notVisited = new Set(adjList.keys());
// Perform BFS on the adjacency list
const BFS = (startingId: string): ArchitectureSpatialMap => {
- const spatialMap = { [startingId]: [0, 0] };
+ const spatialMap: ArchitectureSpatialMap = new Map([[startingId, [0, 0]]]);
const queue = [startingId];
while (queue.length > 0) {
const id = queue.shift();
if (id) {
- visited[id] = 1;
- delete notVisited[id];
- const adj = adjList[id];
- const [posX, posY] = spatialMap[id];
- Object.entries(adj).forEach(([dir, rhsId]) => {
- if (!visited[rhsId]) {
- spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair(
- [posX, posY],
- dir as ArchitectureDirectionPair
- );
+ visited.add(id);
+ notVisited.delete(id);
+ const adj = adjList.get(id);
+ if (!adj) {
+ throw new Error(
+ `BFS error: adjacency list for id ${id} not found. Please report this as a bug.`
+ );
+ }
+ const pos = spatialMap.get(id);
+ if (!pos) {
+ throw new Error(
+ `BFS error: position for id ${id} not found in spatial map. Please report this as a bug.`
+ );
+ }
+ const [posX, posY] = pos;
+ adj.forEach((rhsId, dir) => {
+ if (!visited.has(rhsId)) {
+ spatialMap.set(rhsId, shiftPositionByArchitectureDirectionPair([posX, posY], dir));
queue.push(rhsId);
}
});
@@ -366,12 +366,15 @@ export class ArchitectureDB implements DiagramDB {
}
return spatialMap;
};
- const spatialMaps = [BFS(firstId)];
+ const spatialMaps: ArchitectureSpatialMap[] = [];
+ // Keep performing BFS until all nodes have been visited.
// If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found
- while (Object.keys(notVisited).length > 0) {
- spatialMaps.push(BFS(Object.keys(notVisited)[0]));
+ while (notVisited.size > 0) {
+ const firstId = notVisited.values().next().value!;
+ spatialMaps.push(BFS(firstId));
}
+
this.dataStructures = {
adjList,
spatialMaps,
@@ -382,11 +385,11 @@ export class ArchitectureDB implements DiagramDB {
}
public setElementForId(id: string, element: D3Element): void {
- this.elements[id] = element;
+ this.elements.set(id, element);
}
public getElementById(id: string): D3Element {
- return this.elements[id];
+ return this.elements.get(id);
}
public getConfig(): Required {
diff --git a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts
index f297ecfe401..75795ba8ccd 100644
--- a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts
+++ b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts
@@ -28,6 +28,7 @@ import {
type ArchitectureGroup,
type ArchitectureService,
ArchitectureDirectionName,
+ architectureGroupAlignmentKey,
edgeData,
getOppositeArchitectureDirection,
isArchitectureDirectionXY,
@@ -158,83 +159,89 @@ function getAlignments(
layoutHints: ArchitectureLayoutHint[] = []
): fcose.FcoseAlignmentConstraint {
/**
- * Flattens the alignment object so nodes in different groups will be in the same alignment array IFF their groups don't connect in a conflicting alignment
+ * Flattens the alignment map so nodes in different groups will be in the same alignment array IFF their groups don't connect in a conflicting alignment
*
* i.e., two groups which connect horizontally should not have nodes with vertical alignments to one another
*
* See: #5952
*
- * @param alignmentObj - alignment object with the outer key being the row/col # and the inner key being the group name mapped to the nodes on that axis in the group
+ * @param alignmentMap - alignment map with the outer key being the row/col # and the inner key being the group name mapped to the nodes on that axis in the group
* @param alignmentDir - alignment direction
- * @returns flattened alignment object with an arbitrary key mapping to nodes in the same row/col
+ * @returns flattened alignment map with an arbitrary key mapping to nodes in the same row/col
*/
const flattenAlignments = (
- alignmentObj: Record>,
+ alignmentMap: Map>,
alignmentDir: ArchitectureAlignment
- ): Record => {
- return Object.entries(alignmentObj).reduce(
- (prev, [dir, alignments]) => {
- // prev is the mapping of x/y coordinate to an array of the nodes in that row/column
- let cnt = 0;
- const arr = Object.entries(alignments); // [group name, array of nodes within the group on axis dir]
- if (arr.length === 1) {
- // If only one group exists in the row/column, we don't need to do anything else
- prev[dir] = arr[0][1];
- return prev;
- }
- for (let i = 0; i < arr.length - 1; i++) {
- for (let j = i + 1; j < arr.length; j++) {
- const [aGroupId, aNodeIds] = arr[i];
- const [bGroupId, bNodeIds] = arr[j];
- const alignment = groupAlignments[aGroupId]?.[bGroupId]; // Get how the two groups are intended to align (undefined if they aren't)
-
- if (alignment === alignmentDir) {
- // If the intended alignment between the two groups is the same as the alignment we are parsing
- prev[dir] ??= [];
- prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds]; // add the node ids of both groups to the axis array in prev
- } else if (aGroupId === 'default' || bGroupId === 'default') {
- // If either of the groups are in the default space (not in a group), use the same behavior as above
- prev[dir] ??= [];
- prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds];
- } else {
- // Otherwise, the nodes in the two groups are not intended to align
- const keyA = `${dir}-${cnt++}`;
- prev[keyA] = aNodeIds;
- const keyB = `${dir}-${cnt++}`;
- prev[keyB] = bNodeIds;
- }
+ ): Map => {
+ // flattened is the mapping of x/y coordinate to an array of the nodes in that row/column
+ const flattened = new Map();
+ for (const [numericDir, alignments] of alignmentMap.entries()) {
+ const dir = `${numericDir}`;
+ let cnt = 0;
+ const arr = [...alignments.entries()]; // [group name, array of nodes within the group on axis dir]
+ if (arr.length === 1) {
+ // If only one group exists in the row/column, we don't need to do anything else
+ flattened.set(dir, arr[0][1]);
+ continue;
+ }
+ for (let i = 0; i < arr.length - 1; i++) {
+ for (let j = i + 1; j < arr.length; j++) {
+ const [aGroupId, aNodeIds] = arr[i];
+ const [bGroupId, bNodeIds] = arr[j];
+ // Get how the two groups are intended to align (undefined if they aren't)
+ const alignment = groupAlignments.get(architectureGroupAlignmentKey(aGroupId, bGroupId));
+
+ if (alignment === alignmentDir) {
+ // If the intended alignment between the two groups is the same as the alignment we are parsing
+ flattened.set(dir, [...(flattened.get(dir) ?? []), ...aNodeIds, ...bNodeIds]); // add the node ids of both groups to the axis array in flattened
+ } else if (aGroupId === 'default' || bGroupId === 'default') {
+ // If either of the groups are in the default space (not in a group), use the same behavior as above
+ flattened.set(dir, [...(flattened.get(dir) ?? []), ...aNodeIds, ...bNodeIds]);
+ } else {
+ // Otherwise, the nodes in the two groups are not intended to align
+ const keyA = `${dir}-${cnt++}`;
+ flattened.set(keyA, aNodeIds);
+ const keyB = `${dir}-${cnt++}`;
+ flattened.set(keyB, bNodeIds);
}
}
-
- return prev;
- },
- {} as Record
- );
+ }
+ }
+ return flattened;
};
const alignments = spatialMaps.map((spatialMap) => {
- const horizontalAlignments: Record> = {};
- const verticalAlignments: Record> = {};
+ const horizontalAlignments = new Map>();
+ const verticalAlignments = new Map>();
- // Group service ids in an object with their x and y coordinate as the key
- Object.entries(spatialMap).forEach(([id, [x, y]]) => {
+ // Group service ids in a map with their x and y coordinate as the key
+ spatialMap.forEach(([x, y], id) => {
const nodeGroup = db.getNode(id)?.in ?? 'default';
+ const horizontalAlignment = horizontalAlignments.get(y) ?? new Map();
+ if (!horizontalAlignments.has(y)) {
+ horizontalAlignments.set(y, horizontalAlignment);
+ }
- horizontalAlignments[y] ??= {};
- horizontalAlignments[y][nodeGroup] ??= [];
- horizontalAlignments[y][nodeGroup].push(id);
+ const verticalAlignment = verticalAlignments.get(x) ?? new Map();
+ if (!verticalAlignments.has(x)) {
+ verticalAlignments.set(x, verticalAlignment);
+ }
- verticalAlignments[x] ??= {};
- verticalAlignments[x][nodeGroup] ??= [];
- verticalAlignments[x][nodeGroup].push(id);
+ for (const alignment of [horizontalAlignment, verticalAlignment]) {
+ const nodeList = alignment.get(nodeGroup) ?? [];
+ if (!alignment.has(nodeGroup)) {
+ alignment.set(nodeGroup, nodeList);
+ }
+ nodeList.push(id);
+ }
});
- // Merge the values of each object into a list if the inner list has at least 2 elements
+ // Merge the values of each map into a list if the inner list has at least 2 elements
return {
- horiz: Object.values(flattenAlignments(horizontalAlignments, 'horizontal')).filter(
+ horiz: [...flattenAlignments(horizontalAlignments, 'horizontal').values()].filter(
(arr) => arr.length > 1
),
- vert: Object.values(flattenAlignments(verticalAlignments, 'vertical')).filter(
+ vert: [...flattenAlignments(verticalAlignments, 'vertical').values()].filter(
(arr) => arr.length > 1
),
};
@@ -311,17 +318,17 @@ function getRelativeConstraints(
}
}
});
- const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`;
+ const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}` as const;
const strToPos = (pos: string) => pos.split(',').map((p) => parseInt(p));
spatialMaps.forEach((spatialMap) => {
- const invSpatialMap = Object.fromEntries(
- Object.entries(spatialMap).map(([id, pos]) => [posToStr(pos), id])
+ const invSpatialMap = new Map(
+ [...spatialMap.entries()].map(([key, value]) => [posToStr(value), key])
);
// perform BFS
const queue = [posToStr([0, 0])];
- const visited: Record = {};
+ const visited: Record, number> = {};
const directions: Record = {
L: [-1, 0],
R: [1, 0],
@@ -332,12 +339,12 @@ function getRelativeConstraints(
const curr = queue.shift();
if (curr) {
visited[curr] = 1;
- const currId = invSpatialMap[curr];
+ const currId = invSpatialMap.get(curr);
if (currId) {
const currPos = strToPos(curr);
Object.entries(directions).forEach(([dir, shift]) => {
const newPos = posToStr([currPos[0] + shift[0], currPos[1] + shift[1]]);
- const newId = invSpatialMap[newPos];
+ const newId = invSpatialMap.get(newPos);
// If there is an adjacent service to the current one and it has not yet been visited
if (newId && !visited[newPos]) {
queue.push(newPos);
diff --git a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts
index db0e81b2e5c..feb11fbcf22 100644
--- a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts
+++ b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts
@@ -106,7 +106,7 @@ export const isValidArchitectureDirectionPair = function (
return x !== 'LL' && x !== 'RR' && x !== 'TT' && x !== 'BB';
};
-export type ArchitectureDirectionPairMap = Partial>;
+export type ArchitectureDirectionPairMap = Map;
/**
* Creates a pair of the directions of each side of an edge. This function should be used instead of manually creating it to ensure that the source is always the first character.
@@ -131,9 +131,9 @@ export const getArchitectureDirectionPair = function (
* @returns a new [x, y] coordinate pair
*/
export const shiftPositionByArchitectureDirectionPair = function (
- [x, y]: number[],
+ [x, y]: [number, number],
pair: ArchitectureDirectionPair
-): number[] {
+): [number, number] {
const lhs = pair[0] as ArchitectureDirection;
const rhs = pair[1] as ArchitectureDirection;
if (isArchitectureDirectionX(lhs)) {
@@ -268,24 +268,22 @@ export interface ArchitectureDB extends DiagramDBBase
getDataStructures: () => ArchitectureDataStructures;
}
-export type ArchitectureAdjacencyList = Record;
-export type ArchitectureSpatialMap = Record;
+export type ArchitectureAdjacencyList = Map;
+export type ArchitectureSpatialMap = Map;
/**
* Maps the direction that groups connect from.
*
- * **Outer key**: ID of group A
- *
- * **Inner key**: ID of group B
- *
- * **Value**: 'vertical' or 'horizontal'
- *
- * Note: tmp[groupA][groupB] == tmp[groupB][groupA]
+ * **Key**: `groupA-groupB`, where groupA and groupB are the JSON string ids of the two groups in alphabetical order.
*/
-export type ArchitectureGroupAlignments = Record<
- string,
- Record>
+export type ArchitectureGroupAlignments = Map<
+ `${string}-${string}`,
+ Exclude
>;
+export const architectureGroupAlignmentKey = (groupA: string, groupB: string) => {
+ const [lowerGroupId, upperGroupId] = [groupA, groupB].sort();
+ return `${JSON.stringify(lowerGroupId)}-${JSON.stringify(upperGroupId)}` as const;
+};
export interface ArchitectureDataStructures {
adjList: ArchitectureAdjacencyList;
@@ -294,12 +292,12 @@ export interface ArchitectureDataStructures {
}
export interface ArchitectureState extends Record {
- nodes: Record;
- groups: Record;
+ nodes: Map;
+ groups: Map;
edges: ArchitectureEdge[];
- registeredIds: Record;
+ registeredIds: Map;
dataStructures?: ArchitectureDataStructures;
- elements: Record;
+ elements: Map;
config: ArchitectureDiagramConfig;
layoutHints: ArchitectureLayoutHint[];
}
diff --git a/packages/mermaid/src/diagrams/radar/db.ts b/packages/mermaid/src/diagrams/radar/db.ts
index ff95a3be0fa..e6d3faeb7c5 100644
--- a/packages/mermaid/src/diagrams/radar/db.ts
+++ b/packages/mermaid/src/diagrams/radar/db.ts
@@ -18,6 +18,7 @@ import type {
Entry,
} from '../../../../parser/dist/src/language/generated/ast.js';
import type { RadarAxis, RadarCurve, RadarOptions, RadarDB, RadarData } from './types.js';
+import { log } from '../../logger.js';
const defaultOptions: RadarOptions = {
showLegend: true,
@@ -27,6 +28,9 @@ const defaultOptions: RadarOptions = {
graticule: 'circle',
};
+// Can't see people using more than this, since the gradient makes the diagram unreadable.
+const MAX_TICKS = 32;
+
const defaultRadarData: RadarData = {
axes: [],
curves: [],
@@ -103,6 +107,13 @@ const setOptions = (options: Option[]) => {
min: (optionMap.min?.value as number) ?? defaultOptions.min,
graticule: (optionMap.graticule?.value as 'circle' | 'polygon') ?? defaultOptions.graticule,
};
+
+ if (data.options.ticks > MAX_TICKS) {
+ log.warn(
+ `Radar diagram ticks (${data.options.ticks}) exceeds maximum allowed (${MAX_TICKS}). Using ${MAX_TICKS} instead.`
+ );
+ data.options.ticks = MAX_TICKS;
+ }
};
const clear = () => {
diff --git a/packages/mermaid/src/diagrams/radar/radar.spec.ts b/packages/mermaid/src/diagrams/radar/radar.spec.ts
index 3a506c69e7a..4284f6287b3 100644
--- a/packages/mermaid/src/diagrams/radar/radar.spec.ts
+++ b/packages/mermaid/src/diagrams/radar/radar.spec.ts
@@ -160,6 +160,21 @@ describe('radar diagrams', () => {
await expect(parser.parse(str)).resolves.not.toThrow();
});
+ it('should limit the number of ticks to MAX_TICKS', async () => {
+ // Otherwise, user can set millions of ticks, which causes DoS
+ for (const { ticks, expected } of [
+ { ticks: 12, expected: 12 }, // under limit
+ { ticks: 32, expected: 32 }, // at limit
+ { ticks: 33, expected: 32 }, // over limit
+ ]) {
+ const str = `radar-beta
+ ticks ${ticks}
+ `;
+ await expect(parser.parse(str)).resolves.not.toThrow();
+ expect(getOptions().ticks).toBe(expected);
+ }
+ });
+
describe('renderer', () => {
describe('relativeRadius', () => {
it('should calculate relative radius', () => {
diff --git a/packages/mermaid/src/diagrams/xychart/xychartDb.ts b/packages/mermaid/src/diagrams/xychart/xychartDb.ts
index d230e609a65..95a8b34d123 100644
--- a/packages/mermaid/src/diagrams/xychart/xychartDb.ts
+++ b/packages/mermaid/src/diagrams/xychart/xychartDb.ts
@@ -151,12 +151,13 @@ function transformDataWithoutCategory(data: number[]): SimplePlotDataType {
if (isLinearAxisData(xyChartData.xAxis)) {
const min = xyChartData.xAxis.min;
const max = xyChartData.xAxis.max;
- const step = (max - min) / (data.length - 1);
- const categories: string[] = [];
- for (let i = min; i <= max; i += step) {
- categories.push(`${i}`);
+ if (data.length === 1) {
+ // For backwards compatibility, avoid divide-by-zero error
+ retData = [[`${min}`, data[0]]];
+ } else {
+ const step = (max - min) / (data.length - 1);
+ retData = data.map((datum, index) => [`${min + index * step}`, datum]);
}
- retData = categories.map((c, i) => [c, data[i]]);
}
return retData;
diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts
index 246dce120b6..4c8f007db62 100644
--- a/packages/mermaid/src/mermaidAPI.spec.ts
+++ b/packages/mermaid/src/mermaidAPI.spec.ts
@@ -477,6 +477,34 @@ describe('mermaidAPI', () => {
);
});
+ it.for([
+ // test for CSS sibling components
+ ['should namespace sibling combinator', '& ~ *', '#someId #someId~*'],
+ // CSS ignores these whitespace characters
+ ['should namespace sibling combinator', '& \n\t \r \f \r\n + *', '#someId #someId+*'],
+ /*
+ * Experimental column combinator,
+ * see https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/Column_combinator
+ *
+ * Output whitespace will probably change once JSDOM supports it.
+ */
+ ['should namespace column combinator', '& || *', '#someId #someId || *'],
+ ['should namespace root if using unsupported declarations', '&', '#someId #someId'],
+ // child combinators and descent combinators are already namespaced
+ ['should not namespace child combinator', '& > *', '#someId>*'],
+ ['should not namespace descent combinator', '& *', '#someId *'],
+ ] as const)('%s %s to %s', ([_description, inputSelector, expectedSelector]) => {
+ const result = createUserStyles(
+ { ...mockConfig, themeCSS: `${inputSelector} { color: red; }` },
+ 'someDiagram',
+ new Map(),
+ '#someId'
+ );
+ expect(result).toEqual(
+ `#someId .edge-pattern-dashed{stroke-dasharray:3;}${expectedSelector}{color:red;}`
+ );
+ });
+
it('should remove unsupported at-rules from user CSS', () => {
const result = createUserStyles(
{
@@ -625,18 +653,18 @@ describe('mermaidAPI', () => {
it('resets mermaid config to global defaults', () => {
const config = {
logLevel: 0,
- securityLevel: 'loose',
+ htmlLabels: false,
} as const;
mermaidAPI.initialize(config);
- mermaidAPI.setConfig({ securityLevel: 'strict', logLevel: 1 });
+ mermaidAPI.setConfig({ logLevel: 1, htmlLabels: true });
expect(mermaidAPI.getConfig().logLevel).toBe(1);
- expect(mermaidAPI.getConfig().securityLevel).toBe('strict');
+ expect(mermaidAPI.getConfig().htmlLabels).toBe(true);
mermaidAPI.reset();
expect(mermaidAPI.getConfig().logLevel).toBe(0);
- expect(mermaidAPI.getConfig().securityLevel).toBe('loose');
+ expect(mermaidAPI.getConfig().htmlLabels).toBe(false);
mermaidAPI.globalReset();
expect(mermaidAPI.getConfig().logLevel).toBe(5);
- expect(mermaidAPI.getConfig().securityLevel).toBe('strict');
+ expect(mermaidAPI.getConfig().htmlLabels).toBe(mermaidAPI.defaultConfig.htmlLabels);
});
it('prevents changes to site defaults (sneaky)', () => {
diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts
index d8627420bc0..6f6219961bb 100644
--- a/packages/mermaid/src/mermaidAPI.ts
+++ b/packages/mermaid/src/mermaidAPI.ts
@@ -238,7 +238,37 @@ const compileCSS = (namespace: `#${string}`, css: string) => {
return;
}
element.props = element.props.map((prop) => {
- if (!prop.startsWith(namespace)) {
+ /**
+ * For the root selector `& { ... }`, allow limited inherited
+ * styles to not be namespaced.
+ * These won't do anything to the `