Merge branch 'develop' into feature/class-namespace

This commit is contained in:
Kazuki Tsunemi 2023-04-25 16:40:42 +09:00
commit 95d8e3a5df
34 changed files with 5294 additions and 4224 deletions

View File

@ -50,7 +50,7 @@ body:
attributes:
label: Setup
description: |-
Please fill out the below info.
Please fill out the info below.
Note that you only need to fill out the relevant section
value: |-
- Mermaid version:

29
.github/workflows/build-docs.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: Build Vitepress docs
on:
pull_request:
permissions:
contents: read
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- name: Setup Node.js
uses: actions/setup-node@v3
with:
cache: pnpm
node-version: 18
- name: Install Packages
run: pnpm install --frozen-lockfile
- name: Run Build
run: pnpm --filter mermaid run docs:build:vitepress

View File

@ -38,15 +38,8 @@ jobs:
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
cache: pnpm
node-version: ${{ matrix.node-version }}
- name: Install Packages
run: |
pnpm install --frozen-lockfile
env:
CYPRESS_CACHE_FOLDER: .cache/Cypress
- if: ${{ env.USE_APPLI }}
name: Notify applitools of new batch
# Copied from docs https://applitools.com/docs/topics/integrations/github-integration-ci-setup.html
@ -54,19 +47,22 @@ jobs:
env:
# e.g. mermaid-js/mermaid/my-branch
APPLITOOLS_BRANCH: ${{ github.repository }}/${{ github.ref_name }}
APPLITOOLS_PARENT_BRANCH: ${{ github.inputs.parent_branch }}
APPLITOOLS_PARENT_BRANCH: ${{ github.event.inputs.parent_branch }}
APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }}
APPLITOOLS_SERVER_URL: 'https://eyesapi.applitools.com'
- name: Run E2E Tests
run: pnpm run e2e
- name: Cypress run
uses: cypress-io/github-action@v4
id: cypress
with:
start: pnpm run dev
wait-on: 'http://localhost:9000'
env:
CYPRESS_CACHE_FOLDER: .cache/Cypress
# Mermaid applitools.config.js uses this to pick batch name.
APPLI_BRANCH: ${{ github.ref_name }}
APPLITOOLS_BATCH_ID: ${{ github.sha }}
# e.g. mermaid-js/mermaid/my-branch
APPLITOOLS_BRANCH: ${{ github.repository }}/${{ github.ref_name }}
APPLITOOLS_PARENT_BRANCH: ${{ github.inputs.parent_branch }}
APPLITOOLS_PARENT_BRANCH: ${{ github.event.inputs.parent_branch }}
APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }}
APPLITOOLS_SERVER_URL: 'https://eyesapi.applitools.com'

View File

@ -36,7 +36,7 @@ jobs:
restore-keys: cache-lychee-
- name: Link Checker
uses: lycheeverse/lychee-action@v1.6.1
uses: lycheeverse/lychee-action@v1.7.0
with:
args: >-
--verbose

View File

@ -5,10 +5,6 @@ on:
push:
branches:
- master
pull_request:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
@ -53,7 +49,6 @@ jobs:
# Deployment job
deploy:
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
environment:
name: github-pages
runs-on: ubuntu-latest
@ -61,4 +56,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
uses: actions/deploy-pages@v2

View File

@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: fregante/setup-git-user@v1
- uses: fregante/setup-git-user@v2
- uses: pnpm/action-setup@v2
# uses version from "packageManager" field in package.json

View File

@ -11,6 +11,7 @@ const visualize = process.argv.includes('--visualize');
const watch = process.argv.includes('--watch');
const mermaidOnly = process.argv.includes('--mermaid');
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const sourcemap = false;
type OutputOptions = Exclude<
Exclude<InlineConfig['build'], undefined>['rollupOptions'],
@ -60,9 +61,15 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions)
{
name,
format: 'esm',
sourcemap: true,
sourcemap,
entryFileNames: `${name}.esm${minify ? '.min' : ''}.mjs`,
},
{
name,
format: 'umd',
sourcemap,
entryFileNames: `${name}${minify ? '.min' : ''}.js`,
},
];
if (core) {
@ -79,7 +86,7 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions)
{
name,
format: 'esm',
sourcemap: true,
sourcemap,
entryFileNames: `${name}.core.mjs`,
},
];

View File

@ -286,7 +286,7 @@ describe('Class diagram', () => {
cy.get('svg');
});
it('15: should render a simple class diagram with css classes applied two multiple classes', () => {
it('15: should render a simple class diagram with css classes applied to multiple classes', () => {
imgSnapshotTest(
`
classDiagram

View File

@ -393,9 +393,9 @@ mindmap
<script type="module">
// import mindmap from '../../packages/mermaid-mindmap/src/detector';
import example from '../../packages/mermaid-example-diagram/src/mermaid-example-diagram.core.mjs';
// import example from '../../packages/mermaid-example-diagram/src/mermaid-example-diagram.core.mjs';
import mermaid from './mermaid.esm.mjs';
await mermaid.registerExternalDiagrams([example]);
// await mermaid.registerExternalDiagrams([example]);
mermaid.parseError = function (err, hash) {
// console.error('Mermaid error: ', err);
};

View File

@ -38,7 +38,7 @@
</pre>
<script type="module">
import mermaid from '../packages/mermaid/src/mermaid';
import mermaid from './mermaid.esm.mjs';
mermaid.initialize({
theme: 'forest',
// themeCSS: '.node rect { fill: red; }',

View File

@ -27,7 +27,7 @@ They also serve as proof of concept, for the variety of things that can be built
- [Swimm](https://swimm.io) (**Native support**)
- [Notion](https://notion.so) (**Native support**)
- [Observable](https://observablehq.com/@observablehq/mermaid) (**Native support**)
- [Obsidian](https://help.obsidian.md/How+to/Format+your+notes#Diagram) (**Native support**)
- [Obsidian](https://help.obsidian.md/Editing+and+formatting/Advanced+formatting+syntax#Diagram) (**Native support**)
- [GitBook](https://gitbook.com)
- [Mermaid Plugin](https://github.com/JozoVilcek/gitbook-plugin-mermaid)
- [Markdown with Mermaid CLI](https://github.com/miao1007/gitbook-plugin-mermaid-cli)
@ -161,6 +161,7 @@ They also serve as proof of concept, for the variety of things that can be built
- [codedoc-mermaid-plugin](https://www.npmjs.com/package/codedoc-mermaid-plugin)
- [mdbook](https://rust-lang.github.io/mdBook/index.html)
- [mdbook-mermaid](https://github.com/badboy/mdbook-mermaid)
- [Quarto](https://quarto.org/)
## Browser Extensions

View File

@ -128,7 +128,7 @@ classDiagram
Vehicle <|-- Car
```
Naming convention: a class name should be composed only of alphanumeric characters (including unicode), and underscores.
Naming convention: a class name should be composed only of alphanumeric characters (including unicode), underscores, and dashes (-).
### Class labels
@ -283,12 +283,12 @@ To describe the visibility (or encapsulation) of an attribute or method/function
- `#` Protected
- `~` Package/Internal
> _note_ you can also include additional _classifiers_ to a method definition by adding the following notation to the _end_ of the method, i.e.: after the `()`:
> _note_ you can also include additional _classifiers_ to a method definition by adding the following notation to the _end_ of the method, i.e.: after the `()` or after the return type:
>
> - `*` Abstract e.g.: `someAbstractMethod()*`
> - `$` Static e.g.: `someStaticMethod()$`
> - `*` Abstract e.g.: `someAbstractMethod()*` or `someAbstractMethod() int*`
> - `$` Static e.g.: `someStaticMethod()$` or `someStaticMethod() String$`
> _note_ you can also include additional _classifiers_ to a field definition by adding the following notation to the end of its name:
> _note_ you can also include additional _classifiers_ to a field definition by adding the following notation to the very end:
>
> - `$` Static e.g.: `String someField$`
@ -632,10 +632,26 @@ You would define these actions on a separate line after all classes have been de
## Notes
It is possible to add notes on diagram using `note "line1\nline2"` or note for class using `note for class "line1\nline2"`
It is possible to add notes on the diagram using `note "line1\nline2"`. A note can be added for a specific class using `note for <CLASS NAME> "line1\nline2"`.
### Examples
```mermaid-example
classDiagram
note "This is a general note"
note for MyClass "This is a note for a class"
class MyClass{
}
```
```mermaid
classDiagram
note "This is a general note"
note for MyClass "This is a note for a class"
class MyClass{
}
```
_URL Link:_
```mermaid-example

View File

@ -742,9 +742,9 @@ end
Formatting:
- For bold text, use double asterisks \*\* before and after the text.
- For italics, use single asterisks \* before and after the text.
- With traditional strings, you needed to add <br> tags for text to wrap in nodes. However, markdown strings automatically wrap text when it becomes too long and allows you to start a new line by simply using a newline character instead of a <br> tag.
- For bold text, use double asterisks (`**`) before and after the text.
- For italics, use single asterisks (`*`) before and after the text.
- With traditional strings, you needed to add `<br>` tags for text to wrap in nodes. However, markdown strings automatically wrap text when it becomes too long and allows you to start a new line by simply using a newline character instead of a `<br>` tag.
This feature is applicable to node labels, edge labels, and subgraph labels.

View File

@ -4,7 +4,7 @@
"version": "10.1.0",
"description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"type": "module",
"packageManager": "pnpm@7.30.1",
"packageManager": "pnpm@8.3.1",
"keywords": [
"diagram",
"markdown",
@ -54,65 +54,65 @@
]
},
"devDependencies": {
"@applitools/eyes-cypress": "^3.27.6",
"@commitlint/cli": "^17.2.0",
"@commitlint/config-conventional": "^17.2.0",
"@cspell/eslint-plugin": "^6.14.2",
"@rollup/plugin-typescript": "^11.0.0",
"@applitools/eyes-cypress": "^3.32.0",
"@commitlint/cli": "^17.6.1",
"@commitlint/config-conventional": "^17.6.1",
"@cspell/eslint-plugin": "^6.31.1",
"@rollup/plugin-typescript": "^11.1.0",
"@types/cors": "^2.8.13",
"@types/eslint": "^8.4.10",
"@types/eslint": "^8.37.0",
"@types/express": "^4.17.17",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^21.0.0",
"@types/lodash": "^4.14.188",
"@types/mdast": "^3.0.10",
"@types/node": "^18.11.9",
"@types/prettier": "^2.7.1",
"@types/jsdom": "^21.1.1",
"@types/lodash": "^4.14.194",
"@types/mdast": "^3.0.11",
"@types/node": "^18.16.0",
"@types/prettier": "^2.7.2",
"@types/rollup-plugin-visualizer": "^4.2.1",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",
"@vitest/coverage-c8": "^0.29.0",
"@vitest/spy": "^0.29.0",
"@vitest/ui": "^0.29.0",
"concurrently": "^7.5.0",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitest/coverage-c8": "^0.30.1",
"@vitest/spy": "^0.30.1",
"@vitest/ui": "^0.30.1",
"concurrently": "^8.0.1",
"cors": "^2.8.5",
"coveralls": "^3.1.1",
"cypress": "^12.0.0",
"cypress": "^12.10.0",
"cypress-image-snapshot": "^4.0.1",
"esbuild": "^0.17.0",
"eslint": "^8.32.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-cypress": "^2.12.1",
"esbuild": "^0.17.18",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-cypress": "^2.13.2",
"eslint-plugin-html": "^7.1.0",
"eslint-plugin-jest": "^27.1.5",
"eslint-plugin-jsdoc": "^39.6.2",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-jsdoc": "^43.0.7",
"eslint-plugin-json": "^3.1.0",
"eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-markdown": "^3.0.0",
"eslint-plugin-no-only-tests": "^3.1.0",
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^45.0.0",
"eslint-plugin-unicorn": "^46.0.0",
"express": "^4.18.2",
"globby": "^13.1.2",
"husky": "^8.0.2",
"jest": "^29.3.1",
"globby": "^13.1.4",
"husky": "^8.0.3",
"jest": "^29.5.0",
"jison": "^0.4.18",
"js-yaml": "^4.1.0",
"jsdom": "^21.0.0",
"lint-staged": "^13.0.3",
"jsdom": "^21.1.1",
"lint-staged": "^13.2.1",
"path-browserify": "^1.0.1",
"pnpm": "^7.15.0",
"prettier": "^2.7.1",
"pnpm": "^8.3.1",
"prettier": "^2.8.8",
"prettier-plugin-jsdoc": "^0.4.2",
"rimraf": "^4.0.0",
"rollup-plugin-visualizer": "^5.8.3",
"start-server-and-test": "^1.15.4",
"rimraf": "^5.0.0",
"rollup-plugin-visualizer": "^5.9.0",
"start-server-and-test": "^2.0.0",
"ts-node": "^10.9.1",
"typescript": "^4.8.4",
"vite": "^4.1.1",
"vitest": "^0.29.0"
"typescript": "^5.0.4",
"vite": "^4.3.1",
"vitest": "^0.30.1"
},
"volta": {
"node": "18.15.0"
"node": "18.16.0"
}
}

View File

@ -48,8 +48,8 @@
},
"devDependencies": {
"@types/cytoscape": "^3.19.9",
"concurrently": "^7.5.0",
"rimraf": "^4.0.0",
"concurrently": "^8.0.0",
"rimraf": "^5.0.0",
"mermaid": "workspace:*"
},
"resolutions": {

View File

@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "10.1.0",
"version": "10.2.0-rc.2",
"description": "Markdown-ish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"type": "module",
"module": "./dist/mermaid.core.mjs",
@ -52,20 +52,20 @@
]
},
"dependencies": {
"@braintree/sanitize-url": "^6.0.0",
"@khanacademy/simple-markdown": "^0.8.6",
"@braintree/sanitize-url": "^6.0.2",
"@khanacademy/simple-markdown": "^0.9.0",
"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": "2.4.5",
"dompurify": "3.0.2",
"elkjs": "^0.8.2",
"khroma": "^2.0.0",
"lodash-es": "^4.17.21",
"non-layered-tidy-tree-layout": "^2.0.2",
"stylis": "^4.1.2",
"stylis": "^4.1.3",
"ts-dedent": "^2.2.0",
"uuid": "^9.0.0",
"web-worker": "^1.2.0"
@ -73,43 +73,46 @@
"devDependencies": {
"@types/cytoscape": "^3.19.9",
"@types/d3": "^7.4.0",
"@types/dompurify": "^2.4.0",
"@types/jsdom": "^21.0.0",
"@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": "^9.0.0",
"@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",
"cpy-cli": "^4.2.0",
"cspell": "^6.14.3",
"cspell": "^6.31.1",
"csstree-validator": "^3.0.0",
"globby": "^13.1.2",
"globby": "^13.1.4",
"jison": "^0.4.18",
"js-base64": "^3.7.2",
"jsdom": "^21.0.0",
"js-base64": "^3.7.5",
"jsdom": "^21.1.1",
"micromatch": "^4.0.5",
"path-browserify": "^1.0.1",
"prettier": "^2.7.1",
"prettier": "^2.8.8",
"remark": "^14.0.2",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1",
"rimraf": "^4.0.0",
"start-server-and-test": "^1.14.0",
"typedoc": "^0.23.18",
"typedoc-plugin-markdown": "^3.13.6",
"typescript": "^4.8.4",
"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.46",
"vitepress-plugin-search": "^1.0.4-alpha.19"
"vitepress": "^1.0.0-alpha.72",
"vitepress-plugin-search": "^1.0.4-alpha.20"
},
"files": [
"dist",
"dist/",
"README.md"
],
"sideEffects": false
"sideEffects": false,
"publishConfig": {
"access": "public"
}
}

View File

@ -117,6 +117,7 @@ export const clear = function () {
export const getClass = function (id: string) {
return classes[id];
};
export const getClasses = function () {
return classes;
};
@ -181,9 +182,10 @@ export const addMember = function (className: string, member: string) {
const memberString = member.trim();
if (memberString.startsWith('<<') && memberString.endsWith('>>')) {
// Remove leading and trailing brackets
// 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));
@ -245,6 +247,7 @@ const setTooltip = function (ids: string, tooltip?: string) {
}
});
};
export const getTooltip = function (id: string, namespace?: string) {
if (namespace) {
return namespaces[namespace].classes[id].tooltip;

File diff suppressed because it is too large Load Diff

View File

@ -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);
});
});

View File

@ -0,0 +1,16 @@
import { readFile } from 'node:fs/promises';
// @ts-ignore - no types
import { LALRGenerator } from 'jison';
import path from 'path';
const getAbsolutePath = (relativePath: string) => {
return new URL(path.join(path.dirname(import.meta.url), relativePath)).pathname;
};
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);
});
});

View 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",
},
]
`);
});
});

View File

@ -222,9 +222,8 @@ Function arguments are optional: 'call <callback_name>()' simply executes 'callb
start
: mermaidDoc
| statments
| direction
| directive start
| statements
;
direction
@ -286,8 +285,8 @@ 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
@ -300,7 +299,6 @@ statement
| clickStatement
| cssClassStatement
| noteStatement
| directive
| direction
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
@ -409,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;

View File

@ -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,6 +361,9 @@ export const drawNote = function (elem, note, conf, diagObj) {
};
export const parseMember = function (text) {
// Note: these two regular expressions don't parse the official UML syntax for attributes
// and methods. They parse a Java-style syntax of the form
// "String name" (for attributes) and "String name(int x)" for methods
const fieldRegEx = /^([#+~-])?(\w+)(~\w+~|\[])?\s+(\w+) *([$*])?$/;
const methodRegEx = /^([#+|~-])?(\w+) *\( *(.*)\) *([$*])? *(\w*[[\]|~]*\s*\w*~?)$/;
@ -421,33 +430,48 @@ const buildLegacyDisplay = function (text) {
let displayText = '';
let cssStyle = '';
let returnType = '';
let visibility = '';
let firstChar = text.substring(0, 1);
let lastChar = text.substring(text.length - 1, text.length);
if (firstChar.match(/[#+~-]/)) {
visibility = firstChar;
}
let noClassifierRe = /[\s\w)~]/;
if (!lastChar.match(noClassifierRe)) {
cssStyle = parseClassifier(lastChar);
}
let startIndex = visibility === '' ? 0 : 1;
let endIndex = cssStyle === '' ? text.length : text.length - 1;
text = text.substring(startIndex, endIndex);
let methodStart = text.indexOf('(');
let methodEnd = text.indexOf(')');
if (methodStart > 1 && methodEnd > methodStart && methodEnd <= text.length) {
let visibility = '';
let methodName = '';
let firstChar = text.substring(0, 1);
if (firstChar.match(/\w/)) {
methodName = text.substring(0, methodStart).trim();
} else {
if (firstChar.match(/[#+~-]/)) {
visibility = firstChar;
}
methodName = text.substring(1, methodStart).trim();
}
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;
}
@ -502,6 +526,7 @@ const parseClassifier = function (classifier) {
};
export default {
getClassTitleString,
drawClass,
drawEdge,
drawNote,

View File

@ -1,8 +1,19 @@
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('');
});
});
});

View File

@ -1,6 +1,8 @@
import DOMPurify from 'dompurify';
import { MermaidConfig } from '../../config.type.js';
export const lineBreakRegex = /<br\s*\/?>/gi;
/**
* Gets the rows of lines in a string
*
@ -65,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
*

View File

@ -182,7 +182,7 @@ export const addVertices = async function (vert, svgId, root, doc, diagObj, pare
// Add the element to the DOM
if (node.type !== 'group') {
nodeEl = insertNode(nodes, node, vertex.dir);
nodeEl = await insertNode(nodes, node, vertex.dir);
boundingBox = nodeEl.node().getBBox();
} else {
const svgLabel = doc.createElementNS('http://www.w3.org/2000/svg', 'text');

View File

@ -56,7 +56,6 @@ const getStyles = (options) =>
font-size: 18px;
fill: ${options.textColor};
}
}
`;
export default getStyles;

View File

@ -21,7 +21,7 @@ They also serve as proof of concept, for the variety of things that can be built
- [Swimm](https://swimm.io) (**Native support**)
- [Notion](https://notion.so) (**Native support**)
- [Observable](https://observablehq.com/@observablehq/mermaid) (**Native support**)
- [Obsidian](https://help.obsidian.md/How+to/Format+your+notes#Diagram) (**Native support**)
- [Obsidian](https://help.obsidian.md/Editing+and+formatting/Advanced+formatting+syntax#Diagram) (**Native support**)
- [GitBook](https://gitbook.com)
- [Mermaid Plugin](https://github.com/JozoVilcek/gitbook-plugin-mermaid)
- [Markdown with Mermaid CLI](https://github.com/miao1007/gitbook-plugin-mermaid-cli)
@ -155,6 +155,7 @@ They also serve as proof of concept, for the variety of things that can be built
- [codedoc-mermaid-plugin](https://www.npmjs.com/package/codedoc-mermaid-plugin)
- [mdbook](https://rust-lang.github.io/mdBook/index.html)
- [mdbook-mermaid](https://github.com/badboy/mdbook-mermaid)
- [Quarto](https://quarto.org/)
## Browser Extensions

View File

@ -74,7 +74,7 @@ classDiagram
Vehicle <|-- Car
```
Naming convention: a class name should be composed only of alphanumeric characters (including unicode), and underscores.
Naming convention: a class name should be composed only of alphanumeric characters (including unicode), underscores, and dashes (-).
### Class labels
@ -171,12 +171,12 @@ To describe the visibility (or encapsulation) of an attribute or method/function
- `#` Protected
- `~` Package/Internal
> _note_ you can also include additional _classifiers_ to a method definition by adding the following notation to the _end_ of the method, i.e.: after the `()`:
> _note_ you can also include additional _classifiers_ to a method definition by adding the following notation to the _end_ of the method, i.e.: after the `()` or after the return type:
>
> - `*` Abstract e.g.: `someAbstractMethod()*`
> - `$` Static e.g.: `someStaticMethod()$`
> - `*` Abstract e.g.: `someAbstractMethod()*` or `someAbstractMethod() int*`
> - `$` Static e.g.: `someStaticMethod()$` or `someStaticMethod() String$`
> _note_ you can also include additional _classifiers_ to a field definition by adding the following notation to the end of its name:
> _note_ you can also include additional _classifiers_ to a field definition by adding the following notation to the very end:
>
> - `$` Static e.g.: `String someField$`
@ -420,10 +420,18 @@ click className href "url" "tooltip"
## Notes
It is possible to add notes on diagram using `note "line1\nline2"` or note for class using `note for class "line1\nline2"`
It is possible to add notes on the diagram using `note "line1\nline2"`. A note can be added for a specific class using `note for <CLASS NAME> "line1\nline2"`.
### Examples
```mermaid
classDiagram
note "This is a general note"
note for MyClass "This is a note for a class"
class MyClass{
}
```
_URL Link:_
```mmd

View File

@ -465,9 +465,9 @@ end
Formatting:
- For bold text, use double asterisks \*\* before and after the text.
- For italics, use single asterisks \* before and after the text.
- With traditional strings, you needed to add <br> tags for text to wrap in nodes. However, markdown strings automatically wrap text when it becomes too long and allows you to start a new line by simply using a newline character instead of a <br> tag.
- For bold text, use double asterisks (`**`) before and after the text.
- For italics, use single asterisks (`*`) before and after the text.
- With traditional strings, you needed to add `<br>` tags for text to wrap in nodes. However, markdown strings automatically wrap text when it becomes too long and allows you to start a new line by simply using a newline character instead of a `<br>` tag.
This feature is applicable to node labels, edge labels, and subgraph labels.

View File

@ -12,24 +12,24 @@ vi.mock('dagre-d3');
// mermaidAPI.spec.ts:
import * as accessibility from './accessibility.js'; // Import it this way so we can use spyOn(accessibility,...)
vi.mock('./accessibility', () => ({
vi.mock('./accessibility.js', () => ({
setA11yDiagramInfo: vi.fn(),
addSVGa11yTitleDescription: vi.fn(),
}));
// Mock the renderers specifically so we can test render(). Need to mock draw() for each renderer
vi.mock('./diagrams/c4/c4Renderer');
vi.mock('./diagrams/class/classRenderer');
vi.mock('./diagrams/class/classRenderer-v2');
vi.mock('./diagrams/er/erRenderer');
vi.mock('./diagrams/flowchart/flowRenderer-v2');
vi.mock('./diagrams/git/gitGraphRenderer');
vi.mock('./diagrams/gantt/ganttRenderer');
vi.mock('./diagrams/user-journey/journeyRenderer');
vi.mock('./diagrams/pie/pieRenderer');
vi.mock('./diagrams/requirement/requirementRenderer');
vi.mock('./diagrams/sequence/sequenceRenderer');
vi.mock('./diagrams/state/stateRenderer-v2');
vi.mock('./diagrams/c4/c4Renderer.js');
vi.mock('./diagrams/class/classRenderer.js');
vi.mock('./diagrams/class/classRenderer-v2.js');
vi.mock('./diagrams/er/erRenderer.js');
vi.mock('./diagrams/flowchart/flowRenderer-v2.js');
vi.mock('./diagrams/git/gitGraphRenderer.js');
vi.mock('./diagrams/gantt/ganttRenderer.js');
vi.mock('./diagrams/user-journey/journeyRenderer.js');
vi.mock('./diagrams/pie/pieRenderer.js');
vi.mock('./diagrams/requirement/requirementRenderer.js');
vi.mock('./diagrams/sequence/sequenceRenderer.js');
vi.mock('./diagrams/state/stateRenderer-v2.js');
// -------------------------------------
@ -52,7 +52,7 @@ import assignWithDepth from './assignWithDepth.js';
// --------------
// Mocks
// To mock a module, first define a mock for it, then (if used explicitly in the tests) import it. Be sure the path points to exactly the same file as is imported in mermaidAPI (the module being tested)
vi.mock('./styles', () => {
vi.mock('./styles.js', () => {
return {
addStylesForDiagram: vi.fn(),
default: vi.fn().mockReturnValue(' .userStyle { font-weight:bold; }'),

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,6 @@
"extends": [
"config:base",
":rebaseStalePrs",
"group:allNonMajor",
"schedule:earlyMondays",
":automergeMinor",
":automergeTesters",
@ -14,6 +13,18 @@
{
"matchUpdateTypes": ["minor", "patch", "digest"],
"automerge": true
},
{
"groupName": "all patch dependencies",
"groupSlug": "all-patch",
"matchPackagePatterns": ["*"],
"matchUpdateTypes": ["patch"]
},
{
"groupName": "all minor dependencies",
"groupSlug": "all-minor",
"matchPackagePatterns": ["*"],
"matchUpdateTypes": ["minor"]
}
],
"dependencyDashboard": true,

View File

@ -14,12 +14,17 @@ const lint = async (file: string): Promise<boolean> => {
console.log(`Linting ${file}`);
const jisonCode = await readFile(file, 'utf8');
// @ts-ignore no typings
const jsCode = new jison.Generator(jisonCode, { moduleType: 'amd' }).generate();
const generator = new jison.Generator(jisonCode, { moduleType: 'amd' });
const jsCode = generator.generate();
const [result] = await linter.lintText(jsCode);
if (result.errorCount > 0) {
console.error(`Linting failed for ${file}`);
console.error(result.messages);
}
if (generator.conflicts > 0) {
console.error(`Linting failed for ${file}. Conflicts found in grammar`);
return false;
}
return result.errorCount === 0;
};