Merge branch 'develop' into bug/4439_graph_doesnt_load_when_img_fail_to_load

This commit is contained in:
Pierrick Wauquier
2023-07-11 17:10:19 +02:00
committed by GitHub
139 changed files with 10419 additions and 6105 deletions

View File

@@ -38,6 +38,10 @@ module.exports = {
'lodash',
'unicorn',
],
ignorePatterns: [
// this file is automatically generated by `pnpm run --filter mermaid types:build-config`
'packages/mermaid/src/config.type.ts',
],
rules: {
curly: 'error',
'no-console': 'error',
@@ -123,6 +127,14 @@ module.exports = {
files: ['*.{ts,tsx}'],
plugins: ['tsdoc'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'TSEnumDeclaration',
message:
'Prefer using TypeScript union types over TypeScript enum, since TypeScript enums have a bunch of issues, see https://dev.to/dvddpl/whats-the-problem-with-typescript-enums-2okj',
},
],
'tsdoc/syntax': 'error',
},
},

View File

@@ -53,8 +53,17 @@ body:
Please fill out the info below.
Note that you only need to fill out the relevant section
value: |-
- Mermaid version:
- Mermaid version:
- Browser and Version: [Chrome, Edge, Firefox]
- type: textarea
attributes:
label: Suggested Solutions
description: >
If applicable, suggest solutions that could resolve the bug.
It would help maintainers/contributors to not waste time looking for the solution. Even pointing the line causing the bug would be great!
placeholder: |-
- Variable `parser` in file <filepath> is not initialised ...
- Add a new type for ...
- type: textarea
attributes:
label: Additional Context

17
.github/codecov.yaml vendored Normal file
View File

@@ -0,0 +1,17 @@
codecov:
branch: develop
comment:
layout: 'reach, diff, flags, files'
behavior: default
require_changes: false # if true: only post the comment if coverage changes
require_base: no # [yes :: must have a base report to post]
require_head: yes # [yes :: must have a head report to post]
coverage:
status:
project:
off
# Turing off for now as code coverage isn't stable and causes unnecessary build failures.
# default:
# threshold: 2%

View File

@@ -1,3 +1,4 @@
'Type: Bug / Error': 'bug/*'
'Type: Enhancement': 'feature/*'
'Type: Other': 'other/*'
'Type: Bug / Error': ['bug/*', fix/*]
'Type: Enhancement': ['feature/*', 'feat/*']
'Type: Other': ['other/*', 'chore/*', 'test/*', 'refactor/*']
'Area: Documentation': ['docs/*']

View File

@@ -2,13 +2,13 @@ name: Build Vitepress docs
on:
pull_request:
merge_group:
permissions:
contents: read
jobs:
# Build job
build:
build-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout

View File

@@ -2,6 +2,7 @@ name: Build
on:
push: {}
merge_group:
pull_request:
types:
- opened
@@ -12,7 +13,7 @@ permissions:
contents: read
jobs:
build:
build-mermaid:
runs-on: ubuntu-latest
strategy:
matrix:

View File

@@ -14,7 +14,7 @@ permissions:
contents: read
jobs:
check:
check-readme:
runs-on: ubuntu-latest
steps:
- name: Checkout repository

View File

@@ -1,15 +1,16 @@
on:
push: {}
push:
merge_group:
pull_request:
types:
- opened
- synchronize
- ready_for_review
name: Static analysis
name: Static analysis on Test files
jobs:
test:
check-tests:
runs-on: ubuntu-latest
name: check tests
if: github.repository_owner == 'mermaid-js'

View File

@@ -19,7 +19,7 @@ env:
USE_APPLI: ${{ secrets.APPLITOOLS_API_KEY && 'true' || '' }}
jobs:
test:
e2e-applitools:
runs-on: ubuntu-latest
strategy:
matrix:

View File

@@ -1,12 +1,15 @@
name: E2E
on: [push, pull_request]
on:
push:
pull_request:
merge_group:
permissions:
contents: read
jobs:
build:
e2e:
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -33,7 +36,7 @@ jobs:
# Otherwise (e.g. if running from fork), we run on a single container only
if: ${{ ( env.CYPRESS_RECORD_KEY != '' ) || ( matrix.containers == 1 ) }}
with:
start: pnpm run dev
start: pnpm run dev:coverage
wait-on: 'http://localhost:9000'
# Disable recording if we don't have an API key
# e.g. if this action was run from a fork
@@ -41,7 +44,18 @@ jobs:
parallel: ${{ secrets.CYPRESS_RECORD_KEY != '' }}
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
VITEST_COVERAGE: true
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v3
# Run step only pushes to develop and pull_requests
if: ${{ steps.cypress.conclusion == 'success' && (github.event_name == 'pull_request' || github.ref == 'refs/heads/develop')}}
with:
files: coverage/cypress/lcov.info
flags: e2e
name: mermaid-codecov
fail_ci_if_error: false
verbose: true
token: 6845cc80-77ee-4e17-85a1-026cd95e0766
- name: Upload Artifacts
uses: actions/upload-artifact@v3
if: ${{ failure() && steps.cypress.conclusion == 'failure' }}

View File

@@ -1,7 +1,8 @@
name: Lint
on:
push: {}
push:
merge_group:
pull_request:
types:
- opened
@@ -52,6 +53,19 @@ jobs:
exit 1
fi
- name: Verify `./src/config.type.ts` is in sync with `./src/schemas/config.schema.yaml`
shell: bash
run: |
if ! pnpm run --filter mermaid types:verify-config; then
ERROR_MESSAGE='Running `pnpm run --filter mermaid types:verify-config` failed.'
ERROR_MESSAGE+=' This should be fixed by running'
ERROR_MESSAGE+=' `pnpm run --filter mermaid types:build-config`'
ERROR_MESSAGE+=' on your local machine.'
echo "::error title=Lint failure::${ERROR_MESSAGE}"
# make sure to return an error exitcode so that GitHub actions shows a red-cross
exit 1
fi
- name: Verify Docs
id: verifyDocs
working-directory: ./packages/mermaid

View File

@@ -1,11 +1,15 @@
name: Validate PR Labeler Configuration
on:
push: {}
push:
paths:
- .github/workflows/pr-labeler-config-validator.yml
- .github/workflows/pr-labeler.yml
- .github/pr-labeler.yml
pull_request:
types:
- opened
- synchronize
- ready_for_review
paths:
- .github/workflows/pr-labeler-config-validator.yml
- .github/workflows/pr-labeler.yml
- .github/pr-labeler.yml
jobs:
pr-labeler:

View File

@@ -19,7 +19,7 @@ concurrency:
jobs:
# Build job
build:
build-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -48,11 +48,11 @@ jobs:
path: packages/mermaid/src/vitepress/.vitepress/dist
# Deployment job
deploy:
deploy-docs:
environment:
name: github-pages
runs-on: ubuntu-latest
needs: build
needs: build-docs
steps:
- name: Deploy to GitHub Pages
id: deployment

View File

@@ -6,7 +6,7 @@ on:
- 'release/**'
jobs:
publish:
publish-preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

View File

@@ -1,12 +1,12 @@
name: Unit Tests
on: [push, pull_request]
on: [push, pull_request, merge_group]
permissions:
contents: read
jobs:
build:
unit-test:
runs-on: ubuntu-latest
strategy:
matrix:
@@ -31,7 +31,7 @@ jobs:
- name: Run Unit Tests
run: |
pnpm run ci --coverage
pnpm test:coverage
- name: Run ganttDb tests using California timezone
env:
@@ -39,11 +39,16 @@ jobs:
# since some days have 25 hours instead of 24.
TZ: America/Los_Angeles
run: |
pnpm exec vitest run ./packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts
pnpm exec vitest run ./packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts --coverage
# Coveralls is throwing 500. Disabled for now.
# - name: Upload Coverage to Coveralls
# uses: coverallsapp/github-action@v2
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
# flag-name: unit
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v3
# Run step only pushes to develop and pull_requests
if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/develop' }}
with:
files: ./coverage/vitest/lcov.info
flags: unit
name: mermaid-codecov
fail_ci_if_error: false
verbose: true
token: 6845cc80-77ee-4e17-85a1-026cd95e0766

View File

@@ -5,7 +5,7 @@ on:
workflow_dispatch:
jobs:
build:
update-browser-list:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

1
.gitignore vendored
View File

@@ -42,3 +42,4 @@ stats/
**/user-avatars/*
**/contributor-names.json
.pnpm-store
.nyc_output

View File

@@ -5,4 +5,8 @@ coverage
# Autogenerated by PNPM
pnpm-lock.yaml
stats
packages/mermaid/src/docs/.vitepress/components.d.ts
**/.vitepress/components.d.ts
**/.vitepress/cache
.nyc_output
# Autogenerated by `pnpm run --filter mermaid types:build-config`
packages/mermaid/src/config.type.ts

View File

@@ -2,14 +2,17 @@ import { build, InlineConfig, type PluginOption } from 'vite';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import jisonPlugin from './jisonPlugin.js';
import jsonSchemaPlugin from './jsonSchemaPlugin.js';
import { readFileSync } from 'fs';
import typescript from '@rollup/plugin-typescript';
import { visualizer } from 'rollup-plugin-visualizer';
import type { TemplateType } from 'rollup-plugin-visualizer/dist/plugin/template-types.js';
import istanbul from 'vite-plugin-istanbul';
const visualize = process.argv.includes('--visualize');
const watch = process.argv.includes('--watch');
const mermaidOnly = process.argv.includes('--mermaid');
const coverage = process.env.VITE_COVERAGE === 'true';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const sourcemap = false;
@@ -119,8 +122,15 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions)
},
plugins: [
jisonPlugin(),
jsonSchemaPlugin(), // handles `.schema.yaml` files
// @ts-expect-error According to the type definitions, rollup plugins are incompatible with vite
typescript({ compilerOptions: { declaration: false } }),
istanbul({
exclude: ['node_modules', 'test/', '__mocks__'],
extension: ['.js', '.ts'],
requireEnv: true,
forceBuildInstrument: coverage,
}),
...visualizerOptions(packageName, core),
],
};

150
.vite/jsonSchemaPlugin.ts Normal file
View File

@@ -0,0 +1,150 @@
import { load, JSON_SCHEMA } from 'js-yaml';
import assert from 'node:assert';
import Ajv2019, { type JSONSchemaType } from 'ajv/dist/2019.js';
import { PluginOption } from 'vite';
import type { MermaidConfig, BaseDiagramConfig } from '../packages/mermaid/src/config.type.js';
/**
* All of the keys in the mermaid config that have a mermaid diagram config.
*/
const MERMAID_CONFIG_DIAGRAM_KEYS = [
'flowchart',
'sequence',
'gantt',
'journey',
'class',
'state',
'er',
'pie',
'quadrantChart',
'requirement',
'mindmap',
'timeline',
'gitGraph',
'c4',
'sankey',
] as const;
/**
* Generate default values from the JSON Schema.
*
* AJV does not support nested default values yet (or default values with $ref),
* so we need to manually find them (this may be fixed in ajv v9).
*
* @param mermaidConfigSchema - The Mermaid JSON Schema to use.
* @returns The default mermaid config object.
*/
function generateDefaults(mermaidConfigSchema: JSONSchemaType<MermaidConfig>) {
const ajv = new Ajv2019({
useDefaults: true,
allowUnionTypes: true,
strict: true,
});
ajv.addKeyword({
keyword: 'meta:enum', // used by jsonschema2md
errors: false,
});
ajv.addKeyword({
keyword: 'tsType', // used by json-schema-to-typescript
errors: false,
});
// ajv currently doesn't support nested default values, see https://github.com/ajv-validator/ajv/issues/1718
// (may be fixed in v9) so we need to manually use sub-schemas
const mermaidDefaultConfig = {};
assert.ok(mermaidConfigSchema.$defs);
const baseDiagramConfig = mermaidConfigSchema.$defs.BaseDiagramConfig;
for (const key of MERMAID_CONFIG_DIAGRAM_KEYS) {
const subSchemaRef = mermaidConfigSchema.properties[key].$ref;
const [root, defs, defName] = subSchemaRef.split('/');
assert.strictEqual(root, '#');
assert.strictEqual(defs, '$defs');
const subSchema = {
$schema: mermaidConfigSchema.$schema,
$defs: mermaidConfigSchema.$defs,
...mermaidConfigSchema.$defs[defName],
} as JSONSchemaType<BaseDiagramConfig>;
const validate = ajv.compile(subSchema);
mermaidDefaultConfig[key] = {};
for (const required of subSchema.required ?? []) {
if (subSchema.properties[required] === undefined && baseDiagramConfig.properties[required]) {
mermaidDefaultConfig[key][required] = baseDiagramConfig.properties[required].default;
}
}
if (!validate(mermaidDefaultConfig[key])) {
throw new Error(
`schema for subconfig ${key} does not have valid defaults! Errors were ${JSON.stringify(
validate.errors,
undefined,
2
)}`
);
}
}
const validate = ajv.compile(mermaidConfigSchema);
if (!validate(mermaidDefaultConfig)) {
throw new Error(
`Mermaid config JSON Schema does not have valid defaults! Errors were ${JSON.stringify(
validate.errors,
undefined,
2
)}`
);
}
return mermaidDefaultConfig;
}
/**
* Vite plugin that handles JSON Schemas saved as a `.schema.yaml` file.
*
* Use `my-example.schema.yaml?only-defaults=true` to only load the default values.
*/
export default function jsonSchemaPlugin(): PluginOption {
return {
name: 'json-schema-plugin',
transform(src: string, id: string) {
const idAsUrl = new URL(id, 'file:///');
if (!idAsUrl.pathname.endsWith('schema.yaml')) {
return;
}
if (idAsUrl.searchParams.get('only-defaults')) {
const jsonSchema = load(src, {
filename: idAsUrl.pathname,
// only allow JSON types in our YAML doc (will probably be default in YAML 1.3)
// e.g. `true` will be parsed a boolean `true`, `True` will be parsed as string `"True"`.
schema: JSON_SCHEMA,
}) as JSONSchemaType<MermaidConfig>;
return {
code: `export default ${JSON.stringify(generateDefaults(jsonSchema), undefined, 2)};`,
map: null, // no source map
};
} else {
return {
code: `export default ${JSON.stringify(
load(src, {
filename: idAsUrl.pathname,
// only allow JSON types in our YAML doc (will probably be default in YAML 1.3)
// e.g. `true` will be parsed a boolean `true`, `True` will be parsed as string `"True"`.
schema: JSON_SCHEMA,
}),
undefined,
2
)};`,
map: null, // provide source map if available
};
}
},
};
}

16
CITATION.cff Normal file
View File

@@ -0,0 +1,16 @@
cff-version: 1.2.0
title: 'Mermaid: Generate diagrams from markdown-like text'
message: >-
If you use this software, please cite it using the metadata from this file.
type: software
authors:
- family-names: Sveidqvist
given-names: Knut
- name: 'Contributors to Mermaid'
repository-code: 'https://github.com/mermaid-js/mermaid'
date-released: 2014-12-02
url: 'https://mermaid.js.org/'
abstract: >-
JavaScript based diagramming and charting tool that renders Markdown-inspired
text definitions to create and modify diagrams dynamically.
license: MIT

View File

@@ -27,7 +27,7 @@ Generate diagrams from markdown-like text.
[![NPM](https://img.shields.io/npm/v/mermaid)](https://www.npmjs.com/package/mermaid)
[![Build CI Status](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml/badge.svg)](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml)
[![npm minified gzipped bundle size](https://img.shields.io/bundlephobia/minzip/mermaid)](https://bundlephobia.com/package/mermaid)
[![Coverage Status](https://coveralls.io/repos/github/mermaid-js/mermaid/badge.svg?branch=master)](https://coveralls.io/github/mermaid-js/mermaid?branch=master)
[![Coverage Status](https://codecov.io/github/mermaid-js/mermaid/branch/develop/graph/badge.svg)](https://app.codecov.io/github/mermaid-js/mermaid/tree/develop)
[![CDN Status](https://img.shields.io/jsdelivr/npm/hm/mermaid)](https://www.jsdelivr.com/package/npm/mermaid)
[![NPM Downloads](https://img.shields.io/npm/dm/mermaid)](https://www.npmjs.com/package/mermaid)
[![Join our Slack!](https://img.shields.io/static/v1?message=join%20chat&color=9cf&logo=slack&label=slack)](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE)
@@ -386,7 +386,7 @@ Update version number in `package.json`.
npm publish
```
The above command generates files into the `dist` folder and publishes them to npmjs.org.
The above command generates files into the `dist` folder and publishes them to <https://www.npmjs.com>.
## Related projects
@@ -402,7 +402,7 @@ Detailed information about how to contribute can be found in the [contribution g
## Security and safe diagrams
For public sites, it can be precarious to retrieve text from users on the internet, storing that content for presentation in a browser at a later stage. The reason is that the user content can contain embedded malicious scripts that will run when the data is presented. For Mermaid this is a risk, specially as mermaid diagrams contain many characters that are used in html which makes the standard sanitation unusable as it also breaks the diagrams. We still make an effort to sanitise the incoming code and keep refining the process but it is hard to guarantee that there are no loop holes.
For public sites, it can be precarious to retrieve text from users on the internet, storing that content for presentation in a browser at a later stage. The reason is that the user content can contain embedded malicious scripts that will run when the data is presented. For Mermaid this is a risk, specially as mermaid diagrams contain many characters that are used in html which makes the standard sanitation unusable as it also breaks the diagrams. We still make an effort to sanitize the incoming code and keep refining the process but it is hard to guarantee that there are no loop holes.
As an extra level of security for sites with external users we are happy to introduce a new security level in which the diagram is rendered in a sandboxed iframe preventing javascript in the code from being executed. This is a great step forward for better security.
@@ -410,7 +410,7 @@ _Unfortunately you can not have a cake and eat it at the same time which in this
## Reporting vulnerabilities
To report a vulnerability, please e-mail security@mermaid.live with a description of the issue, the steps you took to create the issue, affected versions, and if known, mitigations for the issue.
To report a vulnerability, please e-mail <security@mermaid.live> with a description of the issue, the steps you took to create the issue, affected versions, and if known, mitigations for the issue.
## Appreciation

View File

@@ -27,7 +27,7 @@ Mermaid
[![NPM](https://img.shields.io/npm/v/mermaid)](https://www.npmjs.com/package/mermaid)
[![Build CI Status](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml/badge.svg)](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml)
[![npm minified gzipped bundle size](https://img.shields.io/bundlephobia/minzip/mermaid)](https://bundlephobia.com/package/mermaid)
[![Coverage Status](https://coveralls.io/repos/github/mermaid-js/mermaid/badge.svg?branch=master)](https://coveralls.io/github/mermaid-js/mermaid?branch=master)
[![Coverage Status](https://codecov.io/github/mermaid-js/mermaid/branch/develop/graph/badge.svg)](https://app.codecov.io/github/mermaid-js/mermaid/tree/develop)
[![CDN Status](https://img.shields.io/jsdelivr/npm/hm/mermaid)](https://www.jsdelivr.com/package/npm/mermaid)
[![NPM Downloads](https://img.shields.io/npm/dm/mermaid)](https://www.npmjs.com/package/mermaid)
[![Join our Slack!](https://img.shields.io/static/v1?message=join%20chat&color=9cf&logo=slack&label=slack)](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE)
@@ -322,7 +322,7 @@ Rel(SystemC, customerA, "Sends e-mails to")
npm publish
```
以上的命令会将文件打包到 `dist` 目录并发布至 npmjs.org.
以上的命令会将文件打包到 `dist` 目录并发布至 <https://www.npmjs.com>.
## 相关项目

View File

@@ -0,0 +1,13 @@
/**
* Mocked Sankey diagram renderer
*/
import { vi } from 'vitest';
export const draw = vi.fn().mockImplementation(() => {
return '';
});
export default {
draw,
};

View File

@@ -40,8 +40,10 @@
"dompurify",
"edgechromium",
"elkjs",
"elle",
"faber",
"flatmap",
"foswiki",
"ftplugin",
"gantt",
"gitea",
@@ -51,6 +53,7 @@
"graphviz",
"grav",
"greywolf",
"gzipped",
"huynh",
"huynhicode",
"inkdrop",
@@ -84,7 +87,10 @@
"mkdocs",
"mmorel",
"mult",
"neurodiverse",
"nextra",
"nikolay",
"nirname",
"orlandoni",
"pathe",
"pbrolin",
@@ -98,10 +104,13 @@
"ranksep",
"rect",
"rects",
"reda",
"redmine",
"rehype",
"roledescription",
"rozhkov",
"sandboxed",
"sankey",
"setupgraphviewbox",
"shiki",
"sidharth",
@@ -116,6 +125,7 @@
"stylis",
"subhash-halder",
"substate",
"sulais",
"sveidqvist",
"swimm",
"techn",
@@ -139,6 +149,7 @@
"vueuse",
"xlink",
"yash",
"yokozuna",
"zenuml"
],
"patterns": [

View File

@@ -2,12 +2,14 @@
const { defineConfig } = require('cypress');
const { addMatchImageSnapshotPlugin } = require('cypress-image-snapshot/plugin');
const coverage = require('@cypress/code-coverage/task');
module.exports = defineConfig({
projectId: 'n2sma2',
e2e: {
specPattern: 'cypress/integration/**/*.{js,jsx,ts,tsx}',
setupNodeEvents(on, config) {
coverage(on, config);
addMatchImageSnapshotPlugin(on, config);
// copy any needed variables from process.env to config.env
config.env.useAppli = process.env.USE_APPLI ? true : false;

View File

@@ -172,7 +172,7 @@ describe('Flowchart v2', () => {
);
});
it('52: handle nested subgraphs in several levels', () => {
it('52: handle nested subgraphs in several levels.', () => {
imgSnapshotTest(
`flowchart TB
b-->B

View File

@@ -0,0 +1,144 @@
import { imgSnapshotTest, renderGraph } from '../../helpers/util.js';
describe('Sankey Diagram', () => {
it('should render a simple example', () => {
imgSnapshotTest(
`
sankey-beta
sourceNode,targetNode,10
`,
{}
);
});
describe('when given a linkColor', function () {
this.beforeAll(() => {
cy.wrap(
`sankey-beta
a,b,10
`
).as('graph');
});
it('links should use hex color', function () {
renderGraph(this.graph, { sankey: { linkColor: '#636465' } });
cy.get('.link path').should((link) => {
expect(link.attr('stroke')).to.equal('#636465');
});
});
it('links should be the same color as source node', function () {
renderGraph(this.graph, { sankey: { linkColor: 'source' } });
cy.get('.link path').then((link) => {
cy.get('.node[id="node-1"] rect').should((node) =>
expect(link.attr('stroke')).to.equal(node.attr('fill'))
);
});
});
it('links should be the same color as target node', function () {
renderGraph(this.graph, { sankey: { linkColor: 'target' } });
cy.get('.link path').then((link) => {
cy.get('.node[id="node-2"] rect').should((node) =>
expect(link.attr('stroke')).to.equal(node.attr('fill'))
);
});
});
it('links must be gradient', function () {
renderGraph(this.graph, { sankey: { linkColor: 'gradient' } });
cy.get('.link path').should((link) => {
expect(link.attr('stroke')).to.equal('url(#linearGradient-3)');
});
});
});
describe('when given a nodeAlignment', function () {
this.beforeAll(() => {
cy.wrap(
`
sankey-beta
a,b,8
b,c,8
c,d,8
d,e,8
x,c,4
c,y,4
`
).as('graph');
});
this.afterEach(() => {
cy.get('.node[id="node-1"]').should((node) => {
expect(node.attr('x')).to.equal('0');
});
cy.get('.node[id="node-2"]').should((node) => {
expect(node.attr('x')).to.equal('100');
});
cy.get('.node[id="node-3"]').should((node) => {
expect(node.attr('x')).to.equal('200');
});
cy.get('.node[id="node-4"]').should((node) => {
expect(node.attr('x')).to.equal('300');
});
cy.get('.node[id="node-5"]').should((node) => {
expect(node.attr('x')).to.equal('400');
});
});
it('should justify nodes', function () {
renderGraph(this.graph, {
sankey: { nodeAlignment: 'justify', width: 410, useMaxWidth: false },
});
cy.get('.node[id="node-6"]').should((node) => {
expect(node.attr('x')).to.equal('0');
});
cy.get('.node[id="node-7"]').should((node) => {
expect(node.attr('x')).to.equal('400');
});
});
it('should align nodes left', function () {
renderGraph(this.graph, {
sankey: { nodeAlignment: 'left', width: 410, useMaxWidth: false },
});
cy.get('.node[id="node-6"]').should((node) => {
expect(node.attr('x')).to.equal('0');
});
cy.get('.node[id="node-7"]').should((node) => {
expect(node.attr('x')).to.equal('300');
});
});
it('should align nodes right', function () {
renderGraph(this.graph, {
sankey: { nodeAlignment: 'right', width: 410, useMaxWidth: false },
});
cy.get('.node[id="node-6"]').should((node) => {
expect(node.attr('x')).to.equal('100');
});
cy.get('.node[id="node-7"]').should((node) => {
expect(node.attr('x')).to.equal('400');
});
});
it('should center nodes', function () {
renderGraph(this.graph, {
sankey: { nodeAlignment: 'center', width: 410, useMaxWidth: false },
});
cy.get('.node[id="node-6"]').should((node) => {
expect(node.attr('x')).to.equal('100');
});
cy.get('.node[id="node-7"]').should((node) => {
expect(node.attr('x')).to.equal('300');
});
});
});
});

View File

@@ -156,6 +156,81 @@ context('Sequence diagram', () => {
`
);
});
it('should render a sequence diagram with basic actor creation and destruction', () => {
imgSnapshotTest(
`
sequenceDiagram
Alice ->> Bob: Hello Bob, how are you ?
Bob ->> Alice: Fine, thank you. And you?
create participant Polo
Alice ->> Polo: Hi Polo!
create actor Ola1 as Ola
Polo ->> Ola1: Hiii
Ola1 ->> Alice: Hi too
destroy Ola1
Alice --x Ola1: Bye!
Alice ->> Bob: And now?
create participant Ola2 as Ola
Alice ->> Ola2: Hello again
destroy Alice
Alice --x Ola2: Bye for me!
destroy Bob
Ola2 --> Bob: The end
`
);
});
it('should render a sequence diagram with actor creation and destruction coupled with backgrounds, loops and notes', () => {
imgSnapshotTest(
`
sequenceDiagram
accTitle: test the accTitle
accDescr: Test a description
participant Alice
participant Bob
autonumber 10 10
rect rgb(200, 220, 100)
rect rgb(200, 255, 200)
Alice ->> Bob: Hello Bob, how are you?
create participant John as John<br />Second Line
Bob-->>John: How about you John?
end
Bob--x Alice: I am good thanks!
Bob-x John: I am good thanks!
Note right of John: John thinks a long<br />long time, so long<br />that the text does<br />not fit on a row.
Bob-->Alice: Checking with John...
Note over John:wrap: John looks like he's still thinking, so Bob prods him a bit.
Bob-x John: Hey John - we're still waiting to know<br />how you're doing
Note over John:nowrap: John's trying hard not to break his train of thought.
destroy John
Bob-x John: John! Cmon!
Note over John: After a few more moments, John<br />finally snaps out of it.
end
autonumber off
alt either this
create actor Lola
Alice->>+Lola: Yes
Lola-->>-Alice: OK
else or this
autonumber
Alice->>Lola: No
else or this will happen
Alice->Lola: Maybe
end
autonumber 200
par this happens in parallel
destroy Bob
Alice -->> Bob: Parallel message 1
and
Alice -->> Lola: Parallel message 2
end
`
);
});
context('font settings', () => {
it('should render different note fonts when configured', () => {
imgSnapshotTest(

View File

@@ -13,8 +13,8 @@
// https://on.cypress.io/configuration
// ***********************************************************
import '@cypress/code-coverage/support';
import '@applitools/eyes-cypress/commands';
// Import commands.js using ES2015 syntax:
import './commands';

View File

@@ -154,6 +154,29 @@
</pre>
<hr />
<pre class="mermaid">
classDiagram
A1 --> B1
namespace A {
class A1 {
+foo : string
}
class A2 {
+bar : int
}
}
namespace B {
class B1 {
+foo : bool
}
class B2 {
+bar : float
}
}
A2 --> B2
</pre>
<hr />
<script type="module">
import mermaid from './mermaid.esm.mjs';
mermaid.initialize({

View File

@@ -1505,6 +1505,34 @@
</pre>
<hr />
<pre class="mermaid">
graph TD
A([Start]) ==> B[Step 1]
B ==> C{Flow 1}
C -- Choice 1.1 --> D[Step 2.1]
C -- Choice 1.3 --> I[Step 2.3]
C == Choice 1.2 ==> E[Step 2.2]
D --> F{Flow 2}
E ==> F{Flow 2}
F{Flow 2} == Choice 2.1 ==> H[Feedback node]
H[Feedback node] ==> B[Step 1]
F{Flow 2} == Choice 2.2 ==> G((Finish))
linkStyle 0,1,4,6,7,8,9 stroke:gold, stroke-width:4px
classDef active_node fill:#0CF,stroke:#09F,stroke-width:6px
classDef unactive_node fill:#e0e0e0,stroke:#bdbdbd,stroke-width:3px
classDef bugged_node fill:#F88,stroke:#F22,stroke-width:3px
classDef start_node,finish_node fill:#3B1,stroke:#391,stroke-width:8px
class A start_node;
class B,C,E,F,H active_node;
class D unactive_node;
class G finish_node;
class I bugged_node
</pre>
<hr />
<h1 id="link-clicked">Anchor for "link-clicked" test</h1>
<script type="module">

View File

@@ -75,6 +75,9 @@
<li>
<h2><a href="./zenuml.html">ZenUML</a></h2>
</li>
<li>
<h2><a href="./sankey.html">Sankey</a></h2>
</li>
</ul>
</body>
</html>

108
demos/sankey.html Normal file
View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>States Mermaid Quick Test Page</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
<style>
div.mermaid {
/* font-family: 'trebuchet ms', verdana, arial; */
font-family: 'Courier New', Courier, monospace !important;
}
</style>
</head>
<body>
<h1>Sankey diagram demos</h1>
<h2>Energy flow</h2>
<pre class="mermaid">
sankey-beta
Agricultural 'waste',Bio-conversion,124.729
Bio-conversion,Liquid,0.597
Bio-conversion,Losses,26.862
Bio-conversion,Solid,280.322
Bio-conversion,Gas,81.144
Biofuel imports,Liquid,35
Biomass imports,Solid,35
Coal imports,Coal,11.606
Coal reserves,Coal,63.965
Coal,Solid,75.571
District heating,Industry,10.639
District heating,Heating and cooling - commercial,22.505
District heating,Heating and cooling - homes,46.184
Electricity grid,Over generation / exports,104.453
Electricity grid,Heating and cooling - homes,113.726
Electricity grid,H2 conversion,27.14
Electricity grid,Industry,342.165
Electricity grid,Road transport,37.797
Electricity grid,Agriculture,4.412
Electricity grid,Heating and cooling - commercial,40.858
Electricity grid,Losses,56.691
Electricity grid,Rail transport,7.863
Electricity grid,Lighting & appliances - commercial,90.008
Electricity grid,Lighting & appliances - homes,93.494
Gas imports,Ngas,40.719
Gas reserves,Ngas,82.233
Gas,Heating and cooling - commercial,0.129
Gas,Losses,1.401
Gas,Thermal generation,151.891
Gas,Agriculture,2.096
Gas,Industry,48.58
Geothermal,Electricity grid,7.013
H2 conversion,H2,20.897
H2 conversion,Losses,6.242
H2,Road transport,20.897
Hydro,Electricity grid,6.995
Liquid,Industry,121.066
Liquid,International shipping,128.69
Liquid,Road transport,135.835
Liquid,Domestic aviation,14.458
Liquid,International aviation,206.267
Liquid,Agriculture,3.64
Liquid,National navigation,33.218
Liquid,Rail transport,4.413
Marine algae,Bio-conversion,4.375
Ngas,Gas,122.952
Nuclear,Thermal generation,839.978
Oil imports,Oil,504.287
Oil reserves,Oil,107.703
Oil,Liquid,611.99
Other waste,Solid,56.587
Other waste,Bio-conversion,77.81
Pumped heat,Heating and cooling - homes,193.026
Pumped heat,Heating and cooling - commercial,70.672
Solar PV,Electricity grid,59.901
Solar Thermal,Heating and cooling - homes,19.263
Solar,Solar Thermal,19.263
Solar,Solar PV,59.901
Solid,Agriculture,0.882
Solid,Thermal generation,400.12
Solid,Industry,46.477
Thermal generation,Electricity grid,525.531
Thermal generation,Losses,787.129
Thermal generation,District heating,79.329
Tidal,Electricity grid,9.452
UK land based bioenergy,Bio-conversion,182.01
Wave,Electricity grid,19.013
Wind,Electricity grid,289.366
</pre>
<script type="module">
import mermaid from './mermaid.esm.mjs';
mermaid.initialize({
theme: 'default',
logLevel: 3,
securityLevel: 'loose',
sankey: {
title: 'Hey, this is Sankey-Beta',
width: 1200,
height: 600,
linkColor: 'gradient',
nodeAlignment: 'justify',
},
});
</script>
</body>
</html>

View File

@@ -1,9 +1,36 @@
version: '3.9'
services:
mermaid:
image: node:18.16.0-alpine3.18
image: node:18.16.1-alpine3.18
stdin_open: true
tty: true
working_dir: /mermaid
mem_limit: '2G'
environment:
- NODE_OPTIONS=--max_old_space_size=2048
volumes:
- ./:/mermaid
- root_cache:/root/.cache
- root_local:/root/.local
- root_npm:/root/.npm
ports:
- 9000:9000
- 3333:3333
cypress:
image: cypress/included:12.17.0
stdin_open: true
tty: true
working_dir: /mermaid
mem_limit: '2G'
entrypoint: cypress
environment:
- DISPLAY
volumes:
- ./:/mermaid
- /tmp/.X11-unix:/tmp/.X11-unix
network_mode: host
volumes:
root_cache:
root_local:
root_npm:

View File

@@ -26,6 +26,10 @@ The definitions that can be generated the Live-Editor are also backwards-compati
[Eddie Jaoude: Can you code your diagrams?](https://www.youtube.com/watch?v=9HZzKkAqrX8)
## Mermaid with OpenAI
[Elle Neal: Mind Mapping with AI: An Accessible Approach for Neurodiverse Learners Tutorial:](https://medium.com/@elle.neal_71064/mind-mapping-with-ai-an-accessible-approach-for-neurodiverse-learners-1a74767359ff), [Demo:](https://databutton.com/v/jk9vrghc)
## Mermaid with HTML
Examples are provided in [Getting Started](../intro/n00b-gettingStarted.md)

View File

@@ -14,7 +14,7 @@
#### Defined in
[defaultConfig.ts:2293](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L2293)
[defaultConfig.ts:266](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L266)
---
@@ -22,35 +22,12 @@
`Const` **default**: `Partial`<`MermaidConfig`>
**Configuration methods in Mermaid version 8.6.0 have been updated, to learn more\[[click
here](8.6.0_docs.md)].**
Default mermaid configuration options.
## **What follows are config instructions for older versions**
These are the default options which can be overridden with the initialization call like so:
**Example 1:**
```js
mermaid.initialize({ flowchart: { htmlLabels: false } });
```
**Example 2:**
```html
<script>
const config = {
startOnLoad: true,
flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'cardinal' },
securityLevel: 'loose',
};
mermaid.initialize(config);
</script>
```
A summary of all options and their defaults is found [here](#mermaidapi-configuration-defaults).
A description of each option follows below.
Please see the Mermaid config JSON Schema for the default JSON values.
Non-JSON JS default values are listed in this file, e.g. functions, or
`undefined` (explicitly set so that `configKeys` finds them).
#### Defined in
[defaultConfig.ts:33](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L33)
[defaultConfig.ts:16](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L16)

View File

@@ -96,7 +96,7 @@ mermaid.initialize(config);
#### Defined in
[mermaidAPI.ts:663](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L663)
[mermaidAPI.ts:667](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L667)
## Functions

View File

@@ -73,9 +73,9 @@ To make a custom theme, modify `themeVariables` via `init`.
You will need to use the [base](#available-themes) theme as it is the only modifiable theme.
| Parameter | Description | Type | Properties |
| -------------- | ------------------------------------ | ------ | --------------------------------------------------------------------------------------------------- |
| themeVariables | Modifiable with the `init` directive | Object | `primaryColor`, `primaryTextColor`, `lineColor` ([see full list](#theme-variables-reference-table)) |
| Parameter | Description | Type | Properties |
| -------------- | ------------------------------------ | ------ | ----------------------------------------------------------------------------------- |
| themeVariables | Modifiable with the `init` directive | Object | `primaryColor`, `primaryTextColor`, `lineColor` ([see full list](#theme-variables)) |
Example of modifying `themeVariables` using the `init` directive:

View File

@@ -66,7 +66,7 @@ They also serve as proof of concept, for the variety of things that can be built
## Blogs
- [Wordpress](https://wordpress.org)
- [WordPress](https://wordpress.org)
- [WordPress Markdown Editor](https://wordpress.org/plugins/wp-githuber-md)
- [WP-ReliableMD](https://wordpress.org/plugins/wp-reliablemd/)
- [Hexo](https://hexo.io)
@@ -84,7 +84,7 @@ They also serve as proof of concept, for the variety of things that can be built
- [Plugin for Mermaid.js](https://github.com/eFrane/vuepress-plugin-mermaidjs)
- [Grav CMS](https://getgrav.org/)
- [Mermaid Diagrams](https://github.com/DanielFlaum/grav-plugin-mermaid-diagrams)
- [Gitlab Markdown Adapter](https://github.com/Goutte/grav-plugin-gitlab-markdown-adapter)
- [GitLab Markdown Adapter](https://github.com/Goutte/grav-plugin-gitlab-markdown-adapter)
## Communication
@@ -104,7 +104,7 @@ They also serve as proof of concept, for the variety of things that can be built
- [Flex Diagrams Extension](https://www.mediawiki.org/wiki/Extension:Flex_Diagrams)
- [Semantic Media Wiki](https://semantic-mediawiki.org)
- [Mermaid Plugin](https://github.com/SemanticMediaWiki/Mermaid)
- [FosWiki](https://foswiki.org)
- [Foswiki](https://foswiki.org)
- [Mermaid Plugin](https://foswiki.org/Extensions/MermaidPlugin)
- [DokuWiki](https://dokuwiki.org)
- [Mermaid Plugin](https://www.dokuwiki.org/plugin:mermaid)
@@ -161,6 +161,8 @@ They also serve as proof of concept, for the variety of things that can be built
- [Nano Mermaid](https://github.com/Yash-Singh1/nano-mermaid)
- [CKEditor](https://github.com/ckeditor/ckeditor5)
- [CKEditor 5 Mermaid plugin](https://github.com/ckeditor/ckeditor5-mermaid)
- [Standard Notes](https://standardnotes.com/)
- [sn-mermaid](https://github.com/nienow/sn-mermaid)
## Document Generation
@@ -172,7 +174,7 @@ They also serve as proof of concept, for the variety of things that can be built
- [rehype-mermaidjs](https://github.com/remcohaszing/rehype-mermaidjs)
- [Gatsby](https://www.gatsbyjs.com/)
- [gatsby-remark-mermaid](https://github.com/remcohaszing/gatsby-remark-mermaid)
- [jSDoc](https://jsdoc.app/)
- [JSDoc](https://jsdoc.app/)
- [jsdoc-mermaid](https://github.com/Jellyvision/jsdoc-mermaid)
- [MkDocs](https://www.mkdocs.org)
- [mkdocs-mermaid2-plugin](https://github.com/fralau/mkdocs-mermaid2-plugin)

View File

@@ -6,8 +6,8 @@
# Announcements
## [subhash-halder contributed quadrant charts so you can show your manager what to select - just like the strategy consultants at BCG do](https://www.mermaidchart.com/blog/posts/subhash-halder-contributed-quadrant-charts-so-you-can-show-your-manager-what-to-select-just-like-the-strategy-consultants-at-bcg-do/)
## [Mermaid Charts ChatGPT Plugin Combines Generative AI and Smart Diagramming For Users](https://www.mermaidchart.com/blog/posts/mermaid-chart-chatgpt-plugin-combines-generative-ai-and-smart-diagramming)
8 June 2023 · 7 mins
29 June 2023 · 4 mins
A quadrant chart is a useful diagram that helps users visualize data and identify patterns in a data set.
Mermaid Charts new ChatGPT plugin integrates AI-powered text prompts with Mermaids intuitive diagramming editor, enabling users to generate, edit, and share complex diagrams with ease and efficiency.

View File

@@ -6,6 +6,18 @@
# Blog
## [Mermaid Charts ChatGPT Plugin Combines Generative AI and Smart Diagramming For Users](https://www.mermaidchart.com/blog/posts/mermaid-chart-chatgpt-plugin-combines-generative-ai-and-smart-diagramming)
29 June 2023 · 4 mins
Mermaid Charts new ChatGPT plugin integrates AI-powered text prompts with Mermaids intuitive diagramming editor, enabling users to generate, edit, and share complex diagrams with ease and efficiency.
## [Sequence diagrams, the only good thing UML brought to software development](https://www.mermaidchart.com/blog/posts/sequence-diagrams-the-good-thing-uml-brought-to-software-development/)
15 June 2023 · 12 mins
Sequence diagrams really shine when youre documenting different parts of a system and the various ways these parts interact with each other.
## [subhash-halder contributed quadrant charts so you can show your manager what to select - just like the strategy consultants at BCG do](https://www.mermaidchart.com/blog/posts/subhash-halder-contributed-quadrant-charts-so-you-can-show-your-manager-what-to-select-just-like-the-strategy-consultants-at-bcg-do/)
8 June 2023 · 7 mins

View File

@@ -919,6 +919,10 @@ In the example below the style defined in the linkStyle statement will belong to
linkStyle 3 stroke:#ff3,stroke-width:4px,color:red;
It is also possible to add style to multiple links in a single statement, by separating link numbers with commas:
linkStyle 1,2,7 color:blue;
### Styling line curves
It is possible to style the type of curve used for lines between items, if the default method does not meet your needs.
@@ -957,10 +961,14 @@ flowchart LR
More convenient than defining the style every time is to define a class of styles and attach this class to the nodes that
should have a different look.
a class definition looks like the example below:
A class definition looks like the example below:
classDef className fill:#f9f,stroke:#333,stroke-width:4px;
Also, it is possible to define style to multiple classes in one statement:
classDef firstClassName,secondClassName font-size:12pt;
Attachment of a class to a node is done as per below:
class nodeId1 className;
@@ -983,6 +991,24 @@ flowchart LR
classDef someclass fill:#f96
```
This form can be used when declaring multiple links between nodes:
```mermaid-example
flowchart LR
A:::foo & B:::bar --> C:::foobar
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef foobar stroke:#00f
```
```mermaid
flowchart LR
A:::foo & B:::bar --> C:::foobar
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef foobar stroke:#00f
```
### Css classes
It is also possible to predefine classes in css styles that can be applied from the graph definition as in the example

View File

@@ -25,25 +25,25 @@ Mermaid can render Gantt diagrams as SVG, PNG or a MarkDown link that can be pas
```mermaid-example
gantt
title A Gantt Diagram
dateFormat YYYY-MM-DD
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :after a1 , 20d
A task :a1, 2014-01-01, 30d
Another task :after a1, 20d
section Another
Task in sec :2014-01-12 , 12d
another task : 24d
Task in Another :2014-01-12, 12d
another task :24d
```
```mermaid
gantt
title A Gantt Diagram
dateFormat YYYY-MM-DD
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :after a1 , 20d
A task :a1, 2014-01-01, 30d
Another task :after a1, 20d
section Another
Task in sec :2014-01-12 , 12d
another task : 24d
Task in Another :2014-01-12, 12d
another task :24d
```
## Syntax
@@ -117,17 +117,17 @@ gantt
It is possible to set multiple dependencies separated by space:
```mermaid-example
gantt
apple :a, 2017-07-20, 1w
banana :crit, b, 2017-07-23, 1d
cherry :active, c, after b a, 1d
gantt
apple :a, 2017-07-20, 1w
banana :crit, b, 2017-07-23, 1d
cherry :active, c, after b a, 1d
```
```mermaid
gantt
apple :a, 2017-07-20, 1w
banana :crit, b, 2017-07-23, 1d
cherry :active, c, after b a, 1d
gantt
apple :a, 2017-07-20, 1w
banana :crit, b, 2017-07-23, 1d
cherry :active, c, after b a, 1d
```
### Title
@@ -146,22 +146,22 @@ You can add milestones to the diagrams. Milestones differ from tasks as they rep
```mermaid-example
gantt
dateFormat HH:mm
axisFormat %H:%M
Initial milestone : milestone, m1, 17:49,2min
taska2 : 10min
taska3 : 5min
Final milestone : milestone, m2, 18:14, 2min
dateFormat HH:mm
axisFormat %H:%M
Initial milestone : milestone, m1, 17:49, 2m
Task A : 10m
Task B : 5m
Final milestone : milestone, m2, 18:08, 4m
```
```mermaid
gantt
dateFormat HH:mm
axisFormat %H:%M
Initial milestone : milestone, m1, 17:49,2min
taska2 : 10min
taska3 : 5min
Final milestone : milestone, m2, 18:14, 2min
dateFormat HH:mm
axisFormat %H:%M
Initial milestone : milestone, m1, 17:49, 2m
Task A : 10m
Task B : 5m
Final milestone : milestone, m2, 18:08, 4m
```
## Setting dates
@@ -296,29 +296,27 @@ Comments can be entered within a gantt chart, which will be ignored by the parse
```mermaid-example
gantt
title A Gantt Diagram
%% this is a comment
dateFormat YYYY-MM-DD
%% This is a comment
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :after a1 , 20d
A task :a1, 2014-01-01, 30d
Another task :after a1, 20d
section Another
Task in sec :2014-01-12 , 12d
another task : 24d
Task in Another :2014-01-12, 12d
another task :24d
```
```mermaid
gantt
title A Gantt Diagram
%% this is a comment
dateFormat YYYY-MM-DD
%% This is a comment
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :after a1 , 20d
A task :a1, 2014-01-01, 30d
Another task :after a1, 20d
section Another
Task in sec :2014-01-12 , 12d
another task : 24d
Task in Another :2014-01-12, 12d
another task :24d
```
## Styling
@@ -440,7 +438,7 @@ Beginner's tip—a full example using interactive links in an html context:
dateFormat YYYY-MM-DD
section Clickable
Visit mermaidjs :active, cl1, 2014-01-07, 3d
Visit mermaidjs :active, cl1, 2014-01-07, 3d
Print arguments :cl2, after cl1, 3d
Print task :cl3, after cl2, 3d

View File

@@ -152,7 +152,7 @@ quadrantChart
y-axis Not Important --> "Important ❤"
quadrant-1 Plan
quadrant-2 Do
quadrant-3 Deligate
quadrant-3 Delegate
quadrant-4 Delete
```
@@ -163,6 +163,6 @@ quadrantChart
y-axis Not Important --> "Important ❤"
quadrant-1 Plan
quadrant-2 Do
quadrant-3 Deligate
quadrant-3 Delegate
quadrant-4 Delete
```

294
docs/syntax/sankey.md Normal file
View File

@@ -0,0 +1,294 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/sankey.md](../../packages/mermaid/src/docs/syntax/sankey.md).
# Sankey diagrams
> A sankey diagram is a visualization used to depict a flow from one set of values to another.
::: warning
This is an experimental diagram. Its syntax are very close to plain CSV, but it is to be extended in the nearest future.
:::
The things being connected are called nodes and the connections are called links.
## Example
This example taken from [observable](https://observablehq.com/@d3/sankey/2?collection=@d3/d3-sankey). It may be rendered a little bit differently, though, in terms of size and colors.
```mermaid-example
sankey-beta
Agricultural 'waste',Bio-conversion,124.729
Bio-conversion,Liquid,0.597
Bio-conversion,Losses,26.862
Bio-conversion,Solid,280.322
Bio-conversion,Gas,81.144
Biofuel imports,Liquid,35
Biomass imports,Solid,35
Coal imports,Coal,11.606
Coal reserves,Coal,63.965
Coal,Solid,75.571
District heating,Industry,10.639
District heating,Heating and cooling - commercial,22.505
District heating,Heating and cooling - homes,46.184
Electricity grid,Over generation / exports,104.453
Electricity grid,Heating and cooling - homes,113.726
Electricity grid,H2 conversion,27.14
Electricity grid,Industry,342.165
Electricity grid,Road transport,37.797
Electricity grid,Agriculture,4.412
Electricity grid,Heating and cooling - commercial,40.858
Electricity grid,Losses,56.691
Electricity grid,Rail transport,7.863
Electricity grid,Lighting & appliances - commercial,90.008
Electricity grid,Lighting & appliances - homes,93.494
Gas imports,Ngas,40.719
Gas reserves,Ngas,82.233
Gas,Heating and cooling - commercial,0.129
Gas,Losses,1.401
Gas,Thermal generation,151.891
Gas,Agriculture,2.096
Gas,Industry,48.58
Geothermal,Electricity grid,7.013
H2 conversion,H2,20.897
H2 conversion,Losses,6.242
H2,Road transport,20.897
Hydro,Electricity grid,6.995
Liquid,Industry,121.066
Liquid,International shipping,128.69
Liquid,Road transport,135.835
Liquid,Domestic aviation,14.458
Liquid,International aviation,206.267
Liquid,Agriculture,3.64
Liquid,National navigation,33.218
Liquid,Rail transport,4.413
Marine algae,Bio-conversion,4.375
Ngas,Gas,122.952
Nuclear,Thermal generation,839.978
Oil imports,Oil,504.287
Oil reserves,Oil,107.703
Oil,Liquid,611.99
Other waste,Solid,56.587
Other waste,Bio-conversion,77.81
Pumped heat,Heating and cooling - homes,193.026
Pumped heat,Heating and cooling - commercial,70.672
Solar PV,Electricity grid,59.901
Solar Thermal,Heating and cooling - homes,19.263
Solar,Solar Thermal,19.263
Solar,Solar PV,59.901
Solid,Agriculture,0.882
Solid,Thermal generation,400.12
Solid,Industry,46.477
Thermal generation,Electricity grid,525.531
Thermal generation,Losses,787.129
Thermal generation,District heating,79.329
Tidal,Electricity grid,9.452
UK land based bioenergy,Bio-conversion,182.01
Wave,Electricity grid,19.013
Wind,Electricity grid,289.366
```
```mermaid
sankey-beta
Agricultural 'waste',Bio-conversion,124.729
Bio-conversion,Liquid,0.597
Bio-conversion,Losses,26.862
Bio-conversion,Solid,280.322
Bio-conversion,Gas,81.144
Biofuel imports,Liquid,35
Biomass imports,Solid,35
Coal imports,Coal,11.606
Coal reserves,Coal,63.965
Coal,Solid,75.571
District heating,Industry,10.639
District heating,Heating and cooling - commercial,22.505
District heating,Heating and cooling - homes,46.184
Electricity grid,Over generation / exports,104.453
Electricity grid,Heating and cooling - homes,113.726
Electricity grid,H2 conversion,27.14
Electricity grid,Industry,342.165
Electricity grid,Road transport,37.797
Electricity grid,Agriculture,4.412
Electricity grid,Heating and cooling - commercial,40.858
Electricity grid,Losses,56.691
Electricity grid,Rail transport,7.863
Electricity grid,Lighting & appliances - commercial,90.008
Electricity grid,Lighting & appliances - homes,93.494
Gas imports,Ngas,40.719
Gas reserves,Ngas,82.233
Gas,Heating and cooling - commercial,0.129
Gas,Losses,1.401
Gas,Thermal generation,151.891
Gas,Agriculture,2.096
Gas,Industry,48.58
Geothermal,Electricity grid,7.013
H2 conversion,H2,20.897
H2 conversion,Losses,6.242
H2,Road transport,20.897
Hydro,Electricity grid,6.995
Liquid,Industry,121.066
Liquid,International shipping,128.69
Liquid,Road transport,135.835
Liquid,Domestic aviation,14.458
Liquid,International aviation,206.267
Liquid,Agriculture,3.64
Liquid,National navigation,33.218
Liquid,Rail transport,4.413
Marine algae,Bio-conversion,4.375
Ngas,Gas,122.952
Nuclear,Thermal generation,839.978
Oil imports,Oil,504.287
Oil reserves,Oil,107.703
Oil,Liquid,611.99
Other waste,Solid,56.587
Other waste,Bio-conversion,77.81
Pumped heat,Heating and cooling - homes,193.026
Pumped heat,Heating and cooling - commercial,70.672
Solar PV,Electricity grid,59.901
Solar Thermal,Heating and cooling - homes,19.263
Solar,Solar Thermal,19.263
Solar,Solar PV,59.901
Solid,Agriculture,0.882
Solid,Thermal generation,400.12
Solid,Industry,46.477
Thermal generation,Electricity grid,525.531
Thermal generation,Losses,787.129
Thermal generation,District heating,79.329
Tidal,Electricity grid,9.452
UK land based bioenergy,Bio-conversion,182.01
Wave,Electricity grid,19.013
Wind,Electricity grid,289.366
```
## Syntax
The idea behind syntax is that a user types `sankey-beta` keyword first, then pastes raw CSV below and get the result.
It implements CSV standard as [described here](https://www.ietf.org/rfc/rfc4180.txt) with subtle **differences**:
- CSV must contain **3 columns only**
- It is **allowed** to have **empty lines** without comma separators for visual purposes
### Basic
It is implied that 3 columns inside CSV should represent `source`, `target` and `value` accordingly:
```mermaid-example
sankey-beta
%% source,target,value
Electricity grid,Over generation / exports,104.453
Electricity grid,Heating and cooling - homes,113.726
Electricity grid,H2 conversion,27.14
```
```mermaid
sankey-beta
%% source,target,value
Electricity grid,Over generation / exports,104.453
Electricity grid,Heating and cooling - homes,113.726
Electricity grid,H2 conversion,27.14
```
### Empty Lines
CSV does not support empty lines without comma delimeters by default. But you can add them if needed:
```mermaid-example
sankey-beta
Bio-conversion,Losses,26.862
Bio-conversion,Solid,280.322
Bio-conversion,Gas,81.144
```
```mermaid
sankey-beta
Bio-conversion,Losses,26.862
Bio-conversion,Solid,280.322
Bio-conversion,Gas,81.144
```
### Commas
If you need to have a comma, wrap it in double quotes:
```mermaid-example
sankey-beta
Pumped heat,"Heating and cooling, homes",193.026
Pumped heat,"Heating and cooling, commercial",70.672
```
```mermaid
sankey-beta
Pumped heat,"Heating and cooling, homes",193.026
Pumped heat,"Heating and cooling, commercial",70.672
```
### Double Quotes
If you need to have double quote, put a pair of them inside quoted string:
```mermaid-example
sankey-beta
Pumped heat,"Heating and cooling, ""homes""",193.026
Pumped heat,"Heating and cooling, ""commercial""",70.672
```
```mermaid
sankey-beta
Pumped heat,"Heating and cooling, ""homes""",193.026
Pumped heat,"Heating and cooling, ""commercial""",70.672
```
## Configuration
You can customize link colors, node alignments and diagram dimensions.
```html
<script>
const config = {
startOnLoad: true,
securityLevel: 'loose',
sankey: {
width: 800,
height: 400,
linkColor: 'source',
nodeAlignment: 'left',
},
};
mermaid.initialize(config);
</script>
```
### Links Coloring
You can adjust links' color by setting `linkColor` to one of those:
- `source` - link will be of a source node color
- `target` - link will be of a target node color
- `gradient` - link color will be smoothly transient between source and target node colors
- hex code of color, like `#a1a1a1`
### Node Alignment
Graph layout can be changed by setting `nodeAlignment` to:
- `justify`
- `center`
- `left`
- `right`

View File

@@ -94,6 +94,43 @@ sequenceDiagram
J->>A: Great!
```
### Actor Creation and Destruction
It is possible to create and destroy actors by messages. To do so, add a create or destroy directive before the message.
create participant B
A --> B: Hello
Create directives support actor/participant distinction and aliases. The sender or the recipient of a message can be destroyed but only the recipient can be created.
```mermaid-example
sequenceDiagram
Alice->>Bob: Hello Bob, how are you ?
Bob->>Alice: Fine, thank you. And you?
create participant Carl
Alice->>Carl: Hi Carl!
create actor D as Donald
Carl->>D: Hi!
destroy Carl
Alice-xCarl: We are too many
destroy Bob
Bob->>Alice: I agree
```
```mermaid
sequenceDiagram
Alice->>Bob: Hello Bob, how are you ?
Bob->>Alice: Fine, thank you. And you?
create participant Carl
Alice->>Carl: Hi Carl!
create actor D as Donald
Carl->>D: Hi!
destroy Carl
Alice-xCarl: We are too many
destroy Bob
Bob->>Alice: I agree
```
### Grouping / Box
The actor(s) can be grouped in vertical boxes. You can define a color (if not, it will be transparent) and/or a descriptive label using the following notation:

View File

@@ -487,7 +487,7 @@ where
- the second _property_ is `color` and its _value_ is `white`
- the third _property_ is `font-weight` and its _value_ is `bold`
- the fourth _property_ is `stroke-width` and its _value_ is `2px`
- the fifth _property_ is `stroke` and its _value_ is `yello`
- the fifth _property_ is `stroke` and its _value_ is `yellow`
### Apply classDef styles to states

View File

@@ -257,9 +257,11 @@ let us look at same example, where we have disabled the multiColor option.
### Customizing Color scheme
You can customize the color scheme using the `cScale0` to `cScale11` theme variables. Mermaid allows you to set unique colors for up-to 12 sections, where `cScale0` variable will drive the value of the first section or time-period, `cScale1` will drive the value of the second section and so on.
You can customize the color scheme using the `cScale0` to `cScale11` theme variables, which will change the background colors. Mermaid allows you to set unique colors for up-to 12 sections, where `cScale0` variable will drive the value of the first section or time-period, `cScale1` will drive the value of the second section and so on.
In case you have more than 12 sections, the color scheme will start to repeat.
If you also want to change the foreground color of a section, you can do so use theme variables corresponding `cScaleLabel0` to `cScaleLabel11` variables.
NOTE: Default values for these theme variables are picked from the selected theme. If you want to override the default values, you can use the `initialize` call to add your custom theme variable values.
Example:
@@ -268,9 +270,9 @@ Now let's override the default values for the `cScale0` to `cScale2` variables:
```mermaid-example
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
'cScale0': '#ff0000',
'cScale0': '#ff0000', 'cScaleLabel0': '#ffffff',
'cScale1': '#00ff00',
'cScale2': '#0000ff'
'cScale2': '#0000ff', 'cScaleLabel2': '#ffffff'
} } }%%
timeline
title History of Social Media Platform
@@ -286,9 +288,9 @@ Now let's override the default values for the `cScale0` to `cScale2` variables:
```mermaid
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
'cScale0': '#ff0000',
'cScale0': '#ff0000', 'cScaleLabel0': '#ffffff',
'cScale1': '#00ff00',
'cScale2': '#0000ff'
'cScale2': '#0000ff', 'cScaleLabel2': '#ffffff'
} } }%%
timeline
title History of Social Media Platform

View File

@@ -1,10 +1,10 @@
{
"name": "mermaid-monorepo",
"private": true,
"version": "10.2.3",
"version": "10.2.4",
"description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"type": "module",
"packageManager": "pnpm@8.5.1",
"packageManager": "pnpm@8.6.5",
"keywords": [
"diagram",
"markdown",
@@ -22,6 +22,7 @@
"build:watch": "pnpm build:vite --watch",
"build": "pnpm run -r clean && pnpm build:types && pnpm build:vite",
"dev": "concurrently \"pnpm build:vite --watch\" \"ts-node-esm .vite/server.ts\"",
"dev:coverage": "pnpm coverage:cypress:clean && VITE_COVERAGE=true pnpm dev",
"release": "pnpm build",
"lint": "eslint --cache --cache-strategy content --ignore-path .gitignore . && pnpm lint:jison && prettier --cache --check .",
"lint:fix": "eslint --cache --cache-strategy content --fix --ignore-path .gitignore . && prettier --write . && ts-node-esm scripts/fixCSpell.ts",
@@ -30,6 +31,10 @@
"cypress": "cypress run",
"cypress:open": "cypress open",
"e2e": "start-server-and-test dev http://localhost:9000/ cypress",
"coverage:cypress:clean": "rimraf .nyc_output coverage/cypress",
"e2e:coverage": "pnpm coverage:cypress:clean && VITE_COVERAGE=true pnpm e2e",
"coverage:merge": "ts-node-esm scripts/coverage.ts",
"coverage": "pnpm test:coverage --run && pnpm e2e:coverage && pnpm coverage:merge",
"ci": "vitest run",
"test": "pnpm lint && vitest run",
"test:watch": "vitest --watch",
@@ -55,11 +60,12 @@
]
},
"devDependencies": {
"@applitools/eyes-cypress": "^3.32.0",
"@applitools/eyes-cypress": "^3.33.1",
"@commitlint/cli": "^17.6.1",
"@commitlint/config-conventional": "^17.6.1",
"@cspell/eslint-plugin": "^6.31.1",
"@rollup/plugin-typescript": "^11.1.0",
"@cypress/code-coverage": "^3.10.7",
"@rollup/plugin-typescript": "^11.1.1",
"@types/cors": "^2.8.13",
"@types/eslint": "^8.37.0",
"@types/express": "^4.17.17",
@@ -72,48 +78,54 @@
"@types/rollup-plugin-visualizer": "^4.2.1",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitest/coverage-c8": "^0.31.0",
"@vitest/spy": "^0.31.0",
"@vitest/ui": "^0.31.0",
"@vitest/coverage-v8": "^0.33.0",
"@vitest/spy": "^0.33.0",
"@vitest/ui": "^0.33.0",
"ajv": "^8.12.0",
"concurrently": "^8.0.1",
"cors": "^2.8.5",
"coveralls": "^3.1.1",
"cypress": "^12.10.0",
"cypress-image-snapshot": "^4.0.1",
"esbuild": "^0.17.18",
"esbuild": "^0.18.0",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-cypress": "^2.13.2",
"eslint-plugin-html": "^7.1.0",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-jsdoc": "^43.0.7",
"eslint-plugin-jsdoc": "^46.0.0",
"eslint-plugin-json": "^3.1.0",
"eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-markdown": "^3.0.0",
"eslint-plugin-no-only-tests": "^3.1.0",
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^46.0.0",
"eslint-plugin-unicorn": "^47.0.0",
"express": "^4.18.2",
"globby": "^13.1.4",
"husky": "^8.0.3",
"jest": "^29.5.0",
"jison": "^0.4.18",
"js-yaml": "^4.1.0",
"jsdom": "^21.1.1",
"jsdom": "^22.0.0",
"lint-staged": "^13.2.1",
"nyc": "^15.1.0",
"path-browserify": "^1.0.1",
"pnpm": "^8.3.1",
"prettier": "^2.8.8",
"prettier-plugin-jsdoc": "^0.4.2",
"rimraf": "^5.0.0",
"rollup-plugin-visualizer": "^5.9.0",
"rollup-plugin-visualizer": "^5.9.2",
"start-server-and-test": "^2.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.4",
"vite": "^4.3.1",
"vitest": "^0.31.0"
"typescript": "^5.1.3",
"vite": "^4.3.9",
"vite-plugin-istanbul": "^4.1.0",
"vitest": "^0.33.0"
},
"volta": {
"node": "18.16.0"
"node": "18.16.1"
},
"nyc": {
"report-dir": "coverage/cypress"
}
}

View File

@@ -52,9 +52,6 @@
"rimraf": "^5.0.0",
"mermaid": "workspace:*"
},
"resolutions": {
"d3": "^7.0.0"
},
"files": [
"dist"
],

View File

@@ -22,7 +22,6 @@ export const setInfo = (inf) => {
info = inf;
};
/** @returns Returns the info flag */
export const getInfo = () => {
return info;
};

View File

@@ -8,7 +8,6 @@ import { log, getConfig, setupGraphViewbox } from './mermaidUtils.js';
* @param {any} text
* @param {any} id
* @param {any} version
* @param diagObj
*/
export const draw = (text, id, version) => {
try {

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
const warning = (s: string) => {
// Todo remove debug code
// eslint-disable-next-line no-console
@@ -28,7 +29,6 @@ export let setLogLevel: (level: keyof typeof LEVELS | number | string) => void;
export let getConfig: () => object;
export let sanitizeText: (str: string) => string;
export let commonDb: () => object;
// eslint-disable @typescript-eslint/no-explicit-any
export let setupGraphViewbox: (
graph: any,
svgElem: any,

View File

@@ -4,4 +4,5 @@ export default {
'src/docs/**': ['pnpm --filter mermaid run docs:build --git'],
'src/docs.mts': ['pnpm --filter mermaid run docs:build --git'],
'src/(defaultConfig|config|mermaidAPI).ts': ['pnpm --filter mermaid run docs:build --git'],
'src/schemas/config.schema.yaml': ['pnpm --filter mermaid run types:build-config --git'],
};

View File

@@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "10.2.3",
"version": "10.2.4",
"description": "Markdown-ish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"type": "module",
"module": "./dist/mermaid.core.mjs",
@@ -27,11 +27,14 @@
"docs:code": "typedoc src/defaultConfig.ts src/config.ts src/mermaidAPI.ts && prettier --write ./src/docs/config/setup",
"docs:build": "rimraf ../../docs && pnpm docs:spellcheck && pnpm docs:code && ts-node-esm src/docs.mts",
"docs:verify": "pnpm docs:spellcheck && 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 && (cd src/vitepress && pnpm --filter ./ install --no-frozen-lockfile --ignore-scripts && pnpm run build) && cpy --flat src/docs/landing/ ./src/vitepress/.vitepress/dist/landing",
"docs:dev": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./ src/vitepress dev\" \"ts-node-esm src/docs.mts --watch --vitepress\"",
"docs:pre:vitepress": "pnpm --filter ./src/docs prefetch && rimraf src/vitepress && pnpm docs:code && ts-node-esm src/docs.mts --vitepress && pnpm --filter ./src/vitepress install --no-frozen-lockfile --ignore-scripts",
"docs:build:vitepress": "pnpm docs:pre:vitepress && (cd src/vitepress && pnpm run build) && cpy --flat src/docs/landing/ ./src/vitepress/.vitepress/dist/landing",
"docs:dev": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev\" \"ts-node-esm src/docs.mts --watch --vitepress\"",
"docs:dev:docker": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev:docker\" \"ts-node-esm src/docs.mts --watch --vitepress\"",
"docs:serve": "pnpm docs:build:vitepress && vitepress serve src/vitepress",
"docs:spellcheck": "cspell --config ../../cSpell.json \"src/docs/**/*.md\"",
"types:build-config": "ts-node-esm --transpileOnly scripts/create-types-from-json-schema.mts",
"types:verify-config": "ts-node-esm scripts/create-types-from-json-schema.mts --verify",
"release": "pnpm build",
"prepublishOnly": "cpy '../../README.*' ./ --cwd=. && pnpm -w run build"
},
@@ -53,13 +56,16 @@
},
"dependencies": {
"@braintree/sanitize-url": "^6.0.2",
"@types/d3-scale": "^4.0.3",
"@types/d3-scale-chromatic": "^3.0.0",
"cytoscape": "^3.23.0",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.1.0",
"d3": "^7.4.0",
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.10",
"dayjs": "^1.11.7",
"dompurify": "3.0.3",
"dompurify": "3.0.4",
"elkjs": "^0.8.2",
"khroma": "^2.0.0",
"lodash-es": "^4.17.21",
@@ -71,8 +77,10 @@
"web-worker": "^1.2.0"
},
"devDependencies": {
"@adobe/jsonschema2md": "^7.1.4",
"@types/cytoscape": "^3.19.9",
"@types/d3": "^7.4.0",
"@types/d3-sankey": "^0.12.1",
"@types/d3-selection": "^3.0.5",
"@types/dompurify": "^3.0.2",
"@types/jsdom": "^21.1.1",
@@ -83,6 +91,7 @@
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"ajv": "^8.11.2",
"chokidar": "^3.5.3",
"concurrently": "^8.0.1",
"coveralls": "^3.1.1",
@@ -92,7 +101,8 @@
"globby": "^13.1.4",
"jison": "^0.4.18",
"js-base64": "^3.7.5",
"jsdom": "^21.1.1",
"jsdom": "^22.0.0",
"json-schema-to-typescript": "^11.0.3",
"micromatch": "^4.0.5",
"path-browserify": "^1.0.1",
"prettier": "^2.8.8",
@@ -105,6 +115,7 @@
"typedoc-plugin-markdown": "^3.15.2",
"typescript": "^5.0.4",
"unist-util-flatmap": "^1.0.0",
"unist-util-visit": "^4.1.2",
"vitepress": "^1.0.0-alpha.72",
"vitepress-plugin-search": "^1.0.4-alpha.20"
},

View File

@@ -0,0 +1,252 @@
/**
* Script to load Mermaid Config JSON Schema from YAML and to:
*
* - Validate JSON Schema
*
* Then to generate:
*
* - config.types.ts TypeScript file
*/
/* eslint-disable no-console */
import { readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import assert from 'node:assert';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { load, JSON_SCHEMA } from 'js-yaml';
import { compile, type JSONSchema } from 'json-schema-to-typescript';
import _Ajv2019, { type JSONSchemaType } from 'ajv/dist/2019.js';
// Workaround for wrong AJV types, see
// https://github.com/ajv-validator/ajv/issues/2132#issuecomment-1290409907
const Ajv2019 = _Ajv2019 as unknown as typeof _Ajv2019.default;
// !!! -- The config.type.js file is created by this script -- !!!
import type { MermaidConfig } from '../src/config.type.js';
// options for running the main command
const verifyOnly = process.argv.includes('--verify');
/** If `true`, automatically `git add` any changes (i.e. during `pnpm run pre-commit`)*/
const git = process.argv.includes('--git');
/**
* All of the keys in the mermaid config that have a mermaid diagram config.
*/
const MERMAID_CONFIG_DIAGRAM_KEYS = [
'flowchart',
'sequence',
'gantt',
'journey',
'class',
'state',
'er',
'pie',
'quadrantChart',
'requirement',
'mindmap',
'timeline',
'gitGraph',
'c4',
'sankey',
];
/**
* Loads the MermaidConfig JSON schema YAML file.
*
* @returns The loaded JSON Schema, use {@link validateSchema} to confirm it is a valid JSON Schema.
*/
async function loadJsonSchemaFromYaml() {
const configSchemaFile = join('src', 'schemas', 'config.schema.yaml');
const contentsYaml = await readFile(configSchemaFile, { encoding: 'utf8' });
const jsonSchema = load(contentsYaml, {
filename: configSchemaFile,
// only allow JSON types in our YAML doc (will probably be default in YAML 1.3)
// e.g. `true` will be parsed a boolean `true`, `True` will be parsed as string `"True"`.
schema: JSON_SCHEMA,
});
return jsonSchema;
}
/**
* Asserts that the given value is a valid JSON Schema object.
*
* @param jsonSchema - The value to validate as JSON Schema 2019-09
* @throws {Error} if the given object is invalid.
*/
function validateSchema(jsonSchema: unknown): asserts jsonSchema is JSONSchemaType<MermaidConfig> {
if (typeof jsonSchema !== 'object') {
throw new Error(`jsonSchema param is not an object: actual type is ${typeof jsonSchema}`);
}
if (jsonSchema === null) {
throw new Error('jsonSchema param must not be null');
}
const ajv = new Ajv2019({
allErrors: true,
allowUnionTypes: true,
strict: true,
});
ajv.addKeyword({
keyword: 'meta:enum', // used by jsonschema2md (in docs.mts script)
errors: false,
});
ajv.addKeyword({
keyword: 'tsType', // used by json-schema-to-typescript
errors: false,
});
ajv.compile(jsonSchema);
}
/**
* Generate a typescript definition from a JSON Schema using json-schema-to-typescript.
*
* @param mermaidConfigSchema - The input JSON Schema.
*/
async function generateTypescript(mermaidConfigSchema: JSONSchemaType<MermaidConfig>) {
/**
* Replace all usages of `allOf` with `extends`.
*
* `extends` is only valid JSON Schema in very old versions of JSON Schema.
* However, json-schema-to-typescript creates much nicer types when using
* `extends`, so we should use them instead when possible.
*
* @param schema - The input schema.
* @returns The schema with `allOf` replaced with `extends`.
*/
function replaceAllOfWithExtends(schema: JSONSchemaType<Record<string, any>>) {
if (schema['allOf']) {
const { allOf, ...schemaWithoutAllOf } = schema;
return {
...schemaWithoutAllOf,
extends: allOf,
};
}
return schema;
}
/**
* For backwards compatibility with older Mermaid Typescript defs,
* we need to make all value optional instead of required.
*
* This is because the `MermaidConfig` type is used as an input, and everything is optional,
* since all the required values have default values.s
*
* In the future, we should make make the input to Mermaid `Partial<MermaidConfig>`.
*
* @todo TODO: Remove this function when Mermaid releases a new breaking change.
* @param schema - The input schema.
* @returns The schema with all required values removed.
*/
function removeRequired(schema: JSONSchemaType<Record<string, any>>) {
return { ...schema, required: [] };
}
/**
* This is a temporary hack to control the order the types are generated in.
*
* By default, json-schema-to-typescript outputs the $defs in the order they
* are used, then any unused schemas at the end.
*
* **The only purpose of this function is to make the `git diff` simpler**
* **We should remove this later to simplify the code**
*
* @todo TODO: Remove this function in a future PR.
* @param schema - The input schema.
* @returns The schema with all `$ref`s removed.
*/
function unrefSubschemas(schema: JSONSchemaType<Record<string, any>>) {
return {
...schema,
properties: Object.fromEntries(
Object.entries(schema.properties).map(([key, propertySchema]) => {
if (MERMAID_CONFIG_DIAGRAM_KEYS.includes(key)) {
const { $ref, ...propertySchemaWithoutRef } = propertySchema as JSONSchemaType<unknown>;
if ($ref === undefined) {
throw Error(
`subSchema ${key} is in MERMAID_CONFIG_DIAGRAM_KEYS but does not have a $ref field`
);
}
const [
_root, // eslint-disable-line @typescript-eslint/no-unused-vars
_defs, // eslint-disable-line @typescript-eslint/no-unused-vars
defName,
] = $ref.split('/');
return [
key,
{
...propertySchemaWithoutRef,
tsType: defName,
},
];
}
return [key, propertySchema];
})
),
};
}
assert.ok(mermaidConfigSchema.$defs);
const modifiedSchema = {
...unrefSubschemas(removeRequired(mermaidConfigSchema)),
$defs: Object.fromEntries(
Object.entries(mermaidConfigSchema.$defs).map(([key, subSchema]) => {
return [key, removeRequired(replaceAllOfWithExtends(subSchema))];
})
),
};
const typescriptFile = await compile(
modifiedSchema as JSONSchema, // json-schema-to-typescript only allows JSON Schema 4 as input type
'MermaidConfig',
{
additionalProperties: false, // in JSON Schema 2019-09, these are called `unevaluatedProperties`
unreachableDefinitions: true, // definition for FontConfig is unreachable
}
);
// TODO, should we somehow use the functions from `docs.mts` instead?
if (verifyOnly) {
const originalFile = await readFile('./src/config.type.ts', { encoding: 'utf-8' });
if (typescriptFile !== originalFile) {
console.error('❌ Error: ./src/config.type.ts will be changed.');
console.error("Please run 'pnpm run --filter mermaid types:build-config' to update this");
process.exitCode = 1;
} else {
console.log('✅ ./src/config.type.ts will be unchanged');
}
} else {
console.log('Writing typescript file to ./src/config.type.ts');
await writeFile('./src/config.type.ts', typescriptFile, { encoding: 'utf8' });
if (git) {
console.log('📧 Git: Adding ./src/config.type.ts changed');
await promisify(execFile)('git', ['add', './src/config.type.ts']);
}
}
}
/** Main function */
async function main() {
if (verifyOnly) {
console.log(
'Verifying that ./src/config.type.ts is in sync with src/schemas/config.schema.yaml'
);
}
const configJsonSchema = await loadJsonSchemaFromYaml();
validateSchema(configJsonSchema);
// Generate types from JSON Schema
await generateTypescript(configJsonSchema);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

View File

@@ -1,27 +1,24 @@
import { MockedD3 } from './tests/MockedD3.js';
import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js';
import { D3Element } from './mermaidAPI.js';
import type { D3Element } from './mermaidAPI.js';
describe('accessibility', () => {
const fauxSvgNode = new MockedD3();
const fauxSvgNode: MockedD3 = new MockedD3();
describe('setA11yDiagramInfo', () => {
it('sets the svg element role to "graphics-document document"', () => {
// @ts-ignore Required to easily handle the d3 select types
it('should set svg element role to "graphics-document document"', () => {
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
setA11yDiagramInfo(fauxSvgNode, 'flowchart');
expect(svgAttrSpy).toHaveBeenCalledWith('role', 'graphics-document document');
});
it('sets the aria-roledescription to the diagram type', () => {
// @ts-ignore Required to easily handle the d3 select types
it('should set aria-roledescription to the diagram type', () => {
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
setA11yDiagramInfo(fauxSvgNode, 'flowchart');
expect(svgAttrSpy).toHaveBeenCalledWith('aria-roledescription', 'flowchart');
});
it('does not set the aria-roledescription if the diagram type is empty', () => {
// @ts-ignore Required to easily handle the d3 select types
it('should not set aria-roledescription if the diagram type is empty', () => {
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
setA11yDiagramInfo(fauxSvgNode, '');
expect(svgAttrSpy).toHaveBeenCalledTimes(1);
@@ -32,8 +29,8 @@ describe('accessibility', () => {
describe('addSVGa11yTitleDescription', () => {
const givenId = 'theBaseId';
describe('with the given svg d3 object:', () => {
it('does nothing if there is no insert defined', () => {
describe('with svg d3 object', () => {
it('should do nothing if there is no insert defined', () => {
const noInsertSvg = {
attr: vi.fn(),
};
@@ -42,26 +39,25 @@ describe('accessibility', () => {
expect(noInsertAttrSpy).not.toHaveBeenCalled();
});
// ----------------
// Convenience functions to DRY up the spec
// convenience functions to DRY up the spec
function expectAriaLabelledByIsTitleId(
function expectAriaLabelledByItTitleId(
svgD3Node: D3Element,
title: string | null | undefined,
desc: string | null | undefined,
title: string | undefined,
desc: string | undefined,
givenId: string
) {
): void {
const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node);
addSVGa11yTitleDescription(svgD3Node, title, desc, givenId);
expect(svgAttrSpy).toHaveBeenCalledWith('aria-labelledby', `chart-title-${givenId}`);
}
function expectAriaDescribedByIsDescId(
function expectAriaDescribedByItDescId(
svgD3Node: D3Element,
title: string | null | undefined,
desc: string | null | undefined,
title: string | undefined,
desc: string | undefined,
givenId: string
) {
): void {
const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node);
addSVGa11yTitleDescription(svgD3Node, title, desc, givenId);
expect(svgAttrSpy).toHaveBeenCalledWith('aria-describedby', `chart-desc-${givenId}`);
@@ -69,154 +65,148 @@ describe('accessibility', () => {
function a11yTitleTagInserted(
svgD3Node: D3Element,
title: string | null | undefined,
desc: string | null | undefined,
title: string | undefined,
desc: string | undefined,
givenId: string,
callNumber: number
) {
): void {
a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'title', title);
}
function a11yDescTagInserted(
svgD3Node: D3Element,
title: string | null | undefined,
desc: string | null | undefined,
title: string | undefined,
desc: string | undefined,
givenId: string,
callNumber: number
) {
): void {
a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'desc', desc);
}
function a11yTagInserted(
svgD3Node: D3Element,
title: string | null | undefined,
desc: string | null | undefined,
_svgD3Node: D3Element,
title: string | undefined,
desc: string | undefined,
givenId: string,
callNumber: number,
expectedPrefix: string,
expectedText: string | null | undefined
) {
const fauxInsertedD3 = new MockedD3();
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxInsertedD3);
// @ts-ignore Required to easily handle the d3 select types
expectedText: string | undefined
): void {
const fauxInsertedD3: MockedD3 = new MockedD3();
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxInsertedD3);
const titleAttrSpy = vi.spyOn(fauxInsertedD3, 'attr').mockReturnValue(fauxInsertedD3);
const titleTextSpy = vi.spyOn(fauxInsertedD3, 'text');
addSVGa11yTitleDescription(fauxSvgNode, title, desc, givenId);
expect(svgInsertSpy).toHaveBeenCalledWith(expectedPrefix, ':first-child');
expect(svginsertpy).toHaveBeenCalledWith(expectedPrefix, ':first-child');
expect(titleAttrSpy).toHaveBeenCalledWith('id', `chart-${expectedPrefix}-${givenId}`);
expect(titleTextSpy).toHaveBeenNthCalledWith(callNumber, expectedText);
}
// ----------------
describe('given an a11y title', () => {
describe('with a11y title', () => {
const a11yTitle = 'a11y title';
describe('given an a11y description', () => {
describe('with a11y description', () => {
const a11yDesc = 'a11y description';
it('sets aria-labelledby to the title id inserted as a child', () => {
expectAriaLabelledByIsTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
it('shold set aria-labelledby to the title id inserted as a child', () => {
expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
});
it('sets aria-describedby to the description id inserted as a child', () => {
expectAriaDescribedByIsDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
it('should set aria-describedby to the description id inserted as a child', () => {
expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
});
it('inserts a title tag as the first child with the text set to the accTitle given', () => {
it('should insert title tag as the first child with the text set to the accTitle given', () => {
a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 2);
});
it('inserts a desc tag as the 2nd child with the text set to accDescription given', () => {
it('should insert desc tag as the 2nd child with the text set to accDescription given', () => {
a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
});
});
describe(`no a11y description`, () => {
describe(`without a11y description`, () => {
const a11yDesc = undefined;
it('sets aria-labelledby to the title id inserted as a child', () => {
expectAriaLabelledByIsTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
it('should set aria-labelledby to the title id inserted as a child', () => {
expectAriaLabelledByItTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
});
it('no aria-describedby is set', () => {
// @ts-ignore Required to easily handle the d3 select types
it('should not set aria-describedby', () => {
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything());
});
it('inserts a title tag as the first child with the text set to the accTitle given', () => {
it('should insert title tag as the first child with the text set to the accTitle given', () => {
a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
});
it('no description tag is inserted', () => {
const fauxTitle = new MockedD3();
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
it('should not insert description tag', () => {
const fauxTitle: MockedD3 = new MockedD3();
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgInsertSpy).not.toHaveBeenCalledWith('desc', ':first-child');
expect(svginsertpy).not.toHaveBeenCalledWith('desc', ':first-child');
});
});
});
describe('no a11y title', () => {
describe('without a11y title', () => {
const a11yTitle = undefined;
describe('given an a11y description', () => {
describe('with a11y description', () => {
const a11yDesc = 'a11y description';
it('no aria-labelledby is set', () => {
// @ts-ignore Required to easily handle the d3 select types
it('should not set aria-labelledby', () => {
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything());
});
it('no title tag inserted', () => {
const fauxTitle = new MockedD3();
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
it('should not insert title tag', () => {
const fauxTitle: MockedD3 = new MockedD3();
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgInsertSpy).not.toHaveBeenCalledWith('title', ':first-child');
expect(svginsertpy).not.toHaveBeenCalledWith('title', ':first-child');
});
it('sets aria-describedby to the description id inserted as a child', () => {
expectAriaDescribedByIsDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
it('should set aria-describedby to the description id inserted as a child', () => {
expectAriaDescribedByItDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
});
it('inserts a desc tag as the 2nd child with the text set to accDescription given', () => {
it('should insert desc tag as the 2nd child with the text set to accDescription given', () => {
a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
});
});
describe('no a11y description', () => {
describe('without a11y description', () => {
const a11yDesc = undefined;
it('no aria-labelledby is set', () => {
// @ts-ignore Required to easily handle the d3 select types
it('should not set aria-labelledby', () => {
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything());
});
it('no aria-describedby is set', () => {
// @ts-ignore Required to easily handle the d3 select types
it('should not set aria-describedby', () => {
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything());
});
it('no title tag inserted', () => {
const fauxTitle = new MockedD3();
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
it('should not insert title tag', () => {
const fauxTitle: MockedD3 = new MockedD3();
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgInsertSpy).not.toHaveBeenCalledWith('title', ':first-child');
expect(svginsertpy).not.toHaveBeenCalledWith('title', ':first-child');
});
it('no description tag inserted', () => {
const fauxDesc = new MockedD3();
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxDesc);
it('should not insert description tag', () => {
const fauxDesc: MockedD3 = new MockedD3();
const svginsertpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxDesc);
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
expect(svgInsertSpy).not.toHaveBeenCalledWith('desc', ':first-child');
expect(svginsertpy).not.toHaveBeenCalledWith('desc', ':first-child');
});
});
});

View File

@@ -1,13 +1,11 @@
/**
* Accessibility (a11y) functions, types, helpers
* Accessibility (a11y) functions, types, helpers.
*
* @see https://www.w3.org/WAI/
* @see https://www.w3.org/TR/wai-aria-1.1/
* @see https://www.w3.org/TR/svg-aam-1.0/
*
*/
import { D3Element } from './mermaidAPI.js';
import isEmpty from 'lodash-es/isEmpty.js';
import type { D3Element } from './mermaidAPI.js';
/**
* SVG element role:
@@ -21,50 +19,47 @@ import isEmpty from 'lodash-es/isEmpty.js';
const SVG_ROLE = 'graphics-document document';
/**
* Add role and aria-roledescription to the svg element
* Add role and aria-roledescription to the svg element.
*
* @param svg - d3 object that contains the SVG HTML element
* @param diagramType - diagram name for to the aria-roledescription
*/
export function setA11yDiagramInfo(svg: D3Element, diagramType: string | null | undefined) {
export function setA11yDiagramInfo(svg: D3Element, diagramType: string) {
svg.attr('role', SVG_ROLE);
if (!isEmpty(diagramType)) {
if (diagramType !== '') {
svg.attr('aria-roledescription', diagramType);
}
}
/**
* Add an accessible title and/or description element to a chart.
* The title is usually not displayed and the description is never displayed.
*
* The following charts display their title as a visual and accessibility element: gantt
* The following charts display their title as a visual and accessibility element: gantt.
*
* @param svg - d3 node to insert the a11y title and desc info
* @param a11yTitle - a11y title. null and undefined are meaningful: means to skip it
* @param a11yDesc - a11y description. null and undefined are meaningful: means to skip it
* @param a11yTitle - a11y title. undefined or empty strings mean to skip them
* @param a11yDesc - a11y description. undefined or empty strings mean to skip them
* @param baseId - id used to construct the a11y title and description id
*/
export function addSVGa11yTitleDescription(
svg: D3Element,
a11yTitle: string | null | undefined,
a11yDesc: string | null | undefined,
a11yTitle: string | undefined,
a11yDesc: string | undefined,
baseId: string
) {
): void {
if (svg.insert === undefined) {
return;
}
if (a11yTitle || a11yDesc) {
if (a11yDesc) {
const descId = 'chart-desc-' + baseId;
svg.attr('aria-describedby', descId);
svg.insert('desc', ':first-child').attr('id', descId).text(a11yDesc);
}
if (a11yTitle) {
const titleId = 'chart-title-' + baseId;
svg.attr('aria-labelledby', titleId);
svg.insert('title', ':first-child').attr('id', titleId).text(a11yTitle);
}
} else {
return;
if (a11yDesc) {
const descId = `chart-desc-${baseId}`;
svg.attr('aria-describedby', descId);
svg.insert('desc', ':first-child').attr('id', descId).text(a11yDesc);
}
if (a11yTitle) {
const titleId = `chart-title-${baseId}`;
svg.attr('aria-labelledby', titleId);
svg.insert('title', ':first-child').attr('id', titleId).text(a11yTitle);
}
}

View File

@@ -20,7 +20,7 @@
* of src to dst in order.
* @param {any} dst - The destination of the merge
* @param {any} src - The source object(s) to merge into destination
* @param {{ depth: number; clobber: boolean }} [config={ depth: 2, clobber: false }] - Depth: depth
* @param {{ depth: number; clobber: boolean }} [config] - Depth: depth
* to traverse within src and dst for merging - clobber: should dissimilar types clobber (default:
* { depth: 2, clobber: false }). Default is `{ depth: 2, clobber: false }`
* @returns {any}

View File

@@ -1,56 +0,0 @@
import * as configApi from './config.js';
describe('when working with site config', function () {
beforeEach(() => {
// Resets the site config to default config
configApi.setSiteConfig({});
});
it('should set site config and config properly', function () {
let config_0 = { foo: 'bar', bar: 0 };
configApi.setSiteConfig(config_0);
let config_1 = configApi.getSiteConfig();
let config_2 = configApi.getConfig();
expect(config_1.foo).toEqual(config_0.foo);
expect(config_1.bar).toEqual(config_0.bar);
expect(config_1).toEqual(config_2);
});
it('should respect secure keys when applying directives', function () {
let config_0 = {
foo: 'bar',
bar: 'cant-be-changed',
secure: [...configApi.defaultConfig.secure, 'bar'],
};
configApi.setSiteConfig(config_0);
const directive = { foo: 'baf', bar: 'should-not-be-allowed' };
const cfg = configApi.updateCurrentConfig(config_0, [directive]);
expect(cfg.foo).toEqual(directive.foo);
expect(cfg.bar).toBe(config_0.bar);
});
it('should set reset config properly', function () {
let config_0 = { foo: 'bar', bar: 0 };
configApi.setSiteConfig(config_0);
let config_1 = { foo: 'baf' };
configApi.setConfig(config_1);
let config_2 = configApi.getConfig();
expect(config_2.foo).toEqual(config_1.foo);
configApi.reset();
let config_3 = configApi.getConfig();
expect(config_3.foo).toEqual(config_0.foo);
let config_4 = configApi.getSiteConfig();
expect(config_4.foo).toEqual(config_0.foo);
});
it('should set global reset config properly', function () {
let config_0 = { foo: 'bar', bar: 0 };
configApi.setSiteConfig(config_0);
let config_1 = configApi.getSiteConfig();
expect(config_1.foo).toEqual(config_0.foo);
let config_2 = configApi.getConfig();
expect(config_2.foo).toEqual(config_0.foo);
configApi.setConfig({ foobar: 'bar0' });
let config_3 = configApi.getConfig();
expect(config_3.foobar).toEqual('bar0');
configApi.reset();
let config_4 = configApi.getConfig();
expect(config_4.foobar).toBeUndefined();
});
});

View File

@@ -0,0 +1,72 @@
import * as configApi from './config.js';
describe('when working with site config', function () {
beforeEach(() => {
// Resets the site config to default config
configApi.setSiteConfig({});
});
it('should set site config and config properly', function () {
const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config_0);
const config_1 = configApi.getSiteConfig();
const config_2 = configApi.getConfig();
expect(config_1.fontFamily).toEqual(config_0.fontFamily);
expect(config_1.fontSize).toEqual(config_0.fontSize);
expect(config_1).toEqual(config_2);
});
it('should respect secure keys when applying directives', function () {
const config_0 = {
fontFamily: 'foo-font',
fontSize: 12345, // can't be changed
secure: [...configApi.defaultConfig.secure!, 'fontSize'],
};
configApi.setSiteConfig(config_0);
const directive = { fontFamily: 'baf', fontSize: 54321 /* fontSize shouldn't be changed */ };
const cfg = configApi.updateCurrentConfig(config_0, [directive]);
expect(cfg.fontFamily).toEqual(directive.fontFamily);
expect(cfg.fontSize).toBe(config_0.fontSize);
});
it('should allow setting partial options', function () {
const defaultConfig = configApi.getConfig();
configApi.setConfig({
quadrantChart: {
chartHeight: 600,
},
});
const updatedConfig = configApi.getConfig();
// deep options we didn't update should remain the same
expect(defaultConfig.quadrantChart!.chartWidth).toEqual(
updatedConfig.quadrantChart!.chartWidth
);
});
it('should set reset config properly', function () {
const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config_0);
const config_1 = { fontFamily: 'baf' };
configApi.setConfig(config_1);
const config_2 = configApi.getConfig();
expect(config_2.fontFamily).toEqual(config_1.fontFamily);
configApi.reset();
const config_3 = configApi.getConfig();
expect(config_3.fontFamily).toEqual(config_0.fontFamily);
const config_4 = configApi.getSiteConfig();
expect(config_4.fontFamily).toEqual(config_0.fontFamily);
});
it('should set global reset config properly', function () {
const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config_0);
const config_1 = configApi.getSiteConfig();
expect(config_1.fontFamily).toEqual(config_0.fontFamily);
const config_2 = configApi.getConfig();
expect(config_2.fontFamily).toEqual(config_0.fontFamily);
configApi.setConfig({ altFontFamily: 'bar-font' });
const config_3 = configApi.getConfig();
expect(config_3.altFontFamily).toEqual('bar-font');
configApi.reset();
const config_4 = configApi.getConfig();
expect(config_4.altFontFamily).toBeUndefined();
});
});

View File

@@ -226,9 +226,11 @@ export const reset = (config = siteConfig): void => {
updateCurrentConfig(config, directives);
};
enum ConfigWarning {
'LAZY_LOAD_DEPRECATED' = 'The configuration options lazyLoadedDiagrams and loadExternalDiagramsAtStartup are deprecated. Please use registerExternalDiagrams instead.',
}
const ConfigWarning = {
LAZY_LOAD_DEPRECATED:
'The configuration options lazyLoadedDiagrams and loadExternalDiagramsAtStartup are deprecated. Please use registerExternalDiagrams instead.',
} as const;
type ConfigWarningStrings = keyof typeof ConfigWarning;
const issuedWarnings: { [key in ConfigWarningStrings]?: boolean } = {};
const issueWarning = (warning: ConfigWarningStrings) => {

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
# Cluster handling
Dagre does not support edges between nodes and clusters or between clusters to other clusters. In order to remedy this shortcoming the dagre wrapper implements a few work-arounds.
Dagre does not support edges between nodes and clusters or between clusters to other clusters. In order to remedy this shortcoming the dagre wrapper implements a few workarounds.
In the diagram below there are two clusters and there are no edges to nodes outside the own cluster.
@@ -73,7 +73,7 @@ Sample object:
}
```
This is set by the renderer of the diagram and insert the data that the wrapper neds for rendering.
This is set by the renderer of the diagram and insert the data that the wrapper needs for rendering.
| property | description |
| ---------- | ------------------------------------------------------------------------------------------------ |
@@ -114,7 +114,7 @@ Required edgeData for proper rendering:
| label | overlap between label and labelText? |
| labelPos | |
| labelType | overlap between label and labelText? |
| thickness | Sets the thinkess of the edge. Can be \['normal', 'thick'\] |
| thickness | Sets the thickness of the edge. Can be \['normal', 'thick'\] |
| pattern | Sets the pattern of the edge. Can be \['solid', 'dotted', 'dashed'\] |
# Markers

View File

@@ -602,6 +602,8 @@ const doublecircle = async (parent, node) => {
const outerCircle = circleGroup.insert('circle');
const innerCircle = circleGroup.insert('circle');
circleGroup.attr('class', node.class);
// center the circle around its coordinate
outerCircle
.attr('style', node.style)

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@ import errorDiagram from '../diagrams/error/errorDiagram.js';
import flowchartElk from '../diagrams/flowchart/elk/detector.js';
import timeline from '../diagrams/timeline/detector.js';
import mindmap from '../diagrams/mindmap/detector.js';
import sankey from '../diagrams/sankey/sankeyDetector.js';
import { registerLazyLoadedDiagrams } from './detectType.js';
import { registerDiagram } from './diagramAPI.js';
@@ -79,6 +80,7 @@ export const addDiagrams = () => {
stateV2,
state,
journey,
quadrantChart
quadrantChart,
sankey
);
};

View File

@@ -7,7 +7,6 @@ import { addStylesForDiagram } from '../styles.js';
import { DiagramDefinition, DiagramDetector } from './types.js';
import * as _commonDb from '../commonDb.js';
import { parseDirective as _parseDirective } from '../directiveUtils.js';
import isEmpty from 'lodash-es/isEmpty.js';
/*
Packaging and exposing resources for external diagrams so that they can import
@@ -51,9 +50,7 @@ export const registerDiagram = (
if (detector) {
addDetector(id, detector);
}
if (!isEmpty(diagram.styles)) {
addStylesForDiagram(id, diagram.styles);
}
addStylesForDiagram(id, diagram.styles);
if (diagram.injectUtils) {
diagram.injectUtils(

View File

@@ -82,3 +82,5 @@ export type ParseDirectiveDefinition = (statement: string, context: string, type
export type HTML = d3.Selection<HTMLIFrameElement, unknown, Element, unknown>;
export type SVG = d3.Selection<SVGSVGElement, unknown, Element, unknown>;
export type DiagramStylesProvider = (options?: any) => string;

View File

@@ -220,7 +220,7 @@ export const drawC4ShapeArray = function (currentBounds, diagram, c4ShapeArray,
let c4ShapeTypeConf = c4ShapeFont(conf, c4Shape.typeC4Shape.text);
c4ShapeTypeConf.fontSize = c4ShapeTypeConf.fontSize - 2;
c4Shape.typeC4Shape.width = calculateTextWidth(
'<<' + c4Shape.typeC4Shape.text + '>>',
'«' + c4Shape.typeC4Shape.text + '»',
c4ShapeTypeConf
);
c4Shape.typeC4Shape.height = c4ShapeTypeConf.fontSize + 2;

View File

@@ -448,9 +448,8 @@ const getNamespaces = function (): NamespaceMap {
export const addClassesToNamespace = function (id: string, classNames: string[]) {
if (namespaces[id] !== undefined) {
classNames.map((className) => {
classes[className].parent = id;
namespaces[id].classes[className] = classes[className];
delete classes[className];
classCounter--;
});
}
};

View File

@@ -264,6 +264,113 @@ class C13["With Città foreign language"]
const str = 'classDiagram\n' + 'note "test"\n';
parser.parse(str);
});
const keywords = [
'direction',
'classDiagram',
'classDiagram-v2',
'namespace',
'{}',
'{',
'}',
'()',
'(',
')',
'[]',
'[',
']',
'class',
'\n',
'cssClass',
'callback',
'link',
'click',
'note',
'note for',
'<<',
'>>',
'call ',
'~',
'~Generic~',
'_self',
'_blank',
'_parent',
'_top',
'<|',
'|>',
'>',
'<',
'*',
'o',
'\\',
'--',
'..',
'-->',
'--|>',
': label',
':::',
'.',
'+',
'alphaNum',
'!',
'0123',
'function()',
'function(arg1, arg2)',
];
it.each(keywords)('should handle a note with %s in it', function (keyword: string) {
const str = `classDiagram
note "This is a keyword: ${keyword}. It truly is."
`;
parser.parse(str);
expect(classDb.getNotes()[0].text).toEqual(`This is a keyword: ${keyword}. It truly is.`);
});
it.each(keywords)(
'should handle note with %s at beginning of string',
function (keyword: string) {
const str = `classDiagram
note "${keyword}"`;
parser.parse(str);
expect(classDb.getNotes()[0].text).toEqual(`${keyword}`);
}
);
it.each(keywords)('should handle a "note for" with a %s in it', function (keyword: string) {
const str = `classDiagram
class Something {
int id
string name
}
note for Something "This is a keyword: ${keyword}. It truly is."
`;
parser.parse(str);
expect(classDb.getNotes()[0].text).toEqual(`This is a keyword: ${keyword}. It truly is.`);
});
it.each(keywords)(
'should handle a "note for" with a %s at beginning of string',
function (keyword: string) {
const str = `classDiagram
class Something {
int id
string name
}
note for Something "${keyword}"
`;
parser.parse(str);
expect(classDb.getNotes()[0].text).toEqual(`${keyword}`);
}
);
it.each(keywords)('should elicit error for %s after NOTE token', function (keyword: string) {
const str = `classDiagram
note ${keyword}`;
expect(() => parser.parse(str)).toThrowError(/(Expecting\s'STR'|Unrecognized\stext)/);
});
});
describe('when parsing class defined in brackets', function () {
@@ -1373,9 +1480,54 @@ class Class2
parser.parse(str);
const testNamespace = parser.yy.getNamespace('Namespace1');
const testClasses = parser.yy.getClasses();
expect(Object.keys(testNamespace.classes).length).toBe(2);
expect(Object.keys(testNamespace.children).length).toBe(0);
expect(testNamespace.classes['Class1'].id).toBe('Class1');
expect(Object.keys(testClasses).length).toBe(2);
});
it('should add relations between classes of different namespaces', function () {
const str = `classDiagram
A1 --> B1
namespace A {
class A1 {
+foo : string
}
class A2 {
+bar : int
}
}
namespace B {
class B1 {
+foo : bool
}
class B2 {
+bar : float
}
}
A2 --> B2`;
parser.parse(str);
const testNamespaceA = parser.yy.getNamespace('A');
const testNamespaceB = parser.yy.getNamespace('B');
const testClasses = parser.yy.getClasses();
const testRelations = parser.yy.getRelations();
expect(Object.keys(testNamespaceA.classes).length).toBe(2);
expect(testNamespaceA.classes['A1'].members[0]).toBe('+foo : string');
expect(testNamespaceA.classes['A2'].members[0]).toBe('+bar : int');
expect(Object.keys(testNamespaceB.classes).length).toBe(2);
expect(testNamespaceB.classes['B1'].members[0]).toBe('+foo : bool');
expect(testNamespaceB.classes['B2'].members[0]).toBe('+bar : float');
expect(Object.keys(testClasses).length).toBe(4);
expect(testClasses['A1'].parent).toBe('A');
expect(testClasses['A2'].parent).toBe('A');
expect(testClasses['B1'].parent).toBe('B');
expect(testClasses['B2'].parent).toBe('B');
expect(testRelations[0].id1).toBe('A1');
expect(testRelations[0].id2).toBe('B1');
expect(testRelations[1].id1).toBe('A2');
expect(testRelations[1].id2).toBe('B2');
});
});

View File

@@ -93,52 +93,51 @@ export const addClasses = function (
log.info(classes);
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
keys.forEach(function (id) {
const vertex = classes[id];
keys
.filter((id) => classes[id].parent == parent)
.forEach(function (id) {
const vertex = classes[id];
/**
* Variable for storing the classes for the vertex
*/
let cssClassStr = '';
if (vertex.cssClasses.length > 0) {
cssClassStr = cssClassStr + ' ' + vertex.cssClasses.join(' ');
}
/**
* Variable for storing the classes for the vertex
*/
const cssClassStr = vertex.cssClasses.join(' ');
const styles = { labelStyle: '', style: '' }; //getStylesFromArray(vertex.styles);
const styles = { labelStyle: '', style: '' }; //getStylesFromArray(vertex.styles);
// Use vertex id as text in the box if no text is provided by the graph definition
const vertexText = vertex.label ?? vertex.id;
const radius = 0;
const shape = 'class_box';
// Use vertex id as text in the box if no text is provided by the graph definition
const vertexText = vertex.label ?? vertex.id;
const radius = 0;
const shape = 'class_box';
// Add the node
const node = {
labelStyle: styles.labelStyle,
shape: shape,
labelText: sanitizeText(vertexText),
classData: vertex,
rx: radius,
ry: radius,
class: cssClassStr,
style: styles.style,
id: vertex.id,
domId: vertex.domId,
tooltip: diagObj.db.getTooltip(vertex.id, parent) || '',
haveCallback: vertex.haveCallback,
link: vertex.link,
width: vertex.type === 'group' ? 500 : undefined,
type: vertex.type,
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
padding: getConfig().flowchart?.padding ?? getConfig().class?.padding,
};
g.setNode(vertex.id, node);
// Add the node
const node = {
labelStyle: styles.labelStyle,
shape: shape,
labelText: sanitizeText(vertexText),
classData: vertex,
rx: radius,
ry: radius,
class: cssClassStr,
style: styles.style,
id: vertex.id,
domId: vertex.domId,
tooltip: diagObj.db.getTooltip(vertex.id, parent) || '',
haveCallback: vertex.haveCallback,
link: vertex.link,
width: vertex.type === 'group' ? 500 : undefined,
type: vertex.type,
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
padding: getConfig().flowchart?.padding ?? getConfig().class?.padding,
};
g.setNode(vertex.id, node);
if (parent) {
g.setParent(vertex.id, parent);
}
if (parent) {
g.setParent(vertex.id, parent);
}
log.info('setNode', node);
});
log.info('setNode', node);
});
};
/**

View File

@@ -141,8 +141,6 @@ const insertMarkers = function (elem) {
export const draw = function (text, id, _version, diagObj) {
const conf = getConfig().class;
idCache = {};
// diagObj.db.clear();
// diagObj.parser.parse(text);
log.info('Rendering diagram ' + text);

View File

@@ -7,6 +7,7 @@ export interface ClassNode {
members: string[];
annotations: string[];
domId: string;
parent?: string;
link?: string;
linkTarget?: string;
haveCallback?: boolean;

View File

@@ -24,31 +24,50 @@
%x namespace
%x namespace-body
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
.*direction\s+TB[^\n]* return 'direction_tb';
.*direction\s+BT[^\n]* return 'direction_bt';
.*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr';
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%\%(?!\{)*[^\n]*(\r?\n?)+ /* skip comments */
\%\%[^\n]*(\r?\n)* /* skip comments */
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
<acc_descr_multiline>[\}] { this.popState(); }
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
.*direction\s+TB[^\n]* return 'direction_tb';
.*direction\s+BT[^\n]* return 'direction_bt';
.*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr';
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%\%(?!\{)*[^\n]*(\r?\n?)+ /* skip comments */
\%\%[^\n]*(\r?\n)* /* skip comments */
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
<acc_descr_multiline>[\}] { this.popState(); }
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
\s*(\r?\n)+ return 'NEWLINE';
\s+ /* skip whitespace */
\s*(\r?\n)+ return 'NEWLINE';
\s+ /* skip whitespace */
"classDiagram-v2" return 'CLASS_DIAGRAM';
"classDiagram" return 'CLASS_DIAGRAM';
"[*]" return 'EDGE_STATE';
"classDiagram-v2" return 'CLASS_DIAGRAM';
"classDiagram" return 'CLASS_DIAGRAM';
"[*]" return 'EDGE_STATE';
/*
---interactivity command---
'call' adds a callback to the specified node. 'call' can only be specified when
the line was introduced with 'click'.
'call <callback_name>(<callback_args>)' attaches the function 'callback_name' with the specified
arguments to the node that was specified by 'click'.
Function arguments are optional: 'call <callback_name>()' simply executes 'callback_name' without any arguments.
*/
<INITIAL>"call"[\s]+ this.begin("callback_name");
<callback_name>\([\s]*\) this.popState();
<callback_name>\( this.popState(); this.begin("callback_args");
<callback_name>[^(]* return 'CALLBACK_NAME';
<callback_args>\) this.popState();
<callback_args>[^)]* return 'CALLBACK_ARGS';
<string>["] this.popState();
<string>[^"]* return "STR";
<*>["] this.begin("string");
<INITIAL,namespace>"namespace" { this.begin('namespace'); return 'NAMESPACE'; }
<namespace>\s*(\r?\n)+ { this.popState(); return 'NEWLINE'; }
@@ -60,26 +79,26 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
<namespace-body>\s+ /* skip whitespace */
<namespace-body>"[*]" return 'EDGE_STATE';
<INITIAL,namespace-body>"class" { this.begin('class'); return 'CLASS';}
<class>\s*(\r?\n)+ { this.popState(); return 'NEWLINE'; }
<class>\s+ /* skip whitespace */
<class>[}] { this.popState(); this.popState(); return 'STRUCT_STOP';}
<class>[{] { this.begin("class-body"); return 'STRUCT_START';}
<class-body>[}] { this.popState(); return 'STRUCT_STOP'; }
<class-body><<EOF>> return "EOF_IN_STRUCT";
<class-body>"[*]" { return 'EDGE_STATE';}
<class-body>[{] return "OPEN_IN_STRUCT";
<class-body>[\n] /* nothing */
<class-body>[^{}\n]* { return "MEMBER";}
<INITIAL,namespace-body>"class" { this.begin('class'); return 'CLASS';}
<class>\s*(\r?\n)+ { this.popState(); return 'NEWLINE'; }
<class>\s+ /* skip whitespace */
<class>[}] { this.popState(); this.popState(); return 'STRUCT_STOP';}
<class>[{] { this.begin("class-body"); return 'STRUCT_START';}
<class-body>[}] { this.popState(); return 'STRUCT_STOP'; }
<class-body><<EOF>> return "EOF_IN_STRUCT";
<class-body>"[*]" { return 'EDGE_STATE';}
<class-body>[{] return "OPEN_IN_STRUCT";
<class-body>[\n] /* nothing */
<class-body>[^{}\n]* { return "MEMBER";}
<*>"cssClass" return 'CSSCLASS';
<*>"callback" return 'CALLBACK';
<*>"link" return 'LINK';
<*>"click" return 'CLICK';
<*>"note for" return 'NOTE_FOR';
<*>"note" return 'NOTE';
<*>"<<" return 'ANNOTATION_START';
<*>">>" return 'ANNOTATION_END';
<*>"cssClass" return 'CSSCLASS';
<*>"callback" return 'CALLBACK';
<*>"link" return 'LINK';
<*>"click" return 'CLICK';
<*>"note for" return 'NOTE_FOR';
<*>"note" return 'NOTE';
<*>"<<" return 'ANNOTATION_START';
<*>">>" return 'ANNOTATION_END';
/*
---interactivity command---
@@ -87,64 +106,43 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
line was introduced with 'click'.
'href "<link>"' attaches the specified link to the node that was specified by 'click'.
*/
<*>"href"[\s]+["] this.begin("href");
<href>["] this.popState();
<href>[^"]* return 'HREF';
<*>"href" return 'HREF';
/*
---interactivity command---
'call' adds a callback to the specified node. 'call' can only be specified when
the line was introduced with 'click'.
'call <callback_name>(<callback_args>)' attaches the function 'callback_name' with the specified
arguments to the node that was specified by 'click'.
Function arguments are optional: 'call <callback_name>()' simply executes 'callback_name' without any arguments.
*/
<*>"call"[\s]+ this.begin("callback_name");
<callback_name>\([\s]*\) this.popState();
<callback_name>\( this.popState(); this.begin("callback_args");
<callback_name>[^(]* return 'CALLBACK_NAME';
<callback_args>\) this.popState();
<callback_args>[^)]* return 'CALLBACK_ARGS';
<generic>[~] this.popState();
<generic>[^~]* return "GENERICTYPE";
<*>"~" this.begin("generic");
<generic>[~] this.popState();
<generic>[^~]* return "GENERICTYPE";
<*>[~] this.begin("generic");
<bqstring>[`] this.popState();
<bqstring>[^`]+ return "BQUOTE_STR";
<*>[`] this.begin("bqstring");
<string>["] this.popState();
<string>[^"]* return "STR";
<*>["] this.begin("string");
<*>"_self" return 'LINK_TARGET';
<*>"_blank" return 'LINK_TARGET';
<*>"_parent" return 'LINK_TARGET';
<*>"_top" return 'LINK_TARGET';
<bqstring>[`] this.popState();
<bqstring>[^`]+ return "BQUOTE_STR";
<*>[`] this.begin("bqstring");
<*>"_self" return 'LINK_TARGET';
<*>"_blank" return 'LINK_TARGET';
<*>"_parent" return 'LINK_TARGET';
<*>"_top" return 'LINK_TARGET';
<*>\s*\<\| return 'EXTENSION';
<*>\s*\|\> return 'EXTENSION';
<*>\s*\> return 'DEPENDENCY';
<*>\s*\< return 'DEPENDENCY';
<*>\s*\* return 'COMPOSITION';
<*>\s*o return 'AGGREGATION';
<*>\s*\(\) return 'LOLLIPOP';
<*>\-\- return 'LINE';
<*>\.\. return 'DOTTED_LINE';
<*>":"{1}[^:\n;]+ return 'LABEL';
<*>":"{3} return 'STYLE_SEPARATOR';
<*>\- return 'MINUS';
<*>"." return 'DOT';
<*>\+ return 'PLUS';
<*>\% return 'PCT';
<*>"=" return 'EQUALS';
<*>\= return 'EQUALS';
<*>\w+ return 'ALPHA';
<*>"[" return 'SQS';
<*>"]" return 'SQE';
<*>[!"#$%&'*+,-.`?\\/] return 'PUNCTUATION';
<*>[0-9]+ return 'NUM';
<*>\s*\<\| return 'EXTENSION';
<*>\s*\|\> return 'EXTENSION';
<*>\s*\> return 'DEPENDENCY';
<*>\s*\< return 'DEPENDENCY';
<*>\s*\* return 'COMPOSITION';
<*>\s*o return 'AGGREGATION';
<*>\s*\(\) return 'LOLLIPOP';
<*>\-\- return 'LINE';
<*>\.\. return 'DOTTED_LINE';
<*>":"{1}[^:\n;]+ return 'LABEL';
<*>":"{3} return 'STYLE_SEPARATOR';
<*>\- return 'MINUS';
<*>"." return 'DOT';
<*>\+ return 'PLUS';
<*>\% return 'PCT';
<*>"=" return 'EQUALS';
<*>\= return 'EQUALS';
<*>\w+ return 'ALPHA';
<*>"[" return 'SQS';
<*>"]" return 'SQE';
<*>[!"#$%&'*+,-.`?\\/] return 'PUNCTUATION';
<*>[0-9]+ return 'NUM';
<*>[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|
[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|
[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|
@@ -206,9 +204,9 @@ Function arguments are optional: 'call <callback_name>()' simply executes 'callb
[\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC]|
[\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]|
[\uFFD2-\uFFD7\uFFDA-\uFFDC]
return 'UNICODE_TEXT';
<*>\s return 'SPACE';
<*><<EOF>> return 'EOF';
return 'UNICODE_TEXT';
<*>\s return 'SPACE';
<*><<EOF>> return 'EOF';
/lex
@@ -321,7 +319,7 @@ classStatements
;
classStatement
: classIdentifier
: classIdentifier
| classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);}
| classIdentifier STRUCT_START members STRUCT_STOP {yy.addMembers($1,$3);}
| classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.setCssClass($1, $3);yy.addMembers($1,$5);}
@@ -391,10 +389,10 @@ clickStatement
| CLICK className CALLBACK_NAME STR {$$ = $1;yy.setClickEvent($2, $3);yy.setTooltip($2, $4);}
| CLICK className CALLBACK_NAME CALLBACK_ARGS {$$ = $1;yy.setClickEvent($2, $3, $4);}
| CLICK className CALLBACK_NAME CALLBACK_ARGS STR {$$ = $1;yy.setClickEvent($2, $3, $4);yy.setTooltip($2, $5);}
| CLICK className HREF {$$ = $1;yy.setLink($2, $3);}
| CLICK className HREF LINK_TARGET {$$ = $1;yy.setLink($2, $3, $4);}
| CLICK className HREF STR {$$ = $1;yy.setLink($2, $3);yy.setTooltip($2, $4);}
| CLICK className HREF STR LINK_TARGET {$$ = $1;yy.setLink($2, $3, $5);yy.setTooltip($2, $4);}
| CLICK className HREF STR {$$ = $1;yy.setLink($2, $4);}
| CLICK className HREF STR LINK_TARGET {$$ = $1;yy.setLink($2, $4, $5);}
| CLICK className HREF STR STR {$$ = $1;yy.setLink($2, $4);yy.setTooltip($2, $5);}
| CLICK className HREF STR STR LINK_TARGET {$$ = $1;yy.setLink($2, $4, $6);yy.setTooltip($2, $5);}
;
cssClassStatement

View File

@@ -568,13 +568,6 @@ export const draw = function (text, id, _version, diagObj) {
: select('body');
// const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
// Parse the text to populate erDb
// try {
// parser.parse(text);
// } catch (err) {
// log.debug('Parsing failed');
// }
// Get a reference to the svg node that contains the text
const svg = root.select(`[id='${id}']`);

View File

@@ -30,7 +30,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
<block>\s+ /* skip whitespace in block */
<block>\b((?:PK)|(?:FK)|(?:UK))\b return 'ATTRIBUTE_KEY'
<block>(.*?)[~](.*?)*[~] return 'ATTRIBUTE_WORD';
<block>[A-Za-z_][A-Za-z0-9\-_\[\]\(\)]* return 'ATTRIBUTE_WORD'
<block>[\*A-Za-z_][A-Za-z0-9\-_\[\]\(\)]* return 'ATTRIBUTE_WORD'
<block>\"[^"]*\" return 'COMMENT';
<block>[\n]+ /* nothing */
<block>"}" { this.popState(); return 'BLOCK_STOP'; }

View File

@@ -154,6 +154,16 @@ describe('when parsing ER diagram it...', function () {
expect(entities[entity].attributes[2].attributeName).toBe('author-ref[name](1)');
});
it('should allow asterisk at the start of title', function () {
const entity = 'BOOK';
const attribute = 'string *title';
erDiagram.parser.parse(`erDiagram\n${entity}{${attribute}}`);
const entities = erDb.getEntities();
expect(Object.keys(entities).length).toBe(1);
expect(entities[entity].attributes.length).toBe(1);
});
it('should not allow leading numbers, dashes or brackets', function () {
const entity = 'BOOK';
const nonLeadingChars = '0-[]()';

View File

@@ -208,21 +208,22 @@ export const updateLink = function (positions, style) {
});
};
export const addClass = function (id, style) {
if (classes[id] === undefined) {
classes[id] = { id: id, styles: [], textStyles: [] };
}
export const addClass = function (ids, style) {
ids.split(',').forEach(function (id) {
if (classes[id] === undefined) {
classes[id] = { id, styles: [], textStyles: [] };
}
if (style !== undefined && style !== null) {
style.forEach(function (s) {
if (s.match('color')) {
const newStyle1 = s.replace('fill', 'bgFill');
const newStyle2 = newStyle1.replace('color', 'fill');
classes[id].textStyles.push(newStyle2);
}
classes[id].styles.push(s);
});
}
if (style !== undefined && style !== null) {
style.forEach(function (s) {
if (s.match('color')) {
const newStyle = s.replace('fill', 'bgFill').replace('color', 'fill');
classes[id].textStyles.push(newStyle);
}
classes[id].styles.push(s);
});
}
});
};
/**
@@ -341,7 +342,10 @@ export const setLink = function (ids, linkStr, target) {
setClass(ids, 'clickable');
};
export const getTooltip = function (id) {
return tooltips[id];
if (tooltips.hasOwnProperty(id)) {
return tooltips[id];
}
return undefined;
};
/**
@@ -442,7 +446,7 @@ export const clear = function (ver = 'gen-1') {
subGraphs = [];
subGraphLookup = {};
subCount = 0;
tooltips = [];
tooltips = {};
firstGraphFlag = true;
version = ver;
commonClear();

View File

@@ -41,3 +41,26 @@ describe('flow db subgraphs', () => {
});
});
});
describe('flow db addClass', () => {
beforeEach(() => {
flowDb.clear();
});
it('should detect many classes', () => {
flowDb.addClass('a,b', ['stroke-width: 8px']);
const classes = flowDb.getClasses();
expect(classes.hasOwnProperty('a')).toBe(true);
expect(classes.hasOwnProperty('b')).toBe(true);
expect(classes['a']['styles']).toEqual(['stroke-width: 8px']);
expect(classes['b']['styles']).toEqual(['stroke-width: 8px']);
});
it('should detect single class', () => {
flowDb.addClass('a', ['stroke-width: 8px']);
const classes = flowDb.getClasses();
expect(classes.hasOwnProperty('a')).toBe(true);
expect(classes['a']['styles']).toEqual(['stroke-width: 8px']);
});
});

View File

@@ -306,13 +306,6 @@ export const draw = function (text, id, _version, diagObj) {
: select('body');
const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
// Parse the graph definition
try {
diagObj.parser.parse(text);
} catch (err) {
log.debug('Parsing failed');
}
// Fetch the default direction, use TD if none was found
let dir = diagObj.db.getDirection();
if (dir === undefined) {

View File

@@ -113,6 +113,22 @@ describe('[Style] when parsing', () => {
expect(classes['exClass'].styles[1]).toBe('border:1px solid red');
});
it('should be possible to declare multiple classes', function () {
const res = flow.parser.parse(
'graph TD;classDef firstClass,secondClass background:#bbb,border:1px solid red;'
);
const classes = flow.parser.yy.getClasses();
expect(classes['firstClass'].styles.length).toBe(2);
expect(classes['firstClass'].styles[0]).toBe('background:#bbb');
expect(classes['firstClass'].styles[1]).toBe('border:1px solid red');
expect(classes['secondClass'].styles.length).toBe(2);
expect(classes['secondClass'].styles[0]).toBe('background:#bbb');
expect(classes['secondClass'].styles[1]).toBe('border:1px solid red');
});
it('should be possible to declare a class with a dot in the style', function () {
const res = flow.parser.parse(
'graph TD;classDef exClass background:#bbb,border:1.5px solid red;'
@@ -322,4 +338,20 @@ describe('[Style] when parsing', () => {
expect(edges[0].type).toBe('arrow_point');
});
it('should handle multiple vertices with style', function () {
const res = flow.parser.parse(`
graph TD
classDef C1 stroke-dasharray:4
classDef C2 stroke-dasharray:6
A & B:::C1 & D:::C1 --> E:::C2
`);
const vert = flow.parser.yy.getVertices();
expect(vert['A'].classes.length).toBe(0);
expect(vert['B'].classes[0]).toBe('C1');
expect(vert['D'].classes[0]).toBe('C1');
expect(vert['E'].classes[0]).toBe('C2');
});
});

View File

@@ -359,7 +359,7 @@ statement
separator: NEWLINE | SEMI | EOF ;
verticeStatement: verticeStatement link node
{ /* console.warn('vs',$1.stmt,$3); */ yy.addLink($1.stmt,$3,$2); $$ = { stmt: $3, nodes: $3.concat($1.nodes) } }
| verticeStatement link node spaceList
@@ -368,12 +368,16 @@ verticeStatement: verticeStatement link node
|node { /*console.warn('noda', $1);*/ $$ = {stmt: $1, nodes:$1 }}
;
node: vertex
node: styledVertex
{ /* console.warn('nod', $1); */ $$ = [$1];}
| node spaceList AMP spaceList vertex
| node spaceList AMP spaceList styledVertex
{ $$ = $1.concat($5); /* console.warn('pip', $1[0], $5, $$); */ }
;
styledVertex: vertex
{ /* console.warn('nod', $1); */ $$ = $1;}
| vertex STYLE_SEPARATOR idString
{$$ = [$1];yy.setClass($1,$3)}
{$$ = $1;yy.setClass($1,$3)}
;
vertex: idString SQS text SQE

View File

@@ -59,8 +59,6 @@ let w;
export const draw = function (text, id, version, diagObj) {
const conf = getConfig().gantt;
// diagObj.db.clear();
// parser.parse(text);
const securityLevel = getConfig().securityLevel;
// Handle root and Document for when rendering in sandbox mode
let sandboxElement;

View File

@@ -1,7 +1,6 @@
import { curveBasis, line, select } from 'd3';
import db from './gitGraphAst.js';
import gitGraphParser from './parser/gitGraph.js';
import { logger } from '../../logger.js';
import { interpolateToCurve } from '../../utils.js';
@@ -328,13 +327,7 @@ function renderLines(svg, commit, direction, branchColor = 0) {
export const draw = function (txt, id, ver) {
try {
const parser = gitGraphParser.parser;
parser.yy = db;
parser.yy.clear();
logger.debug('in gitgraph renderer', txt + '\n', 'id:', id, ver);
// Parse the graph definition
parser.parse(txt + '\n');
config = Object.assign(config, apiConfig, db.getOptions());
logger.debug('effective options', config);

View File

@@ -167,14 +167,8 @@ function positionNodes(cy) {
export const draw = async (text, id, version, diagObj) => {
const conf = getConfig();
// console.log('Config: ', conf);
conf.htmlLabels = false;
// This is done only for throwing the error if the text is not valid.
diagObj.db.clear();
// Parse the graph definition
diagObj.parser.parse(text);
log.debug('Rendering mindmap diagram\n' + text, diagObj.parser);
const securityLevel = getConfig().securityLevel;

View File

@@ -33,9 +33,6 @@ export const draw = (txt, id, _version, diagObj) => {
const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
// Parse the Pie Chart definition
diagObj.db.clear();
diagObj.parser.parse(txt);
log.debug('Parsed info diagram');
const elem = doc.getElementById(id);
width = elem.parentElement.offsetWidth;

View File

@@ -1,7 +1,7 @@
// @ts-ignore: TODO Fix ts errors
import { scaleLinear } from 'd3';
import { log } from '../../logger.js';
import { QuadrantChartConfig } from '../../config.type.js';
import type { BaseDiagramConfig, QuadrantChartConfig } from '../../config.type.js';
import defaultConfig from '../../defaultConfig.js';
import { getThemeVariables } from '../../themes/theme-default.js';
@@ -71,7 +71,8 @@ export interface quadrantBuilderData {
points: QuadrantPointInputType[];
}
export interface QuadrantBuilderConfig extends QuadrantChartConfig {
export interface QuadrantBuilderConfig
extends Required<Omit<QuadrantChartConfig, keyof BaseDiagramConfig>> {
showXAxis: boolean;
showYAxis: boolean;
showTitle: boolean;

View File

@@ -306,8 +306,6 @@ const elementString = (str) => {
export const draw = (text, id, _version, diagObj) => {
conf = getConfig().requirement;
diagObj.db.clear();
diagObj.parser.parse(text);
const securityLevel = conf.securityLevel;
// Handle root and Document for when rendering in sandbox mode

View File

@@ -0,0 +1,99 @@
%% There are leading and trailing spaces, do not crop
Agricultural 'waste',Bio-conversion,124.729
%% line with a comment
%% Normal line
Bio-conversion,Liquid,0.597
%% Line with unquoted sankey keyword
sankey,target,10
%% Quoted sankey keyword
"sankey",target,10
%% Another normal line
Bio-conversion,Losses,26.862
%% Line with integer amount
Bio-conversion,Solid,280
%% Some blank lines in the middle of CSV
%% Another normal line
Bio-conversion,Gas,81.144
%% Quoted line
"Biofuel imports",Liquid,35
%% Quoted line with escaped quotes inside
"""Biomass imports""",Solid,35
%% Lines containing commas inside
%% Quoted and unquoted values should be equal in terms of graph
"District heating","Heating and cooling, commercial",22.505
District heating,"Heating and cooling, homes",46.184
%% A bunch of lines, normal CSV
Coal imports,Coal,11.606
Coal reserves,Coal,63.965
Coal,Solid,75.571
District heating,Industry,10.639
Electricity grid,Over generation / exports,104.453
Electricity grid,Heating and cooling - homes,113.726
Electricity grid,H2 conversion,27.14
Electricity grid,Industry,342.165
Electricity grid,Road transport,37.797
Electricity grid,Agriculture,4.412
Electricity grid,Heating and cooling - commercial,40.858
Electricity grid,Losses,56.691
Electricity grid,Rail transport,7.863
Electricity grid,Lighting & appliances - commercial,90.008
Electricity grid,Lighting & appliances - homes,93.494
Gas imports,Ngas,40.719
Gas reserves,Ngas,82.233
Gas,Heating and cooling - commercial,0.129
Gas,Losses,1.401
Gas,Thermal generation,151.891
Gas,Agriculture,2.096
Gas,Industry,48.58
Geothermal,Electricity grid,7.013
H2 conversion,H2,20.897
H2 conversion,Losses,6.242
H2,Road transport,20.897
Hydro,Electricity grid,6.995
Liquid,Industry,121.066
Liquid,International shipping,128.69
Liquid,Road transport,135.835
Liquid,Domestic aviation,14.458
Liquid,International aviation,206.267
Liquid,Agriculture,3.64
Liquid,National navigation,33.218
Liquid,Rail transport,4.413
Marine algae,Bio-conversion,4.375
Ngas,Gas,122.952
Nuclear,Thermal generation,839.978
Oil imports,Oil,504.287
Oil reserves,Oil,107.703
Oil,Liquid,611.99
Other waste,Solid,56.587
Other waste,Bio-conversion,77.81
Pumped heat,Heating and cooling - homes,193.026
Pumped heat,Heating and cooling - commercial,70.672
Solar PV,Electricity grid,59.901
Solar Thermal,Heating and cooling - homes,19.263
Solar,Solar Thermal,19.263
Solar,Solar PV,59.901
Solid,Agriculture,0.882
Solid,Thermal generation,400.12
Solid,Industry,46.477
Thermal generation,Electricity grid,525.531
Thermal generation,Losses,787.129
Thermal generation,District heating,79.329
Tidal,Electricity grid,9.452
UK land based bioenergy,Bio-conversion,182.01
"""Wave""",Electricity grid,19.013
"""Wind""",Electricity grid,289.366
%% lines at the end, do not remove
Can't render this file because it has a wrong number of fields in line 2.

View File

@@ -0,0 +1,69 @@
/** mermaid */
//---------------------------------------------------------
// We support csv format as defined here:
// https://www.ietf.org/rfc/rfc4180.txt
// There are some minor changes for compliance with jison
// We also parse only 3 columns: source,target,value
// And allow blank lines for visual purposes
//---------------------------------------------------------
%lex
%options case-insensitive
%options easy_keword_rules
%x escaped_text
%x csv
// as per section 6.1 of RFC 2234 [2]
COMMA \u002C
CR \u000D
LF \u000A
CRLF \u000D\u000A
ESCAPED_QUOTE \u0022
DQUOTE \u0022
TEXTDATA [\u0020-\u0021\u0023-\u002B\u002D-\u007E]
%%
<INITIAL>"sankey-beta" { this.pushState('csv'); return 'SANKEY'; }
<INITIAL,csv><<EOF>> { return 'EOF' } // match end of file
<INITIAL,csv>({CRLF}|{LF}) { return 'NEWLINE' }
<INITIAL,csv>{COMMA} { return 'COMMA' }
<INITIAL,csv>{DQUOTE} { this.pushState('escaped_text'); return 'DQUOTE'; }
<INITIAL,csv>{TEXTDATA}* { return 'NON_ESCAPED_TEXT' }
<INITIAL,csv,escaped_text>{DQUOTE}(?!{DQUOTE}) {this.popState('escaped_text'); return 'DQUOTE'; } // unescaped DQUOTE closes string
<INITIAL,csv,escaped_text>({TEXTDATA}|{COMMA}|{CR}|{LF}|{DQUOTE}{DQUOTE})* { return 'ESCAPED_TEXT'; }
/lex
%start start
%% // language grammar
start: SANKEY NEWLINE csv opt_eof;
csv: record csv_tail;
csv_tail: NEWLINE csv | ;
opt_eof: EOF | ;
record
: field\[source] COMMA field\[target] COMMA field\[value] {
const source = yy.findOrCreateNode($source.trim().replaceAll('""', '"'));
const target = yy.findOrCreateNode($target.trim().replaceAll('""', '"'));
const value = parseFloat($value.trim());
yy.addLink(source,target,value);
} // parse only 3 fields, this is not part of CSV standard
;
field
: escaped { $$=$escaped; }
| non_escaped { $$=$non_escaped; }
;
escaped: DQUOTE ESCAPED_TEXT DQUOTE { $$=$ESCAPED_TEXT; };
non_escaped: NON_ESCAPED_TEXT { $$=$NON_ESCAPED_TEXT; };

View File

@@ -0,0 +1,24 @@
// @ts-ignore: jison doesn't export types
import sankey from './sankey.jison';
import db from '../sankeyDB.js';
import { cleanupComments } from '../../../diagram-api/comments.js';
import { prepareTextForParsing } from '../sankeyUtils.js';
import * as fs from 'fs';
import * as path from 'path';
describe('Sankey diagram', function () {
describe('when parsing an info graph it', function () {
beforeEach(function () {
sankey.parser.yy = db;
sankey.parser.yy.clear();
});
it('parses csv', async () => {
const csv = path.resolve(__dirname, './energy.csv');
const data = fs.readFileSync(csv, 'utf8');
const graphDefinition = prepareTextForParsing(cleanupComments('sankey-beta\n\n ' + data));
sankey.parser.parse(graphDefinition);
});
});
});

View File

@@ -0,0 +1,81 @@
import * as configApi from '../../config.js';
import common from '../common/common.js';
import {
setAccTitle,
getAccTitle,
getAccDescription,
setAccDescription,
setDiagramTitle,
getDiagramTitle,
clear as commonClear,
} from '../../commonDb.js';
// Sankey diagram represented by nodes and links between those nodes
let links: SankeyLink[] = [];
// Array of nodes guarantees their order
let nodes: SankeyNode[] = [];
// We also have to track nodes uniqueness (by ID)
let nodesMap: Record<string, SankeyNode> = {};
const clear = (): void => {
links = [];
nodes = [];
nodesMap = {};
commonClear();
};
class SankeyLink {
constructor(public source: SankeyNode, public target: SankeyNode, public value: number = 0) {}
}
/**
* @param source - Node where the link starts
* @param target - Node where the link ends
* @param value - number, float or integer, describes the amount to be passed
*/
const addLink = (source: SankeyNode, target: SankeyNode, value: number): void => {
links.push(new SankeyLink(source, target, value));
};
class SankeyNode {
constructor(public ID: string) {}
}
const findOrCreateNode = (ID: string): SankeyNode => {
ID = common.sanitizeText(ID, configApi.getConfig());
if (!nodesMap[ID]) {
nodesMap[ID] = new SankeyNode(ID);
nodes.push(nodesMap[ID]);
}
return nodesMap[ID];
};
const getNodes = () => nodes;
const getLinks = () => links;
const getGraph = () => ({
nodes: nodes.map((node) => ({ id: node.ID })),
links: links.map((link) => ({
source: link.source.ID,
target: link.target.ID,
value: link.value,
})),
});
export default {
nodesMap,
getConfig: () => configApi.getConfig().sankey,
getNodes,
getLinks,
getGraph,
addLink,
findOrCreateNode,
getAccTitle,
setAccTitle,
getAccDescription,
setAccDescription,
getDiagramTitle,
setDiagramTitle,
clear,
};

View File

@@ -0,0 +1,20 @@
import type { DiagramDetector, ExternalDiagramDefinition } from '../../diagram-api/types.js';
const id = 'sankey';
const detector: DiagramDetector = (txt) => {
return /^\s*sankey-beta/.test(txt);
};
const loader = async () => {
const { diagram } = await import('./sankeyDiagram.js');
return { id, diagram };
};
const plugin: ExternalDiagramDefinition = {
id,
detector,
loader,
};
export default plugin;

View File

@@ -0,0 +1,15 @@
import { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: jison doesn't export types
import parser from './parser/sankey.jison';
import db from './sankeyDB.js';
import renderer from './sankeyRenderer.js';
import { prepareTextForParsing } from './sankeyUtils.js';
const originalParse = parser.parse.bind(parser);
parser.parse = (text: string) => originalParse(prepareTextForParsing(text));
export const diagram: DiagramDefinition = {
parser,
db,
renderer,
};

View File

@@ -0,0 +1,205 @@
import { Diagram } from '../../Diagram.js';
import * as configApi from '../../config.js';
import {
select as d3select,
scaleOrdinal as d3scaleOrdinal,
schemeTableau10 as d3schemeTableau10,
} from 'd3';
import {
sankey as d3Sankey,
sankeyLinkHorizontal as d3SankeyLinkHorizontal,
sankeyLeft as d3SankeyLeft,
sankeyRight as d3SankeyRight,
sankeyCenter as d3SankeyCenter,
sankeyJustify as d3SankeyJustify,
SankeyNode as d3SankeyNode,
} from 'd3-sankey';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import { Uid } from '../../rendering-util/uid.js';
import type { SankeyLinkColor, SankeyNodeAlignment } from '../../config.type.js';
// Map config options to alignment functions
const alignmentsMap: Record<
SankeyNodeAlignment,
(node: d3SankeyNode<object, object>, n: number) => number
> = {
left: d3SankeyLeft,
right: d3SankeyRight,
center: d3SankeyCenter,
justify: d3SankeyJustify,
};
/**
* Draws Sankey diagram.
*
* @param text - The text of the diagram
* @param id - The id of the diagram which will be used as a DOM element id¨
* @param _version - Mermaid version from package.json
* @param diagObj - A standard diagram containing the db and the text and type etc of the diagram
*/
export const draw = function (text: string, id: string, _version: string, diagObj: Diagram): void {
// Get Sankey config
const { securityLevel, sankey: conf } = configApi.getConfig();
const defaultSankeyConfig = configApi!.defaultConfig!.sankey!;
// TODO:
// This code repeats for every diagram
// Figure out what is happening there, probably it should be separated
// The main thing is svg object that is a d3 wrapper for svg operations
//
let sandboxElement: any;
if (securityLevel === 'sandbox') {
sandboxElement = d3select('#i' + id);
}
const root =
securityLevel === 'sandbox'
? d3select(sandboxElement.nodes()[0].contentDocument.body)
: d3select('body');
// @ts-ignore TODO root.select is not callable
const svg = securityLevel === 'sandbox' ? root.select(`[id="${id}"]`) : d3select(`[id="${id}"]`);
// Establish svg dimensions and get width and height
//
const width = conf?.width || defaultSankeyConfig.width!;
const height = conf?.height || defaultSankeyConfig.width!;
const useMaxWidth = conf?.useMaxWidth || defaultSankeyConfig.useMaxWidth!;
const nodeAlignment = conf?.nodeAlignment || defaultSankeyConfig.nodeAlignment!;
// FIX: using max width prevents height from being set, is it intended?
// to add height directly one can use `svg.attr('height', height)`
//
// @ts-ignore TODO: svg type vs selection mismatch
configureSvgSize(svg, height, width, useMaxWidth);
// Prepare data for construction based on diagObj.db
// This must be a mutable object with `nodes` and `links` properties:
//
// {
// "nodes": [ { "id": "Alice" }, { "id": "Bob" }, { "id": "Carol" } ],
// "links": [ { "source": "Alice", "target": "Bob", "value": 23 }, { "source": "Bob", "target": "Carol", "value": 43 } ]
// }
//
// @ts-ignore TODO: db should be coerced to sankey DB type
const graph = diagObj.db.getGraph();
// Get alignment function
const nodeAlign = alignmentsMap[nodeAlignment];
// Construct and configure a Sankey generator
// That will be a function that calculates nodes and links dimensions
//
const nodeWidth = 10;
const sankey = d3Sankey()
.nodeId((d: any) => d.id) // we use 'id' property to identify node
.nodeWidth(nodeWidth)
.nodePadding(10)
.nodeAlign(nodeAlign)
.extent([
[0, 0],
[width, height],
]);
// Compute the Sankey layout: calculate nodes and links positions
// Our `graph` object will be mutated by this and enriched with other properties
//
sankey(graph);
// Get color scheme for the graph
const colorScheme = d3scaleOrdinal(d3schemeTableau10);
// Create rectangles for nodes
svg
.append('g')
.attr('class', 'nodes')
.selectAll('.node')
.data(graph.nodes)
.join('g')
.attr('class', 'node')
.attr('id', (d: any) => (d.uid = Uid.next('node-')).id)
.attr('transform', function (d: any) {
return 'translate(' + d.x0 + ',' + d.y0 + ')';
})
.attr('x', (d: any) => d.x0)
.attr('y', (d: any) => d.y0)
.append('rect')
.attr('height', (d: any) => {
return d.y1 - d.y0;
})
.attr('width', (d: any) => d.x1 - d.x0)
.attr('fill', (d: any) => colorScheme(d.id));
// Create labels for nodes
svg
.append('g')
.attr('class', 'node-labels')
.attr('font-family', 'sans-serif')
.attr('font-size', 14)
.selectAll('text')
.data(graph.nodes)
.join('text')
.attr('x', (d: any) => (d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6))
.attr('y', (d: any) => (d.y1 + d.y0) / 2)
.attr('dy', '0.35em')
.attr('text-anchor', (d: any) => (d.x0 < width / 2 ? 'start' : 'end'))
.text((d: any) => d.id);
// Creates the paths that represent the links.
const link = svg
.append('g')
.attr('class', 'links')
.attr('fill', 'none')
.attr('stroke-opacity', 0.5)
.selectAll('.link')
.data(graph.links)
.join('g')
.attr('class', 'link')
.style('mix-blend-mode', 'multiply');
const linkColor = conf?.linkColor || 'gradient';
if (linkColor === 'gradient') {
const gradient = link
.append('linearGradient')
.attr('id', (d: any) => (d.uid = Uid.next('linearGradient-')).id)
.attr('gradientUnits', 'userSpaceOnUse')
.attr('x1', (d: any) => d.source.x1)
.attr('x2', (d: any) => d.target.x0);
gradient
.append('stop')
.attr('offset', '0%')
.attr('stop-color', (d: any) => colorScheme(d.source.id));
gradient
.append('stop')
.attr('offset', '100%')
.attr('stop-color', (d: any) => colorScheme(d.target.id));
}
let coloring: any;
switch (linkColor) {
case 'gradient':
coloring = (d: any) => d.uid;
break;
case 'source':
coloring = (d: any) => colorScheme(d.source.id);
break;
case 'target':
coloring = (d: any) => colorScheme(d.target.id);
break;
default:
coloring = linkColor;
}
link
.append('path')
.attr('d', d3SankeyLinkHorizontal())
.attr('stroke', coloring)
.attr('stroke-width', (d: any) => Math.max(1, d.width));
};
export default {
draw,
};

View File

@@ -0,0 +1,8 @@
export const prepareTextForParsing = (text: string): string => {
const textToParse = text
.replaceAll(/^[^\S\n\r]+|[^\S\n\r]+$/g, '') // remove all trailing spaces for each row
.replaceAll(/([\n\r])+/g, '\n') // remove empty lines duplicated
.trim();
return textToParse;
};

View File

@@ -38,6 +38,8 @@
"box" { this.begin('LINE'); return 'box'; }
"participant" { this.begin('ID'); return 'participant'; }
"actor" { this.begin('ID'); return 'participant_actor'; }
"create" return 'create';
"destroy" { this.begin('ID'); return 'destroy'; }
<ID>[^\->:\n,;]+?([\-]*[^\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
<ALIAS>"as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; }
<ALIAS>(?:) { this.popState(); this.popState(); return 'NEWLINE'; }
@@ -138,6 +140,7 @@ directive
statement
: participant_statement
| 'create' participant_statement {$2.type='createParticipant'; $$=$2;}
| 'box' restOfLine box_section end
{
$3.unshift({type: 'boxStart', boxData:yy.parseBoxData($2) });
@@ -234,10 +237,11 @@ else_sections
;
participant_statement
: 'participant' actor 'AS' restOfLine 'NEWLINE' {$2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant' actor 'NEWLINE' {$2.type='addParticipant';$$=$2;}
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.type='addActor';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant_actor' actor 'NEWLINE' {$2.type='addActor'; $$=$2;}
: 'participant' actor 'AS' restOfLine 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant' actor 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant';$$=$2;}
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant_actor' actor 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;}
| 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;}
;
note_statement

Some files were not shown because too many files have changed in this diff Show More