Merge branch 'develop' of github.com:mermaid-js/mermaid into develop

This commit is contained in:
Per Brolin 2022-11-14 16:34:57 +01:00
commit 51a84eeb90
20 changed files with 2360 additions and 1295 deletions

View File

@ -1,3 +1,4 @@
{
"!(docs/**/*)*.{ts,js,json,html,md,mts}": ["eslint --fix", "prettier --write"]
"!(docs/**/*)*.{ts,js,json,html,md,mts}": ["eslint --fix", "prettier --write"],
"cSpell.json": ["ts-node-esm scripts/fixCSpell.ts"]
}

View File

@ -43,12 +43,12 @@ Mermaid addresses this problem by enabling users to create easily modifiable dia
<br/>
Mermaid allows even non-programmers to easily create detailed diagrams through the [Mermaid Live Editor](https://mermaid.live/).<br/>
[Tutorials](./docs/Tutorials.md) has video tutorials.
Use Mermaid with your favorite applications, check out the list of [Integrations and Usages of Mermaid](./docs/integrations.md).
[Tutorials](./docs/config/Tutorials.md) has video tutorials.
Use Mermaid with your favorite applications, check out the list of [Integrations and Usages of Mermaid](./docs/misc/integrations.md).
You can also use Mermaid within [GitHub](https://github.blog/2022-02-14-include-diagrams-markdown-files-mermaid/) as well many of your other favorite applications—check out the list of [Integrations and Usages of Mermaid](./docs/integrations.md).
You can also use Mermaid within [GitHub](https://github.blog/2022-02-14-include-diagrams-markdown-files-mermaid/) as well many of your other favorite applications—check out the list of [Integrations and Usages of Mermaid](./docs/misc/integrations.md).
For a more detailed introduction to Mermaid and some of its more basic uses, look to the [Beginner's Guide](./docs/n00b-overview.md), [Usage](./docs/usage.md) and [Tutorials](./docs/Tutorials.md).
For a more detailed introduction to Mermaid and some of its more basic uses, look to the [Beginner's Guide](./docs/community/n00b-overview.md), [Usage](./docs/config/usage.md) and [Tutorials](./docs/config/Tutorials.md).
🌐 [CDN](https://unpkg.com/mermaid/) | 📖 [Documentation](https://mermaidjs.github.io) | 🙌 [Contribution](https://github.com/mermaid-js/mermaid/blob/develop/CONTRIBUTING.md) | 📜 [Changelog](./docs/CHANGELOG.md)

View File

@ -15,5 +15,5 @@ module.exports = defineConfig({
// { deviceName: 'Pixel 2', screenOrientation: 'portrait' },
],
// set batch name to the configuration
batchName: `Mermaid ${process.env.APPLI_BRANCH ?? "'no APPLI_BRANCH set'"}`,
// batchName: `Mermaid ${process.env.APPLI_BRANCH ?? "'no APPLI_BRANCH set'"}`,
});

View File

@ -2,91 +2,86 @@
"version": "0.2",
"language": "en",
"words": [
"blockquotes",
"customizability",
"Gantt",
"jison",
"mermaid",
"mindmap",
"Mindmaps",
"mitigations",
"sandboxed",
"shiki",
"verdana",
"Visio"
],
"ignoreWords": [
"Alois",
"Klink",
"knsv",
"Knut",
"Matthieu",
"Sidharth",
"Sveidqvist",
"Vinod",
"Faber",
"Orlandoni",
"Klemm",
"Mindaugas",
"Laganeckas",
"Cuzon",
"Yash",
"Adamiecki",
"acyclicer",
"adamiecki",
"alois",
"antiscript",
"applitools",
"Asciidoctor",
"Astah",
"Bisheng",
"asciidoctor",
"ashish",
"astah",
"bbox",
"bilkent",
"bisheng",
"brolin",
"codedoc",
"Docsy",
"Doku",
"Gitea",
"Gitgraph",
"Grav",
"Inkdrop",
"Jaoude",
"colour",
"cpettitt",
"customizability",
"cuzon",
"cytoscape",
"dagre",
"descr",
"docsify",
"docsy",
"doku",
"dompurify",
"edgechromium",
"faber",
"flatmap",
"gantt",
"gitea",
"gitgraph",
"graphlib",
"grav",
"greywolf",
"inkdrop",
"jaoude",
"jison",
"kaufmann",
"klemm",
"klink",
"knsv",
"knut",
"laganeckas",
"lucida",
"matthieu",
"mdbook",
"mermerd",
"mindaugas",
"mindmap",
"mindmaps",
"mitigations",
"mkdocs",
"orlandoni",
"phpbb",
"Plantuml",
"Playfair's",
"Podlite",
"redmine",
"sphinxcontrib",
"Tuleap",
"dagre",
"vitepress",
"docsify",
"colour",
"graphlib",
"acyclicer",
"plantuml",
"playfair",
"podlite",
"ranksep",
"descr",
"substate",
"Ashish",
"bbox",
"techn",
"cytoscape",
"Lucida",
"Bilkent",
"cpettitt",
"antiscript",
"ts-nocheck",
"setupGraphViewbox",
"flatmap",
"Kaufmann",
"viewports",
"edgechromium",
"redmine",
"sandboxed",
"setupgraphviewbox",
"shiki",
"sidharth",
"sphinxcontrib",
"statediagram",
"Brolin",
"Greywolf"
"stylis",
"substate",
"sveidqvist",
"techn",
"ts-nocheck",
"tuleap",
"verdana",
"viewports",
"vinod",
"visio",
"vitepress",
"xlink",
"yash"
],
"patterns": [
{
"name": "Markdown links",
"pattern": "\\((.*)\\)",
"description": ""
},
{ "name": "Markdown links", "pattern": "\\((.*)\\)", "description": "" },
{
"name": "Markdown code blocks",
"pattern": "/^(\\s*`{3,}).*[\\s\\S]*?^\\1/gmx",
@ -97,25 +92,14 @@
"pattern": "\\`([^\\`\\r\\n]+?)\\`",
"description": "https://stackoverflow.com/questions/41274241/how-to-capture-inline-markdown-code-but-not-a-markdown-code-fence-with-regex"
},
{
"name": "Link contents",
"pattern": "\\<a(.*)\\>",
"description": ""
},
{
"name": "Snippet references",
"pattern": "-- snippet:(.*)",
"description": ""
},
{ "name": "Link contents", "pattern": "\\<a(.*)\\>", "description": "" },
{ "name": "Snippet references", "pattern": "-- snippet:(.*)", "description": "" },
{
"name": "Snippet references 2",
"pattern": "\\<\\[sample:(.*)",
"description": "another kind of snippet reference"
},
{
"name": "Multi-line code blocks",
"pattern": "/^\\s*```[\\s\\S]*?^\\s*```/gm"
},
{ "name": "Multi-line code blocks", "pattern": "/^\\s*```[\\s\\S]*?^\\s*```/gm" },
{
"name": "HTML Tags",
"pattern": "<[^>]*>",

View File

@ -2,6 +2,8 @@ const utf8ToB64 = (str) => {
return window.btoa(unescape(encodeURIComponent(str)));
};
const batchId = 'mermid-batch' + new Date().getTime();
export const mermaidUrl = (graphStr, options, api) => {
const obj = {
code: graphStr,
@ -49,9 +51,12 @@ export const imgSnapshotTest = (graphStr, _options, api = false, validation) =>
const name = (options.name || cy.state('runnable').fullTitle()).replace(/\s+/g, '-');
if (useAppli) {
cy.log('Opening eyes ' + Cypress.spec.name + ' --- ' + name);
cy.eyesOpen({
appName: 'Mermaid',
testName: name,
batchName: Cypress.spec.name,
batchId: batchId + Cypress.spec.name,
});
}
@ -65,7 +70,9 @@ export const imgSnapshotTest = (graphStr, _options, api = false, validation) =>
// Default name to test title
if (useAppli) {
cy.log('Check eyes' + Cypress.spec.name);
cy.eyesCheckWindow('Click!');
cy.log('Closing eyes: ' + Cypress.spec.name);
cy.eyesClose();
} else {
cy.matchImageSnapshot(name);
@ -101,9 +108,12 @@ export const urlSnapshotTest = (url, _options, api = false, validation) => {
const name = (options.name || cy.state('runnable').fullTitle()).replace(/\s+/g, '-');
if (useAppli) {
cy.log('Opening eyes 2' + Cypress.spec.name);
cy.eyesOpen({
appName: 'Mermaid',
testName: name,
batchName: Cypress.spec.name,
batchId: batchId + Cypress.spec.name,
});
}
@ -115,7 +125,9 @@ export const urlSnapshotTest = (url, _options, api = false, validation) => {
// Default name to test title
if (useAppli) {
cy.log('Check eyes 2' + Cypress.spec.name);
cy.eyesCheckWindow('Click!');
cy.log('Closing eyes 2' + Cypress.spec.name);
cy.eyesClose();
} else {
cy.matchImageSnapshot(name);

View File

@ -30,7 +30,7 @@ Mermaid basically supports two types of configuration options to be overridden b
2. _Diagram specific configurations_ : These are the configurations that are available and applied to a specific diagram. For each diagram there are specific configuration that will alter how that particular diagram looks and behaves.
For example, `mirrorActors` is a configuration that is specific to the `SequenceDiagram` and alter whether the actors are mirrored or not. So this config is available only for the `SequenceDiagram` type.
**NOTE:** These options listed here are not all the configuration options. To get hold of all the configuration options, please refer to the [defaultConfig.js](https://github.com/mermaid-js/mermaid/blob/develop/src/defaultConfig.js) in the source code.
**NOTE:** These options listed here are not all the configuration options. To get hold of all the configuration options, please refer to the [defaultConfig.ts](https://github.com/mermaid-js/mermaid/blob/develop/packages/mermaid/src/defaultConfig.ts) in the source code.
Soon we plan to publish a complete list of top-level configurations & all the diagram specific configurations, with their possible values in the docs
@ -233,7 +233,7 @@ Some common flowchart configurations are:
- _diagramPadding_: number
- _useMaxWidth_: number
For complete list of flowchart configurations, see [defaultConfig.js](https://github.com/mermaid-js/mermaid/blob/develop/src/defaultConfig.js) in the source code.
For complete list of flowchart configurations, see [defaultConfig.ts](https://github.com/mermaid-js/mermaid/blob/develop/packages/mermaid/src/defaultConfig.ts) in the source code.
_Soon we plan to publish a complete list all diagram specific configurations updated in the docs_
The following code snippet changes flowchart config:
@ -277,7 +277,7 @@ Some common sequence configurations are:
- _showSequenceNumbers_: boolean
- _wrap_: boolean
For complete list of sequence diagram configurations, see _defaultConfig.js_ in the source code.
For complete list of sequence diagram configurations, see _defaultConfig.ts_ in the source code.
_Soon we plan to publish a complete list all diagram specific configurations updated in the docs_
So, `wrap` by default has a value of `false` for sequence diagrams.

View File

@ -16,7 +16,7 @@ Renames and re-exports [mermaidAPI](mermaidAPI.md#mermaidapi)
### mermaidAPI
`Const` **mermaidAPI**: `Readonly`<{ `defaultConfig`: `MermaidConfig` = configApi.defaultConfig; `getConfig`: () => `MermaidConfig` = configApi.getConfig; `getSiteConfig`: () => `MermaidConfig` = configApi.getSiteConfig; `globalReset`: () => `void` ; `initialize`: (`options`: `MermaidConfig`) => `Promise`<`void`> ; `parse`: (`text`: `string`, `parseError?`: `ParseErrorFunction`) => `boolean` ; `parseDirective`: (`p`: `any`, `statement`: `string`, `context`: `string`, `type`: `string`) => `void` ; `render`: (`id`: `string`, `text`: `string`, `cb`: (`svgCode`: `string`, `bindFunctions?`: (`element`: `Element`) => `void`) => `void`, `container?`: `Element`) => `Promise`<`void`> ; `reset`: () => `void` ; `setConfig`: (`conf`: `MermaidConfig`) => `MermaidConfig` = configApi.setConfig; `updateSiteConfig`: (`conf`: `MermaidConfig`) => `MermaidConfig` = configApi.updateSiteConfig }>
`Const` **mermaidAPI**: `Readonly`<{ `defaultConfig`: `MermaidConfig` = configApi.defaultConfig; `getConfig`: () => `MermaidConfig` = configApi.getConfig; `getSiteConfig`: () => `MermaidConfig` = configApi.getSiteConfig; `globalReset`: () => `void` ; `initialize`: (`options`: `MermaidConfig`) => `Promise`<`void`> ; `parse`: (`text`: `string`, `parseError?`: `ParseErrorFunction`) => `boolean` ; `parseDirective`: (`p`: `any`, `statement`: `string`, `context`: `string`, `type`: `string`) => `void` ; `render`: (`id`: `string`, `text`: `string`, `cb`: (`svgCode`: `string`, `bindFunctions?`: (`element`: `Element`) => `void`) => `void`, `svgContainingElement?`: `Element`) => `Promise`<`void`> ; `reset`: () => `void` ; `setConfig`: (`conf`: `MermaidConfig`) => `MermaidConfig` = configApi.setConfig; `updateSiteConfig`: (`conf`: `MermaidConfig`) => `MermaidConfig` = configApi.updateSiteConfig }>
## mermaidAPI configuration defaults
@ -80,19 +80,105 @@ mermaid.initialize(config);
#### Defined in
[mermaidAPI.ts:546](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L546)
[mermaidAPI.ts:740](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L740)
## Functions
### decodeEntities
### appendDivSvgG
**decodeEntities**(`text`): `string`
**appendDivSvgG**(`parentRoot`, `id`, `enclosingDivId`, `divStyle?`, `svgXlink?`): `any`
Append an enclosing div, then svg, then g (group) to the d3 parentRoot. Set attributes.
Only set the style attribute on the enclosing div if divStyle is given.
Only set the xmlns:xlink attribute on svg if svgXlink is given.
Return the last node appended
#### Parameters
| Name | Type |
| :----- | :------- |
| `text` | `string` |
| Name | Type | Description |
| :--------------- | :------- | :----------------------------------------------- |
| `parentRoot` | `any` | the d3 node to append things to |
| `id` | `string` | the value to set the id attr to |
| `enclosingDivId` | `string` | the id to set the enclosing div to |
| `divStyle?` | `string` | if given, the style to set the enclosing div to |
| `svgXlink?` | `string` | if given, the link to set the new svg element to |
#### Returns
`any`
- returns the parentRoot that had nodes appended
#### Defined in
[mermaidAPI.ts:283](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L283)
---
### cleanUpSvgCode
**cleanUpSvgCode**(`svgCode?`, `inSandboxMode`, `useArrowMarkerUrls`): `string`
Clean up svgCode. Do replacements needed
#### Parameters
| Name | Type | Default value | Description |
| :------------------- | :-------- | :------------ | :---------------------------------------------------------- |
| `svgCode` | `string` | `''` | the code to clean up |
| `inSandboxMode` | `boolean` | `undefined` | security level |
| `useArrowMarkerUrls` | `boolean` | `undefined` | should arrow marker's use full urls? (vs. just the anchors) |
#### Returns
`string`
the cleaned up svgCode
#### Defined in
[mermaidAPI.ts:234](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L234)
---
### createCssStyles
**createCssStyles**(`config`, `graphType`, `classDefs?`): `string`
Create the user styles
#### Parameters
| Name | Type | Description |
| :---------- | :-------------- | :----------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `config` | `MermaidConfig` | configuration that has style and theme settings to use |
| `graphType` | `string` | used for checking if classDefs should be applied |
| `classDefs` | `undefined` | `null` | `Record`<`string`, `DiagramStyleClassDef`> | the classDefs in the diagram text. Might be null if none were defined. Usually is the result of a call to getClasses(...) |
#### Returns
`string`
the string with all the user styles
#### Defined in
[mermaidAPI.ts:161](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L161)
---
### createUserStyles
**createUserStyles**(`config`, `graphType`, `classDefs`, `svgId`): `string`
#### Parameters
| Name | Type |
| :---------- | :----------------------------------------- |
| `config` | `MermaidConfig` |
| `graphType` | `string` |
| `classDefs` | `Record`<`string`, `DiagramStyleClassDef`> |
| `svgId` | `string` |
#### Returns
@ -100,7 +186,54 @@ mermaid.initialize(config);
#### Defined in
[mermaidAPI.ts:72](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L72)
[mermaidAPI.ts:211](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L211)
---
### cssImportantStyles
**cssImportantStyles**(`cssClass`, `element`, `cssClasses?`): `string`
Create a CSS style that starts with the given class name, then the element,
with an enclosing block that has each of the cssClasses followed by !important;
#### Parameters
| Name | Type | Default value | Description |
| :----------- | :---------- | :------------ | :--------------------------------------------- |
| `cssClass` | `string` | `undefined` | CSS class name |
| `element` | `string` | `undefined` | CSS element |
| `cssClasses` | `string`\[] | `[]` | list of CSS styles to append after the element |
#### Returns
`string`
- the constructed string
#### Defined in
[mermaidAPI.ts:145](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L145)
---
### decodeEntities
**decodeEntities**(`text`): `string`
#### Parameters
| Name | Type | Description |
| :----- | :------- | :----------------- |
| `text` | `string` | text to be decoded |
#### Returns
`string`
#### Defined in
[mermaidAPI.ts:119](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L119)
---
@ -110,9 +243,9 @@ mermaid.initialize(config);
#### Parameters
| Name | Type |
| :----- | :------- |
| `text` | `string` |
| Name | Type | Description |
| :----- | :------- | :----------------- |
| `text` | `string` | text to be encoded |
#### Returns
@ -120,4 +253,56 @@ mermaid.initialize(config);
#### Defined in
[mermaidAPI.ts:46](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L46)
[mermaidAPI.ts:90](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L90)
---
### putIntoIFrame
**putIntoIFrame**(`svgCode?`, `svgElement?`): `string`
Put the svgCode into an iFrame. Return the iFrame code
#### Parameters
| Name | Type | Default value | Description |
| :------------ | :------- | :------------ | :--------------------------------------------------------------------------- |
| `svgCode` | `string` | `''` | the svg code to put inside the iFrame |
| `svgElement?` | `any` | `undefined` | the d3 node that has the current svgElement so we can get the height from it |
#### Returns
`string`
- the code with the iFrame that now contains the svgCode
TODO replace btoa(). Replace with buf.toString('base64')?
#### Defined in
[mermaidAPI.ts:262](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L262)
---
### removeExistingElements
**removeExistingElements**(`doc`, `isSandboxed`, `id`, `divSelector`, `iFrameSelector`): `void`
Remove any existing elements from the given document
#### Parameters
| Name | Type | Description |
| :--------------- | :--------- | :---------------------------------------------- |
| `doc` | `Document` | the document to removed elements from |
| `isSandboxed` | `boolean` | whether or not we are in sandboxed mode |
| `id` | `string` | id for any existing SVG element |
| `divSelector` | `string` | selector for any existing enclosing div element |
| `iFrameSelector` | `string` | selector for any existing iFrame element |
#### Returns
`void`
#### Defined in
[mermaidAPI.ts:334](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L334)

View File

@ -7,7 +7,7 @@
"module": "dist/mermaid.core.mjs",
"types": "dist/mermaid.d.ts",
"type": "module",
"packageManager": "pnpm@7.14.1",
"packageManager": "pnpm@7.15.0",
"exports": {
".": {
"require": "./dist/mermaid.min.js",
@ -33,7 +33,7 @@
"dev": "concurrently \"pnpm build:vite --watch\" \"ts-node-esm .vite/server.ts\"",
"release": "pnpm build",
"lint": "eslint --cache --ignore-path .gitignore . && pnpm --filter mermaid run lint:jison && prettier --check .",
"lint:fix": "eslint --fix --ignore-path .gitignore . && prettier --write .",
"lint:fix": "eslint --fix --ignore-path .gitignore . && prettier --write . && ts-node-esm scripts/fixCSpell.ts",
"cypress": "cypress run",
"cypress:open": "cypress open",
"e2e": "start-server-and-test dev http://localhost:9000/ cypress",
@ -62,67 +62,67 @@
]
},
"dependencies": {
"@braintree/sanitize-url": "6.0.1",
"@types/node": "18.11.8",
"@braintree/sanitize-url": "6.0.2",
"@types/node": "18.11.9",
"@types/uuid": "8.3.4",
"d3": "7.6.1",
"dagre": "0.8.5",
"dagre-d3": "0.6.4",
"dompurify": "2.4.0",
"dompurify": "2.4.1",
"fast-clone": "1.5.13",
"graphlib": "2.1.8",
"khroma": "2.0.0",
"lodash": "4.17.21",
"moment-mini": "2.29.4",
"non-layered-tidy-tree-layout": "2.0.2",
"rollup": "2.79.1",
"rollup": "3.3.0",
"stylis": "4.1.3",
"uuid": "9.0.0"
},
"devDependencies": {
"@applitools/eyes-cypress": "3.27.6",
"@commitlint/cli": "17.1.2",
"@commitlint/config-conventional": "17.1.0",
"@cspell/eslint-plugin": "6.13.2",
"@commitlint/cli": "17.2.0",
"@commitlint/config-conventional": "17.2.0",
"@cspell/eslint-plugin": "6.14.2",
"@types/d3": "7.4.0",
"@types/dompurify": "2.3.4",
"@types/eslint": "8.4.9",
"@types/dompurify": "2.4.0",
"@types/eslint": "8.4.10",
"@types/express": "4.17.14",
"@types/jsdom": "20.0.0",
"@types/lodash": "4.14.186",
"@types/jsdom": "20.0.1",
"@types/lodash": "4.14.188",
"@types/mdast": "3.0.10",
"@types/prettier": "2.7.1",
"@types/stylis": "4.0.2",
"@typescript-eslint/eslint-plugin": "5.41.0",
"@typescript-eslint/parser": "5.41.0",
"@vitest/coverage-c8": "0.24.3",
"@vitest/ui": "0.24.3",
"@typescript-eslint/eslint-plugin": "5.42.1",
"@typescript-eslint/parser": "5.42.1",
"@vitest/coverage-c8": "0.25.1",
"@vitest/ui": "0.25.1",
"concurrently": "7.5.0",
"coveralls": "3.1.1",
"cypress": "10.11.0",
"cypress-image-snapshot": "4.0.1",
"esbuild": "0.15.12",
"eslint": "8.26.0",
"esbuild": "0.15.13",
"eslint": "8.27.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-cypress": "2.12.1",
"eslint-plugin-html": "7.1.0",
"eslint-plugin-jest": "27.1.3",
"eslint-plugin-jsdoc": "39.4.0",
"eslint-plugin-jest": "27.1.5",
"eslint-plugin-jsdoc": "39.6.2",
"eslint-plugin-json": "3.1.0",
"eslint-plugin-markdown": "3.0.0",
"eslint-plugin-no-only-tests": "3.1.0",
"eslint-plugin-tsdoc": "0.2.17",
"express": "4.18.2",
"globby": "13.1.2",
"husky": "8.0.1",
"husky": "8.0.2",
"identity-obj-proxy": "3.0.0",
"jest": "29.2.2",
"jest": "29.3.1",
"jison": "0.4.18",
"jsdom": "20.0.2",
"lint-staged": "13.0.3",
"markdown-it": "13.0.1",
"path-browserify": "1.0.1",
"pnpm": "7.14.1",
"pnpm": "7.15.0",
"prettier": "2.7.1",
"prettier-plugin-jsdoc": "0.4.2",
"remark": "14.0.2",
@ -131,11 +131,11 @@
"ts-node": "10.9.1",
"typescript": "4.8.4",
"unist-util-flatmap": "1.0.0",
"vite": "3.2.2",
"vitepress": "1.0.0-alpha.26",
"vite": "3.2.3",
"vitepress": "1.0.0-alpha.28",
"vitepress-plugin-mermaid": "2.0.8",
"vitepress-plugin-search": "1.0.4-alpha.14",
"vitest": "0.24.3"
"vitepress-plugin-search": "1.0.4-alpha.15",
"vitest": "0.25.1"
},
"resolutions": {
"d3": "7.6.1"
@ -148,6 +148,6 @@
"**/*.scss"
],
"volta": {
"node": "18.12.0"
"node": "18.12.1"
}
}

View File

@ -36,7 +36,7 @@
"docs:verify": "pnpm docs:code && ts-node-esm src/docs.mts --verify",
"docs:pre:vitepress": "rimraf src/vitepress && pnpm docs:code && ts-node-esm src/docs.mts --vitepress",
"docs:build:vitepress": "pnpm docs:pre:vitepress && vitepress build src/vitepress",
"docs:dev": "pnpm docs:pre:vitepress && vitepress dev src/vitepress",
"docs:dev": "pnpm docs:pre:vitepress && concurrently \"vitepress dev src/vitepress\" \"ts-node-esm src/docs.mts --watch --vitepress\"",
"docs:serve": "pnpm docs:build:vitepress && vitepress serve src/vitepress",
"release": "pnpm build",
"lint": "eslint --cache --ignore-path .gitignore . && pnpm lint:jison && prettier --check .",
@ -69,7 +69,7 @@
"d3": "^7.0.0",
"dagre": "^0.8.5",
"dagre-d3": "^0.6.4",
"dompurify": "2.4.0",
"dompurify": "2.4.1",
"fast-clone": "^1.5.13",
"graphlib": "^2.1.8",
"khroma": "^2.0.0",
@ -80,40 +80,43 @@
},
"devDependencies": {
"@applitools/eyes-cypress": "3.27.6",
"@commitlint/cli": "17.1.2",
"@commitlint/config-conventional": "17.1.0",
"@commitlint/cli": "17.2.0",
"@commitlint/config-conventional": "17.2.0",
"@types/d3": "7.4.0",
"@types/dompurify": "2.3.4",
"@types/eslint": "8.4.9",
"@types/dompurify": "2.4.0",
"@types/eslint": "8.4.10",
"@types/express": "4.17.14",
"@types/jsdom": "20.0.0",
"@types/lodash": "4.14.186",
"@types/jsdom": "20.0.1",
"@types/lodash": "4.14.188",
"@types/micromatch": "4.0.2",
"@types/prettier": "2.7.1",
"@types/stylis": "4.0.2",
"@typescript-eslint/eslint-plugin": "5.41.0",
"@typescript-eslint/parser": "5.41.0",
"@typescript-eslint/eslint-plugin": "5.42.1",
"@typescript-eslint/parser": "5.42.1",
"chokidar": "3.5.3",
"concurrently": "7.5.0",
"coveralls": "3.1.1",
"cypress": "10.11.0",
"cypress-image-snapshot": "4.0.1",
"documentation": "13.2.5",
"esbuild": "0.15.12",
"eslint": "8.26.0",
"esbuild": "0.15.13",
"eslint": "8.27.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-cypress": "2.12.1",
"eslint-plugin-html": "7.1.0",
"eslint-plugin-jest": "27.1.3",
"eslint-plugin-jsdoc": "39.4.0",
"eslint-plugin-jest": "27.1.5",
"eslint-plugin-jsdoc": "39.6.2",
"eslint-plugin-json": "3.1.0",
"eslint-plugin-markdown": "3.0.0",
"express": "4.18.2",
"globby": "13.1.2",
"husky": "8.0.1",
"husky": "8.0.2",
"identity-obj-proxy": "3.0.0",
"jison": "0.4.18",
"js-base64": "3.7.2",
"jsdom": "20.0.2",
"lint-staged": "13.0.3",
"micromatch": "^4.0.5",
"moment": "2.29.4",
"path-browserify": "1.0.1",
"prettier": "2.7.1",

View File

@ -17,7 +17,7 @@ let vertexCounter = 0;
let config = configApi.getConfig();
let vertices = {};
let edges = [];
let classes = [];
let classes = {};
let subGraphs = [];
let subGraphLookup = {};
let tooltips = {};

View File

@ -279,7 +279,8 @@ export const getClasses = function (text, diagObj) {
diagObj.parse(text);
return diagObj.db.getClasses();
} catch (e) {
return;
log.error(e);
return {};
}
};

View File

@ -30,7 +30,7 @@
* @todo Write a test file for this. (Will need to be able to deal .mts file. Jest has trouble with
* it.)
*/
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync, rmdirSync } from 'fs';
import { exec } from 'child_process';
import { globby } from 'globby';
import { JSDOM } from 'jsdom';
@ -38,6 +38,8 @@ import type { Code, Root } from 'mdast';
import { posix, dirname, relative } from 'path';
import prettier from 'prettier';
import { remark } from 'remark';
import chokidar from 'chokidar';
import mm from 'micromatch';
// @ts-ignore No typescript declaration file
import flatmap from 'unist-util-flatmap';
@ -47,6 +49,7 @@ const MERMAID_MAJOR_VERSION = (
const verifyOnly: boolean = process.argv.includes('--verify');
const git: boolean = process.argv.includes('--git');
const watch: boolean = process.argv.includes('--watch');
const vitepress: boolean = process.argv.includes('--vitepress');
const noHeader: boolean = process.argv.includes('--noHeader') || vitepress;
@ -61,14 +64,7 @@ const LOGMSG_COPIED = `, and copied to ${FINAL_DOCS_DIR}`;
const WARN_DOCSDIR_DOESNT_MATCH = `Changed files were transformed in ${SOURCE_DOCS_DIR} but do not match the files in ${FINAL_DOCS_DIR}. Please run 'pnpm --filter mermaid run docs:build' after making changes to ${SOURCE_DOCS_DIR} to update the ${FINAL_DOCS_DIR} directory with the transformed files.`;
// TODO: Read from .prettierrc?
const prettierConfig: prettier.Config = {
useTabs: false,
tabWidth: 2,
endOfLine: 'auto',
printWidth: 100,
singleQuote: true,
};
const prettierConfig = prettier.resolveConfig.sync('.') ?? {};
let filesWereTransformed = false;
@ -246,11 +242,15 @@ const transformHtml = (filename: string) => {
copyTransformedContents(filename, !verifyOnly, formattedHTML);
};
const getFilesFromGlobs = async (globs: string[]): Promise<string[]> => {
const getGlobs = (globs: string[]): string[] => {
globs.push('!**/dist');
if (!vitepress) {
globs.push('!**/.vitepress', '!**/vite.config.ts', '!src/docs/index.md');
}
return globs;
};
const getFilesFromGlobs = async (globs: string[]): Promise<string[]> => {
return await globby(globs, { dot: true });
};
@ -263,15 +263,18 @@ const getFilesFromGlobs = async (globs: string[]): Promise<string[]> => {
const sourceDirGlob = posix.join('.', SOURCE_DOCS_DIR, '**');
const action = verifyOnly ? 'Verifying' : 'Transforming';
const mdFiles = await getFilesFromGlobs([posix.join(sourceDirGlob, '*.md')]);
const mdFileGlobs = getGlobs([posix.join(sourceDirGlob, '*.md')]);
const mdFiles = await getFilesFromGlobs(mdFileGlobs);
console.log(`${action} ${mdFiles.length} markdown files...`);
mdFiles.forEach(transformMarkdown);
const htmlFiles = await getFilesFromGlobs([posix.join(sourceDirGlob, '*.html')]);
const htmlFileGlobs = getGlobs([posix.join(sourceDirGlob, '*.html')]);
const htmlFiles = await getFilesFromGlobs(htmlFileGlobs);
console.log(`${action} ${htmlFiles.length} html files...`);
htmlFiles.forEach(transformHtml);
const otherFiles = await getFilesFromGlobs([sourceDirGlob, '!**/*.md', '!**/*.html']);
const otherFileGlobs = getGlobs([sourceDirGlob, '!**/*.md', '!**/*.html']);
const otherFiles = await getFilesFromGlobs(otherFileGlobs);
console.log(`${action} ${otherFiles.length} other files...`);
otherFiles.forEach((file: string) => {
copyTransformedContents(file, !verifyOnly); // no transformation
@ -287,4 +290,32 @@ const getFilesFromGlobs = async (globs: string[]): Promise<string[]> => {
exec(`git add ${FINAL_DOCS_DIR}`);
}
}
if (watch) {
console.log(`Watching for changes in ${SOURCE_DOCS_DIR}`);
const matcher = (globs: string[]) => (file: string) => mm.every(file, globs);
const isMd = matcher(mdFileGlobs);
const isHtml = matcher(htmlFileGlobs);
const isOther = matcher(otherFileGlobs);
chokidar
.watch(SOURCE_DOCS_DIR)
// Delete files from the final docs dir if they are deleted from the source dir
.on('unlink', (file: string) => rmSync(changeToFinalDocDir(file)))
.on('unlinkDir', (file: string) => rmdirSync(changeToFinalDocDir(file)))
.on('all', (event, path) => {
// Ignore other events.
if (!['add', 'change'].includes(event)) {
return;
}
if (isMd(path)) {
transformMarkdown(path);
} else if (isHtml(path)) {
transformHtml(path);
} else if (isOther(path)) {
copyTransformedContents(path, true);
}
});
}
})();

View File

@ -24,7 +24,7 @@ Mermaid basically supports two types of configuration options to be overridden b
2. _Diagram specific configurations_ : These are the configurations that are available and applied to a specific diagram. For each diagram there are specific configuration that will alter how that particular diagram looks and behaves.
For example, `mirrorActors` is a configuration that is specific to the `SequenceDiagram` and alter whether the actors are mirrored or not. So this config is available only for the `SequenceDiagram` type.
**NOTE:** These options listed here are not all the configuration options. To get hold of all the configuration options, please refer to the [defaultConfig.js](https://github.com/mermaid-js/mermaid/blob/develop/src/defaultConfig.js) in the source code.
**NOTE:** These options listed here are not all the configuration options. To get hold of all the configuration options, please refer to the [defaultConfig.ts](https://github.com/mermaid-js/mermaid/blob/develop/packages/mermaid/src/defaultConfig.ts) in the source code.
```
Soon we plan to publish a complete list of top-level configurations & all the diagram specific configurations, with their possible values in the docs
@ -188,7 +188,7 @@ Some common flowchart configurations are:
- _diagramPadding_: number
- _useMaxWidth_: number
For complete list of flowchart configurations, see [defaultConfig.js](https://github.com/mermaid-js/mermaid/blob/develop/src/defaultConfig.js) in the source code.
For complete list of flowchart configurations, see [defaultConfig.ts](https://github.com/mermaid-js/mermaid/blob/develop/packages/mermaid/src/defaultConfig.ts) in the source code.
_Soon we plan to publish a complete list all diagram specific configurations updated in the docs_
The following code snippet changes flowchart config:
@ -221,7 +221,7 @@ Some common sequence configurations are:
- _showSequenceNumbers_: boolean
- _wrap_: boolean
For complete list of sequence diagram configurations, see _defaultConfig.js_ in the source code.
For complete list of sequence diagram configurations, see _defaultConfig.ts_ in the source code.
_Soon we plan to publish a complete list all diagram specific configurations updated in the docs_
So, `wrap` by default has a value of `false` for sequence diagrams.

View File

@ -1,150 +0,0 @@
'use strict';
import mermaid from './mermaid';
import mermaidAPI from './mermaidAPI';
import assignWithDepth from './assignWithDepth';
describe('when using mermaidAPI and ', function () {
describe('doing initialize ', function () {
beforeEach(function () {
document.body.innerHTML = '';
mermaidAPI.globalReset();
});
it('should copy a literal into the configuration', function () {
const orgConfig = mermaidAPI.getConfig();
expect(orgConfig.testLiteral).toBe(undefined);
mermaidAPI.initialize({ testLiteral: true });
const config = mermaidAPI.getConfig();
expect(config.testLiteral).toBe(true);
});
it('should copy a an object into the configuration', function () {
const orgConfig = mermaidAPI.getConfig();
expect(orgConfig.testObject).toBe(undefined);
const object = {
test1: 1,
test2: false,
};
mermaidAPI.initialize({ testObject: object });
let config = mermaidAPI.getConfig();
expect(config.testObject.test1).toBe(1);
mermaidAPI.updateSiteConfig({ testObject: { test3: true } });
config = mermaidAPI.getConfig();
expect(config.testObject.test1).toBe(1);
expect(config.testObject.test2).toBe(false);
expect(config.testObject.test3).toBe(true);
});
it('should reset mermaid config to global defaults', function () {
let config = {
logLevel: 0,
securityLevel: 'loose',
};
mermaidAPI.initialize(config);
mermaidAPI.setConfig({ securityLevel: 'strict', logLevel: 1 });
expect(mermaidAPI.getConfig().logLevel).toBe(1);
expect(mermaidAPI.getConfig().securityLevel).toBe('strict');
mermaidAPI.reset();
expect(mermaidAPI.getConfig().logLevel).toBe(0);
expect(mermaidAPI.getConfig().securityLevel).toBe('loose');
mermaidAPI.globalReset();
expect(mermaidAPI.getConfig().logLevel).toBe(5);
expect(mermaidAPI.getConfig().securityLevel).toBe('strict');
});
it('should prevent changes to site defaults (sneaky)', function () {
let config = {
logLevel: 0,
};
mermaidAPI.initialize(config);
const siteConfig = mermaidAPI.getSiteConfig();
expect(mermaidAPI.getConfig().logLevel).toBe(0);
config.secure = {
toString: function () {
mermaidAPI.initialize({ securityLevel: 'loose' });
},
};
// mermaidAPI.reinitialize(config);
expect(mermaidAPI.getConfig().secure).toEqual(mermaidAPI.getSiteConfig().secure);
expect(mermaidAPI.getConfig().securityLevel).toBe('strict');
mermaidAPI.reset();
expect(mermaidAPI.getSiteConfig()).toEqual(siteConfig);
expect(mermaidAPI.getConfig()).toEqual(siteConfig);
});
it('should prevent clobbering global defaults (direct)', function () {
let config = assignWithDepth({}, mermaidAPI.defaultConfig);
assignWithDepth(config, { logLevel: 0 });
let error = { message: '' };
try {
mermaidAPI['defaultConfig'] = config;
} catch (e) {
error = e;
}
expect(error.message).toBe(
"Cannot assign to read only property 'defaultConfig' of object '#<Object>'"
);
expect(mermaidAPI.defaultConfig['logLevel']).toBe(5);
});
it('should prevent changes to global defaults (direct)', function () {
let error = { message: '' };
try {
mermaidAPI.defaultConfig['logLevel'] = 0;
} catch (e) {
error = e;
}
expect(error.message).toBe(
"Cannot assign to read only property 'logLevel' of object '#<Object>'"
);
expect(mermaidAPI.defaultConfig['logLevel']).toBe(5);
});
it('should prevent sneaky changes to global defaults (assignWithDepth)', function () {
let config = {
logLevel: 0,
};
let error = { message: '' };
try {
assignWithDepth(mermaidAPI.defaultConfig, config);
} catch (e) {
error = e;
}
expect(error.message).toBe(
"Cannot assign to read only property 'logLevel' of object '#<Object>'"
);
expect(mermaidAPI.defaultConfig['logLevel']).toBe(5);
});
});
describe('dompurify config', function () {
it('should allow dompurify config to be set', function () {
mermaidAPI.initialize({ dompurifyConfig: { ADD_ATTR: ['onclick'] } });
expect(mermaidAPI.getConfig().dompurifyConfig.ADD_ATTR).toEqual(['onclick']);
});
});
describe('test mermaidApi.parse() for checking validity of input ', function () {
mermaid.parseError = undefined; // ensure it parseError undefined
it('should throw for an invalid definition (with no mermaid.parseError() defined)', function () {
expect(mermaid.parseError).toEqual(undefined);
expect(() => mermaidAPI.parse('this is not a mermaid diagram definition')).toThrow();
});
it('should not throw for a valid definition', function () {
expect(() => mermaidAPI.parse('graph TD;A--x|text including URL space|B;')).not.toThrow();
});
it('it should return false for invalid definition WITH a parseError() callback defined', function () {
let parseErrorWasCalled = false;
// also test setParseErrorHandler() call working to set mermaid.parseError
expect(
mermaidAPI.parse('this is not a mermaid diagram definition', () => {
parseErrorWasCalled = true;
})
).toEqual(false);
expect(parseErrorWasCalled).toEqual(true);
});
it('should return true for valid definition', function () {
expect(mermaidAPI.parse('graph TD;A--x|text including URL space|B;')).toEqual(true);
});
});
});

View File

@ -0,0 +1,649 @@
'use strict';
import { vi } from 'vitest';
import mermaid from './mermaid';
import { MermaidConfig } from './config.type';
import mermaidAPI, { removeExistingElements } from './mermaidAPI';
import {
encodeEntities,
decodeEntities,
createCssStyles,
createUserStyles,
appendDivSvgG,
cleanUpSvgCode,
putIntoIFrame,
} from './mermaidAPI';
import assignWithDepth from './assignWithDepth';
// --------------
// Mocks
// To mock a module, first define a mock for it, then (if used explicitly in the tests) import it. Be sure the path points to exactly the same file as is imported in mermaidAPI (the module being tested)
vi.mock('./styles', () => {
return {
addStylesForDiagram: vi.fn(),
default: vi.fn().mockReturnValue(' .userStyle { font-weight:bold; }'),
};
});
import getStyles from './styles';
vi.mock('stylis', () => {
return {
stringify: vi.fn(),
compile: vi.fn(),
serialize: vi.fn().mockReturnValue('stylis serialized css'),
};
});
import { compile, serialize } from 'stylis';
import { MockedD3 } from './tests/MockedD3';
// -------------------------------------------------------------------------------------
describe('mermaidAPI', function () {
describe('encodeEntities', () => {
it('removes the ending ; from style [text1]:[optional word]#[text2]; with ', () => {
const text = 'style this; is ; everything :something#not-nothing; and this too;';
expect(encodeEntities(text)).toEqual(
'style this; is ; everything :something#not-nothing; and this too'
);
});
it('removes the ending ; from classDef [text1]:[optional word]#[text2]; with ', () => {
const text = 'classDef this; is ; everything :something#not-nothing; and this too;';
expect(encodeEntities(text)).toEqual(
'classDef this; is ; everything :something#not-nothing; and this too'
);
});
describe('replaces words starting with # and ending with ;', () => {
const testStr = 'Hello #there;';
it('removes the #', () => {
const result = encodeEntities(testStr);
expect(result.substring(0, 7)).toEqual('Hello fl');
});
it('prefix is fl°° if is all digits', () => {
const result = encodeEntities('Hello #77653;');
expect(result.substring(6, result.length)).toEqual('fl°°77653¶ß');
});
it('prefix is fl° if is not all digits', () => {
const result = encodeEntities(testStr);
expect(result.substring(6, result.length)).toEqual('fl°there¶ß');
});
it('always removes the semi-colon and ends with ¶ß', () => {
const result = encodeEntities(testStr);
expect(result.substring(result.length - 2, result.length)).toEqual('¶ß');
});
});
it('does all the replacements on the given text', () => {
const text =
'style this; is ; everything :something#not-nothing; and this too; \n' +
'classDef this; is ; everything :something#not-nothing; and this too; \n' +
'Hello #there; #andHere;#77653;';
const result = encodeEntities(text);
expect(result).toEqual(
'style this; is ; everything :something#not-nothing; and this too \n' +
'classDef this; is ; everything :something#not-nothing; and this too \n' +
'Hello fl°there¶ß fl°andHere¶ßfl°°77653¶ß'
);
});
});
describe('decodeEntities', () => {
it('replaces fl°° with &#', () => {
expect(decodeEntities('fl°°hfl°°ifl°°')).toEqual('&#h&#i&#');
});
it('replaces fl° with &', () => {
expect(decodeEntities('fl°hfl°ifl°')).toEqual('&h&i&');
});
it('replaces ¶ß with ;', () => {
expect(decodeEntities('¶ßh¶ßi¶ß')).toEqual(';h;i;');
});
it('runs all the replacements on the given text', () => {
expect(decodeEntities('¶ßfl°¶ßfl°°¶ß')).toEqual(';&;&#;');
});
});
describe('cleanUpSvgCode', () => {
it('replaces marker end URLs with just the anchor if not sandboxed and not useMarkerUrls', () => {
const markerFullUrl = 'marker-end="url(some-URI#that)"';
let useArrowMarkerUrls = false;
let isSandboxed = false;
let result = cleanUpSvgCode(markerFullUrl, isSandboxed, useArrowMarkerUrls);
expect(result).toEqual('marker-end="url(#that)"');
useArrowMarkerUrls = true;
result = cleanUpSvgCode(markerFullUrl, isSandboxed, useArrowMarkerUrls);
expect(result).toEqual(markerFullUrl); // not changed
useArrowMarkerUrls = false;
isSandboxed = true;
result = cleanUpSvgCode(markerFullUrl, isSandboxed, useArrowMarkerUrls);
expect(result).toEqual(markerFullUrl); // not changed
});
it('decodesEntities', () => {
const result = cleanUpSvgCode('¶ß brrrr', true, true);
expect(result).toEqual('; brrrr');
});
it('replaces old style br tags with new style', () => {
const result = cleanUpSvgCode('<br> brrrr<br>', true, true);
expect(result).toEqual('<br/> brrrr<br/>');
});
});
describe('putIntoIFrame', () => {
const inputSvgCode = 'this is the SVG code';
it('uses the default SVG iFrame height is used if no svgElement given', () => {
const result = putIntoIFrame(inputSvgCode);
expect(result).toMatch(/style="(.*)height:100%(.*);"/);
});
it('default style attributes are: width: 100%, height: 100%, border: 0, margin: 0', () => {
const result = putIntoIFrame(inputSvgCode);
expect(result).toMatch(/style="(.*)width:100%(.*);"/);
expect(result).toMatch(/style="(.*)height:100%(.*);"/);
expect(result).toMatch(/style="(.*)border:0(.*);"/);
expect(result).toMatch(/style="(.*)margin:0(.*);"/);
});
it('sandbox="allow-top-navigation-by-user-activation allow-popups">', () => {
const result = putIntoIFrame(inputSvgCode);
expect(result).toMatch(/sandbox="allow-top-navigation-by-user-activation allow-popups">/);
});
it('msg shown is "The "iframe" tag is not supported by your browser.\\n" if iFrames are not supported in the browser', () => {
const result = putIntoIFrame(inputSvgCode);
expect(result).toMatch(/\s*The "iframe" tag is not supported by your browser\./);
});
it('sets src to base64 version of <body style="IFRAME_SVG_BODY_STYLE">svgCode<//body>', () => {
const base64encodedSrc = btoa('<body style="' + 'margin:0' + '">' + inputSvgCode + '</body>');
const expectedRegExp = new RegExp('src="data:text/html;base64,' + base64encodedSrc + '"');
const result = putIntoIFrame(inputSvgCode);
expect(result).toMatch(expectedRegExp);
});
it('uses the height and appends px from the svgElement given', () => {
const faux_svgElement = {
viewBox: {
baseVal: {
height: 42,
},
},
};
const result = putIntoIFrame(inputSvgCode, faux_svgElement);
expect(result).toMatch(/style="(.*)height:42px;/);
});
});
const fauxParentNode = new MockedD3();
const fauxEnclosingDiv = new MockedD3();
const fauxSvgNode = new MockedD3();
describe('appendDivSvgG', () => {
const fauxGNode = new MockedD3();
const parent_append_spy = vi.spyOn(fauxParentNode, 'append').mockReturnValue(fauxEnclosingDiv);
const div_append_spy = vi.spyOn(fauxEnclosingDiv, 'append').mockReturnValue(fauxSvgNode);
// @ts-ignore @todo TODO why is this getting a type error?
const div_attr_spy = vi.spyOn(fauxEnclosingDiv, 'attr').mockReturnValue(fauxEnclosingDiv);
const svg_append_spy = vi.spyOn(fauxSvgNode, 'append').mockReturnValue(fauxGNode);
// @ts-ignore @todo TODO why is this getting a type error?
const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
it('appends a div node', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(parent_append_spy).toHaveBeenCalledWith('div');
expect(div_append_spy).toHaveBeenCalledWith('svg');
});
it('the id for the div is "d" with the id appended', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId');
});
it('sets the style for the div if one is given', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId', 'given div style', 'given x link');
expect(div_attr_spy).toHaveBeenCalledWith('style', 'given div style');
});
it('appends a svg node to the div node', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId');
});
it('sets the svg width to 100%', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(svg_attr_spy).toHaveBeenCalledWith('width', '100%');
});
it('the svg id is the id', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(svg_attr_spy).toHaveBeenCalledWith('id', 'theId');
});
it('the svg xml namespace is the 2000 standard', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(svg_attr_spy).toHaveBeenCalledWith('xmlns', 'http://www.w3.org/2000/svg');
});
it('sets the svg xlink if one is given', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId', 'div style', 'given x link');
expect(svg_attr_spy).toHaveBeenCalledWith('xmlns:xlink', 'given x link');
});
it('appends a g (group) node to the svg node', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(svg_append_spy).toHaveBeenCalledWith('g');
});
it('returns the given parentRoot d3 nodes', () => {
expect(appendDivSvgG(fauxParentNode, 'theId', 'dtheId')).toEqual(fauxParentNode);
});
});
describe('createCssStyles', () => {
const serif = 'serif';
const sansSerif = 'sans-serif';
const mocked_config_with_htmlLabels: MermaidConfig = {
themeCSS: 'default',
fontFamily: serif,
altFontFamily: sansSerif,
htmlLabels: true,
};
it('gets the cssStyles from the theme', () => {
const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', null);
expect(styles).toMatch(/^\ndefault(.*)/);
});
it('gets the fontFamily from the config', () => {
const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', {});
expect(styles).toMatch(/(.*)\n:root \{ --mermaid-font-family: serif(.*)/);
});
it('gets the alt fontFamily from the config', () => {
const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', undefined);
expect(styles).toMatch(/(.*)\n:root \{ --mermaid-alt-font-family: sans-serif(.*)/);
});
describe('there are some classDefs', () => {
const classDef1 = { id: 'classDef1', styles: ['style1-1', 'style1-2'], textStyles: [] };
const classDef2 = { id: 'classDef2', styles: [], textStyles: ['textStyle2-1'] };
const classDef3 = { id: 'classDef3', textStyles: ['textStyle3-1', 'textStyle3-2'] };
const classDefs = { classDef1, classDef2, classDef3 };
describe('the graph supports classDefs', () => {
const graphType = 'flowchart-v2';
const REGEXP_SPECIALS = ['^', '$', '?', '(', '{', '[', '.', '*', '!'];
// prefix any special RegExp characters in the given string with a \ so we can use the literal character in a RegExp
function escapeForRegexp(str: string) {
const strChars = str.split(''); // split into array of every char
const strEscaped = strChars.map((char) => {
if (REGEXP_SPECIALS.includes(char)) {
return `\\${char}`;
} else {
return char;
}
});
return strEscaped.join('');
}
// Common test expecting given styles to have .classDef1 and .classDef2 statements but not .classDef3
function expect_styles_matchesHtmlElements(styles: string, htmlElement: string) {
expect(styles).toMatch(
new RegExp(
`\\.classDef1 ${escapeForRegexp(
htmlElement
)} \\{ style1-1 !important; style1-2 !important; }`
)
);
// no CSS styles are created if there are no styles for a classDef
expect(styles).not.toMatch(
new RegExp(`\\.classDef2 ${escapeForRegexp(htmlElement)} \\{ style(.*) !important; }`)
);
expect(styles).not.toMatch(
new RegExp(`\\.classDef3 ${escapeForRegexp(htmlElement)} \\{ style(.*) !important; }`)
);
}
// Common test expecting given textStyles to have .classDef2 and .classDef3 statements but not .classDef1
function expect_textStyles_matchesHtmlElements(textStyles: string, htmlElement: string) {
expect(textStyles).toMatch(
new RegExp(
`\\.classDef2 ${escapeForRegexp(htmlElement)} \\{ textStyle2-1 !important; }`
)
);
expect(textStyles).toMatch(
new RegExp(
`\\.classDef3 ${escapeForRegexp(
htmlElement
)} \\{ textStyle3-1 !important; textStyle3-2 !important; }`
)
);
// no CSS styles are created if there are no textStyles for a classDef
expect(textStyles).not.toMatch(
new RegExp(
`\\.classDef1 ${escapeForRegexp(htmlElement)} \\{ textStyle(.*) !important; }`
)
);
}
// common suite and tests to verify that the right styles are created with the right htmlElements
function expect_correct_styles_with_htmlElements(mocked_config: MermaidConfig) {
describe('creates styles for "> *" and "span" elements', () => {
const htmlElements = ['> *', 'span'];
it('creates CSS styles for every style and textStyle in every classDef', () => {
// @todo TODO Can't figure out how to spy on the cssImportantStyles method. That would be a much better approach than manually checking the result
const styles = createCssStyles(mocked_config, graphType, classDefs);
htmlElements.forEach((htmlElement) => {
expect_styles_matchesHtmlElements(styles, htmlElement);
});
expect_textStyles_matchesHtmlElements(styles, 'tspan');
});
});
}
it('there are htmlLabels in the configuration', () => {
expect_correct_styles_with_htmlElements(mocked_config_with_htmlLabels);
});
it('there are flowchart.htmlLabels in the configuration', () => {
const mocked_config_flowchart_htmlLabels: MermaidConfig = {
themeCSS: 'default',
fontFamily: 'serif',
altFontFamily: 'sans-serif',
flowchart: {
htmlLabels: true,
},
};
expect_correct_styles_with_htmlElements(mocked_config_flowchart_htmlLabels);
});
describe('no htmlLabels in the configuration', () => {
const mocked_config_no_htmlLabels = {
themeCSS: 'default',
fontFamily: 'serif',
altFontFamily: 'sans-serif',
};
describe('creates styles for shape elements "rect", "polygon", "ellipse", and "circle"', () => {
const htmlElements = ['rect', 'polygon', 'ellipse', 'circle'];
it('creates CSS styles for every style and textStyle in every classDef', () => {
// @todo TODO Can't figure out how to spy on the cssImportantStyles method. That would be a much better approach than manually checking the result
const styles = createCssStyles(mocked_config_no_htmlLabels, graphType, classDefs);
htmlElements.forEach((htmlElement) => {
expect_styles_matchesHtmlElements(styles, htmlElement);
});
expect_textStyles_matchesHtmlElements(styles, 'tspan');
});
});
});
});
});
});
describe('createUserStyles', () => {
const mockConfig = {
themeCSS: 'default',
htmlLabels: true,
themeVariables: { fontFamily: 'serif' },
};
const classDef1 = { id: 'classDef1', styles: ['style1-1'], textStyles: [] };
it('gets the css styles created', () => {
// @todo TODO if a single function in the module can be mocked, do it for createCssStyles and mock the results.
createUserStyles(mockConfig, 'flowchart-v2', { classDef1 }, 'someId');
const expectedStyles =
'\ndefault' +
'\n.classDef1 > * { style1-1 !important; }' +
'\n.classDef1 span { style1-1 !important; }';
expect(getStyles).toHaveBeenCalledWith('flowchart-v2', expectedStyles, {
fontFamily: 'serif',
});
});
it('calls getStyles to get css for all graph, user css styles, and config theme variables', () => {
createUserStyles(mockConfig, 'someDiagram', {}, 'someId');
expect(getStyles).toHaveBeenCalled();
});
it('returns the result of compiling, stringifying, and serializing the css code with stylis', () => {
const result = createUserStyles(mockConfig, 'someDiagram', {}, 'someId');
expect(compile).toHaveBeenCalled();
expect(serialize).toHaveBeenCalled();
expect(result).toEqual('stylis serialized css');
});
});
describe('removeExistingElements', () => {
const svgId = 'svgId';
const tempDivId = 'tempDivId';
const tempIframeId = 'tempIFrameId';
const givenDocument = new Document();
const rootHtml = givenDocument.createElement('html');
givenDocument.append(rootHtml);
const svgElement = givenDocument.createElement('svg'); // doesn't matter what the tag is in the test
svgElement.id = svgId;
const tempDivElement = givenDocument.createElement('div'); // doesn't matter what the tag is in the test
tempDivElement.id = tempDivId;
const tempiFrameElement = givenDocument.createElement('div'); // doesn't matter what the tag is in the test
tempiFrameElement.id = tempIframeId;
it('removes an existing element with given id', () => {
rootHtml.appendChild(svgElement);
expect(givenDocument.getElementById(svgElement.id)).toEqual(svgElement);
removeExistingElements(givenDocument, false, svgId, tempDivId, tempIframeId);
expect(givenDocument.getElementById(svgElement.id)).toBeNull();
});
describe('is in sandboxed mode', () => {
const inSandboxedMode = true;
it('removes an existing element with the given iFrame selector', () => {
tempiFrameElement.append(svgElement);
rootHtml.append(tempiFrameElement);
rootHtml.append(tempDivElement);
expect(givenDocument.getElementById(tempIframeId)).toEqual(tempiFrameElement);
expect(givenDocument.getElementById(tempDivId)).toEqual(tempDivElement);
expect(givenDocument.getElementById(svgId)).toEqual(svgElement);
removeExistingElements(
givenDocument,
inSandboxedMode,
svgId,
'#' + tempDivId,
'#' + tempIframeId
);
expect(givenDocument.getElementById(tempDivId)).toEqual(tempDivElement);
expect(givenDocument.getElementById(tempIframeId)).toBeNull();
expect(givenDocument.getElementById(svgId)).toBeNull();
});
});
describe('not in sandboxed mode', () => {
const inSandboxedMode = false;
it('removes an existing element with the given enclosing div selector', () => {
tempDivElement.append(svgElement);
rootHtml.append(tempDivElement);
rootHtml.append(tempiFrameElement);
expect(givenDocument.getElementById(tempIframeId)).toEqual(tempiFrameElement);
expect(givenDocument.getElementById(tempDivId)).toEqual(tempDivElement);
expect(givenDocument.getElementById(svgId)).toEqual(svgElement);
removeExistingElements(
givenDocument,
inSandboxedMode,
svgId,
'#' + tempDivId,
'#' + tempIframeId
);
expect(givenDocument.getElementById(tempIframeId)).toEqual(tempiFrameElement);
expect(givenDocument.getElementById(tempDivId)).toBeNull();
expect(givenDocument.getElementById(svgId)).toBeNull();
});
});
});
describe('initialize', function () {
beforeEach(function () {
document.body.innerHTML = '';
mermaidAPI.globalReset();
});
it('copies a literal into the configuration', function () {
const orgConfig: any = mermaidAPI.getConfig();
expect(orgConfig.testLiteral).toBe(undefined);
const testConfig: any = { testLiteral: true };
mermaidAPI.initialize(testConfig);
const config: any = mermaidAPI.getConfig();
expect(config.testLiteral).toBe(true);
});
it('copies a an object into the configuration', function () {
const orgConfig: any = mermaidAPI.getConfig();
expect(orgConfig.testObject).toBe(undefined);
const object = {
test1: 1,
test2: false,
};
const testConfig: any = { testObject: object };
mermaidAPI.initialize(testConfig);
let config: any = mermaidAPI.getConfig();
expect(config.testObject.test1).toBe(1);
const testObjSetting: any = { testObject: { test3: true } };
mermaidAPI.updateSiteConfig(testObjSetting);
config = mermaidAPI.getConfig();
expect(config.testObject.test1).toBe(1);
expect(config.testObject.test2).toBe(false);
expect(config.testObject.test3).toBe(true);
});
it('resets mermaid config to global defaults', function () {
const config = {
logLevel: 0,
securityLevel: 'loose',
};
mermaidAPI.initialize(config);
mermaidAPI.setConfig({ securityLevel: 'strict', logLevel: 1 });
expect(mermaidAPI.getConfig().logLevel).toBe(1);
expect(mermaidAPI.getConfig().securityLevel).toBe('strict');
mermaidAPI.reset();
expect(mermaidAPI.getConfig().logLevel).toBe(0);
expect(mermaidAPI.getConfig().securityLevel).toBe('loose');
mermaidAPI.globalReset();
expect(mermaidAPI.getConfig().logLevel).toBe(5);
expect(mermaidAPI.getConfig().securityLevel).toBe('strict');
});
it('prevents changes to site defaults (sneaky)', function () {
const config: any = {
logLevel: 0,
};
mermaidAPI.initialize(config);
const siteConfig = mermaidAPI.getSiteConfig();
expect(mermaidAPI.getConfig().logLevel).toBe(0);
config.secure = {
toString: function () {
mermaidAPI.initialize({ securityLevel: 'loose' });
},
};
// mermaidAPI.reinitialize(config);
expect(mermaidAPI.getConfig().secure).toEqual(mermaidAPI.getSiteConfig().secure);
expect(mermaidAPI.getConfig().securityLevel).toBe('strict');
mermaidAPI.reset();
expect(mermaidAPI.getSiteConfig()).toEqual(siteConfig);
expect(mermaidAPI.getConfig()).toEqual(siteConfig);
});
it('prevents clobbering global defaults (direct)', function () {
const config = assignWithDepth({}, mermaidAPI.defaultConfig);
assignWithDepth(config, { logLevel: 0 });
let error: any = { message: '' };
try {
// @ts-ignore This is a read-only property. Typescript will not allow assignment, but regular javascript might.
mermaidAPI['defaultConfig'] = config;
} catch (e) {
error = e;
}
expect(error.message).toBe(
"Cannot assign to read only property 'defaultConfig' of object '#<Object>'"
);
expect(mermaidAPI.defaultConfig['logLevel']).toBe(5);
});
it('prevents changes to global defaults (direct)', function () {
let error: any = { message: '' };
try {
mermaidAPI.defaultConfig['logLevel'] = 0;
} catch (e) {
error = e;
}
expect(error.message).toBe(
"Cannot assign to read only property 'logLevel' of object '#<Object>'"
);
expect(mermaidAPI.defaultConfig['logLevel']).toBe(5);
});
it('prevents sneaky changes to global defaults (assignWithDepth)', function () {
const config = {
logLevel: 0,
};
let error: any = { message: '' };
try {
assignWithDepth(mermaidAPI.defaultConfig, config);
} catch (e) {
error = e;
}
expect(error.message).toBe(
"Cannot assign to read only property 'logLevel' of object '#<Object>'"
);
expect(mermaidAPI.defaultConfig['logLevel']).toBe(5);
});
});
describe('dompurify config', function () {
it('allows dompurify config to be set', function () {
mermaidAPI.initialize({ dompurifyConfig: { ADD_ATTR: ['onclick'] } });
expect(mermaidAPI!.getConfig()!.dompurifyConfig!.ADD_ATTR).toEqual(['onclick']);
});
});
describe('parse', function () {
mermaid.parseError = undefined; // ensure it parseError undefined
it('throws for an invalid definition (with no mermaid.parseError() defined)', function () {
expect(mermaid.parseError).toEqual(undefined);
expect(() => mermaidAPI.parse('this is not a mermaid diagram definition')).toThrow();
});
it('does not throw for a valid definition', function () {
expect(() => mermaidAPI.parse('graph TD;A--x|text including URL space|B;')).not.toThrow();
});
it('returns false for invalid definition WITH a parseError() callback defined', function () {
let parseErrorWasCalled = false;
// also test setParseErrorHandler() call working to set mermaid.parseError
expect(
mermaidAPI.parse('this is not a mermaid diagram definition', () => {
parseErrorWasCalled = true;
})
).toEqual(false);
expect(parseErrorWasCalled).toEqual(true);
});
it('returns true for valid definition', function () {
expect(mermaidAPI.parse('graph TD;A--x|text including URL space|B;')).toEqual(true);
});
});
});

View File

@ -29,10 +29,49 @@ import utils, { directiveSanitizer } from './utils';
import DOMPurify from 'dompurify';
import { MermaidConfig } from './config.type';
import { evaluate } from './diagrams/common/common';
import { isEmpty } from 'lodash';
// diagram names that support classDef statements
const CLASSDEF_DIAGRAMS = ['graph', 'flowchart', 'flowchart-v2', 'stateDiagram'];
const MAX_TEXTLENGTH_EXCEEDED_MSG =
'graph TB;a[Maximum text size in diagram exceeded];style a fill:#faa';
const SECURITY_LVL_SANDBOX = 'sandbox';
const SECURITY_LVL_LOOSE = 'loose';
const XMLNS_SVG_STD = 'http://www.w3.org/2000/svg';
const XMLNS_XLINK_STD = 'http://www.w3.org/1999/xlink';
const XMLNS_XHTML_STD = 'http://www.w3.org/1999/xhtml';
// ------------------------------
// iFrame
const IFRAME_WIDTH = '100%';
const IFRAME_HEIGHT = '100%';
const IFRAME_STYLES = 'border:0;margin:0;';
const IFRAME_BODY_STYLE = 'margin:0';
const IFRAME_SANDBOX_OPTS = 'allow-top-navigation-by-user-activation allow-popups';
const IFRAME_NOT_SUPPORTED_MSG = 'The "iframe" tag is not supported by your browser.';
// DOMPurify settings for svgCode
const DOMPURE_TAGS = ['foreignobject'];
const DOMPURE_ATTR = ['dominant-baseline'];
// This is what is returned from getClasses(...) methods.
// It is slightly renamed to ..StyleClassDef instead of just ClassDef because "class" is a greatly ambiguous and overloaded word.
// It makes it clear we're working with a style class definition, even though defining the type is currently difficult.
interface DiagramStyleClassDef {
id: string;
styles?: string[];
textStyles?: string[];
}
// This makes it clear that we're working with a d3 selected element of some kind, even though it's hard to specify the exact type.
// @ts-ignore Could replicate the type definition in d3. This also makes it possible to use the untyped info from the js diagram files.
type D3Element = any;
// ----------------------------------------------------------------------------
/**
* @param text - The mermaid diagram definition.
* @param parseError - If set, handles errors.
@ -43,16 +82,19 @@ function parse(text: string, parseError?: ParseErrorFunction): boolean {
return diagram.parse(text, parseError);
}
/**
*
* @param text - text to be encoded
* @returns
*/
export const encodeEntities = function (text: string): string {
let txt = text;
txt = txt.replace(/style.*:\S*#.*;/g, function (s) {
const innerTxt = s.substring(0, s.length - 1);
return innerTxt;
txt = txt.replace(/style.*:\S*#.*;/g, function (s): string {
return s.substring(0, s.length - 1);
});
txt = txt.replace(/classDef.*:\S*#.*;/g, function (s) {
const innerTxt = s.substring(0, s.length - 1);
return innerTxt;
txt = txt.replace(/classDef.*:\S*#.*;/g, function (s): string {
return s.substring(0, s.length - 1);
});
txt = txt.replace(/#\w+;/g, function (s) {
@ -69,6 +111,11 @@ export const encodeEntities = function (text: string): string {
return txt;
};
/**
*
* @param text - text to be decoded
* @returns
*/
export const decodeEntities = function (text: string): string {
let txt = text;
@ -84,6 +131,226 @@ export const decodeEntities = function (text: string): string {
return txt;
};
// append !important; to each cssClass followed by a final !important, all enclosed in { }
//
/**
* Create a CSS style that starts with the given class name, then the element,
* with an enclosing block that has each of the cssClasses followed by !important;
* @param cssClass - CSS class name
* @param element - CSS element
* @param cssClasses - list of CSS styles to append after the element
* @returns - the constructed string
*/
export const cssImportantStyles = (
cssClass: string,
element: string,
cssClasses: string[] = []
): string => {
return `\n.${cssClass} ${element} { ${cssClasses.join(' !important; ')} !important; }`;
};
/**
* Create the user styles
*
* @param config - configuration that has style and theme settings to use
* @param graphType - used for checking if classDefs should be applied
* @param classDefs - the classDefs in the diagram text. Might be null if none were defined. Usually is the result of a call to getClasses(...)
* @returns the string with all the user styles
*/
export const createCssStyles = (
config: MermaidConfig,
graphType: string,
classDefs: Record<string, DiagramStyleClassDef> | null | undefined = {}
): string => {
let cssStyles = '';
// user provided theme CSS info
// If you add more configuration driven data into the user styles make sure that the value is
// sanitized by the sanitize CSS function TODO where is this method? what should be used to replace it? refactor so that it's always sanitized
if (config.themeCSS !== undefined) {
cssStyles += `\n${config.themeCSS}`;
}
if (config.fontFamily !== undefined) {
cssStyles += `\n:root { --mermaid-font-family: ${config.fontFamily}}`;
}
if (config.altFontFamily !== undefined) {
cssStyles += `\n:root { --mermaid-alt-font-family: ${config.altFontFamily}}`;
}
// classDefs defined in the diagram text
if (!isEmpty(classDefs)) {
if (CLASSDEF_DIAGRAMS.includes(graphType)) {
const htmlLabels = config.htmlLabels || config.flowchart?.htmlLabels; // TODO why specifically check the Flowchart diagram config?
const cssHtmlElements = ['> *', 'span']; // TODO make a constant
const cssShapeElements = ['rect', 'polygon', 'ellipse', 'circle']; // TODO make a constant
const cssElements = htmlLabels ? cssHtmlElements : cssShapeElements;
// create the CSS styles needed for each styleClass definition and css element
for (const classId in classDefs) {
const styleClassDef = classDefs[classId];
// create the css styles for each cssElement and the styles (only if there are styles)
if (!isEmpty(styleClassDef.styles)) {
cssElements.forEach((cssElement) => {
cssStyles += cssImportantStyles(styleClassDef.id, cssElement, styleClassDef.styles);
});
}
// create the css styles for the tspan element and the text styles (only if there are textStyles)
if (!isEmpty(styleClassDef.textStyles)) {
cssStyles += cssImportantStyles(styleClassDef.id, 'tspan', styleClassDef.textStyles);
}
}
}
}
return cssStyles;
};
export const createUserStyles = (
config: MermaidConfig,
graphType: string,
classDefs: Record<string, DiagramStyleClassDef>,
svgId: string
): string => {
const userCSSstyles = createCssStyles(config, graphType, classDefs);
const allStyles = getStyles(graphType, userCSSstyles, config.themeVariables);
// Now turn all of the styles into a (compiled) string that starts with the id
// use the stylis library to compile the css, turn the results into a valid CSS string (serialize(...., stringify))
// @see https://github.com/thysultan/stylis
return serialize(compile(`${svgId}{${allStyles}}`), stringify);
};
/**
* Clean up svgCode. Do replacements needed
*
* @param svgCode - the code to clean up
* @param inSandboxMode - security level
* @param useArrowMarkerUrls - should arrow marker's use full urls? (vs. just the anchors)
* @returns the cleaned up svgCode
*/
export const cleanUpSvgCode = (
svgCode = '',
inSandboxMode: boolean,
useArrowMarkerUrls: boolean
): string => {
let cleanedUpSvg = svgCode;
// Replace marker-end urls with just the # anchor (remove the preceding part of the URL)
if (!useArrowMarkerUrls && !inSandboxMode) {
cleanedUpSvg = cleanedUpSvg.replace(/marker-end="url\(.*?#/g, 'marker-end="url(#');
}
cleanedUpSvg = decodeEntities(cleanedUpSvg);
// replace old br tags with newer style
cleanedUpSvg = cleanedUpSvg.replace(/<br>/g, '<br/>');
return cleanedUpSvg;
};
/**
* Put the svgCode into an iFrame. Return the iFrame code
*
* @param svgCode - the svg code to put inside the iFrame
* @param svgElement - the d3 node that has the current svgElement so we can get the height from it
* @returns - the code with the iFrame that now contains the svgCode
* TODO replace btoa(). Replace with buf.toString('base64')?
*/
export const putIntoIFrame = (svgCode = '', svgElement?: D3Element): string => {
const height = svgElement ? svgElement.viewBox.baseVal.height + 'px' : IFRAME_HEIGHT;
const base64encodedSrc = btoa('<body style="' + IFRAME_BODY_STYLE + '">' + svgCode + '</body>');
return `<iframe style="width:${IFRAME_WIDTH};height:${height};${IFRAME_STYLES}" src="data:text/html;base64,${base64encodedSrc}" sandbox="${IFRAME_SANDBOX_OPTS}">
${IFRAME_NOT_SUPPORTED_MSG}
</iframe>`;
};
/**
* Append an enclosing div, then svg, then g (group) to the d3 parentRoot. Set attributes.
* Only set the style attribute on the enclosing div if divStyle is given.
* Only set the xmlns:xlink attribute on svg if svgXlink is given.
* Return the last node appended
*
* @param parentRoot - the d3 node to append things to
* @param id - the value to set the id attr to
* @param enclosingDivId - the id to set the enclosing div to
* @param divStyle - if given, the style to set the enclosing div to
* @param svgXlink - if given, the link to set the new svg element to
* @returns - returns the parentRoot that had nodes appended
*/
export const appendDivSvgG = (
parentRoot: D3Element,
id: string,
enclosingDivId: string,
divStyle?: string,
svgXlink?: string
): D3Element => {
const enclosingDiv = parentRoot.append('div');
enclosingDiv.attr('id', enclosingDivId);
if (divStyle) {
enclosingDiv.attr('style', divStyle);
}
const svgNode = enclosingDiv
.append('svg')
.attr('id', id)
.attr('width', '100%')
.attr('xmlns', XMLNS_SVG_STD);
if (svgXlink) {
svgNode.attr('xmlns:xlink', svgXlink);
}
svgNode.append('g');
return parentRoot;
};
/**
* Append an iFrame node to the given parentNode and set the id, style, and 'sandbox' attributes
* Return the appended iframe d3 node
*
* @param parentNode - the d3 node to append the iFrame node to
* @param iFrameId - id to use for the iFrame
* @returns the appended iframe d3 node
*/
function sandboxedIframe(parentNode: D3Element, iFrameId: string): D3Element {
return parentNode
.append('iframe')
.attr('id', iFrameId)
.attr('style', 'width: 100%; height: 100%;')
.attr('sandbox', '');
}
/**
* Remove any existing elements from the given document
*
* @param doc - the document to removed elements from
* @param isSandboxed - whether or not we are in sandboxed mode
* @param id - id for any existing SVG element
* @param divSelector - selector for any existing enclosing div element
* @param iFrameSelector - selector for any existing iFrame element
*/
export const removeExistingElements = (
doc: Document,
isSandboxed: boolean,
id: string,
divSelector: string,
iFrameSelector: string
) => {
// Remove existing SVG element if it exists
const existingSvg = doc.getElementById(id);
if (existingSvg) {
existingSvg.remove();
}
// Remove previous temporary element if it exists
const element = isSandboxed ? doc.querySelector(iFrameSelector) : doc.querySelector(divSelector);
if (element) {
element.remove();
}
};
/**
* Function that renders an svg with a graph from a chart definition. Usage example below.
*
@ -100,10 +367,12 @@ export const decodeEntities = function (text: string): string {
* });
* ```
*
* @param id - The id of the element to be rendered
* @param text - The graph definition
* @param cb - Callback which is called after rendering is finished with the svg code as param.
* @param container - Selector to element in which a div with the graph temporarily will be
* @param id - The id for the SVG element (the element to be rendered)
* @param text - The text for the graph definition
* @param cb - Callback which is called after rendering is finished with the svg code as in param.
* @param svgContainingElement - HTML element where the svg will be inserted. (Is usually element with the .mermaid class)
* If no svgContainingElement is provided then the SVG element will be appended to the body.
* Selector to element in which a div with the graph temporarily will be
* inserted. If one is provided a hidden div will be inserted in the body of the page instead. The
* element will be removed when rendering is completed.
* @returns - Resolves when finished rendering.
@ -112,115 +381,92 @@ const render = async function (
id: string,
text: string,
cb: (svgCode: string, bindFunctions?: (element: Element) => void) => void,
container?: Element
svgContainingElement?: Element
): Promise<void> {
addDiagrams();
configApi.reset();
text = text.replace(/\r\n?/g, '\n'); // parser problems on CRLF ignore all CR and leave LF;;
// Add Directives. Must do this before getting the config and before creating the diagram.
const graphInit = utils.detectInit(text);
if (graphInit) {
directiveSanitizer(graphInit);
configApi.addDirective(graphInit);
}
const cnf = configApi.getConfig();
log.debug(cnf);
const config = configApi.getConfig();
log.debug(config);
// Check the maximum allowed text size
if (text.length > cnf.maxTextSize!) {
text = 'graph TB;a[Maximum text size in diagram exceeded];style a fill:#faa';
// TODO: Remove magic number
if (text.length > (config?.maxTextSize ?? 50000)) {
text = MAX_TEXTLENGTH_EXCEEDED_MSG;
}
// clean up text CRLFs
text = text.replace(/\r\n?/g, '\n'); // parser problems on CRLF ignore all CR and leave LF;;
const idSelector = '#' + id;
const iFrameID = 'i' + id;
const iFrameID_selector = '#' + iFrameID;
const enclosingDivID = 'd' + id;
const enclosingDivID_selector = '#' + enclosingDivID;
let root: any = select('body');
// In regular execution the container will be the div with a mermaid class
if (typeof container !== 'undefined') {
// A container was provided by the caller
if (container) {
container.innerHTML = '';
const isSandboxed = config.securityLevel === SECURITY_LVL_SANDBOX;
const isLooseSecurityLevel = config.securityLevel === SECURITY_LVL_LOOSE;
const fontFamily = config.fontFamily;
// -------------------------------------------------------------------------------
// Define the root d3 node
// In regular execution the svgContainingElement will be the element with a mermaid class
if (typeof svgContainingElement !== 'undefined') {
if (svgContainingElement) {
svgContainingElement.innerHTML = '';
}
if (cnf.securityLevel === 'sandbox') {
// IF we are in sandboxed mode, we do everything mermaid related
// in a sandboxed div
const iframe = select(container)
.append('iframe')
.attr('id', 'i' + id)
.attr('style', 'width: 100%; height: 100%;')
.attr('sandbox', '');
// const iframeBody = ;
if (isSandboxed) {
// If we are in sandboxed mode, we do everything mermaid related in a (sandboxed )iFrame
const iframe = sandboxedIframe(select(svgContainingElement), iFrameID);
root = select(iframe.nodes()[0]!.contentDocument!.body);
root.node().style.margin = 0;
} else {
root = select(container);
root = select(svgContainingElement);
}
root
.append('div')
.attr('id', 'd' + id)
.attr('style', 'font-family: ' + cnf.fontFamily)
.append('svg')
.attr('id', id)
.attr('width', '100%')
.attr('xmlns', 'http://www.w3.org/2000/svg')
.attr('xmlns:xlink', 'http://www.w3.org/1999/xlink')
.append('g');
appendDivSvgG(root, id, enclosingDivID, `font-family: ${fontFamily}`, XMLNS_XLINK_STD);
} else {
// No container was provided
// If there is an existing element with the id, we remove it
// this likely a previously rendered diagram
const existingSvg = document.getElementById(id);
if (existingSvg) {
existingSvg.remove();
}
// No svgContainingElement was provided
// Remove previous tpm element if it exists
let element;
if (cnf.securityLevel === 'sandbox') {
element = document.querySelector('#i' + id);
} else {
element = document.querySelector('#d' + id);
}
// If there is an existing element with the id, we remove it. This likely a previously rendered diagram
removeExistingElements(document, isSandboxed, id, iFrameID_selector, enclosingDivID_selector);
if (element) {
element.remove();
}
// Add the tmp div used for rendering with the id `d${id}`
// d+id it will contain a svg with the id "id"
if (cnf.securityLevel === 'sandbox') {
// IF we are in sandboxed mode, we do everything mermaid related
// in a sandboxed div
const iframe = select('body')
.append('iframe')
.attr('id', 'i' + id)
.attr('style', 'width: 100%; height: 100%;')
.attr('sandbox', '');
// Add the temporary div used for rendering with the enclosingDivID.
// This temporary div will contain a svg with the id == id
if (isSandboxed) {
// If we are in sandboxed mode, we do everything mermaid related in a (sandboxed) iFrame
const iframe = sandboxedIframe(select('body'), iFrameID);
root = select(iframe.nodes()[0]!.contentDocument!.body);
root.node().style.margin = 0;
} else {
root = select('body');
}
// This is the temporary div
root
.append('div')
.attr('id', 'd' + id)
// this is the seed of the svg to be rendered
.append('svg')
.attr('id', id)
.attr('width', '100%')
.attr('xmlns', 'http://www.w3.org/2000/svg')
.append('g');
appendDivSvgG(root, id, enclosingDivID);
}
text = encodeEntities(text);
// -------------------------------------------------------------------------------
// Create the diagram
// Important that we do not create the diagram until after the directives have been included
let diag;
let parseEncounteredException;
try {
// diag = new Diagram(text);
diag = await getDiagramFromText(text);
@ -228,75 +474,35 @@ const render = async function (
diag = new Diagram('error');
parseEncounteredException = error;
}
// Get the tmp element containing the the svg
const element = root.select('#d' + id).node();
// Get the temporary div element containing the svg
const element = root.select(enclosingDivID_selector).node();
const graphType = diag.type;
// insert inline style into svg
// -------------------------------------------------------------------------------
// Create and insert the styles (user styles, theme styles, config styles)
// Insert an element into svg. This is where we put the styles
const svg = element.firstChild;
const firstChild = svg.firstChild;
const diagramClassDefs = CLASSDEF_DIAGRAMS.includes(graphType)
? diag.renderer.getClasses(text, diag)
: {};
let userStyles = '';
// user provided theme CSS
// If you add more configuration driven data into the user styles make sure that the value is
// sanitized bye the sanitizeCSS function
if (cnf.themeCSS !== undefined) {
userStyles += `\n${cnf.themeCSS}`;
}
// user provided theme CSS
if (cnf.fontFamily !== undefined) {
userStyles += `\n:root { --mermaid-font-family: ${cnf.fontFamily}}`;
}
// user provided theme CSS
if (cnf.altFontFamily !== undefined) {
userStyles += `\n:root { --mermaid-alt-font-family: ${cnf.altFontFamily}}`;
}
// classDef
if (CLASSDEF_DIAGRAMS.includes(graphType)) {
const classes: any = diag.renderer.getClasses(text, diag);
const htmlLabels = cnf.htmlLabels || cnf.flowchart?.htmlLabels;
for (const className in classes) {
if (htmlLabels) {
userStyles += `\n.${className} > * { ${classes[className].styles.join(
' !important; '
)} !important; }`;
userStyles += `\n.${className} span { ${classes[className].styles.join(
' !important; '
)} !important; }`;
} else {
userStyles += `\n.${className} path { ${classes[className].styles.join(
' !important; '
)} !important; }`;
userStyles += `\n.${className} rect { ${classes[className].styles.join(
' !important; '
)} !important; }`;
userStyles += `\n.${className} polygon { ${classes[className].styles.join(
' !important; '
)} !important; }`;
userStyles += `\n.${className} ellipse { ${classes[className].styles.join(
' !important; '
)} !important; }`;
userStyles += `\n.${className} circle { ${classes[className].styles.join(
' !important; '
)} !important; }`;
if (classes[className].textStyles) {
userStyles += `\n.${className} tspan { ${classes[className].textStyles.join(
' !important; '
)} !important; }`;
}
}
}
}
const stylis = (selector: string, styles: string) =>
serialize(compile(`${selector}{${styles}}`), stringify);
const rules = stylis(`#${id}`, getStyles(graphType, userStyles, cnf.themeVariables));
const rules = createUserStyles(
config,
graphType,
// @ts-ignore convert renderer to TS.
diagramClassDefs,
idSelector
);
const style1 = document.createElement('style');
style1.innerHTML = `#${id} ` + rules;
style1.innerHTML = `${idSelector} ` + rules;
svg.insertBefore(style1, firstChild);
// -------------------------------------------------------------------------------
// Draw the diagram with the renderer
try {
await diag.renderer.draw(text, id, pkg.version, diag);
} catch (e) {
@ -304,45 +510,29 @@ const render = async function (
throw e;
}
root
.select(`[id="${id}"]`)
.selectAll('foreignobject > *')
.attr('xmlns', 'http://www.w3.org/1999/xhtml');
// -------------------------------------------------------------------------------
// Clean up SVG code
root.select(`[id="${id}"]`).selectAll('foreignobject > *').attr('xmlns', XMLNS_XHTML_STD);
// Fix for when the base tag is used
let svgCode = root.select('#d' + id).node().innerHTML;
let svgCode = root.select(enclosingDivID_selector).node().innerHTML;
log.debug('cnf.arrowMarkerAbsolute', cnf.arrowMarkerAbsolute);
if (!evaluate(cnf.arrowMarkerAbsolute) && cnf.securityLevel !== 'sandbox') {
svgCode = svgCode.replace(/marker-end="url\(.*?#/g, 'marker-end="url(#', 'g');
}
svgCode = decodeEntities(svgCode);
// Fix for when the br tag is used
svgCode = svgCode.replace(/<br>/g, '<br/>');
if (cnf.securityLevel === 'sandbox') {
const svgEl = root.select('#d' + id + ' svg').node();
const width = '100%';
let height = '100%';
if (svgEl) {
height = svgEl.viewBox.baseVal.height + 'px';
}
svgCode = `<iframe style="width:${width};height:${height};border:0;margin:0;" src="data:text/html;base64,${btoa(
'<body style="margin:0">' + svgCode + '</body>'
)}" sandbox="allow-top-navigation-by-user-activation allow-popups">
The iframe tag is not supported by your browser.
</iframe>`;
} else {
if (cnf.securityLevel !== 'loose') {
svgCode = DOMPurify.sanitize(svgCode, {
ADD_TAGS: ['foreignobject'],
ADD_ATTR: ['dominant-baseline'],
});
}
log.debug('config.arrowMarkerAbsolute', config.arrowMarkerAbsolute);
svgCode = cleanUpSvgCode(svgCode, isSandboxed, evaluate(config.arrowMarkerAbsolute));
if (isSandboxed) {
const svgEl = root.select(enclosingDivID_selector + ' svg').node();
svgCode = putIntoIFrame(svgCode, svgEl);
} else if (isLooseSecurityLevel) {
// Sanitize the svgCode using DOMPurify
svgCode = DOMPurify.sanitize(svgCode, {
ADD_TAGS: DOMPURE_TAGS,
ADD_ATTR: DOMPURE_ATTR,
});
}
// -------------------------------------------------------------------------------
// Do any callbacks (cb = callback)
if (typeof cb !== 'undefined') {
switch (graphType) {
case 'flowchart':
@ -364,7 +554,9 @@ const render = async function (
}
attachFunctions();
const tmpElementSelector = cnf.securityLevel === 'sandbox' ? '#i' + id : '#d' + id;
// -------------------------------------------------------------------------------
// Remove the temporary element if appropriate
const tmpElementSelector = isSandboxed ? iFrameID_selector : enclosingDivID_selector;
const node = select(tmpElementSelector).node();
if (node && 'remove' in node) {
node.remove();
@ -454,7 +646,9 @@ const handleDirective = function (p: any, directive: any, type: string): void {
}
};
/** @param options - Initial Mermaid options */
/**
* @param options - Initial Mermaid options
* */
async function initialize(options: MermaidConfig) {
// Handle legacy location of font-family configuration
if (options?.fontFamily) {

View File

@ -0,0 +1,126 @@
/**
* This is a mocked/stubbed version of the d3 Selection type. Each of the main functions are all
* mocked (via vi.fn()) so you can track if they have been called, etc.
*/
export class MockedD3 {
public attribs = new Map<string, string | null>();
public id: string | undefined = '';
_children: MockedD3[] = [];
constructor(givenId = 'mock-id') {
this.id = givenId;
}
/** Helpful utility during development/debugging. This is not a real d3 function */
public listChildren(): string {
return this._children
.map((child) => {
return child.id;
})
.join(', ');
}
select = vi.fn().mockImplementation(({ select_str = '' }): MockedD3 => {
// Get the id from an argument string. if it is of the form [id='some-id'], strip off the
// surrounding id[..]
const stripSurroundRegexp = /\[id='(.*)'\]/;
const matchedSurrounds = select_str.match(stripSurroundRegexp);
const cleanId = matchedSurrounds ? matchedSurrounds[1] : select_str;
return new MockedD3(cleanId);
});
append = vi
.fn()
.mockImplementation(function (this: MockedD3, type: string, id = '' + '-appended'): MockedD3 {
const newMock = new MockedD3(id);
newMock.attribs.set('type', type);
this._children.push(newMock);
return newMock;
});
// NOTE: The d3 implementation allows for a selector ('beforeSelector' arg below).
// With this mocked implementation, we assume it will always refer to an node id
// and will always be of the form "#[id of the node to insert before]".
// To keep this simple, any leading '#' is removed and the resulting string is the node id searched.
insert = (type: string, beforeSelector?: string, id = this.id + '-inserted'): MockedD3 => {
const newMock = new MockedD3(id);
newMock.attribs.set('type', type);
if (beforeSelector === undefined) {
this._children.push(newMock);
} else {
const idOnly = beforeSelector[0] == '#' ? beforeSelector.substring(1) : beforeSelector;
const foundIndex = this._children.findIndex((child) => child.id === idOnly);
if (foundIndex < 0) {
this._children.push(newMock);
} else {
this._children.splice(foundIndex, 0, newMock);
}
}
return newMock;
};
attr(attrName: string): null | undefined | string | number;
// attr(attrName: string, attrValue: string): MockedD3;
attr(attrName: string, attrValue?: string): null | undefined | string | number | MockedD3 {
if (arguments.length === 1) {
return this.attribs.get(attrName);
} else {
if (attrName === 'id') {
this.id = attrValue; // also set the id explicitly
}
if (attrValue !== undefined) {
this.attribs.set(attrName, attrValue);
}
return this;
}
}
public lower(attrValue = '') {
this.attribs.set('lower', attrValue);
return this;
}
public style(attrValue = '') {
this.attribs.set('style', attrValue);
return this;
}
public text(attrValue = '') {
this.attribs.set('text', attrValue);
return this;
}
// NOTE: Arbitrarily returns an empty object. The return value could be something different with a mockReturnValue() or mockImplementation()
public node = vi.fn().mockReturnValue({});
nodes = vi.fn().mockImplementation(function (this: MockedD3): MockedD3[] {
return this._children;
});
// This will try to use attrs that have been set.
getBBox = () => {
const x = this.attribs.has('x') ? this.attribs.get('x') : 20;
const y = this.attribs.has('y') ? this.attribs.get('y') : 30;
const width = this.attribs.has('width') ? this.attribs.get('width') : 140;
const height = this.attribs.has('height') ? this.attribs.get('height') : 250;
return {
x: x,
y: y,
width: width,
height: height,
};
};
// --------------------------------------------------------------------------------
// The following functions are here for completeness. They simply return a vi.fn()
insertBefore = vi.fn();
curveBasis = vi.fn();
curveBasisClosed = vi.fn();
curveBasisOpen = vi.fn();
curveLinear = vi.fn();
curveLinearClosed = vi.fn();
curveMonotoneX = vi.fn();
curveMonotoneY = vi.fn();
curveNatural = vi.fn();
curveStep = vi.fn();
curveStepAfter = vi.fn();
curveStepBefore = vi.fn();
}

View File

@ -2,7 +2,7 @@
"plugin": ["typedoc-plugin-markdown"],
"readme": "none",
"githubPages": false,
"gitRemote": "origin",
"sourceLinkTemplate": "https://github.com/mermaid-js/mermaid/blob/{gitRevision}/{path}#L{line}",
"gitRevision": "master",
"out": "src/docs/config/setup",
"entryPointStrategy": "expand",

File diff suppressed because it is too large Load Diff

24
scripts/fixCSpell.ts Normal file
View File

@ -0,0 +1,24 @@
/**
* Sorts all the `words` in the cSpell.json file.
*
* Run from the same folder as the `cSpell.json` file
* (i.e. the root of the Mermaid project).
*/
import { readFileSync, writeFileSync } from 'node:fs';
import prettier from 'prettier';
const filepath = './cSpell.json';
const cSpell: { words: string[] } = JSON.parse(readFileSync(filepath, 'utf8'));
cSpell.words = [...new Set(cSpell.words.map((word) => word.toLowerCase()))];
cSpell.words.sort((a, b) => a.localeCompare(b));
const prettierConfig = prettier.resolveConfig.sync(filepath) ?? {};
writeFileSync(
filepath,
prettier.format(JSON.stringify(cSpell), {
...prettierConfig,
filepath,
})
);