Merge branch 'develop' into bug/4576-c4-support-for-ContainerQueue_Ext

This commit is contained in:
Dmitry Kisler 2023-07-13 12:07:46 +02:00 committed by GitHub
commit 584b51d7c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 4895 additions and 3273 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

@ -11,5 +11,7 @@ comment:
coverage:
status:
project:
default:
threshold: 2%
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

@ -47,13 +47,15 @@ jobs:
VITEST_COVERAGE: true
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v3
if: steps.cypress.conclusion == 'success'
# 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

@ -53,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

@ -43,9 +43,12 @@ jobs:
- 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,5 +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,6 +2,7 @@ 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';
@ -121,6 +122,7 @@ 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({

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
};
}
},
};
}

View File

@ -89,6 +89,8 @@
"mult",
"neurodiverse",
"nextra",
"nikolay",
"nirname",
"orlandoni",
"pathe",
"pbrolin",
@ -102,9 +104,11 @@
"ranksep",
"rect",
"rects",
"reda",
"redmine",
"rehype",
"roledescription",
"rozhkov",
"sandboxed",
"sankey",
"setupgraphviewbox",
@ -121,6 +125,7 @@
"stylis",
"subhash-halder",
"substate",
"sulais",
"sveidqvist",
"swimm",
"techn",
@ -144,6 +149,7 @@
"vueuse",
"xlink",
"yash",
"yokozuna",
"zenuml"
],
"patterns": [

View File

@ -17,7 +17,7 @@ services:
- 9000:9000
- 3333:3333
cypress:
image: cypress/included:12.16.0
image: cypress/included:12.17.0
stdin_open: true
tty: true
working_dir: /mermaid

View File

@ -14,7 +14,7 @@
#### Defined in
[defaultConfig.ts:2300](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L2300)
[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

@ -6,8 +6,8 @@
# Announcements
## [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/)
## [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)
15 June 2023 · 12 mins
29 June 2023 · 4 mins
Sequence diagrams really shine when youre documenting different parts of a system and the various ways these parts interact with each other.
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,12 @@
# 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

View File

@ -991,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

@ -164,156 +164,6 @@ Wave,Electricity grid,19.013
Wind,Electricity grid,289.366
```
::: details
```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.
@ -345,24 +195,6 @@ Electricity grid,Heating and cooling - homes,113.726
Electricity grid,H2 conversion,27.14
```
```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:
@ -387,26 +219,6 @@ Bio-conversion,Solid,280.322
Bio-conversion,Gas,81.144
```
```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:
@ -425,20 +237,6 @@ Pumped heat,"Heating and cooling, homes",193.026
Pumped heat,"Heating and cooling, commercial",70.672
```
```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:
@ -457,20 +255,6 @@ Pumped heat,"Heating and cooling, ""homes""",193.026
Pumped heat,"Heating and cooling, ""commercial""",70.672
```
```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.

View File

@ -78,9 +78,10 @@
"@types/rollup-plugin-visualizer": "^4.2.1",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitest/coverage-v8": "^0.32.2",
"@vitest/spy": "^0.32.2",
"@vitest/ui": "^0.32.2",
"@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",
@ -119,7 +120,7 @@
"typescript": "^5.1.3",
"vite": "^4.3.9",
"vite-plugin-istanbul": "^4.1.0",
"vitest": "^0.32.2"
"vitest": "^0.33.0"
},
"volta": {
"node": "18.16.1"

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

@ -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"
},
@ -74,6 +77,7 @@
"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",
@ -87,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",
@ -97,6 +102,7 @@
"jison": "^0.4.18",
"js-base64": "^3.7.5",
"jsdom": "^22.0.0",
"json-schema-to-typescript": "^11.0.3",
"micromatch": "^4.0.5",
"path-browserify": "^1.0.1",
"prettier": "^2.8.8",
@ -109,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

@ -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

File diff suppressed because it is too large Load Diff

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 () {

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

@ -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

@ -342,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;
};
/**
@ -443,7 +446,7 @@ export const clear = function (ver = 'gen-1') {
subGraphs = [];
subGraphLookup = {};
subCount = 0;
tooltips = [];
tooltips = {};
firstGraphFlag = true;
version = ver;
commonClear();

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

@ -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

@ -18,17 +18,17 @@ import {
} from 'd3-sankey';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import { Uid } from '../../rendering-util/uid.js';
import { SankeyLinkColor, SankeyNodeAlignment } from '../../config.type.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
> = {
[SankeyNodeAlignment.left]: d3SankeyLeft,
[SankeyNodeAlignment.right]: d3SankeyRight,
[SankeyNodeAlignment.center]: d3SankeyCenter,
[SankeyNodeAlignment.justify]: d3SankeyJustify,
left: d3SankeyLeft,
right: d3SankeyRight,
center: d3SankeyCenter,
justify: d3SankeyJustify,
};
/**
@ -157,9 +157,9 @@ export const draw = function (text: string, id: string, _version: string, diagOb
.attr('class', 'link')
.style('mix-blend-mode', 'multiply');
const linkColor = conf?.linkColor || SankeyLinkColor.gradient;
const linkColor = conf?.linkColor || 'gradient';
if (linkColor === SankeyLinkColor.gradient) {
if (linkColor === 'gradient') {
const gradient = link
.append('linearGradient')
.attr('id', (d: any) => (d.uid = Uid.next('linearGradient-')).id)
@ -180,13 +180,13 @@ export const draw = function (text: string, id: string, _version: string, diagOb
let coloring: any;
switch (linkColor) {
case SankeyLinkColor.gradient:
case 'gradient':
coloring = (d: any) => d.uid;
break;
case SankeyLinkColor.source:
case 'source':
coloring = (d: any) => colorScheme(d.source.id);
break;
case SankeyLinkColor.target:
case 'target':
coloring = (d: any) => colorScheme(d.target.id);
break;
default:

View File

@ -57,28 +57,12 @@ export const draw = function (text, id, _version, diagObj) {
: select('body');
const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
// diagObj.db.clear();
// parser.parse(text);
log.debug('Rendering diagram ' + text);
// Fetch the default direction, use TD if none was found
const diagram = root.select(`[id='${id}']`);
insertMarkers(diagram);
// Layout graph, Create a new directed graph
const graph = new graphlib.Graph({
multigraph: true,
compound: true,
// acyclicer: 'greedy',
rankdir: 'RL',
// ranksep: '20'
});
// Default to assigning a new object as a label for each new edge.
graph.setDefaultEdgeLabel(function () {
return {};
});
const rootDoc = diagObj.db.getRootDoc();
renderDoc(rootDoc, diagram, undefined, false, root, doc, diagObj);

View File

@ -30,12 +30,6 @@ export const draw = function (text: string, id: string, version: string, diagObj
// @ts-expect-error - wrong config?
const LEFT_MARGIN = conf.leftMargin ?? 50;
//2. Clear the diagram db before parsing
diagObj.db.clear?.();
//3. Parse the diagram text
diagObj.parser.parse(text + '\n');
log.debug('timeline', diagObj.db);
const securityLevel = conf.securityLevel;

View File

@ -49,8 +49,6 @@ const conf = getConfig().journey;
const LEFT_MARGIN = conf.leftMargin;
export const draw = function (text, id, version, diagObj) {
const conf = getConfig().journey;
diagObj.db.clear();
diagObj.parser.parse(text + '\n');
const securityLevel = getConfig().securityLevel;
// Handle root and Document for when rendering in sandbox mode

View File

@ -34,7 +34,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync, rmdirSync }
import { exec } from 'child_process';
import { globby } from 'globby';
import { JSDOM } from 'jsdom';
import type { Code, Root } from 'mdast';
import type { Code, ListItem, Root, Text } from 'mdast';
import { posix, dirname, relative, join } from 'path';
import prettier from 'prettier';
import { remark } from 'remark';
@ -44,6 +44,7 @@ import chokidar from 'chokidar';
import mm from 'micromatch';
// @ts-ignore No typescript declaration file
import flatmap from 'unist-util-flatmap';
import { visit } from 'unist-util-visit';
const MERMAID_MAJOR_VERSION = (
JSON.parse(readFileSync('../mermaid/package.json', 'utf8')).version as string
@ -122,7 +123,7 @@ const changeToFinalDocDir = (file: string): string => {
const logWasOrShouldBeTransformed = (filename: string, wasCopied: boolean) => {
const changeMsg = wasCopied ? LOGMSG_TRANSFORMED : LOGMSG_TO_BE_TRANSFORMED;
let logMsg: string;
logMsg = ` File ${changeMsg}: ${filename}`;
logMsg = ` File ${changeMsg}: ${filename.replace(FINAL_DOCS_DIR, SOURCE_DOCS_DIR)}`;
if (wasCopied) {
logMsg += LOGMSG_COPIED;
}
@ -150,6 +151,7 @@ const copyTransformedContents = (filename: string, doCopy = false, transformedCo
}
filesTransformed.add(fileInFinalDocDir);
if (doCopy) {
writeFileSync(fileInFinalDocDir, newBuffer);
}
@ -321,6 +323,123 @@ const transformMarkdown = (file: string) => {
copyTransformedContents(file, !verifyOnly, formatted);
};
import { load, JSON_SCHEMA } from 'js-yaml';
// @ts-ignore: we're importing internal jsonschema2md functions
import { default as schemaLoader } from '@adobe/jsonschema2md/lib/schemaProxy.js';
// @ts-ignore: we're importing internal jsonschema2md functions
import { default as traverseSchemas } from '@adobe/jsonschema2md/lib/traverseSchema.js';
// @ts-ignore: we're importing internal jsonschema2md functions
import { default as buildMarkdownFromSchema } from '@adobe/jsonschema2md/lib/markdownBuilder.js';
// @ts-ignore: we're importing internal jsonschema2md functions
import { default as jsonSchemaReadmeBuilder } from '@adobe/jsonschema2md/lib/readmeBuilder.js';
/**
* Transforms the given JSON Schema into Markdown documentation
*/
async function transormJsonSchema(file: string) {
const yamlContents = readSyncedUTF8file(file);
const jsonSchema = load(yamlContents, {
filename: file,
// 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,
});
/** Location of the `schema.yaml` files */
const SCHEMA_INPUT_DIR = 'src/schemas/';
/**
* Location to store the generated `schema.json` file for the website
*
* Because Vitepress doesn't handle bundling `.json` files properly, we need
* to instead place it into a `public/` subdirectory.
*/
const SCHEMA_OUTPUT_DIR = 'src/docs/public/schemas/';
const VITEPRESS_PUBLIC_DIR = 'src/docs/public';
/**
* Location to store the generated Schema Markdown docs.
* Links to JSON Schemas should automatically be rewritten to point to
* `SCHEMA_OUTPUT_DIR`.
*/
const SCHEMA_MARKDOWN_OUTPUT_DIR = join('src', 'docs', 'config', 'schema-docs');
// write .schema.json files
const jsonFileName = file
.replace('.schema.yaml', '.schema.json')
.replace(SCHEMA_INPUT_DIR, SCHEMA_OUTPUT_DIR);
copyTransformedContents(jsonFileName, !verifyOnly, JSON.stringify(jsonSchema, undefined, 2));
const schemas = traverseSchemas([schemaLoader()(jsonFileName, jsonSchema)]);
// ignore output of this function
// for some reason, without calling this function, we get some broken links
// this is probably a bug in @adobe/jsonschema2md
jsonSchemaReadmeBuilder({ readme: true })(schemas);
// write Markdown files
const markdownFiles = buildMarkdownFromSchema({
header: true,
// links,
includeProperties: ['tsType'], // Custom TypeScript type
exampleFormat: 'json',
// skipProperties,
/**
* Automatically rewrite schema paths passed to `schemaLoader`
* (e.g. src/docs/schemas/config.schema.json)
* to relative links (e.g. /schemas/config.schema.json)
*
* See https://vitepress.vuejs.org/guide/asset-handling
*
* @param origin - Original schema path (relative to this script).
* @returns New absolute Vitepress path to schema
*/
rewritelinks: (origin: string) => {
return `/${relative(VITEPRESS_PUBLIC_DIR, origin)}`;
},
})(schemas);
for (const [name, markdownAst] of Object.entries(markdownFiles)) {
/*
* Converts list entries of shape '- tsType: () => Partial<FontConfig>'
* into '- tsType: `() => Partial<FontConfig>`' (e.g. escaping with back-ticks),
* as otherwise VitePress doesn't like the <FontConfig> bit.
*/
visit(markdownAst as Root, 'listItem', (listEntry: ListItem) => {
let listText: Text;
const blockItem = listEntry.children[0];
if (blockItem.type === 'paragraph' && blockItem.children[0].type === 'text') {
listText = blockItem.children[0];
} // @ts-expect-error: MD AST output from @adobe/jsonschema2md is technically wrong
else if (blockItem.type === 'text') {
listText = blockItem;
} else {
return; // skip
}
if (listText.value.startsWith('tsType: ')) {
listText.value = listText.value.replace(/tsType: (.*)/g, 'tsType: `$1`');
}
});
const transformed = remark()
.use(remarkGfm)
.use(remarkFrontmatter, ['yaml']) // support YAML front-matter in Markdown
.use(transformMarkdownAst, {
// mermaid project specific plugin
originalFilename: file,
addAutogeneratedWarning: !noHeader,
removeYAML: !noHeader,
})
.stringify(markdownAst as Root);
const formatted = prettier.format(transformed, {
parser: 'markdown',
...prettierConfig,
});
const newFileName = join(SCHEMA_MARKDOWN_OUTPUT_DIR, `${name}.md`);
copyTransformedContents(newFileName, !verifyOnly, formatted);
}
}
/**
* Transform an HTML file and write the transformed file to the directory for published
* documentation
@ -362,15 +481,15 @@ const transformHtml = (filename: string) => {
};
const getGlobs = (globs: string[]): string[] => {
globs.push(
'!**/dist',
'!**/redirect.spec.ts',
'!**/landing',
'!**/node_modules',
'!**/user-avatars'
);
globs.push('!**/dist/**', '!**/redirect.spec.ts', '!**/landing/**', '!**/node_modules/**');
if (!vitepress) {
globs.push('!**/.vitepress', '!**/vite.config.ts', '!src/docs/index.md', '!**/package.json');
globs.push(
'!**/.vitepress/**',
'!**/vite.config.ts',
'!src/docs/index.md',
'!**/package.json',
'!**/user-avatars/**'
);
}
return globs;
};
@ -388,6 +507,14 @@ const main = async () => {
const sourceDirGlob = posix.join('.', SOURCE_DOCS_DIR, '**');
const action = verifyOnly ? 'Verifying' : 'Transforming';
if (vitepress) {
console.log(`${action} 1 .schema.yaml file`);
await transormJsonSchema('src/schemas/config.schema.yaml');
} else {
// skip because this creates so many Markdown files that it lags git
console.log('Skipping 1 .schema.yaml file');
}
const mdFileGlobs = getGlobs([posix.join(sourceDirGlob, '*.md')]);
const mdFiles = await getFilesFromGlobs(mdFileGlobs);
console.log(`${action} ${mdFiles.length} markdown files...`);

View File

@ -16,8 +16,12 @@ import { teamMembers } from '../contributors';
<p text-lg max-w-200 text-center leading-7>
<Contributors />
<br />
<a href="https://chat.vitest.dev" rel="noopener noreferrer">Join the community</a> and
get involved!
<a
href="https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE"
rel="noopener noreferrer"
>Join the community</a
>
and get involved!
</p>
</div>
</main>

View File

@ -155,6 +155,7 @@ function sidebarConfig() {
{ text: 'Tutorials', link: '/config/Tutorials' },
{ text: 'API-Usage', link: '/config/usage' },
{ text: 'Mermaid API Configuration', link: '/config/setup/README' },
{ text: 'Mermaid Configuration Options', link: '/config/schema-docs/config' },
{ text: 'Directives', link: '/config/directives' },
{ text: 'Theming', link: '/config/theming' },
{ text: 'Accessibility', link: '/config/accessibility' },

View File

@ -1,30 +1,5 @@
import contributorUsernamesJson from './contributor-names.json';
export interface Contributor {
name: string;
avatar: string;
}
export interface SocialEntry {
icon: string | { svg: string };
link: string;
}
export interface CoreTeam {
name: string;
// required to download avatars from GitHub
github: string;
avatar?: string;
twitter?: string;
mastodon?: string;
sponsor?: string;
website?: string;
linkedIn?: string;
title?: string;
org?: string;
desc?: string;
links?: SocialEntry[];
}
import { CoreTeam, knut, plainTeamMembers } from './teamMembers.js';
const contributorUsernames: string[] = contributorUsernamesJson;
@ -38,6 +13,7 @@ const websiteSVG = {
const createLinks = (tm: CoreTeam): CoreTeam => {
tm.avatar = `/user-avatars/${tm.github}.png`;
tm.title = tm.title ?? 'Developer';
tm.links = [{ icon: 'github', link: `https://github.com/${tm.github}` }];
if (tm.mastodon) {
tm.links.push({ icon: 'mastodon', link: tm.mastodon });
@ -54,91 +30,6 @@ const createLinks = (tm: CoreTeam): CoreTeam => {
return tm;
};
const knut: CoreTeam = {
github: 'knsv',
name: 'Knut Sveidqvist',
title: 'Creator',
twitter: 'knutsveidqvist',
sponsor: 'https://github.com/sponsors/knsv',
};
const plainTeamMembers: CoreTeam[] = [
{
github: 'NeilCuzon',
name: 'Neil Cuzon',
title: 'Developer',
},
{
github: 'tylerlong',
name: 'Tyler Liu',
title: 'Developer',
},
{
github: 'sidharthv96',
name: 'Sidharth Vinod',
title: 'Developer',
twitter: 'sidv42',
mastodon: 'https://techhub.social/@sidv',
sponsor: 'https://github.com/sponsors/sidharthv96',
linkedIn: 'sidharth-vinod',
website: 'https://sidharth.dev',
},
{
github: 'ashishjain0512',
name: 'Ashish Jain',
title: 'Developer',
},
{
github: 'mmorel-35',
name: 'Matthieu Morel',
title: 'Developer',
linkedIn: 'matthieumorel35',
},
{
github: 'aloisklink',
name: 'Alois Klink',
title: 'Developer',
linkedIn: 'aloisklink',
website: 'https://aloisklink.com',
},
{
github: 'pbrolin47',
name: 'Per Brolin',
title: 'Developer',
},
{
github: 'Yash-Singh1',
name: 'Yash Singh',
title: 'Developer',
},
{
github: 'GDFaber',
name: 'Marc Faber',
title: 'Developer',
linkedIn: 'marc-faber',
},
{
github: 'MindaugasLaganeckas',
name: 'Mindaugas Laganeckas',
title: 'Developer',
},
{
github: 'jgreywolf',
name: 'Justin Greywolf',
title: 'Developer',
},
{
github: 'IOrlandoni',
name: 'Nacho Orlandoni',
title: 'Developer',
},
{
github: 'huynhicode',
name: 'Steph Huynh',
title: 'Developer',
},
];
const teamMembers = plainTeamMembers.map((tm) => createLinks(tm));
teamMembers.sort(
(a, b) => contributorUsernames.indexOf(a.github) - contributorUsernames.indexOf(b.github)

View File

@ -54,6 +54,15 @@ const MermaidExample = async (md: MarkdownRenderer) => {
return `<div class="tip custom-block"><p class="custom-block-title">NOTE</p><p>${token.content}}</p></div>`;
}
if (token.info.trim() === 'regexp') {
// shiki doesn't yet support regexp code blocks, but the javascript
// one still makes RegExes look good
token.info = 'javascript';
// use trimEnd to move trailing `\n` outside if the JavaScript regex `/` block
token.content = `/${token.content.trimEnd()}/\n`;
return defaultRenderer(tokens, index, options, env, slf);
}
if (token.info.trim() === 'jison') {
return `<div class="language-">
<button class="copy"></button>

View File

@ -19,6 +19,10 @@ async function download(url: string, fileName: URL) {
await writeFile(fileName, Buffer.from(await image.arrayBuffer()));
} catch (error) {
console.error('failed to load', url, error);
// Exit the build process if we are in CI
if (process.env.CI) {
throw error;
}
}
}
@ -32,7 +36,7 @@ async function fetchAvatars() {
download(`https://github.com/${name}.png?size=100`, getAvatarPath(name));
});
await Promise.all(avatars);
await Promise.allSettled(avatars);
}
fetchAvatars();

View File

@ -1,6 +1,8 @@
// Adapted from https://github.dev/vitest-dev/vitest/blob/991ff33ab717caee85ef6cbe1c16dc514186b4cc/scripts/update-contributors.ts#L6
import { writeFile } from 'node:fs/promises';
import { knut, plainTeamMembers } from '../teamMembers.js';
import { existsSync } from 'node:fs';
const pathContributors = new URL('../contributor-names.json', import.meta.url);
@ -35,7 +37,15 @@ async function fetchContributors() {
}
async function generate() {
const collaborators = await fetchContributors();
if (existsSync(pathContributors)) {
// Only fetch contributors once, when running locally.
// In CI, the file won't exist, so it'll fetch every time as expected.
return;
}
// Will fetch all contributors only in CI to speed up local development.
const collaborators = process.env.CI
? await fetchContributors()
: [knut, ...plainTeamMembers].map((m) => m.github);
await writeFile(pathContributors, JSON.stringify(collaborators, null, 2) + '\n', 'utf8');
}

View File

@ -0,0 +1,105 @@
export interface Contributor {
name: string;
avatar: string;
}
export interface SocialEntry {
icon: string | { svg: string };
link: string;
}
export interface CoreTeam {
name: string;
// required to download avatars from GitHub
github: string;
avatar?: string;
twitter?: string;
mastodon?: string;
sponsor?: string;
website?: string;
linkedIn?: string;
title?: string;
org?: string;
desc?: string;
links?: SocialEntry[];
}
export const knut: CoreTeam = {
github: 'knsv',
name: 'Knut Sveidqvist',
title: 'Creator',
twitter: 'knutsveidqvist',
sponsor: 'https://github.com/sponsors/knsv',
};
export const plainTeamMembers: CoreTeam[] = [
{
github: 'NeilCuzon',
name: 'Neil Cuzon',
},
{
github: 'tylerlong',
name: 'Tyler Liu',
},
{
github: 'sidharthv96',
name: 'Sidharth Vinod',
twitter: 'sidv42',
mastodon: 'https://techhub.social/@sidv',
sponsor: 'https://github.com/sponsors/sidharthv96',
linkedIn: 'sidharth-vinod',
website: 'https://sidharth.dev',
},
{
github: 'ashishjain0512',
name: 'Ashish Jain',
},
{
github: 'mmorel-35',
name: 'Matthieu Morel',
linkedIn: 'matthieumorel35',
},
{
github: 'aloisklink',
name: 'Alois Klink',
linkedIn: 'aloisklink',
website: 'https://aloisklink.com',
},
{
github: 'pbrolin47',
name: 'Per Brolin',
},
{
github: 'Yash-Singh1',
name: 'Yash Singh',
},
{
github: 'GDFaber',
name: 'Marc Faber',
linkedIn: 'marc-faber',
},
{
github: 'MindaugasLaganeckas',
name: 'Mindaugas Laganeckas',
},
{
github: 'jgreywolf',
name: 'Justin Greywolf',
},
{
github: 'IOrlandoni',
name: 'Nacho Orlandoni',
},
{
github: 'huynhicode',
name: 'Steph Huynh',
},
{
github: 'Yokozuna59',
name: 'Reda Al Sulais',
},
{
github: 'nirname',
name: 'Nikolay Rozhkov',
},
];

View File

@ -1,7 +1,7 @@
# Announcements
## [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/)
## [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)
15 June 2023 · 12 mins
29 June 2023 · 4 mins
Sequence diagrams really shine when youre documenting different parts of a system and the various ways these parts interact with each other.
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

@ -1,5 +1,11 @@
# 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

View File

@ -4,6 +4,7 @@
"type": "module",
"scripts": {
"dev": "vitepress --port 3333 --open",
"dev:docker": "vitepress --port 3333 --host",
"build": "pnpm prefetch && vitepress build",
"build-no-prefetch": "vitepress build",
"serve": "vitepress serve",
@ -30,7 +31,7 @@
"unplugin-vue-components": "^0.25.0",
"vite": "^4.3.3",
"vite-plugin-pwa": "^0.16.0",
"vitepress": "1.0.0-beta.3",
"vitepress": "1.0.0-beta.5",
"workbox-window": "^7.0.0"
}
}

View File

@ -676,6 +676,16 @@ 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
```
### 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

@ -12,81 +12,6 @@ The things being connected are called nodes and the connections are called links
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
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
```
::: details
```mermaid-example
sankey-beta
@ -160,8 +85,6 @@ 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.
@ -184,15 +107,6 @@ 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:
@ -207,16 +121,6 @@ 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:
@ -228,13 +132,6 @@ 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:
@ -246,13 +143,6 @@ 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.

View File

@ -733,59 +733,7 @@ describe('mermaidAPI', () => {
const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n`;
const expectedDiagramType = testedDiagram.expectedType;
it('aria-roledscription is set to the diagram type, addSVGa11yTitleDescription is called', async () => {
const a11yDiagramInfo_spy = vi.spyOn(accessibility, 'setA11yDiagramInfo');
const a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription');
await mermaidAPI.render(id, diagramText);
expect(a11yDiagramInfo_spy).toHaveBeenCalledWith(
expect.anything(),
expectedDiagramType
);
expect(a11yTitleDesc_spy).toHaveBeenCalled();
});
});
});
});
});
describe('render', () => {
// Be sure to add async before each test (anonymous) method
// These are more like integration tests right now because nothing is mocked.
// But it is faster that a cypress test and there's no real reason to actually evaluate an image pixel by pixel.
// render(id, text, cb?, svgContainingElement?)
// Test all diagram types. Note that old flowchart 'graph' type will invoke the flowRenderer-v2. (See the flowchart v2 detector.)
// We have to have both the specific textDiagramType and the expected type name because the expected type may be slightly different than was is put in the diagram text (ex: in -v2 diagrams)
const diagramTypesAndExpectations = [
{ textDiagramType: 'C4Context', expectedType: 'c4' },
{ textDiagramType: 'classDiagram', expectedType: 'classDiagram' },
{ textDiagramType: 'classDiagram-v2', expectedType: 'classDiagram' },
{ textDiagramType: 'erDiagram', expectedType: 'er' },
{ textDiagramType: 'graph', expectedType: 'flowchart-v2' },
{ textDiagramType: 'flowchart', expectedType: 'flowchart-v2' },
{ textDiagramType: 'gitGraph', expectedType: 'gitGraph' },
{ textDiagramType: 'gantt', expectedType: 'gantt' },
{ textDiagramType: 'journey', expectedType: 'journey' },
{ textDiagramType: 'pie', expectedType: 'pie' },
{ textDiagramType: 'requirementDiagram', expectedType: 'requirement' },
{ textDiagramType: 'sequenceDiagram', expectedType: 'sequence' },
{ textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' },
];
describe('accessibility', () => {
const id = 'mermaid-fauxId';
const a11yTitle = 'a11y title';
const a11yDescr = 'a11y description';
diagramTypesAndExpectations.forEach((testedDiagram) => {
describe(`${testedDiagram.textDiagramType}`, () => {
const diagramType = testedDiagram.textDiagramType;
const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n`;
const expectedDiagramType = testedDiagram.expectedType;
it('aria-roledscription is set to the diagram type, addSVGa11yTitleDescription is called', async () => {
it('should set aria-roledscription to the diagram type AND should call addSVGa11yTitleDescription', async () => {
const a11yDiagramInfo_spy = vi.spyOn(accessibility, 'setA11yDiagramInfo');
const a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription');
await mermaidAPI.render(id, diagramText);

View File

@ -478,7 +478,7 @@ const render = async function (
// Get the temporary div element containing the svg
const element = root.select(enclosingDivID_selector).node();
const graphType = diag.type;
const diagramType = diag.type;
// -------------------------------------------------------------------------------
// Create and insert the styles (user styles, theme styles, config styles)
@ -486,11 +486,11 @@ const render = async function (
// Insert an element into svg. This is where we put the styles
const svg = element.firstChild;
const firstChild = svg.firstChild;
const diagramClassDefs = CLASSDEF_DIAGRAMS.includes(graphType)
const diagramClassDefs = CLASSDEF_DIAGRAMS.includes(diagramType)
? diag.renderer.getClasses(text, diag)
: {};
const rules = createUserStyles(config, graphType, diagramClassDefs, idSelector);
const rules = createUserStyles(config, diagramType, diagramClassDefs, idSelector);
const style1 = document.createElement('style');
style1.innerHTML = rules;
@ -507,9 +507,9 @@ const render = async function (
// This is the d3 node for the svg element
const svgNode = root.select(`${enclosingDivID_selector} svg`);
const a11yTitle = diag.db.getAccTitle?.();
const a11yDescr = diag.db.getAccDescription?.();
addA11yInfo(graphType, svgNode, a11yTitle, a11yDescr);
const a11yTitle: string | undefined = diag.db.getAccTitle?.();
const a11yDescr: string | undefined = diag.db.getAccDescription?.();
addA11yInfo(diagramType, svgNode, a11yTitle, a11yDescr);
// -------------------------------------------------------------------------------
// Clean up SVG code
@ -586,14 +586,18 @@ function initialize(options: MermaidConfig = {}) {
/**
* Add accessibility (a11y) information to the diagram.
*
* @param diagramType - diagram type
* @param svgNode - d3 node to insert the a11y title and desc info
* @param a11yTitle - a11y title
* @param a11yDescr - a11y description
*/
function addA11yInfo(
graphType: string,
diagramType: string,
svgNode: D3Element,
a11yTitle: string | undefined,
a11yDescr: string | undefined
) {
setA11yDiagramInfo(svgNode, graphType);
a11yTitle?: string,
a11yDescr?: string
): void {
setA11yDiagramInfo(svgNode, diagramType);
addSVGa11yTitleDescription(svgNode, a11yTitle, a11yDescr, svgNode.attr('id'));
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,3 @@
import type {} from '@vitest/spy/dist/index.js';
/**
* This is a mocked/stubbed version of the d3 Selection type. Each of the main functions are all
* mocked (via vi.fn()) so you can track if they have been called, etc.
@ -7,9 +5,8 @@ import type {} from '@vitest/spy/dist/index.js';
* Note that node() returns a HTML Element with tag 'svg'. It is an empty element (no innerHTML, no children, etc).
* This potentially allows testing of mermaidAPI render().
*/
export class MockedD3 {
public attribs = new Map<string, string | null>();
public attribs = new Map<string, string>();
public id: string | undefined = '';
_children: MockedD3[] = [];
@ -72,9 +69,9 @@ export class MockedD3 {
return newMock;
};
attr(attrName: string): null | undefined | string | number;
// attr(attrName: string, attrValue: string): MockedD3;
attr(attrName: string, attrValue?: string): null | undefined | string | number | MockedD3 {
attr(attrName: string): undefined | string;
attr(attrName: string, attrValue: string): MockedD3;
attr(attrName: string, attrValue?: string): undefined | string | MockedD3 {
if (arguments.length === 1) {
return this.attribs.get(attrName);
} else {

File diff suppressed because it is too large Load Diff

2
run
View File

@ -27,7 +27,7 @@ $RUN --service-ports mermaid sh -c "npx pnpm run dev"
;;
docs:dev)
$RUN --service-ports mermaid sh -c "cd packages/mermaid/src/docs && npx pnpm prefetch && npx vitepress --port 3333 --host"
$RUN --service-ports mermaid sh -c "npx pnpm run --filter mermaid docs:dev:docker"
;;
cypress)

View File

@ -1,4 +1,5 @@
import jison from './.vite/jisonPlugin.js';
import jsonSchemaPlugin from './.vite/jsonSchemaPlugin.js';
import typescript from '@rollup/plugin-typescript';
import { defineConfig } from 'vitest/config';
@ -8,6 +9,7 @@ export default defineConfig({
},
plugins: [
jison(),
jsonSchemaPlugin(), // handles .schema.yaml JSON Schema files
// @ts-expect-error According to the type definitions, rollup plugins are incompatible with vite
typescript({ compilerOptions: { declaration: false } }),
],