mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-27 03:09:43 +02:00
Merge branch 'develop' into pr/weedySeaDragon/3814
* develop: (815 commits) Move filetype Recommendations to the top Update docs Update integrations.md per review Disable coveralls Update coveralls Ignore bundlephobia Update docs strawman extension and mime type docs Update docs Rename info to note Rename "info" to "note" Update all patch dependencies Fix Directives Documentation Run docs:build Update tutorial link Run build Fix link to Tutorials from n00b-overview page Correct timeline spelling UPdated version to 10.2.3 Remove old changelog ...
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
### Do not refer this package. It is not ready.
|
||||
|
||||
### Refer mermaid-mindmap instead.
|
@@ -1,36 +1,25 @@
|
||||
{
|
||||
"name": "@mermaid-js/mermaid-example-diagram",
|
||||
"version": "9.2.0-rc2",
|
||||
"description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
|
||||
"main": "dist/mermaid-mindmap.core.mjs",
|
||||
"module": "dist/mermaid-mindmap.core.mjs",
|
||||
"version": "9.3.0",
|
||||
"description": "Example of external diagram module for MermaidJS.",
|
||||
"module": "dist/mermaid-example-diagram.core.mjs",
|
||||
"types": "dist/detector.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/mermaid-example-diagram.min.js",
|
||||
"import": "./dist/mermaid-example-diagram.core.mjs"
|
||||
"import": "./dist/mermaid-example-diagram.core.mjs",
|
||||
"types": "./dist/detector.d.ts"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"keywords": [
|
||||
"diagram",
|
||||
"markdown",
|
||||
"mindmap",
|
||||
"example",
|
||||
"mermaid"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build:types": "tsc -p ./tsconfig.json --emitDeclarationOnly",
|
||||
"build:watch": "yarn build:code --watch",
|
||||
"build:esbuild": "concurrently \"yarn build:code\" \"yarn build:types\"",
|
||||
"build": "yarn clean; yarn build:esbuild",
|
||||
"dev": "node .esbuild/serve.cjs",
|
||||
"release": "yarn build",
|
||||
"lint": "eslint --cache --ignore-path .gitignore . && yarn lint:jison && prettier --check .",
|
||||
"lint:fix": "eslint --fix --ignore-path .gitignore . && prettier --write .",
|
||||
"lint:jison": "ts-node-esm src/jison/lint.mts",
|
||||
"todo-prepare": "concurrently \"husky install ../../.husky\" \"yarn build\"",
|
||||
"todo-pre-commit": "lint-staged"
|
||||
"prepublishOnly": "pnpm -w run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -48,10 +37,20 @@
|
||||
"page"
|
||||
]
|
||||
},
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^6.0.0",
|
||||
"cytoscape": "^3.23.0",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
"cytoscape-fcose": "^2.1.0",
|
||||
"d3": "^7.0.0",
|
||||
"khroma": "^2.0.0",
|
||||
"non-layered-tidy-tree-layout": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^7.5.0",
|
||||
"rimraf": "^3.0.2"
|
||||
"@types/cytoscape": "^3.19.9",
|
||||
"concurrently": "^8.0.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"mermaid": "workspace:*"
|
||||
},
|
||||
"resolutions": {
|
||||
"d3": "^7.0.0"
|
||||
|
@@ -1,18 +1,20 @@
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
export const id = 'example-diagram';
|
||||
import type { ExternalDiagramDefinition } from 'mermaid';
|
||||
|
||||
/**
|
||||
* Detector function that will be called by mermaid to determine if the diagram is this type of diagram.
|
||||
*
|
||||
* @param txt - The diagram text will be passed to the detector
|
||||
* @returns True if the diagram text matches a diagram of this type
|
||||
*/
|
||||
const id = 'example-diagram';
|
||||
|
||||
export const detector = (txt: string) => {
|
||||
const detector = (txt: string) => {
|
||||
return txt.match(/^\s*example-diagram/) !== null;
|
||||
};
|
||||
|
||||
export const loadDiagram = async () => {
|
||||
const { diagram } = await import('./diagram-definition');
|
||||
const loader = async () => {
|
||||
const { diagram } = await import('./diagram-definition.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
|
@@ -1,9 +1,9 @@
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import parser from './parser/exampleDiagram';
|
||||
import * as db from './exampleDiagramDb';
|
||||
import renderer from './exampleDiagramRenderer';
|
||||
import styles from './styles';
|
||||
import { injectUtils } from './mermaidUtils';
|
||||
import parser from './parser/exampleDiagram.jison';
|
||||
import * as db from './exampleDiagramDb.js';
|
||||
import renderer from './exampleDiagramRenderer.js';
|
||||
import styles from './styles.js';
|
||||
import { injectUtils } from './mermaidUtils.js';
|
||||
|
||||
export const diagram = {
|
||||
db,
|
||||
@@ -12,5 +12,3 @@ export const diagram = {
|
||||
styles,
|
||||
injectUtils,
|
||||
};
|
||||
|
||||
export { detector, id } from './detector';
|
||||
|
@@ -1,5 +1,17 @@
|
||||
import { parser } from './parser/exampleDiagram';
|
||||
import db from './exampleDiagramDb';
|
||||
import { parser } from './parser/exampleDiagram.jison';
|
||||
import * as db from './exampleDiagramDb.js';
|
||||
import { injectUtils } from './mermaidUtils.js';
|
||||
// Todo fix utils functions for tests
|
||||
import {
|
||||
log,
|
||||
setLogLevel,
|
||||
getConfig,
|
||||
sanitizeText,
|
||||
setupGraphViewBox,
|
||||
} from '../../mermaid/src/diagram-api/diagramAPI.js';
|
||||
|
||||
injectUtils(log, setLogLevel, getConfig, sanitizeText, setupGraphViewBox);
|
||||
|
||||
describe('when parsing an info graph it', function () {
|
||||
let ex;
|
||||
beforeEach(function () {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/** Created by knut on 15-01-14. */
|
||||
import { log } from './mermaidUtils';
|
||||
import { log } from './mermaidUtils.js';
|
||||
|
||||
var message = '';
|
||||
var info = false;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/** Created by knut on 14-12-11. */
|
||||
import { select } from 'd3';
|
||||
import { log, getConfig, setupGraphViewbox } from './mermaidUtils';
|
||||
import { log, getConfig, setupGraphViewbox } from './mermaidUtils.js';
|
||||
|
||||
/**
|
||||
* Draws a an info picture in the tag with id: id based on the graph definition in text.
|
||||
|
@@ -1,4 +1,8 @@
|
||||
const warning = () => null;
|
||||
const warning = (s: string) => {
|
||||
// Todo remove debug code
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Log function was called before initialization', s);
|
||||
};
|
||||
|
||||
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
|
||||
@@ -19,12 +23,11 @@ export const log: Record<keyof typeof LEVELS, typeof console.log> = {
|
||||
error: warning,
|
||||
fatal: warning,
|
||||
};
|
||||
|
||||
export let setLogLevel: (level: keyof typeof LEVELS | number | string) => void;
|
||||
export let getConfig: () => object;
|
||||
export let sanitizeText: (str: string) => string;
|
||||
/**
|
||||
* Placeholder for the real function that will be injected by mermaid.
|
||||
*/
|
||||
export let commonDb: () => object;
|
||||
// eslint-disable @typescript-eslint/no-explicit-any
|
||||
export let setupGraphViewbox: (
|
||||
graph: any,
|
||||
@@ -33,23 +36,15 @@ export let setupGraphViewbox: (
|
||||
useMaxWidth: boolean
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Function called by mermaid that injects utility functions that help the diagram to be a good citizen.
|
||||
*
|
||||
* @param _log - log from mermaid/src/diagramAPI.ts
|
||||
* @param _setLogLevel - setLogLevel from mermaid/src/diagramAPI.ts
|
||||
* @param _getConfig - getConfig from mermaid/src/diagramAPI.ts
|
||||
* @param _sanitizeText - sanitizeText from mermaid/src/diagramAPI.ts
|
||||
* @param _setupGraphViewbox - setupGraphViewbox from mermaid/src/diagramAPI.ts
|
||||
*/
|
||||
export const injectUtils = (
|
||||
_log: Record<keyof typeof LEVELS, typeof console.log>,
|
||||
_setLogLevel: typeof setLogLevel,
|
||||
_getConfig: typeof getConfig,
|
||||
_sanitizeText: typeof sanitizeText,
|
||||
_setupGraphViewbox: typeof setupGraphViewbox
|
||||
_setLogLevel: any,
|
||||
_getConfig: any,
|
||||
_sanitizeText: any,
|
||||
_setupGraphViewbox: any,
|
||||
_commonDb: any
|
||||
) => {
|
||||
_log.debug('Mermaid utils injected into example-diagram');
|
||||
_log.info('Mermaid utils injected');
|
||||
log.trace = _log.trace;
|
||||
log.debug = _log.debug;
|
||||
log.info = _log.info;
|
||||
@@ -60,4 +55,5 @@ export const injectUtils = (
|
||||
getConfig = _getConfig;
|
||||
sanitizeText = _sanitizeText;
|
||||
setupGraphViewbox = _setupGraphViewbox;
|
||||
commonDb = _commonDb;
|
||||
};
|
||||
|
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"name": "@mermaid-js/mermaid-mindmap",
|
||||
"version": "9.2.2",
|
||||
"description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
|
||||
"module": "dist/mermaid-mindmap.core.mjs",
|
||||
"types": "dist/detector.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/mermaid-mindmap.core.mjs",
|
||||
"types": "./dist/detector.d.ts"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"keywords": [
|
||||
"diagram",
|
||||
"markdown",
|
||||
"mindmap",
|
||||
"mermaid"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublishOnly": "pnpm -w run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid"
|
||||
},
|
||||
"author": "Knut Sveidqvist",
|
||||
"license": "MIT",
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"**/parser/*.js",
|
||||
"dist/**/*.js",
|
||||
"cypress/**/*.js"
|
||||
],
|
||||
"globals": [
|
||||
"page"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^6.0.0",
|
||||
"cytoscape": "^3.23.0",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
"cytoscape-fcose": "^2.1.0",
|
||||
"d3": "^7.0.0",
|
||||
"khroma": "^2.0.0",
|
||||
"non-layered-tidy-tree-layout": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^7.5.0",
|
||||
"mermaid": "workspace:*",
|
||||
"rimraf": "^3.0.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"d3": "^7.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css",
|
||||
"**/*.scss"
|
||||
]
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import mindmapParser from './parser/mindmap';
|
||||
import * as mindmapDb from './mindmapDb';
|
||||
import mindmapRenderer from './mindmapRenderer';
|
||||
import mindmapStyles from './styles';
|
||||
import { injectUtils } from './mermaidUtils';
|
||||
|
||||
export const diagram = {
|
||||
db: mindmapDb,
|
||||
renderer: mindmapRenderer,
|
||||
parser: mindmapParser,
|
||||
styles: mindmapStyles,
|
||||
injectUtils,
|
||||
};
|
@@ -1,7 +0,0 @@
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
mermaid: any; // 👈️ turn off type checking
|
||||
}
|
||||
}
|
1
packages/mermaid-zenuml/README.md
Symbolic link
1
packages/mermaid-zenuml/README.md
Symbolic link
@@ -0,0 +1 @@
|
||||
../mermaid/src/docs/syntax/zenuml.md
|
47
packages/mermaid-zenuml/package.json
Normal file
47
packages/mermaid-zenuml/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@mermaid-js/mermaid-zenuml",
|
||||
"version": "0.1.0",
|
||||
"description": "MermaidJS plugin for ZenUML integration",
|
||||
"module": "dist/mermaid-zenuml.core.mjs",
|
||||
"types": "dist/detector.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/mermaid-zenuml.core.mjs",
|
||||
"types": "./dist/detector.d.ts"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"keywords": [
|
||||
"diagram",
|
||||
"markdown",
|
||||
"zenuml",
|
||||
"mermaid"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublishOnly": "pnpm -w run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid",
|
||||
"directory": "packages/mermaid-zenuml"
|
||||
},
|
||||
"contributors": [
|
||||
"Peng Xiao (https://github.com/MrCoder)",
|
||||
"Sidharth Vinod (https://sidharth.dev)",
|
||||
"Dong Cai (https://github.com/dontry)"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zenuml/core": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mermaid": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mermaid": "workspace:>=10.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
21
packages/mermaid-zenuml/src/detector.ts
Normal file
21
packages/mermaid-zenuml/src/detector.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { ExternalDiagramDefinition } from 'mermaid';
|
||||
|
||||
const id = 'zenuml';
|
||||
const regexp = /^\s*zenuml/;
|
||||
|
||||
const detector = (txt: string) => {
|
||||
return txt.match(regexp) !== null;
|
||||
};
|
||||
|
||||
const loader = async () => {
|
||||
const { diagram } = await import('./zenuml-definition.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
@@ -1,6 +1,9 @@
|
||||
import type { MermaidConfig } from 'mermaid';
|
||||
|
||||
const warning = (s: string) => {
|
||||
// Todo remove debug code
|
||||
console.error('Log function was called before initialization', s); // eslint-disable-line
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Log function was called before initialization', s);
|
||||
};
|
||||
|
||||
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
@@ -24,7 +27,7 @@ export const log: Record<keyof typeof LEVELS, typeof console.log> = {
|
||||
};
|
||||
|
||||
export let setLogLevel: (level: keyof typeof LEVELS | number | string) => void;
|
||||
export let getConfig: () => object;
|
||||
export let getConfig: () => MermaidConfig;
|
||||
export let sanitizeText: (str: string) => string;
|
||||
// eslint-disable @typescript-eslint/no-explicit-any
|
||||
export let setupGraphViewbox: (
|
12
packages/mermaid-zenuml/src/parser.ts
Normal file
12
packages/mermaid-zenuml/src/parser.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* ZenUML manage parsing internally. It uses Antlr4 to parse the DSL.
|
||||
* The parser is defined in https://github.com/ZenUml/vue-sequence/blob/main/src/parser/index.js
|
||||
*
|
||||
* This is a dummy parser that satisfies the mermaid API logic.
|
||||
*/
|
||||
export default {
|
||||
parser: { yy: {} },
|
||||
parse: () => {
|
||||
// no op
|
||||
},
|
||||
};
|
17
packages/mermaid-zenuml/src/zenuml-definition.ts
Normal file
17
packages/mermaid-zenuml/src/zenuml-definition.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { injectUtils } from './mermaidUtils.js';
|
||||
import parser from './parser.js';
|
||||
import renderer from './zenumlRenderer.js';
|
||||
|
||||
export const diagram = {
|
||||
db: {
|
||||
clear: () => {
|
||||
// no-op
|
||||
},
|
||||
},
|
||||
renderer,
|
||||
parser,
|
||||
styles: () => {
|
||||
// no-op
|
||||
},
|
||||
injectUtils,
|
||||
};
|
68
packages/mermaid-zenuml/src/zenumlRenderer.ts
Normal file
68
packages/mermaid-zenuml/src/zenumlRenderer.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { getConfig, log } from './mermaidUtils.js';
|
||||
import ZenUml from '@zenuml/core';
|
||||
|
||||
const regexp = /^\s*zenuml/;
|
||||
|
||||
// Create a Zen UML container outside the svg first for rendering, otherwise the Zen UML diagram cannot be rendered properly
|
||||
function createTemporaryZenumlContainer(id: string) {
|
||||
const container = document.createElement('div');
|
||||
container.id = `container-${id}`;
|
||||
container.style.display = 'flex';
|
||||
container.innerHTML = `<div id="zenUMLApp-${id}"></div>`;
|
||||
const app = container.querySelector(`#zenUMLApp-${id}`) as HTMLElement;
|
||||
return { container, app };
|
||||
}
|
||||
|
||||
// Create a foreignObject to wrap the Zen UML container in the svg
|
||||
function createForeignObject(id: string) {
|
||||
const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
|
||||
foreignObject.setAttribute('x', '0');
|
||||
foreignObject.setAttribute('y', '0');
|
||||
foreignObject.setAttribute('width', '100%');
|
||||
foreignObject.setAttribute('height', '100%');
|
||||
const { container, app } = createTemporaryZenumlContainer(id);
|
||||
foreignObject.appendChild(container);
|
||||
return { foreignObject, container, app };
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a Zen UML in the tag with id: id based on the graph definition in text.
|
||||
*
|
||||
* @param text - The text of the diagram
|
||||
* @param id - The id of the diagram which will be used as a DOM element id¨
|
||||
*/
|
||||
export const draw = async function (text: string, id: string) {
|
||||
log.info('draw with Zen UML renderer', ZenUml);
|
||||
|
||||
text = text.replace(regexp, '');
|
||||
const { securityLevel } = getConfig();
|
||||
// Handle root and Document for when rendering in sandbox mode
|
||||
let sandboxElement: HTMLIFrameElement | null = null;
|
||||
if (securityLevel === 'sandbox') {
|
||||
sandboxElement = document.getElementById('i' + id) as HTMLIFrameElement;
|
||||
}
|
||||
|
||||
const root = securityLevel === 'sandbox' ? sandboxElement?.contentWindow?.document : document;
|
||||
|
||||
const svgContainer = root?.querySelector(`svg#${id}`);
|
||||
|
||||
if (!root || !svgContainer) {
|
||||
log.error('Cannot find root or svgContainer');
|
||||
return;
|
||||
}
|
||||
|
||||
const { foreignObject, container, app } = createForeignObject(id);
|
||||
svgContainer.appendChild(foreignObject);
|
||||
// @ts-expect-error @zenuml/core@3.0.0 exports the wrong type for ZenUml
|
||||
const zenuml = new ZenUml(app);
|
||||
// default is a theme name. More themes to be added and will be configurable in the future
|
||||
await zenuml.render(text, 'theme-mermaid');
|
||||
|
||||
const { width, height } = window.getComputedStyle(container);
|
||||
log.debug('zenuml diagram size', width, height);
|
||||
svgContainer.setAttribute('style', `width: ${width}; height: ${height};`);
|
||||
};
|
||||
|
||||
export default {
|
||||
draw,
|
||||
};
|
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"module": "esnext",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
6
packages/mermaid/.gitignore
vendored
6
packages/mermaid/.gitignore
vendored
@@ -1,2 +1,6 @@
|
||||
src/vitepress
|
||||
src/docs/config/setup
|
||||
src/docs/config/setup
|
||||
README.*
|
||||
src/docs/public/user-avatars/
|
||||
src/docs/.vitepress/cache
|
||||
src/docs/.vitepress/components.d.ts
|
||||
|
@@ -1,346 +0,0 @@
|
||||
# mermaid
|
||||
|
||||
[](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml) [](https://www.npmjs.com/package/mermaid) [](https://bundlephobia.com/package/mermaid) [](https://coveralls.io/github/mermaid-js/mermaid?branch=master) [](https://www.jsdelivr.com/package/npm/mermaid) [](https://www.npmjs.com/package/mermaid) [](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) [](https://twitter.com/mermaidjs_)
|
||||
|
||||
English | [简体中文](./README.zh-CN.md)
|
||||
|
||||
<img src="./img/header.png" alt="" />
|
||||
|
||||
:trophy: **Mermaid was nominated and won the [JS Open Source Awards (2019)](https://osawards.com/javascript/2019) in the category "The most exciting use of technology"!!!**
|
||||
|
||||
**Thanks to all involved, people committing pull requests, people answering questions! 🙏**
|
||||
|
||||
<a href="https://mermaid-js.github.io/mermaid/landing/"><img src="https://github.com/mermaid-js/mermaid/blob/master/docs/img/book-banner-post-release.jpg" alt="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!"></a>
|
||||
|
||||
## About
|
||||
|
||||
<!-- <Main description> -->
|
||||
|
||||
Mermaid is a JavaScript-based diagramming and charting tool that uses Markdown-inspired text definitions and a renderer to create and modify complex diagrams. The main purpose of Mermaid is to help documentation catch up with development.
|
||||
|
||||
> Doc-Rot is a Catch-22 that Mermaid helps to solve.
|
||||
|
||||
Diagramming and documentation costs precious developer time and gets outdated quickly.
|
||||
But not having diagrams or docs ruins productivity and hurts organizational learning.<br/>
|
||||
Mermaid addresses this problem by enabling users to create easily modifiable diagrams. It can also be made part of production scripts (and other pieces of code).<br/>
|
||||
<br/>
|
||||
|
||||
Mermaid allows even non-programmers to easily create detailed diagrams through the [Mermaid Live Editor](https://mermaid.live/).<br/>
|
||||
[Tutorials](./docs/Tutorials.md) has video tutorials.
|
||||
Use Mermaid with your favorite applications, check out the list of [Integrations and Usages of Mermaid](./docs/integrations.md).
|
||||
|
||||
You can also use Mermaid within [GitHub](https://github.blog/2022-02-14-include-diagrams-markdown-files-mermaid/) as well many of your other favorite applications—check out the list of [Integrations and Usages of Mermaid](./docs/integrations.md).
|
||||
|
||||
For a more detailed introduction to Mermaid and some of its more basic uses, look to the [Beginner's Guide](./docs/n00b-overview.md), [Usage](./docs/usage.md) and [Tutorials](./docs/Tutorials.md).
|
||||
|
||||
🌐 [CDN](https://unpkg.com/mermaid/) | 📖 [Documentation](https://mermaidjs.github.io) | 🙌 [Contribution](https://github.com/mermaid-js/mermaid/blob/develop/CONTRIBUTING.md) | 📜 [Changelog](./docs/CHANGELOG.md)
|
||||
|
||||
In our release process we rely heavily on visual regression tests using [applitools](https://applitools.com/). Applitools is a great service which has been easy to use and integrate with our tests.
|
||||
|
||||
<a href="https://applitools.com/">
|
||||
<svg width="170" height="32" viewBox="0 0 170 32" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="a" maskUnits="userSpaceOnUse" x="27" y="0" width="143" height="32"><path fill-rule="evenodd" clip-rule="evenodd" d="M27.732.227h141.391v31.19H27.733V.227z" fill="#fff"></path></mask><g mask="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M153.851 22.562l1.971-3.298c1.291 1.219 3.837 2.402 5.988 2.402 1.971 0 2.903-.753 2.903-1.829 0-2.832-10.253-.502-10.253-7.313 0-2.904 2.51-5.45 7.099-5.45 2.904 0 5.234 1.004 6.955 2.367l-1.829 3.226c-1.039-1.075-3.011-2.008-5.126-2.008-1.65 0-2.725.717-2.725 1.685 0 2.546 10.289.395 10.289 7.386 0 3.19-2.724 5.52-7.528 5.52-3.012 0-5.916-1.003-7.744-2.688zm-5.7 2.259h4.553V.908h-4.553v23.913zm-6.273-8.676c0-2.689-1.578-5.02-4.446-5.02-2.832 0-4.409 2.331-4.409 5.02 0 2.724 1.577 5.055 4.409 5.055 2.868 0 4.446-2.33 4.446-5.055zm-13.588 0c0-4.912 3.442-9.07 9.142-9.07 5.736 0 9.178 4.158 9.178 9.07 0 4.911-3.442 9.106-9.178 9.106-5.7 0-9.142-4.195-9.142-9.106zm-5.628 0c0-2.689-1.577-5.02-4.445-5.02-2.832 0-4.41 2.331-4.41 5.02 0 2.724 1.578 5.055 4.41 5.055 2.868 0 4.445-2.33 4.445-5.055zm-13.587 0c0-4.912 3.441-9.07 9.142-9.07 5.736 0 9.178 4.158 9.178 9.07 0 4.911-3.442 9.106-9.178 9.106-5.701 0-9.142-4.195-9.142-9.106zm-8.425 4.338v-8.999h-2.868v-3.98h2.868V2.773h4.553v4.733h3.514v3.979h-3.514v7.78c0 1.111.574 1.936 1.578 1.936.681 0 1.326-.251 1.577-.538l.968 3.478c-.681.609-1.9 1.11-3.8 1.11-3.191 0-4.876-1.648-4.876-4.767zm-8.962 4.338h4.553V7.505h-4.553V24.82zm-.43-21.905a2.685 2.685 0 012.688-2.69c1.506 0 2.725 1.184 2.725 2.69a2.724 2.724 0 01-2.725 2.724c-1.47 0-2.688-1.219-2.688-2.724zM84.482 24.82h4.553V.908h-4.553v23.913zm-6.165-8.676c0-2.976-1.793-5.02-4.41-5.02-1.47 0-3.119.825-3.908 1.973v6.094c.753 1.111 2.438 2.008 3.908 2.008 2.617 0 4.41-2.044 4.41-5.055zm-8.318 6.453v8.82h-4.553V7.504H70v2.187c1.327-1.685 3.227-2.618 5.342-2.618 4.446 0 7.672 3.299 7.672 9.07 0 5.773-3.226 9.107-7.672 9.107-2.043 0-3.907-.86-5.342-2.653zm-10.718-6.453c0-2.976-1.793-5.02-4.41-5.02-1.47 0-3.119.825-3.908 1.973v6.094c.753 1.111 2.438 2.008 3.908 2.008 2.617 0 4.41-2.044 4.41-5.055zm-8.318 6.453v8.82H46.41V7.504h4.553v2.187c1.327-1.685 3.227-2.618 5.342-2.618 4.446 0 7.672 3.299 7.672 9.07 0 5.773-3.226 9.107-7.672 9.107-2.043 0-3.908-.86-5.342-2.653zm-11.758-1.936V18.51c-.753-1.004-2.187-1.542-3.657-1.542-1.793 0-3.263.968-3.263 2.617 0 1.65 1.47 2.582 3.263 2.582 1.47 0 2.904-.502 3.657-1.506zm0 4.159v-1.829c-1.183 1.434-3.227 2.259-5.485 2.259-2.761 0-5.988-1.864-5.988-5.736 0-4.087 3.227-5.593 5.988-5.593 2.33 0 4.337.753 5.485 2.115V13.85c0-1.756-1.506-2.904-3.8-2.904-1.829 0-3.55.717-4.984 2.044L28.63 9.8c2.115-1.901 4.84-2.726 7.564-2.726 3.98 0 7.6 1.578 7.6 6.561v11.186h-4.588z" fill="#00A298"></path></g><path fill-rule="evenodd" clip-rule="evenodd" d="M14.934 16.177c0 1.287-.136 2.541-.391 3.752-1.666-1.039-3.87-2.288-6.777-3.752 2.907-1.465 5.11-2.714 6.777-3.753.255 1.211.39 2.466.39 3.753m4.6-7.666V4.486a78.064 78.064 0 01-4.336 3.567c-1.551-2.367-3.533-4.038-6.14-5.207C11.1 4.658 12.504 6.7 13.564 9.262 5.35 15.155 0 16.177 0 16.177s5.35 1.021 13.564 6.915c-1.06 2.563-2.463 4.603-4.507 6.415 2.607-1.169 4.589-2.84 6.14-5.207a77.978 77.978 0 014.336 3.568v-4.025s-.492-.82-2.846-2.492c.6-1.611.93-3.354.93-5.174a14.8 14.8 0 00-.93-5.174c2.354-1.673 2.846-2.492 2.846-2.492" fill="#00A298"></path></svg>
|
||||
</a>
|
||||
|
||||
<!-- </Main description> -->
|
||||
|
||||
## Examples
|
||||
|
||||
**The following are some examples of the diagrams, charts and graphs that can be made using Mermaid. Click here to jump into the [text syntax](https://mermaid-js.github.io/mermaid/#/n00b-syntaxReference).**
|
||||
|
||||
<!-- <Flowchart> -->
|
||||
|
||||
### Flowchart [<a href="https://mermaid-js.github.io/mermaid/#/flowchart">docs</a> - <a href="https://mermaid.live/edit#pako:eNpNkMtqwzAQRX9FzKqFJK7t1km8KDQP6KJQSLOLvZhIY1tgS0GWmgbb_165IaFaiXvOFTPqgGtBkEJR6zOv0Fj2scsU8-ft8I5G5Gw6fe339GN7tnrYaafE45WvRsLW3Ya4bKVWwzVe_xU-FfVsc9hR62rLwvw_2591z7Y3FuUwgYZMg1L4ObrRzMBW1FAGqb8KKtCLGWRq8Ko7CbS0FdJqA2mBdUsTQGf110VxSK1xdJM2EkuDzd2qNQrypQ7s5TQuXcrW-ie5VoUsx9yZ2seVtac2DYIRz0ppK3eccd0ErRTjD1XfyyRIomSBUUzJPMaXOBb8GC4XRfQcFmL-FEYIwzD8AggvcHE">live editor</a>]
|
||||
|
||||
```
|
||||
flowchart LR
|
||||
|
||||
A[Hard] -->|Text| B(Round)
|
||||
B --> C{Decision}
|
||||
C -->|One| D[Result 1]
|
||||
C -->|Two| E[Result 2]
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
A[Hard] -->|Text| B(Round)
|
||||
B --> C{Decision}
|
||||
C -->|One| D[Result 1]
|
||||
C -->|Two| E[Result 2]
|
||||
```
|
||||
|
||||
### Sequence diagram [<a href="https://mermaid-js.github.io/mermaid/#/sequenceDiagram">docs</a> - <a href="https://mermaid.live/edit#pako:eNo9kMluwjAQhl_F-AykQMuSA1WrbuLQQ3v1ZbAnsVXHkzrjVhHi3etQwKfRv4w-z0FqMihL2eF3wqDxyUEdoVHhwTuNk-12RzaU4g29JzHMY2HpV0BE0VO6V8ETtdkGz1Zb1F8qiPyG5LX84mrLAmpwoWNh-5a0pWCiAxUwGBXeiVHEU4oq8V_6AHYUwAu2lLLTjVQ4bc1rT2yleI0IfJG320faZ9ABbk-Jz3hZnFxBduR9L2oiM5Jj2WBswJn8-cMArSRbbFDJMo8GK0ielVThmKOpNcD4bBxTlGUFvsOxhMT02QctS44JL6HzAS-iJzCYOwfJfTscunYd542aQuXqQU_RZ9kyt11ZFIM9rR3btJ9qaorOGQuR7c9mWSznyzXMF7hcLeBusTB6P9usq_ntrDKrm9kc5PF4_AMJE56Z">live editor</a>]
|
||||
|
||||
```
|
||||
sequenceDiagram
|
||||
Alice->>John: Hello John, how are you?
|
||||
loop Healthcheck
|
||||
John->>John: Fight against hypochondria
|
||||
end
|
||||
Note right of John: Rational thoughts!
|
||||
John-->>Alice: Great!
|
||||
John->>Bob: How about you?
|
||||
Bob-->>John: Jolly good!
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
Alice->>John: Hello John, how are you?
|
||||
loop Healthcheck
|
||||
John->>John: Fight against hypochondria
|
||||
end
|
||||
Note right of John: Rational thoughts!
|
||||
John-->>Alice: Great!
|
||||
John->>Bob: How about you?
|
||||
Bob-->>John: Jolly good!
|
||||
```
|
||||
|
||||
### Gantt chart [<a href="https://mermaid-js.github.io/mermaid/#/gantt">docs</a> - <a href="https://mermaid.live/edit#pako:eNp90cGOgyAQBuBXIZxtFbG29bbZ3fsmvXKZylhJEAyOTZrGd1_sto3xsHMBhu-HBO689hp5xS_giJQbsCbjHTv9jcp9-q63SKhZpb3DhMXSOIiE5ZkoNpnYZGXynh6U-4jBK7JnVfBYJo9QvgjtEya1cj8QwFq0TMz4lZqxTBg0hOF5m1jifI2Lf7Bc490CyxUu1rhc4GLGPOEdhg6Mjq92V44xxanFDhWv4lRjA6MlxZWbIh17DYTf2pAPvGrADphwGMmfbq7mFYURX-jLwCVA91bWg8YYunO69Y8vMgPFI2vvGnOZ-2Owsd0S9UOVpvP29mKoHc_b2nfpYHQLgdrrsUzLvDxALrHcS9hJqeuzOB6avBCN3mciBz5N0y_wxZ0J">live editor</a>]
|
||||
|
||||
```
|
||||
gantt
|
||||
section Section
|
||||
Completed :done, des1, 2014-01-06,2014-01-08
|
||||
Active :active, des2, 2014-01-07, 3d
|
||||
Parallel 1 : des3, after des1, 1d
|
||||
Parallel 2 : des4, after des1, 1d
|
||||
Parallel 3 : des5, after des3, 1d
|
||||
Parallel 4 : des6, after des4, 1d
|
||||
```
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
section Section
|
||||
Completed :done, des1, 2014-01-06,2014-01-08
|
||||
Active :active, des2, 2014-01-07, 3d
|
||||
Parallel 1 : des3, after des1, 1d
|
||||
Parallel 2 : des4, after des1, 1d
|
||||
Parallel 3 : des5, after des3, 1d
|
||||
Parallel 4 : des6, after des4, 1d
|
||||
```
|
||||
|
||||
### Class diagram [<a href="https://mermaid-js.github.io/mermaid/#/classDiagram">docs</a> - <a href="https://mermaid.live/edit#pako:eNpdkTFPwzAQhf-K5QlQ2zQJJG1UBaGWDYmBgYEwXO1LYuTEwXYqlZL_jt02asXm--690zvfgTLFkWaUSTBmI6DS0BTt2lfzkKx-p1PytEO9f1FtdaQkI2ulZNGuVqK1qEtgmOfk7BitSzKdOhg59XuNGgk0RDxed-_IOr6uf8cZ6UhTZ8bvHqS5ub1mr9svZPbjk6DEBlu7AQuXyBkx4gcvDk9cUMJq0XT_YaW0kNK5j-ufAoRzcihaQvLcoN4Jv50vvVxw_xrnD3RCG9QNCO4-8OgpqK1dpoJm7smxhF7agp6kfcfB4jMXVmmalW4tnFDorXrbt4xmVvc4is53GKFUwNF5DtTuO3-sShjrJjLVlqLyvNfS4drazmRB4NuzSti6386YagIjeA3a1rtlEiRRsoAoxiSN4SGOOduGy0UZ3YclT-dhBHQYhj8dc6_I">live editor</a>]
|
||||
|
||||
```
|
||||
classDiagram
|
||||
Class01 <|-- AveryLongClass : Cool
|
||||
<<Interface>> Class01
|
||||
Class09 --> C2 : Where am I?
|
||||
Class09 --* C3
|
||||
Class09 --|> Class07
|
||||
Class07 : equals()
|
||||
Class07 : Object[] elementData
|
||||
Class01 : size()
|
||||
Class01 : int chimp
|
||||
Class01 : int gorilla
|
||||
class Class10 {
|
||||
<<service>>
|
||||
int id
|
||||
size()
|
||||
}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
Class01 <|-- AveryLongClass : Cool
|
||||
<<Interface>> Class01
|
||||
Class09 --> C2 : Where am I?
|
||||
Class09 --* C3
|
||||
Class09 --|> Class07
|
||||
Class07 : equals()
|
||||
Class07 : Object[] elementData
|
||||
Class01 : size()
|
||||
Class01 : int chimp
|
||||
Class01 : int gorilla
|
||||
class Class10 {
|
||||
<<service>>
|
||||
int id
|
||||
size()
|
||||
}
|
||||
```
|
||||
|
||||
### State diagram [<a href="https://mermaid-js.github.io/mermaid/#/stateDiagram">docs</a> - <a href="https://mermaid.live/edit#pako:eNpdkEFvgzAMhf8K8nEqpYSNthx22Xbcqcexg0sCiZQQlDhIFeK_L8A6TfXp6fOz9ewJGssFVOAJSbwr7ByadGR1n8T6evpO0vQ1uZDSekOrXGFsPqJPO6q-2-imH8f_0TeHXm50lfelsAMjnEHFY6xpMdRAUhhRQxUlFy0GTTXU_RytYeAx-AdXZB1ULWovdoCB7OXWN1CRC-Ju-r3uz6UtchGHJqDbsPygU57iysb2reoWHpyOWBINvsqypb3vFMlw3TfWZF5xiY7keC6zkpUnZIUojwW-FAVvrvn51LLnvOXHQ84Q5nn-AVtLcwk">live editor</a>]
|
||||
|
||||
```
|
||||
stateDiagram-v2
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]
|
||||
```
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]
|
||||
```
|
||||
|
||||
### Pie chart [<a href="https://mermaid-js.github.io/mermaid/#/pie">docs</a> - <a href="https://mermaid.live/edit#pako:eNo9jsFugzAMhl8F-VzBgEEh13Uv0F1zcYkTIpEEBadShXj3BU3dzf_n77e8wxQUgYDVkvQSbsFsEgpRtEN_5i_kvzx05XiC-xvUHVzAUXRoVe7v0heFBJ7JkQSRR0Ua08ISpD-ymlaFTN_KcoggNC4bXQATh5-Xn0BwTPSWbhZNRPdvLQEV5dIO_FrPZ43dOJ-cgtfWnDzFJeOZed1EVZ3r0lie06Ocgqs2q2aMPD_HvuqbfsCmpf7aYte2anrU46Cbz1qr60fdIBzH8QvW9lkl">live editor</a>]
|
||||
|
||||
```
|
||||
pie
|
||||
"Dogs" : 386
|
||||
"Cats" : 85.9
|
||||
"Rats" : 15
|
||||
```
|
||||
|
||||
```mermaid
|
||||
pie
|
||||
"Dogs" : 386
|
||||
"Cats" : 85.9
|
||||
"Rats" : 15
|
||||
```
|
||||
|
||||
### Git graph [experimental - <a href="https://mermaid.live/edit#pako:eNqNkMFugzAMhl8F-VyVAR1tOW_aA-zKxSSGRCMJCk6lCvHuNZPKZdM0n-zf3_8r8QIqaIIGMqnB8kfEybQ--y4VnLP8-9RF9Mpkmm40hmlnDKmvkPiH_kfS7nFo_VN0FAf6XwocQGgxa_nGsm1bYEOOWmik1dRjGrmF1q-Cpkkj07u2HCI0PY4zHQATh8-7V9BwTPSE3iwOEd1OjQE1iWkBvk_bzQY7s0Sq4Hs7bHqKo8iGeZqbPN_WR7mpSd1RHpvPVhuMbG7XOq_L-oJlRfW5wteq0qorrpe-PBW9Pr8UJcK6rg-BLYPQ">live editor</a>]
|
||||
|
||||
### User Journey diagram [<a href="https://mermaid-js.github.io/mermaid/#/user-journey">docs</a> - <a href="https://mermaid.live/edit#pako:eNplkMFuwjAQRH9l5TMiTVIC-FqqnjhxzWWJN4khsSN7XRSh_HsdKBVt97R6Mzsj-yoqq0hIAXCywRkaSwNxWHNHsB_hYt1ZmwYUfiueKtbWwIcFtjf5zgH2eCZgQgkrCXt64GgMg2fUzkvIn5Xd_V5COtMFvCH_62ht_5yk7MU8sn61HDTfxD8VYiF6cj1qFd94nWkpuKWYKWRcFdUYOi5FaaZoDYNCpnel2Toha-w8LQQGtofRVEKyC_Qw7TQ2DvsfV2dRUTy6Ch6H-UMb7TlGVtbUupl5cF3ELfPgZZLM8rLR3IbjsrJ94rVq0XH7uS2SIis2mOVUrHNc5bmqjul2U2evaa3WL2mGYpqmL2BGiho">live editor</a>]
|
||||
|
||||
```
|
||||
journey
|
||||
title My working day
|
||||
section Go to work
|
||||
Make tea: 5: Me
|
||||
Go upstairs: 3: Me
|
||||
Do work: 1: Me, Cat
|
||||
section Go home
|
||||
Go downstairs: 5: Me
|
||||
Sit down: 3: Me
|
||||
```
|
||||
|
||||
```mermaid
|
||||
journey
|
||||
title My working day
|
||||
section Go to work
|
||||
Make tea: 5: Me
|
||||
Go upstairs: 3: Me
|
||||
Do work: 1: Me, Cat
|
||||
section Go home
|
||||
Go downstairs: 5: Me
|
||||
Sit down: 3: Me
|
||||
```
|
||||
|
||||
### C4 diagram [<a href="https://mermaid-js.github.io/mermaid/#/c4c">docs</a>]
|
||||
|
||||
```
|
||||
C4Context
|
||||
title System Context diagram for Internet Banking System
|
||||
|
||||
Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.")
|
||||
Person(customerB, "Banking Customer B")
|
||||
Person_Ext(customerC, "Banking Customer C")
|
||||
System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.")
|
||||
|
||||
Person(customerD, "Banking Customer D", "A customer of the bank, <br/> with personal bank accounts.")
|
||||
|
||||
Enterprise_Boundary(b1, "BankBoundary") {
|
||||
|
||||
SystemDb_Ext(SystemE, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.")
|
||||
|
||||
System_Boundary(b2, "BankBoundary2") {
|
||||
System(SystemA, "Banking System A")
|
||||
System(SystemB, "Banking System B", "A system of the bank, with personal bank accounts.")
|
||||
}
|
||||
|
||||
System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.")
|
||||
SystemDb(SystemD, "Banking System D Database", "A system of the bank, with personal bank accounts.")
|
||||
|
||||
Boundary(b3, "BankBoundary3", "boundary") {
|
||||
SystemQueue(SystemF, "Banking System F Queue", "A system of the bank, with personal bank accounts.")
|
||||
SystemQueue_Ext(SystemG, "Banking System G Queue", "A system of the bank, with personal bank accounts.")
|
||||
}
|
||||
}
|
||||
|
||||
BiRel(customerA, SystemAA, "Uses")
|
||||
BiRel(SystemAA, SystemE, "Uses")
|
||||
Rel(SystemAA, SystemC, "Sends e-mails", "SMTP")
|
||||
Rel(SystemC, customerA, "Sends e-mails to")
|
||||
```
|
||||
|
||||
```mermaid
|
||||
C4Context
|
||||
title System Context diagram for Internet Banking System
|
||||
|
||||
Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.")
|
||||
Person(customerB, "Banking Customer B")
|
||||
Person_Ext(customerC, "Banking Customer C")
|
||||
System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.")
|
||||
|
||||
Person(customerD, "Banking Customer D", "A customer of the bank, <br/> with personal bank accounts.")
|
||||
|
||||
Enterprise_Boundary(b1, "BankBoundary") {
|
||||
|
||||
SystemDb_Ext(SystemE, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.")
|
||||
|
||||
System_Boundary(b2, "BankBoundary2") {
|
||||
System(SystemA, "Banking System A")
|
||||
System(SystemB, "Banking System B", "A system of the bank, with personal bank accounts.")
|
||||
}
|
||||
|
||||
System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.")
|
||||
SystemDb(SystemD, "Banking System D Database", "A system of the bank, with personal bank accounts.")
|
||||
|
||||
Boundary(b3, "BankBoundary3", "boundary") {
|
||||
SystemQueue(SystemF, "Banking System F Queue", "A system of the bank, with personal bank accounts.")
|
||||
SystemQueue_Ext(SystemG, "Banking System G Queue", "A system of the bank, with personal bank accounts.")
|
||||
}
|
||||
}
|
||||
|
||||
BiRel(customerA, SystemAA, "Uses")
|
||||
BiRel(SystemAA, SystemE, "Uses")
|
||||
Rel(SystemAA, SystemC, "Sends e-mails", "SMTP")
|
||||
Rel(SystemC, customerA, "Sends e-mails to")
|
||||
```
|
||||
|
||||
## Release
|
||||
|
||||
For those who have the permission to do so:
|
||||
|
||||
Update version number in `package.json`.
|
||||
|
||||
```sh
|
||||
npm publish
|
||||
```
|
||||
|
||||
The above command generates files into the `dist` folder and publishes them to npmjs.org.
|
||||
|
||||
## Related projects
|
||||
|
||||
- [Command Line Interface](https://github.com/mermaid-js/mermaid-cli)
|
||||
- [Live Editor](https://github.com/mermaid-js/mermaid-live-editor)
|
||||
- [HTTP Server](https://github.com/TomWright/mermaid-server)
|
||||
|
||||
## Contributors [](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+first+issue%21%22) [](https://github.com/mermaid-js/mermaid/graphs/contributors) [](https://github.com/mermaid-js/mermaid/graphs/contributors)
|
||||
|
||||
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](CONTRIBUTING.md)
|
||||
|
||||
## Security and safe diagrams
|
||||
|
||||
For public sites, it can be precarious to retrieve text from users on the internet, storing that content for presentation in a browser at a later stage. The reason is that the user content can contain embedded malicious scripts that will run when the data is presented. For Mermaid this is a risk, specially as mermaid diagrams contain many characters that are used in html which makes the standard sanitation unusable as it also breaks the diagrams. We still make an effort to sanitise the incoming code and keep refining the process but it is hard to guarantee that there are no loop holes.
|
||||
|
||||
As an extra level of security for sites with external users we are happy to introduce a new security level in which the diagram is rendered in a sandboxed iframe preventing javascript in the code from being executed. This is a great step forward for better security.
|
||||
|
||||
_Unfortunately you can not have a cake and eat it at the same time which in this case means that some of the interactive functionality gets blocked along with the possible malicious code._
|
||||
|
||||
## Reporting vulnerabilities
|
||||
|
||||
To report a vulnerability, please e-mail security@mermaid.live with a description of the issue, the steps you took to create the issue, affected versions, and if known, mitigations for the issue.
|
||||
|
||||
## Appreciation
|
||||
|
||||
A quick note from Knut Sveidqvist:
|
||||
|
||||
> _Many thanks to the [d3](https://d3js.org/) and [dagre-d3](https://github.com/cpettitt/dagre-d3) projects for providing the graphical layout and drawing libraries!_ >_Thanks also to the [js-sequence-diagram](https://bramp.github.io/js-sequence-diagrams) project for usage of the grammar for the sequence diagrams. Thanks to Jessica Peter for inspiration and starting point for gantt rendering._ >_Thank you to [Tyler Long](https://github.com/tylerlong) who has been a collaborator since April 2017._
|
||||
>
|
||||
> _Thank you to the ever-growing list of [contributors](https://github.com/knsv/mermaid/graphs/contributors) that brought the project this far!_
|
||||
|
||||
---
|
||||
|
||||
_Mermaid was created by Knut Sveidqvist for easier documentation._
|
@@ -1,334 +0,0 @@
|
||||
# mermaid
|
||||
|
||||
[](https://github.com/mermaid-js/mermaid/actions/workflows/build.yml) [](https://www.npmjs.com/package/mermaid) [](https://bundlephobia.com/package/mermaid) [](https://coveralls.io/github/mermaid-js/mermaid?branch=master) [](https://www.jsdelivr.com/package/npm/mermaid) [](https://www.npmjs.com/package/mermaid) [](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) [](https://twitter.com/mermaidjs_)
|
||||
|
||||
[English](./README.md) | 简体中文
|
||||
|
||||
<img src="./img/header.png" alt="" />
|
||||
|
||||
:trophy: **Mermaid 被提名并获得了 [JS Open Source Awards (2019)](https://osawards.com/javascript/2019) 的 "The most exciting use of technology" 奖项!!!**
|
||||
|
||||
**感谢所有参与进来提交 PR,解答疑问的人们! 🙏**
|
||||
|
||||
<a href="https://mermaid-js.github.io/mermaid/landing/"><img src="https://github.com/mermaid-js/mermaid/blob/master/docs/img/book-banner-pre-release.jpg" alt="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!"></a>
|
||||
|
||||
## 关于 Mermaid
|
||||
|
||||
<!-- <Main description> -->
|
||||
|
||||
Mermaid 是一个基于 Javascript 的图表绘制工具,通过解析类 Markdown 的文本语法来实现图表的创建和动态修改。Mermaid 诞生的主要目的是让文档的更新能够及时跟上开发进度。
|
||||
|
||||
> Doc-Rot 是 Mermaid 致力于解决的一个难题。
|
||||
|
||||
绘图和编写文档花费了开发者宝贵的开发时间,而且随着业务的变更,它很快就会过期。 但是如果缺少了图表或文档,对于生产力和团队新人的业务学习都会产生巨大的阻碍。 <br/>
|
||||
Mermaid 通过允许用户创建便于修改的图表来解决这一难题,它也可以作为生产脚本(或其他代码)的一部分。<br/>
|
||||
<br/>
|
||||
Mermaid 甚至能让非程序员也能通过 [Mermaid Live Editor](https://mermaid.live/) 轻松创建详细的图表。<br/>
|
||||
你可以访问 [教程](./docs/Tutorials.md) 来查看 Live Editor 的视频教程,也可以查看 [Mermaid 的集成和使用](./docs/integrations.md) 这个清单来检查你的文档工具是否已经集成了 Mermaid 支持。
|
||||
|
||||
如果想要查看关于 Mermaid 更详细的介绍及基础使用方式,可以查看 [入门指引](./docs/n00b-overview.md), [用法](./docs/usage.md) 和 [教程](./docs/Tutorials.md).
|
||||
|
||||
🌐 [CDN](https://unpkg.com/mermaid/) | 📖 [文档](https://mermaidjs.github.io) | 🙌 [贡献](https://github.com/mermaid-js/mermaid/blob/develop/CONTRIBUTING.md) | 📜 [更新日志](./docs/CHANGELOG.md)
|
||||
|
||||
<!-- </Main description> -->
|
||||
|
||||
## 示例
|
||||
|
||||
**下面是一些可以使用 Mermaid 创建的图表示例。点击 [语法](https://mermaid-js.github.io/mermaid/#/n00b-syntaxReference) 查看详情。**
|
||||
|
||||
<table>
|
||||
<!-- <Flowchart> -->
|
||||
|
||||
### 流程图 [<a href="https://mermaid-js.github.io/mermaid/#/flowchart">文档</a> - <a href="https://mermaidjs.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW0hhcmRdIC0tPnxUZXh0fCBCKFJvdW5kKVxuICAgIEIgLS0-IEN7RGVjaXNpb259XG4gICAgQyAtLT58T25lfCBEW1Jlc3VsdCAxXVxuICAgIEMgLS0-fFR3b3wgRVtSZXN1bHQgMl0iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9fQ">live editor</a>]
|
||||
|
||||
```
|
||||
flowchart LR
|
||||
A[Hard] -->|Text| B(Round)
|
||||
B --> C{Decision}
|
||||
C -->|One| D[Result 1]
|
||||
C -->|Two| E[Result 2]
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Hard] -->|Text| B(Round)
|
||||
B --> C{Decision}
|
||||
C -->|One| D[Result 1]
|
||||
C -->|Two| E[Result 2]
|
||||
```
|
||||
|
||||
### 时序图 [<a href="https://mermaid-js.github.io/mermaid/#/sequenceDiagram">文档</a> - <a href="https://mermaidjs.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG5BbGljZS0-PkpvaG46IEhlbGxvIEpvaG4sIGhvdyBhcmUgeW91P1xubG9vcCBIZWFsdGhjaGVja1xuICAgIEpvaG4tPj5Kb2huOiBGaWdodCBhZ2FpbnN0IGh5cG9jaG9uZHJpYVxuZW5kXG5Ob3RlIHJpZ2h0IG9mIEpvaG46IFJhdGlvbmFsIHRob3VnaHRzIVxuSm9obi0tPj5BbGljZTogR3JlYXQhXG5Kb2huLT4-Qm9iOiBIb3cgYWJvdXQgeW91P1xuQm9iLS0-PkpvaG46IEpvbGx5IGdvb2QhIiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifX0">live editor</a>]
|
||||
|
||||
```
|
||||
sequenceDiagram
|
||||
Alice->>John: Hello John, how are you?
|
||||
loop Healthcheck
|
||||
John->>John: Fight against hypochondria
|
||||
end
|
||||
Note right of John: Rational thoughts!
|
||||
John-->>Alice: Great!
|
||||
John->>Bob: How about you?
|
||||
Bob-->>John: Jolly good!
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
Alice->>John: Hello John, how are you?
|
||||
loop Healthcheck
|
||||
John->>John: Fight against hypochondria
|
||||
end
|
||||
Note right of John: Rational thoughts!
|
||||
John-->>Alice: Great!
|
||||
John->>Bob: How about you?
|
||||
Bob-->>John: Jolly good!
|
||||
```
|
||||
|
||||
### 甘特图 [<a href="https://mermaid-js.github.io/mermaid/#/gantt">文档</a> - <a href="https://mermaidjs.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ2FudHRcbnNlY3Rpb24gU2VjdGlvblxuQ29tcGxldGVkIDpkb25lLCAgICBkZXMxLCAyMDE0LTAxLTA2LDIwMTQtMDEtMDhcbkFjdGl2ZSAgICAgICAgOmFjdGl2ZSwgIGRlczIsIDIwMTQtMDEtMDcsIDNkXG5QYXJhbGxlbCAxICAgOiAgICAgICAgIGRlczMsIGFmdGVyIGRlczEsIDFkXG5QYXJhbGxlbCAyICAgOiAgICAgICAgIGRlczQsIGFmdGVyIGRlczEsIDFkXG5QYXJhbGxlbCAzICAgOiAgICAgICAgIGRlczUsIGFmdGVyIGRlczMsIDFkXG5QYXJhbGxlbCA0ICAgOiAgICAgICAgIGRlczYsIGFmdGVyIGRlczQsIDFkIiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifX0">live editor</a>]
|
||||
|
||||
```
|
||||
gantt
|
||||
section Section
|
||||
Completed :done, des1, 2014-01-06,2014-01-08
|
||||
Active :active, des2, 2014-01-07, 3d
|
||||
Parallel 1 : des3, after des1, 1d
|
||||
Parallel 2 : des4, after des1, 1d
|
||||
Parallel 3 : des5, after des3, 1d
|
||||
Parallel 4 : des6, after des4, 1d
|
||||
```
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
section Section
|
||||
Completed :done, des1, 2014-01-06,2014-01-08
|
||||
Active :active, des2, 2014-01-07, 3d
|
||||
Parallel 1 : des3, after des1, 1d
|
||||
Parallel 2 : des4, after des1, 1d
|
||||
Parallel 3 : des5, after des3, 1d
|
||||
Parallel 4 : des6, after des4, 1d
|
||||
```
|
||||
|
||||
### 类图 [<a href="https://mermaid-js.github.io/mermaid/#/classDiagram">文档</a> - <a href="https://mermaidjs.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiY2xhc3NEaWFncmFtXG5DbGFzczAxIDx8LS0gQXZlcnlMb25nQ2xhc3MgOiBDb29sXG48PGludGVyZmFjZT4-IENsYXNzMDFcbkNsYXNzMDkgLS0-IEMyIDogV2hlcmUgYW0gaT9cbkNsYXNzMDkgLS0qIEMzXG5DbGFzczA5IC0tfD4gQ2xhc3MwN1xuQ2xhc3MwNyA6IGVxdWFscygpXG5DbGFzczA3IDogT2JqZWN0W10gZWxlbWVudERhdGFcbkNsYXNzMDEgOiBzaXplKClcbkNsYXNzMDEgOiBpbnQgY2hpbXBcbkNsYXNzMDEgOiBpbnQgZ29yaWxsYVxuY2xhc3MgQ2xhc3MxMCB7XG4gID4-c2VydmljZT4-XG4gIGludCBpZFxuICBzaXplKClcbn0iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9fQ">live editor</a>]
|
||||
|
||||
```
|
||||
classDiagram
|
||||
Class01 <|-- AveryLongClass : Cool
|
||||
<<Interface>> Class01
|
||||
Class09 --> C2 : Where am I?
|
||||
Class09 --* C3
|
||||
Class09 --|> Class07
|
||||
Class07 : equals()
|
||||
Class07 : Object[] elementData
|
||||
Class01 : size()
|
||||
Class01 : int chimp
|
||||
Class01 : int gorilla
|
||||
class Class10 {
|
||||
<<service>>
|
||||
int id
|
||||
size()
|
||||
}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
Class01 <|-- AveryLongClass : Cool
|
||||
<<Interface>> Class01
|
||||
Class09 --> C2 : Where am I?
|
||||
Class09 --* C3
|
||||
Class09 --|> Class07
|
||||
Class07 : equals()
|
||||
Class07 : Object[] elementData
|
||||
Class01 : size()
|
||||
Class01 : int chimp
|
||||
Class01 : int gorilla
|
||||
class Class10 {
|
||||
<<service>>
|
||||
int id
|
||||
size()
|
||||
}
|
||||
```
|
||||
|
||||
### 状态图 [[<a href="https://mermaid-js.github.io/mermaid/#/stateDiagram">docs</a> - <a href="https://mermaid.live/#/edit/eyJjb2RlIjoic3RhdGVEaWFncmFtLXYyXG4gICAgWypdIC0tPiBTdGlsbFxuICAgIFN0aWxsIC0tPiBbKl1cbiAgICBTdGlsbCAtLT4gTW92aW5nXG4gICAgTW92aW5nIC0tPiBTdGlsbFxuICAgIE1vdmluZyAtLT4gQ3Jhc2hcbiAgICBDcmFzaCAtLT4gWypdIiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQiLCJ0aGVtZVZhcmlhYmxlcyI6eyJiYWNrZ3JvdW5kIjoid2hpdGUiLCJwcmltYXJ5Q29sb3IiOiIjRUNFQ0ZGIiwic2Vjb25kYXJ5Q29sb3IiOiIjZmZmZmRlIiwidGVydGlhcnlDb2xvciI6ImhzbCg4MCwgMTAwJSwgOTYuMjc0NTA5ODAzOSUpIiwicHJpbWFyeUJvcmRlckNvbG9yIjoiaHNsKDI0MCwgNjAlLCA4Ni4yNzQ1MDk4MDM5JSkiLCJzZWNvbmRhcnlCb3JkZXJDb2xvciI6ImhzbCg2MCwgNjAlLCA4My41Mjk0MTE3NjQ3JSkiLCJ0ZXJ0aWFyeUJvcmRlckNvbG9yIjoiaHNsKDgwLCA2MCUsIDg2LjI3NDUwOTgwMzklKSIsInByaW1hcnlUZXh0Q29sb3IiOiIjMTMxMzAwIiwic2Vjb25kYXJ5VGV4dENvbG9yIjoiIzAwMDAyMSIsInRlcnRpYXJ5VGV4dENvbG9yIjoicmdiKDkuNTAwMDAwMDAwMSwgOS41MDAwMDAwMDAxLCA5LjUwMDAwMDAwMDEpIiwibGluZUNvbG9yIjoiIzMzMzMzMyIsInRleHRDb2xvciI6IiMzMzMiLCJtYWluQmtnIjoiI0VDRUNGRiIsInNlY29uZEJrZyI6IiNmZmZmZGUiLCJib3JkZXIxIjoiIzkzNzBEQiIsImJvcmRlcjIiOiIjYWFhYTMzIiwiYXJyb3doZWFkQ29sb3IiOiIjMzMzMzMzIiwiZm9udEZhbWlseSI6IlwidHJlYnVjaGV0IG1zXCIsIHZlcmRhbmEsIGFyaWFsIiwiZm9udFNpemUiOiIxNnB4IiwibGFiZWxCYWNrZ3JvdW5kIjoiI2U4ZThlOCIsIm5vZGVCa2ciOiIjRUNFQ0ZGIiwibm9kZUJvcmRlciI6IiM5MzcwREIiLCJjbHVzdGVyQmtnIjoiI2ZmZmZkZSIsImNsdXN0ZXJCb3JkZXIiOiIjYWFhYTMzIiwiZGVmYXVsdExpbmtDb2xvciI6IiMzMzMzMzMiLCJ0aXRsZUNvbG9yIjoiIzMzMyIsImVkZ2VMYWJlbEJhY2tncm91bmQiOiIjZThlOGU4IiwiYWN0b3JCb3JkZXIiOiJoc2woMjU5LjYyNjE2ODIyNDMsIDU5Ljc3NjUzNjMxMjglLCA4Ny45MDE5NjA3ODQzJSkiLCJhY3RvckJrZyI6IiNFQ0VDRkYiLCJhY3RvclRleHRDb2xvciI6ImJsYWNrIiwiYWN0b3JMaW5lQ29sb3IiOiJncmV5Iiwic2lnbmFsQ29sb3IiOiIjMzMzIiwic2lnbmFsVGV4dENvbG9yIjoiIzMzMyIsImxhYmVsQm94QmtnQ29sb3IiOiIjRUNFQ0ZGIiwibGFiZWxCb3hCb3JkZXJDb2xvciI6ImhzbCgyNTkuNjI2MTY4MjI0MywgNTkuNzc2NTM2MzEyOCUsIDg3LjkwMTk2MDc4NDMlKSIsImxhYmVsVGV4dENvbG9yIjoiYmxhY2siLCJsb29wVGV4dENvbG9yIjoiYmxhY2siLCJub3RlQm9yZGVyQ29sb3IiOiIjYWFhYTMzIiwibm90ZUJrZ0NvbG9yIjoiI2ZmZjVhZCIsIm5vdGVUZXh0Q29sb3IiOiJibGFjayIsImFjdGl2YXRpb25Cb3JkZXJDb2xvciI6IiM2NjYiLCJhY3RpdmF0aW9uQmtnQ29sb3IiOiIjZjRmNGY0Iiwic2VxdWVuY2VOdW1iZXJDb2xvciI6IndoaXRlIiwic2VjdGlvbkJrZ0NvbG9yIjoicmdiYSgxMDIsIDEwMiwgMjU1LCAwLjQ5KSIsImFsdFNlY3Rpb25Ca2dDb2xvciI6IndoaXRlIiwic2VjdGlvbkJrZ0NvbG9yMiI6IiNmZmY0MDAiLCJ0YXNrQm9yZGVyQ29sb3IiOiIjNTM0ZmJjIiwidGFza0JrZ0NvbG9yIjoiIzhhOTBkZCIsInRhc2tUZXh0TGlnaHRDb2xvciI6IndoaXRlIiwidGFza1RleHRDb2xvciI6IndoaXRlIiwidGFza1RleHREYXJrQ29sb3IiOiJibGFjayIsInRhc2tUZXh0T3V0c2lkZUNvbG9yIjoiYmxhY2siLCJ0YXNrVGV4dENsaWNrYWJsZUNvbG9yIjoiIzAwMzE2MyIsImFjdGl2ZVRhc2tCb3JkZXJDb2xvciI6IiM1MzRmYmMiLCJhY3RpdmVUYXNrQmtnQ29sb3IiOiIjYmZjN2ZmIiwiZ3JpZENvbG9yIjoibGlnaHRncmV5IiwiZG9uZVRhc2tCa2dDb2xvciI6ImxpZ2h0Z3JleSIsImRvbmVUYXNrQm9yZGVyQ29sb3IiOiJncmV5IiwiY3JpdEJvcmRlckNvbG9yIjoiI2ZmODg4OCIsImNyaXRCa2dDb2xvciI6InJlZCIsInRvZGF5TGluZUNvbG9yIjoicmVkIiwibGFiZWxDb2xvciI6ImJsYWNrIiwiZXJyb3JCa2dDb2xvciI6IiM1NTIyMjIiLCJlcnJvclRleHRDb2xvciI6IiM1NTIyMjIiLCJjbGFzc1RleHQiOiIjMTMxMzAwIiwiZmlsbFR5cGUwIjoiI0VDRUNGRiIsImZpbGxUeXBlMSI6IiNmZmZmZGUiLCJmaWxsVHlwZTIiOiJoc2woMzA0LCAxMDAlLCA5Ni4yNzQ1MDk4MDM5JSkiLCJmaWxsVHlwZTMiOiJoc2woMTI0LCAxMDAlLCA5My41Mjk0MTE3NjQ3JSkiLCJmaWxsVHlwZTQiOiJoc2woMTc2LCAxMDAlLCA5Ni4yNzQ1MDk4MDM5JSkiLCJmaWxsVHlwZTUiOiJoc2woLTQsIDEwMCUsIDkzLjUyOTQxMTc2NDclKSIsImZpbGxUeXBlNiI6ImhzbCg4LCAxMDAlLCA5Ni4yNzQ1MDk4MDM5JSkiLCJmaWxsVHlwZTciOiJoc2woMTg4LCAxMDAlLCA5My41Mjk0MTE3NjQ3JSkifX0sInVwZGF0ZUVkaXRvciI6ZmFsc2V9">live editor</a>]
|
||||
|
||||
```
|
||||
stateDiagram-v2
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]
|
||||
```
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]
|
||||
```
|
||||
|
||||
### 饼图 [<a href="https://mermaid-js.github.io/mermaid/#/pie">文档</a> - <a href="https://mermaidjs.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoicGllXG5cIkRvZ3NcIiA6IDQyLjk2XG5cIkNhdHNcIiA6IDUwLjA1XG5cIlJhdHNcIiA6IDEwLjAxIiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifX0">live editor</a>]
|
||||
|
||||
```
|
||||
pie
|
||||
"Dogs" : 386
|
||||
"Cats" : 85
|
||||
"Rats" : 15
|
||||
```
|
||||
|
||||
```mermaid
|
||||
pie
|
||||
"Dogs" : 386
|
||||
"Cats" : 85
|
||||
"Rats" : 15
|
||||
```
|
||||
|
||||
### Git 图 [实验特性 - <a href="https://mermaidjs.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ2l0R3JhcGg6XG5vcHRpb25zXG57XG4gICAgXCJub2RlU3BhY2luZ1wiOiAxNTAsXG4gICAgXCJub2RlUmFkaXVzXCI6IDEwXG59XG5lbmRcbmNvbW1pdFxuYnJhbmNoIG5ld2JyYW5jaFxuY2hlY2tvdXQgbmV3YnJhbmNoXG5jb21taXRcbmNvbW1pdFxuY2hlY2tvdXQgbWFzdGVyXG5jb21taXRcbmNvbW1pdFxubWVyZ2UgbmV3YnJhbmNoXG4iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9fQ">live editor</a>]
|
||||
|
||||
### 用户体验旅程图 [<a href="https://mermaid-js.github.io/mermaid/#/user-journey">文档</a> - <a href="https://mermaidjs.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic3RhdGVEaWFncmFtXG4gICAgWypdIC0tPiBTdGlsbFxuICAgIFN0aWxsIC0tPiBbKl1cbiAgICBTdGlsbCAtLT4gTW92aW5nXG4gICAgTW92aW5nIC0tPiBTdGlsbFxuICAgIE1vdmluZyAtLT4gQ3Jhc2hcbiAgICBDcmFzaCAtLT4gWypdIiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifX0">live editor</a>]
|
||||
|
||||
```
|
||||
journey
|
||||
title My working day
|
||||
section Go to work
|
||||
Make tea: 5: Me
|
||||
Go upstairs: 3: Me
|
||||
Do work: 1: Me, Cat
|
||||
section Go home
|
||||
Go downstairs: 5: Me
|
||||
Sit down: 3: Me
|
||||
```
|
||||
|
||||
```mermaid
|
||||
journey
|
||||
title My working day
|
||||
section Go to work
|
||||
Make tea: 5: Me
|
||||
Go upstairs: 3: Me
|
||||
Do work: 1: Me, Cat
|
||||
section Go home
|
||||
Go downstairs: 5: Me
|
||||
Sit down: 3: Me
|
||||
```
|
||||
|
||||
### C4 图 [<a href="https://mermaid-js.github.io/mermaid/#/c4c">文档</a>]
|
||||
|
||||
```
|
||||
C4Context
|
||||
title System Context diagram for Internet Banking System
|
||||
|
||||
Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.")
|
||||
Person(customerB, "Banking Customer B")
|
||||
Person_Ext(customerC, "Banking Customer C")
|
||||
System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.")
|
||||
|
||||
Person(customerD, "Banking Customer D", "A customer of the bank, <br/> with personal bank accounts.")
|
||||
|
||||
Enterprise_Boundary(b1, "BankBoundary") {
|
||||
|
||||
SystemDb_Ext(SystemE, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.")
|
||||
|
||||
System_Boundary(b2, "BankBoundary2") {
|
||||
System(SystemA, "Banking System A")
|
||||
System(SystemB, "Banking System B", "A system of the bank, with personal bank accounts.")
|
||||
}
|
||||
|
||||
System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.")
|
||||
SystemDb(SystemD, "Banking System D Database", "A system of the bank, with personal bank accounts.")
|
||||
|
||||
Boundary(b3, "BankBoundary3", "boundary") {
|
||||
SystemQueue(SystemF, "Banking System F Queue", "A system of the bank, with personal bank accounts.")
|
||||
SystemQueue_Ext(SystemG, "Banking System G Queue", "A system of the bank, with personal bank accounts.")
|
||||
}
|
||||
}
|
||||
|
||||
BiRel(customerA, SystemAA, "Uses")
|
||||
BiRel(SystemAA, SystemE, "Uses")
|
||||
Rel(SystemAA, SystemC, "Sends e-mails", "SMTP")
|
||||
Rel(SystemC, customerA, "Sends e-mails to")
|
||||
```
|
||||
|
||||
```mermaid
|
||||
C4Context
|
||||
title System Context diagram for Internet Banking System
|
||||
|
||||
Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.")
|
||||
Person(customerB, "Banking Customer B")
|
||||
Person_Ext(customerC, "Banking Customer C")
|
||||
System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.")
|
||||
|
||||
Person(customerD, "Banking Customer D", "A customer of the bank, <br/> with personal bank accounts.")
|
||||
|
||||
Enterprise_Boundary(b1, "BankBoundary") {
|
||||
|
||||
SystemDb_Ext(SystemE, "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc.")
|
||||
|
||||
System_Boundary(b2, "BankBoundary2") {
|
||||
System(SystemA, "Banking System A")
|
||||
System(SystemB, "Banking System B", "A system of the bank, with personal bank accounts.")
|
||||
}
|
||||
|
||||
System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.")
|
||||
SystemDb(SystemD, "Banking System D Database", "A system of the bank, with personal bank accounts.")
|
||||
|
||||
Boundary(b3, "BankBoundary3", "boundary") {
|
||||
SystemQueue(SystemF, "Banking System F Queue", "A system of the bank, with personal bank accounts.")
|
||||
SystemQueue_Ext(SystemG, "Banking System G Queue", "A system of the bank, with personal bank accounts.")
|
||||
}
|
||||
}
|
||||
|
||||
BiRel(customerA, SystemAA, "Uses")
|
||||
BiRel(SystemAA, SystemE, "Uses")
|
||||
Rel(SystemAA, SystemC, "Sends e-mails", "SMTP")
|
||||
Rel(SystemC, customerA, "Sends e-mails to")
|
||||
```
|
||||
|
||||
## 发布
|
||||
|
||||
对于有权限的同学来说,你可以通过以下步骤来完成发布操作:
|
||||
|
||||
更新 `package.json` 中的版本号,然后执行如下命令:
|
||||
|
||||
```sh
|
||||
npm publish
|
||||
```
|
||||
|
||||
以上的命令会将文件打包到 `dist` 目录并发布至 npmjs.org.
|
||||
|
||||
## 相关项目
|
||||
|
||||
- [Command Line Interface](https://github.com/mermaid-js/mermaid-cli)
|
||||
- [Live Editor](https://github.com/mermaid-js/mermaid-live-editor)
|
||||
- [HTTP Server](https://github.com/TomWright/mermaid-server)
|
||||
|
||||
## 贡献者 [](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+first+issue%21%22) [](https://github.com/mermaid-js/mermaid/graphs/contributors) [](https://github.com/mermaid-js/mermaid/graphs/contributors)
|
||||
|
||||
Mermaid 是一个不断发展中的社区,并且还在接收新的贡献者。有很多不同的方式可以参与进来,而且我们还在寻找额外的帮助。如果你想知道如何开始贡献,请查看 [这个 issue](https://github.com/mermaid-js/mermaid/issues/866)。
|
||||
|
||||
关于如何贡献的详细信息可以在 [贡献指南](CONTRIBUTING.md) 中找到。
|
||||
|
||||
## 安全
|
||||
|
||||
对于公开网站来说,从互联网上的用户处检索文本、存储供后续在浏览器中展示的内容可能是不安全的,理由是用户的内容可能嵌入一些数据加载完成之后就会运行的恶意脚本,这些对于 Mermaid 来说毫无疑问是一个风险,尤其是 mermaid 图表还包含了许多在 html 中使用的字符,这意味着我们难以使用常规的手段来过滤不安全代码,因为这些常规手段会造成图表损坏。我们仍然在努力对获取到的代码进行安全过滤并不断完善我们的程序,但很难保证没有漏洞。
|
||||
|
||||
作为拥有外部用户的网站的额外安全级别,我们很高兴推出一个新的安全级别,其中的图表在沙盒 iframe 中渲染,防止代码中的 javascript 被执行,这是在安全性方面迈出的一大步。
|
||||
|
||||
_很不幸的是,鱼与熊掌不可兼得,在这个场景下它意味着在可能的恶意代码被阻止时,也会损失部分交互能力_。
|
||||
|
||||
## 报告漏洞
|
||||
|
||||
如果想要报告漏洞,请发送邮件到 security@mermaid.live, 并附上问题的描述、复现问题的步骤、受影响的版本,以及解决问题的方案(如果有的话)。
|
||||
|
||||
## 鸣谢
|
||||
|
||||
来自 Knut Sveidqvist:
|
||||
|
||||
> _特别感谢 [d3](https://d3js.org/) 和 [dagre-d3](https://github.com/cpettitt/dagre-d3) 这两个优秀的项目,它们提供了图形布局和绘图工具库! _ >_同样感谢 [js-sequence-diagram](https://bramp.github.io/js-sequence-diagrams) 提供了时序图语法的使用。 感谢 Jessica Peter 提供了甘特图渲染的灵感。_ >_感谢 [Tyler Long](https://github.com/tylerlong) 从 2017 年四月开始成为了项目的合作者。_
|
||||
>
|
||||
> _感谢越来越多的 [贡献者们](https://github.com/knsv/mermaid/graphs/contributors),没有你们,就没有这个项目的今天!_
|
||||
|
||||
---
|
||||
|
||||
_Mermaid 是由 Knut Sveidqvist 创建,它为了更简单的文档编写而生。_
|
@@ -1,16 +1,15 @@
|
||||
{
|
||||
"name": "mermaid",
|
||||
"version": "9.2.2",
|
||||
"version": "10.2.3",
|
||||
"description": "Markdown-ish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
|
||||
"main": "./dist/mermaid.min.js",
|
||||
"type": "module",
|
||||
"module": "./dist/mermaid.core.mjs",
|
||||
"types": "./dist/mermaid.d.ts",
|
||||
"type": "commonjs",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/mermaid.min.js",
|
||||
"types": "./dist/mermaid.d.ts",
|
||||
"import": "./dist/mermaid.core.mjs",
|
||||
"types": "./dist/mermaid.d.ts"
|
||||
"default": "./dist/mermaid.core.mjs"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
@@ -29,12 +28,12 @@
|
||||
"docs:build": "rimraf ../../docs && pnpm docs:spellcheck && pnpm docs:code && ts-node-esm src/docs.mts",
|
||||
"docs:verify": "pnpm docs:spellcheck && pnpm docs:code && ts-node-esm src/docs.mts --verify",
|
||||
"docs:pre:vitepress": "rimraf src/vitepress && pnpm docs:code && ts-node-esm src/docs.mts --vitepress",
|
||||
"docs:build:vitepress": "pnpm docs:pre:vitepress && vitepress build src/vitepress",
|
||||
"docs:dev": "pnpm docs:pre:vitepress && concurrently \"vitepress dev src/vitepress\" \"ts-node-esm src/docs.mts --watch --vitepress\"",
|
||||
"docs:build:vitepress": "pnpm docs:pre:vitepress && (cd src/vitepress && pnpm --filter ./ install --no-frozen-lockfile --ignore-scripts && pnpm run build) && cpy --flat src/docs/landing/ ./src/vitepress/.vitepress/dist/landing",
|
||||
"docs:dev": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./ src/vitepress dev\" \"ts-node-esm src/docs.mts --watch --vitepress\"",
|
||||
"docs:serve": "pnpm docs:build:vitepress && vitepress serve src/vitepress",
|
||||
"docs:spellcheck": "cspell --config ../../cSpell.json \"src/docs/**/*.md\"",
|
||||
"release": "pnpm build",
|
||||
"prepublishOnly": "pnpm -w run build"
|
||||
"prepublishOnly": "cpy '../../README.*' ./ --cwd=. && pnpm -w run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -53,57 +52,67 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^6.0.0",
|
||||
"d3": "^7.0.0",
|
||||
"dagre-d3-es": "7.0.4",
|
||||
"dompurify": "2.4.1",
|
||||
"fast-clone": "^1.5.13",
|
||||
"graphlib": "^2.1.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"cytoscape": "^3.23.0",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
"cytoscape-fcose": "^2.1.0",
|
||||
"d3": "^7.4.0",
|
||||
"dagre-d3-es": "7.0.10",
|
||||
"dayjs": "^1.11.7",
|
||||
"dompurify": "3.0.3",
|
||||
"elkjs": "^0.8.2",
|
||||
"khroma": "^2.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moment-mini": "^2.24.0",
|
||||
"mdast-util-from-markdown": "^1.3.0",
|
||||
"non-layered-tidy-tree-layout": "^2.0.2",
|
||||
"stylis": "^4.1.2",
|
||||
"uuid": "^9.0.0"
|
||||
"stylis": "^4.1.3",
|
||||
"ts-dedent": "^2.2.0",
|
||||
"uuid": "^9.0.0",
|
||||
"web-worker": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cytoscape": "^3.19.9",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/dompurify": "^2.4.0",
|
||||
"@types/jsdom": "^20.0.1",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/dompurify": "^3.0.2",
|
||||
"@types/jsdom": "^21.1.1",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/micromatch": "^4.0.2",
|
||||
"@types/prettier": "^2.7.1",
|
||||
"@types/prettier": "^2.7.2",
|
||||
"@types/stylis": "^4.0.2",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
||||
"@typescript-eslint/parser": "^5.42.1",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.0",
|
||||
"@typescript-eslint/parser": "^5.59.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"concurrently": "^7.5.0",
|
||||
"concurrently": "^8.0.1",
|
||||
"coveralls": "^3.1.1",
|
||||
"cspell": "^6.14.3",
|
||||
"globby": "^13.1.2",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"cpy-cli": "^4.2.0",
|
||||
"cspell": "^6.31.1",
|
||||
"csstree-validator": "^3.0.0",
|
||||
"globby": "^13.1.4",
|
||||
"jison": "^0.4.18",
|
||||
"js-base64": "^3.7.2",
|
||||
"jsdom": "^20.0.2",
|
||||
"js-base64": "^3.7.5",
|
||||
"jsdom": "^21.1.1",
|
||||
"micromatch": "^4.0.5",
|
||||
"moment": "^2.29.4",
|
||||
"path-browserify": "^1.0.1",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier": "^2.8.8",
|
||||
"remark": "^14.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"start-server-and-test": "^1.14.0",
|
||||
"typedoc": "^0.23.18",
|
||||
"typedoc-plugin-markdown": "^3.13.6",
|
||||
"typescript": "^4.8.4",
|
||||
"unist-util-flatmap": "^1.0.0"
|
||||
"remark-frontmatter": "^4.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"rimraf": "^5.0.0",
|
||||
"start-server-and-test": "^2.0.0",
|
||||
"typedoc": "^0.24.5",
|
||||
"typedoc-plugin-markdown": "^3.15.2",
|
||||
"typescript": "^5.0.4",
|
||||
"unist-util-flatmap": "^1.0.0",
|
||||
"vitepress": "^1.0.0-alpha.72",
|
||||
"vitepress-plugin-search": "^1.0.4-alpha.20"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"dist/",
|
||||
"README.md"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css",
|
||||
"**/*.scss"
|
||||
]
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
|
@@ -1,27 +1,32 @@
|
||||
import * as configApi from './config';
|
||||
import { log } from './logger';
|
||||
import { getDiagram, registerDiagram } from './diagram-api/diagramAPI';
|
||||
import { detectType, getDiagramLoader } from './diagram-api/detectType';
|
||||
import { extractFrontMatter } from './diagram-api/frontmatter';
|
||||
import { isDetailedError, type DetailedError } from './utils';
|
||||
import * as configApi from './config.js';
|
||||
import { log } from './logger.js';
|
||||
import { getDiagram, registerDiagram } from './diagram-api/diagramAPI.js';
|
||||
import { detectType, getDiagramLoader } from './diagram-api/detectType.js';
|
||||
import { extractFrontMatter } from './diagram-api/frontmatter.js';
|
||||
import { UnknownDiagramError } from './errors.js';
|
||||
import { DetailedError } from './utils.js';
|
||||
import { cleanupComments } from './diagram-api/comments.js';
|
||||
|
||||
export type ParseErrorFunction = (err: string | DetailedError, hash?: any) => void;
|
||||
export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?: any) => void;
|
||||
|
||||
/**
|
||||
* An object representing a parsed mermaid diagram definition.
|
||||
* @privateRemarks This is exported as part of the public mermaidAPI.
|
||||
*/
|
||||
export class Diagram {
|
||||
type = 'graph';
|
||||
parser;
|
||||
renderer;
|
||||
db;
|
||||
private detectTypeFailed = false;
|
||||
constructor(public txt: string, parseError?: ParseErrorFunction) {
|
||||
private detectError?: UnknownDiagramError;
|
||||
constructor(public text: string) {
|
||||
this.text += '\n';
|
||||
const cnf = configApi.getConfig();
|
||||
this.txt = txt;
|
||||
try {
|
||||
this.type = detectType(txt, cnf);
|
||||
this.type = detectType(text, cnf);
|
||||
} catch (e) {
|
||||
this.handleError(e, parseError);
|
||||
this.type = 'error';
|
||||
this.detectTypeFailed = true;
|
||||
this.detectError = e as UnknownDiagramError;
|
||||
}
|
||||
const diagram = getDiagram(this.type);
|
||||
log.debug('Type ' + this.type);
|
||||
@@ -39,50 +44,28 @@ export class Diagram {
|
||||
// Similarly, we can't do this in getDiagramFromText() because some code
|
||||
// calls diagram.db.clear(), which would reset anything set by
|
||||
// extractFrontMatter().
|
||||
this.parser.parse = (text: string) => originalParse(extractFrontMatter(text, this.db));
|
||||
|
||||
this.parser.parse = (text: string) =>
|
||||
originalParse(cleanupComments(extractFrontMatter(text, this.db)));
|
||||
|
||||
this.parser.parser.yy = this.db;
|
||||
if (diagram.init) {
|
||||
diagram.init(cnf);
|
||||
log.debug('Initialized diagram ' + this.type, cnf);
|
||||
log.info('Initialized diagram ' + this.type, cnf);
|
||||
}
|
||||
this.txt += '\n';
|
||||
|
||||
this.parse(this.txt, parseError);
|
||||
this.parse();
|
||||
}
|
||||
|
||||
parse(text: string, parseError?: ParseErrorFunction): boolean {
|
||||
if (this.detectTypeFailed) {
|
||||
return false;
|
||||
parse() {
|
||||
if (this.detectError) {
|
||||
throw this.detectError;
|
||||
}
|
||||
try {
|
||||
text = text + '\n';
|
||||
this.db.clear?.();
|
||||
this.parser.parse(text);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.handleError(error, parseError);
|
||||
}
|
||||
return false;
|
||||
this.db.clear?.();
|
||||
this.parser.parse(this.text);
|
||||
}
|
||||
|
||||
handleError(error: unknown, parseError?: ParseErrorFunction) {
|
||||
// Is this the correct way to access mermaid's parseError()
|
||||
// method ? (or global.mermaid.parseError()) ?
|
||||
|
||||
if (parseError === undefined) {
|
||||
// No mermaid.parseError() handler defined, so re-throw it
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (isDetailedError(error)) {
|
||||
// Handle case where error string and hash were
|
||||
// wrapped in object like`const error = { str, hash };`
|
||||
parseError(error.str, error.hash);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, assume it is just an error string and pass it on
|
||||
parseError(error as string);
|
||||
async render(id: string, version: string) {
|
||||
await this.renderer.draw(this.text, id, version, this);
|
||||
}
|
||||
|
||||
getParser() {
|
||||
@@ -94,30 +77,29 @@ export class Diagram {
|
||||
}
|
||||
}
|
||||
|
||||
export const getDiagramFromText = (
|
||||
txt: string,
|
||||
parseError?: ParseErrorFunction
|
||||
): Diagram | Promise<Diagram> => {
|
||||
const type = detectType(txt, configApi.getConfig());
|
||||
/**
|
||||
* Parse the text asynchronously and generate a Diagram object asynchronously.
|
||||
* **Warning:** This function may be changed in the future.
|
||||
* @alpha
|
||||
* @param text - The mermaid diagram definition.
|
||||
* @returns A the Promise of a Diagram object.
|
||||
* @throws {@link UnknownDiagramError} if the diagram type can not be found.
|
||||
* @privateRemarks This is exported as part of the public mermaidAPI.
|
||||
*/
|
||||
export const getDiagramFromText = async (text: string): Promise<Diagram> => {
|
||||
const type = detectType(text, configApi.getConfig());
|
||||
try {
|
||||
// Trying to find the diagram
|
||||
getDiagram(type);
|
||||
return new Diagram(txt, parseError);
|
||||
} catch (error) {
|
||||
const loader = getDiagramLoader(type);
|
||||
if (!loader) {
|
||||
throw new Error(`Diagram ${type} not found.`);
|
||||
throw new UnknownDiagramError(`Diagram ${type} not found.`);
|
||||
}
|
||||
// TODO: Uncomment for v10
|
||||
// // Diagram not available, loading it
|
||||
// const { diagram } = await loader();
|
||||
// registerDiagram(type, diagram, undefined, diagram.injectUtils);
|
||||
// // new diagram will try getDiagram again and if fails then it is a valid throw
|
||||
return loader().then(({ diagram }) => {
|
||||
registerDiagram(type, diagram, undefined);
|
||||
return new Diagram(txt, parseError);
|
||||
});
|
||||
// Diagram not available, loading it.
|
||||
// new diagram will try getDiagram again and if fails then it is a valid throw
|
||||
const { id, diagram } = await loader();
|
||||
registerDiagram(id, diagram);
|
||||
}
|
||||
return new Diagram(text);
|
||||
};
|
||||
|
||||
export default Diagram;
|
||||
|
@@ -3,26 +3,16 @@
|
||||
*
|
||||
* We can't easily use `vi.spyOn(mermaidAPI, "function")` since the object is frozen with `Object.freeze()`.
|
||||
*/
|
||||
import * as configApi from '../config';
|
||||
import * as configApi from '../config.js';
|
||||
import { vi } from 'vitest';
|
||||
import { addDiagrams } from '../diagram-api/diagram-orchestration';
|
||||
import Diagram, { type ParseErrorFunction } from '../Diagram';
|
||||
|
||||
// Normally, we could just do the following to get the original `parse()`
|
||||
// implementation, however, requireActual returns a promise and it's not documented how to use withing mock file.
|
||||
|
||||
/** {@inheritDoc mermaidAPI.parse} */
|
||||
function parse(text: string, parseError?: ParseErrorFunction): boolean {
|
||||
addDiagrams();
|
||||
const diagram = new Diagram(text, parseError);
|
||||
return diagram.parse(text, parseError);
|
||||
}
|
||||
import { mermaidAPI as mAPI } from '../mermaidAPI.js';
|
||||
|
||||
// original version cannot be modified since it was frozen with `Object.freeze()`
|
||||
export const mermaidAPI = {
|
||||
render: vi.fn(),
|
||||
renderAsync: vi.fn(),
|
||||
parse,
|
||||
render: vi.fn().mockResolvedValue({
|
||||
svg: '<svg></svg>',
|
||||
}),
|
||||
parse: mAPI.parse,
|
||||
parseDirective: vi.fn(),
|
||||
initialize: vi.fn(),
|
||||
getConfig: configApi.getConfig,
|
||||
|
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* This method will add a basic title and description element to a chart. The yy parser will need to
|
||||
* respond to getAccTitle and getAccDescription, where the title is the title element on the chart,
|
||||
* which is generally not displayed and the accDescription is the description element on the chart,
|
||||
* which is never displayed.
|
||||
*
|
||||
* The following charts display their title as a visual and accessibility element: gantt
|
||||
*
|
||||
* @param yy_parser
|
||||
* @param svg
|
||||
* @param id
|
||||
*/
|
||||
export default function addSVGAccessibilityFields(yy_parser, svg, id) {
|
||||
if (typeof svg.insert === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
let title_string = yy_parser.getAccTitle();
|
||||
let description = yy_parser.getAccDescription();
|
||||
svg.attr('role', 'img').attr('aria-labelledby', 'chart-title-' + id + ' chart-desc-' + id);
|
||||
svg
|
||||
.insert('desc', ':first-child')
|
||||
.attr('id', 'chart-desc-' + id)
|
||||
.text(description);
|
||||
svg
|
||||
.insert('title', ':first-child')
|
||||
.attr('id', 'chart-title-' + id)
|
||||
.text(title_string);
|
||||
}
|
227
packages/mermaid/src/accessibility.spec.ts
Normal file
227
packages/mermaid/src/accessibility.spec.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { MockedD3 } from './tests/MockedD3.js';
|
||||
import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js';
|
||||
import { D3Element } from './mermaidAPI.js';
|
||||
|
||||
describe('accessibility', () => {
|
||||
const fauxSvgNode = new MockedD3();
|
||||
|
||||
describe('setA11yDiagramInfo', () => {
|
||||
it('sets the svg element role to "graphics-document document"', () => {
|
||||
// @ts-ignore Required to easily handle the d3 select types
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
setA11yDiagramInfo(fauxSvgNode, 'flowchart');
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('role', 'graphics-document document');
|
||||
});
|
||||
|
||||
it('sets the aria-roledescription to the diagram type', () => {
|
||||
// @ts-ignore Required to easily handle the d3 select types
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
setA11yDiagramInfo(fauxSvgNode, 'flowchart');
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('aria-roledescription', 'flowchart');
|
||||
});
|
||||
|
||||
it('does not set the aria-roledescription if the diagram type is empty', () => {
|
||||
// @ts-ignore Required to easily handle the d3 select types
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
setA11yDiagramInfo(fauxSvgNode, '');
|
||||
expect(svgAttrSpy).toHaveBeenCalledTimes(1);
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('role', expect.anything()); // only called to set the role
|
||||
});
|
||||
});
|
||||
|
||||
describe('addSVGa11yTitleDescription', () => {
|
||||
const givenId = 'theBaseId';
|
||||
|
||||
describe('with the given svg d3 object:', () => {
|
||||
it('does nothing if there is no insert defined', () => {
|
||||
const noInsertSvg = {
|
||||
attr: vi.fn(),
|
||||
};
|
||||
const noInsertAttrSpy = vi.spyOn(noInsertSvg, 'attr').mockReturnValue(noInsertSvg);
|
||||
addSVGa11yTitleDescription(noInsertSvg, 'some title', 'some desc', givenId);
|
||||
expect(noInsertAttrSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// ----------------
|
||||
// Convenience functions to DRY up the spec
|
||||
|
||||
function expectAriaLabelledByIsTitleId(
|
||||
svgD3Node: D3Element,
|
||||
title: string | null | undefined,
|
||||
desc: string | null | undefined,
|
||||
givenId: string
|
||||
) {
|
||||
// @ts-ignore Required to easily handle the d3 select types
|
||||
const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node);
|
||||
addSVGa11yTitleDescription(svgD3Node, title, desc, givenId);
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('aria-labelledby', `chart-title-${givenId}`);
|
||||
}
|
||||
|
||||
function expectAriaDescribedByIsDescId(
|
||||
svgD3Node: D3Element,
|
||||
title: string | null | undefined,
|
||||
desc: string | null | undefined,
|
||||
givenId: string
|
||||
) {
|
||||
// @ts-ignore Required to easily handle the d3 select types
|
||||
const svgAttrSpy = vi.spyOn(svgD3Node, 'attr').mockReturnValue(svgD3Node);
|
||||
addSVGa11yTitleDescription(svgD3Node, title, desc, givenId);
|
||||
expect(svgAttrSpy).toHaveBeenCalledWith('aria-describedby', `chart-desc-${givenId}`);
|
||||
}
|
||||
|
||||
function a11yTitleTagInserted(
|
||||
svgD3Node: D3Element,
|
||||
title: string | null | undefined,
|
||||
desc: string | null | undefined,
|
||||
givenId: string,
|
||||
callNumber: number
|
||||
) {
|
||||
a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'title', title);
|
||||
}
|
||||
|
||||
function a11yDescTagInserted(
|
||||
svgD3Node: D3Element,
|
||||
title: string | null | undefined,
|
||||
desc: string | null | undefined,
|
||||
givenId: string,
|
||||
callNumber: number
|
||||
) {
|
||||
a11yTagInserted(svgD3Node, title, desc, givenId, callNumber, 'desc', desc);
|
||||
}
|
||||
|
||||
function a11yTagInserted(
|
||||
svgD3Node: D3Element,
|
||||
title: string | null | undefined,
|
||||
desc: string | null | undefined,
|
||||
givenId: string,
|
||||
callNumber: number,
|
||||
expectedPrefix: string,
|
||||
expectedText: string | null | undefined
|
||||
) {
|
||||
const fauxInsertedD3 = new MockedD3();
|
||||
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxInsertedD3);
|
||||
// @ts-ignore Required to easily handle the d3 select types
|
||||
const titleAttrSpy = vi.spyOn(fauxInsertedD3, 'attr').mockReturnValue(fauxInsertedD3);
|
||||
const titleTextSpy = vi.spyOn(fauxInsertedD3, 'text');
|
||||
|
||||
addSVGa11yTitleDescription(fauxSvgNode, title, desc, givenId);
|
||||
expect(svgInsertSpy).toHaveBeenCalledWith(expectedPrefix, ':first-child');
|
||||
expect(titleAttrSpy).toHaveBeenCalledWith('id', `chart-${expectedPrefix}-${givenId}`);
|
||||
expect(titleTextSpy).toHaveBeenNthCalledWith(callNumber, expectedText);
|
||||
}
|
||||
// ----------------
|
||||
|
||||
describe('given an a11y title', () => {
|
||||
const a11yTitle = 'a11y title';
|
||||
|
||||
describe('given an a11y description', () => {
|
||||
const a11yDesc = 'a11y description';
|
||||
|
||||
it('sets aria-labelledby to the title id inserted as a child', () => {
|
||||
expectAriaLabelledByIsTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
});
|
||||
|
||||
it('sets aria-describedby to the description id inserted as a child', () => {
|
||||
expectAriaDescribedByIsDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
});
|
||||
|
||||
it('inserts a title tag as the first child with the text set to the accTitle given', () => {
|
||||
a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 2);
|
||||
});
|
||||
|
||||
it('inserts a desc tag as the 2nd child with the text set to accDescription given', () => {
|
||||
a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`no a11y description`, () => {
|
||||
const a11yDesc = undefined;
|
||||
|
||||
it('sets aria-labelledby to the title id inserted as a child', () => {
|
||||
expectAriaLabelledByIsTitleId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
});
|
||||
|
||||
it('no aria-describedby is set', () => {
|
||||
// @ts-ignore Required to easily handle the d3 select types
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything());
|
||||
});
|
||||
|
||||
it('inserts a title tag as the first child with the text set to the accTitle given', () => {
|
||||
a11yTitleTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
|
||||
});
|
||||
|
||||
it('no description tag is inserted', () => {
|
||||
const fauxTitle = new MockedD3();
|
||||
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgInsertSpy).not.toHaveBeenCalledWith('desc', ':first-child');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('no a11y title', () => {
|
||||
const a11yTitle = undefined;
|
||||
|
||||
describe('given an a11y description', () => {
|
||||
const a11yDesc = 'a11y description';
|
||||
|
||||
it('no aria-labelledby is set', () => {
|
||||
// @ts-ignore Required to easily handle the d3 select types
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything());
|
||||
});
|
||||
|
||||
it('no title tag inserted', () => {
|
||||
const fauxTitle = new MockedD3();
|
||||
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgInsertSpy).not.toHaveBeenCalledWith('title', ':first-child');
|
||||
});
|
||||
|
||||
it('sets aria-describedby to the description id inserted as a child', () => {
|
||||
expectAriaDescribedByIsDescId(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
});
|
||||
|
||||
it('inserts a desc tag as the 2nd child with the text set to accDescription given', () => {
|
||||
a11yDescTagInserted(fauxSvgNode, a11yTitle, a11yDesc, givenId, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no a11y description', () => {
|
||||
const a11yDesc = undefined;
|
||||
|
||||
it('no aria-labelledby is set', () => {
|
||||
// @ts-ignore Required to easily handle the d3 select types
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-labelledby', expect.anything());
|
||||
});
|
||||
|
||||
it('no aria-describedby is set', () => {
|
||||
// @ts-ignore Required to easily handle the d3 select types
|
||||
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgAttrSpy).not.toHaveBeenCalledWith('aria-describedby', expect.anything());
|
||||
});
|
||||
|
||||
it('no title tag inserted', () => {
|
||||
const fauxTitle = new MockedD3();
|
||||
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxTitle);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgInsertSpy).not.toHaveBeenCalledWith('title', ':first-child');
|
||||
});
|
||||
|
||||
it('no description tag inserted', () => {
|
||||
const fauxDesc = new MockedD3();
|
||||
const svgInsertSpy = vi.spyOn(fauxSvgNode, 'insert').mockReturnValue(fauxDesc);
|
||||
addSVGa11yTitleDescription(fauxSvgNode, a11yTitle, a11yDesc, givenId);
|
||||
expect(svgInsertSpy).not.toHaveBeenCalledWith('desc', ':first-child');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
70
packages/mermaid/src/accessibility.ts
Normal file
70
packages/mermaid/src/accessibility.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Accessibility (a11y) functions, types, helpers
|
||||
* @see https://www.w3.org/WAI/
|
||||
* @see https://www.w3.org/TR/wai-aria-1.1/
|
||||
* @see https://www.w3.org/TR/svg-aam-1.0/
|
||||
*
|
||||
*/
|
||||
import { D3Element } from './mermaidAPI.js';
|
||||
|
||||
import isEmpty from 'lodash-es/isEmpty.js';
|
||||
|
||||
/**
|
||||
* SVG element role:
|
||||
* The SVG element role _should_ be set to 'graphics-document' per SVG standard
|
||||
* but in practice is not always done by browsers, etc. (As of 2022-12-08).
|
||||
* A fallback role of 'document' should be set for those browsers, etc., that only support ARIA 1.0.
|
||||
*
|
||||
* @see https://www.w3.org/TR/svg-aam-1.0/#roleMappingGeneralRules
|
||||
* @see https://www.w3.org/TR/graphics-aria-1.0/#graphics-document
|
||||
*/
|
||||
const SVG_ROLE = 'graphics-document document';
|
||||
|
||||
/**
|
||||
* Add role and aria-roledescription to the svg element
|
||||
*
|
||||
* @param svg - d3 object that contains the SVG HTML element
|
||||
* @param diagramType - diagram name for to the aria-roledescription
|
||||
*/
|
||||
export function setA11yDiagramInfo(svg: D3Element, diagramType: string | null | undefined) {
|
||||
svg.attr('role', SVG_ROLE);
|
||||
if (!isEmpty(diagramType)) {
|
||||
svg.attr('aria-roledescription', diagramType);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Add an accessible title and/or description element to a chart.
|
||||
* The title is usually not displayed and the description is never displayed.
|
||||
*
|
||||
* The following charts display their title as a visual and accessibility element: gantt
|
||||
*
|
||||
* @param svg - d3 node to insert the a11y title and desc info
|
||||
* @param a11yTitle - a11y title. null and undefined are meaningful: means to skip it
|
||||
* @param a11yDesc - a11y description. null and undefined are meaningful: means to skip it
|
||||
* @param baseId - id used to construct the a11y title and description id
|
||||
*/
|
||||
export function addSVGa11yTitleDescription(
|
||||
svg: D3Element,
|
||||
a11yTitle: string | null | undefined,
|
||||
a11yDesc: string | null | undefined,
|
||||
baseId: string
|
||||
) {
|
||||
if (svg.insert === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (a11yTitle || a11yDesc) {
|
||||
if (a11yDesc) {
|
||||
const descId = 'chart-desc-' + baseId;
|
||||
svg.attr('aria-describedby', descId);
|
||||
svg.insert('desc', ':first-child').attr('id', descId).text(a11yDesc);
|
||||
}
|
||||
if (a11yTitle) {
|
||||
const titleId = 'chart-title-' + baseId;
|
||||
svg.attr('aria-labelledby', titleId);
|
||||
svg.insert('title', ':first-child').attr('id', titleId).text(a11yTitle);
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
@@ -32,20 +32,20 @@ const assignWithDepth = function (dst, src, config) {
|
||||
return dst;
|
||||
} else if (Array.isArray(src) && Array.isArray(dst)) {
|
||||
src.forEach((s) => {
|
||||
if (dst.indexOf(s) === -1) {
|
||||
if (!dst.includes(s)) {
|
||||
dst.push(s);
|
||||
}
|
||||
});
|
||||
return dst;
|
||||
}
|
||||
if (typeof dst === 'undefined' || depth <= 0) {
|
||||
if (dst === undefined || depth <= 0) {
|
||||
if (dst !== undefined && dst !== null && typeof dst === 'object' && typeof src === 'object') {
|
||||
return Object.assign(dst, src);
|
||||
} else {
|
||||
return src;
|
||||
}
|
||||
}
|
||||
if (typeof src !== 'undefined' && typeof dst === 'object' && typeof src === 'object') {
|
||||
if (src !== undefined && typeof dst === 'object' && typeof src === 'object') {
|
||||
Object.keys(src).forEach((key) => {
|
||||
if (
|
||||
typeof src[key] === 'object' &&
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { sanitizeText as _sanitizeText } from './diagrams/common/common';
|
||||
import { getConfig } from './config';
|
||||
import { sanitizeText as _sanitizeText } from './diagrams/common/common.js';
|
||||
import { getConfig } from './config.js';
|
||||
let title = '';
|
||||
let diagramTitle = '';
|
||||
let description = '';
|
||||
|
||||
const sanitizeText = (txt: string): string => _sanitizeText(txt, getConfig());
|
||||
|
||||
export const clear = function (): void {
|
||||
@@ -36,10 +37,10 @@ export const getDiagramTitle = function (): string {
|
||||
};
|
||||
|
||||
export default {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setAccTitle,
|
||||
getDiagramTitle,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle: getDiagramTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
clear,
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import * as configApi from './config';
|
||||
import * as configApi from './config.js';
|
||||
|
||||
describe('when working with site config', function () {
|
||||
beforeEach(() => {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import assignWithDepth from './assignWithDepth';
|
||||
import { log } from './logger';
|
||||
import theme from './themes';
|
||||
import config from './defaultConfig';
|
||||
import type { MermaidConfig } from './config.type';
|
||||
import assignWithDepth from './assignWithDepth.js';
|
||||
import { log } from './logger.js';
|
||||
import theme from './themes/index.js';
|
||||
import config from './defaultConfig.js';
|
||||
import type { MermaidConfig } from './config.type.js';
|
||||
|
||||
export const defaultConfig: MermaidConfig = Object.freeze(config);
|
||||
|
||||
@@ -18,8 +18,7 @@ export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: any[])
|
||||
|
||||
// Join directives
|
||||
let sumOfDirectives: MermaidConfig = {};
|
||||
for (let i = 0; i < _directives.length; i++) {
|
||||
const d = _directives[i];
|
||||
for (const d of _directives) {
|
||||
sanitize(d);
|
||||
|
||||
// Apply the data from the directive where the the overrides the themeVariables
|
||||
@@ -153,7 +152,7 @@ export const getConfig = (): MermaidConfig => {
|
||||
export const sanitize = (options: any) => {
|
||||
// Checking that options are not in the list of excluded options
|
||||
['secure', ...(siteConfig.secure ?? [])].forEach((key) => {
|
||||
if (typeof options[key] !== 'undefined') {
|
||||
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]);
|
||||
@@ -170,14 +169,13 @@ export const sanitize = (options: any) => {
|
||||
// Check that there no attempts of xss, there should be no tags at all in the directive
|
||||
// blocking data urls as base64 urls can contain svg's with inline script tags
|
||||
Object.keys(options).forEach((key) => {
|
||||
if (typeof options[key] === 'string') {
|
||||
if (
|
||||
options[key].indexOf('<') > -1 ||
|
||||
options[key].indexOf('>') > -1 ||
|
||||
options[key].indexOf('url(data:') > -1
|
||||
) {
|
||||
delete options[key];
|
||||
}
|
||||
if (
|
||||
typeof options[key] === 'string' &&
|
||||
(options[key].includes('<') ||
|
||||
options[key].includes('>') ||
|
||||
options[key].includes('url(data:'))
|
||||
) {
|
||||
delete options[key];
|
||||
}
|
||||
if (typeof options[key] === 'object') {
|
||||
sanitize(options[key]);
|
||||
@@ -245,6 +243,7 @@ const checkConfig = (config: MermaidConfig) => {
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
// @ts-expect-error Properties were removed in v10. Warning should exist.
|
||||
if (config.lazyLoadedDiagrams || config.loadExternalDiagramsAtStartup) {
|
||||
issueWarning('LAZY_LOAD_DEPRECATED');
|
||||
}
|
||||
|
@@ -3,10 +3,6 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
export interface MermaidConfig {
|
||||
/** @deprecated use mermaid.registerLazyDiagrams instead */
|
||||
lazyLoadedDiagrams?: string[];
|
||||
/** @deprecated use mermaid.registerLazyDiagrams instead */
|
||||
loadExternalDiagramsAtStartup?: boolean;
|
||||
theme?: string;
|
||||
themeVariables?: any;
|
||||
themeCSS?: string;
|
||||
@@ -26,10 +22,12 @@ export interface MermaidConfig {
|
||||
sequence?: SequenceDiagramConfig;
|
||||
gantt?: GanttDiagramConfig;
|
||||
journey?: JourneyDiagramConfig;
|
||||
timeline?: TimelineDiagramConfig;
|
||||
class?: ClassDiagramConfig;
|
||||
state?: StateDiagramConfig;
|
||||
er?: ErDiagramConfig;
|
||||
pie?: PieDiagramConfig;
|
||||
quadrantChart?: QuadrantChartConfig;
|
||||
requirement?: RequirementDiagramConfig;
|
||||
mindmap?: MindmapDiagramConfig;
|
||||
gitGraph?: GitGraphDiagramConfig;
|
||||
@@ -225,7 +223,30 @@ export interface MindmapDiagramConfig extends BaseDiagramConfig {
|
||||
maxNodeWidth: number;
|
||||
}
|
||||
|
||||
export type PieDiagramConfig = BaseDiagramConfig;
|
||||
export interface PieDiagramConfig extends BaseDiagramConfig {
|
||||
textPosition?: number;
|
||||
}
|
||||
|
||||
export interface QuadrantChartConfig extends BaseDiagramConfig {
|
||||
chartWidth: number;
|
||||
chartHeight: number;
|
||||
titleFontSize: number;
|
||||
titlePadding: number;
|
||||
quadrantPadding: number;
|
||||
xAxisLabelPadding: number;
|
||||
yAxisLabelPadding: number;
|
||||
xAxisLabelFontSize: number;
|
||||
yAxisLabelFontSize: number;
|
||||
quadrantLabelFontSize: number;
|
||||
quadrantTextTopPadding: number;
|
||||
pointTextPadding: number;
|
||||
pointLabelFontSize: number;
|
||||
pointRadius: number;
|
||||
xAxisPosition: 'top' | 'bottom';
|
||||
yAxisPosition: 'left' | 'right';
|
||||
quadrantInternalBorderStrokeWidth: number;
|
||||
quadrantExternalBorderStrokeWidth: number;
|
||||
}
|
||||
|
||||
export interface ErDiagramConfig extends BaseDiagramConfig {
|
||||
titleTopMargin?: number;
|
||||
@@ -267,6 +288,10 @@ export interface ClassDiagramConfig extends BaseDiagramConfig {
|
||||
padding?: number;
|
||||
textHeight?: number;
|
||||
defaultRenderer?: string;
|
||||
nodeSpacing?: number;
|
||||
rankSpacing?: number;
|
||||
diagramPadding?: number;
|
||||
htmlLabels?: boolean;
|
||||
}
|
||||
|
||||
export interface JourneyDiagramConfig extends BaseDiagramConfig {
|
||||
@@ -292,6 +317,32 @@ export interface JourneyDiagramConfig extends BaseDiagramConfig {
|
||||
sectionColours?: string[];
|
||||
}
|
||||
|
||||
export interface TimelineDiagramConfig extends BaseDiagramConfig {
|
||||
diagramMarginX?: number;
|
||||
diagramMarginY?: number;
|
||||
leftMargin?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
padding?: number;
|
||||
boxMargin?: number;
|
||||
boxTextMargin?: number;
|
||||
noteMargin?: number;
|
||||
messageMargin?: number;
|
||||
messageAlign?: string;
|
||||
bottomMarginAdj?: number;
|
||||
rightAngles?: boolean;
|
||||
taskFontSize?: string | number;
|
||||
taskFontFamily?: string;
|
||||
taskMargin?: number;
|
||||
activationWidth?: number;
|
||||
textPlacement?: string;
|
||||
actorColours?: string[];
|
||||
sectionFills?: string[];
|
||||
sectionColours?: string[];
|
||||
disableMulticolor?: boolean;
|
||||
useMaxWidth?: boolean;
|
||||
}
|
||||
|
||||
export interface GanttDiagramConfig extends BaseDiagramConfig {
|
||||
titleTopMargin?: number;
|
||||
barHeight?: number;
|
||||
@@ -306,6 +357,7 @@ export interface GanttDiagramConfig extends BaseDiagramConfig {
|
||||
axisFormat?: string;
|
||||
tickInterval?: string;
|
||||
topAxis?: boolean;
|
||||
displayMode?: string;
|
||||
}
|
||||
|
||||
export interface SequenceDiagramConfig extends BaseDiagramConfig {
|
||||
@@ -356,6 +408,7 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig {
|
||||
curve?: string;
|
||||
padding?: number;
|
||||
defaultRenderer?: string;
|
||||
wrappingWidth?: number;
|
||||
}
|
||||
|
||||
export interface FontConfig {
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import intersectRect from './intersect/intersect-rect';
|
||||
import { log } from '../logger';
|
||||
import createLabel from './createLabel';
|
||||
import intersectRect from './intersect/intersect-rect.js';
|
||||
import { log } from '../logger.js';
|
||||
import createLabel from './createLabel.js';
|
||||
import { createText } from '../rendering-util/createText.js';
|
||||
import { select } from 'd3';
|
||||
import { getConfig } from '../config';
|
||||
import { evaluate } from '../diagrams/common/common';
|
||||
import { getConfig } from '../config.js';
|
||||
import { evaluate } from '../diagrams/common/common.js';
|
||||
|
||||
const rect = (parent, node) => {
|
||||
log.trace('Creating subgraph rect for ', node.id, node);
|
||||
log.info('Creating subgraph rect for ', node.id, node);
|
||||
|
||||
// Add outer g element
|
||||
const shapeSvg = parent
|
||||
@@ -17,12 +18,18 @@ const rect = (parent, node) => {
|
||||
// add the rect
|
||||
const rect = shapeSvg.insert('rect', ':first-child');
|
||||
|
||||
const useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
|
||||
|
||||
// Create the label and insert it after the rect
|
||||
const label = shapeSvg.insert('g').attr('class', 'cluster-label');
|
||||
|
||||
const text = label
|
||||
.node()
|
||||
.appendChild(createLabel(node.labelText, node.labelStyle, undefined, true));
|
||||
// const text = label
|
||||
// .node()
|
||||
// .appendChild(createLabel(node.labelText, node.labelStyle, undefined, true));
|
||||
const text =
|
||||
node.labelType === 'markdown'
|
||||
? createText(label, node.labelText, { style: node.labelStyle, useHtmlLabels })
|
||||
: label.node().appendChild(createLabel(node.labelText, node.labelStyle, undefined, true));
|
||||
|
||||
// Get the size of the label
|
||||
let bbox = text.getBBox();
|
||||
@@ -56,15 +63,20 @@ const rect = (parent, node) => {
|
||||
.attr('width', width)
|
||||
.attr('height', node.height + padding);
|
||||
|
||||
if (useHtmlLabels) {
|
||||
label.attr(
|
||||
'transform',
|
||||
// This puts the labal on top of the box instead of inside it
|
||||
'translate(' + (node.x - bbox.width / 2) + ', ' + (node.y - node.height / 2) + ')'
|
||||
);
|
||||
} else {
|
||||
label.attr(
|
||||
'transform',
|
||||
// This puts the labal on top of the box instead of inside it
|
||||
'translate(' + node.x + ', ' + (node.y - node.height / 2) + ')'
|
||||
);
|
||||
}
|
||||
// Center the label
|
||||
label.attr(
|
||||
'transform',
|
||||
'translate(' +
|
||||
(node.x - bbox.width / 2) +
|
||||
', ' +
|
||||
(node.y - node.height / 2 + node.padding / 3) +
|
||||
')'
|
||||
);
|
||||
|
||||
const rectBox = rect.node().getBBox();
|
||||
node.width = rectBox.width;
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { select } from 'd3';
|
||||
import { log } from '../logger';
|
||||
import { getConfig } from '../config';
|
||||
import { evaluate } from '../diagrams/common/common';
|
||||
import { decodeEntities } from '../mermaidAPI';
|
||||
import { log } from '../logger.js';
|
||||
import { getConfig } from '../config.js';
|
||||
import { evaluate } from '../diagrams/common/common.js';
|
||||
import { decodeEntities } from '../mermaidAPI.js';
|
||||
|
||||
/**
|
||||
* @param dom
|
||||
@@ -41,7 +41,13 @@ function addHtmlLabel(node) {
|
||||
div.attr('xmlns', 'http://www.w3.org/1999/xhtml');
|
||||
return fo.node();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param _vertexText
|
||||
* @param style
|
||||
* @param isTitle
|
||||
* @param isNode
|
||||
* @deprecated svg-util/createText instead
|
||||
*/
|
||||
const createLabel = (_vertexText, style, isTitle, isNode) => {
|
||||
let vertexText = _vertexText || '';
|
||||
if (typeof vertexText === 'object') {
|
||||
@@ -54,7 +60,7 @@ const createLabel = (_vertexText, style, isTitle, isNode) => {
|
||||
const node = {
|
||||
isNode,
|
||||
label: decodeEntities(vertexText).replace(
|
||||
/fa[lrsb]?:fa-[\w-]+/g,
|
||||
/fa[blrs]?:fa-[\w-]+/g,
|
||||
(s) => `<i class='${s.replace(':', ' ')}'></i>`
|
||||
),
|
||||
labelStyle: style.replace('fill:', 'color:'),
|
||||
@@ -74,7 +80,7 @@ const createLabel = (_vertexText, style, isTitle, isNode) => {
|
||||
rows = [];
|
||||
}
|
||||
|
||||
for (let j = 0; j < rows.length; j++) {
|
||||
for (const row of rows) {
|
||||
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
||||
tspan.setAttribute('dy', '1em');
|
||||
@@ -84,7 +90,7 @@ const createLabel = (_vertexText, style, isTitle, isNode) => {
|
||||
} else {
|
||||
tspan.setAttribute('class', 'row');
|
||||
}
|
||||
tspan.textContent = rows[j].trim();
|
||||
tspan.textContent = row.trim();
|
||||
svgLabel.appendChild(tspan);
|
||||
}
|
||||
return svgLabel;
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { log } from '../logger';
|
||||
import createLabel from './createLabel';
|
||||
import { log } from '../logger.js';
|
||||
import createLabel from './createLabel.js';
|
||||
import { createText } from '../rendering-util/createText.js';
|
||||
import { line, curveBasis, select } from 'd3';
|
||||
import { getConfig } from '../config';
|
||||
import utils from '../utils';
|
||||
import { evaluate } from '../diagrams/common/common';
|
||||
import { getConfig } from '../config.js';
|
||||
import utils from '../utils.js';
|
||||
import { evaluate } from '../diagrams/common/common.js';
|
||||
|
||||
let edgeLabels = {};
|
||||
let terminalLabels = {};
|
||||
@@ -14,8 +15,17 @@ export const clear = () => {
|
||||
};
|
||||
|
||||
export const insertEdgeLabel = (elem, edge) => {
|
||||
const useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
|
||||
// Create the actual text element
|
||||
const labelElement = createLabel(edge.label, edge.labelStyle);
|
||||
const labelElement =
|
||||
edge.labelType === 'markdown'
|
||||
? createText(elem, edge.label, {
|
||||
style: edge.labelStyle,
|
||||
useHtmlLabels,
|
||||
addSvgBackground: true,
|
||||
})
|
||||
: createLabel(edge.label, edge.labelStyle);
|
||||
log.info('abc82', edge, edge.labelType);
|
||||
|
||||
// Create outer g, edgeLabel, this will be positioned after graph layout
|
||||
const edgeLabel = elem.insert('g').attr('class', 'edgeLabel');
|
||||
@@ -26,7 +36,7 @@ export const insertEdgeLabel = (elem, edge) => {
|
||||
|
||||
// Center the label
|
||||
let bbox = labelElement.getBBox();
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
if (useHtmlLabels) {
|
||||
const div = labelElement.children[0];
|
||||
const dv = select(labelElement);
|
||||
bbox = div.getBoundingClientRect();
|
||||
@@ -107,6 +117,7 @@ export const insertEdgeLabel = (elem, edge) => {
|
||||
terminalLabels[edge.id].endRight = endEdgeLabelRight;
|
||||
setTerminalWidth(fo, edge.endLabelRight);
|
||||
}
|
||||
return labelElement;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -130,9 +141,21 @@ export const positionEdgeLabel = (edge, paths) => {
|
||||
if (path) {
|
||||
// // debugger;
|
||||
const pos = utils.calcLabelPosition(path);
|
||||
log.info('Moving label from (', x, ',', y, ') to (', pos.x, ',', pos.y, ') abc78');
|
||||
// x = pos.x;
|
||||
// y = pos.y;
|
||||
log.info(
|
||||
'Moving label ' + edge.label + ' from (',
|
||||
x,
|
||||
',',
|
||||
y,
|
||||
') to (',
|
||||
pos.x,
|
||||
',',
|
||||
pos.y,
|
||||
') abc78'
|
||||
);
|
||||
if (paths.updatedPath) {
|
||||
x = pos.x;
|
||||
y = pos.y;
|
||||
}
|
||||
}
|
||||
el.attr('transform', 'translate(' + x + ', ' + y + ')');
|
||||
}
|
||||
@@ -324,7 +347,7 @@ const cutPathAtIntersect = (_points, boundryNode) => {
|
||||
pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y);
|
||||
});
|
||||
// // if (!pointPresent) {
|
||||
if (!points.find((e) => e.x === inter.x && e.y === inter.y)) {
|
||||
if (!points.some((e) => e.x === inter.x && e.y === inter.y)) {
|
||||
points.push(inter);
|
||||
} else {
|
||||
log.warn('abc88 no intersect', inter, points);
|
||||
@@ -440,6 +463,9 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph
|
||||
case 'thick':
|
||||
strokeClasses = 'edge-thickness-thick';
|
||||
break;
|
||||
case 'invisible':
|
||||
strokeClasses = 'edge-thickness-thick';
|
||||
break;
|
||||
default:
|
||||
strokeClasses = '';
|
||||
}
|
||||
@@ -463,7 +489,7 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph
|
||||
.attr('style', edge.style);
|
||||
|
||||
// DEBUG code, adds a red circle at each edge coordinate
|
||||
// edge.points.forEach(point => {
|
||||
// edge.points.forEach((point) => {
|
||||
// elem
|
||||
// .append('circle')
|
||||
// .style('stroke', 'red')
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { intersection } from './edges';
|
||||
import { setLogLevel } from '../logger';
|
||||
import { intersection } from './edges.js';
|
||||
import { setLogLevel } from '../logger.js';
|
||||
|
||||
describe('Graphlib decorations', () => {
|
||||
let node;
|
||||
|
@@ -1,20 +1,20 @@
|
||||
import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js';
|
||||
import * as graphlibJson from 'dagre-d3-es/src/graphlib/json';
|
||||
import insertMarkers from './markers';
|
||||
import { updateNodeBounds } from './shapes/util';
|
||||
import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js';
|
||||
import insertMarkers from './markers.js';
|
||||
import { updateNodeBounds } from './shapes/util.js';
|
||||
import {
|
||||
clear as clearGraphlib,
|
||||
clusterDb,
|
||||
adjustClustersAndEdges,
|
||||
findNonClusterChild,
|
||||
sortNodesByHierarchy,
|
||||
} from './mermaid-graphlib';
|
||||
import { insertNode, positionNode, clear as clearNodes, setNodeElem } from './nodes';
|
||||
import { insertCluster, clear as clearClusters } from './clusters';
|
||||
import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges';
|
||||
import { log } from '../logger';
|
||||
} from './mermaid-graphlib.js';
|
||||
import { insertNode, positionNode, clear as clearNodes, setNodeElem } from './nodes.js';
|
||||
import { insertCluster, clear as clearClusters } from './clusters.js';
|
||||
import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges.js';
|
||||
import { log } from '../logger.js';
|
||||
|
||||
const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
|
||||
const recursiveRender = async (_elem, graph, diagramtype, parentCluster) => {
|
||||
log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster);
|
||||
const dir = graph.graph().rankdir;
|
||||
log.trace('Dir in recursive render - dir:', dir);
|
||||
@@ -35,44 +35,46 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
|
||||
|
||||
// Insert nodes, this will insert them into the dom and each node will get a size. The size is updated
|
||||
// to the abstract node and is later used by dagre for the layout
|
||||
graph.nodes().forEach(function (v) {
|
||||
const node = graph.node(v);
|
||||
if (typeof parentCluster !== 'undefined') {
|
||||
const data = JSON.parse(JSON.stringify(parentCluster.clusterData));
|
||||
// data.clusterPositioning = true;
|
||||
log.info('Setting data for cluster XXX (', v, ') ', data, parentCluster);
|
||||
graph.setNode(parentCluster.id, data);
|
||||
if (!graph.parent(v)) {
|
||||
log.trace('Setting parent', v, parentCluster.id);
|
||||
graph.setParent(v, parentCluster.id, data);
|
||||
await Promise.all(
|
||||
graph.nodes().map(async function (v) {
|
||||
const node = graph.node(v);
|
||||
if (parentCluster !== undefined) {
|
||||
const data = JSON.parse(JSON.stringify(parentCluster.clusterData));
|
||||
// data.clusterPositioning = true;
|
||||
log.info('Setting data for cluster XXX (', v, ') ', data, parentCluster);
|
||||
graph.setNode(parentCluster.id, data);
|
||||
if (!graph.parent(v)) {
|
||||
log.trace('Setting parent', v, parentCluster.id);
|
||||
graph.setParent(v, parentCluster.id, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info('(Insert) Node XXX' + v + ': ' + JSON.stringify(graph.node(v)));
|
||||
if (node && node.clusterNode) {
|
||||
// const children = graph.children(v);
|
||||
log.info('Cluster identified', v, node.width, graph.node(v));
|
||||
const o = recursiveRender(nodes, node.graph, diagramtype, graph.node(v));
|
||||
const newEl = o.elem;
|
||||
updateNodeBounds(node, newEl);
|
||||
node.diff = o.diff || 0;
|
||||
log.info('Node bounds (abc123)', v, node, node.width, node.x, node.y);
|
||||
setNodeElem(newEl, node);
|
||||
log.info('(Insert) Node XXX' + v + ': ' + JSON.stringify(graph.node(v)));
|
||||
if (node && node.clusterNode) {
|
||||
// const children = graph.children(v);
|
||||
log.info('Cluster identified', v, node.width, graph.node(v));
|
||||
const o = await recursiveRender(nodes, node.graph, diagramtype, graph.node(v));
|
||||
const newEl = o.elem;
|
||||
updateNodeBounds(node, newEl);
|
||||
node.diff = o.diff || 0;
|
||||
log.info('Node bounds (abc123)', v, node, node.width, node.x, node.y);
|
||||
setNodeElem(newEl, node);
|
||||
|
||||
log.warn('Recursive render complete ', newEl, node);
|
||||
} else {
|
||||
if (graph.children(v).length > 0) {
|
||||
// This is a cluster but not to be rendered recursively
|
||||
// Render as before
|
||||
log.info('Cluster - the non recursive path XXX', v, node.id, node, graph);
|
||||
log.info(findNonClusterChild(node.id, graph));
|
||||
clusterDb[node.id] = { id: findNonClusterChild(node.id, graph), node };
|
||||
// insertCluster(clusters, graph.node(v));
|
||||
log.warn('Recursive render complete ', newEl, node);
|
||||
} else {
|
||||
log.info('Node - the non recursive path', v, node.id, node);
|
||||
insertNode(nodes, graph.node(v), dir);
|
||||
if (graph.children(v).length > 0) {
|
||||
// This is a cluster but not to be rendered recursively
|
||||
// Render as before
|
||||
log.info('Cluster - the non recursive path XXX', v, node.id, node, graph);
|
||||
log.info(findNonClusterChild(node.id, graph));
|
||||
clusterDb[node.id] = { id: findNonClusterChild(node.id, graph), node };
|
||||
// insertCluster(clusters, graph.node(v));
|
||||
} else {
|
||||
log.info('Node - the non recursive path', v, node.id, node);
|
||||
await insertNode(nodes, graph.node(v), dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Insert labels, this will insert them into the dom so that the width can be calculated
|
||||
// Also figure out which edges point to/from clusters and adjust them accordingly
|
||||
@@ -146,7 +148,7 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
|
||||
return { elem, diff };
|
||||
};
|
||||
|
||||
export const render = (elem, graph, markers, diagramtype, id) => {
|
||||
export const render = async (elem, graph, markers, diagramtype, id) => {
|
||||
insertMarkers(elem, markers, diagramtype, id);
|
||||
clearNodes();
|
||||
clearEdges();
|
||||
@@ -157,7 +159,7 @@ export const render = (elem, graph, markers, diagramtype, id) => {
|
||||
adjustClustersAndEdges(graph);
|
||||
log.warn('Graph after:', graphlibJson.write(graph));
|
||||
// log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph));
|
||||
recursiveRender(elem, graph, diagramtype);
|
||||
await recursiveRender(elem, graph, diagramtype);
|
||||
};
|
||||
|
||||
// const shapeDefinitions = {};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import intersectEllipse from './intersect-ellipse';
|
||||
import intersectEllipse from './intersect-ellipse.js';
|
||||
|
||||
/**
|
||||
* @param node
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/* eslint "no-console": off */
|
||||
|
||||
import intersectLine from './intersect-line';
|
||||
import intersectLine from './intersect-line.js';
|
||||
|
||||
export default intersectPolygon;
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/** Setup arrow head and define the marker. The result is appended to the svg. */
|
||||
|
||||
import { log } from '../logger';
|
||||
import { log } from '../logger.js';
|
||||
|
||||
// Only add the number of markers that the diagram needs
|
||||
const insertMarkers = (elem, markerArray, type, id) => {
|
||||
|
@@ -1,30 +1,23 @@
|
||||
/** Decorates with functions required by mermaids dagre-wrapper. */
|
||||
import { log } from '../logger';
|
||||
import * as graphlibJson from 'dagre-d3-es/src/graphlib/json';
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib';
|
||||
import { log } from '../logger.js';
|
||||
import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js';
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
|
||||
export let clusterDb = {};
|
||||
let decendants = {};
|
||||
let descendants = {};
|
||||
let parents = {};
|
||||
|
||||
export const clear = () => {
|
||||
decendants = {};
|
||||
descendants = {};
|
||||
parents = {};
|
||||
clusterDb = {};
|
||||
};
|
||||
|
||||
const isDecendant = (id, ancenstorId) => {
|
||||
const isDescendant = (id, ancenstorId) => {
|
||||
// if (id === ancenstorId) return true;
|
||||
|
||||
log.trace(
|
||||
'In isDecendant',
|
||||
ancenstorId,
|
||||
' ',
|
||||
id,
|
||||
' = ',
|
||||
decendants[ancenstorId].indexOf(id) >= 0
|
||||
);
|
||||
if (decendants[ancenstorId].indexOf(id) >= 0) {
|
||||
log.trace('In isDecendant', ancenstorId, ' ', id, ' = ', descendants[ancenstorId].includes(id));
|
||||
if (descendants[ancenstorId].includes(id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -32,7 +25,7 @@ const isDecendant = (id, ancenstorId) => {
|
||||
};
|
||||
|
||||
const edgeInCluster = (edge, clusterId) => {
|
||||
log.info('Decendants of ', clusterId, ' is ', decendants[clusterId]);
|
||||
log.info('Decendants of ', clusterId, ' is ', descendants[clusterId]);
|
||||
log.info('Edge is ', edge);
|
||||
// Edges to/from the cluster is not in the cluster, they are in the parent
|
||||
if (edge.v === clusterId) {
|
||||
@@ -42,15 +35,15 @@ const edgeInCluster = (edge, clusterId) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!decendants[clusterId]) {
|
||||
if (!descendants[clusterId]) {
|
||||
log.debug('Tilt, ', clusterId, ',not in decendants');
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
decendants[clusterId].indexOf(edge.v) >= 0 ||
|
||||
isDecendant(edge.v, clusterId) ||
|
||||
isDecendant(edge.w, clusterId) ||
|
||||
decendants[clusterId].indexOf(edge.w) >= 0
|
||||
descendants[clusterId].includes(edge.v) ||
|
||||
isDescendant(edge.v, clusterId) ||
|
||||
isDescendant(edge.w, clusterId) ||
|
||||
descendants[clusterId].includes(edge.w)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -132,14 +125,14 @@ const copy = (clusterId, graph, newGraph, rootId) => {
|
||||
graph.removeNode(node);
|
||||
});
|
||||
};
|
||||
export const extractDecendants = (id, graph) => {
|
||||
export const extractDescendants = (id, graph) => {
|
||||
// log.debug('Extracting ', id);
|
||||
const children = graph.children(id);
|
||||
let res = [].concat(children);
|
||||
let res = [...children];
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
parents[children[i]] = id;
|
||||
res = res.concat(extractDecendants(children[i], graph));
|
||||
for (const child of children) {
|
||||
parents[child] = id;
|
||||
res = [...res, ...extractDescendants(child, graph)];
|
||||
}
|
||||
|
||||
return res;
|
||||
@@ -154,13 +147,13 @@ export const extractDecendants = (id, graph) => {
|
||||
export const validate = (graph) => {
|
||||
const edges = graph.edges();
|
||||
log.trace('Edges: ', edges);
|
||||
for (let i = 0; i < edges.length; i++) {
|
||||
if (graph.children(edges[i].v).length > 0) {
|
||||
log.trace('The node ', edges[i].v, ' is part of and edge even though it has children');
|
||||
for (const edge of edges) {
|
||||
if (graph.children(edge.v).length > 0) {
|
||||
log.trace('The node ', edge.v, ' is part of and edge even though it has children');
|
||||
return false;
|
||||
}
|
||||
if (graph.children(edges[i].w).length > 0) {
|
||||
log.trace('The node ', edges[i].w, ' is part of and edge even though it has children');
|
||||
if (graph.children(edge.w).length > 0) {
|
||||
log.trace('The node ', edge.w, ' is part of and edge even though it has children');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -183,8 +176,8 @@ export const findNonClusterChild = (id, graph) => {
|
||||
log.trace('This is a valid node', id);
|
||||
return id;
|
||||
}
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const _id = findNonClusterChild(children[i], graph);
|
||||
for (const child of children) {
|
||||
const _id = findNonClusterChild(child, graph);
|
||||
if (_id) {
|
||||
log.trace('Found replacement for', id, ' => ', _id);
|
||||
return _id;
|
||||
@@ -226,7 +219,7 @@ export const adjustClustersAndEdges = (graph, depth) => {
|
||||
' Replacement id in edges: ',
|
||||
findNonClusterChild(id, graph)
|
||||
);
|
||||
decendants[id] = extractDecendants(id, graph);
|
||||
descendants[id] = extractDescendants(id, graph);
|
||||
clusterDb[id] = { id: findNonClusterChild(id, graph), clusterData: graph.node(id) };
|
||||
}
|
||||
});
|
||||
@@ -236,7 +229,7 @@ export const adjustClustersAndEdges = (graph, depth) => {
|
||||
const children = graph.children(id);
|
||||
const edges = graph.edges();
|
||||
if (children.length > 0) {
|
||||
log.debug('Cluster identified', id, decendants);
|
||||
log.debug('Cluster identified', id, descendants);
|
||||
edges.forEach((edge) => {
|
||||
// log.debug('Edge, decendants: ', edge, decendants[id]);
|
||||
|
||||
@@ -245,19 +238,19 @@ export const adjustClustersAndEdges = (graph, depth) => {
|
||||
// Any edge where either the one of the nodes is descending to the cluster but not the other
|
||||
// if (decendants[id].indexOf(edge.v) < 0 && decendants[id].indexOf(edge.w) < 0) {
|
||||
|
||||
const d1 = isDecendant(edge.v, id);
|
||||
const d2 = isDecendant(edge.w, id);
|
||||
const d1 = isDescendant(edge.v, id);
|
||||
const d2 = isDescendant(edge.w, id);
|
||||
|
||||
// d1 xor d2 - if either d1 is true and d2 is false or the other way around
|
||||
if (d1 ^ d2) {
|
||||
log.warn('Edge: ', edge, ' leaves cluster ', id);
|
||||
log.warn('Decendants of XXX ', id, ': ', decendants[id]);
|
||||
log.warn('Decendants of XXX ', id, ': ', descendants[id]);
|
||||
clusterDb[id].externalConnections = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log.debug('Not a cluster ', id, decendants);
|
||||
log.debug('Not a cluster ', id, descendants);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -277,7 +270,7 @@ export const adjustClustersAndEdges = (graph, depth) => {
|
||||
'ids:',
|
||||
e.v,
|
||||
e.w,
|
||||
'Translateing: ',
|
||||
'Translating: ',
|
||||
clusterDb[e.v],
|
||||
' --- ',
|
||||
clusterDb[e.w]
|
||||
@@ -347,8 +340,7 @@ export const extractor = (graph, depth) => {
|
||||
// for (let i = 0;)
|
||||
let nodes = graph.nodes();
|
||||
let hasChildren = false;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
for (const node of nodes) {
|
||||
const children = graph.children(node);
|
||||
hasChildren = hasChildren || children.length > 0;
|
||||
}
|
||||
@@ -360,9 +352,7 @@ export const extractor = (graph, depth) => {
|
||||
// const clusters = Object.keys(clusterDb);
|
||||
// clusters.forEach(clusterId => {
|
||||
log.debug('Nodes = ', nodes, depth);
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
|
||||
for (const node of nodes) {
|
||||
log.debug(
|
||||
'Extracting node',
|
||||
node,
|
||||
@@ -394,11 +384,9 @@ export const extractor = (graph, depth) => {
|
||||
|
||||
const graphSettings = graph.graph();
|
||||
let dir = graphSettings.rankdir === 'TB' ? 'LR' : 'TB';
|
||||
if (clusterDb[node]) {
|
||||
if (clusterDb[node].clusterData && clusterDb[node].clusterData.dir) {
|
||||
dir = clusterDb[node].clusterData.dir;
|
||||
log.warn('Fixing dir', clusterDb[node].clusterData.dir, dir);
|
||||
}
|
||||
if (clusterDb[node] && clusterDb[node].clusterData && clusterDb[node].clusterData.dir) {
|
||||
dir = clusterDb[node].clusterData.dir;
|
||||
log.warn('Fixing dir', clusterDb[node].clusterData.dir, dir);
|
||||
}
|
||||
|
||||
const clusterGraph = new graphlib.Graph({
|
||||
@@ -446,8 +434,7 @@ export const extractor = (graph, depth) => {
|
||||
|
||||
nodes = graph.nodes();
|
||||
log.warn('New list of nodes', nodes);
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
for (const node of nodes) {
|
||||
const data = graph.node(node);
|
||||
log.warn(' Now next level', node, data);
|
||||
if (data.clusterNode) {
|
||||
@@ -464,7 +451,7 @@ const sorter = (graph, nodes) => {
|
||||
nodes.forEach((node) => {
|
||||
const children = graph.children(node);
|
||||
const sorted = sorter(graph, children);
|
||||
result = result.concat(sorted);
|
||||
result = [...result, ...sorted];
|
||||
});
|
||||
|
||||
return result;
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import * as graphlibJson from 'dagre-d3-es/src/graphlib/json';
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib';
|
||||
import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js';
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
import {
|
||||
validate,
|
||||
adjustClustersAndEdges,
|
||||
extractDecendants,
|
||||
extractDescendants,
|
||||
sortNodesByHierarchy,
|
||||
} from './mermaid-graphlib';
|
||||
import { setLogLevel, log } from '../logger';
|
||||
} from './mermaid-graphlib.js';
|
||||
import { setLogLevel, log } from '../logger.js';
|
||||
|
||||
describe('Graphlib decorations', () => {
|
||||
let g;
|
||||
@@ -400,7 +400,7 @@ flowchart TB
|
||||
expect(aGraph.parent('B')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
describe('extractDecendants', function () {
|
||||
describe('extractDescendants', function () {
|
||||
let g;
|
||||
beforeEach(function () {
|
||||
setLogLevel(1);
|
||||
@@ -443,9 +443,9 @@ describe('extractDecendants', function () {
|
||||
g.setEdge('A', 'C', { data: 'link2' }, '2');
|
||||
|
||||
// log.info(g.edges())
|
||||
const d1 = extractDecendants('A', g);
|
||||
const d2 = extractDecendants('B', g);
|
||||
const d3 = extractDecendants('C', g);
|
||||
const d1 = extractDescendants('A', g);
|
||||
const d2 = extractDescendants('B', g);
|
||||
const d3 = extractDescendants('C', g);
|
||||
|
||||
expect(d1).toEqual(['a']);
|
||||
expect(d2).toEqual(['b']);
|
||||
|
@@ -1,15 +1,15 @@
|
||||
import { select } from 'd3';
|
||||
import { log } from '../logger';
|
||||
import { labelHelper, updateNodeBounds, insertPolygonShape } from './shapes/util';
|
||||
import { getConfig } from '../config';
|
||||
import { log } from '../logger.js';
|
||||
import { labelHelper, updateNodeBounds, insertPolygonShape } from './shapes/util.js';
|
||||
import { getConfig } from '../config.js';
|
||||
import intersect from './intersect/index.js';
|
||||
import createLabel from './createLabel';
|
||||
import note from './shapes/note';
|
||||
import { parseMember } from '../diagrams/class/svgDraw';
|
||||
import { evaluate } from '../diagrams/common/common';
|
||||
import createLabel from './createLabel.js';
|
||||
import note from './shapes/note.js';
|
||||
import { parseMember } from '../diagrams/class/svgDraw.js';
|
||||
import { evaluate } from '../diagrams/common/common.js';
|
||||
|
||||
const question = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
|
||||
const question = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -69,8 +69,8 @@ const choice = (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const hexagon = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
|
||||
const hexagon = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
|
||||
const f = 4;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -96,8 +96,8 @@ const hexagon = (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const rect_left_inv_arrow = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
|
||||
const rect_left_inv_arrow = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -122,8 +122,8 @@ const rect_left_inv_arrow = (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const lean_right = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
|
||||
const lean_right = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -145,8 +145,8 @@ const lean_right = (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const lean_left = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
|
||||
const lean_left = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -168,8 +168,8 @@ const lean_left = (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const trapezoid = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
|
||||
const trapezoid = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -191,8 +191,8 @@ const trapezoid = (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const inv_trapezoid = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
|
||||
const inv_trapezoid = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -214,8 +214,8 @@ const inv_trapezoid = (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const rect_right_inv_arrow = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
|
||||
const rect_right_inv_arrow = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -238,8 +238,8 @@ const rect_right_inv_arrow = (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const cylinder = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
|
||||
const cylinder = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const rx = w / 2;
|
||||
@@ -310,13 +310,19 @@ const cylinder = (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const rect = (parent, node) => {
|
||||
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes, true);
|
||||
const rect = async (parent, node) => {
|
||||
const { shapeSvg, bbox, halfPadding } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
'node ' + node.classes,
|
||||
true
|
||||
);
|
||||
|
||||
log.trace('Classes = ', node.classes);
|
||||
// add the rect
|
||||
const rect = shapeSvg.insert('rect', ':first-child');
|
||||
|
||||
// const totalWidth = bbox.width + node.padding * 2;
|
||||
// const totalHeight = bbox.height + node.padding * 2;
|
||||
const totalWidth = bbox.width + node.padding;
|
||||
const totalHeight = bbox.height + node.padding;
|
||||
rect
|
||||
@@ -324,6 +330,8 @@ const rect = (parent, node) => {
|
||||
.attr('style', node.style)
|
||||
.attr('rx', node.rx)
|
||||
.attr('ry', node.ry)
|
||||
// .attr('x', -bbox.width / 2 - node.padding)
|
||||
// .attr('y', -bbox.height / 2 - node.padding)
|
||||
.attr('x', -bbox.width / 2 - halfPadding)
|
||||
.attr('y', -bbox.height / 2 - halfPadding)
|
||||
.attr('width', totalWidth)
|
||||
@@ -349,8 +357,8 @@ const rect = (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const labelRect = (parent, node) => {
|
||||
const { shapeSvg } = labelHelper(parent, node, 'label', true);
|
||||
const labelRect = async (parent, node) => {
|
||||
const { shapeSvg } = await labelHelper(parent, node, 'label', true);
|
||||
|
||||
log.trace('Classes = ', node.classes);
|
||||
// add the rect
|
||||
@@ -391,12 +399,10 @@ const labelRect = (parent, node) => {
|
||||
function applyNodePropertyBorders(rect, borders, totalWidth, totalHeight) {
|
||||
const strokeDashArray = [];
|
||||
const addBorder = (length) => {
|
||||
strokeDashArray.push(length);
|
||||
strokeDashArray.push(0);
|
||||
strokeDashArray.push(length, 0);
|
||||
};
|
||||
const skipBorder = (length) => {
|
||||
strokeDashArray.push(0);
|
||||
strokeDashArray.push(length);
|
||||
strokeDashArray.push(0, length);
|
||||
};
|
||||
if (borders.includes('t')) {
|
||||
log.debug('add top border');
|
||||
@@ -538,8 +544,8 @@ const rectWithTitle = (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const stadium = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
|
||||
const stadium = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
|
||||
const h = bbox.height + node.padding;
|
||||
const w = bbox.width + h / 4 + node.padding;
|
||||
@@ -564,8 +570,8 @@ const stadium = (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const circle = (parent, node) => {
|
||||
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, undefined, true);
|
||||
const circle = async (parent, node) => {
|
||||
const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, undefined, true);
|
||||
const circle = shapeSvg.insert('circle', ':first-child');
|
||||
|
||||
// center the circle around its coordinate
|
||||
@@ -589,8 +595,8 @@ const circle = (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const doublecircle = (parent, node) => {
|
||||
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, undefined, true);
|
||||
const doublecircle = async (parent, node) => {
|
||||
const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, undefined, true);
|
||||
const gap = 5;
|
||||
const circleGroup = shapeSvg.insert('g', ':first-child');
|
||||
const outerCircle = circleGroup.insert('circle');
|
||||
@@ -625,8 +631,8 @@ const doublecircle = (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const subroutine = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
|
||||
const subroutine = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -774,7 +780,7 @@ const class_box = (parent, node) => {
|
||||
maxWidth += interfaceBBox.width;
|
||||
}
|
||||
|
||||
let classTitleString = node.classData.id;
|
||||
let classTitleString = node.classData.label;
|
||||
|
||||
if (node.classData.type !== undefined && node.classData.type !== '') {
|
||||
if (getConfig().flowchart.htmlLabels) {
|
||||
@@ -929,61 +935,6 @@ const class_box = (parent, node) => {
|
||||
);
|
||||
verticalPos += classTitleBBox.height + rowPadding;
|
||||
});
|
||||
//
|
||||
// let bbox;
|
||||
// if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
// const div = interfaceLabel.children[0];
|
||||
// const dv = select(interfaceLabel);
|
||||
// bbox = div.getBoundingClientRect();
|
||||
// dv.attr('width', bbox.width);
|
||||
// dv.attr('height', bbox.height);
|
||||
// }
|
||||
// bbox = labelContainer.getBBox();
|
||||
|
||||
// log.info('Text 2', text2);
|
||||
// const textRows = text2.slice(1, text2.length);
|
||||
// let titleBox = text.getBBox();
|
||||
// const descr = label
|
||||
// .node()
|
||||
// .appendChild(createLabel(textRows.join('<br/>'), node.labelStyle, true, true));
|
||||
|
||||
// if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
// const div = descr.children[0];
|
||||
// const dv = select(descr);
|
||||
// bbox = div.getBoundingClientRect();
|
||||
// dv.attr('width', bbox.width);
|
||||
// dv.attr('height', bbox.height);
|
||||
// }
|
||||
// // bbox = label.getBBox();
|
||||
// // log.info(descr);
|
||||
// select(descr).attr(
|
||||
// 'transform',
|
||||
// 'translate( ' +
|
||||
// // (titleBox.width - bbox.width) / 2 +
|
||||
// (bbox.width > titleBox.width ? 0 : (titleBox.width - bbox.width) / 2) +
|
||||
// ', ' +
|
||||
// (titleBox.height + halfPadding + 5) +
|
||||
// ')'
|
||||
// );
|
||||
// select(text).attr(
|
||||
// 'transform',
|
||||
// 'translate( ' +
|
||||
// // (titleBox.width - bbox.width) / 2 +
|
||||
// (bbox.width < titleBox.width ? 0 : -(titleBox.width - bbox.width) / 2) +
|
||||
// ', ' +
|
||||
// 0 +
|
||||
// ')'
|
||||
// );
|
||||
// // Get the size of the label
|
||||
|
||||
// // Bounding box for title and text
|
||||
// bbox = label.node().getBBox();
|
||||
|
||||
// // Center the label
|
||||
// label.attr(
|
||||
// 'transform',
|
||||
// 'translate(' + -bbox.width / 2 + ', ' + (-bbox.height / 2 - halfPadding + 3) + ')'
|
||||
// );
|
||||
|
||||
rect
|
||||
.attr('class', 'outer title-state')
|
||||
@@ -992,13 +943,6 @@ const class_box = (parent, node) => {
|
||||
.attr('width', maxWidth + node.padding)
|
||||
.attr('height', maxHeight + node.padding);
|
||||
|
||||
// innerLine
|
||||
// .attr('class', 'divider')
|
||||
// .attr('x1', -bbox.width / 2 - halfPadding)
|
||||
// .attr('x2', bbox.width / 2 + halfPadding)
|
||||
// .attr('y1', -bbox.height / 2 - halfPadding + titleBox.height + halfPadding)
|
||||
// .attr('y2', -bbox.height / 2 - halfPadding + titleBox.height + halfPadding);
|
||||
|
||||
updateNodeBounds(node, rect);
|
||||
|
||||
node.intersect = function (point) {
|
||||
@@ -1009,6 +953,7 @@ const class_box = (parent, node) => {
|
||||
};
|
||||
|
||||
const shapes = {
|
||||
rhombus: question,
|
||||
question,
|
||||
rect,
|
||||
labelRect,
|
||||
@@ -1036,7 +981,7 @@ const shapes = {
|
||||
|
||||
let nodeElems = {};
|
||||
|
||||
export const insertNode = (elem, node, dir) => {
|
||||
export const insertNode = async (elem, node, dir) => {
|
||||
let newEl;
|
||||
let el;
|
||||
|
||||
@@ -1049,9 +994,9 @@ export const insertNode = (elem, node, dir) => {
|
||||
target = node.linkTarget || '_blank';
|
||||
}
|
||||
newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target);
|
||||
el = shapes[node.shape](newEl, node, dir);
|
||||
el = await shapes[node.shape](newEl, node, dir);
|
||||
} else {
|
||||
el = shapes[node.shape](elem, node, dir);
|
||||
el = await shapes[node.shape](elem, node, dir);
|
||||
newEl = el;
|
||||
}
|
||||
if (node.tooltip) {
|
||||
@@ -1066,6 +1011,7 @@ export const insertNode = (elem, node, dir) => {
|
||||
if (node.haveCallback) {
|
||||
nodeElems[node.id].attr('class', nodeElems[node.id].attr('class') + ' clickable');
|
||||
}
|
||||
return newEl;
|
||||
};
|
||||
export const setNodeElem = (elem, node) => {
|
||||
nodeElems[node.id] = elem;
|
||||
@@ -1076,6 +1022,7 @@ export const clear = () => {
|
||||
|
||||
export const positionNode = (node) => {
|
||||
const el = nodeElems[node.id];
|
||||
|
||||
log.trace(
|
||||
'Transforming node',
|
||||
node.diff,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/** Setup arrow head and define the marker. The result is appended to the svg. */
|
||||
|
||||
// import { log } from '../logger';
|
||||
// import { log } from '../logger.js';
|
||||
|
||||
// Only add the number of markers that the diagram needs
|
||||
const insertPatterns = (elem, patternArray, type, id) => {
|
||||
|
@@ -1,9 +1,19 @@
|
||||
import { updateNodeBounds, labelHelper } from './util';
|
||||
import { log } from '../../logger';
|
||||
import { updateNodeBounds, labelHelper } from './util.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { getConfig } from '../../config.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
|
||||
const note = (parent, node) => {
|
||||
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes, true);
|
||||
const note = async (parent, node) => {
|
||||
const useHtmlLabels = node.useHtmlLabels || getConfig().flowchart.htmlLabels;
|
||||
if (!useHtmlLabels) {
|
||||
node.centerLabel = true;
|
||||
}
|
||||
const { shapeSvg, bbox, halfPadding } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
'node ' + node.classes,
|
||||
true
|
||||
);
|
||||
|
||||
log.info('Classes = ', node.classes);
|
||||
// add the rect
|
||||
|
@@ -1,10 +1,13 @@
|
||||
import createLabel from '../createLabel';
|
||||
import { getConfig } from '../../config';
|
||||
import { decodeEntities } from '../../mermaidAPI';
|
||||
import createLabel from '../createLabel.js';
|
||||
import { createText } from '../../rendering-util/createText.js';
|
||||
import { getConfig } from '../../config.js';
|
||||
import { decodeEntities } from '../../mermaidAPI.js';
|
||||
import { select } from 'd3';
|
||||
import { evaluate, sanitizeText } from '../../diagrams/common/common';
|
||||
export const labelHelper = (parent, node, _classes, isNode) => {
|
||||
import { evaluate, sanitizeText } from '../../diagrams/common/common.js';
|
||||
|
||||
export const labelHelper = async (parent, node, _classes, isNode) => {
|
||||
let classes;
|
||||
const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig().flowchart.htmlLabels);
|
||||
if (!_classes) {
|
||||
classes = 'node default';
|
||||
} else {
|
||||
@@ -21,15 +24,23 @@ export const labelHelper = (parent, node, _classes, isNode) => {
|
||||
|
||||
// Replace labelText with default value if undefined
|
||||
let labelText;
|
||||
if (typeof node.labelText === 'undefined') {
|
||||
if (node.labelText === undefined) {
|
||||
labelText = '';
|
||||
} else {
|
||||
labelText = typeof node.labelText === 'string' ? node.labelText : node.labelText[0];
|
||||
}
|
||||
|
||||
const text = label
|
||||
.node()
|
||||
.appendChild(
|
||||
const textNode = label.node();
|
||||
let text;
|
||||
if (node.labelType === 'markdown') {
|
||||
// text = textNode;
|
||||
text = createText(label, sanitizeText(decodeEntities(labelText), getConfig()), {
|
||||
useHtmlLabels,
|
||||
width: node.width || getConfig().flowchart.wrappingWidth,
|
||||
classes: 'markdown-node-label',
|
||||
});
|
||||
} else {
|
||||
text = textNode.appendChild(
|
||||
createLabel(
|
||||
sanitizeText(decodeEntities(labelText), getConfig()),
|
||||
node.labelStyle,
|
||||
@@ -37,23 +48,61 @@ export const labelHelper = (parent, node, _classes, isNode) => {
|
||||
isNode
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Get the size of the label
|
||||
let bbox = text.getBBox();
|
||||
const halfPadding = node.padding / 2;
|
||||
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
|
||||
// if there are images, need to wait for them to load before getting the bounding box
|
||||
const images = div.getElementsByTagName('img');
|
||||
if (images) {
|
||||
const noImgText = labelText.replace(/<img[^>]*>/g, '').trim() === '';
|
||||
|
||||
await Promise.all(
|
||||
[...images].map(
|
||||
(img) =>
|
||||
new Promise((res) =>
|
||||
img.addEventListener('load', function () {
|
||||
img.style.display = 'flex';
|
||||
img.style.flexDirection = 'column';
|
||||
|
||||
if (noImgText) {
|
||||
// default size if no text
|
||||
const bodyFontSize = getConfig().fontSize
|
||||
? getConfig().fontSize
|
||||
: window.getComputedStyle(document.body).fontSize;
|
||||
const enlargingFactor = 5;
|
||||
img.style.width = parseInt(bodyFontSize, 10) * enlargingFactor + 'px';
|
||||
} else {
|
||||
img.style.width = '100%';
|
||||
}
|
||||
res(img);
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
bbox = div.getBoundingClientRect();
|
||||
dv.attr('width', bbox.width);
|
||||
dv.attr('height', bbox.height);
|
||||
}
|
||||
|
||||
const halfPadding = node.padding / 2;
|
||||
|
||||
// Center the label
|
||||
label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');
|
||||
|
||||
if (useHtmlLabels) {
|
||||
label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');
|
||||
} else {
|
||||
label.attr('transform', 'translate(' + 0 + ', ' + -bbox.height / 2 + ')');
|
||||
}
|
||||
if (node.centerLabel) {
|
||||
label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');
|
||||
}
|
||||
label.insert('rect', ':first-child');
|
||||
return { shapeSvg, bbox, halfPadding, label };
|
||||
};
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import theme from './themes';
|
||||
import { MermaidConfig } from './config.type';
|
||||
import theme from './themes/index.js';
|
||||
import { MermaidConfig } from './config.type.js';
|
||||
/**
|
||||
* **Configuration methods in Mermaid version 8.6.0 have been updated, to learn more[[click
|
||||
* here](8.6.0_docs.md)].**
|
||||
@@ -88,13 +88,13 @@ const config: Partial<MermaidConfig> = {
|
||||
*
|
||||
* **Notes**:
|
||||
*
|
||||
* - **strict**: (**default**) tags in text are encoded, click functionality is disabled
|
||||
* - **loose**: tags in text are allowed, click functionality is enabled
|
||||
* - **antiscript**: html tags in text are allowed, (only script element is removed), click
|
||||
* functionality is enabled
|
||||
* - **sandbox**: With this security level all rendering takes place in a sandboxed iframe. This
|
||||
* - **strict**: (**default**) HTML tags in the text are encoded and click functionality is disabled.
|
||||
* - **antiscript**: HTML tags in text are allowed (only script elements are removed), and click
|
||||
* functionality is enabled.
|
||||
* - **loose**: HTML tags in text are allowed and click functionality is enabled.
|
||||
* - **sandbox**: With this security level, all rendering takes place in a sandboxed iframe. This
|
||||
* prevent any JavaScript from running in the context. This may hinder interactive functionality
|
||||
* of the diagram like scripts, popups in sequence diagram or links to other tabs/targets etc.
|
||||
* of the diagram, like scripts, popups in the sequence diagram, links to other tabs or targets, etc.
|
||||
*/
|
||||
securityLevel: 'strict',
|
||||
|
||||
@@ -247,16 +247,29 @@ const config: Partial<MermaidConfig> = {
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------------- | ----------- | ------- | -------- | ----------------------- |
|
||||
* | defaultRenderer | See notes | boolean | 4 | dagre-d3, dagre-wrapper |
|
||||
* | defaultRenderer | See notes | boolean | 4 | dagre-d3, dagre-wrapper, elk |
|
||||
*
|
||||
* **Notes:**
|
||||
*
|
||||
* Decides which rendering engine that is to be used for the rendering. Legal values are:
|
||||
* dagre-d3 dagre-wrapper - wrapper for dagre implemented in mermaid
|
||||
* dagre-d3 dagre-wrapper - wrapper for dagre implemented in mermaid, elk for layout using
|
||||
* elkjs
|
||||
*
|
||||
* Default value: 'dagre-wrapper'
|
||||
*/
|
||||
defaultRenderer: 'dagre-wrapper',
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------------- | ----------- | ------- | -------- | ----------------------- |
|
||||
* | wrappingWidth | See notes | number | 4 | width of nodes where text is wrapped |
|
||||
*
|
||||
* **Notes:**
|
||||
*
|
||||
* When using markdown strings the text ius wrapped automatically, this
|
||||
* value sets the max width of a text before it continues on a new line.
|
||||
* Default value: 'dagre-wrapper'
|
||||
*/
|
||||
wrappingWidth: 200,
|
||||
},
|
||||
|
||||
/** The object containing configurations specific for sequence diagrams */
|
||||
@@ -658,6 +671,17 @@ const config: Partial<MermaidConfig> = {
|
||||
*/
|
||||
numberSectionStyles: 4,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ----------- | ------------------------- | ------ | -------- | --------- |
|
||||
* | displayMode | Controls the display mode | string | 4 | 'compact' |
|
||||
*
|
||||
* **Notes**:
|
||||
*
|
||||
* - **compact**: Enables displaying multiple tasks on the same row.
|
||||
*/
|
||||
displayMode: '',
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ---------- | ---------------------------- | ---- | -------- | ---------------- |
|
||||
@@ -683,7 +707,6 @@ const config: Partial<MermaidConfig> = {
|
||||
* Default value: undefined
|
||||
*/
|
||||
tickInterval: undefined,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ----------- | ----------- | ------- | -------- | ----------- |
|
||||
@@ -861,6 +884,156 @@ const config: Partial<MermaidConfig> = {
|
||||
sectionFills: ['#191970', '#8B008B', '#4B0082', '#2F4F4F', '#800000', '#8B4513', '#00008B'],
|
||||
sectionColours: ['#fff'],
|
||||
},
|
||||
/** The object containing configurations specific for timeline diagrams */
|
||||
timeline: {
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | -------------- | ---------------------------------------------------- | ------- | -------- | ------------------ |
|
||||
* | diagramMarginX | Margin to the right and left of the sequence diagram | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 50
|
||||
*/
|
||||
diagramMarginX: 50,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | -------------- | -------------------------------------------------- | ------- | -------- | ------------------ |
|
||||
* | diagramMarginY | Margin to the over and under the sequence diagram. | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 10
|
||||
*/
|
||||
diagramMarginY: 10,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ----------- | --------------------- | ------- | -------- | ------------------ |
|
||||
* | actorMargin | Margin between actors | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 50
|
||||
*/
|
||||
leftMargin: 150,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------- | -------------------- | ------- | -------- | ------------------ |
|
||||
* | width | Width of actor boxes | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 150
|
||||
*/
|
||||
width: 150,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------- | --------------------- | ------- | -------- | ------------------ |
|
||||
* | height | Height of actor boxes | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 65
|
||||
*/
|
||||
height: 50,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------- | ------------------------ | ------- | -------- | ------------------ |
|
||||
* | boxMargin | Margin around loop boxes | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 10
|
||||
*/
|
||||
boxMargin: 10,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------- | -------------------------------------------- | ------- | -------- | ------------------ |
|
||||
* | boxTextMargin | Margin around the text in loop/alt/opt boxes | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 5
|
||||
*/
|
||||
boxTextMargin: 5,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ---------- | ------------------- | ------- | -------- | ------------------ |
|
||||
* | noteMargin | Margin around notes | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:** Default value: 10
|
||||
*/
|
||||
noteMargin: 10,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------- | ----------------------- | ------- | -------- | ------------------ |
|
||||
* | messageMargin | Space between messages. | Integer | Required | Any Positive Value |
|
||||
*
|
||||
* **Notes:**
|
||||
*
|
||||
* Space between messages.
|
||||
*
|
||||
* Default value: 35
|
||||
*/
|
||||
messageMargin: 35,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------ | --------------------------- | ---- | -------- | ------------------------- |
|
||||
* | messageAlign | Multiline message alignment | 3 | 4 | 'left', 'center', 'right' |
|
||||
*
|
||||
* **Notes:** Default value: 'center'
|
||||
*/
|
||||
messageAlign: 'center',
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------------- | ------------------------------------------ | ------- | -------- | ------------------ |
|
||||
* | bottomMarginAdj | Prolongs the edge of the diagram downwards | Integer | 4 | Any Positive Value |
|
||||
*
|
||||
* **Notes:**
|
||||
*
|
||||
* Depending on css styling this might need adjustment.
|
||||
*
|
||||
* Default value: 1
|
||||
*/
|
||||
bottomMarginAdj: 1,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ----------- | ----------- | ------- | -------- | ----------- |
|
||||
* | useMaxWidth | See notes | boolean | 4 | true, false |
|
||||
*
|
||||
* **Notes:**
|
||||
*
|
||||
* When this flag is set the height and width is set to 100% and is then scaling with the
|
||||
* available space if not the absolute space required is used.
|
||||
*
|
||||
* Default value: true
|
||||
*/
|
||||
useMaxWidth: true,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ----------- | --------------------------------- | ---- | -------- | ----------- |
|
||||
* | rightAngles | Curved Arrows become Right Angles | 3 | 4 | true, false |
|
||||
*
|
||||
* **Notes:**
|
||||
*
|
||||
* This will display arrows that start and begin at the same node as right angles, rather than a
|
||||
* curves
|
||||
*
|
||||
* Default value: false
|
||||
*/
|
||||
rightAngles: false,
|
||||
taskFontSize: 14,
|
||||
taskFontFamily: '"Open Sans", sans-serif',
|
||||
taskMargin: 50,
|
||||
// width of activation box
|
||||
activationWidth: 10,
|
||||
|
||||
// text placement as: tspan | fo | old only text as before
|
||||
textPlacement: 'fo',
|
||||
actorColours: ['#8FBC8F', '#7CFC00', '#00FFFF', '#20B2AA', '#B0E0E6', '#FFFFE0'],
|
||||
|
||||
sectionFills: ['#191970', '#8B008B', '#4B0082', '#2F4F4F', '#800000', '#8B4513', '#00008B'],
|
||||
sectionColours: ['#fff'],
|
||||
disableMulticolor: false,
|
||||
},
|
||||
class: {
|
||||
/**
|
||||
* ### titleTopMargin
|
||||
@@ -1096,6 +1269,193 @@ const config: Partial<MermaidConfig> = {
|
||||
* Default value: true
|
||||
*/
|
||||
useMaxWidth: true,
|
||||
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------ | -------------------------------------------------------------------------------- | ------- | -------- | ------------------- |
|
||||
* | textPosition | Axial position of slice's label from zero at the center to 1 at the outside edge | Number | Optional | Decimal from 0 to 1 |
|
||||
*
|
||||
* **Notes:** Default value: 0.75
|
||||
*/
|
||||
textPosition: 0.75,
|
||||
},
|
||||
|
||||
quadrantChart: {
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------------- | ---------------------------------- | ------- | -------- | ------------------- |
|
||||
* | chartWidth | Width of the chart | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 500
|
||||
*/
|
||||
chartWidth: 500,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------------- | ---------------------------------- | ------- | -------- | ------------------- |
|
||||
* | chartHeight | Height of the chart | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 500
|
||||
*/
|
||||
chartHeight: 500,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------------ | ---------------------------------- | ------- | -------- | ------------------- |
|
||||
* | titlePadding | Chart title top and bottom padding | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 10
|
||||
*/
|
||||
titlePadding: 10,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------------ | ---------------------------------- | ------- | -------- | ------------------- |
|
||||
* | titleFontSize | Chart title font size | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 20
|
||||
*/
|
||||
titleFontSize: 20,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------------- | ---------------------------------- | ------- | -------- | ------------------- |
|
||||
* | quadrantPadding | Padding around the quadrant square | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 5
|
||||
*/
|
||||
quadrantPadding: 5,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ---------------------- | -------------------------------------------------------------------------- | ------- | -------- | ------------------- |
|
||||
* | quadrantTextTopPadding | quadrant title padding from top if the quadrant is rendered on top | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 5
|
||||
*/
|
||||
quadrantTextTopPadding: 5,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------------ | ---------------------------------- | ------- | -------- | ------------------- |
|
||||
* | quadrantLabelFontSize | quadrant title font size | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 16
|
||||
*/
|
||||
quadrantLabelFontSize: 16,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------------------------------- | ------------------------------------------------------------- | ------- | -------- | ------------------- |
|
||||
* | quadrantInternalBorderStrokeWidth | stroke width of edges of the box that are inside the quadrant | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 1
|
||||
*/
|
||||
quadrantInternalBorderStrokeWidth: 1,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------------------------------- | -------------------------------------------------------------- | ------- | -------- | ------------------- |
|
||||
* | quadrantExternalBorderStrokeWidth | stroke width of edges of the box that are outside the quadrant | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 2
|
||||
*/
|
||||
quadrantExternalBorderStrokeWidth: 2,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------------- | ---------------------------------- | ------- | -------- | ------------------- |
|
||||
* | xAxisLabelPadding | Padding around x-axis labels | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 5
|
||||
*/
|
||||
xAxisLabelPadding: 5,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------------ | ---------------------------------- | ------- | -------- | ------------------- |
|
||||
* | xAxisLabelFontSize | x-axis label font size | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 16
|
||||
*/
|
||||
xAxisLabelFontSize: 16,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------- | ------------------------------- | ------- | -------- | ------------------- |
|
||||
* | xAxisPosition | position of x-axis labels | string | Optional | 'top' or 'bottom' |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: top
|
||||
*/
|
||||
xAxisPosition: 'top',
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | --------------- | ---------------------------------- | ------- | -------- | ------------------- |
|
||||
* | yAxisLabelPadding | Padding around y-axis labels | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 5
|
||||
*/
|
||||
yAxisLabelPadding: 5,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------------ | ---------------------------------- | ------- | -------- | ------------------- |
|
||||
* | yAxisLabelFontSize | y-axis label font size | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 16
|
||||
*/
|
||||
yAxisLabelFontSize: 16,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------- | ------------------------------- | ------- | -------- | ------------------- |
|
||||
* | yAxisPosition | position of y-axis labels | string | Optional | 'left' or 'right' |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: left
|
||||
*/
|
||||
yAxisPosition: 'left',
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ---------------------- | -------------------------------------- | ------- | -------- | ------------------- |
|
||||
* | pointTextPadding | padding between point and point label | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 5
|
||||
*/
|
||||
pointTextPadding: 5,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ---------------------- | ---------------------- | ------- | -------- | ------------------- |
|
||||
* | pointTextPadding | point title font size | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 12
|
||||
*/
|
||||
pointLabelFontSize: 12,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ------------- | ------------------------------- | ------- | -------- | ------------------- |
|
||||
* | pointRadius | radius of the point to be drawn | number | Optional | Any positive number |
|
||||
*
|
||||
* **Notes:**
|
||||
* Default value: 5
|
||||
*/
|
||||
pointRadius: 5,
|
||||
/**
|
||||
* | Parameter | Description | Type | Required | Values |
|
||||
* | ----------- | ----------- | ------- | -------- | ----------- |
|
||||
* | useMaxWidth | See Notes | boolean | Required | true, false |
|
||||
*
|
||||
* **Notes:**
|
||||
*
|
||||
* When this flag is set to true, the diagram width is locked to 100% and scaled based on
|
||||
* available space. If set to false, the diagram reserves its absolute width.
|
||||
*
|
||||
* Default value: true
|
||||
*/
|
||||
useMaxWidth: true,
|
||||
},
|
||||
|
||||
/** The object containing configurations specific for req diagrams */
|
||||
|
94
packages/mermaid/src/diagram-api/comments.spec.ts
Normal file
94
packages/mermaid/src/diagram-api/comments.spec.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
// tests to check that comments are removed
|
||||
|
||||
import { cleanupComments } from './comments.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('comments', () => {
|
||||
it('should remove comments', () => {
|
||||
const text = `
|
||||
|
||||
%% This is a comment
|
||||
%% This is another comment
|
||||
graph TD
|
||||
A-->B
|
||||
%% This is a comment
|
||||
`;
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
"graph TD
|
||||
A-->B
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should keep init statements when removing comments', () => {
|
||||
const text = `
|
||||
%% This is a comment
|
||||
|
||||
%% This is another comment
|
||||
%%{init: {'theme': 'forest'}}%%
|
||||
%%{ init: {'theme': 'space before init'}}%%
|
||||
%%{init: {'theme': 'space after ending'}}%%
|
||||
graph TD
|
||||
A-->B
|
||||
|
||||
B-->C
|
||||
%% This is a comment
|
||||
`;
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
"%%{init: {'theme': 'forest'}}%%
|
||||
%%{ init: {'theme': 'space before init'}}%%
|
||||
%%{init: {'theme': 'space after ending'}}%%
|
||||
graph TD
|
||||
A-->B
|
||||
|
||||
B-->C
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should remove indented comments', () => {
|
||||
const text = `
|
||||
%% This is a comment
|
||||
graph TD
|
||||
A-->B
|
||||
%% This is a comment
|
||||
C-->D
|
||||
`;
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
"graph TD
|
||||
A-->B
|
||||
C-->D
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should remove empty newlines from start', () => {
|
||||
const text = `
|
||||
|
||||
|
||||
|
||||
|
||||
%% This is a comment
|
||||
graph TD
|
||||
A-->B
|
||||
`;
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
"graph TD
|
||||
A-->B
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should remove comments at end of text with no newline', () => {
|
||||
const text = `
|
||||
graph TD
|
||||
A-->B
|
||||
%% This is a comment`;
|
||||
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
"graph TD
|
||||
A-->B
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
8
packages/mermaid/src/diagram-api/comments.ts
Normal file
8
packages/mermaid/src/diagram-api/comments.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Remove all lines starting with `%%` from the text that don't contain a `%%{`
|
||||
* @param text - The text to remove comments from
|
||||
* @returns cleaned text
|
||||
*/
|
||||
export const cleanupComments = (text: string): string => {
|
||||
return text.trimStart().replace(/^\s*%%(?!{)[^\n]+\n?/gm, '');
|
||||
};
|
@@ -1,10 +1,16 @@
|
||||
import { MermaidConfig } from '../config.type';
|
||||
import { log } from '../logger';
|
||||
import { DetectorRecord, DiagramDetector, DiagramLoader } from './types';
|
||||
import { frontMatterRegex } from './frontmatter';
|
||||
import { MermaidConfig } from '../config.type.js';
|
||||
import { log } from '../logger.js';
|
||||
import type {
|
||||
DetectorRecord,
|
||||
DiagramDetector,
|
||||
DiagramLoader,
|
||||
ExternalDiagramDefinition,
|
||||
} from './types.js';
|
||||
import { frontMatterRegex } from './frontmatter.js';
|
||||
import { getDiagram, registerDiagram } from './diagramAPI.js';
|
||||
import { UnknownDiagramError } from '../errors.js';
|
||||
|
||||
const directive =
|
||||
/[%]{2}[{]\s*(?:(?:(\w+)\s*:|(\w+))\s*(?:(?:(\w+))|((?:(?![}][%]{2}).|\r?\n)*))?\s*)(?:[}][%]{2})?/gi;
|
||||
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> = {};
|
||||
@@ -40,15 +46,72 @@ export const detectType = function (text: string, config?: MermaidConfig): strin
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`No diagram type detected for text: ${text}`);
|
||||
throw new UnknownDiagramError(
|
||||
`No diagram type detected matching given configuration for text: ${text}`
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers lazy-loaded diagrams to Mermaid.
|
||||
*
|
||||
* The diagram function is loaded asynchronously, so that diagrams are only loaded
|
||||
* if the diagram is detected.
|
||||
*
|
||||
* @remarks
|
||||
* Please note that the order of diagram detectors is important.
|
||||
* The first detector to return `true` is the diagram that will be loaded
|
||||
* and used, so put more specific detectors at the beginning!
|
||||
*
|
||||
* @param diagrams - Diagrams to lazy load, and their detectors, in order of importance.
|
||||
*/
|
||||
export const registerLazyLoadedDiagrams = (...diagrams: ExternalDiagramDefinition[]) => {
|
||||
for (const { id, detector, loader } of diagrams) {
|
||||
addDetector(id, detector, loader);
|
||||
}
|
||||
};
|
||||
|
||||
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]) {
|
||||
throw new Error(`Detector with key ${key} already exists`);
|
||||
log.error(`Detector with key ${key} already exists`);
|
||||
} else {
|
||||
detectors[key] = { detector, loader };
|
||||
}
|
||||
detectors[key] = { detector, loader };
|
||||
log.debug(`Detector with key ${key} added${loader ? ' with loader' : ''}`);
|
||||
};
|
||||
|
||||
export const getDiagramLoader = (key: string) => detectors[key].loader;
|
||||
export const getDiagramLoader = (key: string) => {
|
||||
return detectors[key].loader;
|
||||
};
|
||||
|
@@ -0,0 +1,82 @@
|
||||
import { it, describe, expect } from 'vitest';
|
||||
import { detectType } from './detectType.js';
|
||||
import { addDiagrams } from './diagram-orchestration.js';
|
||||
|
||||
describe('diagram-orchestration', () => {
|
||||
it('should register diagrams', () => {
|
||||
expect(() => detectType('graph TD; A-->B')).toThrow();
|
||||
addDiagrams();
|
||||
expect(detectType('graph TD; A-->B')).toBe('flowchart');
|
||||
});
|
||||
|
||||
describe('proper diagram types should be detetced', () => {
|
||||
beforeAll(() => {
|
||||
addDiagrams();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ text: 'graph TD;', expected: 'flowchart' },
|
||||
{ text: 'flowchart TD;', expected: 'flowchart-v2' },
|
||||
{ text: 'flowchart-v2 TD;', expected: 'flowchart-v2' },
|
||||
{ text: 'flowchart-elk TD;', expected: 'flowchart-elk' },
|
||||
{ text: 'error', expected: 'error' },
|
||||
{ text: 'C4Context;', expected: 'c4' },
|
||||
{ text: 'classDiagram', expected: 'class' },
|
||||
{ text: 'classDiagram-v2', expected: 'classDiagram' },
|
||||
{ text: 'erDiagram', expected: 'er' },
|
||||
{ text: 'journey', expected: 'journey' },
|
||||
{ text: 'gantt', expected: 'gantt' },
|
||||
{ text: 'pie', expected: 'pie' },
|
||||
{ text: 'requirementDiagram', expected: 'requirement' },
|
||||
{ text: 'info', expected: 'info' },
|
||||
{ text: 'sequenceDiagram', expected: 'sequence' },
|
||||
{ text: 'mindmap', expected: 'mindmap' },
|
||||
{ text: 'timeline', expected: 'timeline' },
|
||||
{ text: 'gitGraph', expected: 'gitGraph' },
|
||||
{ text: 'stateDiagram', expected: 'state' },
|
||||
{ text: 'stateDiagram-v2', expected: 'stateDiagram' },
|
||||
])(
|
||||
'should $text be detected as $expected',
|
||||
({ text, expected }: { text: string; expected: string }) => {
|
||||
expect(detectType(text)).toBe(expected);
|
||||
}
|
||||
);
|
||||
|
||||
it('should detect proper flowchart type based on config', () => {
|
||||
// graph & dagre-d3 => flowchart
|
||||
expect(detectType('graph TD; A-->B')).toBe('flowchart');
|
||||
// graph & dagre-d3 => flowchart
|
||||
expect(detectType('graph TD; A-->B', { flowchart: { defaultRenderer: 'dagre-d3' } })).toBe(
|
||||
'flowchart'
|
||||
);
|
||||
// flowchart & dagre-d3 => error
|
||||
expect(() =>
|
||||
detectType('flowchart TD; A-->B', { flowchart: { defaultRenderer: 'dagre-d3' } })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
'"No diagram type detected matching given configuration for text: flowchart TD; A-->B"'
|
||||
);
|
||||
|
||||
// graph & dagre-wrapper => flowchart-v2
|
||||
expect(
|
||||
detectType('graph TD; A-->B', { flowchart: { defaultRenderer: 'dagre-wrapper' } })
|
||||
).toBe('flowchart-v2');
|
||||
// flowchart ==> flowchart-v2
|
||||
expect(detectType('flowchart TD; A-->B')).toBe('flowchart-v2');
|
||||
// flowchart && dagre-wrapper ==> flowchart-v2
|
||||
expect(
|
||||
detectType('flowchart TD; A-->B', { flowchart: { defaultRenderer: 'dagre-wrapper' } })
|
||||
).toBe('flowchart-v2');
|
||||
// flowchart && elk ==> flowchart-elk
|
||||
expect(detectType('flowchart TD; A-->B', { flowchart: { defaultRenderer: 'elk' } })).toBe(
|
||||
'flowchart-elk'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not detect flowchart if pie contains flowchart', () => {
|
||||
expect(
|
||||
detectType(`pie title: "flowchart"
|
||||
flowchart: 1 "pie" pie: 2 "pie"`)
|
||||
).toBe('pie');
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,98 +1,25 @@
|
||||
import { registerDiagram } from './diagramAPI';
|
||||
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import gitGraphParser from '../diagrams/git/parser/gitGraph';
|
||||
import { gitGraphDetector } from '../diagrams/git/gitGraphDetector';
|
||||
import gitGraphDb from '../diagrams/git/gitGraphAst';
|
||||
import gitGraphRenderer from '../diagrams/git/gitGraphRenderer';
|
||||
import gitGraphStyles from '../diagrams/git/styles';
|
||||
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import c4Parser from '../diagrams/c4/parser/c4Diagram';
|
||||
import { c4Detector } from '../diagrams/c4/c4Detector';
|
||||
import c4Db from '../diagrams/c4/c4Db';
|
||||
import c4Renderer from '../diagrams/c4/c4Renderer';
|
||||
import c4Styles from '../diagrams/c4/styles';
|
||||
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import classParser from '../diagrams/class/parser/classDiagram';
|
||||
import { classDetector } from '../diagrams/class/classDetector';
|
||||
import { classDetectorV2 } from '../diagrams/class/classDetector-V2';
|
||||
import classDb from '../diagrams/class/classDb';
|
||||
import classRenderer from '../diagrams/class/classRenderer';
|
||||
import classRendererV2 from '../diagrams/class/classRenderer-v2';
|
||||
import classStyles from '../diagrams/class/styles';
|
||||
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import erParser from '../diagrams/er/parser/erDiagram';
|
||||
import { erDetector } from '../diagrams/er/erDetector';
|
||||
import erDb from '../diagrams/er/erDb';
|
||||
import erRenderer from '../diagrams/er/erRenderer';
|
||||
import erStyles from '../diagrams/er/styles';
|
||||
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import flowParser from '../diagrams/flowchart/parser/flow';
|
||||
import { flowDetector } from '../diagrams/flowchart/flowDetector';
|
||||
import { flowDetectorV2 } from '../diagrams/flowchart/flowDetector-v2';
|
||||
import flowDb from '../diagrams/flowchart/flowDb';
|
||||
import flowRenderer from '../diagrams/flowchart/flowRenderer';
|
||||
import flowRendererV2 from '../diagrams/flowchart/flowRenderer-v2';
|
||||
import flowStyles from '../diagrams/flowchart/styles';
|
||||
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import ganttParser from '../diagrams/gantt/parser/gantt';
|
||||
import { ganttDetector } from '../diagrams/gantt/ganttDetector';
|
||||
import ganttDb from '../diagrams/gantt/ganttDb';
|
||||
import ganttRenderer from '../diagrams/gantt/ganttRenderer';
|
||||
import ganttStyles from '../diagrams/gantt/styles';
|
||||
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import infoParser from '../diagrams/info/parser/info';
|
||||
import infoDb from '../diagrams/info/infoDb';
|
||||
import infoRenderer from '../diagrams/info/infoRenderer';
|
||||
import { infoDetector } from '../diagrams/info/infoDetector';
|
||||
import infoStyles from '../diagrams/info/styles';
|
||||
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import pieParser from '../diagrams/pie/parser/pie';
|
||||
import { pieDetector } from '../diagrams/pie/pieDetector';
|
||||
import pieDb from '../diagrams/pie/pieDb';
|
||||
import pieRenderer from '../diagrams/pie/pieRenderer';
|
||||
import pieStyles from '../diagrams/pie/styles';
|
||||
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import requirementParser from '../diagrams/requirement/parser/requirementDiagram';
|
||||
import { requirementDetector } from '../diagrams/requirement/requirementDetector';
|
||||
import requirementDb from '../diagrams/requirement/requirementDb';
|
||||
import requirementRenderer from '../diagrams/requirement/requirementRenderer';
|
||||
import requirementStyles from '../diagrams/requirement/styles';
|
||||
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import sequenceParser from '../diagrams/sequence/parser/sequenceDiagram';
|
||||
import { sequenceDetector } from '../diagrams/sequence/sequenceDetector';
|
||||
import sequenceDb from '../diagrams/sequence/sequenceDb';
|
||||
import sequenceRenderer from '../diagrams/sequence/sequenceRenderer';
|
||||
import sequenceStyles from '../diagrams/sequence/styles';
|
||||
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import stateParser from '../diagrams/state/parser/stateDiagram';
|
||||
import { stateDetector } from '../diagrams/state/stateDetector';
|
||||
import { stateDetectorV2 } from '../diagrams/state/stateDetector-V2';
|
||||
import stateDb from '../diagrams/state/stateDb';
|
||||
import stateRenderer from '../diagrams/state/stateRenderer';
|
||||
import stateRendererV2 from '../diagrams/state/stateRenderer-v2';
|
||||
import stateStyles from '../diagrams/state/styles';
|
||||
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import journeyParser from '../diagrams/user-journey/parser/journey';
|
||||
import { journeyDetector } from '../diagrams/user-journey/journeyDetector';
|
||||
import journeyDb from '../diagrams/user-journey/journeyDb';
|
||||
import journeyRenderer from '../diagrams/user-journey/journeyRenderer';
|
||||
import journeyStyles from '../diagrams/user-journey/styles';
|
||||
import { setConfig } from '../config';
|
||||
|
||||
import errorRenderer from '../diagrams/error/errorRenderer';
|
||||
import errorStyles from '../diagrams/error/styles';
|
||||
import c4 from '../diagrams/c4/c4Detector.js';
|
||||
import flowchart from '../diagrams/flowchart/flowDetector.js';
|
||||
import flowchartV2 from '../diagrams/flowchart/flowDetector-v2.js';
|
||||
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 quadrantChart from '../diagrams/quadrant-chart/quadrantDetector.js';
|
||||
import requirement from '../diagrams/requirement/requirementDetector.js';
|
||||
import sequence from '../diagrams/sequence/sequenceDetector.js';
|
||||
import classDiagram from '../diagrams/class/classDetector.js';
|
||||
import classDiagramV2 from '../diagrams/class/classDetector-V2.js';
|
||||
import state from '../diagrams/state/stateDetector.js';
|
||||
import stateV2 from '../diagrams/state/stateDetector-V2.js';
|
||||
import journey from '../diagrams/user-journey/journeyDetector.js';
|
||||
import errorDiagram from '../diagrams/error/errorDiagram.js';
|
||||
import flowchartElk from '../diagrams/flowchart/elk/detector.js';
|
||||
import timeline from '../diagrams/timeline/detector.js';
|
||||
import mindmap from '../diagrams/mindmap/detector.js';
|
||||
import { registerLazyLoadedDiagrams } from './detectType.js';
|
||||
import { registerDiagram } from './diagramAPI.js';
|
||||
|
||||
let hasLoadedDiagrams = false;
|
||||
export const addDiagrams = () => {
|
||||
@@ -102,243 +29,56 @@ export const addDiagrams = () => {
|
||||
// This is added here to avoid race-conditions.
|
||||
// We could optimize the loading logic somehow.
|
||||
hasLoadedDiagrams = true;
|
||||
registerDiagram('error', errorDiagram, (text) => {
|
||||
return text.toLowerCase().trim() === 'error';
|
||||
});
|
||||
registerDiagram(
|
||||
'error',
|
||||
// Special diagram with error messages but setup as a regular diagram
|
||||
'---',
|
||||
// --- diagram type may appear if YAML front-matter is not parsed correctly
|
||||
{
|
||||
db: {
|
||||
clear: () => {
|
||||
// Quite ok, clear needs to be there for error to work as a regular diagram
|
||||
// Quite ok, clear needs to be there for --- to work as a regular diagram
|
||||
},
|
||||
},
|
||||
styles: errorStyles,
|
||||
renderer: errorRenderer,
|
||||
styles: {}, // should never be used
|
||||
renderer: {}, // should never be used
|
||||
parser: {
|
||||
parser: { yy: {} },
|
||||
parse: () => {
|
||||
// no op
|
||||
throw new Error(
|
||||
'Diagrams beginning with --- are not valid. ' +
|
||||
'If you were trying to use a YAML front-matter, please ensure that ' +
|
||||
"you've correctly opened and closed the YAML front-matter with un-indented `---` blocks"
|
||||
);
|
||||
},
|
||||
},
|
||||
init: () => {
|
||||
// no op
|
||||
},
|
||||
init: () => null, // no op
|
||||
},
|
||||
(text) => text.toLowerCase().trim() === 'error'
|
||||
(text) => {
|
||||
return text.toLowerCase().trimStart().startsWith('---');
|
||||
}
|
||||
);
|
||||
|
||||
registerDiagram(
|
||||
'c4',
|
||||
{
|
||||
parser: c4Parser,
|
||||
db: c4Db,
|
||||
renderer: c4Renderer,
|
||||
styles: c4Styles,
|
||||
init: (cnf) => {
|
||||
c4Renderer.setConf(cnf.c4);
|
||||
},
|
||||
},
|
||||
c4Detector
|
||||
);
|
||||
registerDiagram(
|
||||
'class',
|
||||
{
|
||||
parser: classParser,
|
||||
db: classDb,
|
||||
renderer: classRenderer,
|
||||
styles: classStyles,
|
||||
init: (cnf) => {
|
||||
if (!cnf.class) {
|
||||
cnf.class = {};
|
||||
}
|
||||
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
classDb.clear();
|
||||
},
|
||||
},
|
||||
classDetector
|
||||
);
|
||||
registerDiagram(
|
||||
'classDiagram',
|
||||
{
|
||||
parser: classParser,
|
||||
db: classDb,
|
||||
renderer: classRendererV2,
|
||||
styles: classStyles,
|
||||
init: (cnf) => {
|
||||
if (!cnf.class) {
|
||||
cnf.class = {};
|
||||
}
|
||||
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
classDb.clear();
|
||||
},
|
||||
},
|
||||
classDetectorV2
|
||||
);
|
||||
registerDiagram(
|
||||
'er',
|
||||
{
|
||||
parser: erParser,
|
||||
db: erDb,
|
||||
renderer: erRenderer,
|
||||
styles: erStyles,
|
||||
},
|
||||
erDetector
|
||||
);
|
||||
registerDiagram(
|
||||
'gantt',
|
||||
{
|
||||
parser: ganttParser,
|
||||
db: ganttDb,
|
||||
renderer: ganttRenderer,
|
||||
styles: ganttStyles,
|
||||
},
|
||||
ganttDetector
|
||||
);
|
||||
registerDiagram(
|
||||
'info',
|
||||
{
|
||||
parser: infoParser,
|
||||
db: infoDb,
|
||||
renderer: infoRenderer,
|
||||
styles: infoStyles,
|
||||
},
|
||||
infoDetector
|
||||
);
|
||||
registerDiagram(
|
||||
'pie',
|
||||
{
|
||||
parser: pieParser,
|
||||
db: pieDb,
|
||||
renderer: pieRenderer,
|
||||
styles: pieStyles,
|
||||
},
|
||||
pieDetector
|
||||
);
|
||||
registerDiagram(
|
||||
'requirement',
|
||||
{
|
||||
parser: requirementParser,
|
||||
db: requirementDb,
|
||||
renderer: requirementRenderer,
|
||||
styles: requirementStyles,
|
||||
},
|
||||
requirementDetector
|
||||
);
|
||||
registerDiagram(
|
||||
'sequence',
|
||||
{
|
||||
parser: sequenceParser,
|
||||
db: sequenceDb,
|
||||
renderer: sequenceRenderer,
|
||||
styles: sequenceStyles,
|
||||
init: (cnf) => {
|
||||
if (!cnf.sequence) {
|
||||
cnf.sequence = {};
|
||||
}
|
||||
cnf.sequence.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
if ('sequenceDiagram' in cnf) {
|
||||
throw new Error(
|
||||
'`mermaid config.sequenceDiagram` has been renamed to `config.sequence`. Please update your mermaid config.'
|
||||
);
|
||||
}
|
||||
sequenceDb.setWrap(cnf.wrap);
|
||||
sequenceRenderer.setConf(cnf.sequence);
|
||||
},
|
||||
},
|
||||
sequenceDetector
|
||||
);
|
||||
registerDiagram(
|
||||
'state',
|
||||
{
|
||||
parser: stateParser,
|
||||
db: stateDb,
|
||||
renderer: stateRenderer,
|
||||
styles: stateStyles,
|
||||
init: (cnf) => {
|
||||
if (!cnf.state) {
|
||||
cnf.state = {};
|
||||
}
|
||||
cnf.state.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
stateDb.clear();
|
||||
},
|
||||
},
|
||||
stateDetector
|
||||
);
|
||||
registerDiagram(
|
||||
'stateDiagram',
|
||||
{
|
||||
parser: stateParser,
|
||||
db: stateDb,
|
||||
renderer: stateRendererV2,
|
||||
styles: stateStyles,
|
||||
init: (cnf) => {
|
||||
if (!cnf.state) {
|
||||
cnf.state = {};
|
||||
}
|
||||
cnf.state.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
stateDb.clear();
|
||||
},
|
||||
},
|
||||
stateDetectorV2
|
||||
);
|
||||
registerDiagram(
|
||||
'journey',
|
||||
{
|
||||
parser: journeyParser,
|
||||
db: journeyDb,
|
||||
renderer: journeyRenderer,
|
||||
styles: journeyStyles,
|
||||
init: (cnf) => {
|
||||
journeyRenderer.setConf(cnf.journey);
|
||||
journeyDb.clear();
|
||||
},
|
||||
},
|
||||
journeyDetector
|
||||
);
|
||||
|
||||
registerDiagram(
|
||||
'flowchart',
|
||||
{
|
||||
parser: flowParser,
|
||||
db: flowDb,
|
||||
renderer: flowRendererV2,
|
||||
styles: flowStyles,
|
||||
init: (cnf) => {
|
||||
if (!cnf.flowchart) {
|
||||
cnf.flowchart = {};
|
||||
}
|
||||
// TODO, broken as of 2022-09-14 (13809b50251845475e6dca65cc395761be38fbd2)
|
||||
cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
flowRenderer.setConf(cnf.flowchart);
|
||||
flowDb.clear();
|
||||
flowDb.setGen('gen-1');
|
||||
},
|
||||
},
|
||||
flowDetector
|
||||
);
|
||||
registerDiagram(
|
||||
'flowchart-v2',
|
||||
{
|
||||
parser: flowParser,
|
||||
db: flowDb,
|
||||
renderer: flowRendererV2,
|
||||
styles: flowStyles,
|
||||
init: (cnf) => {
|
||||
if (!cnf.flowchart) {
|
||||
cnf.flowchart = {};
|
||||
}
|
||||
cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
// flowchart-v2 uses dagre-wrapper, which doesn't have access to flowchart cnf
|
||||
setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } });
|
||||
flowRendererV2.setConf(cnf.flowchart);
|
||||
flowDb.clear();
|
||||
flowDb.setGen('gen-2');
|
||||
},
|
||||
},
|
||||
flowDetectorV2
|
||||
);
|
||||
registerDiagram(
|
||||
'gitGraph',
|
||||
{ parser: gitGraphParser, db: gitGraphDb, renderer: gitGraphRenderer, styles: gitGraphStyles },
|
||||
gitGraphDetector
|
||||
// Ordering of detectors is important. The first one to return true will be used.
|
||||
registerLazyLoadedDiagrams(
|
||||
c4,
|
||||
classDiagramV2,
|
||||
classDiagram,
|
||||
er,
|
||||
gantt,
|
||||
info,
|
||||
pie,
|
||||
requirement,
|
||||
sequence,
|
||||
flowchartElk,
|
||||
flowchartV2,
|
||||
flowchart,
|
||||
mindmap,
|
||||
timeline,
|
||||
git,
|
||||
stateV2,
|
||||
state,
|
||||
journey,
|
||||
quadrantChart
|
||||
);
|
||||
};
|
||||
|
@@ -1,9 +1,14 @@
|
||||
import { detectType } from './detectType';
|
||||
import { getDiagram, registerDiagram } from './diagramAPI';
|
||||
import { addDiagrams } from './diagram-orchestration';
|
||||
import { DiagramDetector } from './types';
|
||||
import { detectType } from './detectType.js';
|
||||
import { getDiagram, registerDiagram } from './diagramAPI.js';
|
||||
import { addDiagrams } from './diagram-orchestration.js';
|
||||
import { DiagramDetector } from './types.js';
|
||||
import { getDiagramFromText } from '../Diagram.js';
|
||||
import { it, describe, expect, beforeAll } from 'vitest';
|
||||
|
||||
addDiagrams();
|
||||
beforeAll(async () => {
|
||||
await getDiagramFromText('sequenceDiagram');
|
||||
});
|
||||
|
||||
describe('DiagramAPI', () => {
|
||||
it('should return default diagrams', () => {
|
||||
@@ -11,13 +16,17 @@ describe('DiagramAPI', () => {
|
||||
});
|
||||
|
||||
it('should throw error if diagram is not defined', () => {
|
||||
expect(() => getDiagram('loki')).toThrow();
|
||||
expect(() => getDiagram('loki')).toThrowErrorMatchingInlineSnapshot(
|
||||
'"Diagram loki not found."'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle diagram registrations', () => {
|
||||
expect(() => getDiagram('loki')).toThrow();
|
||||
expect(() => detectType('loki diagram')).toThrow(
|
||||
'No diagram type detected for text: loki diagram'
|
||||
expect(() => getDiagram('loki')).toThrowErrorMatchingInlineSnapshot(
|
||||
'"Diagram loki not found."'
|
||||
);
|
||||
expect(() => detectType('loki diagram')).toThrowErrorMatchingInlineSnapshot(
|
||||
'"No diagram type detected matching given configuration for text: loki diagram"'
|
||||
);
|
||||
const detector: DiagramDetector = (str: string) => {
|
||||
return str.match('loki') !== null;
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import { addDetector } from './detectType';
|
||||
import { log as _log, setLogLevel as _setLogLevel } from '../logger';
|
||||
import { getConfig as _getConfig } from '../config';
|
||||
import { sanitizeText as _sanitizeText } from '../diagrams/common/common';
|
||||
import { setupGraphViewbox as _setupGraphViewbox } from '../setupGraphViewbox';
|
||||
import { addStylesForDiagram } from '../styles';
|
||||
import { DiagramDefinition, DiagramDetector } from './types';
|
||||
import { addDetector } from './detectType.js';
|
||||
import { log as _log, setLogLevel as _setLogLevel } from '../logger.js';
|
||||
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 { DiagramDefinition, DiagramDetector } from './types.js';
|
||||
import * as _commonDb from '../commonDb.js';
|
||||
import { parseDirective as _parseDirective } from '../directiveUtils.js';
|
||||
|
||||
/*
|
||||
Packaging and exposing resources for external diagrams so that they can import
|
||||
@@ -16,6 +18,11 @@ export const setLogLevel = _setLogLevel;
|
||||
export const getConfig = _getConfig;
|
||||
export const sanitizeText = (text: string) => _sanitizeText(text, getConfig());
|
||||
export const setupGraphViewbox = _setupGraphViewbox;
|
||||
export const getCommonDb = () => {
|
||||
return _commonDb;
|
||||
};
|
||||
export const parseDirective = (p: any, statement: string, context: string, type: string) =>
|
||||
_parseDirective(p, statement, context, type);
|
||||
|
||||
const diagrams: Record<string, DiagramDefinition> = {};
|
||||
export interface Detectors {
|
||||
@@ -46,7 +53,15 @@ export const registerDiagram = (
|
||||
addStylesForDiagram(id, diagram.styles);
|
||||
|
||||
if (diagram.injectUtils) {
|
||||
diagram.injectUtils(log, setLogLevel, getConfig, sanitizeText, setupGraphViewbox);
|
||||
diagram.injectUtils(
|
||||
log,
|
||||
setLogLevel,
|
||||
getConfig,
|
||||
sanitizeText,
|
||||
setupGraphViewbox,
|
||||
getCommonDb(),
|
||||
parseDirective
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,3 +71,9 @@ export const getDiagram = (name: string): DiagramDefinition => {
|
||||
}
|
||||
throw new Error(`Diagram ${name} not found.`);
|
||||
};
|
||||
|
||||
export class DiagramNotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(`Diagram ${message} not found.`);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { vi } from 'vitest';
|
||||
import { extractFrontMatter } from './frontmatter';
|
||||
import { extractFrontMatter } from './frontmatter.js';
|
||||
|
||||
const dbMock = () => ({ setDiagramTitle: vi.fn() });
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { DiagramDb } from './types';
|
||||
import { DiagramDb } from './types.js';
|
||||
// The "* as yaml" part is necessary for tree-shaking
|
||||
import * as yaml from 'js-yaml';
|
||||
|
||||
@@ -7,10 +7,12 @@ import * as yaml from 'js-yaml';
|
||||
// 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 = /^(?:---\s*[\r\n])(.*?)(?:[\r\n]---\s*[\r\n]+)/s;
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -33,6 +35,10 @@ export function extractFrontMatter(text: string, db: DiagramDb): string {
|
||||
db.setDiagramTitle?.(parsed.title);
|
||||
}
|
||||
|
||||
if (parsed?.displayMode) {
|
||||
db.setDisplayMode?.(parsed.displayMode);
|
||||
}
|
||||
|
||||
return text.slice(matches[0].length);
|
||||
} else {
|
||||
return text;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { MermaidConfig } from '../config.type';
|
||||
import { MermaidConfig } from '../config.type.js';
|
||||
|
||||
export interface InjectUtils {
|
||||
_log: any;
|
||||
@@ -6,6 +6,8 @@ export interface InjectUtils {
|
||||
_getConfig: any;
|
||||
_sanitizeText: any;
|
||||
_setupGraphViewbox: any;
|
||||
_commonDb: any;
|
||||
_parseDirective: any;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -14,6 +16,10 @@ export interface InjectUtils {
|
||||
export interface DiagramDb {
|
||||
clear?: () => void;
|
||||
setDiagramTitle?: (title: string) => void;
|
||||
setDisplayMode?: (title: string) => void;
|
||||
getAccTitle?: () => string;
|
||||
getAccDescription?: () => string;
|
||||
bindFunctions?: (element: Element) => void;
|
||||
}
|
||||
|
||||
export interface DiagramDefinition {
|
||||
@@ -27,7 +33,9 @@ export interface DiagramDefinition {
|
||||
_setLogLevel: InjectUtils['_setLogLevel'],
|
||||
_getConfig: InjectUtils['_getConfig'],
|
||||
_sanitizeText: InjectUtils['_sanitizeText'],
|
||||
_setupGraphViewbox: InjectUtils['_setupGraphViewbox']
|
||||
_setupGraphViewbox: InjectUtils['_setupGraphViewbox'],
|
||||
_commonDb: InjectUtils['_commonDb'],
|
||||
_parseDirective: InjectUtils['_parseDirective']
|
||||
) => void;
|
||||
}
|
||||
|
||||
|
68
packages/mermaid/src/diagram.spec.ts
Normal file
68
packages/mermaid/src/diagram.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { Diagram, getDiagramFromText } from './Diagram.js';
|
||||
import { addDetector } from './diagram-api/detectType.js';
|
||||
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
|
||||
|
||||
addDiagrams();
|
||||
|
||||
describe('diagram detection', () => {
|
||||
test('should detect inbuilt diagrams', async () => {
|
||||
const graph = (await getDiagramFromText('graph TD; A-->B')) as Diagram;
|
||||
expect(graph).toBeInstanceOf(Diagram);
|
||||
expect(graph.type).toBe('flowchart-v2');
|
||||
const sequence = (await getDiagramFromText(
|
||||
'sequenceDiagram; Alice->>+John: Hello John, how are you?'
|
||||
)) as Diagram;
|
||||
expect(sequence).toBeInstanceOf(Diagram);
|
||||
expect(sequence.type).toBe('sequence');
|
||||
});
|
||||
|
||||
test('should detect external diagrams', async () => {
|
||||
addDetector(
|
||||
'loki',
|
||||
(str) => str.startsWith('loki'),
|
||||
() =>
|
||||
Promise.resolve({
|
||||
id: 'loki',
|
||||
diagram: {
|
||||
db: {},
|
||||
parser: {
|
||||
parse: () => {
|
||||
// no-op
|
||||
},
|
||||
parser: {
|
||||
yy: {},
|
||||
},
|
||||
},
|
||||
renderer: {},
|
||||
styles: {},
|
||||
},
|
||||
})
|
||||
);
|
||||
const diagram = (await getDiagramFromText('loki TD; A-->B')) as Diagram;
|
||||
expect(diagram).toBeInstanceOf(Diagram);
|
||||
expect(diagram.type).toBe('loki');
|
||||
});
|
||||
|
||||
test('should throw the right error for incorrect diagram', async () => {
|
||||
await expect(getDiagramFromText('graph TD; A-->')).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Parse error on line 2:
|
||||
graph TD; A-->
|
||||
--------------^
|
||||
Expecting 'AMP', 'ALPHA', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'MINUS', 'BRKT', 'DOT', 'PUNCTUATION', 'UNICODE_TEXT', 'PLUS', 'EQUALS', 'MULT', 'UNDERSCORE', got 'EOF'"
|
||||
`);
|
||||
await expect(getDiagramFromText('sequenceDiagram; A-->B')).rejects
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Parse error on line 1:
|
||||
...quenceDiagram; A-->B
|
||||
-----------------------^
|
||||
Expecting 'TXT', got 'NEWLINE'"
|
||||
`);
|
||||
});
|
||||
|
||||
test('should throw the right error for unregistered diagrams', async () => {
|
||||
await expect(getDiagramFromText('thor TD; A-->B')).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
'"No diagram type detected matching given configuration for text: thor TD; A-->B"'
|
||||
);
|
||||
});
|
||||
});
|
@@ -1,7 +1,7 @@
|
||||
import mermaidAPI from '../../mermaidAPI';
|
||||
import * as configApi from '../../config';
|
||||
import { sanitizeText } from '../common/common';
|
||||
import { setAccTitle, getAccTitle, getAccDescription, setAccDescription } from '../../commonDb';
|
||||
import mermaidAPI from '../../mermaidAPI.js';
|
||||
import * as configApi from '../../config.js';
|
||||
import { sanitizeText } from '../common/common.js';
|
||||
import { setAccTitle, getAccTitle, getAccDescription, setAccDescription } from '../../commonDb.js';
|
||||
|
||||
let c4ShapeArray = [];
|
||||
let boundaryParseStack = [''];
|
||||
|
@@ -1,5 +1,20 @@
|
||||
import type { DiagramDetector } from '../../diagram-api/types';
|
||||
import type { ExternalDiagramDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
export const c4Detector: DiagramDetector = (txt) => {
|
||||
const id = 'c4';
|
||||
|
||||
const detector = (txt: string) => {
|
||||
return txt.match(/^\s*C4Context|C4Container|C4Component|C4Dynamic|C4Deployment/) !== null;
|
||||
};
|
||||
|
||||
const loader = async () => {
|
||||
const { diagram } = await import('./c4Diagram.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
|
17
packages/mermaid/src/diagrams/c4/c4Diagram.ts
Normal file
17
packages/mermaid/src/diagrams/c4/c4Diagram.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import c4Parser from './parser/c4Diagram.jison';
|
||||
import c4Db from './c4Db.js';
|
||||
import c4Renderer from './c4Renderer.js';
|
||||
import c4Styles from './styles.js';
|
||||
import { MermaidConfig } from '../../config.type.js';
|
||||
import { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser: c4Parser,
|
||||
db: c4Db,
|
||||
renderer: c4Renderer,
|
||||
styles: c4Styles,
|
||||
init: (cnf: MermaidConfig) => {
|
||||
c4Renderer.setConf(cnf.c4);
|
||||
},
|
||||
};
|
@@ -1,14 +1,13 @@
|
||||
import { select } from 'd3';
|
||||
import svgDraw from './svgDraw';
|
||||
import { log } from '../../logger';
|
||||
import { parser } from './parser/c4Diagram';
|
||||
import common from '../common/common';
|
||||
import c4Db from './c4Db';
|
||||
import * as configApi from '../../config';
|
||||
import assignWithDepth from '../../assignWithDepth';
|
||||
import { wrapLabel, calculateTextWidth, calculateTextHeight } from '../../utils';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox';
|
||||
import addSVGAccessibilityFields from '../../accessibility';
|
||||
import svgDraw from './svgDraw.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { parser } from './parser/c4Diagram.jison';
|
||||
import common from '../common/common.js';
|
||||
import c4Db from './c4Db.js';
|
||||
import * as configApi from '../../config.js';
|
||||
import assignWithDepth from '../../assignWithDepth.js';
|
||||
import { wrapLabel, calculateTextWidth, calculateTextHeight } from '../../utils.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
|
||||
let globalBoundaryMaxX = 0,
|
||||
globalBoundaryMaxY = 0;
|
||||
@@ -48,7 +47,7 @@ class Bounds {
|
||||
}
|
||||
|
||||
updateVal(obj, key, val, fun) {
|
||||
if (typeof obj[key] === 'undefined') {
|
||||
if (obj[key] === undefined) {
|
||||
obj[key] = val;
|
||||
} else {
|
||||
obj[key] = fun(val, obj[key]);
|
||||
@@ -177,12 +176,12 @@ function calcC4ShapeTextWH(textType, c4Shape, c4ShapeTextWrap, textConf, textLim
|
||||
let lineHeight = 0;
|
||||
c4Shape[textType].height = 0;
|
||||
c4Shape[textType].width = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
for (const line of lines) {
|
||||
c4Shape[textType].width = Math.max(
|
||||
calculateTextWidth(lines[i], textConf),
|
||||
calculateTextWidth(line, textConf),
|
||||
c4Shape[textType].width
|
||||
);
|
||||
lineHeight = calculateTextHeight(lines[i], textConf);
|
||||
lineHeight = calculateTextHeight(line, textConf);
|
||||
c4Shape[textType].height = c4Shape[textType].height + lineHeight;
|
||||
}
|
||||
// c4Shapes[textType].height = c4Shapes[textType].textLines * textConf.fontSize;
|
||||
@@ -212,9 +211,9 @@ export const drawC4ShapeArray = function (currentBounds, diagram, c4ShapeArray,
|
||||
// Upper Y is relative point
|
||||
let Y = 0;
|
||||
// Draw the c4ShapeArray
|
||||
for (let i = 0; i < c4ShapeKeys.length; i++) {
|
||||
for (const c4ShapeKey of c4ShapeKeys) {
|
||||
Y = 0;
|
||||
const c4Shape = c4ShapeArray[c4ShapeKeys[i]];
|
||||
const c4Shape = c4ShapeArray[c4ShapeKey];
|
||||
|
||||
// calc c4 shape type width and height
|
||||
|
||||
@@ -461,8 +460,7 @@ function drawInsideBoundary(
|
||||
// conf.width * conf.c4ShapeInRow + conf.c4ShapeMargin * conf.c4ShapeInRow * 2,
|
||||
// parentBounds.data.widthLimit / Math.min(conf.c4BoundaryInRow, currentBoundaries.length)
|
||||
// );
|
||||
for (let i = 0; i < currentBoundaries.length; i++) {
|
||||
let currentBoundary = currentBoundaries[i];
|
||||
for (let [i, currentBoundary] of currentBoundaries.entries()) {
|
||||
let Y = 0;
|
||||
currentBoundary.image = { width: 0, height: 0, Y: 0 };
|
||||
if (currentBoundary.sprite) {
|
||||
@@ -676,7 +674,6 @@ export const draw = function (_text, id, _version, diagObj) {
|
||||
(height + extraVertForTitle)
|
||||
);
|
||||
|
||||
addSVGAccessibilityFields(parser.yy, diagram, id);
|
||||
log.debug(`models:`, box);
|
||||
};
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import c4Db from '../c4Db';
|
||||
import c4Db from '../c4Db.js';
|
||||
import c4 from './c4Diagram.jison';
|
||||
import { setConfig } from '../../../config';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict',
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import c4Db from '../c4Db';
|
||||
import c4Db from '../c4Db.js';
|
||||
import c4 from './c4Diagram.jison';
|
||||
import { setConfig } from '../../../config';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict',
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import c4Db from '../c4Db';
|
||||
import c4Db from '../c4Db.js';
|
||||
import c4 from './c4Diagram.jison';
|
||||
import { setConfig } from '../../../config';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict',
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import c4Db from '../c4Db';
|
||||
import c4Db from '../c4Db.js';
|
||||
import c4 from './c4Diagram.jison';
|
||||
import { setConfig } from '../../../config';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict',
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import c4Db from '../c4Db';
|
||||
import c4Db from '../c4Db.js';
|
||||
import c4 from './c4Diagram.jison';
|
||||
import { setConfig } from '../../../config';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict',
|
||||
|
@@ -1,28 +1,9 @@
|
||||
import common from '../common/common';
|
||||
import common from '../common/common.js';
|
||||
import * as svgDrawCommon from '../common/svgDrawCommon';
|
||||
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;
|
||||
return svgDrawCommon.drawRect(elem, rectData);
|
||||
};
|
||||
|
||||
export const drawImage = function (elem, width, height, x, y, link) {
|
||||
@@ -35,184 +16,6 @@ export const drawImage = function (elem, width, height, x, y, 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);
|
||||
var sanitizedLink = sanitizeUrl(link);
|
||||
imageElem.attr('xlink:href', '#' + sanitizedLink);
|
||||
};
|
||||
|
||||
export const drawText = function (elem, textData) {
|
||||
let prevTextHeight = 0,
|
||||
textHeight = 0;
|
||||
const lines = textData.text.split(common.lineBreakRegex);
|
||||
|
||||
let textElems = [];
|
||||
let dy = 0;
|
||||
let yfunc = () => textData.y;
|
||||
if (
|
||||
typeof textData.valign !== 'undefined' &&
|
||||
typeof textData.textMargin !== 'undefined' &&
|
||||
textData.textMargin > 0
|
||||
) {
|
||||
switch (textData.valign) {
|
||||
case 'top':
|
||||
case 'start':
|
||||
yfunc = () => Math.round(textData.y + textData.textMargin);
|
||||
break;
|
||||
case 'middle':
|
||||
case 'center':
|
||||
yfunc = () =>
|
||||
Math.round(textData.y + (prevTextHeight + textHeight + textData.textMargin) / 2);
|
||||
break;
|
||||
case 'bottom':
|
||||
case 'end':
|
||||
yfunc = () =>
|
||||
Math.round(
|
||||
textData.y +
|
||||
(prevTextHeight + textHeight + 2 * textData.textMargin) -
|
||||
textData.textMargin
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (
|
||||
typeof textData.anchor !== 'undefined' &&
|
||||
typeof textData.textMargin !== 'undefined' &&
|
||||
typeof textData.width !== 'undefined'
|
||||
) {
|
||||
switch (textData.anchor) {
|
||||
case 'left':
|
||||
case 'start':
|
||||
textData.x = Math.round(textData.x + textData.textMargin);
|
||||
textData.anchor = 'start';
|
||||
textData.dominantBaseline = 'text-after-edge';
|
||||
textData.alignmentBaseline = 'middle';
|
||||
break;
|
||||
case 'middle':
|
||||
case 'center':
|
||||
textData.x = Math.round(textData.x + textData.width / 2);
|
||||
textData.anchor = 'middle';
|
||||
textData.dominantBaseline = 'middle';
|
||||
textData.alignmentBaseline = 'middle';
|
||||
break;
|
||||
case 'right':
|
||||
case 'end':
|
||||
textData.x = Math.round(textData.x + textData.width - textData.textMargin);
|
||||
textData.anchor = 'end';
|
||||
textData.dominantBaseline = 'text-before-edge';
|
||||
textData.alignmentBaseline = 'middle';
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i];
|
||||
if (
|
||||
typeof textData.textMargin !== 'undefined' &&
|
||||
textData.textMargin === 0 &&
|
||||
typeof textData.fontSize !== 'undefined'
|
||||
) {
|
||||
dy = i * textData.fontSize;
|
||||
}
|
||||
|
||||
const textElem = elem.append('text');
|
||||
textElem.attr('x', textData.x);
|
||||
textElem.attr('y', yfunc());
|
||||
if (typeof textData.anchor !== 'undefined') {
|
||||
textElem
|
||||
.attr('text-anchor', textData.anchor)
|
||||
.attr('dominant-baseline', textData.dominantBaseline)
|
||||
.attr('alignment-baseline', textData.alignmentBaseline);
|
||||
}
|
||||
if (typeof textData.fontFamily !== 'undefined') {
|
||||
textElem.style('font-family', textData.fontFamily);
|
||||
}
|
||||
if (typeof textData.fontSize !== 'undefined') {
|
||||
textElem.style('font-size', textData.fontSize);
|
||||
}
|
||||
if (typeof textData.fontWeight !== 'undefined') {
|
||||
textElem.style('font-weight', textData.fontWeight);
|
||||
}
|
||||
if (typeof textData.fill !== 'undefined') {
|
||||
textElem.attr('fill', textData.fill);
|
||||
}
|
||||
if (typeof textData.class !== 'undefined') {
|
||||
textElem.attr('class', textData.class);
|
||||
}
|
||||
if (typeof textData.dy !== 'undefined') {
|
||||
textElem.attr('dy', textData.dy);
|
||||
} else if (dy !== 0) {
|
||||
textElem.attr('dy', dy);
|
||||
}
|
||||
|
||||
if (textData.tspan) {
|
||||
const span = textElem.append('tspan');
|
||||
span.attr('x', textData.x);
|
||||
if (typeof textData.fill !== 'undefined') {
|
||||
span.attr('fill', textData.fill);
|
||||
}
|
||||
span.text(line);
|
||||
} else {
|
||||
textElem.text(line);
|
||||
}
|
||||
if (
|
||||
typeof textData.valign !== 'undefined' &&
|
||||
typeof textData.textMargin !== 'undefined' &&
|
||||
textData.textMargin > 0
|
||||
) {
|
||||
textHeight += (textElem._groups || textElem)[0][0].getBBox().height;
|
||||
prevTextHeight = textHeight;
|
||||
}
|
||||
|
||||
textElems.push(textElem);
|
||||
}
|
||||
|
||||
return textElems;
|
||||
};
|
||||
|
||||
export const drawLabel = function (elem, txtObject) {
|
||||
/**
|
||||
* @param {any} x
|
||||
* @param {any} y
|
||||
* @param {any} width
|
||||
* @param {any} height
|
||||
* @param {any} cut
|
||||
* @returns {any}
|
||||
*/
|
||||
function genPoints(x, y, width, height, cut) {
|
||||
return (
|
||||
x +
|
||||
',' +
|
||||
y +
|
||||
' ' +
|
||||
(x + width) +
|
||||
',' +
|
||||
y +
|
||||
' ' +
|
||||
(x + width) +
|
||||
',' +
|
||||
(y + height - cut) +
|
||||
' ' +
|
||||
(x + width - cut * 1.2) +
|
||||
',' +
|
||||
(y + height) +
|
||||
' ' +
|
||||
x +
|
||||
',' +
|
||||
(y + height)
|
||||
);
|
||||
}
|
||||
const polygon = elem.append('polygon');
|
||||
polygon.attr('points', genPoints(txtObject.x, txtObject.y, txtObject.width, txtObject.height, 7));
|
||||
polygon.attr('class', 'labelBox');
|
||||
|
||||
txtObject.y = txtObject.y + txtObject.height / 2;
|
||||
|
||||
drawText(elem, txtObject);
|
||||
return polygon;
|
||||
};
|
||||
|
||||
export const drawRels = (elem, rels, conf) => {
|
||||
const relsElem = elem.append('g');
|
||||
let i = 0;
|
||||
@@ -412,9 +215,10 @@ export const drawC4Shape = function (elem, c4Shape, conf) {
|
||||
const c4ShapeElem = elem.append('g');
|
||||
c4ShapeElem.attr('class', 'person-man');
|
||||
|
||||
// <rect fill="#08427B" height="119.2188" rx="2.5" ry="2.5" style="stroke:#073B6F;stroke-width:0.5;" width="110" x="120" y="7"/>
|
||||
// <rect fill="#08427B" height="119.2188" rx="2.5" ry="2.5" stroke="#073B6F" stroke-width="0.5" width="110" x="120" y="7"/>
|
||||
// draw rect of c4Shape
|
||||
const rect = getNoteRect();
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
|
||||
switch (c4Shape.typeC4Shape.text) {
|
||||
case 'person':
|
||||
case 'external_person':
|
||||
@@ -429,9 +233,10 @@ export const drawC4Shape = function (elem, c4Shape, conf) {
|
||||
rect.fill = fillColor;
|
||||
rect.width = c4Shape.width;
|
||||
rect.height = c4Shape.height;
|
||||
rect.style = 'stroke:' + strokeColor + ';stroke-width:0.5;';
|
||||
rect.stroke = strokeColor;
|
||||
rect.rx = 2.5;
|
||||
rect.ry = 2.5;
|
||||
rect.attrs = { 'stroke-width': 0.5 };
|
||||
drawRect(c4ShapeElem, rect);
|
||||
break;
|
||||
case 'system_db':
|
||||
@@ -549,12 +354,12 @@ export const drawC4Shape = function (elem, c4Shape, conf) {
|
||||
textFontConf = conf[c4Shape.typeC4Shape.text + 'Font']();
|
||||
textFontConf.fontColor = fontColor;
|
||||
|
||||
if (c4Shape.thchn && c4Shape.thchn.text !== '') {
|
||||
if (c4Shape.techn && c4Shape.techn?.text !== '') {
|
||||
_drawTextCandidateFunc(conf)(
|
||||
c4Shape.thchn.text,
|
||||
c4Shape.techn.text,
|
||||
c4ShapeElem,
|
||||
c4Shape.x,
|
||||
c4Shape.y + c4Shape.thchn.Y,
|
||||
c4Shape.y + c4Shape.techn.Y,
|
||||
c4Shape.width,
|
||||
c4Shape.height,
|
||||
{ fill: fontColor, 'font-style': 'italic' },
|
||||
@@ -656,6 +461,7 @@ export const insertArrowHead = function (elem) {
|
||||
.append('path')
|
||||
.attr('d', 'M 0 0 L 10 5 L 0 10 z'); // this is actual shape for arrowhead
|
||||
};
|
||||
|
||||
export const insertArrowEnd = function (elem) {
|
||||
elem
|
||||
.append('defs')
|
||||
@@ -670,6 +476,7 @@ export const insertArrowEnd = function (elem) {
|
||||
.append('path')
|
||||
.attr('d', 'M 10 0 L 0 5 L 10 10 z'); // this is actual shape for arrowhead
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup arrow head and define the marker. The result is appended to the svg.
|
||||
*
|
||||
@@ -688,6 +495,7 @@ export const insertArrowFilledHead = function (elem) {
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup node number. The result is appended to the svg.
|
||||
*
|
||||
@@ -709,6 +517,7 @@ export const insertDynamicNumber = function (elem) {
|
||||
.attr('r', 6);
|
||||
// .style("fill", '#f00');
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup arrow head and define the marker. The result is appended to the svg.
|
||||
*
|
||||
@@ -745,37 +554,6 @@ export const insertArrowCrossHead = function (elem) {
|
||||
// this is actual shape for arrowhead
|
||||
};
|
||||
|
||||
export const getTextObj = function () {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
fill: undefined,
|
||||
anchor: undefined,
|
||||
style: '#666',
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
textMargin: 0,
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
tspan: true,
|
||||
valign: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const getNoteRect = function () {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
fill: '#EDF2AE',
|
||||
stroke: '#666',
|
||||
width: 100,
|
||||
anchor: 'start',
|
||||
height: 100,
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
};
|
||||
};
|
||||
|
||||
const getC4ShapeFont = (cnf, typeC4Shape) => {
|
||||
return {
|
||||
fontFamily: cnf[typeC4Shape + 'FontFamily'],
|
||||
@@ -896,13 +674,10 @@ const _drawTextCandidateFunc = (function () {
|
||||
|
||||
export default {
|
||||
drawRect,
|
||||
drawText,
|
||||
drawLabel,
|
||||
drawBoundary,
|
||||
drawC4Shape,
|
||||
drawRels,
|
||||
drawImage,
|
||||
drawEmbeddedImage,
|
||||
insertArrowHead,
|
||||
insertArrowEnd,
|
||||
insertArrowFilledHead,
|
||||
@@ -911,7 +686,4 @@ export default {
|
||||
insertDatabaseIcon,
|
||||
insertComputerIcon,
|
||||
insertClockIcon,
|
||||
getTextObj,
|
||||
getNoteRect,
|
||||
sanitizeUrl,
|
||||
};
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { select } from 'd3';
|
||||
import { log } from '../../logger';
|
||||
import * as configApi from '../../config';
|
||||
import common from '../common/common';
|
||||
import utils from '../../utils';
|
||||
import mermaidAPI from '../../mermaidAPI';
|
||||
// @ts-expect-error - d3 types issue
|
||||
import { select, Selection } from 'd3';
|
||||
import { log } from '../../logger.js';
|
||||
import * as configApi from '../../config.js';
|
||||
import common from '../common/common.js';
|
||||
import utils from '../../utils.js';
|
||||
import mermaidAPI from '../../mermaidAPI.js';
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
@@ -12,59 +13,79 @@ import {
|
||||
clear as commonClear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../../commonDb';
|
||||
} from '../../commonDb.js';
|
||||
import {
|
||||
ClassRelation,
|
||||
ClassNode,
|
||||
ClassNote,
|
||||
ClassMap,
|
||||
NamespaceMap,
|
||||
NamespaceNode,
|
||||
} from './classTypes.js';
|
||||
|
||||
const MERMAID_DOM_ID_PREFIX = 'classid-';
|
||||
const MERMAID_DOM_ID_PREFIX = 'classId-';
|
||||
|
||||
let relations = [];
|
||||
let classes = {};
|
||||
let notes = [];
|
||||
let relations: ClassRelation[] = [];
|
||||
let classes: ClassMap = {};
|
||||
let notes: ClassNote[] = [];
|
||||
let classCounter = 0;
|
||||
let namespaces: NamespaceMap = {};
|
||||
let namespaceCounter = 0;
|
||||
|
||||
let funs = [];
|
||||
let functions: any[] = [];
|
||||
|
||||
const sanitizeText = (txt) => common.sanitizeText(txt, configApi.getConfig());
|
||||
const sanitizeText = (txt: string) => common.sanitizeText(txt, configApi.getConfig());
|
||||
|
||||
export const parseDirective = function (statement, context, type) {
|
||||
export const parseDirective = function (statement: string, context: string, type: string) {
|
||||
// @ts-ignore Don't wanna mess it up
|
||||
mermaidAPI.parseDirective(this, statement, context, type);
|
||||
};
|
||||
|
||||
const splitClassNameAndType = function (id) {
|
||||
const splitClassNameAndType = function (id: string) {
|
||||
let genericType = '';
|
||||
let className = id;
|
||||
|
||||
if (id.indexOf('~') > 0) {
|
||||
let split = id.split('~');
|
||||
className = split[0];
|
||||
|
||||
genericType = common.sanitizeText(split[1], configApi.getConfig());
|
||||
const split = id.split('~');
|
||||
className = sanitizeText(split[0]);
|
||||
genericType = sanitizeText(split[1]);
|
||||
}
|
||||
|
||||
return { className: className, type: genericType };
|
||||
};
|
||||
|
||||
export const setClassLabel = function (id: string, label: string) {
|
||||
if (label) {
|
||||
label = sanitizeText(label);
|
||||
}
|
||||
|
||||
const { className } = splitClassNameAndType(id);
|
||||
classes[className].label = label;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function called by parser when a node definition has been found.
|
||||
*
|
||||
* @param id
|
||||
* @param id - Id of the class to add
|
||||
* @public
|
||||
*/
|
||||
export const addClass = function (id) {
|
||||
let classId = splitClassNameAndType(id);
|
||||
export const addClass = function (id: string) {
|
||||
const classId = splitClassNameAndType(id);
|
||||
// Only add class if not exists
|
||||
if (typeof classes[classId.className] !== 'undefined') {
|
||||
if (classes[classId.className] !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
classes[classId.className] = {
|
||||
id: classId.className,
|
||||
type: classId.type,
|
||||
label: classId.className,
|
||||
cssClasses: [],
|
||||
methods: [],
|
||||
members: [],
|
||||
annotations: [],
|
||||
domId: MERMAID_DOM_ID_PREFIX + classId.className + '-' + classCounter,
|
||||
};
|
||||
} as ClassNode;
|
||||
|
||||
classCounter++;
|
||||
};
|
||||
@@ -72,35 +93,36 @@ export const addClass = function (id) {
|
||||
/**
|
||||
* Function to lookup domId from id in the graph definition.
|
||||
*
|
||||
* @param id
|
||||
* @param id - class ID to lookup
|
||||
* @public
|
||||
*/
|
||||
export const lookUpDomId = function (id) {
|
||||
const classKeys = Object.keys(classes);
|
||||
for (let i = 0; i < classKeys.length; i++) {
|
||||
if (classes[classKeys[i]].id === id) {
|
||||
return classes[classKeys[i]].domId;
|
||||
}
|
||||
export const lookUpDomId = function (id: string): string {
|
||||
if (id in classes) {
|
||||
return classes[id].domId;
|
||||
}
|
||||
throw new Error('Class not found: ' + id);
|
||||
};
|
||||
|
||||
export const clear = function () {
|
||||
relations = [];
|
||||
classes = {};
|
||||
notes = [];
|
||||
funs = [];
|
||||
funs.push(setupToolTips);
|
||||
functions = [];
|
||||
functions.push(setupToolTips);
|
||||
namespaces = {};
|
||||
namespaceCounter = 0;
|
||||
commonClear();
|
||||
};
|
||||
|
||||
export const getClass = function (id) {
|
||||
export const getClass = function (id: string) {
|
||||
return classes[id];
|
||||
};
|
||||
|
||||
export const getClasses = function () {
|
||||
return classes;
|
||||
};
|
||||
|
||||
export const getRelations = function () {
|
||||
export const getRelations = function (): ClassRelation[] {
|
||||
return relations;
|
||||
};
|
||||
|
||||
@@ -108,7 +130,7 @@ export const getNotes = function () {
|
||||
return notes;
|
||||
};
|
||||
|
||||
export const addRelation = function (relation) {
|
||||
export const addRelation = function (relation: ClassRelation) {
|
||||
log.debug('Adding relation: ' + JSON.stringify(relation));
|
||||
addClass(relation.id1);
|
||||
addClass(relation.id2);
|
||||
@@ -133,11 +155,11 @@ export const addRelation = function (relation) {
|
||||
* Adds an annotation to the specified class Annotations mark special properties of the given type
|
||||
* (like 'interface' or 'service')
|
||||
*
|
||||
* @param className The class name
|
||||
* @param annotation The name of the annotation without any brackets
|
||||
* @param className - The class name
|
||||
* @param annotation - The name of the annotation without any brackets
|
||||
* @public
|
||||
*/
|
||||
export const addAnnotation = function (className, annotation) {
|
||||
export const addAnnotation = function (className: string, annotation: string) {
|
||||
const validatedClassName = splitClassNameAndType(className).className;
|
||||
classes[validatedClassName].annotations.push(annotation);
|
||||
};
|
||||
@@ -145,13 +167,13 @@ export const addAnnotation = function (className, annotation) {
|
||||
/**
|
||||
* Adds a member to the specified class
|
||||
*
|
||||
* @param className The class name
|
||||
* @param member The full name of the member. If the member is enclosed in <<brackets>> it is
|
||||
* @param className - The class name
|
||||
* @param member - The full name of the member. If the member is enclosed in `<<brackets>>` it is
|
||||
* treated as an annotation If the member is ending with a closing bracket ) it is treated as a
|
||||
* method Otherwise the member will be treated as a normal property
|
||||
* @public
|
||||
*/
|
||||
export const addMember = function (className, member) {
|
||||
export const addMember = function (className: string, member: string) {
|
||||
const validatedClassName = splitClassNameAndType(className).className;
|
||||
const theClass = classes[validatedClassName];
|
||||
|
||||
@@ -160,10 +182,10 @@ export const addMember = function (className, member) {
|
||||
const memberString = member.trim();
|
||||
|
||||
if (memberString.startsWith('<<') && memberString.endsWith('>>')) {
|
||||
// Remove leading and trailing brackets
|
||||
// theClass.annotations.push(memberString.substring(2, memberString.length - 2));
|
||||
// its an annotation
|
||||
theClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2)));
|
||||
} else if (memberString.indexOf(')') > 0) {
|
||||
//its a method
|
||||
theClass.methods.push(sanitizeText(memberString));
|
||||
} else if (memberString) {
|
||||
theClass.members.push(sanitizeText(memberString));
|
||||
@@ -171,14 +193,14 @@ export const addMember = function (className, member) {
|
||||
}
|
||||
};
|
||||
|
||||
export const addMembers = function (className, members) {
|
||||
export const addMembers = function (className: string, members: string[]) {
|
||||
if (Array.isArray(members)) {
|
||||
members.reverse();
|
||||
members.forEach((member) => addMember(className, member));
|
||||
}
|
||||
};
|
||||
|
||||
export const addNote = function (text, className) {
|
||||
export const addNote = function (text: string, className: string) {
|
||||
const note = {
|
||||
id: `note${notes.length}`,
|
||||
class: className,
|
||||
@@ -187,27 +209,26 @@ export const addNote = function (text, className) {
|
||||
notes.push(note);
|
||||
};
|
||||
|
||||
export const cleanupLabel = function (label) {
|
||||
if (label.substring(0, 1) === ':') {
|
||||
return common.sanitizeText(label.substr(1).trim(), configApi.getConfig());
|
||||
} else {
|
||||
return sanitizeText(label.trim());
|
||||
export const cleanupLabel = function (label: string) {
|
||||
if (label.startsWith(':')) {
|
||||
label = label.substring(1);
|
||||
}
|
||||
return sanitizeText(label.trim());
|
||||
};
|
||||
|
||||
/**
|
||||
* Called by parser when a special node is found, e.g. a clickable element.
|
||||
*
|
||||
* @param ids Comma separated list of ids
|
||||
* @param className Class to add
|
||||
* @param ids - Comma separated list of ids
|
||||
* @param className - Class to add
|
||||
*/
|
||||
export const setCssClass = function (ids, className) {
|
||||
export const setCssClass = function (ids: string, className: string) {
|
||||
ids.split(',').forEach(function (_id) {
|
||||
let id = _id;
|
||||
if (_id[0].match(/\d/)) {
|
||||
id = MERMAID_DOM_ID_PREFIX + id;
|
||||
}
|
||||
if (typeof classes[id] !== 'undefined') {
|
||||
if (classes[id] !== undefined) {
|
||||
classes[id].cssClasses.push(className);
|
||||
}
|
||||
});
|
||||
@@ -216,35 +237,39 @@ export const setCssClass = function (ids, className) {
|
||||
/**
|
||||
* Called by parser when a tooltip is found, e.g. a clickable element.
|
||||
*
|
||||
* @param ids Comma separated list of ids
|
||||
* @param tooltip Tooltip to add
|
||||
* @param ids - Comma separated list of ids
|
||||
* @param tooltip - Tooltip to add
|
||||
*/
|
||||
const setTooltip = function (ids, tooltip) {
|
||||
const config = configApi.getConfig();
|
||||
const setTooltip = function (ids: string, tooltip?: string) {
|
||||
ids.split(',').forEach(function (id) {
|
||||
if (typeof tooltip !== 'undefined') {
|
||||
classes[id].tooltip = common.sanitizeText(tooltip, config);
|
||||
if (tooltip !== undefined) {
|
||||
classes[id].tooltip = sanitizeText(tooltip);
|
||||
}
|
||||
});
|
||||
};
|
||||
export const getTooltip = function (id) {
|
||||
|
||||
export const getTooltip = function (id: string, namespace?: string) {
|
||||
if (namespace) {
|
||||
return namespaces[namespace].classes[id].tooltip;
|
||||
}
|
||||
|
||||
return classes[id].tooltip;
|
||||
};
|
||||
/**
|
||||
* Called by parser when a link is found. Adds the URL to the vertex data.
|
||||
*
|
||||
* @param ids Comma separated list of ids
|
||||
* @param linkStr URL to create a link for
|
||||
* @param target Target of the link, _blank by default as originally defined in the svgDraw.js file
|
||||
* @param ids - Comma separated list of ids
|
||||
* @param linkStr - URL to create a link for
|
||||
* @param target - Target of the link, _blank by default as originally defined in the svgDraw.js file
|
||||
*/
|
||||
export const setLink = function (ids, linkStr, target) {
|
||||
export const setLink = function (ids: string, linkStr: string, target: string) {
|
||||
const config = configApi.getConfig();
|
||||
ids.split(',').forEach(function (_id) {
|
||||
let id = _id;
|
||||
if (_id[0].match(/\d/)) {
|
||||
id = MERMAID_DOM_ID_PREFIX + id;
|
||||
}
|
||||
if (typeof classes[id] !== 'undefined') {
|
||||
if (classes[id] !== undefined) {
|
||||
classes[id].link = utils.formatUrl(linkStr, config);
|
||||
if (config.securityLevel === 'sandbox') {
|
||||
classes[id].linkTarget = '_top';
|
||||
@@ -261,11 +286,11 @@ export const setLink = function (ids, linkStr, target) {
|
||||
/**
|
||||
* Called by parser when a click definition is found. Registers an event handler.
|
||||
*
|
||||
* @param ids Comma separated list of ids
|
||||
* @param functionName Function to be called on click
|
||||
* @param functionArgs Function args the function should be called with
|
||||
* @param ids - Comma separated list of ids
|
||||
* @param functionName - Function to be called on click
|
||||
* @param functionArgs - Function args the function should be called with
|
||||
*/
|
||||
export const setClickEvent = function (ids, functionName, functionArgs) {
|
||||
export const setClickEvent = function (ids: string, functionName: string, functionArgs: string) {
|
||||
ids.split(',').forEach(function (id) {
|
||||
setClickFunc(id, functionName, functionArgs);
|
||||
classes[id].haveCallback = true;
|
||||
@@ -273,19 +298,19 @@ export const setClickEvent = function (ids, functionName, functionArgs) {
|
||||
setCssClass(ids, 'clickable');
|
||||
};
|
||||
|
||||
const setClickFunc = function (domId, functionName, functionArgs) {
|
||||
const setClickFunc = function (domId: string, functionName: string, functionArgs: string) {
|
||||
const config = configApi.getConfig();
|
||||
let id = domId;
|
||||
let elemId = lookUpDomId(id);
|
||||
|
||||
if (config.securityLevel !== 'loose') {
|
||||
return;
|
||||
}
|
||||
if (typeof functionName === 'undefined') {
|
||||
if (functionName === undefined) {
|
||||
return;
|
||||
}
|
||||
if (typeof classes[id] !== 'undefined') {
|
||||
let argList = [];
|
||||
|
||||
const id = domId;
|
||||
if (classes[id] !== undefined) {
|
||||
const elemId = lookUpDomId(id);
|
||||
let argList: string[] = [];
|
||||
if (typeof functionArgs === 'string') {
|
||||
/* Splits functionArgs by ',', ignoring all ',' in double quoted strings */
|
||||
argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
|
||||
@@ -305,7 +330,7 @@ const setClickFunc = function (domId, functionName, functionArgs) {
|
||||
argList.push(elemId);
|
||||
}
|
||||
|
||||
funs.push(function () {
|
||||
functions.push(function () {
|
||||
const elem = document.querySelector(`[id="${elemId}"]`);
|
||||
if (elem !== null) {
|
||||
elem.addEventListener(
|
||||
@@ -320,8 +345,8 @@ const setClickFunc = function (domId, functionName, functionArgs) {
|
||||
}
|
||||
};
|
||||
|
||||
export const bindFunctions = function (element) {
|
||||
funs.forEach(function (fun) {
|
||||
export const bindFunctions = function (element: Element) {
|
||||
functions.forEach(function (fun) {
|
||||
fun(element);
|
||||
});
|
||||
};
|
||||
@@ -339,8 +364,10 @@ export const relationType = {
|
||||
LOLLIPOP: 4,
|
||||
};
|
||||
|
||||
const setupToolTips = function (element) {
|
||||
let tooltipElem = select('.mermaidTooltip');
|
||||
const setupToolTips = function (element: Element) {
|
||||
let tooltipElem: Selection<HTMLDivElement, unknown, HTMLElement, unknown> =
|
||||
select('.mermaidTooltip');
|
||||
// @ts-ignore - _groups is a dynamic property
|
||||
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
|
||||
tooltipElem = select('body').append('div').attr('class', 'mermaidTooltip').style('opacity', 0);
|
||||
}
|
||||
@@ -350,12 +377,14 @@ const setupToolTips = function (element) {
|
||||
const nodes = svg.selectAll('g.node');
|
||||
nodes
|
||||
.on('mouseover', function () {
|
||||
// @ts-expect-error - select is not part of the d3 type definition
|
||||
const el = select(this);
|
||||
const title = el.attr('title');
|
||||
// Dont try to draw a tooltip if no data is provided
|
||||
// Don't try to draw a tooltip if no data is provided
|
||||
if (title === null) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore - getBoundingClientRect is not part of the d3 type definition
|
||||
const rect = this.getBoundingClientRect();
|
||||
|
||||
tooltipElem.transition().duration(200).style('opacity', '.9');
|
||||
@@ -368,18 +397,65 @@ const setupToolTips = function (element) {
|
||||
})
|
||||
.on('mouseout', function () {
|
||||
tooltipElem.transition().duration(500).style('opacity', 0);
|
||||
// @ts-expect-error - select is not part of the d3 type definition
|
||||
const el = select(this);
|
||||
el.classed('hover', false);
|
||||
});
|
||||
};
|
||||
funs.push(setupToolTips);
|
||||
functions.push(setupToolTips);
|
||||
|
||||
let direction = 'TB';
|
||||
const getDirection = () => direction;
|
||||
const setDirection = (dir) => {
|
||||
const setDirection = (dir: string) => {
|
||||
direction = dir;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function called by parser when a namespace definition has been found.
|
||||
*
|
||||
* @param id - Id of the namespace to add
|
||||
* @public
|
||||
*/
|
||||
export const addNamespace = function (id: string) {
|
||||
if (namespaces[id] !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
namespaces[id] = {
|
||||
id: id,
|
||||
classes: {},
|
||||
children: {},
|
||||
domId: MERMAID_DOM_ID_PREFIX + id + '-' + namespaceCounter,
|
||||
} as NamespaceNode;
|
||||
|
||||
namespaceCounter++;
|
||||
};
|
||||
|
||||
const getNamespace = function (name: string): NamespaceNode {
|
||||
return namespaces[name];
|
||||
};
|
||||
|
||||
const getNamespaces = function (): NamespaceMap {
|
||||
return namespaces;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function called by parser when a namespace definition has been found.
|
||||
*
|
||||
* @param id - Id of the namespace to add
|
||||
* @param classNames - Ids of the class to add
|
||||
* @public
|
||||
*/
|
||||
export const addClassesToNamespace = function (id: string, classNames: string[]) {
|
||||
if (namespaces[id] !== undefined) {
|
||||
classNames.map((className) => {
|
||||
namespaces[id].classes[className] = classes[className];
|
||||
delete classes[className];
|
||||
classCounter--;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
parseDirective,
|
||||
setAccTitle,
|
||||
@@ -412,4 +488,9 @@ export default {
|
||||
lookUpDomId,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
setClassLabel,
|
||||
addNamespace,
|
||||
addClassesToNamespace,
|
||||
getNamespace,
|
||||
getNamespaces,
|
||||
};
|
@@ -1,6 +1,8 @@
|
||||
import type { DiagramDetector } from '../../diagram-api/types';
|
||||
import type { DiagramDetector, ExternalDiagramDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
export const classDetectorV2: DiagramDetector = (txt, config) => {
|
||||
const id = 'classDiagram';
|
||||
|
||||
const detector: DiagramDetector = (txt, config) => {
|
||||
// If we have configured to use dagre-wrapper then we should return true in this function for classDiagram code thus making it use the new class diagram
|
||||
if (
|
||||
txt.match(/^\s*classDiagram/) !== null &&
|
||||
@@ -11,3 +13,16 @@ export const classDetectorV2: DiagramDetector = (txt, config) => {
|
||||
// We have not opted to use the new renderer so we should return true if we detect a class diagram
|
||||
return txt.match(/^\s*classDiagram-v2/) !== null;
|
||||
};
|
||||
|
||||
const loader = async () => {
|
||||
const { diagram } = await import('./classDiagram-v2.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import type { DiagramDetector } from '../../diagram-api/types';
|
||||
import type { DiagramDetector, ExternalDiagramDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
export const classDetector: DiagramDetector = (txt, config) => {
|
||||
const id = 'class';
|
||||
|
||||
const detector: DiagramDetector = (txt, config) => {
|
||||
// If we have configured to use dagre-wrapper then we should never return true in this function
|
||||
if (config?.class?.defaultRenderer === 'dagre-wrapper') {
|
||||
return false;
|
||||
@@ -8,3 +10,16 @@ export const classDetector: DiagramDetector = (txt, config) => {
|
||||
// We have not opted to use the new renderer so we should return true if we detect a class diagram
|
||||
return txt.match(/^\s*classDiagram/) !== null;
|
||||
};
|
||||
|
||||
const loader = async () => {
|
||||
const { diagram } = await import('./classDiagram.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { parser } from './parser/classDiagram';
|
||||
import classDb from './classDb';
|
||||
import { parser } from './parser/classDiagram.jison';
|
||||
import classDb from './classDb.js';
|
||||
|
||||
describe('class diagram, ', function () {
|
||||
describe('when parsing data from a classDiagram it', function () {
|
||||
|
20
packages/mermaid/src/diagrams/class/classDiagram-v2.ts
Normal file
20
packages/mermaid/src/diagrams/class/classDiagram-v2.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import parser from './parser/classDiagram.jison';
|
||||
import db from './classDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './classRenderer-v2.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
renderer,
|
||||
styles,
|
||||
init: (cnf) => {
|
||||
if (!cnf.class) {
|
||||
cnf.class = {};
|
||||
}
|
||||
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
db.clear();
|
||||
},
|
||||
};
|
@@ -1,949 +0,0 @@
|
||||
import { parser } from './parser/classDiagram';
|
||||
import classDb from './classDb';
|
||||
import { vi } from 'vitest';
|
||||
const spyOn = vi.spyOn;
|
||||
|
||||
describe('class diagram, ', function () {
|
||||
describe('when parsing an info graph it', function () {
|
||||
beforeEach(function () {
|
||||
parser.yy = classDb;
|
||||
});
|
||||
|
||||
it('should handle backquoted class names', function () {
|
||||
const str = 'classDiagram\n' + 'class `Car`';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
it.skip('should handle a leading newline axa', function () {
|
||||
const str = '\nclassDiagram\n' + 'class Car';
|
||||
|
||||
try {
|
||||
parser.parse(str);
|
||||
// Fail test if above expression doesn't throw anything.
|
||||
} catch (e) {
|
||||
expect(true).toBe(false);
|
||||
}
|
||||
});
|
||||
it('should handle relation definitions', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'Class01 <|-- Class02\n' +
|
||||
'Class03 *-- Class04\n' +
|
||||
'Class05 o-- Class06\n' +
|
||||
'Class07 .. Class08\n' +
|
||||
'Class09 -- Class1';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle backquoted relation definitions', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'`Class01` <|-- Class02\n' +
|
||||
'Class03 *-- Class04\n' +
|
||||
'Class05 o-- Class06\n' +
|
||||
'Class07 .. Class08\n' +
|
||||
'Class09 -- Class1';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle relation definition of different types and directions', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'Class11 <|.. Class12\n' +
|
||||
'Class13 --> Class14\n' +
|
||||
'Class15 ..> Class16\n' +
|
||||
'Class17 ..|> Class18\n' +
|
||||
'Class19 <--* Class20';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle cardinality and labels', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'Class01 "1" *-- "many" Class02 : contains\n' +
|
||||
'Class03 o-- Class04 : aggregation\n' +
|
||||
'Class05 --> "1" Class06';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle visibility for methods and members', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class TestClass\n' +
|
||||
'TestClass : -int privateMember\n' +
|
||||
'TestClass : +int publicMember\n' +
|
||||
'TestClass : #int protectedMember\n' +
|
||||
'TestClass : -privateMethod()\n' +
|
||||
'TestClass : +publicMethod()\n' +
|
||||
'TestClass : #protectedMethod()\n';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle generic class', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Car~T~\n' +
|
||||
'Driver -- Car : drives >\n' +
|
||||
'Car *-- Wheel : have 4 >\n' +
|
||||
'Car -- Person : < owns';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle generic class with a literal name', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class `Car`~T~\n' +
|
||||
'Driver -- `Car` : drives >\n' +
|
||||
'`Car` *-- Wheel : have 4 >\n' +
|
||||
'`Car` -- Person : < owns';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should break when another `{`is encountered before closing the first one while defining generic class with brackets', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Dummy_Class~T~ {\n' +
|
||||
'String data\n' +
|
||||
' void methods()\n' +
|
||||
'}\n' +
|
||||
'\n' +
|
||||
'class Dummy_Class {\n' +
|
||||
'class Flight {\n' +
|
||||
' flightNumber : Integer\n' +
|
||||
' departureTime : Date\n' +
|
||||
'}';
|
||||
let testPassed = false;
|
||||
try {
|
||||
parser.parse(str);
|
||||
} catch (error) {
|
||||
testPassed = true;
|
||||
}
|
||||
expect(testPassed).toBe(true);
|
||||
});
|
||||
|
||||
it('should break when EOF is encountered before closing the first `{` while defining generic class with brackets', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Dummy_Class~T~ {\n' +
|
||||
'String data\n' +
|
||||
' void methods()\n' +
|
||||
'}\n' +
|
||||
'\n' +
|
||||
'class Dummy_Class {\n';
|
||||
let testPassed = false;
|
||||
try {
|
||||
parser.parse(str);
|
||||
} catch (error) {
|
||||
testPassed = true;
|
||||
}
|
||||
expect(testPassed).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle generic class with brackets', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Dummy_Class~T~ {\n' +
|
||||
'String data\n' +
|
||||
' void methods()\n' +
|
||||
'}\n' +
|
||||
'\n' +
|
||||
'class Flight {\n' +
|
||||
' flightNumber : Integer\n' +
|
||||
' departureTime : Date\n' +
|
||||
'}';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle generic class with brackets and a literal name', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class `Dummy_Class`~T~ {\n' +
|
||||
'String data\n' +
|
||||
' void methods()\n' +
|
||||
'}\n' +
|
||||
'\n' +
|
||||
'class Flight {\n' +
|
||||
' flightNumber : Integer\n' +
|
||||
' departureTime : Date\n' +
|
||||
'}';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle class definitions', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Car\n' +
|
||||
'Driver -- Car : drives >\n' +
|
||||
'Car *-- Wheel : have 4 >\n' +
|
||||
'Car -- Person : < owns';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle method statements', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'Object <|-- ArrayList\n' +
|
||||
'Object : equals()\n' +
|
||||
'ArrayList : Object[] elementData\n' +
|
||||
'ArrayList : size()';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle parsing of method statements grouped by brackets', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Dummy_Class {\n' +
|
||||
'String data\n' +
|
||||
' void methods()\n' +
|
||||
'}\n' +
|
||||
'\n' +
|
||||
'class Flight {\n' +
|
||||
' flightNumber : Integer\n' +
|
||||
' departureTime : Date\n' +
|
||||
'}';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle return types on methods', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'Object <|-- ArrayList\n' +
|
||||
'Object : equals()\n' +
|
||||
'Object : -Object[] objects\n' +
|
||||
'Object : +getObjects() Object[]\n' +
|
||||
'ArrayList : Dummy elementData\n' +
|
||||
'ArrayList : getDummy() Dummy';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle return types on methods grouped by brackets', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Dummy_Class {\n' +
|
||||
'string data\n' +
|
||||
'getDummy() Dummy\n' +
|
||||
'}\n' +
|
||||
'\n' +
|
||||
'class Flight {\n' +
|
||||
' int flightNumber\n' +
|
||||
' datetime departureTime\n' +
|
||||
' getDepartureTime() datetime\n' +
|
||||
'}';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle parsing of separators', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Foo1 {\n' +
|
||||
' You can use\n' +
|
||||
' several lines\n' +
|
||||
'..\n' +
|
||||
'as you want\n' +
|
||||
'and group\n' +
|
||||
'==\n' +
|
||||
'things together.\n' +
|
||||
'__\n' +
|
||||
'You can have as many groups\n' +
|
||||
'as you want\n' +
|
||||
'--\n' +
|
||||
'End of class\n' +
|
||||
'}\n' +
|
||||
'\n' +
|
||||
'class User {\n' +
|
||||
'.. Simple Getter ..\n' +
|
||||
'+ getName()\n' +
|
||||
'+ getAddress()\n' +
|
||||
'.. Some setter ..\n' +
|
||||
'+ setName()\n' +
|
||||
'__ private data __\n' +
|
||||
'int age\n' +
|
||||
'-- encrypted --\n' +
|
||||
'String password\n' +
|
||||
'}';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle a comment', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1 {\n' +
|
||||
'%% Comment\n' +
|
||||
'int : test\n' +
|
||||
'string : foo\n' +
|
||||
'test()\n' +
|
||||
'foo()\n' +
|
||||
'}';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle comments at the start', function () {
|
||||
const str = `%% Comment
|
||||
classDiagram
|
||||
class Class1 {
|
||||
int : test
|
||||
string : foo
|
||||
test()
|
||||
foo()
|
||||
}`;
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle comments at the end', function () {
|
||||
const str = `classDiagram
|
||||
class Class1 {
|
||||
int : test
|
||||
string : foo
|
||||
test()
|
||||
foo()
|
||||
|
||||
}
|
||||
%% Comment
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle comments at the end no trailing newline', function () {
|
||||
const str = `classDiagram
|
||||
class Class1 {
|
||||
int : test
|
||||
string : foo
|
||||
test()
|
||||
foo()
|
||||
}
|
||||
%% Comment`;
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle a comment with multiple line feeds', function () {
|
||||
const str = `classDiagram
|
||||
|
||||
|
||||
%% Comment
|
||||
|
||||
class Class1 {
|
||||
int : test
|
||||
string : foo
|
||||
test()
|
||||
foo()
|
||||
}`;
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle a comment with mermaid class diagram code in them', function () {
|
||||
const str = `classDiagram
|
||||
%% Comment Class01 <|-- Class02
|
||||
class Class1 {
|
||||
int : test
|
||||
string : foo
|
||||
test()
|
||||
foo()
|
||||
}`;
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle a comment inside brackets', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1 {\n' +
|
||||
'%% Comment Class01 <|-- Class02\n' +
|
||||
'int : test\n' +
|
||||
'string : foo\n' +
|
||||
'test()\n' +
|
||||
'foo()\n' +
|
||||
'}';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle click statement with link', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1 {\n' +
|
||||
'%% Comment Class01 <|-- Class02\n' +
|
||||
'int : test\n' +
|
||||
'string : foo\n' +
|
||||
'test()\n' +
|
||||
'foo()\n' +
|
||||
'}\n' +
|
||||
'link Class01 "google.com" ';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle click statement with click and href link', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1 {\n' +
|
||||
'%% Comment Class01 <|-- Class02\n' +
|
||||
'int : test\n' +
|
||||
'string : foo\n' +
|
||||
'test()\n' +
|
||||
'foo()\n' +
|
||||
'}\n' +
|
||||
'click Class01 href "google.com" ';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle click statement with link and tooltip', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1 {\n' +
|
||||
'%% Comment Class01 <|-- Class02\n' +
|
||||
'int : test\n' +
|
||||
'string : foo\n' +
|
||||
'test()\n' +
|
||||
'foo()\n' +
|
||||
'}\n' +
|
||||
'link Class01 "google.com" "A Tooltip" ';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle click statement with click and href link and tooltip', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1 {\n' +
|
||||
'%% Comment Class01 <|-- Class02\n' +
|
||||
'int : test\n' +
|
||||
'string : foo\n' +
|
||||
'test()\n' +
|
||||
'foo()\n' +
|
||||
'}\n' +
|
||||
'click Class01 href "google.com" "A Tooltip" ';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle click statement with callback', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1 {\n' +
|
||||
'%% Comment Class01 <|-- Class02\n' +
|
||||
'int : test\n' +
|
||||
'string : foo\n' +
|
||||
'test()\n' +
|
||||
'foo()\n' +
|
||||
'}\n' +
|
||||
'callback Class01 "functionCall" ';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle click statement with click and call callback', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1 {\n' +
|
||||
'%% Comment Class01 <|-- Class02\n' +
|
||||
'int : test\n' +
|
||||
'string : foo\n' +
|
||||
'test()\n' +
|
||||
'foo()\n' +
|
||||
'}\n' +
|
||||
'click Class01 call functionCall() ';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle click statement with callback and tooltip', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1 {\n' +
|
||||
'%% Comment Class01 <|-- Class02\n' +
|
||||
'int : test\n' +
|
||||
'string : foo\n' +
|
||||
'test()\n' +
|
||||
'foo()\n' +
|
||||
'}\n' +
|
||||
'callback Class01 "functionCall" "A Tooltip" ';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle click statement with click and call callback and tooltip', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1 {\n' +
|
||||
'%% Comment Class01 <|-- Class02\n' +
|
||||
'int : test\n' +
|
||||
'string : foo\n' +
|
||||
'test()\n' +
|
||||
'foo()\n' +
|
||||
'}\n' +
|
||||
'click Class01 call functionCall() "A Tooltip" ';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle dashed relation definition of different types and directions', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'Class11 <|.. Class12\n' +
|
||||
'Class13 <.. Class14\n' +
|
||||
'Class15 ..|> Class16\n' +
|
||||
'Class17 ..> Class18\n' +
|
||||
'Class19 .. Class20';
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle generic types in members', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Car~T~\n' +
|
||||
'Car : -List~Wheel~ wheels\n' +
|
||||
'Car : +setWheels(List~Wheel~ wheels)\n' +
|
||||
'Car : +getWheels() List~Wheel~';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle generic types in members in class with brackets', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Car {\n' +
|
||||
'List~Wheel~ wheels\n' +
|
||||
'setWheels(List~Wheel~ wheels)\n' +
|
||||
'+getWheels() List~Wheel~\n' +
|
||||
'}';
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle "note for"', function () {
|
||||
const str = 'classDiagram\n' + 'Class11 <|.. Class12\n' + 'note for Class11 "test"\n';
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle "note"', function () {
|
||||
const str = 'classDiagram\n' + 'note "test"\n';
|
||||
parser.parse(str);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when fetching data from a classDiagram graph it', function () {
|
||||
beforeEach(function () {
|
||||
parser.yy = classDb;
|
||||
parser.yy.clear();
|
||||
});
|
||||
it('should handle relation definitions EXTENSION', function () {
|
||||
const str = 'classDiagram\n' + 'Class01 <|-- Class02';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
const relations = parser.yy.getRelations();
|
||||
|
||||
expect(parser.yy.getClass('Class01').id).toBe('Class01');
|
||||
expect(parser.yy.getClass('Class02').id).toBe('Class02');
|
||||
expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION);
|
||||
expect(relations[0].relation.type2).toBe('none');
|
||||
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
|
||||
});
|
||||
|
||||
it('should handle accTitle and accDescr', function () {
|
||||
const str = `classDiagram
|
||||
accTitle: My Title
|
||||
accDescr: My Description
|
||||
|
||||
Class01 <|-- Class02
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
expect(parser.yy.getAccTitle()).toBe('My Title');
|
||||
expect(parser.yy.getAccDescription()).toBe('My Description');
|
||||
});
|
||||
it('should handle accTitle and multiline accDescr', function () {
|
||||
const str = `classDiagram
|
||||
accTitle: My Title
|
||||
accDescr {
|
||||
This is mu multi
|
||||
line description
|
||||
}
|
||||
|
||||
Class01 <|-- Class02
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
expect(parser.yy.getAccTitle()).toBe('My Title');
|
||||
expect(parser.yy.getAccDescription()).toBe('This is mu multi\nline description');
|
||||
});
|
||||
|
||||
it('should handle relation definitions AGGREGATION and dotted line', function () {
|
||||
const str = 'classDiagram\n' + 'Class01 o.. Class02';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
const relations = parser.yy.getRelations();
|
||||
|
||||
expect(parser.yy.getClass('Class01').id).toBe('Class01');
|
||||
expect(parser.yy.getClass('Class02').id).toBe('Class02');
|
||||
expect(relations[0].relation.type1).toBe(classDb.relationType.AGGREGATION);
|
||||
expect(relations[0].relation.type2).toBe('none');
|
||||
expect(relations[0].relation.lineType).toBe(classDb.lineType.DOTTED_LINE);
|
||||
});
|
||||
|
||||
it('should handle relation definitions COMPOSITION on both sides', function () {
|
||||
const str = 'classDiagram\n' + 'Class01 *--* Class02';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
const relations = parser.yy.getRelations();
|
||||
|
||||
expect(parser.yy.getClass('Class01').id).toBe('Class01');
|
||||
expect(parser.yy.getClass('Class02').id).toBe('Class02');
|
||||
expect(relations[0].relation.type1).toBe(classDb.relationType.COMPOSITION);
|
||||
expect(relations[0].relation.type2).toBe(classDb.relationType.COMPOSITION);
|
||||
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
|
||||
});
|
||||
|
||||
it('should handle relation definitions no types', function () {
|
||||
const str = 'classDiagram\n' + 'Class01 -- Class02';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
const relations = parser.yy.getRelations();
|
||||
|
||||
expect(parser.yy.getClass('Class01').id).toBe('Class01');
|
||||
expect(parser.yy.getClass('Class02').id).toBe('Class02');
|
||||
expect(relations[0].relation.type1).toBe('none');
|
||||
expect(relations[0].relation.type2).toBe('none');
|
||||
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
|
||||
});
|
||||
|
||||
it('should handle relation definitions with type only on right side', function () {
|
||||
const str = 'classDiagram\n' + 'Class01 --|> Class02';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
const relations = parser.yy.getRelations();
|
||||
|
||||
expect(parser.yy.getClass('Class01').id).toBe('Class01');
|
||||
expect(parser.yy.getClass('Class02').id).toBe('Class02');
|
||||
expect(relations[0].relation.type1).toBe('none');
|
||||
expect(relations[0].relation.type2).toBe(classDb.relationType.EXTENSION);
|
||||
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
|
||||
});
|
||||
|
||||
it('should handle multiple classes and relation definitions', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'Class01 <|-- Class02\n' +
|
||||
'Class03 *-- Class04\n' +
|
||||
'Class05 o-- Class06\n' +
|
||||
'Class07 .. Class08\n' +
|
||||
'Class09 -- Class10';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
const relations = parser.yy.getRelations();
|
||||
|
||||
expect(parser.yy.getClass('Class01').id).toBe('Class01');
|
||||
expect(parser.yy.getClass('Class10').id).toBe('Class10');
|
||||
|
||||
expect(relations.length).toBe(5);
|
||||
|
||||
expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION);
|
||||
expect(relations[0].relation.type2).toBe('none');
|
||||
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
|
||||
expect(relations[3].relation.type1).toBe('none');
|
||||
expect(relations[3].relation.type2).toBe('none');
|
||||
expect(relations[3].relation.lineType).toBe(classDb.lineType.DOTTED_LINE);
|
||||
});
|
||||
|
||||
it('should handle generic class with relation definitions', function () {
|
||||
const str = 'classDiagram\n' + 'Class01~T~ <|-- Class02';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
const relations = parser.yy.getRelations();
|
||||
|
||||
expect(parser.yy.getClass('Class01').id).toBe('Class01');
|
||||
expect(parser.yy.getClass('Class01').type).toBe('T');
|
||||
expect(parser.yy.getClass('Class02').id).toBe('Class02');
|
||||
expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION);
|
||||
expect(relations[0].relation.type2).toBe('none');
|
||||
expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE);
|
||||
});
|
||||
|
||||
it('should handle class annotations', function () {
|
||||
const str = 'classDiagram\n' + 'class Class1\n' + '<<interface>> Class1';
|
||||
parser.parse(str);
|
||||
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.annotations.length).toBe(1);
|
||||
expect(testClass.members.length).toBe(0);
|
||||
expect(testClass.methods.length).toBe(0);
|
||||
expect(testClass.annotations[0]).toBe('interface');
|
||||
});
|
||||
|
||||
it('should handle class annotations with members and methods', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1\n' +
|
||||
'Class1 : int test\n' +
|
||||
'Class1 : test()\n' +
|
||||
'<<interface>> Class1';
|
||||
parser.parse(str);
|
||||
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.annotations.length).toBe(1);
|
||||
expect(testClass.members.length).toBe(1);
|
||||
expect(testClass.methods.length).toBe(1);
|
||||
expect(testClass.annotations[0]).toBe('interface');
|
||||
});
|
||||
|
||||
it('should handle class annotations in brackets', function () {
|
||||
const str = 'classDiagram\n' + 'class Class1 {\n' + '<<interface>>\n' + '}';
|
||||
parser.parse(str);
|
||||
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.annotations.length).toBe(1);
|
||||
expect(testClass.members.length).toBe(0);
|
||||
expect(testClass.methods.length).toBe(0);
|
||||
expect(testClass.annotations[0]).toBe('interface');
|
||||
});
|
||||
|
||||
it('should handle class annotations in brackets with members and methods', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1 {\n' +
|
||||
'<<interface>>\n' +
|
||||
'int : test\n' +
|
||||
'test()\n' +
|
||||
'}';
|
||||
parser.parse(str);
|
||||
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.annotations.length).toBe(1);
|
||||
expect(testClass.members.length).toBe(1);
|
||||
expect(testClass.methods.length).toBe(1);
|
||||
expect(testClass.annotations[0]).toBe('interface');
|
||||
});
|
||||
|
||||
it('should add bracket members in right order', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1 {\n' +
|
||||
'int : test\n' +
|
||||
'string : foo\n' +
|
||||
'test()\n' +
|
||||
'foo()\n' +
|
||||
'}';
|
||||
parser.parse(str);
|
||||
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.members.length).toBe(2);
|
||||
expect(testClass.methods.length).toBe(2);
|
||||
expect(testClass.members[0]).toBe('int : test');
|
||||
expect(testClass.members[1]).toBe('string : foo');
|
||||
expect(testClass.methods[0]).toBe('test()');
|
||||
expect(testClass.methods[1]).toBe('foo()');
|
||||
});
|
||||
|
||||
it('should handle abstract methods', function () {
|
||||
const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()*';
|
||||
parser.parse(str);
|
||||
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.annotations.length).toBe(0);
|
||||
expect(testClass.members.length).toBe(0);
|
||||
expect(testClass.methods.length).toBe(1);
|
||||
expect(testClass.methods[0]).toBe('someMethod()*');
|
||||
});
|
||||
|
||||
it('should handle static methods', function () {
|
||||
const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()$';
|
||||
parser.parse(str);
|
||||
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.annotations.length).toBe(0);
|
||||
expect(testClass.members.length).toBe(0);
|
||||
expect(testClass.methods.length).toBe(1);
|
||||
expect(testClass.methods[0]).toBe('someMethod()$');
|
||||
});
|
||||
|
||||
it('should associate link and css appropriately', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1\n' +
|
||||
'Class1 : someMethod()\n' +
|
||||
'link Class1 "google.com"';
|
||||
parser.parse(str);
|
||||
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.link).toBe('google.com');
|
||||
expect(testClass.cssClasses.length).toBe(1);
|
||||
expect(testClass.cssClasses[0]).toBe('clickable');
|
||||
});
|
||||
|
||||
it('should associate click and href link and css appropriately', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1\n' +
|
||||
'Class1 : someMethod()\n' +
|
||||
'click Class1 href "google.com"';
|
||||
parser.parse(str);
|
||||
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.link).toBe('google.com');
|
||||
expect(testClass.cssClasses.length).toBe(1);
|
||||
expect(testClass.cssClasses[0]).toBe('clickable');
|
||||
});
|
||||
|
||||
it('should associate link with tooltip', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1\n' +
|
||||
'Class1 : someMethod()\n' +
|
||||
'link Class1 "google.com" "A tooltip"';
|
||||
parser.parse(str);
|
||||
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.link).toBe('google.com');
|
||||
expect(testClass.tooltip).toBe('A tooltip');
|
||||
expect(testClass.cssClasses.length).toBe(1);
|
||||
expect(testClass.cssClasses[0]).toBe('clickable');
|
||||
});
|
||||
|
||||
it('should associate click and href link with tooltip', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1\n' +
|
||||
'Class1 : someMethod()\n' +
|
||||
'click Class1 href "google.com" "A tooltip"';
|
||||
parser.parse(str);
|
||||
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.link).toBe('google.com');
|
||||
expect(testClass.tooltip).toBe('A tooltip');
|
||||
expect(testClass.cssClasses.length).toBe(1);
|
||||
expect(testClass.cssClasses[0]).toBe('clickable');
|
||||
});
|
||||
|
||||
it('should associate click and href link with tooltip and target appropriately', function () {
|
||||
spyOn(classDb, 'setLink');
|
||||
spyOn(classDb, 'setTooltip');
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1\n' +
|
||||
'Class1 : someMethod()\n' +
|
||||
'click Class1 href "google.com" "A tooltip" _self';
|
||||
parser.parse(str);
|
||||
|
||||
expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self');
|
||||
expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip');
|
||||
});
|
||||
|
||||
it('should associate click and href link appropriately', function () {
|
||||
spyOn(classDb, 'setLink');
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1\n' +
|
||||
'Class1 : someMethod()\n' +
|
||||
'click Class1 href "google.com"';
|
||||
parser.parse(str);
|
||||
|
||||
expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com');
|
||||
});
|
||||
|
||||
it('should associate click and href link with target appropriately', function () {
|
||||
spyOn(classDb, 'setLink');
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1\n' +
|
||||
'Class1 : someMethod()\n' +
|
||||
'click Class1 href "google.com" _self';
|
||||
parser.parse(str);
|
||||
|
||||
expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self');
|
||||
});
|
||||
|
||||
it('should associate link appropriately', function () {
|
||||
spyOn(classDb, 'setLink');
|
||||
spyOn(classDb, 'setTooltip');
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1\n' +
|
||||
'Class1 : someMethod()\n' +
|
||||
'link Class1 "google.com" "A tooltip" _self';
|
||||
parser.parse(str);
|
||||
|
||||
expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self');
|
||||
expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip');
|
||||
});
|
||||
|
||||
it('should associate callback appropriately', function () {
|
||||
spyOn(classDb, 'setClickEvent');
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1\n' +
|
||||
'Class1 : someMethod()\n' +
|
||||
'callback Class1 "functionCall"';
|
||||
parser.parse(str);
|
||||
|
||||
expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall');
|
||||
});
|
||||
|
||||
it('should associate click and call callback appropriately', function () {
|
||||
spyOn(classDb, 'setClickEvent');
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1\n' +
|
||||
'Class1 : someMethod()\n' +
|
||||
'click Class1 call functionCall()';
|
||||
parser.parse(str);
|
||||
|
||||
expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall');
|
||||
});
|
||||
|
||||
it('should associate callback appropriately with an arbitrary number of args', function () {
|
||||
spyOn(classDb, 'setClickEvent');
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1\n' +
|
||||
'Class1 : someMethod()\n' +
|
||||
'click Class1 call functionCall("test0", test1, test2)';
|
||||
parser.parse(str);
|
||||
|
||||
expect(classDb.setClickEvent).toHaveBeenCalledWith(
|
||||
'Class1',
|
||||
'functionCall',
|
||||
'"test0", test1, test2'
|
||||
);
|
||||
});
|
||||
|
||||
it('should associate callback with tooltip', function () {
|
||||
spyOn(classDb, 'setClickEvent');
|
||||
spyOn(classDb, 'setTooltip');
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class1\n' +
|
||||
'Class1 : someMethod()\n' +
|
||||
'click Class1 call functionCall() "A tooltip"';
|
||||
parser.parse(str);
|
||||
|
||||
expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall');
|
||||
expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip');
|
||||
});
|
||||
});
|
||||
});
|
1568
packages/mermaid/src/diagrams/class/classDiagram.spec.ts
Normal file
1568
packages/mermaid/src/diagrams/class/classDiagram.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
20
packages/mermaid/src/diagrams/class/classDiagram.ts
Normal file
20
packages/mermaid/src/diagrams/class/classDiagram.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import parser from './parser/classDiagram.jison';
|
||||
import db from './classDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './classRenderer.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
renderer,
|
||||
styles,
|
||||
init: (cnf) => {
|
||||
if (!cnf.class) {
|
||||
cnf.class = {};
|
||||
}
|
||||
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
db.clear();
|
||||
},
|
||||
};
|
@@ -1,13 +0,0 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fs = require('fs');
|
||||
|
||||
import { LALRGenerator } from 'jison';
|
||||
|
||||
describe('class diagram grammar', function () {
|
||||
it('should introduce no new conflicts', function () {
|
||||
const file = require.resolve('./parser/classDiagram.jison');
|
||||
const grammarSource = fs.readFileSync(file, 'utf8');
|
||||
const grammarParser = new LALRGenerator(grammarSource, {});
|
||||
expect(grammarParser.conflicts < 16).toBe(true);
|
||||
});
|
||||
});
|
@@ -0,0 +1,16 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
// @ts-ignore - no types
|
||||
import { LALRGenerator } from 'jison';
|
||||
|
||||
const getAbsolutePath = (relativePath: string) => {
|
||||
return fileURLToPath(new URL(relativePath, import.meta.url));
|
||||
};
|
||||
|
||||
describe('class diagram grammar', function () {
|
||||
it('should have no conflicts', async function () {
|
||||
const grammarSource = await readFile(getAbsolutePath('./parser/classDiagram.jison'), 'utf8');
|
||||
const grammarParser = new LALRGenerator(grammarSource, {});
|
||||
expect(grammarParser.conflicts).toBe(0);
|
||||
});
|
||||
});
|
78
packages/mermaid/src/diagrams/class/classParser.spec.ts
Normal file
78
packages/mermaid/src/diagrams/class/classParser.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { setConfig } from '../../config.js';
|
||||
import classDB from './classDb.js';
|
||||
// @ts-ignore - no types in jison
|
||||
import classDiagram from './parser/classDiagram.jison';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict',
|
||||
});
|
||||
|
||||
describe('when parsing class diagram', function () {
|
||||
beforeEach(function () {
|
||||
classDiagram.parser.yy = classDB;
|
||||
classDiagram.parser.yy.clear();
|
||||
});
|
||||
|
||||
it('should parse diagram with direction', () => {
|
||||
classDiagram.parser.parse(`classDiagram
|
||||
direction TB
|
||||
class Student {
|
||||
-idCard : IdCard
|
||||
}
|
||||
class IdCard{
|
||||
-id : int
|
||||
-name : string
|
||||
}
|
||||
class Bike{
|
||||
-id : int
|
||||
-name : string
|
||||
}
|
||||
Student "1" --o "1" IdCard : carries
|
||||
Student "1" --o "1" Bike : rides`);
|
||||
|
||||
expect(Object.keys(classDB.getClasses()).length).toBe(3);
|
||||
expect(classDB.getClasses().Student).toMatchInlineSnapshot(`
|
||||
{
|
||||
"annotations": [],
|
||||
"cssClasses": [],
|
||||
"domId": "classId-Student-0",
|
||||
"id": "Student",
|
||||
"label": "Student",
|
||||
"members": [
|
||||
"-idCard : IdCard",
|
||||
],
|
||||
"methods": [],
|
||||
"type": "",
|
||||
}
|
||||
`);
|
||||
expect(classDB.getRelations().length).toBe(2);
|
||||
expect(classDB.getRelations()).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"id1": "Student",
|
||||
"id2": "IdCard",
|
||||
"relation": {
|
||||
"lineType": 0,
|
||||
"type1": "none",
|
||||
"type2": 0,
|
||||
},
|
||||
"relationTitle1": "1",
|
||||
"relationTitle2": "1",
|
||||
"title": "carries",
|
||||
},
|
||||
{
|
||||
"id1": "Student",
|
||||
"id2": "Bike",
|
||||
"relation": {
|
||||
"lineType": 0,
|
||||
"type1": "none",
|
||||
"type2": 0,
|
||||
},
|
||||
"relationTitle1": "1",
|
||||
"relationTitle2": "1",
|
||||
"title": "rides",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
@@ -1,527 +0,0 @@
|
||||
import { select } from 'd3';
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib';
|
||||
import { log } from '../../logger';
|
||||
import { getConfig } from '../../config';
|
||||
import { render } from '../../dagre-wrapper/index.js';
|
||||
import utils from '../../utils';
|
||||
import { curveLinear } from 'd3';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../utils';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox';
|
||||
import common from '../common/common';
|
||||
import addSVGAccessibilityFields from '../../accessibility';
|
||||
|
||||
let idCache = {};
|
||||
|
||||
const sanitizeText = (txt) => common.sanitizeText(txt, getConfig());
|
||||
|
||||
let conf = {
|
||||
dividerMargin: 10,
|
||||
padding: 5,
|
||||
textHeight: 10,
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that adds the vertices found during parsing to the graph to be rendered.
|
||||
*
|
||||
* @param {Object<
|
||||
* string,
|
||||
* { cssClasses: string[]; text: string; id: string; type: string; domId: string }
|
||||
* >} classes
|
||||
* Object containing the vertices.
|
||||
* @param {SVGGElement} g The graph that is to be drawn.
|
||||
* @param _id
|
||||
* @param diagObj
|
||||
*/
|
||||
export const addClasses = function (classes, g, _id, diagObj) {
|
||||
// const svg = select(`[id="${svgId}"]`);
|
||||
const keys = Object.keys(classes);
|
||||
log.info('keys:', keys);
|
||||
log.info(classes);
|
||||
|
||||
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
||||
keys.forEach(function (id) {
|
||||
const vertex = classes[id];
|
||||
|
||||
/**
|
||||
* Variable for storing the classes for the vertex
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
let cssClassStr = '';
|
||||
if (vertex.cssClasses.length > 0) {
|
||||
cssClassStr = cssClassStr + ' ' + vertex.cssClasses.join(' ');
|
||||
}
|
||||
// if (vertex.classes.length > 0) {
|
||||
// classStr = vertex.classes.join(' ');
|
||||
// }
|
||||
|
||||
const styles = { labelStyle: '' }; //getStylesFromArray(vertex.styles);
|
||||
|
||||
// Use vertex id as text in the box if no text is provided by the graph definition
|
||||
let vertexText = vertex.text !== undefined ? vertex.text : vertex.id;
|
||||
|
||||
// We create a SVG label, either by delegating to addHtmlLabel or manually
|
||||
// let vertexNode;
|
||||
// if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
// const node = {
|
||||
// label: vertexText.replace(
|
||||
// eslint-disable-next-line @cspell/spellchecker
|
||||
// /fa[lrsb]?:fa-[\w-]+/g,
|
||||
// s => `<i class='${s.replace(':', ' ')}'></i>`
|
||||
// )
|
||||
// };
|
||||
// vertexNode = addHtmlLabel(svg, node).node();
|
||||
// vertexNode.parentNode.removeChild(vertexNode);
|
||||
// } else {
|
||||
// const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
// svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
|
||||
|
||||
// const rows = vertexText.split(common.lineBreakRegex);
|
||||
|
||||
// for (let j = 0; j < rows.length; j++) {
|
||||
// const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||
// tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
||||
// tspan.setAttribute('dy', '1em');
|
||||
// tspan.setAttribute('x', '1');
|
||||
// tspan.textContent = rows[j];
|
||||
// svgLabel.appendChild(tspan);
|
||||
// }
|
||||
// vertexNode = svgLabel;
|
||||
// }
|
||||
|
||||
let radious = 0;
|
||||
let _shape = '';
|
||||
// Set the shape based parameters
|
||||
switch (vertex.type) {
|
||||
case 'class':
|
||||
_shape = 'class_box';
|
||||
break;
|
||||
default:
|
||||
_shape = 'class_box';
|
||||
}
|
||||
// Add the node
|
||||
g.setNode(vertex.id, {
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: _shape,
|
||||
labelText: sanitizeText(vertexText),
|
||||
classData: vertex,
|
||||
rx: radious,
|
||||
ry: radious,
|
||||
class: cssClassStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
domId: vertex.domId,
|
||||
tooltip: diagObj.db.getTooltip(vertex.id) || '',
|
||||
haveCallback: vertex.haveCallback,
|
||||
link: vertex.link,
|
||||
width: vertex.type === 'group' ? 500 : undefined,
|
||||
type: vertex.type,
|
||||
padding: getConfig().flowchart.padding,
|
||||
});
|
||||
|
||||
log.info('setNode', {
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: _shape,
|
||||
labelText: vertexText,
|
||||
rx: radious,
|
||||
ry: radious,
|
||||
class: cssClassStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
width: vertex.type === 'group' ? 500 : undefined,
|
||||
type: vertex.type,
|
||||
padding: getConfig().flowchart.padding,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that adds the additional vertices (notes) found during parsing to the graph to be rendered.
|
||||
*
|
||||
* @param {{text: string; class: string; placement: number}[]} notes
|
||||
* Object containing the additional vertices (notes).
|
||||
* @param {SVGGElement} g The graph that is to be drawn.
|
||||
* @param {number} startEdgeId starting index for note edge
|
||||
* @param classes
|
||||
*/
|
||||
export const addNotes = function (notes, g, startEdgeId, classes) {
|
||||
log.info(notes);
|
||||
|
||||
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
||||
notes.forEach(function (note, i) {
|
||||
const vertex = note;
|
||||
|
||||
/**
|
||||
* Variable for storing the classes for the vertex
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
let cssNoteStr = '';
|
||||
|
||||
const styles = { labelStyle: '', style: '' };
|
||||
|
||||
// Use vertex id as text in the box if no text is provided by the graph definition
|
||||
let vertexText = vertex.text;
|
||||
|
||||
let radious = 0;
|
||||
let _shape = 'note';
|
||||
// Add the node
|
||||
g.setNode(vertex.id, {
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: _shape,
|
||||
labelText: sanitizeText(vertexText),
|
||||
noteData: vertex,
|
||||
rx: radious,
|
||||
ry: radious,
|
||||
class: cssNoteStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
domId: vertex.id,
|
||||
tooltip: '',
|
||||
type: 'note',
|
||||
padding: getConfig().flowchart.padding,
|
||||
});
|
||||
|
||||
log.info('setNode', {
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: _shape,
|
||||
labelText: vertexText,
|
||||
rx: radious,
|
||||
ry: radious,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
type: 'note',
|
||||
padding: getConfig().flowchart.padding,
|
||||
});
|
||||
|
||||
if (!vertex.class || !(vertex.class in classes)) {
|
||||
return;
|
||||
}
|
||||
const edgeId = startEdgeId + i;
|
||||
const edgeData = {};
|
||||
//Set relationship style and line type
|
||||
edgeData.classes = 'relation';
|
||||
edgeData.pattern = 'dotted';
|
||||
|
||||
edgeData.id = `edgeNote${edgeId}`;
|
||||
// Set link type for rendering
|
||||
edgeData.arrowhead = 'none';
|
||||
|
||||
log.info(`Note edge: ${JSON.stringify(edgeData)}, ${JSON.stringify(vertex)}`);
|
||||
//Set edge extra labels
|
||||
edgeData.startLabelRight = '';
|
||||
edgeData.endLabelLeft = '';
|
||||
|
||||
//Set relation arrow types
|
||||
edgeData.arrowTypeStart = 'none';
|
||||
edgeData.arrowTypeEnd = 'none';
|
||||
let style = 'fill:none';
|
||||
let labelStyle = '';
|
||||
|
||||
edgeData.style = style;
|
||||
edgeData.labelStyle = labelStyle;
|
||||
|
||||
edgeData.curve = interpolateToCurve(conf.curve, curveLinear);
|
||||
|
||||
// Add the edge to the graph
|
||||
g.setEdge(vertex.id, vertex.class, edgeData, edgeId);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add edges to graph based on parsed graph definition
|
||||
*
|
||||
* @param relations
|
||||
* @param {object} g The graph object
|
||||
*/
|
||||
export const addRelations = function (relations, g) {
|
||||
const conf = getConfig().flowchart;
|
||||
let cnt = 0;
|
||||
|
||||
let defaultStyle;
|
||||
let defaultLabelStyle;
|
||||
|
||||
// if (typeof relations.defaultStyle !== 'undefined') {
|
||||
// const defaultStyles = getStylesFromArray(relations.defaultStyle);
|
||||
// defaultStyle = defaultStyles.style;
|
||||
// defaultLabelStyle = defaultStyles.labelStyle;
|
||||
// }
|
||||
|
||||
relations.forEach(function (edge) {
|
||||
cnt++;
|
||||
const edgeData = {};
|
||||
//Set relationship style and line type
|
||||
edgeData.classes = 'relation';
|
||||
edgeData.pattern = edge.relation.lineType == 1 ? 'dashed' : 'solid';
|
||||
|
||||
edgeData.id = 'id' + cnt;
|
||||
// Set link type for rendering
|
||||
if (edge.type === 'arrow_open') {
|
||||
edgeData.arrowhead = 'none';
|
||||
} else {
|
||||
edgeData.arrowhead = 'normal';
|
||||
}
|
||||
|
||||
log.info(edgeData, edge);
|
||||
//Set edge extra labels
|
||||
//edgeData.startLabelLeft = edge.relationTitle1;
|
||||
edgeData.startLabelRight = edge.relationTitle1 === 'none' ? '' : edge.relationTitle1;
|
||||
edgeData.endLabelLeft = edge.relationTitle2 === 'none' ? '' : edge.relationTitle2;
|
||||
//edgeData.endLabelRight = edge.relationTitle2;
|
||||
|
||||
//Set relation arrow types
|
||||
edgeData.arrowTypeStart = getArrowMarker(edge.relation.type1);
|
||||
edgeData.arrowTypeEnd = getArrowMarker(edge.relation.type2);
|
||||
let style = '';
|
||||
let labelStyle = '';
|
||||
|
||||
if (typeof edge.style !== 'undefined') {
|
||||
const styles = getStylesFromArray(edge.style);
|
||||
style = styles.style;
|
||||
labelStyle = styles.labelStyle;
|
||||
} else {
|
||||
style = 'fill:none';
|
||||
if (typeof defaultStyle !== 'undefined') {
|
||||
style = defaultStyle;
|
||||
}
|
||||
if (typeof defaultLabelStyle !== 'undefined') {
|
||||
labelStyle = defaultLabelStyle;
|
||||
}
|
||||
}
|
||||
|
||||
edgeData.style = style;
|
||||
edgeData.labelStyle = labelStyle;
|
||||
|
||||
if (typeof edge.interpolate !== 'undefined') {
|
||||
edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear);
|
||||
} else if (typeof relations.defaultInterpolate !== 'undefined') {
|
||||
edgeData.curve = interpolateToCurve(relations.defaultInterpolate, curveLinear);
|
||||
} else {
|
||||
edgeData.curve = interpolateToCurve(conf.curve, curveLinear);
|
||||
}
|
||||
|
||||
edge.text = edge.title;
|
||||
if (typeof edge.text === 'undefined') {
|
||||
if (typeof edge.style !== 'undefined') {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
}
|
||||
} else {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
edgeData.labelpos = 'c';
|
||||
|
||||
if (getConfig().flowchart.htmlLabels) {
|
||||
edgeData.labelType = 'html';
|
||||
edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>';
|
||||
} else {
|
||||
edgeData.labelType = 'text';
|
||||
edgeData.label = edge.text.replace(common.lineBreakRegex, '\n');
|
||||
|
||||
if (typeof edge.style === 'undefined') {
|
||||
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
|
||||
}
|
||||
|
||||
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
|
||||
}
|
||||
}
|
||||
// Add the edge to the graph
|
||||
g.setEdge(edge.id1, edge.id2, edgeData, cnt);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges the value of `conf` with the passed `cnf`
|
||||
*
|
||||
* @param {object} cnf Config to merge
|
||||
*/
|
||||
export const setConf = function (cnf) {
|
||||
const keys = Object.keys(cnf);
|
||||
|
||||
keys.forEach(function (key) {
|
||||
conf[key] = cnf[key];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a flowchart in the tag with id: id based on the graph definition in text.
|
||||
*
|
||||
* @param {string} text
|
||||
* @param {string} id
|
||||
* @param _version
|
||||
* @param diagObj
|
||||
*/
|
||||
export const draw = function (text, id, _version, diagObj) {
|
||||
log.info('Drawing class - ', id);
|
||||
// diagObj.db.clear();
|
||||
// const parser = diagObj.db.parser;
|
||||
// parser.yy = classDb;
|
||||
|
||||
// Parse the graph definition
|
||||
// try {
|
||||
// parser.parse(text);
|
||||
// } catch (err) {
|
||||
// log.debug('Parsing failed');
|
||||
// }
|
||||
|
||||
// Fetch the default direction, use TD if none was found
|
||||
//let dir = 'TD';
|
||||
|
||||
const conf = getConfig().flowchart;
|
||||
const securityLevel = getConfig().securityLevel;
|
||||
log.info('config:', conf);
|
||||
const nodeSpacing = conf.nodeSpacing || 50;
|
||||
const rankSpacing = conf.rankSpacing || 50;
|
||||
|
||||
// Create the input mermaid.graph
|
||||
const g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: diagObj.db.getDirection(),
|
||||
nodesep: nodeSpacing,
|
||||
ranksep: rankSpacing,
|
||||
marginx: 8,
|
||||
marginy: 8,
|
||||
})
|
||||
.setDefaultEdgeLabel(function () {
|
||||
return {};
|
||||
});
|
||||
|
||||
// let subG;
|
||||
// const subGraphs = flowDb.getSubGraphs();
|
||||
// log.info('Subgraphs - ', subGraphs);
|
||||
// for (let i = subGraphs.length - 1; i >= 0; i--) {
|
||||
// subG = subGraphs[i];
|
||||
// log.info('Subgraph - ', subG);
|
||||
// flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes);
|
||||
// }
|
||||
|
||||
// Fetch the vertices/nodes and edges/links from the parsed graph definition
|
||||
const classes = diagObj.db.getClasses();
|
||||
const relations = diagObj.db.getRelations();
|
||||
const notes = diagObj.db.getNotes();
|
||||
|
||||
log.info(relations);
|
||||
addClasses(classes, g, id, diagObj);
|
||||
addRelations(relations, g);
|
||||
addNotes(notes, g, relations.length + 1, classes);
|
||||
|
||||
// Add custom shapes
|
||||
// flowChartShapes.addToRenderV2(addShape);
|
||||
|
||||
// Set up an SVG group so that we can translate the final graph.
|
||||
let sandboxElement;
|
||||
if (securityLevel === 'sandbox') {
|
||||
sandboxElement = select('#i' + id);
|
||||
}
|
||||
const root =
|
||||
securityLevel === 'sandbox'
|
||||
? select(sandboxElement.nodes()[0].contentDocument.body)
|
||||
: select('body');
|
||||
const svg = root.select(`[id="${id}"]`);
|
||||
|
||||
// Run the renderer. This is what draws the final graph.
|
||||
const element = root.select('#' + id + ' g');
|
||||
render(
|
||||
element,
|
||||
g,
|
||||
['aggregation', 'extension', 'composition', 'dependency', 'lollipop'],
|
||||
'classDiagram',
|
||||
id
|
||||
);
|
||||
|
||||
utils.insertTitle(svg, 'classTitleText', conf.titleTopMargin, diagObj.db.getDiagramTitle());
|
||||
|
||||
setupGraphViewbox(g, svg, conf.diagramPadding, conf.useMaxWidth);
|
||||
|
||||
// Add label rects for non html labels
|
||||
if (!conf.htmlLabels) {
|
||||
const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
|
||||
const labels = doc.querySelectorAll('[id="' + id + '"] .edgeLabel .label');
|
||||
for (let k = 0; k < labels.length; k++) {
|
||||
const label = labels[k];
|
||||
|
||||
// Get dimensions of label
|
||||
const dim = label.getBBox();
|
||||
|
||||
const rect = doc.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
rect.setAttribute('rx', 0);
|
||||
rect.setAttribute('ry', 0);
|
||||
rect.setAttribute('width', dim.width);
|
||||
rect.setAttribute('height', dim.height);
|
||||
// rect.setAttribute('style', 'fill:#e8e8e8;');
|
||||
|
||||
label.insertBefore(rect, label.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
addSVGAccessibilityFields(diagObj.db, svg, id);
|
||||
// If node has a link, wrap it in an anchor SVG object.
|
||||
// const keys = Object.keys(classes);
|
||||
// keys.forEach(function(key) {
|
||||
// const vertex = classes[key];
|
||||
|
||||
// if (vertex.link) {
|
||||
// const node = select('#' + id + ' [id="' + key + '"]');
|
||||
// if (node) {
|
||||
// const link = document.createElementNS('http://www.w3.org/2000/svg', 'a');
|
||||
// link.setAttributeNS('http://www.w3.org/2000/svg', 'class', vertex.classes.join(' '));
|
||||
// link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link);
|
||||
// link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener');
|
||||
|
||||
// const linkNode = node.insert(function() {
|
||||
// return link;
|
||||
// }, ':first-child');
|
||||
|
||||
// const shape = node.select('.label-container');
|
||||
// if (shape) {
|
||||
// linkNode.append(function() {
|
||||
// return shape.node();
|
||||
// });
|
||||
// }
|
||||
|
||||
// const label = node.select('.label');
|
||||
// if (label) {
|
||||
// linkNode.append(function() {
|
||||
// return label.node();
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the arrow marker for a type index
|
||||
*
|
||||
* @param {number} type The type to look for
|
||||
* @returns {'aggregation' | 'extension' | 'composition' | 'dependency'} The arrow marker
|
||||
*/
|
||||
function getArrowMarker(type) {
|
||||
let marker;
|
||||
switch (type) {
|
||||
case 0:
|
||||
marker = 'aggregation';
|
||||
break;
|
||||
case 1:
|
||||
marker = 'extension';
|
||||
break;
|
||||
case 2:
|
||||
marker = 'composition';
|
||||
break;
|
||||
case 3:
|
||||
marker = 'dependency';
|
||||
break;
|
||||
case 4:
|
||||
marker = 'lollipop';
|
||||
break;
|
||||
default:
|
||||
marker = 'none';
|
||||
}
|
||||
return marker;
|
||||
}
|
||||
|
||||
export default {
|
||||
setConf,
|
||||
draw,
|
||||
};
|
431
packages/mermaid/src/diagrams/class/classRenderer-v2.ts
Normal file
431
packages/mermaid/src/diagrams/class/classRenderer-v2.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
// @ts-ignore d3 types are not available
|
||||
import { select, curveLinear } from 'd3';
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { getConfig } from '../../config.js';
|
||||
import { render } from '../../dagre-wrapper/index.js';
|
||||
import utils from '../../utils.js';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../utils.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import common from '../common/common.js';
|
||||
import { ClassRelation, ClassNote, ClassMap, EdgeData, NamespaceMap } from './classTypes.js';
|
||||
|
||||
const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig());
|
||||
|
||||
let conf = {
|
||||
dividerMargin: 10,
|
||||
padding: 5,
|
||||
textHeight: 10,
|
||||
curve: undefined,
|
||||
};
|
||||
|
||||
interface RectParameters {
|
||||
id: string;
|
||||
shape: 'rect';
|
||||
labelStyle: string;
|
||||
domId: string;
|
||||
labelText: string;
|
||||
padding: number | undefined;
|
||||
style?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that adds the vertices found during parsing to the graph to be rendered.
|
||||
*
|
||||
* @param namespaces - Object containing the vertices.
|
||||
* @param g - The graph that is to be drawn.
|
||||
* @param _id - id of the graph
|
||||
* @param diagObj - The diagram object
|
||||
*/
|
||||
export const addNamespaces = function (
|
||||
namespaces: NamespaceMap,
|
||||
g: graphlib.Graph,
|
||||
_id: string,
|
||||
diagObj: any
|
||||
) {
|
||||
const keys = Object.keys(namespaces);
|
||||
log.info('keys:', keys);
|
||||
log.info(namespaces);
|
||||
|
||||
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
||||
keys.forEach(function (id) {
|
||||
const vertex = namespaces[id];
|
||||
|
||||
// parent node must be one of [rect, roundedWithTitle, noteGroup, divider]
|
||||
const shape = 'rect';
|
||||
|
||||
const node: RectParameters = {
|
||||
shape: shape,
|
||||
id: vertex.id,
|
||||
domId: vertex.domId,
|
||||
labelText: sanitizeText(vertex.id),
|
||||
labelStyle: '',
|
||||
style: 'fill: none; stroke: black',
|
||||
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||
padding: getConfig().flowchart?.padding ?? getConfig().class?.padding,
|
||||
};
|
||||
|
||||
g.setNode(vertex.id, node);
|
||||
addClasses(vertex.classes, g, _id, diagObj, vertex.id);
|
||||
|
||||
log.info('setNode', node);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that adds the vertices found during parsing to the graph to be rendered.
|
||||
*
|
||||
* @param classes - Object containing the vertices.
|
||||
* @param g - The graph that is to be drawn.
|
||||
* @param _id - id of the graph
|
||||
* @param diagObj - The diagram object
|
||||
* @param parent - id of the parent namespace, if it exists
|
||||
*/
|
||||
export const addClasses = function (
|
||||
classes: ClassMap,
|
||||
g: graphlib.Graph,
|
||||
_id: string,
|
||||
diagObj: any,
|
||||
parent?: string
|
||||
) {
|
||||
const keys = Object.keys(classes);
|
||||
log.info('keys:', keys);
|
||||
log.info(classes);
|
||||
|
||||
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
||||
keys.forEach(function (id) {
|
||||
const vertex = classes[id];
|
||||
|
||||
/**
|
||||
* Variable for storing the classes for the vertex
|
||||
*/
|
||||
let cssClassStr = '';
|
||||
if (vertex.cssClasses.length > 0) {
|
||||
cssClassStr = cssClassStr + ' ' + vertex.cssClasses.join(' ');
|
||||
}
|
||||
|
||||
const styles = { labelStyle: '', style: '' }; //getStylesFromArray(vertex.styles);
|
||||
|
||||
// Use vertex id as text in the box if no text is provided by the graph definition
|
||||
const vertexText = vertex.label ?? vertex.id;
|
||||
const radius = 0;
|
||||
const shape = 'class_box';
|
||||
|
||||
// Add the node
|
||||
const node = {
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: shape,
|
||||
labelText: sanitizeText(vertexText),
|
||||
classData: vertex,
|
||||
rx: radius,
|
||||
ry: radius,
|
||||
class: cssClassStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
domId: vertex.domId,
|
||||
tooltip: diagObj.db.getTooltip(vertex.id, parent) || '',
|
||||
haveCallback: vertex.haveCallback,
|
||||
link: vertex.link,
|
||||
width: vertex.type === 'group' ? 500 : undefined,
|
||||
type: vertex.type,
|
||||
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||
padding: getConfig().flowchart?.padding ?? getConfig().class?.padding,
|
||||
};
|
||||
g.setNode(vertex.id, node);
|
||||
|
||||
if (parent) {
|
||||
g.setParent(vertex.id, parent);
|
||||
}
|
||||
|
||||
log.info('setNode', node);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that adds the additional vertices (notes) found during parsing to the graph to be rendered.
|
||||
*
|
||||
* @param notes - Object containing the additional vertices (notes).
|
||||
* @param g - The graph that is to be drawn.
|
||||
* @param startEdgeId - starting index for note edge
|
||||
* @param classes - Classes
|
||||
*/
|
||||
export const addNotes = function (
|
||||
notes: ClassNote[],
|
||||
g: graphlib.Graph,
|
||||
startEdgeId: number,
|
||||
classes: ClassMap
|
||||
) {
|
||||
log.info(notes);
|
||||
|
||||
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
||||
notes.forEach(function (note, i) {
|
||||
const vertex = note;
|
||||
|
||||
/**
|
||||
* Variable for storing the classes for the vertex
|
||||
*
|
||||
*/
|
||||
const cssNoteStr = '';
|
||||
|
||||
const styles = { labelStyle: '', style: '' };
|
||||
|
||||
// Use vertex id as text in the box if no text is provided by the graph definition
|
||||
const vertexText = vertex.text;
|
||||
|
||||
const radius = 0;
|
||||
const shape = 'note';
|
||||
// Add the node
|
||||
const node = {
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: shape,
|
||||
labelText: sanitizeText(vertexText),
|
||||
noteData: vertex,
|
||||
rx: radius,
|
||||
ry: radius,
|
||||
class: cssNoteStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
domId: vertex.id,
|
||||
tooltip: '',
|
||||
type: 'note',
|
||||
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||
padding: getConfig().flowchart?.padding ?? getConfig().class?.padding,
|
||||
};
|
||||
g.setNode(vertex.id, node);
|
||||
log.info('setNode', node);
|
||||
|
||||
if (!vertex.class || !(vertex.class in classes)) {
|
||||
return;
|
||||
}
|
||||
const edgeId = startEdgeId + i;
|
||||
|
||||
const edgeData: EdgeData = {
|
||||
id: `edgeNote${edgeId}`,
|
||||
//Set relationship style and line type
|
||||
classes: 'relation',
|
||||
pattern: 'dotted',
|
||||
// Set link type for rendering
|
||||
arrowhead: 'none',
|
||||
//Set edge extra labels
|
||||
startLabelRight: '',
|
||||
endLabelLeft: '',
|
||||
//Set relation arrow types
|
||||
arrowTypeStart: 'none',
|
||||
arrowTypeEnd: 'none',
|
||||
style: 'fill:none',
|
||||
labelStyle: '',
|
||||
curve: interpolateToCurve(conf.curve, curveLinear),
|
||||
};
|
||||
|
||||
// Add the edge to the graph
|
||||
g.setEdge(vertex.id, vertex.class, edgeData, edgeId);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add edges to graph based on parsed graph definition
|
||||
*
|
||||
* @param relations -
|
||||
* @param g - The graph object
|
||||
*/
|
||||
export const addRelations = function (relations: ClassRelation[], g: graphlib.Graph) {
|
||||
const conf = getConfig().flowchart;
|
||||
let cnt = 0;
|
||||
|
||||
relations.forEach(function (edge) {
|
||||
cnt++;
|
||||
const edgeData: EdgeData = {
|
||||
//Set relationship style and line type
|
||||
classes: 'relation',
|
||||
pattern: edge.relation.lineType == 1 ? 'dashed' : 'solid',
|
||||
id: 'id' + cnt,
|
||||
// Set link type for rendering
|
||||
arrowhead: edge.type === 'arrow_open' ? 'none' : 'normal',
|
||||
//Set edge extra labels
|
||||
startLabelRight: edge.relationTitle1 === 'none' ? '' : edge.relationTitle1,
|
||||
endLabelLeft: edge.relationTitle2 === 'none' ? '' : edge.relationTitle2,
|
||||
//Set relation arrow types
|
||||
arrowTypeStart: getArrowMarker(edge.relation.type1),
|
||||
arrowTypeEnd: getArrowMarker(edge.relation.type2),
|
||||
style: 'fill:none',
|
||||
labelStyle: '',
|
||||
curve: interpolateToCurve(conf?.curve, curveLinear),
|
||||
};
|
||||
|
||||
log.info(edgeData, edge);
|
||||
|
||||
if (edge.style !== undefined) {
|
||||
const styles = getStylesFromArray(edge.style);
|
||||
edgeData.style = styles.style;
|
||||
edgeData.labelStyle = styles.labelStyle;
|
||||
}
|
||||
|
||||
edge.text = edge.title;
|
||||
if (edge.text === undefined) {
|
||||
if (edge.style !== undefined) {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
}
|
||||
} else {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
edgeData.labelpos = 'c';
|
||||
|
||||
// TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
|
||||
if (getConfig().flowchart?.htmlLabels ?? getConfig().htmlLabels) {
|
||||
edgeData.labelType = 'html';
|
||||
edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>';
|
||||
} else {
|
||||
edgeData.labelType = 'text';
|
||||
edgeData.label = edge.text.replace(common.lineBreakRegex, '\n');
|
||||
|
||||
if (edge.style === undefined) {
|
||||
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
|
||||
}
|
||||
|
||||
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
|
||||
}
|
||||
}
|
||||
// Add the edge to the graph
|
||||
g.setEdge(edge.id1, edge.id2, edgeData, cnt);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges the value of `conf` with the passed `cnf`
|
||||
*
|
||||
* @param cnf - Config to merge
|
||||
*/
|
||||
export const setConf = function (cnf: any) {
|
||||
conf = {
|
||||
...conf,
|
||||
...cnf,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a flowchart in the tag with id: id based on the graph definition in text.
|
||||
*
|
||||
* @param text -
|
||||
* @param id -
|
||||
* @param _version -
|
||||
* @param diagObj -
|
||||
*/
|
||||
export const draw = async function (text: string, id: string, _version: string, diagObj: any) {
|
||||
log.info('Drawing class - ', id);
|
||||
|
||||
// TODO V10: Why flowchart? Might be a mistake when copying.
|
||||
const conf = getConfig().flowchart ?? getConfig().class;
|
||||
const securityLevel = getConfig().securityLevel;
|
||||
log.info('config:', conf);
|
||||
const nodeSpacing = conf?.nodeSpacing ?? 50;
|
||||
const rankSpacing = conf?.rankSpacing ?? 50;
|
||||
|
||||
// Create the input mermaid.graph
|
||||
const g: graphlib.Graph = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: diagObj.db.getDirection(),
|
||||
nodesep: nodeSpacing,
|
||||
ranksep: rankSpacing,
|
||||
marginx: 8,
|
||||
marginy: 8,
|
||||
})
|
||||
.setDefaultEdgeLabel(function () {
|
||||
return {};
|
||||
});
|
||||
|
||||
// Fetch the vertices/nodes and edges/links from the parsed graph definition
|
||||
const namespaces: NamespaceMap = diagObj.db.getNamespaces();
|
||||
const classes: ClassMap = diagObj.db.getClasses();
|
||||
const relations: ClassRelation[] = diagObj.db.getRelations();
|
||||
const notes: ClassNote[] = diagObj.db.getNotes();
|
||||
log.info(relations);
|
||||
addNamespaces(namespaces, g, id, diagObj);
|
||||
addClasses(classes, g, id, diagObj);
|
||||
addRelations(relations, g);
|
||||
addNotes(notes, g, relations.length + 1, classes);
|
||||
|
||||
// Set up an SVG group so that we can translate the final graph.
|
||||
let sandboxElement;
|
||||
if (securityLevel === 'sandbox') {
|
||||
sandboxElement = select('#i' + id);
|
||||
}
|
||||
const root =
|
||||
securityLevel === 'sandbox'
|
||||
? // @ts-ignore Ignore type error for now
|
||||
|
||||
select(sandboxElement.nodes()[0].contentDocument.body)
|
||||
: select('body');
|
||||
// @ts-ignore Ignore type error for now
|
||||
const svg = root.select(`[id="${id}"]`);
|
||||
|
||||
// Run the renderer. This is what draws the final graph.
|
||||
// @ts-ignore Ignore type error for now
|
||||
const element = root.select('#' + id + ' g');
|
||||
await render(
|
||||
element,
|
||||
g,
|
||||
['aggregation', 'extension', 'composition', 'dependency', 'lollipop'],
|
||||
'classDiagram',
|
||||
id
|
||||
);
|
||||
|
||||
utils.insertTitle(svg, 'classTitleText', conf?.titleTopMargin ?? 5, diagObj.db.getDiagramTitle());
|
||||
|
||||
setupGraphViewbox(g, svg, conf?.diagramPadding, conf?.useMaxWidth);
|
||||
|
||||
// Add label rects for non html labels
|
||||
if (!conf?.htmlLabels) {
|
||||
// @ts-ignore Ignore type error for now
|
||||
const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
|
||||
const labels = doc.querySelectorAll('[id="' + id + '"] .edgeLabel .label');
|
||||
for (const label of labels) {
|
||||
// Get dimensions of label
|
||||
const dim = label.getBBox();
|
||||
|
||||
const rect = doc.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
rect.setAttribute('rx', 0);
|
||||
rect.setAttribute('ry', 0);
|
||||
rect.setAttribute('width', dim.width);
|
||||
rect.setAttribute('height', dim.height);
|
||||
|
||||
label.insertBefore(rect, label.firstChild);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the arrow marker for a type index
|
||||
*
|
||||
* @param type - The type to look for
|
||||
* @returns The arrow marker
|
||||
*/
|
||||
function getArrowMarker(type: number) {
|
||||
let marker;
|
||||
switch (type) {
|
||||
case 0:
|
||||
marker = 'aggregation';
|
||||
break;
|
||||
case 1:
|
||||
marker = 'extension';
|
||||
break;
|
||||
case 2:
|
||||
marker = 'composition';
|
||||
break;
|
||||
case 3:
|
||||
marker = 'dependency';
|
||||
break;
|
||||
case 4:
|
||||
marker = 'lollipop';
|
||||
break;
|
||||
default:
|
||||
marker = 'none';
|
||||
}
|
||||
return marker;
|
||||
}
|
||||
|
||||
export default {
|
||||
setConf,
|
||||
draw,
|
||||
};
|
@@ -1,11 +1,10 @@
|
||||
import { select } from 'd3';
|
||||
import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js';
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
import { log } from '../../logger';
|
||||
import svgDraw from './svgDraw';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox';
|
||||
import { getConfig } from '../../config';
|
||||
import addSVGAccessibilityFields from '../../accessibility';
|
||||
import { log } from '../../logger.js';
|
||||
import svgDraw from './svgDraw.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
import { getConfig } from '../../config.js';
|
||||
|
||||
let idCache = {};
|
||||
const padding = 20;
|
||||
@@ -180,8 +179,8 @@ export const draw = function (text, id, _version, diagObj) {
|
||||
const classes = diagObj.db.getClasses();
|
||||
const keys = Object.keys(classes);
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const classDef = classes[keys[i]];
|
||||
for (const key of keys) {
|
||||
const classDef = classes[key];
|
||||
const node = svgDraw.drawClass(diagram, classDef, conf, diagObj);
|
||||
idCache[node.id] = node;
|
||||
|
||||
@@ -240,7 +239,7 @@ export const draw = function (text, id, _version, diagObj) {
|
||||
|
||||
dagreLayout(g);
|
||||
g.nodes().forEach(function (v) {
|
||||
if (typeof v !== 'undefined' && typeof g.node(v) !== 'undefined') {
|
||||
if (v !== undefined && g.node(v) !== undefined) {
|
||||
log.debug('Node ' + v + ': ' + JSON.stringify(g.node(v)));
|
||||
root
|
||||
.select('#' + (diagObj.db.lookUpDomId(v) || v))
|
||||
@@ -256,7 +255,7 @@ export const draw = function (text, id, _version, diagObj) {
|
||||
});
|
||||
|
||||
g.edges().forEach(function (e) {
|
||||
if (typeof e !== 'undefined' && typeof g.edge(e) !== 'undefined') {
|
||||
if (e !== undefined && g.edge(e) !== undefined) {
|
||||
log.debug('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(g.edge(e)));
|
||||
svgDraw.drawEdge(diagram, g.edge(e), g.edge(e).relation, conf, diagObj);
|
||||
}
|
||||
@@ -272,7 +271,6 @@ export const draw = function (text, id, _version, diagObj) {
|
||||
const vBox = `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`;
|
||||
log.debug(`viewBox ${vBox}`);
|
||||
diagram.attr('viewBox', vBox);
|
||||
addSVGAccessibilityFields(diagObj.db, diagram, id);
|
||||
};
|
||||
|
||||
export default {
|
||||
|
64
packages/mermaid/src/diagrams/class/classTypes.ts
Normal file
64
packages/mermaid/src/diagrams/class/classTypes.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export interface ClassNode {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
cssClasses: string[];
|
||||
methods: string[];
|
||||
members: string[];
|
||||
annotations: string[];
|
||||
domId: string;
|
||||
link?: string;
|
||||
linkTarget?: string;
|
||||
haveCallback?: boolean;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export interface ClassNote {
|
||||
id: string;
|
||||
class: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface EdgeData {
|
||||
arrowheadStyle?: string;
|
||||
labelpos?: string;
|
||||
labelType?: string;
|
||||
label?: string;
|
||||
classes: string;
|
||||
pattern: string;
|
||||
id: string;
|
||||
arrowhead: string;
|
||||
startLabelRight: string;
|
||||
endLabelLeft: string;
|
||||
arrowTypeStart: string;
|
||||
arrowTypeEnd: string;
|
||||
style: string;
|
||||
labelStyle: string;
|
||||
curve: any;
|
||||
}
|
||||
|
||||
export type ClassRelation = {
|
||||
id1: string;
|
||||
id2: string;
|
||||
relationTitle1: string;
|
||||
relationTitle2: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
style: string[];
|
||||
relation: {
|
||||
type1: number;
|
||||
type2: number;
|
||||
lineType: number;
|
||||
};
|
||||
};
|
||||
|
||||
export interface NamespaceNode {
|
||||
id: string;
|
||||
domId: string;
|
||||
classes: ClassMap;
|
||||
children: NamespaceMap;
|
||||
}
|
||||
|
||||
export type ClassMap = Record<string, ClassNode>;
|
||||
export type NamespaceMap = Record<string, NamespaceNode>;
|
@@ -19,6 +19,10 @@
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
%x acc_descr_multiline
|
||||
%x class
|
||||
%x class-body
|
||||
%x namespace
|
||||
%x namespace-body
|
||||
%%
|
||||
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
|
||||
.*direction\s+TB[^\n]* return 'direction_tb';
|
||||
@@ -41,35 +45,41 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
|
||||
\s*(\r?\n)+ return 'NEWLINE';
|
||||
\s+ /* skip whitespace */
|
||||
|
||||
"classDiagram-v2" return 'CLASS_DIAGRAM';
|
||||
"classDiagram" return 'CLASS_DIAGRAM';
|
||||
[{] { this.begin("struct"); /*console.log('Starting struct');*/ return 'STRUCT_START';}
|
||||
<INITIAL,struct>"[*]" { /*console.log('EDGE_STATE=',yytext);*/ return 'EDGE_STATE';}
|
||||
<struct><<EOF>> return "EOF_IN_STRUCT";
|
||||
<struct>[{] return "OPEN_IN_STRUCT";
|
||||
<struct>[}] { /*console.log('Ending struct');*/this.popState(); return 'STRUCT_STOP';}}
|
||||
<struct>[\n] /* nothing */
|
||||
<struct>[^{}\n]* { /*console.log('lex-member: ' + yytext);*/ return "MEMBER";}
|
||||
"[*]" return 'EDGE_STATE';
|
||||
|
||||
"class" return 'CLASS';
|
||||
"cssClass" return 'CSSCLASS';
|
||||
"callback" return 'CALLBACK';
|
||||
"link" return 'LINK';
|
||||
"click" return 'CLICK';
|
||||
"note for" return 'NOTE_FOR';
|
||||
"note" return 'NOTE';
|
||||
"<<" return 'ANNOTATION_START';
|
||||
">>" return 'ANNOTATION_END';
|
||||
[~] this.begin("generic");
|
||||
<generic>[~] this.popState();
|
||||
<generic>[^~]* return "GENERICTYPE";
|
||||
["] this.begin("string");
|
||||
<string>["] this.popState();
|
||||
<string>[^"]* return "STR";
|
||||
<INITIAL,namespace>"namespace" { this.begin('namespace'); return 'NAMESPACE'; }
|
||||
<namespace>\s*(\r?\n)+ { this.popState(); return 'NEWLINE'; }
|
||||
<namespace>\s+ /* skip whitespace */
|
||||
<namespace>[{] { this.begin("namespace-body"); return 'STRUCT_START';}
|
||||
<namespace-body>[}] { this.popState(); return 'STRUCT_STOP'; }
|
||||
<namespace-body><<EOF>> return "EOF_IN_STRUCT";
|
||||
<namespace-body>\s*(\r?\n)+ return 'NEWLINE';
|
||||
<namespace-body>\s+ /* skip whitespace */
|
||||
<namespace-body>"[*]" return 'EDGE_STATE';
|
||||
|
||||
[`] this.begin("bqstring");
|
||||
<bqstring>[`] this.popState();
|
||||
<bqstring>[^`]+ return "BQUOTE_STR";
|
||||
<INITIAL,namespace-body>"class" { this.begin('class'); return 'CLASS';}
|
||||
<class>\s*(\r?\n)+ { this.popState(); return 'NEWLINE'; }
|
||||
<class>\s+ /* skip whitespace */
|
||||
<class>[}] { this.popState(); this.popState(); return 'STRUCT_STOP';}
|
||||
<class>[{] { this.begin("class-body"); return 'STRUCT_START';}
|
||||
<class-body>[}] { this.popState(); return 'STRUCT_STOP'; }
|
||||
<class-body><<EOF>> return "EOF_IN_STRUCT";
|
||||
<class-body>"[*]" { return 'EDGE_STATE';}
|
||||
<class-body>[{] return "OPEN_IN_STRUCT";
|
||||
<class-body>[\n] /* nothing */
|
||||
<class-body>[^{}\n]* { return "MEMBER";}
|
||||
|
||||
<*>"cssClass" return 'CSSCLASS';
|
||||
<*>"callback" return 'CALLBACK';
|
||||
<*>"link" return 'LINK';
|
||||
<*>"click" return 'CLICK';
|
||||
<*>"note for" return 'NOTE_FOR';
|
||||
<*>"note" return 'NOTE';
|
||||
<*>"<<" return 'ANNOTATION_START';
|
||||
<*>">>" return 'ANNOTATION_END';
|
||||
|
||||
/*
|
||||
---interactivity command---
|
||||
@@ -77,7 +87,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
line was introduced with 'click'.
|
||||
'href "<link>"' attaches the specified link to the node that was specified by 'click'.
|
||||
*/
|
||||
"href"[\s]+["] this.begin("href");
|
||||
<*>"href"[\s]+["] this.begin("href");
|
||||
<href>["] this.popState();
|
||||
<href>[^"]* return 'HREF';
|
||||
|
||||
@@ -89,39 +99,53 @@ the line was introduced with 'click'.
|
||||
arguments to the node that was specified by 'click'.
|
||||
Function arguments are optional: 'call <callback_name>()' simply executes 'callback_name' without any arguments.
|
||||
*/
|
||||
"call"[\s]+ this.begin("callback_name");
|
||||
<*>"call"[\s]+ this.begin("callback_name");
|
||||
<callback_name>\([\s]*\) this.popState();
|
||||
<callback_name>\( this.popState(); this.begin("callback_args");
|
||||
<callback_name>[^(]* return 'CALLBACK_NAME';
|
||||
<callback_args>\) this.popState();
|
||||
<callback_args>[^)]* return 'CALLBACK_ARGS';
|
||||
|
||||
"_self" return 'LINK_TARGET';
|
||||
"_blank" return 'LINK_TARGET';
|
||||
"_parent" return 'LINK_TARGET';
|
||||
"_top" return 'LINK_TARGET';
|
||||
<generic>[~] this.popState();
|
||||
<generic>[^~]* return "GENERICTYPE";
|
||||
<*>[~] this.begin("generic");
|
||||
|
||||
\s*\<\| return 'EXTENSION';
|
||||
\s*\|\> return 'EXTENSION';
|
||||
\s*\> return 'DEPENDENCY';
|
||||
\s*\< return 'DEPENDENCY';
|
||||
\s*\* return 'COMPOSITION';
|
||||
\s*o return 'AGGREGATION';
|
||||
\s*\(\) return 'LOLLIPOP';
|
||||
\-\- return 'LINE';
|
||||
\.\. return 'DOTTED_LINE';
|
||||
":"{1}[^:\n;]+ return 'LABEL';
|
||||
":"{3} return 'STYLE_SEPARATOR';
|
||||
\- return 'MINUS';
|
||||
"." return 'DOT';
|
||||
\+ return 'PLUS';
|
||||
\% return 'PCT';
|
||||
"=" return 'EQUALS';
|
||||
\= return 'EQUALS';
|
||||
\w+ return 'ALPHA';
|
||||
[!"#$%&'*+,-.`?\\/] return 'PUNCTUATION';
|
||||
[0-9]+ return 'NUM';
|
||||
[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|
|
||||
<string>["] this.popState();
|
||||
<string>[^"]* return "STR";
|
||||
<*>["] this.begin("string");
|
||||
|
||||
<bqstring>[`] this.popState();
|
||||
<bqstring>[^`]+ return "BQUOTE_STR";
|
||||
<*>[`] this.begin("bqstring");
|
||||
|
||||
<*>"_self" return 'LINK_TARGET';
|
||||
<*>"_blank" return 'LINK_TARGET';
|
||||
<*>"_parent" return 'LINK_TARGET';
|
||||
<*>"_top" return 'LINK_TARGET';
|
||||
|
||||
<*>\s*\<\| return 'EXTENSION';
|
||||
<*>\s*\|\> return 'EXTENSION';
|
||||
<*>\s*\> return 'DEPENDENCY';
|
||||
<*>\s*\< return 'DEPENDENCY';
|
||||
<*>\s*\* return 'COMPOSITION';
|
||||
<*>\s*o return 'AGGREGATION';
|
||||
<*>\s*\(\) return 'LOLLIPOP';
|
||||
<*>\-\- return 'LINE';
|
||||
<*>\.\. return 'DOTTED_LINE';
|
||||
<*>":"{1}[^:\n;]+ return 'LABEL';
|
||||
<*>":"{3} return 'STYLE_SEPARATOR';
|
||||
<*>\- return 'MINUS';
|
||||
<*>"." return 'DOT';
|
||||
<*>\+ return 'PLUS';
|
||||
<*>\% return 'PCT';
|
||||
<*>"=" return 'EQUALS';
|
||||
<*>\= return 'EQUALS';
|
||||
<*>\w+ return 'ALPHA';
|
||||
<*>"[" return 'SQS';
|
||||
<*>"]" return 'SQE';
|
||||
<*>[!"#$%&'*+,-.`?\\/] return 'PUNCTUATION';
|
||||
<*>[0-9]+ return 'NUM';
|
||||
<*>[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|
|
||||
[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|
|
||||
[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|
|
||||
[\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA]|
|
||||
@@ -182,9 +206,9 @@ Function arguments are optional: 'call <callback_name>()' simply executes 'callb
|
||||
[\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC]|
|
||||
[\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]|
|
||||
[\uFFD2-\uFFD7\uFFDA-\uFFDC]
|
||||
return 'UNICODE_TEXT';
|
||||
\s return 'SPACE';
|
||||
<<EOF>> return 'EOF';
|
||||
return 'UNICODE_TEXT';
|
||||
<*>\s return 'SPACE';
|
||||
<*><<EOF>> return 'EOF';
|
||||
|
||||
/lex
|
||||
|
||||
@@ -198,9 +222,8 @@ Function arguments are optional: 'call <callback_name>()' simply executes 'callb
|
||||
|
||||
start
|
||||
: mermaidDoc
|
||||
| statments
|
||||
| direction
|
||||
| directive start
|
||||
| statements
|
||||
;
|
||||
|
||||
direction
|
||||
@@ -249,35 +272,64 @@ statements
|
||||
| statement NEWLINE statements
|
||||
;
|
||||
|
||||
classLabel
|
||||
: SQS STR SQE { $$=$2; }
|
||||
;
|
||||
|
||||
namespaceName
|
||||
: alphaNumToken { $$=$1; }
|
||||
| alphaNumToken namespaceName { $$=$1+$2; }
|
||||
;
|
||||
|
||||
className
|
||||
: alphaNumToken { $$=$1; }
|
||||
| classLiteralName { $$=$1; }
|
||||
| alphaNumToken className { $$=$1+$2; }
|
||||
| alphaNumToken GENERICTYPE { $$=$1+'~'+$2; }
|
||||
| classLiteralName GENERICTYPE { $$=$1+'~'+$2; }
|
||||
| alphaNumToken GENERICTYPE { $$=$1+'~'+$2+'~'; }
|
||||
| classLiteralName GENERICTYPE { $$=$1+'~'+$2+'~'; }
|
||||
;
|
||||
|
||||
statement
|
||||
: relationStatement { yy.addRelation($1); }
|
||||
| relationStatement LABEL { $1.title = yy.cleanupLabel($2); yy.addRelation($1); }
|
||||
| namespaceStatement
|
||||
| classStatement
|
||||
| methodStatement
|
||||
| annotationStatement
|
||||
| clickStatement
|
||||
| cssClassStatement
|
||||
| noteStatement
|
||||
| directive
|
||||
| direction
|
||||
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
|
||||
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); }
|
||||
;
|
||||
|
||||
namespaceStatement
|
||||
: namespaceIdentifier STRUCT_START classStatements STRUCT_STOP {yy.addClassesToNamespace($1, $3);}
|
||||
| namespaceIdentifier STRUCT_START NEWLINE classStatements STRUCT_STOP {yy.addClassesToNamespace($1, $4);}
|
||||
;
|
||||
|
||||
namespaceIdentifier
|
||||
: NAMESPACE namespaceName {$$=$2; yy.addNamespace($2);}
|
||||
;
|
||||
|
||||
classStatements
|
||||
: classStatement {$$=[$1]}
|
||||
| classStatement NEWLINE {$$=[$1]}
|
||||
| classStatement NEWLINE classStatements {$3.unshift($1); $$=$3}
|
||||
;
|
||||
|
||||
classStatement
|
||||
: CLASS className {yy.addClass($2);}
|
||||
| CLASS className STYLE_SEPARATOR alphaNumToken {yy.addClass($2);yy.setCssClass($2, $4);}
|
||||
| CLASS className STRUCT_START members STRUCT_STOP {/*console.log($2,JSON.stringify($4));*/yy.addClass($2);yy.addMembers($2,$4);}
|
||||
| CLASS className STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.addClass($2);yy.setCssClass($2, $4);yy.addMembers($2,$6);}
|
||||
: classIdentifier
|
||||
| classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);}
|
||||
| classIdentifier STRUCT_START members STRUCT_STOP {yy.addMembers($1,$3);}
|
||||
| classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.setCssClass($1, $3);yy.addMembers($1,$5);}
|
||||
;
|
||||
|
||||
classIdentifier
|
||||
: CLASS className {$$=$2; yy.addClass($2);}
|
||||
| CLASS className classLabel {$$=$2; yy.addClass($2);yy.setClassLabel($2, $3);}
|
||||
;
|
||||
|
||||
annotationStatement
|
||||
@@ -355,7 +407,7 @@ textToken : textNoTagsToken | TAGSTART | TAGEND | '==' | '--' | PCT | DEFA
|
||||
|
||||
textNoTagsToken: alphaNumToken | SPACE | MINUS | keywords ;
|
||||
|
||||
alphaNumToken : UNICODE_TEXT | NUM | ALPHA;
|
||||
alphaNumToken : UNICODE_TEXT | NUM | ALPHA | MINUS;
|
||||
|
||||
classLiteralName : BQUOTE_STR;
|
||||
|
||||
|
@@ -41,7 +41,7 @@ const getStyles = (options) =>
|
||||
|
||||
.divider {
|
||||
stroke: ${options.nodeBorder};
|
||||
stroke: 1;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
g.clickable {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { line, curveBasis } from 'd3';
|
||||
import utils from '../../utils';
|
||||
import { log } from '../../logger';
|
||||
import { parseGenericTypes } from '../common/common';
|
||||
import utils from '../../utils.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { parseGenericTypes } from '../common/common.js';
|
||||
|
||||
let edgeCount = 0;
|
||||
export const drawEdge = function (elem, path, relation, conf, diagObj) {
|
||||
@@ -102,7 +102,7 @@ export const drawEdge = function (elem, path, relation, conf, diagObj) {
|
||||
p2_card_y = cardinality_2_point.y;
|
||||
}
|
||||
|
||||
if (typeof relation.title !== 'undefined') {
|
||||
if (relation.title !== undefined) {
|
||||
const g = elem.append('g').attr('class', 'classLabel');
|
||||
const label = g
|
||||
.append('text')
|
||||
@@ -125,7 +125,7 @@ export const drawEdge = function (elem, path, relation, conf, diagObj) {
|
||||
}
|
||||
|
||||
log.info('Rendering relation ' + JSON.stringify(relation));
|
||||
if (typeof relation.relationTitle1 !== 'undefined' && relation.relationTitle1 !== 'none') {
|
||||
if (relation.relationTitle1 !== undefined && relation.relationTitle1 !== 'none') {
|
||||
const g = elem.append('g').attr('class', 'cardinality');
|
||||
g.append('text')
|
||||
.attr('class', 'type1')
|
||||
@@ -135,7 +135,7 @@ export const drawEdge = function (elem, path, relation, conf, diagObj) {
|
||||
.attr('font-size', '6')
|
||||
.text(relation.relationTitle1);
|
||||
}
|
||||
if (typeof relation.relationTitle2 !== 'undefined' && relation.relationTitle2 !== 'none') {
|
||||
if (relation.relationTitle2 !== undefined && relation.relationTitle2 !== 'none') {
|
||||
const g = elem.append('g').attr('class', 'cardinality');
|
||||
g.append('text')
|
||||
.attr('class', 'type2')
|
||||
@@ -199,11 +199,7 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
|
||||
isFirst = false;
|
||||
});
|
||||
|
||||
let classTitleString = classDef.id;
|
||||
|
||||
if (classDef.type !== undefined && classDef.type !== '') {
|
||||
classTitleString += '<' + classDef.type + '>';
|
||||
}
|
||||
let classTitleString = getClassTitleString(classDef);
|
||||
|
||||
const classTitle = title.append('tspan').text(classTitleString).attr('class', 'title');
|
||||
|
||||
@@ -291,6 +287,16 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
|
||||
return classInfo;
|
||||
};
|
||||
|
||||
export const getClassTitleString = function (classDef) {
|
||||
let classTitleString = classDef.id;
|
||||
|
||||
if (classDef.type) {
|
||||
classTitleString += '<' + classDef.type + '>';
|
||||
}
|
||||
|
||||
return classTitleString;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a note diagram
|
||||
*
|
||||
@@ -355,107 +361,59 @@ export const drawNote = function (elem, note, conf, diagObj) {
|
||||
};
|
||||
|
||||
export const parseMember = function (text) {
|
||||
const fieldRegEx = /^(\+|-|~|#)?(\w+)(~\w+~|\[\])?\s+(\w+) *(\*|\$)?$/;
|
||||
const methodRegEx = /^([+|\-|~|#])?(\w+) *\( *(.*)\) *(\*|\$)? *(\w*[~|[\]]*\s*\w*~?)$/;
|
||||
|
||||
let fieldMatch = text.match(fieldRegEx);
|
||||
let methodMatch = text.match(methodRegEx);
|
||||
|
||||
if (fieldMatch && !methodMatch) {
|
||||
return buildFieldDisplay(fieldMatch);
|
||||
} else if (methodMatch) {
|
||||
return buildMethodDisplay(methodMatch);
|
||||
} else {
|
||||
return buildLegacyDisplay(text);
|
||||
}
|
||||
};
|
||||
|
||||
const buildFieldDisplay = function (parsedText) {
|
||||
let cssStyle = '';
|
||||
let displayText = '';
|
||||
|
||||
try {
|
||||
let visibility = parsedText[1] ? parsedText[1].trim() : '';
|
||||
let fieldType = parsedText[2] ? parsedText[2].trim() : '';
|
||||
let genericType = parsedText[3] ? parseGenericTypes(parsedText[3].trim()) : '';
|
||||
let fieldName = parsedText[4] ? parsedText[4].trim() : '';
|
||||
let classifier = parsedText[5] ? parsedText[5].trim() : '';
|
||||
|
||||
displayText = visibility + fieldType + genericType + ' ' + fieldName;
|
||||
cssStyle = parseClassifier(classifier);
|
||||
} catch (err) {
|
||||
displayText = parsedText;
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: displayText,
|
||||
cssStyle: cssStyle,
|
||||
};
|
||||
};
|
||||
|
||||
const buildMethodDisplay = function (parsedText) {
|
||||
let cssStyle = '';
|
||||
let displayText = '';
|
||||
|
||||
try {
|
||||
let visibility = parsedText[1] ? parsedText[1].trim() : '';
|
||||
let methodName = parsedText[2] ? parsedText[2].trim() : '';
|
||||
let parameters = parsedText[3] ? parseGenericTypes(parsedText[3].trim()) : '';
|
||||
let classifier = parsedText[4] ? parsedText[4].trim() : '';
|
||||
let returnType = parsedText[5] ? ' : ' + parseGenericTypes(parsedText[5]).trim() : '';
|
||||
|
||||
displayText = visibility + methodName + '(' + parameters + ')' + returnType;
|
||||
cssStyle = parseClassifier(classifier);
|
||||
} catch (err) {
|
||||
displayText = parsedText;
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: displayText,
|
||||
cssStyle: cssStyle,
|
||||
};
|
||||
};
|
||||
|
||||
const buildLegacyDisplay = function (text) {
|
||||
// if for some reason we don't have any match, use old format to parse text
|
||||
let displayText = '';
|
||||
let cssStyle = '';
|
||||
let memberText = '';
|
||||
let returnType = '';
|
||||
let methodStart = text.indexOf('(');
|
||||
let methodEnd = text.indexOf(')');
|
||||
|
||||
if (methodStart > 1 && methodEnd > methodStart && methodEnd <= text.length) {
|
||||
let visibility = '';
|
||||
let methodName = '';
|
||||
let visibility = '';
|
||||
let firstChar = text.substring(0, 1);
|
||||
let lastChar = text.substring(text.length - 1, text.length);
|
||||
|
||||
let firstChar = text.substring(0, 1);
|
||||
if (firstChar.match(/\w/)) {
|
||||
methodName = text.substring(0, methodStart).trim();
|
||||
} else {
|
||||
if (firstChar.match(/\+|-|~|#/)) {
|
||||
visibility = firstChar;
|
||||
}
|
||||
if (firstChar.match(/[#+~-]/)) {
|
||||
visibility = firstChar;
|
||||
}
|
||||
|
||||
methodName = text.substring(1, methodStart).trim();
|
||||
}
|
||||
let noClassifierRe = /[\s\w)~]/;
|
||||
if (!lastChar.match(noClassifierRe)) {
|
||||
cssStyle = parseClassifier(lastChar);
|
||||
}
|
||||
|
||||
const startIndex = visibility === '' ? 0 : 1;
|
||||
let endIndex = cssStyle === '' ? text.length : text.length - 1;
|
||||
text = text.substring(startIndex, endIndex);
|
||||
|
||||
const methodStart = text.indexOf('(');
|
||||
const methodEnd = text.indexOf(')');
|
||||
const isMethod = methodStart > 1 && methodEnd > methodStart && methodEnd <= text.length;
|
||||
|
||||
if (isMethod) {
|
||||
let methodName = text.substring(0, methodStart).trim();
|
||||
|
||||
const parameters = text.substring(methodStart + 1, methodEnd);
|
||||
const classifier = text.substring(methodEnd + 1, 1);
|
||||
cssStyle = parseClassifier(text.substring(methodEnd + 1, methodEnd + 2));
|
||||
|
||||
displayText = visibility + methodName + '(' + parseGenericTypes(parameters.trim()) + ')';
|
||||
|
||||
if (methodEnd < text.length) {
|
||||
returnType = text.substring(methodEnd + 2).trim();
|
||||
// special case: classifier after the closing parenthesis
|
||||
let potentialClassifier = text.substring(methodEnd + 1, methodEnd + 2);
|
||||
if (cssStyle === '' && !potentialClassifier.match(noClassifierRe)) {
|
||||
cssStyle = parseClassifier(potentialClassifier);
|
||||
returnType = text.substring(methodEnd + 2).trim();
|
||||
} else {
|
||||
returnType = text.substring(methodEnd + 1).trim();
|
||||
}
|
||||
|
||||
if (returnType !== '') {
|
||||
if (returnType.charAt(0) === ':') {
|
||||
returnType = returnType.substring(1).trim();
|
||||
}
|
||||
returnType = ' : ' + parseGenericTypes(returnType);
|
||||
displayText += returnType;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// finally - if all else fails, just send the text back as written (other than parsing for generic types)
|
||||
displayText = parseGenericTypes(text);
|
||||
displayText = visibility + parseGenericTypes(text);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -463,6 +421,7 @@ const buildLegacyDisplay = function (text) {
|
||||
cssStyle,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a <tspan> for a member in a diagram
|
||||
*
|
||||
@@ -503,6 +462,7 @@ const parseClassifier = function (classifier) {
|
||||
};
|
||||
|
||||
export default {
|
||||
getClassTitleString,
|
||||
drawClass,
|
||||
drawEdge,
|
||||
drawNote,
|
||||
|
@@ -1,8 +1,19 @@
|
||||
import svgDraw from './svgDraw';
|
||||
import svgDraw from './svgDraw.js';
|
||||
|
||||
describe('class member Renderer, ', function () {
|
||||
describe('when parsing text to build method display string', function () {
|
||||
it('should handle simple method declaration', function () {
|
||||
describe('given a string representing class method, ', function () {
|
||||
it('should handle class names with generics', function () {
|
||||
const classDef = {
|
||||
id: 'Car',
|
||||
type: 'T',
|
||||
label: 'Car',
|
||||
};
|
||||
|
||||
let actual = svgDraw.getClassTitleString(classDef);
|
||||
expect(actual).toBe('Car<T>');
|
||||
});
|
||||
|
||||
describe('when parsing base method declaration', function () {
|
||||
it('should handle simple declaration', function () {
|
||||
const str = 'foo()';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
@@ -10,71 +21,7 @@ describe('class member Renderer, ', function () {
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle public visibility', function () {
|
||||
const str = '+foo()';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('+foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle private visibility', function () {
|
||||
const str = '-foo()';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('-foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle protected visibility', function () {
|
||||
const str = '#foo()';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('#foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle package/internal visibility', function () {
|
||||
const str = '~foo()';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('~foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should ignore unknown character for visibility', function () {
|
||||
const str = '!foo()';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle abstract method classifier', function () {
|
||||
const str = 'foo()*';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo()');
|
||||
expect(actual.cssStyle).toBe('font-style:italic;');
|
||||
});
|
||||
|
||||
it('should handle static method classifier', function () {
|
||||
const str = 'foo()$';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo()');
|
||||
expect(actual.cssStyle).toBe('text-decoration:underline;');
|
||||
});
|
||||
|
||||
it('should ignore unknown character for classifier', function () {
|
||||
const str = 'foo()!';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle simple method declaration with parameters', function () {
|
||||
it('should handle declaration with parameters', function () {
|
||||
const str = 'foo(int id)';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
@@ -82,7 +29,7 @@ describe('class member Renderer, ', function () {
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle simple method declaration with multiple parameters', function () {
|
||||
it('should handle declaration with multiple parameters', function () {
|
||||
const str = 'foo(int id, object thing)';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
@@ -90,7 +37,7 @@ describe('class member Renderer, ', function () {
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle simple method declaration with single item in parameters', function () {
|
||||
it('should handle declaration with single item in parameters', function () {
|
||||
const str = 'foo(id)';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
@@ -98,7 +45,7 @@ describe('class member Renderer, ', function () {
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle simple method declaration with single item in parameters with extra spaces', function () {
|
||||
it('should handle declaration with single item in parameters with extra spaces', function () {
|
||||
const str = ' foo ( id) ';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
@@ -106,22 +53,6 @@ describe('class member Renderer, ', function () {
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle method declaration with return value', function () {
|
||||
const str = 'foo(id) int';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(id) : int');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle method declaration with generic return value', function () {
|
||||
const str = 'foo(id) List~int~';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(id) : List<int>');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle method declaration with generic parameter', function () {
|
||||
const str = 'foo(List~int~)';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
@@ -130,6 +61,46 @@ describe('class member Renderer, ', function () {
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle method declaration with normal and generic parameter', function () {
|
||||
const str = 'foo(int, List~int~)';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(int, List<int>)');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle declaration with return value', function () {
|
||||
const str = 'foo(id) int';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(id) : int');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle declaration with colon return value', function () {
|
||||
const str = 'foo(id) : int';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(id) : int');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle declaration with generic return value', function () {
|
||||
const str = 'foo(id) List~int~';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(id) : List<int>');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle declaration with colon generic return value', function () {
|
||||
const str = 'foo(id) : List~int~';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(id) : List<int>');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle method declaration with all possible markup', function () {
|
||||
const str = '+foo ( List~int~ ids )* List~Item~';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
@@ -138,7 +109,7 @@ describe('class member Renderer, ', function () {
|
||||
expect(actual.cssStyle).toBe('font-style:italic;');
|
||||
});
|
||||
|
||||
it('should handle method declaration with nested markup', function () {
|
||||
it('should handle method declaration with nested generics', function () {
|
||||
const str = '+foo ( List~List~int~~ ids )* List~List~Item~~';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
@@ -147,8 +118,134 @@ describe('class member Renderer, ', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when parsing text to build field display string', function () {
|
||||
it('should handle simple field declaration', function () {
|
||||
describe('when parsing method visibility', function () {
|
||||
it('should correctly handle public', function () {
|
||||
const str = '+foo()';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('+foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should correctly handle private', function () {
|
||||
const str = '-foo()';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('-foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should correctly handle protected', function () {
|
||||
const str = '#foo()';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('#foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should correctly handle package/internal', function () {
|
||||
const str = '~foo()';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('~foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when parsing method classifier', function () {
|
||||
it('should handle abstract method', function () {
|
||||
const str = 'foo()*';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo()');
|
||||
expect(actual.cssStyle).toBe('font-style:italic;');
|
||||
});
|
||||
|
||||
it('should handle abstract method with return type', function () {
|
||||
const str = 'foo(name: String) int*';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(name: String) : int');
|
||||
expect(actual.cssStyle).toBe('font-style:italic;');
|
||||
});
|
||||
|
||||
it('should handle abstract method classifier after parenthesis with return type', function () {
|
||||
const str = 'foo(name: String)* int';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(name: String) : int');
|
||||
expect(actual.cssStyle).toBe('font-style:italic;');
|
||||
});
|
||||
|
||||
it('should handle static method classifier', function () {
|
||||
const str = 'foo()$';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo()');
|
||||
expect(actual.cssStyle).toBe('text-decoration:underline;');
|
||||
});
|
||||
|
||||
it('should handle static method classifier with return type', function () {
|
||||
const str = 'foo(name: String) int$';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(name: String) : int');
|
||||
expect(actual.cssStyle).toBe('text-decoration:underline;');
|
||||
});
|
||||
|
||||
it('should handle static method classifier with colon and return type', function () {
|
||||
const str = 'foo(name: String): int$';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(name: String) : int');
|
||||
expect(actual.cssStyle).toBe('text-decoration:underline;');
|
||||
});
|
||||
|
||||
it('should handle static method classifier after parenthesis with return type', function () {
|
||||
const str = 'foo(name: String)$ int';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo(name: String) : int');
|
||||
expect(actual.cssStyle).toBe('text-decoration:underline;');
|
||||
});
|
||||
|
||||
it('should ignore unknown character for classifier', function () {
|
||||
const str = 'foo()!';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo()');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a string representing class member, ', function () {
|
||||
describe('when parsing member declaration', function () {
|
||||
it('should handle simple field', function () {
|
||||
const str = 'id';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('id');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle field with type', function () {
|
||||
const str = 'int id';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('int id');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle field with type (name first)', function () {
|
||||
const str = 'id: int';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('id: int');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle array field', function () {
|
||||
const str = 'int[] ids';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
@@ -156,7 +253,15 @@ describe('class member Renderer, ', function () {
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle field declaration with generic type', function () {
|
||||
it('should handle array field (name first)', function () {
|
||||
const str = 'ids: int[]';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('ids: int[]');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle field with generic type', function () {
|
||||
const str = 'List~int~ ids';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
@@ -164,12 +269,62 @@ describe('class member Renderer, ', function () {
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle static field classifier', function () {
|
||||
it('should handle field with generic type (name first)', function () {
|
||||
const str = 'ids: List~int~';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('ids: List<int>');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when parsing classifiers', function () {
|
||||
it('should handle static field', function () {
|
||||
const str = 'String foo$';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('String foo');
|
||||
expect(actual.cssStyle).toBe('text-decoration:underline;');
|
||||
});
|
||||
|
||||
it('should handle static field (name first)', function () {
|
||||
const str = 'foo: String$';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo: String');
|
||||
expect(actual.cssStyle).toBe('text-decoration:underline;');
|
||||
});
|
||||
|
||||
it('should handle static field with generic type', function () {
|
||||
const str = 'List~String~ foo$';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('List<String> foo');
|
||||
expect(actual.cssStyle).toBe('text-decoration:underline;');
|
||||
});
|
||||
|
||||
it('should handle static field with generic type (name first)', function () {
|
||||
const str = 'foo: List~String~$';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('foo: List<String>');
|
||||
expect(actual.cssStyle).toBe('text-decoration:underline;');
|
||||
});
|
||||
|
||||
it('should handle field with nested generic type', function () {
|
||||
const str = 'List~List~int~~ idLists';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('List<List<int>> idLists');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
|
||||
it('should handle field with nested generic type (name first)', function () {
|
||||
const str = 'idLists: List~List~int~~';
|
||||
let actual = svgDraw.parseMember(str);
|
||||
|
||||
expect(actual.displayText).toBe('idLists: List<List<int>>');
|
||||
expect(actual.cssStyle).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { sanitizeText, removeScript, parseGenericTypes } from './common';
|
||||
import { sanitizeText, removeScript, parseGenericTypes } from './common.js';
|
||||
|
||||
describe('when securityLevel is antiscript, all script must be removed', function () {
|
||||
/**
|
||||
@@ -68,5 +68,7 @@ describe('generic parser', function () {
|
||||
expect(parseGenericTypes('test ~Array~Array~string[]~~~')).toEqual(
|
||||
'test <Array<Array<string[]>>>'
|
||||
);
|
||||
expect(parseGenericTypes('~test')).toEqual('~test');
|
||||
expect(parseGenericTypes('~test Array~string~')).toEqual('~test Array<string>');
|
||||
});
|
||||
});
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import { MermaidConfig } from '../../config.type';
|
||||
import { MermaidConfig } from '../../config.type.js';
|
||||
|
||||
export const lineBreakRegex = /<br\s*\/?>/gi;
|
||||
|
||||
/**
|
||||
* Gets the rows of lines in a string
|
||||
@@ -47,7 +49,9 @@ export const sanitizeText = (text: string, config: MermaidConfig): string => {
|
||||
if (config.dompurifyConfig) {
|
||||
text = DOMPurify.sanitize(sanitizeMore(text, config), config.dompurifyConfig).toString();
|
||||
} else {
|
||||
text = DOMPurify.sanitize(sanitizeMore(text, config));
|
||||
text = DOMPurify.sanitize(sanitizeMore(text, config), {
|
||||
FORBID_TAGS: ['style'],
|
||||
}).toString();
|
||||
}
|
||||
return text;
|
||||
};
|
||||
@@ -63,8 +67,6 @@ export const sanitizeTextOrArray = (
|
||||
return a.flat().map((x: string) => sanitizeText(x, config));
|
||||
};
|
||||
|
||||
export const lineBreakRegex = /<br\s*\/?>/gi;
|
||||
|
||||
/**
|
||||
* Whether or not a text has any line breaks
|
||||
*
|
||||
@@ -136,6 +138,32 @@ const getUrl = (useAbsolute: boolean): string => {
|
||||
export const evaluate = (val?: string | boolean): boolean =>
|
||||
val === false || ['false', 'null', '0'].includes(String(val).trim().toLowerCase()) ? false : true;
|
||||
|
||||
/**
|
||||
* Wrapper around Math.max which removes non-numeric values
|
||||
* Returns the larger of a set of supplied numeric expressions.
|
||||
* @param values - Numeric expressions to be evaluated
|
||||
* @returns The smaller value
|
||||
*/
|
||||
export const getMax = function (...values: number[]): number {
|
||||
const newValues: number[] = values.filter((value) => {
|
||||
return !isNaN(value);
|
||||
});
|
||||
return Math.max(...newValues);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper around Math.min which removes non-numeric values
|
||||
* Returns the smaller of a set of supplied numeric expressions.
|
||||
* @param values - Numeric expressions to be evaluated
|
||||
* @returns The smaller value
|
||||
*/
|
||||
export const getMin = function (...values: number[]): number {
|
||||
const newValues: number[] = values.filter((value) => {
|
||||
return !isNaN(value);
|
||||
});
|
||||
return Math.min(...newValues);
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes generics in typescript syntax
|
||||
*
|
||||
@@ -152,11 +180,17 @@ export const evaluate = (val?: string | boolean): boolean =>
|
||||
export const parseGenericTypes = function (text: string): string {
|
||||
let cleanedText = text;
|
||||
|
||||
if (text.indexOf('~') !== -1) {
|
||||
cleanedText = cleanedText.replace(/~([^~].*)/, '<$1');
|
||||
cleanedText = cleanedText.replace(/~([^~]*)$/, '>$1');
|
||||
if (text.split('~').length - 1 >= 2) {
|
||||
let newCleanedText = cleanedText;
|
||||
|
||||
return parseGenericTypes(cleanedText);
|
||||
// use a do...while loop instead of replaceAll to detect recursion
|
||||
// e.g. Array~Array~T~~
|
||||
do {
|
||||
cleanedText = newCleanedText;
|
||||
newCleanedText = cleanedText.replace(/~([^\s,:;]+)~/, '<$1>');
|
||||
} while (newCleanedText != cleanedText);
|
||||
|
||||
return parseGenericTypes(newCleanedText);
|
||||
} else {
|
||||
return cleanedText;
|
||||
}
|
||||
@@ -172,4 +206,6 @@ export default {
|
||||
removeScript,
|
||||
getUrl,
|
||||
evaluate,
|
||||
getMax,
|
||||
getMin,
|
||||
};
|
||||
|
114
packages/mermaid/src/diagrams/common/svgDrawCommon.js
Normal file
114
packages/mermaid/src/diagrams/common/svgDrawCommon.js
Normal 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,
|
||||
};
|
||||
};
|
@@ -1,6 +1,6 @@
|
||||
import { log } from '../../logger';
|
||||
import mermaidAPI from '../../mermaidAPI';
|
||||
import * as configApi from '../../config';
|
||||
import { log } from '../../logger.js';
|
||||
import mermaidAPI from '../../mermaidAPI.js';
|
||||
import * as configApi from '../../config.js';
|
||||
|
||||
import {
|
||||
setAccTitle,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
clear as commonClear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../../commonDb';
|
||||
} from '../../commonDb.js';
|
||||
|
||||
let entities = {};
|
||||
let relationships = [];
|
||||
@@ -20,6 +20,7 @@ const Cardinality = {
|
||||
ZERO_OR_MORE: 'ZERO_OR_MORE',
|
||||
ONE_OR_MORE: 'ONE_OR_MORE',
|
||||
ONLY_ONE: 'ONLY_ONE',
|
||||
MD_PARENT: 'MD_PARENT',
|
||||
};
|
||||
|
||||
const Identification = {
|
||||
@@ -32,7 +33,7 @@ export const parseDirective = function (statement, context, type) {
|
||||
};
|
||||
|
||||
const addEntity = function (name) {
|
||||
if (typeof entities[name] === 'undefined') {
|
||||
if (entities[name] === undefined) {
|
||||
entities[name] = { attributes: [] };
|
||||
log.info('Added new entity :', name);
|
||||
}
|
||||
|
@@ -1,5 +1,20 @@
|
||||
import type { DiagramDetector } from '../../diagram-api/types';
|
||||
import type { DiagramDetector, ExternalDiagramDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
export const erDetector: DiagramDetector = (txt) => {
|
||||
const id = 'er';
|
||||
|
||||
const detector: DiagramDetector = (txt) => {
|
||||
return txt.match(/^\s*erDiagram/) !== null;
|
||||
};
|
||||
|
||||
const loader = async () => {
|
||||
const { diagram } = await import('./erDiagram.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
|
12
packages/mermaid/src/diagrams/er/erDiagram.ts
Normal file
12
packages/mermaid/src/diagrams/er/erDiagram.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import erParser from './parser/erDiagram.jison';
|
||||
import erDb from './erDb.js';
|
||||
import erRenderer from './erRenderer.js';
|
||||
import erStyles from './styles.js';
|
||||
|
||||
export const diagram = {
|
||||
parser: erParser,
|
||||
db: erDb,
|
||||
renderer: erRenderer,
|
||||
styles: erStyles,
|
||||
};
|
@@ -7,6 +7,8 @@ const ERMarkers = {
|
||||
ONE_OR_MORE_END: 'ONE_OR_MORE_END',
|
||||
ZERO_OR_MORE_START: 'ZERO_OR_MORE_START',
|
||||
ZERO_OR_MORE_END: 'ZERO_OR_MORE_END',
|
||||
MD_PARENT_END: 'MD_PARENT_END',
|
||||
MD_PARENT_START: 'MD_PARENT_START',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -18,6 +20,30 @@ const ERMarkers = {
|
||||
const insertMarkers = function (elem, conf) {
|
||||
let marker;
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.MD_PARENT_START)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.MD_PARENT_END)
|
||||
.attr('refX', 19)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
|
@@ -1,17 +1,16 @@
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib';
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
import { line, curveBasis, select } from 'd3';
|
||||
import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js';
|
||||
import { getConfig } from '../../config';
|
||||
import { log } from '../../logger';
|
||||
import utils from '../../utils';
|
||||
import erMarkers from './erMarkers';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox';
|
||||
import addSVGAccessibilityFields from '../../accessibility';
|
||||
import { parseGenericTypes } from '../common/common';
|
||||
import { v4 as uuid4 } from 'uuid';
|
||||
import { getConfig } from '../../config.js';
|
||||
import { log } from '../../logger.js';
|
||||
import utils from '../../utils.js';
|
||||
import erMarkers from './erMarkers.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
import { parseGenericTypes } from '../common/common.js';
|
||||
import { v5 as uuid5 } from 'uuid';
|
||||
|
||||
/** Regex used to remove chars from the entity name so the result can be used in an id */
|
||||
const BAD_ID_CHARS_REGEXP = /[^A-Za-z0-9]([\W])*/g;
|
||||
const BAD_ID_CHARS_REGEXP = /[^\dA-Za-z](\W)*/g;
|
||||
|
||||
// Configuration
|
||||
let conf = {};
|
||||
@@ -28,8 +27,8 @@ let entityNameIds = new Map();
|
||||
*/
|
||||
export const setConf = function (cnf) {
|
||||
const keys = Object.keys(cnf);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
conf[keys[i]] = cnf[keys[i]];
|
||||
for (const key of keys) {
|
||||
conf[key] = cnf[key];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,7 +59,7 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
|
||||
|
||||
// Check to see if any of the attributes has a key or a comment
|
||||
attributes.forEach((item) => {
|
||||
if (item.attributeKeyType !== undefined) {
|
||||
if (item.attributeKeyTypeList !== undefined && item.attributeKeyTypeList.length > 0) {
|
||||
hasKeyType = true;
|
||||
}
|
||||
|
||||
@@ -113,6 +112,9 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
|
||||
nodeHeight = Math.max(typeBBox.height, nameBBox.height);
|
||||
|
||||
if (hasKeyType) {
|
||||
const keyTypeNodeText =
|
||||
item.attributeKeyTypeList !== undefined ? item.attributeKeyTypeList.join(',') : '';
|
||||
|
||||
const keyTypeNode = groupNode
|
||||
.append('text')
|
||||
.classed('er entityLabel', true)
|
||||
@@ -123,7 +125,7 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
|
||||
.style('text-anchor', 'left')
|
||||
.style('font-family', getConfig().fontFamily)
|
||||
.style('font-size', attrFontSize + 'px')
|
||||
.text(item.attributeKeyType || '');
|
||||
.text(keyTypeNodeText);
|
||||
|
||||
attributeNode.kn = keyTypeNode;
|
||||
const keyTypeBBox = keyTypeNode.node().getBBox();
|
||||
@@ -356,7 +358,7 @@ const drawEntities = function (svgNode, entities, graph) {
|
||||
|
||||
const adjustEntities = function (svgNode, graph) {
|
||||
graph.nodes().forEach(function (v) {
|
||||
if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') {
|
||||
if (v !== undefined && graph.node(v) !== undefined) {
|
||||
svgNode
|
||||
.select('#' + v)
|
||||
.attr(
|
||||
@@ -387,7 +389,7 @@ const getEdgeName = function (rel) {
|
||||
* Add each relationship to the graph
|
||||
*
|
||||
* @param relationships The relationships to be added
|
||||
* @param {Graph} g The graph
|
||||
* @param g The graph
|
||||
* @returns {Array} The array of relationships
|
||||
*/
|
||||
const addRelationships = function (relationships, g) {
|
||||
@@ -476,6 +478,9 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
|
||||
case diagObj.db.Cardinality.ONLY_ONE:
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')');
|
||||
break;
|
||||
case diagObj.db.Cardinality.MD_PARENT:
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.MD_PARENT_END + ')');
|
||||
break;
|
||||
}
|
||||
|
||||
switch (rel.relSpec.cardB) {
|
||||
@@ -500,6 +505,9 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
|
||||
case diagObj.db.Cardinality.ONLY_ONE:
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')');
|
||||
break;
|
||||
case diagObj.db.Cardinality.MD_PARENT:
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.MD_PARENT_START + ')');
|
||||
break;
|
||||
}
|
||||
|
||||
// Now label the relationship
|
||||
@@ -642,26 +650,41 @@ export const draw = function (text, id, _version, diagObj) {
|
||||
configureSvgSize(svg, height, width, conf.useMaxWidth);
|
||||
|
||||
svg.attr('viewBox', `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`);
|
||||
|
||||
addSVGAccessibilityFields(diagObj.db, svg, id);
|
||||
}; // draw
|
||||
|
||||
/**
|
||||
* UUID namespace for ER diagram IDs
|
||||
*
|
||||
* This can be generated via running:
|
||||
*
|
||||
* ```js
|
||||
* const { v5: uuid5 } = await import('uuid');
|
||||
* uuid5(
|
||||
* 'https://mermaid-js.github.io/mermaid/syntax/entityRelationshipDiagram.html',
|
||||
* uuid5.URL
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
const MERMAID_ERDIAGRAM_UUID = '28e9f9db-3c8d-5aa5-9faf-44286ae5937c';
|
||||
|
||||
/**
|
||||
* Return a unique id based on the given string. Start with the prefix, then a hyphen, then the
|
||||
* simplified str, then a hyphen, then a unique uuid. (Hyphens are only included if needed.)
|
||||
* simplified str, then a hyphen, then a unique uuid based on the str. (Hyphens are only included if needed.)
|
||||
* Although the official XML standard for ids says that many more characters are valid in the id,
|
||||
* this keeps things simple by accepting only A-Za-z0-9.
|
||||
*
|
||||
* @param {string} [str?=''] Given string to use as the basis for the id. Default is `''`
|
||||
* @param {string} [prefix?=''] String to put at the start, followed by '-'. Default is `''`
|
||||
* @param str
|
||||
* @param prefix
|
||||
* @param {string} str Given string to use as the basis for the id. Default is `''`
|
||||
* @param {string} prefix String to put at the start, followed by '-'. Default is `''`
|
||||
* @returns {string}
|
||||
* @see https://www.w3.org/TR/xml/#NT-Name
|
||||
*/
|
||||
export function generateId(str = '', prefix = '') {
|
||||
const simplifiedStr = str.replace(BAD_ID_CHARS_REGEXP, '');
|
||||
return `${strWithHyphen(prefix)}${strWithHyphen(simplifiedStr)}${uuid4()}`;
|
||||
// we use `uuid v5` so that UUIDs are consistent given a string.
|
||||
return `${strWithHyphen(prefix)}${strWithHyphen(simplifiedStr)}${uuid5(
|
||||
str,
|
||||
MERMAID_ERDIAGRAM_UUID
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
12
packages/mermaid/src/diagrams/er/erRenderer.spec.ts
Normal file
12
packages/mermaid/src/diagrams/er/erRenderer.spec.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { generateId } from './erRenderer.js';
|
||||
|
||||
describe('erRenderer', () => {
|
||||
describe('generateId', () => {
|
||||
it('should be deterministic', () => {
|
||||
const id1 = generateId('hello world', 'my-prefix');
|
||||
const id2 = generateId('hello world', 'my-prefix');
|
||||
|
||||
expect(id1).toBe(id2);
|
||||
});
|
||||
});
|
||||
});
|
@@ -19,8 +19,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
|
||||
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
|
||||
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
|
||||
\%%(?!\{)[^\n]* /* skip comments */
|
||||
[^\}]\%\%[^\n]* /* skip comments */
|
||||
[\n]+ return 'NEWLINE';
|
||||
\s+ /* skip whitespace */
|
||||
[\s]+ return 'SPACE';
|
||||
@@ -28,10 +26,11 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
\"[^"]*\" return 'WORD';
|
||||
"erDiagram" return 'ER_DIAGRAM';
|
||||
"{" { this.begin("block"); return 'BLOCK_START'; }
|
||||
<block>"," return 'COMMA';
|
||||
<block>\s+ /* skip whitespace in block */
|
||||
<block>\b((?:PK)|(?:FK))\b return 'ATTRIBUTE_KEY'
|
||||
<block>\b((?:PK)|(?:FK)|(?:UK))\b return 'ATTRIBUTE_KEY'
|
||||
<block>(.*?)[~](.*?)*[~] return 'ATTRIBUTE_WORD';
|
||||
<block>[A-Za-z][A-Za-z0-9\-_\[\]]* return 'ATTRIBUTE_WORD'
|
||||
<block>[A-Za-z_][A-Za-z0-9\-_\[\]\(\)]* return 'ATTRIBUTE_WORD'
|
||||
<block>\"[^"]*\" return 'COMMENT';
|
||||
<block>[\n]+ /* nothing */
|
||||
<block>"}" { this.popState(); return 'BLOCK_STOP'; }
|
||||
@@ -58,6 +57,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
o\| return 'ZERO_OR_ONE';
|
||||
o\{ return 'ZERO_OR_MORE';
|
||||
\|\{ return 'ONE_OR_MORE';
|
||||
\s*u return 'MD_PARENT';
|
||||
\.\. return 'NON_IDENTIFYING';
|
||||
\-\- return 'IDENTIFYING';
|
||||
"to" return 'IDENTIFYING';
|
||||
@@ -131,11 +131,12 @@ attributes
|
||||
|
||||
attribute
|
||||
: attributeType attributeName { $$ = { attributeType: $1, attributeName: $2 }; }
|
||||
| attributeType attributeName attributeKeyType { $$ = { attributeType: $1, attributeName: $2, attributeKeyType: $3 }; }
|
||||
| attributeType attributeName attributeKeyTypeList { $$ = { attributeType: $1, attributeName: $2, attributeKeyTypeList: $3 }; }
|
||||
| attributeType attributeName attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeComment: $3 }; }
|
||||
| attributeType attributeName attributeKeyType attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeKeyType: $3, attributeComment: $4 }; }
|
||||
| attributeType attributeName attributeKeyTypeList attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeKeyTypeList: $3, attributeComment: $4 }; }
|
||||
;
|
||||
|
||||
|
||||
attributeType
|
||||
: ATTRIBUTE_WORD { $$=$1; }
|
||||
;
|
||||
@@ -144,6 +145,11 @@ attributeName
|
||||
: ATTRIBUTE_WORD { $$=$1; }
|
||||
;
|
||||
|
||||
attributeKeyTypeList
|
||||
: attributeKeyType { $$ = [$1]; }
|
||||
| attributeKeyTypeList COMMA attributeKeyType { $1.push($3); $$ = $1; }
|
||||
;
|
||||
|
||||
attributeKeyType
|
||||
: ATTRIBUTE_KEY { $$=$1; }
|
||||
;
|
||||
@@ -165,6 +171,7 @@ cardinality
|
||||
| 'ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE; }
|
||||
| 'ONE_OR_MORE' { $$ = yy.Cardinality.ONE_OR_MORE; }
|
||||
| 'ONLY_ONE' { $$ = yy.Cardinality.ONLY_ONE; }
|
||||
| 'MD_PARENT' { $$ = yy.Cardinality.MD_PARENT; }
|
||||
;
|
||||
|
||||
relType
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { setConfig } from '../../../config';
|
||||
import erDb from '../erDb';
|
||||
import erDiagram from './erDiagram'; // jison file
|
||||
import { setConfig } from '../../../config.js';
|
||||
import erDb from '../erDb.js';
|
||||
import erDiagram from './erDiagram.jison'; // jison file
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict',
|
||||
@@ -135,6 +135,37 @@ describe('when parsing ER diagram it...', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('attribute name', () => {
|
||||
it('should allow alphanumeric characters, dashes, underscores and brackets (not leading chars)', function () {
|
||||
const entity = 'BOOK';
|
||||
const attribute1 = 'string myBookTitle';
|
||||
const attribute2 = 'string MYBOOKSUBTITLE_1';
|
||||
const attribute3 = 'string author-ref[name](1)';
|
||||
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram\n${entity} {\n${attribute1}\n${attribute2}\n${attribute3}\n}`
|
||||
);
|
||||
const entities = erDb.getEntities();
|
||||
|
||||
expect(Object.keys(entities).length).toBe(1);
|
||||
expect(entities[entity].attributes.length).toBe(3);
|
||||
expect(entities[entity].attributes[0].attributeName).toBe('myBookTitle');
|
||||
expect(entities[entity].attributes[1].attributeName).toBe('MYBOOKSUBTITLE_1');
|
||||
expect(entities[entity].attributes[2].attributeName).toBe('author-ref[name](1)');
|
||||
});
|
||||
|
||||
it('should not allow leading numbers, dashes or brackets', function () {
|
||||
const entity = 'BOOK';
|
||||
const nonLeadingChars = '0-[]()';
|
||||
[...nonLeadingChars].forEach((nonLeadingChar) => {
|
||||
expect(() => {
|
||||
const attribute = `string ${nonLeadingChar}author`;
|
||||
erDiagram.parser.parse(`erDiagram\n${entity} {\n${attribute}\n}`);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow an entity with a single attribute to be defined', function () {
|
||||
const entity = 'BOOK';
|
||||
const attribute = 'string title';
|
||||
@@ -176,17 +207,40 @@ describe('when parsing ER diagram it...', function () {
|
||||
expect(entities[entity].attributes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow an entity with attribute starting with fk or pk and a comment', function () {
|
||||
it('should allow an entity with attribute starting with fk, pk or uk and a comment', function () {
|
||||
const entity = 'BOOK';
|
||||
const attribute1 = 'int fk_title FK';
|
||||
const attribute2 = 'string pk_author PK';
|
||||
const attribute3 = 'float pk_price PK "comment"';
|
||||
const attribute3 = 'string uk_address UK';
|
||||
const attribute4 = 'float pk_price PK "comment"';
|
||||
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram\n${entity} {\n${attribute1} \n\n${attribute2}\n${attribute3}\n}`
|
||||
`erDiagram\n${entity} {\n${attribute1} \n\n${attribute2}\n${attribute3}\n${attribute4}\n}`
|
||||
);
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities[entity].attributes.length).toBe(3);
|
||||
expect(entities[entity].attributes.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should allow an entity with attributes that have many constraints and comments', function () {
|
||||
const entity = 'CUSTOMER';
|
||||
const attribute1 = 'int customer_number PK, FK "comment1"';
|
||||
const attribute2 = 'datetime customer_status_start_datetime PK,UK, FK';
|
||||
const attribute3 = 'datetime customer_status_end_datetime PK , UK "comment3"';
|
||||
const attribute4 = 'string customer_firstname';
|
||||
const attribute5 = 'string customer_lastname "comment5"';
|
||||
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram\n${entity} {\n${attribute1}\n${attribute2}\n${attribute3}\n${attribute4}\n${attribute5}\n}`
|
||||
);
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities[entity].attributes[0].attributeKeyTypeList).toEqual(['PK', 'FK']);
|
||||
expect(entities[entity].attributes[0].attributeComment).toBe('comment1');
|
||||
expect(entities[entity].attributes[1].attributeKeyTypeList).toEqual(['PK', 'UK', 'FK']);
|
||||
expect(entities[entity].attributes[2].attributeKeyTypeList).toEqual(['PK', 'UK']);
|
||||
expect(entities[entity].attributes[2].attributeComment).toBe('comment3');
|
||||
expect(entities[entity].attributes[3].attributeKeyTypeList).toBeUndefined();
|
||||
expect(entities[entity].attributes[4].attributeKeyTypeList).toBeUndefined();
|
||||
expect(entities[entity].attributes[4].attributeComment).toBe('comment5');
|
||||
});
|
||||
|
||||
it('should allow an entity with attribute that has a generic type', function () {
|
||||
@@ -214,6 +268,19 @@ describe('when parsing ER diagram it...', function () {
|
||||
expect(entities[entity].attributes.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should allow an entity with attribute that is a limited length string', function () {
|
||||
const entity = 'BOOK';
|
||||
const attribute1 = 'character(10) isbn FK';
|
||||
const attribute2 = 'varchar(5) postal_code "Five digits"';
|
||||
|
||||
erDiagram.parser.parse(`erDiagram\n${entity} {\n${attribute1}\n${attribute2}\n}`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(Object.keys(entities).length).toBe(1);
|
||||
expect(entities[entity].attributes.length).toBe(2);
|
||||
expect(entities[entity].attributes[0].attributeType).toBe('character(10)');
|
||||
expect(entities[entity].attributes[1].attributeType).toBe('varchar(5)');
|
||||
});
|
||||
|
||||
it('should allow an entity with multiple attributes to be defined', function () {
|
||||
const entity = 'BOOK';
|
||||
const attribute1 = 'string title';
|
||||
@@ -323,34 +390,34 @@ describe('when parsing ER diagram it...', function () {
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow for a accessibility title and description (accDescr)', function () {
|
||||
describe('accessible title and description', () => {
|
||||
const teacherRole = 'is teacher of';
|
||||
const line1 = `TEACHER }o--o{ STUDENT : "${teacherRole}"`;
|
||||
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram
|
||||
it('should allow for a accessibility title and description (accDescr)', function () {
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram
|
||||
accTitle: graph title
|
||||
accDescr: this graph is about stuff
|
||||
${line1}`
|
||||
);
|
||||
expect(erDb.getAccTitle()).toBe('graph title');
|
||||
expect(erDb.getAccDescription()).toBe('this graph is about stuff');
|
||||
});
|
||||
);
|
||||
expect(erDb.getAccTitle()).toBe('graph title');
|
||||
expect(erDb.getAccDescription()).toBe('this graph is about stuff');
|
||||
});
|
||||
|
||||
it('should allow for a accessibility title and multi line description (accDescr)', function () {
|
||||
const teacherRole = 'is teacher of';
|
||||
const line1 = `TEACHER }o--o{ STUDENT : "${teacherRole}"`;
|
||||
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram
|
||||
it('parses a multi line description (accDescr)', function () {
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram
|
||||
accTitle: graph title
|
||||
accDescr {
|
||||
this graph is about stuff
|
||||
}\n
|
||||
accDescr { this graph is
|
||||
about
|
||||
stuff
|
||||
}\n
|
||||
${line1}`
|
||||
);
|
||||
expect(erDb.getAccTitle()).toBe('graph title');
|
||||
expect(erDb.getAccDescription()).toBe('this graph is about stuff');
|
||||
);
|
||||
expect(erDb.getAccTitle()).toEqual('graph title');
|
||||
expect(erDb.getAccDescription()).toEqual('this graph is\nabout\nstuff');
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow more than one relationship between the same two entities', function () {
|
||||
@@ -651,5 +718,14 @@ describe('when parsing ER diagram it...', function () {
|
||||
const rels = erDb.getRelationships();
|
||||
expect(rels[0].roleA).toBe('places');
|
||||
});
|
||||
|
||||
it('should represent parent-child relationship correctly', function () {
|
||||
erDiagram.parser.parse('erDiagram\nPROJECT u--o{ TEAM_MEMBER : "parent"');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.MD_PARENT);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -33,6 +33,17 @@ const getStyles = (options) =>
|
||||
font-size: 18px;
|
||||
fill: ${options.textColor};
|
||||
}
|
||||
#MD_PARENT_START {
|
||||
fill: #f5f5f5 !important;
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
}
|
||||
#MD_PARENT_END {
|
||||
fill: #f5f5f5 !important;
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user