Compare commits

..

3 Commits

Author SHA1 Message Date
Sidharth Vinod
5e210753b7 feat: Add live-reload 2023-08-13 18:51:02 +05:30
Sidharth Vinod
350aaf9b51 chore: Remove @vitest/coverage-c8 2023-08-13 18:45:49 +05:30
Sidharth Vinod
5cd792f027 feat: Add esbuild 2023-08-13 18:33:41 +05:30
90 changed files with 2438 additions and 3076 deletions

View File

@@ -82,8 +82,8 @@ export function generateDefaults(mermaidConfigSchema: JSONSchemaType<MermaidConf
`schema for subconfig ${key} does not have valid defaults! Errors were ${JSON.stringify(
validate.errors,
undefined,
2,
)}`,
2
)}`
);
}
}
@@ -95,8 +95,8 @@ export function generateDefaults(mermaidConfigSchema: JSONSchemaType<MermaidConf
`Mermaid config JSON Schema does not have valid defaults! Errors were ${JSON.stringify(
validate.errors,
undefined,
2,
)}`,
2
)}`
);
}

View File

@@ -1,49 +1,21 @@
import { build } from 'esbuild';
import { mkdir, writeFile } from 'node:fs/promises';
import { MermaidBuildOptions, defaultOptions, getBuildConfig } from './util.js';
import { getBuildConfig } from './util.js';
import { packageOptions } from '../.build/common.js';
const shouldVisualize = process.argv.includes('--visualize');
const buildPackage = async (entryName: keyof typeof packageOptions) => {
const commonOptions = { ...defaultOptions, entryName } as const;
const buildConfigs = [
// package.mjs
{ ...commonOptions },
// package.min.mjs
{
...commonOptions,
minify: true,
metafile: shouldVisualize,
},
// package.core.mjs
{ ...commonOptions, core: true },
];
if (entryName === 'mermaid') {
const iifeOptions: MermaidBuildOptions = { ...commonOptions, format: 'iife' };
buildConfigs.push(
// mermaid.js
{ ...iifeOptions },
// mermaid.min.js
{ ...iifeOptions, minify: true, metafile: shouldVisualize },
);
}
const results = await Promise.all(buildConfigs.map((option) => build(getBuildConfig(option))));
if (shouldVisualize) {
for (const { metafile } of results) {
if (!metafile) {
continue;
}
const fileName = Object.keys(metafile.outputs)
.filter((file) => !file.includes('chunks') && file.endsWith('js'))[0]
.replace('dist/', '');
// Upload metafile into https://esbuild.github.io/analyze/
await writeFile(`stats/${fileName}.meta.json`, JSON.stringify(metafile));
}
await build(getBuildConfig({ entryName, minify: false }));
const { metafile } = await build(
getBuildConfig({ entryName, minify: true, metafile: shouldVisualize })
);
if (metafile) {
// Upload metafile into https://esbuild.github.io/analyze/
await writeFile(`stats/meta-${entryName}.json`, JSON.stringify(metafile));
}
await build(getBuildConfig({ entryName, minify: false, core: true }));
await build(getBuildConfig({ entryName, minify: true, format: 'iife' }));
};
const handler = (e) => {
@@ -54,7 +26,9 @@ const handler = (e) => {
const main = async () => {
await mkdir('stats').catch(() => {});
const packageNames = Object.keys(packageOptions) as (keyof typeof packageOptions)[];
await Promise.allSettled(packageNames.map((pkg) => buildPackage(pkg).catch(handler)));
for (const pkg of packageNames) {
await buildPackage(pkg).catch(handler);
}
};
void main();

View File

@@ -1,39 +1,26 @@
import express from 'express';
import type { NextFunction, Request, Response } from 'express';
import cors from 'cors';
import { getBuildConfig, defaultOptions } from './util.js';
import { getBuildConfig } from './util.js';
import { context } from 'esbuild';
import chokidar from 'chokidar';
const mermaidCtx = await context(
getBuildConfig({ ...defaultOptions, minify: false, core: false, entryName: 'mermaid' }),
getBuildConfig({ minify: false, core: false, entryName: 'mermaid' })
);
const mermaidIIFECtx = await context(
getBuildConfig({
...defaultOptions,
minify: false,
core: false,
entryName: 'mermaid',
format: 'iife',
}),
getBuildConfig({ minify: false, core: false, entryName: 'mermaid', format: 'iife' })
);
const externalCtx = await context(
getBuildConfig({
...defaultOptions,
minify: false,
core: false,
entryName: 'mermaid-example-diagram',
}),
getBuildConfig({ minify: false, core: false, entryName: 'mermaid-example-diagram' })
);
const zenumlCtx = await context(
getBuildConfig({ ...defaultOptions, minify: false, core: false, entryName: 'mermaid-zenuml' }),
getBuildConfig({ minify: false, core: false, entryName: 'mermaid-zenuml' })
);
const contexts = [mermaidCtx, mermaidIIFECtx, externalCtx, zenumlCtx];
const rebuildAll = async () => {
console.time('Rebuild time');
await Promise.all(contexts.map((ctx) => ctx.rebuild()));
console.timeEnd('Rebuild time');
};
let clients: { id: number; response: Response }[] = [];
@@ -75,7 +62,6 @@ function sendEventsToAll() {
}
async function createServer() {
handleFileChange();
const app = express();
chokidar
.watch('**/src/**/*.{js,ts,yaml,json}', {

View File

@@ -8,21 +8,14 @@ import { jisonPlugin } from './jisonPlugin.js';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
export interface MermaidBuildOptions {
interface MermaidBuildOptions {
minify: boolean;
core: boolean;
metafile: boolean;
format: 'esm' | 'iife';
core?: boolean;
metafile?: boolean;
format?: 'esm' | 'iife';
entryName: keyof typeof packageOptions;
}
export const defaultOptions: Omit<MermaidBuildOptions, 'entryName'> = {
minify: false,
metafile: false,
core: false,
format: 'esm',
} as const;
const buildOptions = (override: BuildOptions): BuildOptions => {
return {
bundle: true,
@@ -39,37 +32,29 @@ const buildOptions = (override: BuildOptions): BuildOptions => {
};
};
const getFileName = (fileName: string, { core, format, minify }: MermaidBuildOptions) => {
if (core) {
fileName += '.core';
} else if (format === 'esm') {
fileName += '.esm';
}
if (minify) {
fileName += '.min';
}
return fileName;
};
export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
const { core, entryName, metafile, format, minify } = options;
export const getBuildConfig = ({
minify,
core,
entryName,
metafile,
format,
}: MermaidBuildOptions): BuildOptions => {
const external: string[] = ['require', 'fs', 'path'];
const { name, file, packageName } = packageOptions[entryName];
const outFileName = getFileName(name, options);
let output: BuildOptions = buildOptions({
absWorkingDir: resolve(__dirname, `../packages/${packageName}`),
entryPoints: {
[outFileName]: `src/${file}`,
[`${name}${core ? '.core' : format === 'iife' ? '' : '.esm'}${
minify ? '.min' : ''
}`]: `src/${file}`,
},
metafile,
minify,
logLevel: 'info',
chunkNames: `chunks/${outFileName}/[name]-[hash]`,
});
if (core) {
const { dependencies } = JSON.parse(
readFileSync(resolve(__dirname, `../packages/${packageName}/package.json`), 'utf-8'),
readFileSync(resolve(__dirname, `../packages/${packageName}/package.json`), 'utf-8')
);
// Core build is used to generate file without bundled dependencies.
// This is used by downstream projects to bundle dependencies themselves.
@@ -82,8 +67,6 @@ export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
output.format = 'iife';
output.splitting = false;
output.globalName = '__esbuild_esm_mermaid';
// Workaround for removing the .default access in esbuild IIFE.
// https://github.com/mermaid-js/mermaid/pull/4109#discussion_r1292317396
output.footer = {
js: 'globalThis.mermaid = globalThis.__esbuild_esm_mermaid.default;',
};

View File

@@ -48,8 +48,6 @@ module.exports = {
'no-prototype-builtins': 'off',
'no-unused-vars': 'off',
'cypress/no-async-tests': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/ban-ts-comment': [

View File

@@ -1,27 +1,14 @@
name-template: '$NEXT_PATCH_VERSION'
tag-template: '$NEXT_PATCH_VERSION'
categories:
- title: '🚨 **Breaking Changes**'
labels:
- 'Breaking Change'
- title: '🚀 Features'
labels:
- 'Type: Enhancement'
- 'feature' # deprecated, new PRs shouldn't have this
- title: '🐛 Bug Fixes'
labels:
- 'Type: Bug / Error'
- 'fix' # deprecated, new PRs shouldn't have this
- title: '🧰 Maintenance'
labels:
- 'Type: Other'
- 'chore' # deprecated, new PRs shouldn't have this
- title: '⚡️ Performance'
labels:
- 'Type: Performance'
- title: '📚 Documentation'
labels:
- 'Area: Documentation'
label: 'Type: Other'
change-template: '- $TITLE (#$NUMBER) @$AUTHOR'
sort-by: title
sort-direction: ascending

View File

@@ -30,7 +30,7 @@ jobs:
# Install NPM dependencies, cache them correctly
# and run all Cypress tests
- name: Cypress run
uses: cypress-io/github-action@v6
uses: cypress-io/github-action@v4
id: cypress
# If CYPRESS_RECORD_KEY is set, run in parallel on all containers
# Otherwise (e.g. if running from fork), we run on a single container only
@@ -44,19 +44,14 @@ jobs:
parallel: ${{ secrets.CYPRESS_RECORD_KEY != '' }}
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
VITE_COVERAGE: true
CYPRESS_COVERAGE: true
VITEST_COVERAGE: true
CYPRESS_COMMIT: ${{ github.sha }}
- name: Check coverage files
run: |
ls -lh ./coverage
ls -lh ./coverage/cypress
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v3
# Run step only pushes to develop and pull_requests
if: ${{ steps.cypress.conclusion == 'success' && (github.event_name == 'pull_request' || github.ref == 'refs/heads/develop')}}
with:
files: ./coverage/cypress/lcov.info
files: coverage/cypress/lcov.info
flags: e2e
name: mermaid-codecov
fail_ci_if_error: false

View File

@@ -62,22 +62,8 @@ jobs:
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 no circular dependencies
working-directory: ./packages/mermaid
shell: bash
run: |
if ! pnpm run --filter mermaid checkCircle; then
ERROR_MESSAGE='Circular dependency detected.'
ERROR_MESSAGE+=' This should be fixed by removing the circular dependency.'
ERROR_MESSAGE+=' Run `pnpm run --filter mermaid checkCircle` on your local machine'
ERROR_MESSAGE+=' to see the circular dependency.'
echo "::error title=Lint failure::${ERROR_MESSAGE}"
# make sure to return an error exitcode so that GitHub actions shows a red-cross
exit 1
# make sure to return an error exitcode so that GitHub actions shows a red-cross
exit 1
fi
- name: Verify Docs

View File

@@ -43,6 +43,8 @@ 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

1
.gitignore vendored
View File

@@ -46,4 +46,3 @@ stats/
demos/dev/**
!/demos/dev/example.html
!/demos/dev/reload.js

View File

@@ -32,7 +32,7 @@ const visualizerOptions = (packageName: string, core = false): PluginOption[] =>
template: chartType as TemplateType,
gzipSize: true,
brotliSize: true,
}) as PluginOption,
}) as PluginOption
);
};

13
__mocks__/pieRenderer.js Normal file
View File

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

View File

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

View File

@@ -44,7 +44,6 @@
"faber",
"flatmap",
"foswiki",
"frontmatter",
"ftplugin",
"gantt",
"gitea",
@@ -57,7 +56,6 @@
"gzipped",
"huynh",
"huynhicode",
"iife",
"inkdrop",
"jaoude",
"jgreywolf",
@@ -95,7 +93,6 @@
"nikolay",
"nirname",
"orlandoni",
"outdir",
"pathe",
"pbrolin",
"phpbb",
@@ -110,7 +107,6 @@
"rects",
"reda",
"redmine",
"regexes",
"rehype",
"roledescription",
"rozhkov",

View File

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

View File

@@ -1,85 +1,89 @@
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
describe('pie chart', () => {
describe('Pie Chart', () => {
it('should render a simple pie diagram', () => {
imgSnapshotTest(
`pie title Sports in Sweden
"Bandy": 40
"Ice-Hockey": 80
"Football": 90
`
pie title Sports in Sweden
"Bandy" : 40
"Ice-Hockey" : 80
"Football" : 90
`,
{}
);
cy.get('svg');
});
it('should render a simple pie diagram with long labels', () => {
imgSnapshotTest(
`pie title NETFLIX
"Time spent looking for movie": 90
"Time spent watching it": 10
`
pie title NETFLIX
"Time spent looking for movie" : 90
"Time spent watching it" : 10
`,
{}
);
cy.get('svg');
});
it('should render a simple pie diagram with capital letters for labels', () => {
imgSnapshotTest(
`pie title What Voldemort doesn't have?
"FRIENDS": 2
"FAMILY": 3
"NOSE": 45
`
pie title What Voldemort doesn't have?
"FRIENDS" : 2
"FAMILY" : 3
"NOSE" : 45
`,
{}
);
cy.get('svg');
});
it('should render a pie diagram when useMaxWidth is true (default)', () => {
renderGraph(
`pie title Sports in Sweden
"Bandy": 40
"Ice-Hockey": 80
"Football": 90
`
pie title Sports in Sweden
"Bandy" : 40
"Ice-Hockey" : 80
"Football" : 90
`,
{ pie: { useMaxWidth: true } }
);
cy.get('svg').should((svg) => {
expect(svg).to.have.attr('width', '100%');
// expect(svg).to.have.attr('height');
// const height = parseFloat(svg.attr('height'));
// expect(height).to.eq(450);
const style = svg.attr('style');
expect(style).to.match(/^max-width: [\d.]+px;$/);
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
expect(maxWidthValue).to.eq(984);
});
});
it('should render a pie diagram when useMaxWidth is false', () => {
renderGraph(
`pie title Sports in Sweden
"Bandy": 40
"Ice-Hockey": 80
"Football": 90
`
pie title Sports in Sweden
"Bandy" : 40
"Ice-Hockey" : 80
"Football" : 90
`,
{ pie: { useMaxWidth: false } }
);
cy.get('svg').should((svg) => {
// const height = parseFloat(svg.attr('height'));
const width = parseFloat(svg.attr('width'));
// expect(height).to.eq(450);
expect(width).to.eq(984);
expect(svg).to.not.have.attr('style');
});
});
it('should render a pie diagram when textPosition is setted', () => {
it('should render a pie diagram when textPosition is set', () => {
imgSnapshotTest(
`pie
"Dogs": 50
"Cats": 25
`,
`
pie
"Dogs": 50
"Cats": 25
`,
{ logLevel: 1, pie: { textPosition: 0.9 } }
);
});
it('should render a pie diagram with showData', () => {
imgSnapshotTest(
`pie showData
"Dogs": 50
"Cats": 25
`
);
cy.get('svg');
});
});

View File

@@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />

View File

@@ -139,6 +139,6 @@ if (typeof document !== 'undefined') {
void contentLoaded().finally(markRendered);
}
},
false,
false
);
}

View File

@@ -5,8 +5,6 @@
<title>Mermaid development page</title>
</head>
<body>
<pre class="mermaid">info</pre>
<pre id="diagram" class="mermaid">
graph TB
a --> b
@@ -33,6 +31,32 @@ graph TB
el.innerHTML = svg;
</script>
<script src="/dev/reload.js"></script>
<script>
// Set to false to disable live reload
const liveReload = true;
// Connect to the server and reload the page if the server sends a reload message
const connectToEvents = () => {
const events = new EventSource('/events');
const loadTime = Date.now();
events.onmessage = (event) => {
const time = JSON.parse(event.data);
if (time && time > loadTime) {
location.reload();
}
};
events.onerror = (error) => {
console.error(error);
events.close();
// Try to reconnect after 1 second in case of errors
setTimeout(connectToEvents, 1000);
};
events.onopen = () => {
console.log('Connected to live reload server');
};
};
if (liveReload) {
connectToEvents();
}
</script>
</body>
</html>

View File

@@ -1,22 +0,0 @@
// Connect to the server and reload the page if the server sends a reload message
const connectToEvents = () => {
const events = new EventSource('/events');
const loadTime = Date.now();
events.onmessage = (event) => {
const time = JSON.parse(event.data);
if (time && time > loadTime) {
location.reload();
}
};
events.onerror = (error) => {
console.error(error);
events.close();
// Try to reconnect after 1 second in case of errors
setTimeout(connectToEvents, 1000);
};
events.onopen = () => {
console.log('Connected to live reload server');
};
};
setTimeout(connectToEvents, 500);

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
@@ -7,6 +7,7 @@
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
<style>
div.mermaid {
/* font-family: 'trebuchet ms', verdana, arial; */
font-family: 'Courier New', Courier, monospace !important;
}
</style>
@@ -16,32 +17,37 @@
<h1>Pie chart demos</h1>
<pre class="mermaid">
pie title Pets adopted by volunteers
accTitle: simple pie char demo
accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs.
"Dogs": 386
"Cats": 85
"Rats": 15
accTitle: simple pie char demo
accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs.
"Dogs" : 386
"Cats" : 85
"Rats" : 15
</pre>
<hr />
<pre class="mermaid">
%%{init: {"pie": {"textPosition": 0.9}, "themeVariables": {"pieOuterStrokeWidth": "5px"}}}%%
pie
title Key elements in Product X
%%{init: {"pie": {"textPosition": 0.9}, "themeVariables": {"pieOuterStrokeWidth": "5px"}} }%%
pie
title Key elements in Product X
accTitle: Key elements in Product X
accDescr: This is a pie chart showing the key elements in Product X.
"Calcium": 42.96
"Potassium": 50.05
"Magnesium": 10.01
"Iron": 5
accDescr: This is a pie chart showing the key elements in Product X.
"Calcium" : 42.96
"Potassium" : 50.05
"Magnesium" : 10.01
"Iron" : 5
</pre>
<script type="module">
import mermaid from '/mermaid.esm.mjs';
mermaid.initialize({
theme: 'forest',
// themeCSS: '.node rect { fill: red; }',
logLevel: 3,
securityLevel: 'loose',
// flowchart: { curve: 'basis' },
// gantt: { axisFormat: '%m/%d/%Y' },
sequence: { actorMargin: 50 },
// sequenceDiagram: { actorMargin: 300 } // deprecated
});
</script>
</body>

View File

@@ -1,7 +1,7 @@
version: '3.9'
services:
mermaid:
image: node:18.17.1-alpine3.18
image: node:18.17.0-alpine3.18
stdin_open: true
tty: true
working_dir: /mermaid
@@ -17,7 +17,7 @@ services:
- 9000:9000
- 3333:3333
cypress:
image: cypress/included:12.17.4
image: cypress/included:12.17.2
stdin_open: true
tty: true
working_dir: /mermaid

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,17 +10,17 @@
### configKeys
`Const` **configKeys**: `Set`<`string`>
`Const` **configKeys**: `string`\[]
#### Defined in
[defaultConfig.ts:268](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L268)
[defaultConfig.ts:266](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L266)
---
### default
`Const` **default**: `RequiredDeep`<`MermaidConfig`>
`Const` **default**: `Partial`<`MermaidConfig`>
Default mermaid configuration options.
@@ -30,4 +30,4 @@ Non-JSON JS default values are listed in this file, e.g. functions, or
#### Defined in
[defaultConfig.ts:18](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L18)
[defaultConfig.ts:16](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L16)

View File

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

View File

@@ -14,12 +14,8 @@ It is a JavaScript based diagramming and charting tool that renders Markdown-ins
<img src="/header.png" alt="" />
<div class='badges'>
[![Build CI Status](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml/badge.svg)](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml) [![NPM](https://img.shields.io/npm/v/mermaid)](https://www.npmjs.com/package/mermaid) [![npm minified gzipped bundle size](https://img.shields.io/bundlephobia/minzip/mermaid)](https://bundlephobia.com/package/mermaid) [![Coverage Status](https://coveralls.io/repos/github/mermaid-js/mermaid/badge.svg?branch=master)](https://coveralls.io/github/mermaid-js/mermaid?branch=master) [![CDN Status](https://img.shields.io/jsdelivr/npm/hm/mermaid)](https://www.jsdelivr.com/package/npm/mermaid) [![NPM](https://img.shields.io/npm/dm/mermaid)](https://www.npmjs.com/package/mermaid) [![Join our Slack!](https://img.shields.io/static/v1?message=join%20chat&color=9cf&logo=slack&label=slack)](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) [![Twitter Follow](https://img.shields.io/twitter/follow/mermaidjs_?style=social)](https://twitter.com/mermaidjs_)
</div>
<!-- Mermaid book banner -->
[![Explore Mermaid.js in depth, with real-world examples, tips & tricks from the creator... The first official book on Mermaid is available for purchase. Check it out!](img/book-banner-post-release.jpg)](https://mermaid-js.github.io/mermaid/landing/)
@@ -393,12 +389,8 @@ The above command generates files into the `dist` folder and publishes them to \
## Contributors
<div class='badges'>
[![Good first issue](https://img.shields.io/github/labels/mermaid-js/mermaid/Good%20first%20issue%21)](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+first+issue%21%22) [![Contributors](https://img.shields.io/github/contributors/mermaid-js/mermaid)](https://github.com/mermaid-js/mermaid/graphs/contributors) [![Commits](https://img.shields.io/github/commit-activity/m/mermaid-js/mermaid)](https://github.com/mermaid-js/mermaid/graphs/contributors)
</div>
Mermaid is a growing community and is always accepting new contributors. There's a lot of different ways to help out and we're always looking for extra hands! Look at [this issue](https://github.com/mermaid-js/mermaid/issues/866) if you want to know where to start helping out.
Detailed information about how to contribute can be found in the [contribution guide](https://github.com/mermaid-js/mermaid/blob/develop/CONTRIBUTING.md)
@@ -432,14 +424,20 @@ A quick note from Knut Sveidqvist:
_Mermaid was created by Knut Sveidqvist for easier documentation._
<style scoped>
.badges > p {
display: flex;
}
.badges > p > a {
margin: 0 0.5rem;
#contributors + p,
#about-mermaid + p + p + blockquote + img + p
{
display: flex
}
.dark #VPContent > div > div > div.content > div > main > div > div > img {
#contributors + p a,
#about-mermaid + p + p + blockquote + img + p a
{
margin: 0 0.5rem
}
.dark #VPContent > div > div > div.content > div > main > div > div > img
{
filter: invert(1) hue-rotate(217deg) contrast(0.72);
}
</style>

View File

@@ -748,48 +748,6 @@ flowchart LR
B1 --> B2
```
#### Limitation
If any of a subgraph's nodes are linked to the outside, subgraph direction will be ignored. Instead the subgraph will inherit the direction of the parent graph:
```mermaid-example
flowchart LR
subgraph subgraph1
direction TB
top1[top] --> bottom1[bottom]
end
subgraph subgraph2
direction TB
top2[top] --> bottom2[bottom]
end
%% ^ These subgraphs are identical, except for the links to them:
%% Link *to* subgraph1: subgraph1 direction is mantained
outside --> subgraph1
%% Link *within* subgraph2:
%% subgraph2 inherits the direction of the top-level graph (LR)
outside ---> top2
```
```mermaid
flowchart LR
subgraph subgraph1
direction TB
top1[top] --> bottom1[bottom]
end
subgraph subgraph2
direction TB
top2[top] --> bottom2[bottom]
end
%% ^ These subgraphs are identical, except for the links to them:
%% Link *to* subgraph1: subgraph1 direction is mantained
outside --> subgraph1
%% Link *within* subgraph2:
%% subgraph2 inherits the direction of the top-level graph (LR)
outside ---> top2
```
## Markdown Strings
The "Markdown Strings" feature enhances flowcharts and mind maps by offering a more versatile string type, which supports text formatting options such as bold and italics, and automatically wraps text within labels.

View File

@@ -4,7 +4,7 @@
"version": "10.2.4",
"description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"type": "module",
"packageManager": "pnpm@8.6.12",
"packageManager": "pnpm@8.6.11",
"keywords": [
"diagram",
"markdown",
@@ -60,37 +60,37 @@
]
},
"devDependencies": {
"@applitools/eyes-cypress": "^3.37.0",
"@applitools/eyes-cypress": "^3.33.1",
"@commitlint/cli": "^17.6.1",
"@commitlint/config-conventional": "^17.6.1",
"@cspell/eslint-plugin": "^7.0.1",
"@cypress/code-coverage": "^3.11.0",
"@rollup/plugin-typescript": "^11.1.2",
"@cspell/eslint-plugin": "^6.31.1",
"@cypress/code-coverage": "^3.10.7",
"@rollup/plugin-typescript": "^11.1.1",
"@types/cors": "^2.8.13",
"@types/eslint": "^8.44.2",
"@types/eslint": "^8.37.0",
"@types/express": "^4.17.17",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^21.1.1",
"@types/lodash": "^4.14.194",
"@types/mdast": "^3.0.11",
"@types/node": "^18.16.0",
"@types/prettier": "^3.0.0",
"@types/prettier": "^2.7.2",
"@types/rollup-plugin-visualizer": "^4.2.1",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"@vitest/coverage-v8": "^0.34.2",
"@vitest/spy": "^0.34.0",
"@vitest/ui": "^0.34.0",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitest/coverage-v8": "^0.33.0",
"@vitest/spy": "^0.33.0",
"@vitest/ui": "^0.33.0",
"ajv": "^8.12.0",
"chokidar": "^3.5.3",
"concurrently": "^8.0.1",
"cors": "^2.8.5",
"cypress": "^12.17.4",
"cypress": "^12.10.0",
"cypress-image-snapshot": "^4.0.1",
"esbuild": "^0.19.2",
"eslint": "^8.47.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-cypress": "^2.14.0",
"esbuild": "^0.19.0",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-cypress": "^2.13.2",
"eslint-plugin-html": "^7.1.0",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-jsdoc": "^46.0.0",
@@ -111,19 +111,19 @@
"nyc": "^15.1.0",
"path-browserify": "^1.0.1",
"pnpm": "^8.6.8",
"prettier": "^3.0.2",
"prettier": "^2.8.8",
"prettier-plugin-jsdoc": "^0.4.2",
"rimraf": "^5.0.0",
"rollup-plugin-visualizer": "^5.9.2",
"start-server-and-test": "^2.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.6",
"vite": "^4.4.9",
"vite-plugin-istanbul": "^5.0.0",
"vitest": "^0.34.2"
"typescript": "^5.1.3",
"vite": "^4.3.9",
"vite-plugin-istanbul": "^4.1.0",
"vitest": "^0.33.0"
},
"volta": {
"node": "18.17.1"
"node": "18.17.0"
},
"nyc": {
"report-dir": "coverage/cypress"

View File

@@ -19,7 +19,6 @@
"mermaid"
],
"scripts": {
"clean": "rimraf dist",
"prepublishOnly": "pnpm -w run build"
},
"repository": {

View File

@@ -1,22 +0,0 @@
{
"detectiveOptions": {
"ts": {
"skipTypeImports": true
},
"es6": {
"skipTypeImports": true
}
},
"fileExtensions": [
"js",
"ts"
],
"excludeRegExp": [
"node_modules",
"docs",
"vitepress",
"detector",
"Detector"
],
"tsConfig": "./tsconfig.json"
}

View File

@@ -38,7 +38,6 @@
"docs:verify-version": "ts-node-esm scripts/update-release-version.mts --verify",
"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",
"checkCircle": "npx madge --circular ./src",
"release": "pnpm build",
"prepublishOnly": "cpy '../../README.*' ./ --cwd=. && pnpm -w run build"
},
@@ -78,13 +77,12 @@
},
"devDependencies": {
"@adobe/jsonschema2md": "^7.1.4",
"@types/d3-scale": "^4.0.3",
"@types/d3-scale-chromatic": "^3.0.0",
"@types/cytoscape": "^3.19.9",
"@types/d3": "^7.4.0",
"@types/d3-sankey": "^0.12.1",
"@types/d3-scale": "^4.0.3",
"@types/d3-scale-chromatic": "^3.0.0",
"@types/d3-selection": "^3.0.5",
"@types/d3-shape": "^3.1.1",
"@types/dompurify": "^3.0.2",
"@types/jsdom": "^21.1.1",
"@types/lodash-es": "^4.17.7",
@@ -113,7 +111,6 @@
"remark-gfm": "^3.0.1",
"rimraf": "^5.0.0",
"start-server-and-test": "^2.0.0",
"type-fest": "^4.1.0",
"typedoc": "^0.24.5",
"typedoc-plugin-markdown": "^3.15.2",
"typescript": "^5.0.4",

View File

@@ -24,7 +24,6 @@ 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
// @ts-ignore Incorrect types
const Ajv2019 = _Ajv2019 as unknown as typeof _Ajv2019.default;
// !!! -- The config.type.js file is created by this script -- !!!

View File

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

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* assignWithDepth Extends the functionality of {@link Object.assign} with the
* assignWithDepth Extends the functionality of {@link ObjectConstructor.assign} with the
* ability to merge arbitrary-depth objects For each key in src with path `k` (recursively)
* performs an Object.assign(dst[`k`], src[`k`]) with a slight change from the typical handling of
* undefined for dst[`k`]: instead of raising an error, dst[`k`] is auto-initialized to `{}` and

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
import type { RequiredDeep } from 'type-fest';
import theme from './themes/index.js';
import type { MermaidConfig } from './config.type.js';
import { type MermaidConfig } from './config.type.js';
// Uses our custom Vite jsonSchemaPlugin to load only the default values from
// our JSON Schema
@@ -15,7 +13,7 @@ import defaultConfigJson from './schemas/config.schema.yaml?only-defaults=true';
* Non-JSON JS default values are listed in this file, e.g. functions, or
* `undefined` (explicitly set so that `configKeys` finds them).
*/
const config: RequiredDeep<MermaidConfig> = {
const config: Partial<MermaidConfig> = {
...defaultConfigJson,
// Set, even though they're `undefined` so that `configKeys` finds these keys
// TODO: Should we replace these with `null` so that they can go in the JSON Schema?
@@ -234,7 +232,7 @@ const config: RequiredDeep<MermaidConfig> = {
},
pie: {
...defaultConfigJson.pie,
useWidth: 984,
useWidth: undefined,
},
requirement: {
...defaultConfigJson.requirement,
@@ -265,5 +263,5 @@ const keyify = (obj: any, prefix = ''): string[] =>
return [...res, prefix + el];
}, []);
export const configKeys: Set<string> = new Set(keyify(config, ''));
export const configKeys: string[] = keyify(config, '');
export default config;

View File

@@ -6,10 +6,14 @@ import type {
DiagramLoader,
ExternalDiagramDefinition,
} from './types.js';
import { anyCommentRegex, directiveRegex, frontMatterRegex } from './regexes.js';
import { frontMatterRegex } from './frontmatter.js';
import { getDiagram, registerDiagram } from './diagramAPI.js';
import { UnknownDiagramError } from '../errors.js';
export const detectors: Record<string, DetectorRecord> = {};
const directive = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
const anyComment = /\s*%%.*\n/gm;
const detectors: Record<string, DetectorRecord> = {};
/**
* Detects the type of the graph text.
@@ -34,10 +38,7 @@ export const detectors: Record<string, DetectorRecord> = {};
* @returns A graph definition key
*/
export const detectType = function (text: string, config?: MermaidConfig): string {
text = text
.replace(frontMatterRegex, '')
.replace(directiveRegex, '')
.replace(anyCommentRegex, '\n');
text = text.replace(frontMatterRegex, '').replace(directive, '').replace(anyComment, '\n');
for (const [key, { detector }] of Object.entries(detectors)) {
const diagram = detector(text, config);
if (diagram) {
@@ -69,6 +70,39 @@ export const registerLazyLoadedDiagrams = (...diagrams: ExternalDiagramDefinitio
}
};
export const loadRegisteredDiagrams = async () => {
log.debug(`Loading registered diagrams`);
// Load all lazy loaded diagrams in parallel
const results = await Promise.allSettled(
Object.entries(detectors).map(async ([key, { detector, loader }]) => {
if (loader) {
try {
getDiagram(key);
} catch (error) {
try {
// Register diagram if it is not already registered
const { diagram, id } = await loader();
registerDiagram(id, diagram, detector);
} catch (err) {
// Remove failed diagram from detectors
log.error(`Failed to load external diagram with key ${key}. Removing from detectors.`);
delete detectors[key];
throw err;
}
}
}
})
);
const failed = results.filter((result) => result.status === 'rejected');
if (failed.length > 0) {
log.error(`Failed to load ${failed.length} external diagrams`);
for (const res of failed) {
log.error(res);
}
throw new Error(`Failed to load ${failed.length} external diagrams`);
}
};
export const addDetector = (key: string, detector: DiagramDetector, loader?: DiagramLoader) => {
if (detectors[key]) {
log.error(`Detector with key ${key} already exists`);

View File

@@ -5,7 +5,7 @@ import er from '../diagrams/er/erDetector.js';
import git from '../diagrams/git/gitGraphDetector.js';
import gantt from '../diagrams/gantt/ganttDetector.js';
import { info } from '../diagrams/info/infoDetector.js';
import { pie } from '../diagrams/pie/pieDetector.js';
import pie from '../diagrams/pie/pieDetector.js';
import quadrantChart from '../diagrams/quadrant-chart/quadrantDetector.js';
import requirement from '../diagrams/requirement/requirementDetector.js';
import sequence from '../diagrams/sequence/sequenceDetector.js';

View File

@@ -4,7 +4,7 @@ import { getConfig as _getConfig } from '../config.js';
import { sanitizeText as _sanitizeText } from '../diagrams/common/common.js';
import { setupGraphViewbox as _setupGraphViewbox } from '../setupGraphViewbox.js';
import { addStylesForDiagram } from '../styles.js';
import type { DiagramDefinition, DiagramDetector } from './types.js';
import { DiagramDefinition, DiagramDetector } from './types.js';
import * as _commonDb from '../commonDb.js';
import { parseDirective as _parseDirective } from '../directiveUtils.js';

View File

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

View File

@@ -1,53 +1,46 @@
import type { MermaidConfig } from '../config.type.js';
import { frontMatterRegex } from './regexes.js';
import type { DiagramDB } from './types.js';
import { DiagramDB } from './types.js';
// The "* as yaml" part is necessary for tree-shaking
import * as yaml from 'js-yaml';
interface FrontMatterMetadata {
// Match Jekyll-style front matter blocks (https://jekyllrb.com/docs/front-matter/).
// Based on regex used by Jekyll: https://github.com/jekyll/jekyll/blob/6dd3cc21c40b98054851846425af06c64f9fb466/lib/jekyll/document.rb#L10
// Note that JS doesn't support the "\A" anchor, which means we can't use
// multiline mode.
// Relevant YAML spec: https://yaml.org/spec/1.2.2/#914-explicit-documents
export const frontMatterRegex = /^-{3}\s*[\n\r](.*?)[\n\r]-{3}\s*[\n\r]+/s;
type FrontMatterMetadata = {
title?: string;
// Allows custom display modes. Currently used for compact mode in gantt charts.
displayMode?: string;
config?: MermaidConfig;
}
};
/**
* Extract and parse frontmatter from text, if present, and sets appropriate
* properties in the provided db.
* @param text - The text that may have a YAML frontmatter.
* @param db - Diagram database, could be of any diagram.
* @param setDiagramConfig - Optional function to set diagram config.
* @returns text with frontmatter stripped out
*/
export function extractFrontMatter(
text: string,
db: DiagramDB,
setDiagramConfig?: (config: MermaidConfig) => void
): string {
export function extractFrontMatter(text: string, db: DiagramDB): string {
const matches = text.match(frontMatterRegex);
if (!matches) {
if (matches) {
const parsed: FrontMatterMetadata = yaml.load(matches[1], {
// To keep things simple, only allow strings, arrays, and plain objects.
// https://www.yaml.org/spec/1.2/spec.html#id2802346
schema: yaml.FAILSAFE_SCHEMA,
}) as FrontMatterMetadata;
if (parsed?.title) {
db.setDiagramTitle?.(parsed.title);
}
if (parsed?.displayMode) {
db.setDisplayMode?.(parsed.displayMode);
}
return text.slice(matches[0].length);
} else {
return text;
}
const parsed: FrontMatterMetadata = yaml.load(matches[1], {
// To support config, we need JSON schema.
// https://www.yaml.org/spec/1.2/spec.html#id2803231
schema: yaml.JSON_SCHEMA,
}) as FrontMatterMetadata;
if (parsed?.title) {
// toString() is necessary because YAML could parse the title as a number/boolean
db.setDiagramTitle?.(parsed.title.toString());
}
if (parsed?.displayMode) {
// toString() is necessary because YAML could parse the title as a number/boolean
db.setDisplayMode?.(parsed.displayMode.toString());
}
if (parsed?.config) {
setDiagramConfig?.(parsed.config);
}
return text.slice(matches[0].length);
}

View File

@@ -1,36 +0,0 @@
import { log } from '../logger.js';
import { detectors } from './detectType.js';
import { getDiagram, registerDiagram } from './diagramAPI.js';
export const loadRegisteredDiagrams = async () => {
log.debug(`Loading registered diagrams`);
// Load all lazy loaded diagrams in parallel
const results = await Promise.allSettled(
Object.entries(detectors).map(async ([key, { detector, loader }]) => {
if (loader) {
try {
getDiagram(key);
} catch (error) {
try {
// Register diagram if it is not already registered
const { diagram, id } = await loader();
registerDiagram(id, diagram, detector);
} catch (err) {
// Remove failed diagram from detectors
log.error(`Failed to load external diagram with key ${key}. Removing from detectors.`);
delete detectors[key];
throw err;
}
}
}
})
);
const failed = results.filter((result) => result.status === 'rejected');
if (failed.length > 0) {
log.error(`Failed to load ${failed.length} external diagrams`);
for (const res of failed) {
log.error(res);
}
throw new Error(`Failed to load ${failed.length} external diagrams`);
}
};

View File

@@ -1,11 +0,0 @@
// Match Jekyll-style front matter blocks (https://jekyllrb.com/docs/front-matter/).
// Based on regex used by Jekyll: https://github.com/jekyll/jekyll/blob/6dd3cc21c40b98054851846425af06c64f9fb466/lib/jekyll/document.rb#L10
// Note that JS doesn't support the "\A" anchor, which means we can't use
// multiline mode.
// Relevant YAML spec: https://yaml.org/spec/1.2.2/#914-explicit-documents
export const frontMatterRegex = /^-{3}\s*[\n\r](.*?)[\n\r]-{3}\s*[\n\r]+/s;
export const directiveRegex =
/%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
export const anyCommentRegex = /\s*%%.*\n/gm;

View File

@@ -1,5 +1,5 @@
import { Diagram } from '../Diagram.js';
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
import type { MermaidConfig } from '../config.type.js';
import type * as d3 from 'd3';
export interface InjectUtils {
@@ -16,19 +16,11 @@ export interface InjectUtils {
* Generic Diagram DB that may apply to any diagram type.
*/
export interface DiagramDB {
// config
getConfig?: () => BaseDiagramConfig | undefined;
// db
clear?: () => void;
setDiagramTitle?: (title: string) => void;
getDiagramTitle?: () => string;
setAccTitle?: (title: string) => void;
getAccTitle?: () => string;
setAccDescription?: (describetion: string) => void;
getAccDescription?: () => string;
setDisplayMode?: (title: string) => void;
getAccTitle?: () => string;
getAccDescription?: () => string;
bindFunctions?: (element: Element) => void;
}

View File

@@ -1,5 +1,5 @@
import common from '../common/common.js';
import * as svgDrawCommon from '../common/svgDrawCommon.js';
import * as svgDrawCommon from '../common/svgDrawCommon';
import { sanitizeUrl } from '@braintree/sanitize-url';
export const drawRect = function (elem, rectData) {

View File

@@ -1,15 +1,15 @@
import { sanitizeText, removeScript, parseGenericTypes } from './common.js';
describe('when securityLevel is antiscript, all script must be removed', () => {
describe('when securityLevel is antiscript, all script must be removed', function () {
/**
* @param original - The original text
* @param result - The expected sanitized text
* @param {string} original The original text
* @param {string} result The expected sanitized text
*/
function compareRemoveScript(original: string, result: string) {
function compareRemoveScript(original, result) {
expect(removeScript(original).trim()).toEqual(result);
}
it('should remove all script block, script inline.', () => {
it('should remove all script block, script inline.', function () {
const labelString = `1
Act1: Hello 1<script src="http://abc.com/script1.js"></script>1
<b>Act2</b>:
@@ -25,7 +25,7 @@ describe('when securityLevel is antiscript, all script must be removed', () => {
compareRemoveScript(labelString, exactlyString);
});
it('should remove all javascript urls', () => {
it('should remove all javascript urls', function () {
compareRemoveScript(
`This is a <a href="javascript:runHijackingScript();">clean link</a> + <a href="javascript:runHijackingScript();">clean link</a>
and <a href="javascript&colon;bipassedMining();">me too</a>`,
@@ -34,11 +34,11 @@ describe('when securityLevel is antiscript, all script must be removed', () => {
);
});
it('should detect malicious images', () => {
it('should detect malicious images', function () {
compareRemoveScript(`<img onerror="alert('hello');">`, `<img>`);
});
it('should detect iframes', () => {
it('should detect iframes', function () {
compareRemoveScript(
`<iframe src="http://abc.com/script1.js"></iframe>
<iframe src="http://example.com/iframeexample"></iframe>`,
@@ -47,8 +47,8 @@ describe('when securityLevel is antiscript, all script must be removed', () => {
});
});
describe('Sanitize text', () => {
it('should remove script tag', () => {
describe('Sanitize text', function () {
it('should remove script tag', function () {
const maliciousStr = 'javajavascript:script:alert(1)';
const result = sanitizeText(maliciousStr, {
securityLevel: 'strict',
@@ -58,8 +58,8 @@ describe('Sanitize text', () => {
});
});
describe('generic parser', () => {
it('should parse generic types', () => {
describe('generic parser', function () {
it('should parse generic types', function () {
expect(parseGenericTypes('test~T~')).toEqual('test<T>');
expect(parseGenericTypes('test~Array~Array~string~~~')).toEqual('test<Array<Array<string>>>');
expect(parseGenericTypes('test~Array~Array~string[]~~~')).toEqual(

View File

@@ -1,7 +1,6 @@
import DOMPurify from 'dompurify';
import { MermaidConfig } from '../../config.type.js';
// Remove and ignore br:s
export const lineBreakRegex = /<br\s*\/?>/gi;
/**

View File

@@ -1,58 +0,0 @@
export interface RectData {
x: number;
y: number;
fill: string;
width: number;
height: number;
stroke: string;
class?: string;
color?: string;
rx?: number;
ry?: number;
attrs?: Record<string, string | number>;
anchor?: string;
}
export interface Bound {
startx: number;
stopx: number;
starty: number;
stopy: number;
fill: string;
stroke: string;
}
export interface TextData {
x: number;
y: number;
anchor: string;
text: string;
textMargin: number;
class?: string;
}
export interface TextObject {
x: number;
y: number;
width: number;
height: number;
fill?: string;
anchor?: string;
'text-anchor': string;
style: string;
textMargin: number;
rx: number;
ry: number;
tspan: boolean;
valign?: string;
}
export type D3RectElement = d3.Selection<SVGRectElement, unknown, Element | null, unknown>;
export type D3UseElement = d3.Selection<SVGUseElement, unknown, Element | null, unknown>;
export type D3ImageElement = d3.Selection<SVGImageElement, unknown, Element | null, unknown>;
export type D3TextElement = d3.Selection<SVGTextElement, unknown, Element | null, unknown>;
export type D3TSpanElement = d3.Selection<SVGTSpanElement, unknown, Element | null, unknown>;

View File

@@ -0,0 +1,114 @@
import { sanitizeUrl } from '@braintree/sanitize-url';
export const drawRect = function (elem, rectData) {
const rectElem = elem.append('rect');
rectElem.attr('x', rectData.x);
rectElem.attr('y', rectData.y);
rectElem.attr('fill', rectData.fill);
rectElem.attr('stroke', rectData.stroke);
rectElem.attr('width', rectData.width);
rectElem.attr('height', rectData.height);
rectElem.attr('rx', rectData.rx);
rectElem.attr('ry', rectData.ry);
if (rectData.attrs !== 'undefined' && rectData.attrs !== null) {
for (let attrKey in rectData.attrs) {
rectElem.attr(attrKey, rectData.attrs[attrKey]);
}
}
if (rectData.class !== 'undefined') {
rectElem.attr('class', rectData.class);
}
return rectElem;
};
/**
* Draws a background rectangle
*
* @param {any} elem Diagram (reference for bounds)
* @param {any} bounds Shape of the rectangle
*/
export const drawBackgroundRect = function (elem, bounds) {
const rectElem = drawRect(elem, {
x: bounds.startx,
y: bounds.starty,
width: bounds.stopx - bounds.startx,
height: bounds.stopy - bounds.starty,
fill: bounds.fill,
stroke: bounds.stroke,
class: 'rect',
});
rectElem.lower();
};
export const drawText = function (elem, textData) {
// Remove and ignore br:s
const nText = textData.text.replace(/<br\s*\/?>/gi, ' ');
const textElem = elem.append('text');
textElem.attr('x', textData.x);
textElem.attr('y', textData.y);
textElem.attr('class', 'legend');
textElem.style('text-anchor', textData.anchor);
if (textData.class !== undefined) {
textElem.attr('class', textData.class);
}
const span = textElem.append('tspan');
span.attr('x', textData.x + textData.textMargin * 2);
span.text(nText);
return textElem;
};
export const drawImage = function (elem, x, y, link) {
const imageElem = elem.append('image');
imageElem.attr('x', x);
imageElem.attr('y', y);
var sanitizedLink = sanitizeUrl(link);
imageElem.attr('xlink:href', sanitizedLink);
};
export const drawEmbeddedImage = function (elem, x, y, link) {
const imageElem = elem.append('use');
imageElem.attr('x', x);
imageElem.attr('y', y);
const sanitizedLink = sanitizeUrl(link);
imageElem.attr('xlink:href', '#' + sanitizedLink);
};
export const getNoteRect = function () {
return {
x: 0,
y: 0,
width: 100,
height: 100,
fill: '#EDF2AE',
stroke: '#666',
anchor: 'start',
rx: 0,
ry: 0,
};
};
export const getTextObj = function () {
return {
x: 0,
y: 0,
width: 100,
height: 100,
fill: undefined,
anchor: undefined,
'text-anchor': 'start',
style: '#666',
textMargin: 0,
rx: 0,
ry: 0,
tspan: true,
valign: undefined,
};
};

View File

@@ -1,126 +0,0 @@
import { sanitizeUrl } from '@braintree/sanitize-url';
import type { Group, SVG } from '../../diagram-api/types.js';
import type {
Bound,
D3ImageElement,
D3RectElement,
D3TSpanElement,
D3TextElement,
D3UseElement,
RectData,
TextData,
TextObject,
} from './commonTypes.js';
import { lineBreakRegex } from './common.js';
export const drawRect = (element: SVG | Group, rectData: RectData): D3RectElement => {
const rectElement: D3RectElement = element.append('rect');
rectElement.attr('x', rectData.x);
rectElement.attr('y', rectData.y);
rectElement.attr('fill', rectData.fill);
rectElement.attr('stroke', rectData.stroke);
rectElement.attr('width', rectData.width);
rectElement.attr('height', rectData.height);
rectData.rx !== undefined && rectElement.attr('rx', rectData.rx);
rectData.ry !== undefined && rectElement.attr('ry', rectData.ry);
if (rectData.attrs !== undefined) {
for (const attrKey in rectData.attrs) {
rectElement.attr(attrKey, rectData.attrs[attrKey]);
}
}
rectData.class !== undefined && rectElement.attr('class', rectData.class);
return rectElement;
};
/**
* Draws a background rectangle
*
* @param element - Diagram (reference for bounds)
* @param bounds - Shape of the rectangle
*/
export const drawBackgroundRect = (element: SVG | Group, bounds: Bound): void => {
const rectData: RectData = {
x: bounds.startx,
y: bounds.starty,
width: bounds.stopx - bounds.startx,
height: bounds.stopy - bounds.starty,
fill: bounds.fill,
stroke: bounds.stroke,
class: 'rect',
};
const rectElement: D3RectElement = drawRect(element, rectData);
rectElement.lower();
};
export const drawText = (element: SVG | Group, textData: TextData): D3TextElement => {
const nText: string = textData.text.replace(lineBreakRegex, ' ');
const textElem: D3TextElement = element.append('text');
textElem.attr('x', textData.x);
textElem.attr('y', textData.y);
textElem.attr('class', 'legend');
textElem.style('text-anchor', textData.anchor);
textData.class !== undefined && textElem.attr('class', textData.class);
const tspan: D3TSpanElement = textElem.append('tspan');
tspan.attr('x', textData.x + textData.textMargin * 2);
tspan.text(nText);
return textElem;
};
export const drawImage = (elem: SVG | Group, x: number, y: number, link: string): void => {
const imageElement: D3ImageElement = elem.append('image');
imageElement.attr('x', x);
imageElement.attr('y', y);
const sanitizedLink: string = sanitizeUrl(link);
imageElement.attr('xlink:href', sanitizedLink);
};
export const drawEmbeddedImage = (
element: SVG | Group,
x: number,
y: number,
link: string
): void => {
const imageElement: D3UseElement = element.append('use');
imageElement.attr('x', x);
imageElement.attr('y', y);
const sanitizedLink: string = sanitizeUrl(link);
imageElement.attr('xlink:href', `#${sanitizedLink}`);
};
export const getNoteRect = (): RectData => {
const noteRectData: RectData = {
x: 0,
y: 0,
width: 100,
height: 100,
fill: '#EDF2AE',
stroke: '#666',
anchor: 'start',
rx: 0,
ry: 0,
};
return noteRectData;
};
export const getTextObj = (): TextObject => {
const testObject: TextObject = {
x: 0,
y: 0,
width: 100,
height: 100,
'text-anchor': 'start',
style: '#666',
textMargin: 0,
rx: 0,
ry: 0,
tspan: true,
};
return testObject;
};

View File

@@ -0,0 +1,10 @@
name,amounts
Foo, 33
Rishab, 12
Alexis, 41
Tom, 16
Courtney, 59
Christina, 38
Jack, 21
Mickey, 25
Paul, 30
1 name amounts
2 Foo 33
3 Rishab 12
4 Alexis 41
5 Tom 16
6 Courtney 59
7 Christina 38
8 Jack 21
9 Mickey 25
10 Paul 30

View File

@@ -0,0 +1,132 @@
import pieDb from '../pieDb.js';
import pie from './pie.jison';
import { setConfig } from '../../../config.js';
setConfig({
securityLevel: 'strict',
});
describe('when parsing pie', function () {
beforeEach(function () {
pie.parser.yy = pieDb;
pie.parser.yy.clear();
});
it('should handle very simple pie', function () {
const res = pie.parser.parse(`pie
"ash" : 100
`);
const sections = pieDb.getSections();
const section1 = sections['ash'];
expect(section1).toBe(100);
});
it('should handle simple pie', function () {
const res = pie.parser.parse(`pie
"ash" : 60
"bat" : 40
`);
const sections = pieDb.getSections();
const section1 = sections['ash'];
expect(section1).toBe(60);
});
it('should handle simple pie with comments', function () {
const res = pie.parser.parse(`pie
%% comments
"ash" : 60
"bat" : 40
`);
const sections = pieDb.getSections();
const section1 = sections['ash'];
expect(section1).toBe(60);
});
it('should handle simple pie with a directive', function () {
const res = pie.parser.parse(`%%{init: {'logLevel':0}}%%
pie
"ash" : 60
"bat" : 40
`);
const sections = pieDb.getSections();
const section1 = sections['ash'];
expect(section1).toBe(60);
});
it('should handle simple pie with a title', function () {
const res = pie.parser.parse(`pie title a 60/40 pie
"ash" : 60
"bat" : 40
`);
const sections = pieDb.getSections();
const title = pieDb.getDiagramTitle();
const section1 = sections['ash'];
expect(section1).toBe(60);
expect(title).toBe('a 60/40 pie');
});
it('should handle simple pie without an acc description (accDescr)', function () {
const res = pie.parser.parse(`pie title a neat chart
"ash" : 60
"bat" : 40
`);
const sections = pieDb.getSections();
const title = pieDb.getDiagramTitle();
const description = pieDb.getAccDescription();
const section1 = sections['ash'];
expect(section1).toBe(60);
expect(title).toBe('a neat chart');
expect(description).toBe('');
});
it('should handle simple pie with an acc description (accDescr)', function () {
const res = pie.parser.parse(`pie title a neat chart
accDescr: a neat description
"ash" : 60
"bat" : 40
`);
const sections = pieDb.getSections();
const title = pieDb.getDiagramTitle();
const description = pieDb.getAccDescription();
const section1 = sections['ash'];
expect(section1).toBe(60);
expect(title).toBe('a neat chart');
expect(description).toBe('a neat description');
});
it('should handle simple pie with a multiline acc description (accDescr)', function () {
const res = pie.parser.parse(`pie title a neat chart
accDescr {
a neat description
on multiple lines
}
"ash" : 60
"bat" : 40
`);
const sections = pieDb.getSections();
const title = pieDb.getDiagramTitle();
const description = pieDb.getAccDescription();
const section1 = sections['ash'];
expect(section1).toBe(60);
expect(title).toBe('a neat chart');
expect(description).toBe('a neat description\non multiple lines');
});
it('should handle simple pie with positive decimal', function () {
const res = pie.parser.parse(`pie
"ash" : 60.67
"bat" : 40
`);
const sections = pieDb.getSections();
const section1 = sections['ash'];
expect(section1).toBe(60.67);
});
it('should handle simple pie with negative decimal', function () {
expect(() => {
pie.parser.parse(`pie
"ash" : 60.67
"bat" : 40..12
`);
}).toThrowError();
});
});

View File

@@ -1,180 +0,0 @@
// @ts-ignore: JISON doesn't support types
import { parser } from './parser/pie.jison';
import { DEFAULT_PIE_DB, db } from './pieDb.js';
import { setConfig } from '../../config.js';
setConfig({
securityLevel: 'strict',
});
describe('pie', () => {
beforeAll(() => {
parser.yy = db;
});
beforeEach(() => {
parser.yy.clear();
});
describe('parse', () => {
it('should handle very simple pie', () => {
parser.parse(`pie
"ash": 100
`);
const sections = db.getSections();
expect(sections['ash']).toBe(100);
});
it('should handle simple pie', () => {
parser.parse(`pie
"ash" : 60
"bat" : 40
`);
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with showData', () => {
parser.parse(`pie showData
"ash" : 60
"bat" : 40
`);
expect(db.getShowData()).toBeTruthy();
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with comments', () => {
parser.parse(`pie
%% comments
"ash" : 60
"bat" : 40
`);
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with a directive', () => {
parser.parse(`%%{init: {'logLevel':0}}%%
pie
"ash" : 60
"bat" : 40
`);
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with a title', () => {
parser.parse(`pie title a 60/40 pie
"ash" : 60
"bat" : 40
`);
expect(db.getDiagramTitle()).toBe('a 60/40 pie');
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with an acc title (accTitle)', () => {
parser.parse(`pie title a neat chart
accTitle: a neat acc title
"ash" : 60
"bat" : 40
`);
expect(db.getDiagramTitle()).toBe('a neat chart');
expect(db.getAccTitle()).toBe('a neat acc title');
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with an acc description (accDescr)', () => {
parser.parse(`pie title a neat chart
accDescr: a neat description
"ash" : 60
"bat" : 40
`);
expect(db.getDiagramTitle()).toBe('a neat chart');
expect(db.getAccDescription()).toBe('a neat description');
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with a multiline acc description (accDescr)', () => {
parser.parse(`pie title a neat chart
accDescr {
a neat description
on multiple lines
}
"ash" : 60
"bat" : 40
`);
expect(db.getDiagramTitle()).toBe('a neat chart');
expect(db.getAccDescription()).toBe('a neat description\non multiple lines');
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with positive decimal', () => {
parser.parse(`pie
"ash" : 60.67
"bat" : 40
`);
const sections = db.getSections();
expect(sections['ash']).toBe(60.67);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with negative decimal', () => {
expect(() => {
parser.parse(`pie
"ash" : -60.67
"bat" : 40.12
`);
}).toThrowError();
});
});
describe('config', () => {
it.todo('setConfig', () => {
// db.setConfig({ useWidth: 850, useMaxWidth: undefined });
const config = db.getConfig();
expect(config.useWidth).toBe(850);
expect(config.useMaxWidth).toBeTruthy();
});
it('getConfig', () => {
expect(db.getConfig()).toStrictEqual(DEFAULT_PIE_DB.config);
});
it.todo('resetConfig', () => {
// db.setConfig({ textPosition: 0 });
// db.resetConfig();
expect(db.getConfig().textPosition).toStrictEqual(DEFAULT_PIE_DB.config.textPosition);
});
});
});

View File

@@ -0,0 +1,69 @@
import { log } from '../../logger.js';
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js';
import common from '../common/common.js';
import {
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
getAccDescription,
setAccDescription,
clear as commonClear,
} from '../../commonDb.js';
let sections = {};
let showData = false;
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
const addSection = function (id, value) {
id = common.sanitizeText(id, configApi.getConfig());
if (sections[id] === undefined) {
sections[id] = value;
log.debug('Added new section :', id);
}
};
const getSections = () => sections;
const setShowData = function (toggle) {
showData = toggle;
};
const getShowData = function () {
return showData;
};
const cleanupValue = function (value) {
if (value.substring(0, 1) === ':') {
value = value.substring(1).trim();
return Number(value.trim());
} else {
return Number(value.trim());
}
};
const clear = function () {
sections = {};
showData = false;
commonClear();
};
export default {
parseDirective,
getConfig: () => configApi.getConfig().pie,
addSection,
getSections,
cleanupValue,
clear,
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
setShowData,
getShowData,
getAccDescription,
setAccDescription,
};

View File

@@ -1,84 +0,0 @@
import { log } from '../../logger.js';
import { parseDirective as _parseDirective } from '../../directiveUtils.js';
import { getConfig as commonGetConfig } from '../../config.js';
import { sanitizeText } from '../common/common.js';
import {
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
getAccDescription,
setAccDescription,
clear as commonClear,
} from '../../commonDb.js';
import type { ParseDirectiveDefinition } from '../../diagram-api/types.js';
import type { PieFields, PieDB, Sections } from './pieTypes.js';
import type { RequiredDeep } from 'type-fest';
import type { PieDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
export const DEFAULT_PIE_CONFIG: Required<PieDiagramConfig> = DEFAULT_CONFIG.pie;
export const DEFAULT_PIE_DB: RequiredDeep<PieFields> = {
sections: {},
showData: false,
config: DEFAULT_PIE_CONFIG,
} as const;
let sections: Sections = DEFAULT_PIE_DB.sections;
let showData: boolean = DEFAULT_PIE_DB.showData;
const config: Required<PieDiagramConfig> = structuredClone(DEFAULT_PIE_CONFIG);
const getConfig = (): Required<PieDiagramConfig> => structuredClone(config);
const parseDirective: ParseDirectiveDefinition = (statement, context, type) => {
_parseDirective(this, statement, context, type);
};
const clear = (): void => {
sections = structuredClone(DEFAULT_PIE_DB.sections);
showData = DEFAULT_PIE_DB.showData;
commonClear();
};
const addSection = (label: string, value: number): void => {
label = sanitizeText(label, commonGetConfig());
if (sections[label] === undefined) {
sections[label] = value;
log.debug(`added new section: ${label}, with value: ${value}`);
}
};
const getSections = (): Sections => sections;
const cleanupValue = (value: string): number => {
if (value.substring(0, 1) === ':') {
value = value.substring(1).trim();
}
return Number(value.trim());
};
const setShowData = (toggle: boolean): void => {
showData = toggle;
};
const getShowData = (): boolean => showData;
export const db: PieDB = {
getConfig,
parseDirective,
clear,
setDiagramTitle,
getDiagramTitle,
setAccTitle,
getAccTitle,
setAccDescription,
getAccDescription,
addSection,
getSections,
cleanupValue,
setShowData,
getShowData,
};

View File

@@ -15,8 +15,10 @@ const loader: DiagramLoader = async () => {
return { id, diagram };
};
export const pie: ExternalDiagramDefinition = {
const plugin: ExternalDiagramDefinition = {
id,
detector,
loader,
};
export default plugin;

View File

@@ -1,9 +1,9 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types
import parser from './parser/pie.jison';
import { db } from './pieDb.js';
import styles from './pieStyles.js';
import { renderer } from './pieRenderer.js';
import db from './pieDb.js';
import styles from './styles.js';
import renderer from './pieRenderer.js';
export const diagram: DiagramDefinition = {
parser,

View File

@@ -0,0 +1,204 @@
/** Created by AshishJ on 11-09-2019. */
import { select, scaleOrdinal, pie as d3pie, arc } from 'd3';
import { log } from '../../logger.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import * as configApi from '../../config.js';
import { parseFontSize } from '../../utils.js';
let conf = configApi.getConfig();
/**
* Draws a Pie Chart with the data given in text.
*
* @param text
* @param id
*/
let width;
const height = 450;
export const draw = (txt, id, _version, diagObj) => {
try {
conf = configApi.getConfig();
log.debug('Rendering info diagram\n' + txt);
const securityLevel = configApi.getConfig().securityLevel;
// Handle root and Document for when rendering in sandbox mode
let sandboxElement;
if (securityLevel === 'sandbox') {
sandboxElement = select('#i' + id);
}
const root =
securityLevel === 'sandbox'
? select(sandboxElement.nodes()[0].contentDocument.body)
: select('body');
const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
// Parse the Pie Chart definition
const elem = doc.getElementById(id);
width = elem.parentElement.offsetWidth;
if (width === undefined) {
width = 1200;
}
if (conf.useWidth !== undefined) {
width = conf.useWidth;
}
if (conf.pie.useWidth !== undefined) {
width = conf.pie.useWidth;
}
const diagram = root.select('#' + id);
configureSvgSize(diagram, height, width, conf.pie.useMaxWidth);
// Set viewBox
elem.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
// Fetch the default direction, use TD if none was found
var margin = 40;
var legendRectSize = 18;
var legendSpacing = 4;
var radius = Math.min(width, height) / 2 - margin;
var svg = diagram
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
var data = diagObj.db.getSections();
var sum = 0;
Object.keys(data).forEach(function (key) {
sum += data[key];
});
const themeVariables = conf.themeVariables;
var myGeneratedColors = [
themeVariables.pie1,
themeVariables.pie2,
themeVariables.pie3,
themeVariables.pie4,
themeVariables.pie5,
themeVariables.pie6,
themeVariables.pie7,
themeVariables.pie8,
themeVariables.pie9,
themeVariables.pie10,
themeVariables.pie11,
themeVariables.pie12,
];
const textPosition = conf.pie?.textPosition ?? 0.75;
let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth);
outerStrokeWidth ??= 2;
// Set the color scale
var color = scaleOrdinal().range(myGeneratedColors);
// Compute the position of each group on the pie:
var pieData = Object.entries(data).map(function (el, idx) {
return {
order: idx,
name: el[0],
value: el[1],
};
});
var pie = d3pie()
.value(function (d) {
return d.value;
})
.sort(function (a, b) {
// Sort slices in clockwise direction
return a.order - b.order;
});
var dataReady = pie(pieData);
// Shape helper to build arcs:
var arcGenerator = arc().innerRadius(0).outerRadius(radius);
var labelArcGenerator = arc()
.innerRadius(radius * textPosition)
.outerRadius(radius * textPosition);
svg
.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', radius + outerStrokeWidth / 2)
.attr('class', 'pieOuterCircle');
// Build the pie chart: each part of the pie is a path that we build using the arc function.
svg
.selectAll('mySlices')
.data(dataReady)
.enter()
.append('path')
.attr('d', arcGenerator)
.attr('fill', function (d) {
return color(d.data.name);
})
.attr('class', 'pieCircle');
// Now add the percentage.
// Use the centroid method to get the best coordinates.
svg
.selectAll('mySlices')
.data(dataReady)
.enter()
.append('text')
.text(function (d) {
return ((d.data.value / sum) * 100).toFixed(0) + '%';
})
.attr('transform', function (d) {
return 'translate(' + labelArcGenerator.centroid(d) + ')';
})
.style('text-anchor', 'middle')
.attr('class', 'slice');
svg
.append('text')
.text(diagObj.db.getDiagramTitle())
.attr('x', 0)
.attr('y', -(height - 50) / 2)
.attr('class', 'pieTitleText');
// Add the legends/annotations for each section
var legend = svg
.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function (d, i) {
const height = legendRectSize + legendSpacing;
const offset = (height * color.domain().length) / 2;
const horizontal = 12 * legendRectSize;
const vertical = i * height - offset;
return 'translate(' + horizontal + ',' + vertical + ')';
});
legend
.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color);
legend
.data(dataReady)
.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function (d) {
if (diagObj.db.getShowData() || conf.showData || conf.pie.showData) {
return d.data.name + ' [' + d.data.value + ']';
} else {
return d.data.name;
}
});
} catch (e) {
log.error('Error while rendering info diagram');
log.error(e);
}
};
export default {
draw,
};

View File

@@ -1,179 +0,0 @@
import d3, { scaleOrdinal, pie as d3pie, arc } from 'd3';
import { log } from '../../logger.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import { getConfig } from '../../config.js';
import { cleanAndMerge, parseFontSize } from '../../utils.js';
import type { DrawDefinition, Group, SVG } from '../../diagram-api/types.js';
import type { D3Sections, PieDB, Sections } from './pieTypes.js';
import type { MermaidConfig, PieDiagramConfig } from '../../config.type.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
const createPieArcs = (sections: Sections): d3.PieArcDatum<D3Sections>[] => {
// Compute the position of each group on the pie:
const pieData: D3Sections[] = Object.entries(sections).map(
(element: [string, number]): D3Sections => {
return {
label: element[0],
value: element[1],
};
}
);
const pie: d3.Pie<unknown, D3Sections> = d3pie<D3Sections>().value(
(d3Section: D3Sections): number => d3Section.value
);
return pie(pieData);
};
/**
* Draws a Pie Chart with the data given in text.
*
* @param text - pie chart code
* @param id - diagram id
* @param _version - MermaidJS version from package.json.
* @param diagObj - A standard diagram containing the DB and the text and type etc of the diagram.
*/
export const draw: DrawDefinition = (text, id, _version, diagObj) => {
log.debug('rendering pie chart\n' + text);
const db = diagObj.db as PieDB;
const globalConfig: MermaidConfig = getConfig();
const pieConfig: Required<PieDiagramConfig> = cleanAndMerge(db.getConfig(), globalConfig.pie);
const height = 450;
// TODO: remove document width
const width: number =
document.getElementById(id)?.parentElement?.offsetWidth ?? pieConfig.useWidth;
const svg: SVG = selectSvgElement(id);
// Set viewBox
svg.attr('viewBox', `0 0 ${width} ${height}`);
configureSvgSize(svg, height, width, pieConfig.useMaxWidth);
const MARGIN = 40;
const LEGEND_RECT_SIZE = 18;
const LEGEND_SPACING = 4;
const group: Group = svg.append('g');
group.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
const { themeVariables } = globalConfig;
let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth);
outerStrokeWidth ??= 2;
const textPosition: number = pieConfig.textPosition;
const radius: number = Math.min(width, height) / 2 - MARGIN;
// Shape helper to build arcs:
const arcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Sections>> = arc<
d3.PieArcDatum<D3Sections>
>()
.innerRadius(0)
.outerRadius(radius);
const labelArcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Sections>> = arc<
d3.PieArcDatum<D3Sections>
>()
.innerRadius(radius * textPosition)
.outerRadius(radius * textPosition);
group
.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', radius + outerStrokeWidth / 2)
.attr('class', 'pieOuterCircle');
const sections: Sections = db.getSections();
const arcs: d3.PieArcDatum<D3Sections>[] = createPieArcs(sections);
const myGeneratedColors = [
themeVariables.pie1,
themeVariables.pie2,
themeVariables.pie3,
themeVariables.pie4,
themeVariables.pie5,
themeVariables.pie6,
themeVariables.pie7,
themeVariables.pie8,
themeVariables.pie9,
themeVariables.pie10,
themeVariables.pie11,
themeVariables.pie12,
];
// Set the color scale
const color: d3.ScaleOrdinal<string, 12, never> = scaleOrdinal(myGeneratedColors);
// Build the pie chart: each part of the pie is a path that we build using the arc function.
group
.selectAll('mySlices')
.data(arcs)
.enter()
.append('path')
.attr('d', arcGenerator)
.attr('fill', (datum: d3.PieArcDatum<D3Sections>) => {
return color(datum.data.label);
})
.attr('class', 'pieCircle');
let sum = 0;
Object.keys(sections).forEach((key: string): void => {
sum += sections[key];
});
// Now add the percentage.
// Use the centroid method to get the best coordinates.
group
.selectAll('mySlices')
.data(arcs)
.enter()
.append('text')
.text((datum: d3.PieArcDatum<D3Sections>): string => {
return ((datum.data.value / sum) * 100).toFixed(0) + '%';
})
.attr('transform', (datum: d3.PieArcDatum<D3Sections>): string => {
return 'translate(' + labelArcGenerator.centroid(datum) + ')';
})
.style('text-anchor', 'middle')
.attr('class', 'slice');
group
.append('text')
.text(db.getDiagramTitle())
.attr('x', 0)
.attr('y', -(height - 50) / 2)
.attr('class', 'pieTitleText');
// Add the legends/annotations for each section
const legend = group
.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', (_datum, index: number): string => {
const height = LEGEND_RECT_SIZE + LEGEND_SPACING;
const offset = (height * color.domain().length) / 2;
const horizontal = 12 * LEGEND_RECT_SIZE;
const vertical = index * height - offset;
return 'translate(' + horizontal + ',' + vertical + ')';
});
legend
.append('rect')
.attr('width', LEGEND_RECT_SIZE)
.attr('height', LEGEND_RECT_SIZE)
.style('fill', color)
.style('stroke', color);
legend
.data(arcs)
.append('text')
.attr('x', LEGEND_RECT_SIZE + LEGEND_SPACING)
.attr('y', LEGEND_RECT_SIZE - LEGEND_SPACING)
.text((datum: d3.PieArcDatum<D3Sections>): string => {
const { label, value } = datum.data;
if (db.getShowData()) {
return `${label} [${value}]`;
}
return label;
});
};
export const renderer = { draw };

View File

@@ -1,64 +0,0 @@
import type { PieDiagramConfig } from '../../config.type.js';
import type { DiagramDB, ParseDirectiveDefinition } from '../../diagram-api/types.js';
export interface PieFields {
sections: Sections;
showData: boolean;
config: PieDiagramConfig;
}
export interface PieStyleOptions {
fontFamily: string;
pie1: string;
pie2: string;
pie3: string;
pie4: string;
pie5: string;
pie6: string;
pie7: string;
pie8: string;
pie9: string;
pie10: string;
pie11: string;
pie12: string;
pieTitleTextSize: string;
pieTitleTextColor: string;
pieSectionTextSize: string;
pieSectionTextColor: string;
pieLegendTextSize: string;
pieLegendTextColor: string;
pieStrokeColor: string;
pieStrokeWidth: string;
pieOuterStrokeWidth: string;
pieOuterStrokeColor: string;
pieOpacity: string;
}
export type Sections = Record<string, number>;
export interface D3Sections {
label: string;
value: number;
}
export interface PieDB extends DiagramDB {
// config
getConfig: () => Required<PieDiagramConfig>;
// common db
parseDirective: ParseDirectiveDefinition;
clear: () => void;
setDiagramTitle: (title: string) => void;
getDiagramTitle: () => string;
setAccTitle: (title: string) => void;
getAccTitle: () => string;
setAccDescription: (describetion: string) => void;
getAccDescription: () => string;
// diagram db
addSection: (label: string, value: number) => void;
getSections: () => Sections;
cleanupValue: (value: string) => number;
setShowData: (toggle: boolean) => void;
getShowData: () => boolean;
}

View File

@@ -1,7 +1,4 @@
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
import type { PieStyleOptions } from './pieTypes.js';
const getStyles: DiagramStylesProvider = (options: PieStyleOptions) =>
const getStyles = (options) =>
`
.pieCircle{
stroke: ${options.pieStrokeColor};

View File

@@ -3,7 +3,7 @@ import { select, selectAll } from 'd3';
import svgDraw, { ACTOR_TYPE_WIDTH, drawText, fixLifeLineHeights } from './svgDraw.js';
import { log } from '../../logger.js';
import common from '../common/common.js';
import * as svgDrawCommon from '../common/svgDrawCommon.js';
import * as svgDrawCommon from '../common/svgDrawCommon';
import * as configApi from '../../config.js';
import assignWithDepth from '../../assignWithDepth.js';
import utils from '../../utils.js';

View File

@@ -1,5 +1,5 @@
import common from '../common/common.js';
import * as svgDrawCommon from '../common/svgDrawCommon.js';
import * as svgDrawCommon from '../common/svgDrawCommon';
import { addFunction } from '../../interactionDb.js';
import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js';
import { sanitizeUrl } from '@braintree/sanitize-url';

View File

@@ -1,5 +1,5 @@
import { arc as d3arc } from 'd3';
import * as svgDrawCommon from '../common/svgDrawCommon.js';
import * as svgDrawCommon from '../common/svgDrawCommon';
export const drawRect = function (elem, rectData) {
return svgDrawCommon.drawRect(elem, rectData);

View File

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

View File

@@ -101,7 +101,7 @@ function sidebarAll() {
return [
{
text: '📔 Introduction',
collapsed: false,
collapsible: true,
items: [
{ text: 'About Mermaid', link: '/intro/' },
{ text: 'Deployment', link: '/intro/n00b-gettingStarted' },
@@ -123,7 +123,7 @@ function sidebarSyntax() {
return [
{
text: '📊 Diagram Syntax',
collapsed: false,
collapsible: true,
items: [
{ text: 'Flowchart', link: '/syntax/flowchart' },
{ text: 'Sequence Diagram', link: '/syntax/sequenceDiagram' },
@@ -154,7 +154,7 @@ function sidebarConfig() {
return [
{
text: '⚙️ Deployment and Configuration',
collapsed: false,
collapsible: true,
items: [
{ text: 'Configuration', link: '/config/configuration' },
{ text: 'Tutorials', link: '/config/Tutorials' },
@@ -176,7 +176,7 @@ function sidebarEcosystem() {
return [
{
text: '📚 Ecosystem',
collapsed: false,
collapsible: true,
items: [
{ text: 'Showcases', link: '/ecosystem/showcases' },
{ text: 'Use-Cases and Integrations', link: '/ecosystem/integrations' },
@@ -189,7 +189,7 @@ function sidebarCommunity() {
return [
{
text: '🙌 Contributions and Community',
collapsed: false,
collapsible: true,
items: [
{ text: 'Overview for Beginners', link: '/community/n00b-overview' },
...sidebarCommunityDevelopContribute(),
@@ -207,7 +207,7 @@ function sidebarCommunityDevelopContribute() {
{
text: 'Contributing to Mermaid',
link: page_path + '#contributing-to-mermaid',
collapsed: false,
collapsible: true,
items: [
{
text: 'Technical Requirements and Setup',
@@ -238,7 +238,7 @@ function sidebarNews() {
return [
{
text: '📰 Latest News',
collapsed: false,
collapsible: true,
items: [
{ text: 'Announcements', link: '/news/announcements' },
{ text: 'Blog', link: '/news/blog' },

View File

@@ -24,9 +24,6 @@ const getBaseFile = (url: URL): Redirect => {
return { path, id };
};
/**
* Used to redirect old documentation pages to corresponding new pages.
*/
const idRedirectMap: Record<string, string> = {
'8.6.0_docs': '',
accessibility: 'config/theming',
@@ -71,9 +68,6 @@ const idRedirectMap: Record<string, string> = {
'user-journey': 'syntax/userJourney',
};
/**
* Used to redirect pages that have been moved in the vitepress site.
*/
const urlRedirectMap: Record<string, string> = {
'/misc/faq.html': 'configure/faq.html',
'/syntax/c4c.html': 'syntax/c4.html',

View File

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

View File

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

View File

@@ -8,12 +8,8 @@ It is a JavaScript based diagramming and charting tool that renders Markdown-ins
<img src="/header.png" alt="" />
<div class='badges'>
[![Build CI Status](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml/badge.svg)](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml) [![NPM](https://img.shields.io/npm/v/mermaid)](https://www.npmjs.com/package/mermaid) [![npm minified gzipped bundle size](https://img.shields.io/bundlephobia/minzip/mermaid)](https://bundlephobia.com/package/mermaid) [![Coverage Status](https://coveralls.io/repos/github/mermaid-js/mermaid/badge.svg?branch=master)](https://coveralls.io/github/mermaid-js/mermaid?branch=master) [![CDN Status](https://img.shields.io/jsdelivr/npm/hm/mermaid)](https://www.jsdelivr.com/package/npm/mermaid) [![NPM](https://img.shields.io/npm/dm/mermaid)](https://www.npmjs.com/package/mermaid) [![Join our Slack!](https://img.shields.io/static/v1?message=join%20chat&color=9cf&logo=slack&label=slack)](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) [![Twitter Follow](https://img.shields.io/twitter/follow/mermaidjs_?style=social)](https://twitter.com/mermaidjs_)
</div>
<!-- Mermaid book banner -->
[![Explore Mermaid.js in depth, with real-world examples, tips & tricks from the creator... The first official book on Mermaid is available for purchase. Check it out!](img/book-banner-post-release.jpg)](https://mermaid-js.github.io/mermaid/landing/)
@@ -170,12 +166,8 @@ The above command generates files into the `dist` folder and publishes them to <
## Contributors
<div class='badges'>
[![Good first issue](https://img.shields.io/github/labels/mermaid-js/mermaid/Good%20first%20issue%21)](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+first+issue%21%22) [![Contributors](https://img.shields.io/github/contributors/mermaid-js/mermaid)](https://github.com/mermaid-js/mermaid/graphs/contributors) [![Commits](https://img.shields.io/github/commit-activity/m/mermaid-js/mermaid)](https://github.com/mermaid-js/mermaid/graphs/contributors)
</div>
Mermaid is a growing community and is always accepting new contributors. There's a lot of different ways to help out and we're always looking for extra hands! Look at [this issue](https://github.com/mermaid-js/mermaid/issues/866) if you want to know where to start helping out.
Detailed information about how to contribute can be found in the [contribution guide](https://github.com/mermaid-js/mermaid/blob/develop/CONTRIBUTING.md)
@@ -209,14 +201,20 @@ A quick note from Knut Sveidqvist:
_Mermaid was created by Knut Sveidqvist for easier documentation._
<style scoped>
.badges > p {
display: flex;
}
.badges > p > a {
margin: 0 0.5rem;
#contributors + p,
#about-mermaid + p + p + blockquote + img + p
{
display: flex
}
.dark #VPContent > div > div > div.content > div > main > div > div > img {
#contributors + p a,
#about-mermaid + p + p + blockquote + img + p a
{
margin: 0 0.5rem
}
.dark #VPContent > div > div > div.content > div > main > div > div > img
{
filter: invert(1) hue-rotate(217deg) contrast(0.72);
}
</style>

View File

@@ -17,22 +17,21 @@
"dependencies": {
"@vueuse/core": "^10.1.0",
"jiti": "^1.18.2",
"vue": "^3.3",
"mermaid": "workspace:^"
"vue": "^3.2.47"
},
"devDependencies": {
"@iconify-json/carbon": "^1.1.16",
"@unocss/reset": "^0.55.2",
"@unocss/reset": "^0.54.0",
"@vite-pwa/vitepress": "^0.2.0",
"@vitejs/plugin-vue": "^4.2.1",
"fast-glob": "^3.2.12",
"https-localhost": "^4.7.1",
"pathe": "^1.1.0",
"unocss": "^0.55.2",
"unocss": "^0.54.0",
"unplugin-vue-components": "^0.25.0",
"vite": "^4.3.9",
"vite-plugin-pwa": "^0.16.0",
"vitepress": "1.0.0-rc.4",
"vitepress": "1.0.0-beta.7",
"workbox-window": "^7.0.0"
}
}

View File

@@ -471,29 +471,6 @@ flowchart LR
B1 --> B2
```
#### Limitation
If any of a subgraph's nodes are linked to the outside, subgraph direction will be ignored. Instead the subgraph will inherit the direction of the parent graph:
```mermaid-example
flowchart LR
subgraph subgraph1
direction TB
top1[top] --> bottom1[bottom]
end
subgraph subgraph2
direction TB
top2[top] --> bottom2[bottom]
end
%% ^ These subgraphs are identical, except for the links to them:
%% Link *to* subgraph1: subgraph1 direction is mantained
outside --> subgraph1
%% Link *within* subgraph2:
%% subgraph2 inherits the direction of the top-level graph (LR)
outside ---> top2
```
## Markdown Strings
The "Markdown Strings" feature enhances flowcharts and mind maps by offering a more versatile string type, which supports text formatting options such as bold and italics, and automatically wraps text within labels.

View File

@@ -7,8 +7,11 @@ import { MermaidConfig } from './config.type.js';
import { log } from './logger.js';
import utils from './utils.js';
import { mermaidAPI, ParseOptions, RenderResult } from './mermaidAPI.js';
import { registerLazyLoadedDiagrams, detectType } from './diagram-api/detectType.js';
import { loadRegisteredDiagrams } from './diagram-api/loadDiagram.js';
import {
registerLazyLoadedDiagrams,
loadRegisteredDiagrams,
detectType,
} from './diagram-api/detectType.js';
import type { ParseErrorFunction } from './Diagram.js';
import { isDetailedError } from './utils.js';
import type { DetailedError } from './utils.js';

View File

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

View File

@@ -7,21 +7,21 @@
# - `scripts/create-types-from-json-schema.mjs`
# Used to generate the `src/config.type.ts` TypeScript types for MermaidConfig
# with the `json-schema-to-typescript` NPM package.
# - `.build/jsonSchema.ts`
# - `.vite/jsonSchemaPlugin.ts`
# Used to generate the default values from the `default` keys in this
# JSON Schema using the `ajv` NPM package.
# Non-JSON values, like functions or `undefined`, still need to be manually
# set in `src/defaultConfig.ts`)
# - `src/docs.mts`
# Used to generate Markdown documentation for this JSON Schema by using
# Used to genereate Markdown documentation for this JSON Schema by using
# the `@adobe/jsonschema2md` NPM package.
# Useful things to know when editing this file
# Useful things to know when editting this file
# - Use the `|` character for multi-line strings
# - Use `meta:enum` to document enum values (from jsonschema2md)
# - Use `tsType` to override the TypeScript type (from json-schema-to-typescript)
# - If adding a new object to `MermaidConfig` (e.g. a new diagram type),
# you may need to add it to `.build/jsonSchema.ts` and `src/docs.mts`
# you may need to add it to `.vite/jsonSchemaPlugin.ts` and `src/docs.mts`
# to get the docs/default values to generate properly.
$id: https://mermaid-js.github.io/schemas/config.schema.json
$schema: https://json-schema.org/draft/2019-09/schema
@@ -1851,7 +1851,7 @@ $defs: # JSON Schema definition (maybe we should move these to a seperate file)
The color of the links in the sankey diagram.
anyOf:
- $ref: '#/$defs/SankeyLinkColor'
- description: An arbitrary [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value)
- description: An arbtirary [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value)
type: string
default: gradient
nodeAlignment:

View File

@@ -1,4 +1,5 @@
import { log } from './logger.js';
import { SVG } from './diagram-api/types.js';
/**
* Applies d3 attributes
@@ -35,7 +36,7 @@ export const calculateSvgSizeAttrs = function (height, width, useMaxWidth) {
/**
* Applies attributes from `calculateSvgSizeAttrs`
*
* @param {import('./diagram-api/types.js').SVG} svgElem The SVG Element to configure
* @param {SVG} svgElem The SVG Element to configure
* @param {number} height The height of the SVG
* @param {number} width The width of the SVG
* @param {boolean} useMaxWidth Whether or not to use max-width and set width to 100%

View File

@@ -21,7 +21,7 @@ import flowchartElk from './diagrams/flowchart/elk/styles.js';
import er from './diagrams/er/styles.js';
import git from './diagrams/git/styles.js';
import gantt from './diagrams/gantt/styles.js';
import pie from './diagrams/pie/pieStyles.js';
import pie from './diagrams/pie/styles.js';
import requirement from './diagrams/requirement/styles.js';
import sequence from './diagrams/sequence/styles.js';
import state from './diagrams/state/styles.js';

View File

@@ -1,5 +1,5 @@
import { vi } from 'vitest';
import utils, { cleanAndMerge } from './utils.js';
import utils from './utils.js';
import assignWithDepth from './assignWithDepth.js';
import { detectType } from './diagram-api/detectType.js';
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
@@ -10,51 +10,51 @@ addDiagrams();
describe('when assignWithDepth: should merge objects within objects', function () {
it('should handle simple, depth:1 types (identity)', function () {
const config_0 = { foo: 'bar', bar: 0 };
const config_1 = { foo: 'bar', bar: 0 };
const result = assignWithDepth(config_0, config_1);
let config_0 = { foo: 'bar', bar: 0 };
let config_1 = { foo: 'bar', bar: 0 };
let result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_1);
});
it('should handle simple, depth:1 types (dst: undefined)', function () {
const config_0 = undefined;
const config_1 = { foo: 'bar', bar: 0 };
const result = assignWithDepth(config_0, config_1);
let config_0 = undefined;
let config_1 = { foo: 'bar', bar: 0 };
let result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_1);
});
it('should handle simple, depth:1 types (src: undefined)', function () {
const config_0 = { foo: 'bar', bar: 0 };
const config_1 = undefined;
const result = assignWithDepth(config_0, config_1);
let config_0 = { foo: 'bar', bar: 0 };
let config_1 = undefined;
let result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_0);
});
it('should handle simple, depth:1 types (merge)', function () {
const config_0 = { foo: 'bar', bar: 0 };
const config_1 = { foo: 'foo' };
const result = assignWithDepth(config_0, config_1);
let config_0 = { foo: 'bar', bar: 0 };
let config_1 = { foo: 'foo' };
let result = assignWithDepth(config_0, config_1);
expect(result).toEqual({ foo: 'foo', bar: 0 });
});
it('should handle depth:2 types (dst: orphan)', function () {
const config_0 = { foo: 'bar', bar: { foo: 'bar' } };
const config_1 = { foo: 'bar' };
const result = assignWithDepth(config_0, config_1);
let config_0 = { foo: 'bar', bar: { foo: 'bar' } };
let config_1 = { foo: 'bar' };
let result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_0);
});
it('should handle depth:2 types (dst: object, src: simple type)', function () {
const config_0 = { foo: 'bar', bar: { foo: 'bar' } };
const config_1 = { foo: 'foo', bar: 'should NOT clobber' };
const result = assignWithDepth(config_0, config_1);
let config_0 = { foo: 'bar', bar: { foo: 'bar' } };
let config_1 = { foo: 'foo', bar: 'should NOT clobber' };
let result = assignWithDepth(config_0, config_1);
expect(result).toEqual({ foo: 'foo', bar: { foo: 'bar' } });
});
it('should handle depth:2 types (src: orphan)', function () {
const config_0 = { foo: 'bar' };
const config_1 = { foo: 'bar', bar: { foo: 'bar' } };
const result = assignWithDepth(config_0, config_1);
let config_0 = { foo: 'bar' };
let config_1 = { foo: 'bar', bar: { foo: 'bar' } };
let result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_1);
});
it('should handle depth:2 types (merge)', function () {
const config_0 = { foo: 'bar', bar: { foo: 'bar' }, boofar: 1 };
const config_1 = { foo: 'foo', bar: { bar: 0 }, foobar: 'foobar' };
const result = assignWithDepth(config_0, config_1);
let config_0 = { foo: 'bar', bar: { foo: 'bar' }, boofar: 1 };
let config_1 = { foo: 'foo', bar: { bar: 0 }, foobar: 'foobar' };
let result = assignWithDepth(config_0, config_1);
expect(result).toEqual({
foo: 'foo',
bar: { foo: 'bar', bar: 0 },
@@ -63,17 +63,17 @@ describe('when assignWithDepth: should merge objects within objects', function (
});
});
it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 2)', function () {
const config_0 = {
let config_0 = {
foo: 'bar',
bar: { foo: 'bar', bar: { foo: { message: 'this', willbe: 'clobbered' } } },
boofar: 1,
};
const config_1 = {
let config_1 = {
foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } },
foobar: 'foobar',
};
const result = assignWithDepth(config_0, config_1);
let result = assignWithDepth(config_0, config_1);
expect(result).toEqual({
foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } },
@@ -82,7 +82,7 @@ describe('when assignWithDepth: should merge objects within objects', function (
});
});
it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 1)', function () {
const config_0 = {
let config_0 = {
foo: 'bar',
bar: {
foo: 'bar',
@@ -90,12 +90,12 @@ describe('when assignWithDepth: should merge objects within objects', function (
},
boofar: 1,
};
const config_1 = {
let config_1 = {
foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'this' } } },
foobar: 'foobar',
};
const result = assignWithDepth(config_0, config_1, { depth: 1 });
let result = assignWithDepth(config_0, config_1, { depth: 1 });
expect(result).toEqual({
foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'this' } } },
@@ -104,17 +104,17 @@ describe('when assignWithDepth: should merge objects within objects', function (
});
});
it('should handle depth:3 types (merge with no clobber because assignWithDepth::depth == 3)', function () {
const config_0 = {
let config_0 = {
foo: 'bar',
bar: { foo: 'bar', bar: { foo: { message: '', willbe: 'present' } } },
boofar: 1,
};
const config_1 = {
let config_1 = {
foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'this' } } },
foobar: 'foobar',
};
const result = assignWithDepth(config_0, config_1, { depth: 3 });
let result = assignWithDepth(config_0, config_1, { depth: 3 });
expect(result).toEqual({
foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'this', willbe: 'present' } } },
@@ -125,8 +125,8 @@ describe('when assignWithDepth: should merge objects within objects', function (
});
describe('when memoizing', function () {
it('should return the same value', function () {
const fib: any = memoize(
function (n: number, x: string, canary: { flag: boolean }) {
const fib = memoize(
function (n, x, canary) {
canary.flag = true;
if (n < 2) {
return 1;
@@ -260,7 +260,7 @@ describe('when formatting urls', function () {
it('should handle links', function () {
const url = 'https://mermaid-js.github.io/mermaid/#/';
const config = { securityLevel: 'loose' };
let config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config);
expect(result).toEqual(url);
@@ -271,7 +271,7 @@ describe('when formatting urls', function () {
it('should handle anchors', function () {
const url = '#interaction';
const config = { securityLevel: 'loose' };
let config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config);
expect(result).toEqual(url);
@@ -282,7 +282,7 @@ describe('when formatting urls', function () {
it('should handle mailto', function () {
const url = 'mailto:user@user.user';
const config = { securityLevel: 'loose' };
let config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config);
expect(result).toEqual(url);
@@ -293,7 +293,7 @@ describe('when formatting urls', function () {
it('should handle other protocols', function () {
const url = 'notes://do-your-thing/id';
const config = { securityLevel: 'loose' };
let config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config);
expect(result).toEqual(url);
@@ -304,7 +304,7 @@ describe('when formatting urls', function () {
it('should handle scripts', function () {
const url = 'javascript:alert("test")';
const config = { securityLevel: 'loose' };
let config = { securityLevel: 'loose' };
let result = utils.formatUrl(url, config);
expect(result).toEqual(url);
@@ -425,42 +425,6 @@ describe('when parsing font sizes', function () {
});
it('handles unparseable input', function () {
// @ts-expect-error Explicitly testing unparsable input
expect(utils.parseFontSize({ fontSize: 14 })).toEqual([undefined, undefined]);
});
});
describe('cleanAndMerge', () => {
test('should merge objects', () => {
expect(cleanAndMerge({ a: 1, b: 2 }, { b: 3 })).toEqual({ a: 1, b: 3 });
expect(cleanAndMerge({ a: 1 }, { a: 2 })).toEqual({ a: 2 });
});
test('should remove undefined values', () => {
expect(cleanAndMerge({ a: 1, b: 2 }, { b: undefined })).toEqual({ a: 1, b: 2 });
expect(cleanAndMerge({ a: 1, b: 2 }, { a: 2, b: undefined })).toEqual({ a: 2, b: 2 });
expect(cleanAndMerge({ a: 1, b: { c: 2 } }, { a: 2, b: undefined })).toEqual({
a: 2,
b: { c: 2 },
});
// @ts-expect-error Explicitly testing different type
expect(cleanAndMerge({ a: 1, b: { c: 2 } }, { a: 2, b: { c: undefined } })).toEqual({
a: 2,
b: { c: 2 },
});
});
test('should create deep copies of object', () => {
const input: { a: number; b?: number } = { a: 1 };
const output = cleanAndMerge(input, { b: 2 });
expect(output).toEqual({ a: 1, b: 2 });
output.b = 3;
expect(input).toEqual({ a: 1 });
const inputDeep = { a: { b: 1 } };
const outputDeep = cleanAndMerge(inputDeep, { a: { b: 2 } });
expect(outputDeep).toEqual({ a: { b: 2 } });
outputDeep.a.b = 3;
expect(inputDeep).toEqual({ a: { b: 1 } });
});
});

View File

@@ -31,8 +31,6 @@ import { detectType } from './diagram-api/detectType.js';
import assignWithDepth from './assignWithDepth.js';
import { MermaidConfig } from './config.type.js';
import memoize from 'lodash-es/memoize.js';
import merge from 'lodash-es/merge.js';
import { directiveRegex } from './diagram-api/regexes.js';
export const ZERO_WIDTH_SPACE = '\u200b';
@@ -59,7 +57,7 @@ const d3CurveTypes = {
curveStepAfter: curveStepAfter,
curveStepBefore: curveStepBefore,
};
const directive = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
const directiveWithoutOpen =
/\s*(?:(\w+)(?=:):|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
@@ -97,36 +95,32 @@ const directiveWithoutOpen =
* @param config - Optional mermaid configuration object.
* @returns The json object representing the init passed to mermaid.initialize()
*/
export const detectInit = function (
text: string,
config?: MermaidConfig
): MermaidConfig | undefined {
export const detectInit = function (text: string, config?: MermaidConfig): MermaidConfig {
const inits = detectDirective(text, /(?:init\b)|(?:initialize\b)/);
let results = {};
if (Array.isArray(inits)) {
const args = inits.map((init) => init.args);
sanitizeDirective(args);
directiveSanitizer(args);
results = assignWithDepth(results, [...args]);
} else {
results = inits.args;
}
if (!results) {
return;
if (results) {
let type = detectType(text, config);
['config'].forEach((prop) => {
if (results[prop] !== undefined) {
if (type === 'flowchart-v2') {
type = 'flowchart';
}
results[type] = results[prop];
delete results[prop];
}
});
}
let type = detectType(text, config);
['config'].forEach((prop) => {
if (results[prop] !== undefined) {
if (type === 'flowchart-v2') {
type = 'flowchart';
}
results[type] = results[prop];
delete results[prop];
}
});
// Todo: refactor this, these results are never used
return results;
};
@@ -168,10 +162,10 @@ export const detectDirective = function (
);
let match;
const result = [];
while ((match = directiveRegex.exec(text)) !== null) {
while ((match = directive.exec(text)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (match.index === directiveRegex.lastIndex) {
directiveRegex.lastIndex++;
if (match.index === directive.lastIndex) {
directive.lastIndex++;
}
if (
(match && !type) ||
@@ -808,7 +802,7 @@ export const calculateTextDimensions: (
);
export const initIdGenerator = class iterator {
constructor(deterministic, seed?: any) {
constructor(deterministic, seed) {
this.deterministic = deterministic;
// TODO: Seed is only used for length?
this.seed = seed;
@@ -847,63 +841,67 @@ export const entityDecode = function (html: string): string {
*
* @param args - Directive's JSON
*/
export const sanitizeDirective = (args: unknown): void => {
log.debug('sanitizeDirective called with', args);
export const directiveSanitizer = (args: any) => {
log.debug('directiveSanitizer called with', args);
if (typeof args === 'object') {
// check for array
if (args.length) {
args.forEach((arg) => directiveSanitizer(arg));
} else {
// This is an object
Object.keys(args).forEach((key) => {
log.debug('Checking key', key);
if (key.startsWith('__')) {
log.debug('sanitize deleting __ option', key);
delete args[key];
}
// Return if not an object
if (typeof args !== 'object' || args == null) {
return;
}
if (key.includes('proto')) {
log.debug('sanitize deleting proto option', key);
delete args[key];
}
// Sanitize each element if an array
if (Array.isArray(args)) {
args.forEach((arg) => sanitizeDirective(arg));
return;
}
if (key.includes('constr')) {
log.debug('sanitize deleting constr option', key);
delete args[key];
}
// Sanitize each key if an object
for (const key of Object.keys(args)) {
log.debug('Checking key', key);
if (
key.startsWith('__') ||
key.includes('proto') ||
key.includes('constr') ||
!configKeys.has(key) ||
args[key] == null
) {
log.debug('sanitize deleting key: ', key);
delete args[key];
continue;
}
// Recurse if an object
if (typeof args[key] === 'object') {
log.debug('sanitizing object', key);
sanitizeDirective(args[key]);
continue;
}
const cssMatchers = ['themeCSS', 'fontFamily', 'altFontFamily'];
for (const cssKey of cssMatchers) {
if (key.includes(cssKey)) {
log.debug('sanitizing css option', key);
args[key] = sanitizeCss(args[key]);
}
if (key.includes('themeCSS')) {
log.debug('sanitizing themeCss option');
args[key] = sanitizeCss(args[key]);
}
if (key.includes('fontFamily')) {
log.debug('sanitizing fontFamily option');
args[key] = sanitizeCss(args[key]);
}
if (key.includes('altFontFamily')) {
log.debug('sanitizing altFontFamily option');
args[key] = sanitizeCss(args[key]);
}
if (!configKeys.includes(key)) {
log.debug('sanitize deleting option', key);
delete args[key];
} else {
if (typeof args[key] === 'object') {
log.debug('sanitize deleting object', key);
directiveSanitizer(args[key]);
}
}
});
}
}
if (args.themeVariables) {
for (const k of Object.keys(args.themeVariables)) {
const kArr = Object.keys(args.themeVariables);
for (const k of kArr) {
const val = args.themeVariables[k];
if (val?.match && !val.match(/^[\d "#%(),.;A-Za-z]+$/)) {
if (val && val.match && !val.match(/^[\d "#%(),.;A-Za-z]+$/)) {
args.themeVariables[k] = '';
}
}
}
log.debug('After sanitization', args);
};
export const sanitizeCss = (str: string): string => {
export const sanitizeCss = (str) => {
let startCnt = 0;
let endCnt = 0;
@@ -996,17 +994,12 @@ export const parseFontSize = (fontSize: string | number | undefined): [number?,
}
};
export function cleanAndMerge<T>(defaultData: T, data?: Partial<T>): T {
return merge({}, defaultData, data);
}
export default {
assignWithDepth,
wrapLabel,
calculateTextHeight,
calculateTextWidth,
calculateTextDimensions,
cleanAndMerge,
detectInit,
detectDirective,
isSubstringInArray,
@@ -1020,8 +1013,8 @@ export default {
random,
runFunc,
entityDecode,
initIdGenerator,
sanitizeDirective,
initIdGenerator: initIdGenerator,
directiveSanitizer,
sanitizeCss,
insertTitle,
parseFontSize,

2796
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,24 +5,20 @@
* (i.e. the root of the Mermaid project).
*/
import { readFile, writeFile } from 'node:fs/promises';
import { readFileSync, writeFileSync } from 'node:fs';
import prettier from 'prettier';
const main = async () => {
const filepath = './cSpell.json';
const cSpell: { words: string[] } = JSON.parse(await readFile(filepath, 'utf8'));
const filepath = './cSpell.json';
const cSpell: { words: string[] } = JSON.parse(readFileSync(filepath, 'utf8'));
cSpell.words = [...new Set(cSpell.words.map((word) => word.toLowerCase()))];
cSpell.words.sort((a, b) => a.localeCompare(b));
cSpell.words = [...new Set(cSpell.words.map((word) => word.toLowerCase()))];
cSpell.words.sort((a, b) => a.localeCompare(b));
const prettierConfig = (await prettier.resolveConfig(filepath)) ?? {};
await writeFile(
const prettierConfig = prettier.resolveConfig.sync(filepath) ?? {};
writeFileSync(
filepath,
prettier.format(JSON.stringify(cSpell), {
...prettierConfig,
filepath,
await prettier.format(JSON.stringify(cSpell), {
...prettierConfig,
filepath,
}),
);
};
void main();
})
);

View File

@@ -12,7 +12,7 @@
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.88.2",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
},

View File

@@ -5,14 +5,5 @@
// ensure that nobody can accidentally use this config for a build
"noEmit": true
},
"include": [
"packages",
"tests",
"scripts",
"cypress",
"__mocks__",
"./.eslintrc.cjs",
"./*",
"demos/dev"
]
"include": ["packages", "tests", "scripts", "cypress", "__mocks__", "./.eslintrc.cjs", "./*"]
}