Compare commits

..

16 Commits

Author SHA1 Message Date
Sidharth Vinod
3944e9ac00 chore: Remove circular dependency 2023-08-22 13:36:32 +05:30
Sidharth Vinod
7e251ee8bc Merge remote-tracking branch 'origin/develop' into feature/frontmatterConfig
* origin/develop:
  chore: Remove duplicate CI action
  chore: Add circular dependency check in CI
  refactor: Remove circular dependencies
2023-08-22 13:30:36 +05:30
Knut Sveidqvist
6e0f41180f Merge pull request #4761 from mermaid-js/sidv/RemoveCircularDeps
Remove Circular Dependencies
2023-08-22 07:37:32 +00:00
Sidharth Vinod
fd45dbfc14 Merge branch 'develop' into feature/frontmatterConfig 2023-08-21 14:18:12 +00:00
sidharthv96
5c9eafabae Update docs 2023-08-21 09:36:24 +00:00
Sidharth Vinod
2b9dc0ea80 docs: Add frontmatter config demos 2023-08-21 15:02:29 +05:30
Sidharth Vinod
8ac7dc81e0 docs: Add frontmatter config docs 2023-08-21 15:02:05 +05:30
Sidharth Vinod
2967b3c1bb fix: XSS vulnerability 2023-08-21 14:25:25 +05:30
Sidharth Vinod
534fd85339 chore: Minor typo fixes 2023-08-21 12:53:29 +05:30
Sidharth Vinod
7298008374 Merge branch 'feature/frontmatterConfig' of https://github.com/mermaid-js/mermaid into feature/frontmatterConfig
* 'feature/frontmatterConfig' of https://github.com/mermaid-js/mermaid:
  Update docs
2023-08-21 12:30:23 +05:30
Sidharth Vinod
844a175039 chore: Add test with both frontmatter and directive 2023-08-21 12:30:04 +05:30
sidharthv96
fd35a54735 Update docs 2023-08-21 06:43:15 +00:00
Sidharth Vinod
a6e6c3fb18 feat: Add support for config in frontmatter 2023-08-21 12:09:38 +05:30
Sidharth Vinod
767baa4ec6 chore: Fix type in assignWithDepth 2023-08-21 10:18:23 +05:30
Sidharth Vinod
f422a66dde refactor: Move sanitizeDirective into addDirective 2023-08-21 10:17:55 +05:30
Sidharth Vinod
fae976e994 refactor: Rename and cleanup directiveSanitizer 2023-08-21 10:13:48 +05:30
24 changed files with 346 additions and 168 deletions

View File

@@ -44,6 +44,7 @@
"faber", "faber",
"flatmap", "flatmap",
"foswiki", "foswiki",
"frontmatter",
"ftplugin", "ftplugin",
"gantt", "gantt",
"gitea", "gitea",

View File

@@ -14,7 +14,6 @@ describe('Configuration and directives - nodes should be light blue', () => {
`, `,
{} {}
); );
cy.get('svg');
}); });
it('Settings from initialize - nodes should be green', () => { it('Settings from initialize - nodes should be green', () => {
imgSnapshotTest( imgSnapshotTest(
@@ -28,7 +27,6 @@ graph TD
end `, end `,
{ theme: 'forest' } { theme: 'forest' }
); );
cy.get('svg');
}); });
it('Settings from initialize overriding themeVariable - nodes should be red', () => { it('Settings from initialize overriding themeVariable - nodes should be red', () => {
imgSnapshotTest( imgSnapshotTest(
@@ -46,7 +44,6 @@ graph TD
`, `,
{ theme: 'base', themeVariables: { primaryColor: '#ff0000' }, logLevel: 0 } { theme: 'base', themeVariables: { primaryColor: '#ff0000' }, logLevel: 0 }
); );
cy.get('svg');
}); });
it('Settings from directive - nodes should be grey', () => { it('Settings from directive - nodes should be grey', () => {
imgSnapshotTest( imgSnapshotTest(
@@ -62,7 +59,24 @@ graph TD
`, `,
{} {}
); );
cy.get('svg'); });
it('Settings from frontmatter - nodes should be grey', () => {
imgSnapshotTest(
`
---
config:
theme: neutral
---
graph TD
A(Start) --> B[/Another/]
A[/Another/] --> C[End]
subgraph section
B
C
end
`,
{}
);
}); });
it('Settings from directive overriding theme variable - nodes should be red', () => { it('Settings from directive overriding theme variable - nodes should be red', () => {
@@ -79,7 +93,6 @@ graph TD
`, `,
{} {}
); );
cy.get('svg');
}); });
it('Settings from initialize and directive - nodes should be grey', () => { it('Settings from initialize and directive - nodes should be grey', () => {
imgSnapshotTest( imgSnapshotTest(
@@ -95,7 +108,6 @@ graph TD
`, `,
{ theme: 'forest' } { theme: 'forest' }
); );
cy.get('svg');
}); });
it('Theme from initialize, directive overriding theme variable - nodes should be red', () => { it('Theme from initialize, directive overriding theme variable - nodes should be red', () => {
imgSnapshotTest( imgSnapshotTest(
@@ -111,8 +123,71 @@ graph TD
`, `,
{ theme: 'base' } { theme: 'base' }
); );
cy.get('svg');
}); });
it('Theme from initialize, frontmatter overriding theme variable - nodes should be red', () => {
imgSnapshotTest(
`
---
config:
theme: base
themeVariables:
primaryColor: '#ff0000'
---
graph TD
A(Start) --> B[/Another/]
A[/Another/] --> C[End]
subgraph section
B
C
end
`,
{ theme: 'forest' }
);
});
it('Theme from initialize, frontmatter overriding theme variable, directive overriding primaryColor - nodes should be red', () => {
imgSnapshotTest(
`
---
config:
theme: base
themeVariables:
primaryColor: '#00ff00'
---
%%{init: {'theme': 'base', 'themeVariables':{ 'primaryColor': '#ff0000'}}}%%
graph TD
A(Start) --> B[/Another/]
A[/Another/] --> C[End]
subgraph section
B
C
end
`,
{ theme: 'forest' }
);
});
it('should render if values are not quoted properly', () => {
// #ff0000 is not quoted properly, and will evaluate to null.
// This test ensures that the rendering still works.
imgSnapshotTest(
`---
config:
theme: base
themeVariables:
primaryColor: #ff0000
---
graph TD
A(Start) --> B[/Another/]
A[/Another/] --> C[End]
subgraph section
B
C
end
`,
{ theme: 'forest' }
);
});
it('Theme variable from initialize, theme from directive - nodes should be red', () => { it('Theme variable from initialize, theme from directive - nodes should be red', () => {
imgSnapshotTest( imgSnapshotTest(
` `
@@ -127,13 +202,11 @@ graph TD
`, `,
{ themeVariables: { primaryColor: '#ff0000' } } { themeVariables: { primaryColor: '#ff0000' } }
); );
cy.get('svg');
}); });
describe('when rendering several diagrams', () => { describe('when rendering several diagrams', () => {
it('diagrams should not taint later diagrams', () => { it('diagrams should not taint later diagrams', () => {
const url = 'http://localhost:9000/theme-directives.html'; const url = 'http://localhost:9000/theme-directives.html';
cy.visit(url); cy.visit(url);
cy.get('svg');
cy.matchImageSnapshot('conf-and-directives.spec-when-rendering-several-diagrams-diagram-1'); cy.matchImageSnapshot('conf-and-directives.spec-when-rendering-several-diagrams-diagram-1');
}); });
}); });

View File

@@ -21,6 +21,8 @@
<pre class="mermaid"> <pre class="mermaid">
--- ---
title: This is a title title: This is a title
config:
theme: forest
--- ---
erDiagram erDiagram
%% title This is a title %% title This is a title

View File

@@ -123,6 +123,13 @@
<h3>flowchart</h3> <h3>flowchart</h3>
<pre class="mermaid"> <pre class="mermaid">
---
title: This is another complicated flow
config:
theme: base
flowchart:
curve: cardinal
---
flowchart LR flowchart LR
sid-B3655226-6C29-4D00-B685-3D5C734DC7E1[" sid-B3655226-6C29-4D00-B685-3D5C734DC7E1["

View File

@@ -10,10 +10,41 @@ When mermaid starts, configuration is extracted to determine a configuration to
- The default configuration - The default configuration
- Overrides at the site level are set by the initialize call, and will be applied to all diagrams in the site/app. The term for this is the **siteConfig**. - Overrides at the site level are set by the initialize call, and will be applied to all diagrams in the site/app. The term for this is the **siteConfig**.
- Directives - diagram authors can update select configuration parameters directly in the diagram code via directives. These are applied to the render config. - Frontmatter (v\<MERMAID_RELEASE_VERSION>+) - diagram authors can update select configuration parameters in the frontmatter of the diagram. These are applied to the render config.
- Directives (Deprecated by Frontmatter) - diagram authors can update select configuration parameters directly in the diagram code via directives. These are applied to the render config.
**The render config** is configuration that is used when rendering by applying these configurations. **The render config** is configuration that is used when rendering by applying these configurations.
## Frontmatter config
The entire mermaid configuration (except the secure configs) can be overridden by the diagram author in the frontmatter of the diagram. The frontmatter is a YAML block at the top of the diagram.
```mermaid-example
---
title: Hello Title
config:
theme: base
themeVariables:
primaryColor: "#00ff00"
---
flowchart
Hello --> World
```
```mermaid
---
title: Hello Title
config:
theme: base
themeVariables:
primaryColor: "#00ff00"
---
flowchart
Hello --> World
```
## Theme configuration ## Theme configuration
## Starting mermaid ## Starting mermaid

View File

@@ -6,6 +6,9 @@
# Directives # Directives
> **Warning**
> Directives are deprecated from v\<MERMAID_RELEASE_VERSION>. Please use the `config` key in frontmatter to pass configuration. See [Configuration](./configuration.md) for more details.
## Directives ## Directives
Directives give a diagram author the capability to alter the appearance of a diagram before rendering by changing the applied configuration. Directives give a diagram author the capability to alter the appearance of a diagram before rendering by changing the applied configuration.

View File

@@ -16,4 +16,4 @@
#### Defined in #### Defined in
[mermaidAPI.ts:77](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L77) [mermaidAPI.ts:78](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L78)

View File

@@ -39,7 +39,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
#### Defined in #### Defined in
[mermaidAPI.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L97) [mermaidAPI.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L98)
--- ---
@@ -51,4 +51,4 @@ The svg code for the rendered graph.
#### Defined in #### Defined in
[mermaidAPI.ts:87](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L87) [mermaidAPI.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L88)

View File

@@ -14,7 +14,7 @@
#### Defined in #### Defined in
[config.ts:7](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L7) [config.ts:8](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L8)
## Functions ## Functions
@@ -26,9 +26,9 @@ Pushes in a directive to the configuration
#### Parameters #### Parameters
| Name | Type | Description | | Name | Type | Description |
| :---------- | :---- | :----------------------- | | :---------- | :-------------- | :----------------------- |
| `directive` | `any` | The directive to push in | | `directive` | `MermaidConfig` | The directive to push in |
#### Returns #### Returns
@@ -36,7 +36,7 @@ Pushes in a directive to the configuration
#### Defined in #### Defined in
[config.ts:191](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L191) [config.ts:188](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L188)
--- ---
@@ -60,7 +60,7 @@ The currentConfig
#### Defined in #### Defined in
[config.ts:137](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L137) [config.ts:131](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L131)
--- ---
@@ -118,7 +118,7 @@ The siteConfig
#### Defined in #### Defined in
[config.ts:223](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L223) [config.ts:218](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L218)
--- ---
@@ -147,7 +147,7 @@ options in-place
#### Defined in #### Defined in
[config.ts:152](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L152) [config.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L146)
--- ---
@@ -242,10 +242,10 @@ The new siteConfig
#### Parameters #### Parameters
| Name | Type | | Name | Type |
| :------------ | :-------------- | | :------------ | :----------------- |
| `siteCfg` | `MermaidConfig` | | `siteCfg` | `MermaidConfig` |
| `_directives` | `any`\[] | | `_directives` | `MermaidConfig`\[] |
#### Returns #### Returns
@@ -253,7 +253,7 @@ The new siteConfig
#### Defined in #### Defined in
[config.ts:14](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L14) [config.ts:15](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L15)
--- ---

View File

@@ -10,7 +10,7 @@
### configKeys ### configKeys
`Const` **configKeys**: `string`\[] `Const` **configKeys**: `Set`<`string`>
#### Defined in #### Defined in

View File

@@ -25,7 +25,7 @@ Renames and re-exports [mermaidAPI](mermaidAPI.md#mermaidapi)
#### Defined in #### Defined in
[mermaidAPI.ts:81](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L81) [mermaidAPI.ts:82](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L82)
## Variables ## Variables
@@ -96,7 +96,7 @@ mermaid.initialize(config);
#### Defined in #### Defined in
[mermaidAPI.ts:668](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L668) [mermaidAPI.ts:673](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L673)
## Functions ## Functions
@@ -127,7 +127,7 @@ Return the last node appended
#### Defined in #### Defined in
[mermaidAPI.ts:309](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L309) [mermaidAPI.ts:310](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L310)
--- ---
@@ -153,7 +153,7 @@ the cleaned up svgCode
#### Defined in #### Defined in
[mermaidAPI.ts:255](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L255) [mermaidAPI.ts:256](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L256)
--- ---
@@ -179,7 +179,7 @@ the string with all the user styles
#### Defined in #### Defined in
[mermaidAPI.ts:184](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L184) [mermaidAPI.ts:185](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L185)
--- ---
@@ -202,7 +202,7 @@ the string with all the user styles
#### Defined in #### Defined in
[mermaidAPI.ts:232](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L232) [mermaidAPI.ts:233](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L233)
--- ---
@@ -229,7 +229,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
#### Defined in #### Defined in
[mermaidAPI.ts:168](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L168) [mermaidAPI.ts:169](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L169)
--- ---
@@ -249,7 +249,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
#### Defined in #### Defined in
[mermaidAPI.ts:154](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L154) [mermaidAPI.ts:155](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L155)
--- ---
@@ -269,7 +269,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
#### Defined in #### Defined in
[mermaidAPI.ts:125](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L125) [mermaidAPI.ts:126](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L126)
--- ---
@@ -295,7 +295,7 @@ Put the svgCode into an iFrame. Return the iFrame code
#### Defined in #### Defined in
[mermaidAPI.ts:286](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L286) [mermaidAPI.ts:287](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L287)
--- ---
@@ -320,4 +320,4 @@ Remove any existing elements from the given document
#### Defined in #### Defined in
[mermaidAPI.ts:359](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L359) [mermaidAPI.ts:360](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L360)

View File

@@ -48,7 +48,7 @@ export class Diagram {
// extractFrontMatter(). // extractFrontMatter().
this.parser.parse = (text: string) => this.parser.parse = (text: string) =>
originalParse(cleanupComments(extractFrontMatter(text, this.db))); originalParse(cleanupComments(extractFrontMatter(text, this.db, configApi.addDirective)));
this.parser.parser.yy = this.db; this.parser.parser.yy = this.db;
this.init = diagram.init; this.init = diagram.init;

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/** /**
* assignWithDepth Extends the functionality of {@link ObjectConstructor.assign} with the * 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) * 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 * 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 * undefined for dst[`k`]: instead of raising an error, dst[`k`] is auto-initialized to `{}` and

View File

@@ -1,11 +1,13 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as configApi from './config.js'; import * as configApi from './config.js';
import type { MermaidConfig } from './config.type.js';
describe('when working with site config', function () { describe('when working with site config', () => {
beforeEach(() => { beforeEach(() => {
// Resets the site config to default config // Resets the site config to default config
configApi.setSiteConfig({}); configApi.setSiteConfig({});
}); });
it('should set site config and config properly', function () { it('should set site config and config properly', () => {
const config_0 = { fontFamily: 'foo-font', fontSize: 150 }; const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config_0); configApi.setSiteConfig(config_0);
const config_1 = configApi.getSiteConfig(); const config_1 = configApi.getSiteConfig();
@@ -14,19 +16,26 @@ describe('when working with site config', function () {
expect(config_1.fontSize).toEqual(config_0.fontSize); expect(config_1.fontSize).toEqual(config_0.fontSize);
expect(config_1).toEqual(config_2); expect(config_1).toEqual(config_2);
}); });
it('should respect secure keys when applying directives', function () { it('should respect secure keys when applying directives', () => {
const config_0 = { const config_0: MermaidConfig = {
fontFamily: 'foo-font', fontFamily: 'foo-font',
securityLevel: 'strict', // can't be changed
fontSize: 12345, // can't be changed fontSize: 12345, // can't be changed
secure: [...configApi.defaultConfig.secure!, 'fontSize'], secure: [...configApi.defaultConfig.secure!, 'fontSize'],
}; };
configApi.setSiteConfig(config_0); configApi.setSiteConfig(config_0);
const directive = { fontFamily: 'baf', fontSize: 54321 /* fontSize shouldn't be changed */ }; const directive: MermaidConfig = {
const cfg = configApi.updateCurrentConfig(config_0, [directive]); fontFamily: 'baf',
// fontSize and securityLevel shouldn't be changed
fontSize: 54321,
securityLevel: 'loose',
};
const cfg: MermaidConfig = configApi.updateCurrentConfig(config_0, [directive]);
expect(cfg.fontFamily).toEqual(directive.fontFamily); expect(cfg.fontFamily).toEqual(directive.fontFamily);
expect(cfg.fontSize).toBe(config_0.fontSize); expect(cfg.fontSize).toBe(config_0.fontSize);
expect(cfg.securityLevel).toBe(config_0.securityLevel);
}); });
it('should allow setting partial options', function () { it('should allow setting partial options', () => {
const defaultConfig = configApi.getConfig(); const defaultConfig = configApi.getConfig();
configApi.setConfig({ configApi.setConfig({
@@ -42,7 +51,7 @@ describe('when working with site config', function () {
updatedConfig.quadrantChart!.chartWidth updatedConfig.quadrantChart!.chartWidth
); );
}); });
it('should set reset config properly', function () { it('should set reset config properly', () => {
const config_0 = { fontFamily: 'foo-font', fontSize: 150 }; const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config_0); configApi.setSiteConfig(config_0);
const config_1 = { fontFamily: 'baf' }; const config_1 = { fontFamily: 'baf' };
@@ -55,7 +64,7 @@ describe('when working with site config', function () {
const config_4 = configApi.getSiteConfig(); const config_4 = configApi.getSiteConfig();
expect(config_4.fontFamily).toEqual(config_0.fontFamily); expect(config_4.fontFamily).toEqual(config_0.fontFamily);
}); });
it('should set global reset config properly', function () { it('should set global reset config properly', () => {
const config_0 = { fontFamily: 'foo-font', fontSize: 150 }; const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config_0); configApi.setSiteConfig(config_0);
const config_1 = configApi.getSiteConfig(); const config_1 = configApi.getSiteConfig();

View File

@@ -3,15 +3,16 @@ import { log } from './logger.js';
import theme from './themes/index.js'; import theme from './themes/index.js';
import config from './defaultConfig.js'; import config from './defaultConfig.js';
import type { MermaidConfig } from './config.type.js'; import type { MermaidConfig } from './config.type.js';
import { sanitizeDirective } from './utils.js';
export const defaultConfig: MermaidConfig = Object.freeze(config); export const defaultConfig: MermaidConfig = Object.freeze(config);
let siteConfig: MermaidConfig = assignWithDepth({}, defaultConfig); let siteConfig: MermaidConfig = assignWithDepth({}, defaultConfig);
let configFromInitialize: MermaidConfig; let configFromInitialize: MermaidConfig;
let directives: any[] = []; let directives: MermaidConfig[] = [];
let currentConfig: MermaidConfig = assignWithDepth({}, defaultConfig); let currentConfig: MermaidConfig = assignWithDepth({}, defaultConfig);
export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: any[]) => { export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: MermaidConfig[]) => {
// start with config being the siteConfig // start with config being the siteConfig
let cfg: MermaidConfig = assignWithDepth({}, siteCfg); let cfg: MermaidConfig = assignWithDepth({}, siteCfg);
// let sCfg = assignWithDepth(defaultConfig, siteConfigDelta); // let sCfg = assignWithDepth(defaultConfig, siteConfigDelta);
@@ -20,7 +21,6 @@ export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: any[])
let sumOfDirectives: MermaidConfig = {}; let sumOfDirectives: MermaidConfig = {};
for (const d of _directives) { for (const d of _directives) {
sanitize(d); sanitize(d);
// Apply the data from the directive where the the overrides the themeVariables // Apply the data from the directive where the the overrides the themeVariables
sumOfDirectives = assignWithDepth(sumOfDirectives, d); sumOfDirectives = assignWithDepth(sumOfDirectives, d);
} }
@@ -111,12 +111,6 @@ export const getSiteConfig = (): MermaidConfig => {
* @returns The currentConfig merged with the sanitized conf * @returns The currentConfig merged with the sanitized conf
*/ */
export const setConfig = (conf: MermaidConfig): MermaidConfig => { export const setConfig = (conf: MermaidConfig): MermaidConfig => {
// sanitize(conf);
// Object.keys(conf).forEach(key => {
// const manipulator = manipulators[key];
// conf[key] = manipulator ? manipulator(conf[key]) : conf[key];
// });
checkConfig(conf); checkConfig(conf);
assignWithDepth(currentConfig, conf); assignWithDepth(currentConfig, conf);
@@ -150,9 +144,12 @@ export const getConfig = (): MermaidConfig => {
* @param options - The potential setConfig parameter * @param options - The potential setConfig parameter
*/ */
export const sanitize = (options: any) => { export const sanitize = (options: any) => {
if (!options) {
return;
}
// Checking that options are not in the list of excluded options // Checking that options are not in the list of excluded options
['secure', ...(siteConfig.secure ?? [])].forEach((key) => { ['secure', ...(siteConfig.secure ?? [])].forEach((key) => {
if (options[key] !== undefined) { if (Object.hasOwn(options, key)) {
// DO NOT attempt to print options[key] within `${}` as a malicious script // DO NOT attempt to print options[key] within `${}` as a malicious script
// can exploit the logger's attempt to stringify the value and execute arbitrary code // can exploit the logger's attempt to stringify the value and execute arbitrary code
log.debug(`Denied attempt to modify a secure key ${key}`, options[key]); log.debug(`Denied attempt to modify a secure key ${key}`, options[key]);
@@ -162,7 +159,7 @@ export const sanitize = (options: any) => {
// Check that there no attempts of prototype pollution // Check that there no attempts of prototype pollution
Object.keys(options).forEach((key) => { Object.keys(options).forEach((key) => {
if (key.indexOf('__') === 0) { if (key.startsWith('__')) {
delete options[key]; delete options[key];
} }
}); });
@@ -188,16 +185,14 @@ export const sanitize = (options: any) => {
* *
* @param directive - The directive to push in * @param directive - The directive to push in
*/ */
export const addDirective = (directive: any) => { export const addDirective = (directive: MermaidConfig) => {
if (directive.fontFamily) { sanitizeDirective(directive);
if (!directive.themeVariables) {
directive.themeVariables = { fontFamily: directive.fontFamily }; // If the directive has a fontFamily, but no themeVariables, add the fontFamily to the themeVariables
} else { if (directive.fontFamily && (!directive.themeVariables || !directive.themeVariables.fontFamily)) {
if (!directive.themeVariables.fontFamily) { directive.themeVariables = { fontFamily: directive.fontFamily };
directive.themeVariables = { fontFamily: directive.fontFamily };
}
}
} }
directives.push(directive); directives.push(directive);
updateCurrentConfig(siteConfig, directives); updateCurrentConfig(siteConfig, directives);
}; };

View File

@@ -265,5 +265,5 @@ const keyify = (obj: any, prefix = ''): string[] =>
return [...res, prefix + el]; return [...res, prefix + el];
}, []); }, []);
export const configKeys: string[] = keyify(config, ''); export const configKeys: Set<string> = new Set(keyify(config, ''));
export default config; export default config;

View File

@@ -2,8 +2,13 @@ import { vi } from 'vitest';
import { extractFrontMatter } from './frontmatter.js'; import { extractFrontMatter } from './frontmatter.js';
const dbMock = () => ({ setDiagramTitle: vi.fn() }); const dbMock = () => ({ setDiagramTitle: vi.fn() });
const setConfigMock = vi.fn();
describe('extractFrontmatter', () => { describe('extractFrontmatter', () => {
beforeEach(() => {
setConfigMock.mockClear();
});
it('returns text unchanged if no frontmatter', () => { it('returns text unchanged if no frontmatter', () => {
expect(extractFrontMatter('diagram', dbMock())).toEqual('diagram'); expect(extractFrontMatter('diagram', dbMock())).toEqual('diagram');
}); });
@@ -75,4 +80,21 @@ describe('extractFrontmatter', () => {
'tag suffix cannot contain exclamation marks' 'tag suffix cannot contain exclamation marks'
); );
}); });
it('handles frontmatter with config', () => {
const text = `---
title: hello
config:
graph:
string: hello
number: 14
boolean: false
array: [1, 2, 3]
---
diagram`;
expect(extractFrontMatter(text, {}, setConfigMock)).toEqual('diagram');
expect(setConfigMock).toHaveBeenCalledWith({
graph: { string: 'hello', number: 14, boolean: false, array: [1, 2, 3] },
});
});
}); });

View File

@@ -1,40 +1,53 @@
import type { DiagramDB } from './types.js'; import type { MermaidConfig } from '../config.type.js';
import { frontMatterRegex } from './regexes.js'; import { frontMatterRegex } from './regexes.js';
import type { DiagramDB } from './types.js';
// The "* as yaml" part is necessary for tree-shaking // The "* as yaml" part is necessary for tree-shaking
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
type FrontMatterMetadata = { interface FrontMatterMetadata {
title?: string; title?: string;
// Allows custom display modes. Currently used for compact mode in gantt charts. // Allows custom display modes. Currently used for compact mode in gantt charts.
displayMode?: string; displayMode?: string;
}; config?: MermaidConfig;
}
/** /**
* Extract and parse frontmatter from text, if present, and sets appropriate * Extract and parse frontmatter from text, if present, and sets appropriate
* properties in the provided db. * properties in the provided db.
* @param text - The text that may have a YAML frontmatter. * @param text - The text that may have a YAML frontmatter.
* @param db - Diagram database, could be of any diagram. * @param db - Diagram database, could be of any diagram.
* @param setDiagramConfig - Optional function to set diagram config.
* @returns text with frontmatter stripped out * @returns text with frontmatter stripped out
*/ */
export function extractFrontMatter(text: string, db: DiagramDB): string { export function extractFrontMatter(
text: string,
db: DiagramDB,
setDiagramConfig?: (config: MermaidConfig) => void
): string {
const matches = text.match(frontMatterRegex); const matches = text.match(frontMatterRegex);
if (matches) { if (!matches) {
const parsed: FrontMatterMetadata = yaml.load(matches[1], {
// To keep things simple, only allow strings, arrays, and plain objects.
// https://www.yaml.org/spec/1.2/spec.html#id2802346
schema: yaml.FAILSAFE_SCHEMA,
}) as FrontMatterMetadata;
if (parsed?.title) {
db.setDiagramTitle?.(parsed.title);
}
if (parsed?.displayMode) {
db.setDisplayMode?.(parsed.displayMode);
}
return text.slice(matches[0].length);
} else {
return text; return text;
} }
const parsed: FrontMatterMetadata = yaml.load(matches[1], {
// To support config, we need JSON schema.
// https://www.yaml.org/spec/1.2/spec.html#id2803231
schema: yaml.JSON_SCHEMA,
}) as FrontMatterMetadata;
if (parsed?.title) {
// toString() is necessary because YAML could parse the title as a number/boolean
db.setDiagramTitle?.(parsed.title.toString());
}
if (parsed?.displayMode) {
// toString() is necessary because YAML could parse the title as a number/boolean
db.setDisplayMode?.(parsed.displayMode.toString());
}
if (parsed?.config) {
setDiagramConfig?.(parsed.config);
}
return text.slice(matches[0].length);
} }

View File

@@ -1,7 +1,5 @@
import * as configApi from './config.js'; import * as configApi from './config.js';
import { log } from './logger.js'; import { log } from './logger.js';
import { directiveSanitizer } from './utils.js';
let currentDirective: { type?: string; args?: any } | undefined = {}; let currentDirective: { type?: string; args?: any } | undefined = {};
@@ -60,9 +58,6 @@ const handleDirective = function (p: any, directive: any, type: string): void {
delete directive.args[prop]; delete directive.args[prop];
} }
}); });
log.info('sanitize in handleDirective', directive.args);
directiveSanitizer(directive.args);
log.info('sanitize in handleDirective (done)', directive.args);
configApi.addDirective(directive.args); configApi.addDirective(directive.args);
break; break;
} }

View File

@@ -4,10 +4,28 @@ When mermaid starts, configuration is extracted to determine a configuration to
- The default configuration - The default configuration
- Overrides at the site level are set by the initialize call, and will be applied to all diagrams in the site/app. The term for this is the **siteConfig**. - Overrides at the site level are set by the initialize call, and will be applied to all diagrams in the site/app. The term for this is the **siteConfig**.
- Directives - diagram authors can update select configuration parameters directly in the diagram code via directives. These are applied to the render config. - Frontmatter (v<MERMAID_RELEASE_VERSION>+) - diagram authors can update select configuration parameters in the frontmatter of the diagram. These are applied to the render config.
- Directives (Deprecated by Frontmatter) - diagram authors can update select configuration parameters directly in the diagram code via directives. These are applied to the render config.
**The render config** is configuration that is used when rendering by applying these configurations. **The render config** is configuration that is used when rendering by applying these configurations.
## Frontmatter config
The entire mermaid configuration (except the secure configs) can be overridden by the diagram author in the frontmatter of the diagram. The frontmatter is a YAML block at the top of the diagram.
```mermaid-example
---
title: Hello Title
config:
theme: base
themeVariables:
primaryColor: "#00ff00"
---
flowchart
Hello --> World
```
## Theme configuration ## Theme configuration
## Starting mermaid ## Starting mermaid

View File

@@ -1,5 +1,9 @@
# Directives # Directives
```warning
Directives are deprecated from v<MERMAID_RELEASE_VERSION>. Please use the `config` key in frontmatter to pass configuration. See [Configuration](./configuration.md) for more details.
```
## Directives ## Directives
Directives give a diagram author the capability to alter the appearance of a diagram before rendering by changing the applied configuration. Directives give a diagram author the capability to alter the appearance of a diagram before rendering by changing the applied configuration.

View File

@@ -23,13 +23,14 @@ import { attachFunctions } from './interactionDb.js';
import { log, setLogLevel } from './logger.js'; import { log, setLogLevel } from './logger.js';
import getStyles from './styles.js'; import getStyles from './styles.js';
import theme from './themes/index.js'; import theme from './themes/index.js';
import utils, { directiveSanitizer } from './utils.js'; import utils from './utils.js';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { MermaidConfig } from './config.type.js'; import { MermaidConfig } from './config.type.js';
import { evaluate } from './diagrams/common/common.js'; import { evaluate } from './diagrams/common/common.js';
import isEmpty from 'lodash-es/isEmpty.js'; import isEmpty from 'lodash-es/isEmpty.js';
import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js'; import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js';
import { parseDirective } from './directiveUtils.js'; import { parseDirective } from './directiveUtils.js';
import { extractFrontMatter } from './diagram-api/frontmatter.js';
// diagram names that support classDef statements // diagram names that support classDef statements
const CLASSDEF_DIAGRAMS = [ const CLASSDEF_DIAGRAMS = [
@@ -385,10 +386,14 @@ const render = async function (
configApi.reset(); configApi.reset();
// Add Directives. Must do this before getting the config and before creating the diagram. // We need to add the directives before creating the diagram.
// So extractFrontMatter is called twice. Once here and once in the diagram parser.
// This can be fixed in a future refactor.
extractFrontMatter(text, {}, configApi.addDirective);
// Add Directives.
const graphInit = utils.detectInit(text); const graphInit = utils.detectInit(text);
if (graphInit) { if (graphInit) {
directiveSanitizer(graphInit);
configApi.addDirective(graphInit); configApi.addDirective(graphInit);
} }

View File

@@ -13,10 +13,10 @@
# Non-JSON values, like functions or `undefined`, still need to be manually # Non-JSON values, like functions or `undefined`, still need to be manually
# set in `src/defaultConfig.ts`) # set in `src/defaultConfig.ts`)
# - `src/docs.mts` # - `src/docs.mts`
# Used to genereate Markdown documentation for this JSON Schema by using # Used to generate Markdown documentation for this JSON Schema by using
# the `@adobe/jsonschema2md` NPM package. # the `@adobe/jsonschema2md` NPM package.
# Useful things to know when editting this file # Useful things to know when editing this file
# - Use the `|` character for multi-line strings # - Use the `|` character for multi-line strings
# - Use `meta:enum` to document enum values (from jsonschema2md) # - Use `meta:enum` to document enum values (from jsonschema2md)
# - Use `tsType` to override the TypeScript type (from json-schema-to-typescript) # - Use `tsType` to override the TypeScript type (from json-schema-to-typescript)
@@ -1851,7 +1851,7 @@ $defs: # JSON Schema definition (maybe we should move these to a seperate file)
The color of the links in the sankey diagram. The color of the links in the sankey diagram.
anyOf: anyOf:
- $ref: '#/$defs/SankeyLinkColor' - $ref: '#/$defs/SankeyLinkColor'
- description: An arbtirary [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) - description: An arbitrary [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value)
type: string type: string
default: gradient default: gradient
nodeAlignment: nodeAlignment:

View File

@@ -97,32 +97,36 @@ const directiveWithoutOpen =
* @param config - Optional mermaid configuration object. * @param config - Optional mermaid configuration object.
* @returns The json object representing the init passed to mermaid.initialize() * @returns The json object representing the init passed to mermaid.initialize()
*/ */
export const detectInit = function (text: string, config?: MermaidConfig): MermaidConfig { export const detectInit = function (
text: string,
config?: MermaidConfig
): MermaidConfig | undefined {
const inits = detectDirective(text, /(?:init\b)|(?:initialize\b)/); const inits = detectDirective(text, /(?:init\b)|(?:initialize\b)/);
let results = {}; let results = {};
if (Array.isArray(inits)) { if (Array.isArray(inits)) {
const args = inits.map((init) => init.args); const args = inits.map((init) => init.args);
directiveSanitizer(args); sanitizeDirective(args);
results = assignWithDepth(results, [...args]); results = assignWithDepth(results, [...args]);
} else { } else {
results = inits.args; results = inits.args;
} }
if (results) {
let type = detectType(text, config); if (!results) {
['config'].forEach((prop) => { return;
if (results[prop] !== undefined) {
if (type === 'flowchart-v2') {
type = 'flowchart';
}
results[type] = results[prop];
delete results[prop];
}
});
} }
// Todo: refactor this, these results are never used let type = detectType(text, config);
['config'].forEach((prop) => {
if (results[prop] !== undefined) {
if (type === 'flowchart-v2') {
type = 'flowchart';
}
results[type] = results[prop];
delete results[prop];
}
});
return results; return results;
}; };
@@ -843,67 +847,63 @@ export const entityDecode = function (html: string): string {
* *
* @param args - Directive's JSON * @param args - Directive's JSON
*/ */
export const directiveSanitizer = (args: any) => { export const sanitizeDirective = (args: unknown): void => {
log.debug('directiveSanitizer called with', args); log.debug('sanitizeDirective called with', args);
if (typeof args === 'object') {
// check for array
if (args.length) {
args.forEach((arg) => directiveSanitizer(arg));
} else {
// This is an object
Object.keys(args).forEach((key) => {
log.debug('Checking key', key);
if (key.startsWith('__')) {
log.debug('sanitize deleting __ option', key);
delete args[key];
}
if (key.includes('proto')) { // Return if not an object
log.debug('sanitize deleting proto option', key); if (typeof args !== 'object' || args == null) {
delete args[key]; return;
} }
if (key.includes('constr')) { // Sanitize each element if an array
log.debug('sanitize deleting constr option', key); if (Array.isArray(args)) {
delete args[key]; args.forEach((arg) => sanitizeDirective(arg));
} return;
}
if (key.includes('themeCSS')) { // Sanitize each key if an object
log.debug('sanitizing themeCss option'); for (const key of Object.keys(args)) {
args[key] = sanitizeCss(args[key]); log.debug('Checking key', key);
} if (
if (key.includes('fontFamily')) { key.startsWith('__') ||
log.debug('sanitizing fontFamily option'); key.includes('proto') ||
args[key] = sanitizeCss(args[key]); key.includes('constr') ||
} !configKeys.has(key) ||
if (key.includes('altFontFamily')) { args[key] == null
log.debug('sanitizing altFontFamily option'); ) {
args[key] = sanitizeCss(args[key]); log.debug('sanitize deleting key: ', key);
} delete args[key];
if (!configKeys.includes(key)) { continue;
log.debug('sanitize deleting option', key); }
delete args[key];
} else { // Recurse if an object
if (typeof args[key] === 'object') { if (typeof args[key] === 'object') {
log.debug('sanitize deleting object', key); log.debug('sanitizing object', key);
directiveSanitizer(args[key]); sanitizeDirective(args[key]);
} continue;
} }
});
const cssMatchers = ['themeCSS', 'fontFamily', 'altFontFamily'];
for (const cssKey of cssMatchers) {
if (key.includes(cssKey)) {
log.debug('sanitizing css option', key);
args[key] = sanitizeCss(args[key]);
}
} }
} }
if (args.themeVariables) { if (args.themeVariables) {
const kArr = Object.keys(args.themeVariables); for (const k of Object.keys(args.themeVariables)) {
for (const k of kArr) {
const val = args.themeVariables[k]; const val = args.themeVariables[k];
if (val && val.match && !val.match(/^[\d "#%(),.;A-Za-z]+$/)) { if (val?.match && !val.match(/^[\d "#%(),.;A-Za-z]+$/)) {
args.themeVariables[k] = ''; args.themeVariables[k] = '';
} }
} }
} }
log.debug('After sanitization', args); log.debug('After sanitization', args);
}; };
export const sanitizeCss = (str) => {
export const sanitizeCss = (str: string): string => {
let startCnt = 0; let startCnt = 0;
let endCnt = 0; let endCnt = 0;
@@ -1020,8 +1020,8 @@ export default {
random, random,
runFunc, runFunc,
entityDecode, entityDecode,
initIdGenerator: initIdGenerator, initIdGenerator,
directiveSanitizer, sanitizeDirective,
sanitizeCss, sanitizeCss,
insertTitle, insertTitle,
parseFontSize, parseFontSize,