Compare commits

..

65 Commits

Author SHA1 Message Date
Sidharth Vinod
345b75cc0e temp 2023-08-29 23:14:28 +05:30
Sidharth Vinod
0ebea7744b temp 2023-08-29 22:34:11 +05:30
Sidharth Vinod
44b93c039a Merge pull request #4727 from Yokozuna59/add-info-langium-parser
feat: add `@mermaid-js/parser` package and `info` langium parser
2023-08-28 08:10:30 +00:00
Sidharth Vinod
4d5313699e chore: Add comment for yy. 2023-08-28 13:39:37 +05:30
Sidharth Vinod
cd198290d7 Merge branch 'next' into pr/Yokozuna59/4727
* next:
  chore: Increase heap size when building
  fix(er): bug if relationship is declared first
  test(er): add cypress test on entity name alias
  feat(er): use square brackets to add aliases
  docs(er): add release version for entity name aliases
  feat(er): add entity name alias
2023-08-28 13:32:27 +05:30
Sidharth Vinod
60ed7d3273 chore: Increase heap size when building 2023-08-26 23:43:27 +05:30
Sidharth Vinod
9bcfba6620 Merge branch 'develop' into next
* develop:
  fix(er): bug if relationship is declared first
  test(er): add cypress test on entity name alias
  feat(er): use square brackets to add aliases
  docs(er): add release version for entity name aliases
  feat(er): add entity name alias
2023-08-26 23:09:05 +05:30
Reda Al Sulais
7ea3c64268 chore: increase test-util.ts converage by returning undefined 2023-08-26 14:37:36 +03:00
Reda Al Sulais
2b6a34e9e0 chore: add vitest imports to test-util.ts 2023-08-26 14:20:22 +03:00
Reda Al Sulais
458b90c78d chore: run pnpm lint:fix 2023-08-26 14:06:41 +03:00
Reda Al Sulais
dd284c0986 Merge branch 'add-info-langium-parser' of https://github.com/Yokozuna59/mermaid into add-info-langium-parser 2023-08-26 14:02:19 +03:00
Reda Al Sulais
21539dfb6a create noErrorsOrAlternatives parser helper function 2023-08-26 14:01:56 +03:00
Reda Al Sulais
91785b8284 Merge branch 'next' into add-info-langium-parser 2023-08-26 13:48:31 +03:00
Reda Al Sulais
f202770b70 Merge branch develop into next 2023-08-26 13:44:24 +03:00
Reda Al Sulais
8186a54962 chore: export InfoModule from infoModule.ts 2023-08-26 13:39:17 +03:00
Sidharth Vinod
866909b803 Merge branch 'develop' into next
* develop:
  chore: Update editor.bash to build latest version
  chore: Build after clone
  chore: Force install npm to avoid cache.
  fix: live editor exists error
  chore: Add netlify.toml
  chore: Update editor script
  chore: Add live editor build script for previews
  docs: Fix sankey demo
  feat(sankey): Show values (#4674)
2023-08-25 10:07:46 +05:30
Sidharth Vinod
408910e6e8 Merge branch 'develop' into next
* develop:
  Fixed docs according review
  chore: Fix type imports
  Fixed links
  Split development documentation in several separate pages, fixes for the sidebar menu
2023-08-23 13:19:12 +05:30
Reda Al Sulais
24c8e575f4 docs(parser): create packages/parser README.md file 2023-08-22 20:39:19 +03:00
Reda Al Sulais
8d0ca2c876 build: build .langium file using generate from langium-cli 2023-08-22 13:38:23 +03:00
Reda Al Sulais
fc96ebefd4 build: update langium and langium-cli to v2.0.1 2023-08-22 13:19:52 +03:00
Reda Al Sulais
394330175f Merge remote-tracking branch 'upstream/next' into add-info-langium-parser
Signed-off-by: Reda Al Sulais <u.yokozuna@gmail.com>
2023-08-22 13:13:03 +03:00
Sidharth Vinod
f946c3da06 Merge branch 'develop' into next
* develop:
  chore: Remove circular dependency
  Update docs
  docs: Add frontmatter config demos
  docs: Add frontmatter config docs
  fix: XSS vulnerability
  chore: Minor typo fixes
  chore: Add test with both frontmatter and directive
  Update docs
  feat: Add support for config in frontmatter
  chore: Fix type in assignWithDepth
  refactor: Move `sanitizeDirective` into `addDirective`
  refactor: Rename and cleanup `directiveSanitizer`
2023-08-22 13:58:24 +05:30
Sidharth Vinod
156fbd1958 Merge branch 'develop' into next
* develop:
  chore: Remove duplicate CI action
  chore: Add circular dependency check in CI
  refactor: Remove circular dependencies
2023-08-22 13:31:59 +05:30
Sidharth Vinod
7dd0d126e2 Merge branch 'develop' into next
* develop:
  deps: Update unocss and webpack to address vulnerability.
  chore(deps): update all patch dependencies
  ci(release-drafter): add more release notes categories
2023-08-22 10:21:13 +05:30
Reda Al Sulais
205360c109 fix: fix if statment logic checks if parser is not undefined 2023-08-21 03:09:05 +03:00
Reda Al Sulais
984a0e6d06 chore: add a comment illustrate why we build packages sequentially 2023-08-21 03:05:23 +03:00
Reda Al Sulais
eb63568ceb chore: refactore && into if in populateCommonDb 2023-08-21 03:04:39 +03:00
Reda Al Sulais
cc6f896b69 chore: remove ./* part from exports in parser/package.json
Co-authored-by: Alois Klink <alois@aloisklink.com>
2023-08-21 03:03:50 +03:00
Reda Al Sulais
83e47a7216 fix: use execFileSync instead of execSync in generateLangium 2023-08-21 02:54:30 +03:00
Reda Al Sulais
1d64549cce fix(mermaid): mark mermaid-parser dependecy with ^
Co-authored-by: Alois Klink <alois@aloisklink.com>
2023-08-21 02:54:13 +03:00
Reda Al Sulais
4ae361bd1f reorder packages/parser after packages/mermaid/src/vitepress 2023-08-20 18:45:05 +03:00
Reda Al Sulais
6502496a2c Merge remote-tracking branch 'upstream/next' into add-info-langium-parser 2023-08-20 18:41:32 +03:00
Sidharth Vinod
8678ceeb3c Merge pull request #4749 from Yokozuna59/remove-duplicate-dev-dependency
remove duplicate `@types/d3-scale` dev dependency
2023-08-20 15:38:50 +00:00
Reda Al Sulais
9cb62f4d2e remove duplicate @types/d3-scale dev dependency 2023-08-20 18:20:06 +03:00
Reda Al Sulais
6c0ef54e18 Merge branch 'next' into add-info-langium-parser 2023-08-20 18:01:37 +03:00
Sidharth Vinod
fd731c5ccd Merge branch 'develop' into next
* develop: (56 commits)
  chore: Add comments on redirectMaps
  remove `chart` from `pie.spec.ts` description
  Update docs
  change `defaultConfig` type to `RequiredDeep` and use it in `pieDb`
  use `DiagramStylesProvider` in `pieStyles.ts`
  remove `setConfig` and `resetConfig` in pie
  add `structuredClone` in pie `getConfig`
  cleanAndMerge pieConfig
  remove cleanClone
  feat: Add cleanAndMerge and tests
  chore: Rename utils.spec.ts
  move db assignment from `beforeEach` to `beforeAll`
  create `structuredCleanClone` helper function
  add more types to pieRenderer
  add `resetConfig` to `clear` in pieDb
  rename `reset` to `resetConfig`
  use `structedClone` in `pieDb`
  remove `PieDiagramConfig` and import generated one
  remove unnecessary lines in pie files
  remove unused `HTML` import in pieRenderer
  ...
2023-08-20 20:28:52 +05:30
Reda Al Sulais
cbe9490dc0 feat!: integrate info parser into mermaid package
BREAKING CHANGE: remove `showInfo` from `infoDb`.
2023-08-20 17:25:49 +03:00
Reda Al Sulais
82054bfabc chore: make parser as optional in ParserDefinition 2023-08-20 17:22:13 +03:00
Reda Al Sulais
963dd75c39 chore(parser): build parser package using esbuild and vite
Co-authored-by: Sidharth Vinod <sidharthv96@gmail.com>
2023-08-20 17:16:12 +03:00
Reda Al Sulais
1c24617f98 feat(parser): create info parser with exporting parser internals 2023-08-20 15:38:46 +03:00
Reda Al Sulais
1559c2ca21 feat(parser): create common directory for langium parsers 2023-08-20 15:36:02 +03:00
Reda Al Sulais
6141722b1f feat: create parser package in packages directory 2023-08-20 15:31:40 +03:00
Reda Al Sulais
222d8eed4e Merge remote-tracking branch 'upstream/develop' into next
Signed-off-by: Reda Al Sulais <u.yokozuna@gmail.com>
2023-08-19 16:20:13 +03:00
Sidharth Vinod
718d52a72c chore: Move liveReload code into script. 2023-08-17 14:30:47 +05:30
Sidharth Vinod
fe1a06271a Fix minify undefined 2023-08-17 12:55:25 +05:30
Sidharth Vinod
bd2370555b Merge branch 'develop' into next
* develop:
  chore: Move SVG import to comment.
  build docs
  Remove whitespace on empty line
  Documentation for #2509
2023-08-17 12:18:40 +05:30
Sidharth Vinod
86c9ee4e90 Merge pull request #4733 from mermaid-js/sidv/splitChunks
Split chunks into individual dirs
2023-08-17 06:47:23 +00:00
Sidharth Vinod
b26bcf1343 chore: Fix minify 2023-08-17 08:22:00 +05:30
Sidharth Vinod
5d5c6275f9 Merge branch 'develop' into next
* develop:
  Update all minor dependencies
  Update all patch dependencies
  make more `RectData` required and remove optional assignment
  use lineBreakRegex in `svgDrawCommon`
  fix svgDrawCommon import by adding `.js`
  add types to `svgDrawCommon.ts`
  convert `svgDrawCommon` to TS
2023-08-17 08:20:11 +05:30
Sidharth Vinod
9c1a47d1fc Merge pull request #4729 from mermaid-js/sidv/esbuildV11
Use ESBuild (replaces UMD with IIFE bundle)
2023-08-16 12:06:07 +00:00
Sidharth Vinod
13852b7f4e Fix import 2023-08-14 09:24:34 +05:30
Sidharth Vinod
4fd7a88a15 chore: Fix outfile names 2023-08-14 08:52:56 +05:30
Sidharth Vinod
5c2a6b5eb1 chore: Add analyzer comment 2023-08-14 08:37:02 +05:30
Sidharth Vinod
9cbebbb8a0 chore: Split chunks into folders 2023-08-14 08:35:49 +05:30
Sidharth Vinod
e26d987c4e chore: Split chunks into folders 2023-08-14 08:34:11 +05:30
Sidharth Vinod
53669efaf8 chore: Add defaultOptions to server 2023-08-14 08:30:51 +05:30
Sidharth Vinod
b68f45ef59 chore: Split chunks into folders 2023-08-14 08:27:14 +05:30
Sidharth Vinod
8f44de651b chore: IIFE to cSpell 2023-08-14 00:55:48 +05:30
Sidharth Vinod
2ede244da0 chore: Minor comments
Co-authored-by: Alois Klink <alois@aloisklink.com>
2023-08-14 00:55:48 +05:30
Sidharth Vinod
77a181978e chore: Replace Date.now with console.time
Co-authored-by: Alois Klink <alois@aloisklink.com>
2023-08-14 00:55:48 +05:30
Sidharth Vinod
170bbce0d3 chore: Build at start 2023-08-14 00:55:41 +05:30
Sidharth Vinod
fc99d9be41 chore: Add build times to live reload 2023-08-14 00:55:41 +05:30
Sidharth Vinod
9fb9bed806 chore: Add live-reload 2023-08-14 00:55:34 +05:30
Sidharth Vinod
01b2f80a95 chore: Remove @vitest/coverage-c8 2023-08-14 00:54:33 +05:30
Sidharth Vinod
da7ff777d1 chore: Add esbuild (Breaking change)
mermaid.min.js and mermaid.js will now be IIFE instead of UMD.
2023-08-14 00:52:45 +05:30
129 changed files with 5583 additions and 4772 deletions

25
.build/common.ts Normal file
View File

@@ -0,0 +1,25 @@
/**
* Shared common options for both ESBuild and Vite
*/
export const packageOptions = {
parser: {
name: 'mermaid-parser',
packageName: 'parser',
file: 'index.ts',
},
mermaid: {
name: 'mermaid',
packageName: 'mermaid',
file: 'mermaid.ts',
},
'mermaid-example-diagram': {
name: 'mermaid-example-diagram',
packageName: 'mermaid-example-diagram',
file: 'detector.ts',
},
'mermaid-zenuml': {
name: 'mermaid-zenuml',
packageName: 'mermaid-zenuml',
file: 'detector.ts',
},
} as const;

View File

@@ -0,0 +1,5 @@
import { generate } from 'langium-cli';
export async function generateLangium() {
await generate({ file: `./packages/parser/langium-config.json` });
}

122
.build/jsonSchema.ts Normal file
View File

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

9
.build/langium-cli.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
declare module 'langium-cli' {
export interface GenerateOptions {
file?: string;
mode?: 'development' | 'production';
watch?: boolean;
}
export function generate(options: GenerateOptions): Promise<boolean>;
}

65
.esbuild/build.ts Normal file
View File

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

15
.esbuild/jisonPlugin.ts Normal file
View File

@@ -0,0 +1,15 @@
import { readFile } from 'node:fs/promises';
import { transformJison } from '../.build/jisonTransformer.js';
import { Plugin } from 'esbuild';
export const jisonPlugin: Plugin = {
name: 'jison',
setup(build) {
build.onLoad({ filter: /\.jison$/ }, async (args) => {
// Load the file from the file system
const source = await readFile(args.path, 'utf8');
const contents = transformJison(source);
return { contents, warnings: [] };
});
},
};

View File

@@ -0,0 +1,35 @@
import type { JSONSchemaType } from 'ajv/dist/2019.js';
import type { MermaidConfig } from '../packages/mermaid/src/config.type.js';
import { readFile } from 'node:fs/promises';
import { getDefaults, getSchema, loadSchema } from '../.build/jsonSchema.js';
/**
* ESBuild plugin that handles JSON Schemas saved as a `.schema.yaml` file.
*
* Use `my-example.schema.yaml?only-defaults=true` to only load the default values.
*/
export const jsonSchemaPlugin = {
name: 'json-schema-plugin',
setup(build) {
let schema: JSONSchemaType<MermaidConfig> | undefined = undefined;
let content = '';
build.onLoad({ filter: /config\.schema\.yaml$/ }, async (args) => {
// Load the file from the file system
const source = await readFile(args.path, 'utf8');
const resolvedSchema: JSONSchemaType<MermaidConfig> =
content === source && schema ? schema : loadSchema(source, args.path);
if (content !== source) {
content = source;
schema = resolvedSchema;
}
const contents = args.suffix.includes('only-defaults')
? getDefaults(resolvedSchema)
: getSchema(resolvedSchema);
return { contents, warnings: [] };
});
},
};
export default jsonSchemaPlugin;

116
.esbuild/server.ts Normal file
View File

@@ -0,0 +1,116 @@
import express from 'express';
import type { NextFunction, Request, Response } from 'express';
import cors from 'cors';
import { getBuildConfig, defaultOptions } from './util.js';
import { context } from 'esbuild';
import chokidar from 'chokidar';
import { generateLangium } from '../.build/generateLangium.js';
const parserCtx = await context(
getBuildConfig({ ...defaultOptions, minify: false, core: false, entryName: 'parser' })
);
const mermaidCtx = await context(
getBuildConfig({ ...defaultOptions, minify: false, core: false, entryName: 'mermaid' })
);
const mermaidIIFECtx = await context(
getBuildConfig({
...defaultOptions,
minify: false,
core: false,
entryName: 'mermaid',
format: 'iife',
})
);
const externalCtx = await context(
getBuildConfig({
...defaultOptions,
minify: false,
core: false,
entryName: 'mermaid-example-diagram',
})
);
const zenumlCtx = await context(
getBuildConfig({ ...defaultOptions, minify: false, core: false, entryName: 'mermaid-zenuml' })
);
const contexts = [parserCtx, mermaidCtx, mermaidIIFECtx, externalCtx, zenumlCtx];
const rebuildAll = async () => {
console.time('Rebuild time');
await Promise.all(contexts.map((ctx) => ctx.rebuild()));
console.timeEnd('Rebuild time');
};
let clients: { id: number; response: Response }[] = [];
function eventsHandler(request: Request, response: Response, next: NextFunction) {
const headers = {
'Content-Type': 'text/event-stream',
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
};
response.writeHead(200, headers);
const clientId = Date.now();
clients.push({
id: clientId,
response,
});
request.on('close', () => {
clients = clients.filter((client) => client.id !== clientId);
});
}
let timeoutId: NodeJS.Timeout | undefined = undefined;
/**
* Debounce file change events to avoid rebuilding multiple times.
*/
function handleFileChange() {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(async () => {
await rebuildAll();
sendEventsToAll();
timeoutId = undefined;
}, 100);
}
function sendEventsToAll() {
clients.forEach(({ response }) => response.write(`data: ${Date.now()}\n\n`));
}
async function createServer() {
await generateLangium();
handleFileChange();
const app = express();
chokidar
.watch('**/src/**/*.{js,ts,langium,yaml,json}', {
ignoreInitial: true,
ignored: [/node_modules/, /dist/, /docs/, /coverage/],
})
.on('all', async (event, path) => {
// Ignore other events.
if (!['add', 'change'].includes(event)) {
return;
}
if (/\.langium$/.test(path)) {
await generateLangium();
}
console.log(`${path} changed. Rebuilding...`);
handleFileChange();
});
app.use(cors());
app.get('/events', eventsHandler);
app.use(express.static('./packages/parser/dist'));
app.use(express.static('./packages/mermaid/dist'));
app.use(express.static('./packages/mermaid-zenuml/dist'));
app.use(express.static('./packages/mermaid-example-diagram/dist'));
app.use(express.static('demos'));
app.use(express.static('cypress/platform'));
app.listen(9000, () => {
console.log(`Listening on http://localhost:9000`);
});
}
createServer();

98
.esbuild/util.ts Normal file
View File

@@ -0,0 +1,98 @@
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import type { BuildOptions } from 'esbuild';
import { readFileSync } from 'fs';
import jsonSchemaPlugin from './jsonSchemaPlugin.js';
import { packageOptions } from '../.build/common.js';
import { jisonPlugin } from './jisonPlugin.js';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
export interface MermaidBuildOptions {
minify: boolean;
core: boolean;
metafile: boolean;
format: 'esm' | 'iife';
entryName: keyof typeof packageOptions;
}
export const defaultOptions: Omit<MermaidBuildOptions, 'entryName'> = {
minify: false,
metafile: false,
core: false,
format: 'esm',
} as const;
const buildOptions = (override: BuildOptions): BuildOptions => {
return {
bundle: true,
minify: true,
keepNames: true,
platform: 'browser',
tsconfig: 'tsconfig.json',
resolveExtensions: ['.ts', '.js', '.json', '.jison', '.yaml'],
external: ['require', 'fs', 'path'],
outdir: 'dist',
plugins: [jisonPlugin, jsonSchemaPlugin],
sourcemap: 'external',
...override,
};
};
const getFileName = (fileName: string, { core, format, minify }: MermaidBuildOptions) => {
if (core) {
fileName += '.core';
} else if (format === 'esm') {
fileName += '.esm';
}
if (minify) {
fileName += '.min';
}
return fileName;
};
export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
const { core, entryName, metafile, format, minify } = options;
const external: string[] = ['require', 'fs', 'path'];
const { name, file, packageName } = packageOptions[entryName];
const outFileName = getFileName(name, options);
let output: BuildOptions = buildOptions({
absWorkingDir: resolve(__dirname, `../packages/${packageName}`),
entryPoints: {
[outFileName]: `src/${file}`,
},
metafile,
minify,
logLevel: 'info',
chunkNames: `chunks/${outFileName}/[name]-[hash]`,
});
if (core) {
const { dependencies } = JSON.parse(
readFileSync(resolve(__dirname, `../packages/${packageName}/package.json`), 'utf-8')
);
// Core build is used to generate file without bundled dependencies.
// This is used by downstream projects to bundle dependencies themselves.
// Ignore dependencies and any dependencies of dependencies
external.push(...Object.keys(dependencies));
output.external = external;
}
if (format === 'iife') {
output.format = 'iife';
output.splitting = false;
output.globalName = '__esbuild_esm_mermaid';
// Workaround for removing the .default access in esbuild IIFE.
// https://github.com/mermaid-js/mermaid/pull/4109#discussion_r1292317396
output.footer = {
js: 'globalThis.mermaid = globalThis.__esbuild_esm_mermaid.default;',
};
output.outExtension = { '.js': '.js' };
} else {
output.format = 'esm';
output.splitting = true;
output.outExtension = { '.js': '.mjs' };
}
return output;
};

View File

@@ -6,3 +6,6 @@ cypress/plugins/index.js
coverage
*.json
node_modules
# autogenereated by langium-cli
generated/

4
.gitignore vendored
View File

@@ -46,3 +46,7 @@ stats/
demos/dev/**
!/demos/dev/example.html
!/demos/dev/reload.js
# autogenereated by langium-cli
generated/

View File

@@ -10,3 +10,6 @@ stats
.nyc_output
# Autogenerated by `pnpm run --filter mermaid types:build-config`
packages/mermaid/src/config.type.ts
# autogenereated by langium-cli
generated/

View File

@@ -3,11 +3,12 @@ import { resolve } from 'path';
import { fileURLToPath } from 'url';
import jisonPlugin from './jisonPlugin.js';
import jsonSchemaPlugin from './jsonSchemaPlugin.js';
import { readFileSync } from 'fs';
import typescript from '@rollup/plugin-typescript';
import { visualizer } from 'rollup-plugin-visualizer';
import type { TemplateType } from 'rollup-plugin-visualizer/dist/plugin/template-types.js';
import istanbul from 'vite-plugin-istanbul';
import { packageOptions } from '../.build/common.js';
import { generateLangium } from '../.build/generateLangium.js';
const visualize = process.argv.includes('--visualize');
const watch = process.argv.includes('--watch');
@@ -36,24 +37,6 @@ const visualizerOptions = (packageName: string, core = false): PluginOption[] =>
);
};
const packageOptions = {
mermaid: {
name: 'mermaid',
packageName: 'mermaid',
file: 'mermaid.ts',
},
'mermaid-example-diagram': {
name: 'mermaid-example-diagram',
packageName: 'mermaid-example-diagram',
file: 'detector.ts',
},
'mermaid-zenuml': {
name: 'mermaid-zenuml',
packageName: 'mermaid-zenuml',
file: 'detector.ts',
},
};
interface BuildOptions {
minify: boolean | 'esbuild';
core?: boolean;
@@ -72,34 +55,8 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions)
sourcemap,
entryFileNames: `${name}.esm${minify ? '.min' : ''}.mjs`,
},
{
name,
format: 'umd',
sourcemap,
entryFileNames: `${name}${minify ? '.min' : ''}.js`,
},
];
if (core) {
const { dependencies } = JSON.parse(
readFileSync(resolve(__dirname, `../packages/${packageName}/package.json`), 'utf-8')
);
// Core build is used to generate file without bundled dependencies.
// This is used by downstream projects to bundle dependencies themselves.
// Ignore dependencies and any dependencies of dependencies
// Adapted from the RegEx used by `rollup-plugin-node`
external.push(new RegExp('^(?:' + Object.keys(dependencies).join('|') + ')(?:/.+)?$'));
// This needs to be an array. Otherwise vite will build esm & umd with same name and overwrite esm with umd.
output = [
{
name,
format: 'esm',
sourcemap,
entryFileNames: `${name}.core.mjs`,
},
];
}
const config: InlineConfig = {
configFile: false,
build: {
@@ -126,7 +83,7 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions)
// @ts-expect-error According to the type definitions, rollup plugins are incompatible with vite
typescript({ compilerOptions: { declaration: false } }),
istanbul({
exclude: ['node_modules', 'test/', '__mocks__'],
exclude: ['node_modules', 'test/', '__mocks__', 'generated'],
extension: ['.js', '.ts'],
requireEnv: true,
forceBuildInstrument: coverage,
@@ -146,24 +103,28 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions)
const buildPackage = async (entryName: keyof typeof packageOptions) => {
await build(getBuildConfig({ minify: false, entryName }));
await build(getBuildConfig({ minify: 'esbuild', entryName }));
await build(getBuildConfig({ minify: false, core: true, entryName }));
};
const main = async () => {
const packageNames = Object.keys(packageOptions) as (keyof typeof packageOptions)[];
for (const pkg of packageNames.filter((pkg) => !mermaidOnly || pkg === 'mermaid')) {
for (const pkg of packageNames.filter(
(pkg) => !mermaidOnly || pkg === 'mermaid' || pkg === 'parser'
)) {
await buildPackage(pkg);
}
};
await generateLangium();
if (watch) {
await build(getBuildConfig({ minify: false, watch, core: false, entryName: 'parser' }));
build(getBuildConfig({ minify: false, watch, core: false, entryName: 'mermaid' }));
if (!mermaidOnly) {
build(getBuildConfig({ minify: false, watch, entryName: 'mermaid-example-diagram' }));
build(getBuildConfig({ minify: false, watch, entryName: 'mermaid-zenuml' }));
}
} else if (visualize) {
await build(getBuildConfig({ minify: false, watch, core: false, entryName: 'parser' }));
await build(getBuildConfig({ minify: false, core: true, entryName: 'mermaid' }));
await build(getBuildConfig({ minify: false, core: false, entryName: 'mermaid' }));
} else {

View File

@@ -1,10 +1,10 @@
import { transformJison } from './jisonTransformer.js';
import { transformJison } from '../.build/jisonTransformer.js';
const fileRegex = /\.(jison)$/;
export default function jison() {
return {
name: 'jison',
transform(src: string, id: string) {
if (fileRegex.test(id)) {
return {

View File

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

View File

@@ -14,6 +14,7 @@ async function createServer() {
});
app.use(cors());
app.use(express.static('./packages/parser/dist'));
app.use(express.static('./packages/mermaid/dist'));
app.use(express.static('./packages/mermaid-zenuml/dist'));
app.use(express.static('./packages/mermaid-example-diagram/dist'));

View File

@@ -71,8 +71,6 @@ Documentation is necessary for all non bugfix/refactoring changes.
Only make changes to files that are in [`/packages/mermaid/src/docs`](packages/mermaid/src/docs)
**_DO NOT CHANGE FILES IN `/docs` MANUALLY_**
The `/docs` folder will be rebuilt and committed as part of a pre-commit hook.
**_DO NOT CHANGE FILES IN `/docs`_**
[Join our slack community if you want closer contact!](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE)

View File

@@ -38,10 +38,7 @@
"docsy",
"doku",
"dompurify",
"dont",
"doublecircle",
"edgechromium",
"elems",
"elkjs",
"elle",
"faber",
@@ -60,6 +57,7 @@
"gzipped",
"huynh",
"huynhicode",
"iife",
"inkdrop",
"jaoude",
"jgreywolf",
@@ -73,6 +71,7 @@
"knut",
"knutsveidqvist",
"laganeckas",
"langium",
"linetype",
"lintstagedrc",
"logmsg",
@@ -84,6 +83,7 @@
"mdbook",
"mermaidjs",
"mermerd",
"metafile",
"mindaugas",
"mindmap",
"mindmaps",
@@ -97,6 +97,7 @@
"nirname",
"npmjs",
"orlandoni",
"outdir",
"pathe",
"pbrolin",
"phpbb",

View File

@@ -0,0 +1,11 @@
describe('IIFE', () => {
beforeEach(() => {
cy.visit('http://localhost:9000/iife.html');
});
it('should render when using mermaid.min.js', () => {
cy.window().should('have.property', 'rendered', true);
cy.get('svg').should('be.visible');
cy.get('#d2').should('contain', 'Hello');
});
});

View File

@@ -1,16 +0,0 @@
describe('Sequencediagram', () => {
it('should render a simple sequence diagrams', () => {
const url = 'http://localhost:9000/webpackUsage.html';
cy.visit(url);
cy.get('body').find('svg').should('have.length', 1);
});
it('should handle html escapings properly', () => {
const url = 'http://localhost:9000/webpackUsage.html?test-html-escaping=true';
cy.visit(url);
cy.get('body').find('svg').should('have.length', 1);
cy.get('g.label > foreignobject > div').should('not.contain.text', '<b>');
});
});

View File

@@ -386,6 +386,30 @@ describe('Class diagram V2', () => {
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
});
it('18: should handle the direction statement with LR', () => {
imgSnapshotTest(
`
classDiagram
direction LR
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
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
});
it('17a: should handle the direction statement with BT', () => {
imgSnapshotTest(
`
@@ -433,31 +457,7 @@ describe('Class diagram V2', () => {
);
});
it('18a: should handle the direction statement with LR', () => {
imgSnapshotTest(
`
classDiagram
direction LR
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
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
});
it('18b: should render a simple class diagram with notes', () => {
it('18: should render a simple class diagram with notes', () => {
imgSnapshotTest(
`
classDiagram-v2
@@ -562,13 +562,4 @@ class C13["With Città foreign language"]
`
);
});
it('should render a simple class diagram with no members', () => {
imgSnapshotTest(
`
classDiagram-v2
class Class10
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
});
});

View File

@@ -1,7 +1,7 @@
<html>
<head>
<meta charset="utf-8" />
<script src="./viewer.js" type="module"></script>
<script type="module" src="./viewer.js"></script>
<link
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
rel="stylesheet"

View File

@@ -11,8 +11,7 @@ example-diagram
<!-- <script src="//cdn.jsdelivr.net/npm/mermaid@9.1.7/dist/mermaid.min.js"></script> -->
<!-- <script type="module" src="./external-diagrams-mindmap.mjs" /> -->
<script type="module">
import exampleDiagram from '../../packages/mermaid-example-diagram/dist/mermaid-example-diagram.core.mjs';
// import example from '../../packages/mermaid-example-diagram/src/detector';
import exampleDiagram from './mermaid-example-diagram.esm.mjs';
import mermaid from './mermaid.esm.mjs';
await mermaid.registerExternalDiagrams([exampleDiagram]);

View File

@@ -0,0 +1,29 @@
<html>
<body>
<pre id="diagram" class="mermaid">
graph TB
a --> b
a --> c
b --> d
c --> d
</pre>
<div id="d2"></div>
<script src="/mermaid.min.js"></script>
<script>
mermaid.initialize({
startOnLoad: true,
});
const value = `graph TD\nHello --> World`;
const el = document.getElementById('d2');
mermaid.render('did', value).then(({ svg }) => {
console.log(svg);
el.innerHTML = svg;
if (window.Cypress) {
window.rendered = true;
}
});
</script>
</body>
</html>

View File

@@ -17,20 +17,20 @@
graph TB
Function-->URL
click Function clickByFlow "Add a div"
click URL "http://localhost:9000/webpackUsage.html" "Visit <strong>mermaid docs</strong>"
click URL "http://localhost:9000/info.html" "Visit <strong>mermaid docs</strong>"
</pre>
<pre id="FirstLine" class="mermaid2">
graph TB
1Function-->2URL
click 1Function clickByFlow "Add a div"
click 2URL "http://localhost:9000/webpackUsage.html" "Visit <strong>mermaid docs</strong>"
click 2URL "http://localhost:9000/info.html" "Visit <strong>mermaid docs</strong>"
</pre>
<pre id="FirstLine" class="mermaid2">
classDiagram
class Test
class ShapeLink
link ShapeLink "http://localhost:9000/webpackUsage.html" "This is a tooltip for a link"
link ShapeLink "http://localhost:9000/info.html" "This is a tooltip for a link"
class ShapeCallback
callback ShapeCallback "clickByClass" "This is a tooltip for a callback"
</pre>
@@ -42,7 +42,7 @@
<pre id="FirstLine" class="mermaid">
classDiagram-v2
class ShapeLink
link ShapeLink "http://localhost:9000/webpackUsage.html" "This is a tooltip for a link"
link ShapeLink "http://localhost:9000/info.html" "This is a tooltip for a link"
</pre>
</div>
@@ -77,7 +77,7 @@
Calling a Callback (look at the console log) :cl2, after cl1, 3d
Calling a Callback with args :cl3, after cl1, 3d
click cl1 href "http://localhost:9000/webpackUsage.html"
click cl1 href "http://localhost:9000/info.html"
click cl2 call clickByGantt()
click cl3 call clickByGantt("test1", test2, test3)
@@ -102,9 +102,15 @@
div.className = 'created-by-gant-click';
div.style = 'padding: 20px; background: green; color: white;';
div.innerText = 'Clicked By Gant';
if (arg1) div.innerText += ' ' + arg1;
if (arg2) div.innerText += ' ' + arg2;
if (arg3) div.innerText += ' ' + arg3;
if (arg1) {
div.innerText += ' ' + arg1;
}
if (arg2) {
div.innerText += ' ' + arg2;
}
if (arg3) {
div.innerText += ' ' + arg3;
}
document.getElementsByTagName('body')[0].appendChild(div);
}

View File

@@ -1,6 +1,6 @@
import mermaid2 from './mermaid.esm.mjs';
import externalExample from '../../packages/mermaid-example-diagram/dist/mermaid-example-diagram.core.mjs';
import zenUml from '../../packages/mermaid-zenuml/dist/mermaid-zenuml.core.mjs';
import mermaid from './mermaid.esm.mjs';
import externalExample from './mermaid-example-diagram.esm.mjs';
import zenUml from './mermaid-zenuml.esm.mjs';
function b64ToUtf8(str) {
return decodeURIComponent(escape(window.atob(str)));
@@ -45,9 +45,9 @@ const contentLoaded = async function () {
document.getElementsByTagName('body')[0].appendChild(div);
}
await mermaid2.registerExternalDiagrams([externalExample, zenUml]);
mermaid2.initialize(graphObj.mermaid);
await mermaid2.run();
await mermaid.registerExternalDiagrams([externalExample, zenUml]);
mermaid.initialize(graphObj.mermaid);
await mermaid.run();
}
};
@@ -95,18 +95,14 @@ const contentLoadedApi = async function () {
divs[i] = div;
}
const defaultE2eCnf = { theme: 'forest' };
const defaultE2eCnf = { theme: 'forest', startOnLoad: false };
const cnf = merge(defaultE2eCnf, graphObj.mermaid);
mermaid2.initialize(cnf);
mermaid.initialize(cnf);
for (let i = 0; i < numCodes; i++) {
const { svg, bindFunctions } = await mermaid2.render(
'newid' + i,
graphObj.code[i],
divs[i]
);
const { svg, bindFunctions } = await mermaid.render('newid' + i, graphObj.code[i], divs[i]);
div.innerHTML = svg;
bindFunctions(div);
}
@@ -114,18 +110,21 @@ const contentLoadedApi = async function () {
const div = document.createElement('div');
div.id = 'block';
div.className = 'mermaid';
console.warn('graphObj.mermaid', graphObj.mermaid);
console.warn('graphObj', graphObj);
document.getElementsByTagName('body')[0].appendChild(div);
mermaid2.initialize(graphObj.mermaid);
const { svg, bindFunctions } = await mermaid2.render('newid', graphObj.code, div);
mermaid.initialize(graphObj.mermaid);
const { svg, bindFunctions } = await mermaid.render('newid', graphObj.code, div);
div.innerHTML = svg;
console.log(div.innerHTML);
bindFunctions(div);
}
}
};
if (typeof document !== 'undefined') {
mermaid.initialize({
startOnLoad: false,
});
/*!
* Wait for document loaded before starting the execution
*/

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<style>
/* .mermaid {
font-family: "trebuchet ms", verdana, arial;;
} */
/* .mermaid {
font-family: 'arial';
} */
</style>
</head>
<body>
<div id="graph-to-be"></div>
<script type="module" charset="utf-8">
import './bundle-test.js';
</script>
</body>
</html>

View File

@@ -1,6 +1,5 @@
<html>
<head>
<script src="./viewer.js" type="module"></script>
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
<style>
.malware {
@@ -33,12 +32,6 @@
</script>
</head>
<body>
<script type="module">
import mermaid from './mermaid.esm.mjs';
mermaid.initialize({
startOnLoad: false,
useMaxWidth: true,
});
</script>
<script type="module" src="./viewer.js"></script>
</body>
</html>

View File

@@ -38,14 +38,12 @@
+quack()
}
class Fish{
-Listint sizeInFeet
-int sizeInFeet
-canEat()
}
class Zebra{
+bool is_wild
+run(List~T~, List~OT~)
%% +run-composite(List~T, K~)
+run-nested(List~List~OT~~)
+run()
}
</pre>
@@ -82,7 +80,6 @@
Class01 : #size()
Class01 : -int chimp
Class01 : +int gorilla
Class01 : +abstractAttribute string*
class Class10~T~ {
&lt;&lt;service&gt;&gt;
int id
@@ -125,8 +122,6 @@
classDiagram
direction LR
Animal ()-- Dog
Animal ()-- Cat
note for Cat "should have no members area"
Dog : bark()
Dog : species()
</pre>
@@ -156,7 +151,6 @@
~InternalProperty : string
~AnotherInternalProperty : List~List~string~~
}
class People List~List~Person~~
</pre>
<hr />

View File

@@ -5,6 +5,8 @@
<title>Mermaid development page</title>
</head>
<body>
<pre class="mermaid">info</pre>
<pre id="diagram" class="mermaid">
graph TB
a --> b
@@ -30,5 +32,7 @@ graph TB
console.log(svg);
el.innerHTML = svg;
</script>
<script src="/dev/reload.js"></script>
</body>
</html>

22
demos/dev/reload.js Normal file
View File

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

View File

@@ -125,21 +125,6 @@
</pre>
<hr />
<pre class="mermaid">
erDiagram
_customer_order {
bigint id PK
bigint customer_id FK
text shipping_address
text delivery_method
timestamp_with_time_zone ordered_at
numeric total_tax_amount
numeric total_price
text payment_method
}
</pre>
<hr />
<script type="module">
import mermaid from './mermaid.esm.mjs';
mermaid.initialize({

View File

@@ -37,7 +37,7 @@
</pre>
<script type="module">
import mermaid from './mermaid.esm.mjs';
import mermaid from '/mermaid.esm.mjs';
mermaid.initialize({
theme: 'forest',
logLevel: 3,

View File

@@ -42,7 +42,7 @@ Once the release happens we add a tag to the `release` branch and merge it with
2. Check out the `develop` branch
3. Create a new branch for your work. Please name the branch following our naming convention below.
We use the following naming convention for branches:
We use the follow naming convention for branches:
```txt
[feature | bug | chore | docs]/[issue number]_[short description using dashes ('-') or underscores ('_') instead of spaces]

View File

@@ -22,7 +22,7 @@ In GitHub, you first **fork** a repository when you are going to make changes an
Then you **clone** a copy to your local development machine (e.g. where you code) to make a copy with all the files to work with.
[Fork mermaid](https://github.com/mermaid-js/mermaid/fork) to start contributing to the main project and its documentation, or [search for other repositories](https://github.com/orgs/mermaid-js/repositories).
[Fork mermaid](https://github.com/mermaid-js/mermaid/fork) to start contributing to the main project and its documentaion, or [search for other repositories](https://github.com/orgs/mermaid-js/repositories).
[Here is a GitHub document that gives an overview of the process.](https://docs.github.com/en/get-started/quickstart/fork-a-repo)

View File

@@ -22,7 +22,7 @@ In GitHub, you first **fork** a repository when you are going to make changes an
Then you **clone** a copy to your local development machine (e.g. where you code) to make a copy with all the files to work with.
[Fork mermaid](https://github.com/mermaid-js/mermaid/fork) to start contributing to the main project and its documentation, or [search for other repositories](https://github.com/orgs/mermaid-js/repositories).
[Fork mermaid](https://github.com/mermaid-js/mermaid/fork) to start contributing to the main project and its documentaion, or [search for other repositories](https://github.com/orgs/mermaid-js/repositories).
[Here is a GitHub document that gives an overview of the process.](https://docs.github.com/en/get-started/quickstart/fork-a-repo)

View File

@@ -19,7 +19,7 @@ For instance:
#### Store data found during parsing
There are some jison specific sub steps here where the parser stores the data encountered when parsing the diagram, this data is later used by the renderer. You can during the parsing call an object provided to the parser by the user of the parser. This object can be called during parsing for storing data.
There are some jison specific sub steps here where the parser stores the data encountered when parsing the diagram, this data is later used by the renderer. You can during the parsing call a object provided to the parser by the user of the parser. This object can be called during parsing for storing data.
```jison
statement
@@ -35,7 +35,7 @@ In the extract of the grammar above, it is defined that a call to the setTitle m
> **Note**
> Make sure that the `parseError` function for the parser is defined and calling `mermaid.parseError`. This way a common way of detecting parse errors is provided for the end-user.
For more info look at the example diagram type:
For more info look in the example diagram type:
The `yy` object has the following function:
@@ -54,7 +54,7 @@ parser.yy = db;
### Step 2: Rendering
Write a renderer that given the data found during parsing renders the diagram. To look at an example look at sequenceRenderer.js rather than the flowchart renderer as this is a more generic example.
Write a renderer that given the data found during parsing renders the diagram. To look at an example look at sequenceRenderer.js rather then the flowchart renderer as this is a more generic example.
Place the renderer in the diagram folder.
@@ -62,7 +62,7 @@ Place the renderer in the diagram folder.
The second thing to do is to add the capability to detect the new diagram to type to the detectType in `diagram-api/detectType.ts`. The detection should return a key for the new diagram type.
[This key will be used to as the aria roledescription](#aria-roledescription), so it should be a word that clearly describes the diagram type.
For example, if your new diagram uses a UML deployment diagram, a good key would be "UMLDeploymentDiagram" because assistive technologies such as a screen reader
For example, if your new diagram use a UML deployment diagram, a good key would be "UMLDeploymentDiagram" because assistive technologies such as a screen reader
would voice that as "U-M-L Deployment diagram." Another good key would be "deploymentDiagram" because that would be voiced as "Deployment Diagram." A bad key would be "deployment" because that would not sufficiently describe the diagram.
Note that the diagram type key does not have to be the same as the diagram keyword chosen for the [grammar](#grammar), but it is helpful if they are the same.
@@ -122,7 +122,7 @@ There are a few features that are common between the different types of diagrams
- Themes, there is a common way to modify the styling of diagrams in Mermaid.
- Comments should follow mermaid standards
Here are some pointers on how to handle these different areas.
Here some pointers on how to handle these different areas.
## Accessibility
@@ -140,7 +140,7 @@ See [the definition of aria-roledescription](https://www.w3.org/TR/wai-aria-1.1/
### accessible title and description
The syntax for accessible titles and descriptions is described in [the Accessibility documentation section.](../config/accessibility.md)
The syntax for accessible titles and descriptions is described in [the Accessibility documenation section.](../config/accessibility.md)
As a design goal, the jison syntax should be similar between the diagrams.

View File

@@ -126,7 +126,7 @@ The following code snippet changes `theme` to `forest`:
`%%{init: { "theme": "forest" } }%%`
Possible theme values are: `default`, `base`, `dark`, `forest` and `neutral`.
Possible theme values are: `default`,`base`, `dark`, `forest` and `neutral`.
Default Value is `default`.
Example:
@@ -291,7 +291,7 @@ Let us see an example:
sequenceDiagram
Alice->Bob: Hello Bob, how are you?
Bob->Alice: Fine, how did your mother like the book I suggested? And did you catch the new book about alien invasion?
Bob->Alice: Fine, how did you mother like the book I suggested? And did you catch the new book about alien invasion?
Alice->Bob: Good.
Bob->Alice: Cool
```
@@ -300,7 +300,7 @@ Bob->Alice: Cool
sequenceDiagram
Alice->Bob: Hello Bob, how are you?
Bob->Alice: Fine, how did your mother like the book I suggested? And did you catch the new book about alien invasion?
Bob->Alice: Fine, how did you mother like the book I suggested? And did you catch the new book about alien invasion?
Alice->Bob: Good.
Bob->Alice: Cool
```
@@ -317,7 +317,7 @@ By applying that snippet to the diagram above, `wrap` will be enabled:
%%{init: { "sequence": { "wrap": true, "width":300 } } }%%
sequenceDiagram
Alice->Bob: Hello Bob, how are you?
Bob->Alice: Fine, how did your mother like the book I suggested? And did you catch the new book about alien invasion?
Bob->Alice: Fine, how did you mother like the book I suggested? And did you catch the new book about alien invasion?
Alice->Bob: Good.
Bob->Alice: Cool
```
@@ -326,7 +326,7 @@ Bob->Alice: Cool
%%{init: { "sequence": { "wrap": true, "width":300 } } }%%
sequenceDiagram
Alice->Bob: Hello Bob, how are you?
Bob->Alice: Fine, how did your mother like the book I suggested? And did you catch the new book about alien invasion?
Bob->Alice: Fine, how did you mother like the book I suggested? And did you catch the new book about alien invasion?
Alice->Bob: Good.
Bob->Alice: Cool
```

View File

@@ -41,7 +41,7 @@ pnpm add mermaid
**Hosting mermaid on a web page:**
> Note: This topic is explored in greater depth in the [User Guide for Beginners](../intro/getting-started.md)
> Note:This topic explored in greater depth in the [User Guide for Beginners](../intro/getting-started.md)
The easiest way to integrate mermaid on a web page requires two elements:
@@ -64,7 +64,7 @@ Example:
```html
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
</script>
```
@@ -83,7 +83,7 @@ Example:
B-->D(fa:fa-spinner);
</pre>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
</script>
</body>
</html>
@@ -100,7 +100,7 @@ Mermaid can load multiple diagrams, in the same page.
## Enabling Click Event and Tags in Nodes
A `securityLevel` configuration has to first be cleared. `securityLevel` sets the level of trust for the parsed diagrams and limits click functionality. This was introduced in version 8.2 as a security improvement, aimed at preventing malicious use.
A `securityLevel` configuration has to first be cleared. `securityLevel` sets the level of trust for the parsed diagrams and limits click functionality. This was introduce in version 8.2 as a security improvement, aimed at preventing malicious use.
**It is the site owner's responsibility to discriminate between trustworthy and untrustworthy user-bases and we encourage the use of discretion.**
@@ -115,13 +115,13 @@ Values:
- **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 prevents any JavaScript from running in the context. This may hinder interactive functionality of the diagram, like scripts, popups in the sequence diagram, links to other tabs or targets, etc.
- **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 the sequence diagram, links to other tabs or targets, etc.
> **Note**
> This changes the default behaviour of mermaid so that after upgrade to 8.2, unless the `securityLevel` is not changed, tags in flowcharts are encoded as tags and clicking is disabled.
> **sandbox** security level is still in the beta version.
**If you are taking responsibility for the diagram source security you can set the `securityLevel` to a value of your choosing. This allows clicks and tags are allowed.**
**If you are taking responsibility for the diagram source security you can set the `securityLevel` to a value of your choosing . This allows clicks and tags are allowed.**
**To change `securityLevel`, you have to call `mermaid.initialize`:**

View File

@@ -49,7 +49,6 @@ They also serve as proof of concept, for the variety of things that can be built
- [Mermaid plugin for GitBook](https://github.com/wwformat/gitbook-plugin-mermaid-pdf)
- [LiveBook](https://livebook.dev) (**Native support**)
- [Atlassian Products](https://www.atlassian.com)
- [Mermaid Live Editor for Confluence Cloud](https://marketplace.atlassian.com/apps/1231571/mermaid-live-editor-for-confluence?hosting=cloud&tab=overview)
- [Mermaid Plugin for Confluence](https://marketplace.atlassian.com/apps/1214124/mermaid-plugin-for-confluence?hosting=server&tab=overview)
- [CloudScript.io Addon](https://marketplace.atlassian.com/apps/1219878/cloudscript-io-mermaid-addon?hosting=cloud&tab=overview)
- [Auto convert diagrams in Jira](https://github.com/coddingtonbear/jirafs-mermaid)

View File

@@ -103,7 +103,7 @@ When writing the .html file, we give two instructions inside the html code to th
a. The mermaid code for the diagram we want to create.
b. The importing of mermaid library through the `mermaid.esm.mjs` or `mermaid.esm.min.mjs` and the `mermaid.initialize()` call, which dictates the appearance of diagrams and also starts the rendering process.
b. The importing of mermaid library through the `mermaid.esm.mjs` or `mermaid.esm.min.mjs` and the `mermaid.initialize()` call, which dictates the appearance of diagrams and also starts the rendering process .
**a. The embedded mermaid diagram definition inside a `<pre class="mermaid">`:**
@@ -128,7 +128,7 @@ b. The importing of mermaid library through the `mermaid.esm.mjs` or `mermaid.es
```html
<body>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true });
</script>
</body>
@@ -168,7 +168,7 @@ Rendering in Mermaid is initialized by `mermaid.initialize()` call. However, doi
</pre>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true });
</script>
</body>
@@ -221,4 +221,4 @@ In this example mermaid.js is referenced in `src` as a separate JavaScript file,
**Comments from Knut Sveidqvist, creator of mermaid:**
- In early versions of mermaid, the `<script>` tag was invoked in the `<head>` part of the web page. Nowadays we can place it in the `<body>` as seen above. Older parts of the documentation frequently reflect the previous way which still works.
- In early versions of mermaid, the `<script>` tag was invoked in the `<head>` part of the web page. Nowadays we can place it in the `<body>` as seen above. Older parts of the documentation frequently reflects the previous way which still works.

View File

@@ -296,7 +296,7 @@ To select a version:
Replace `<version>` with the desired version number.
Latest Version: <https://cdn.jsdelivr.net/npm/mermaid@10>
Latest Version: <https://cdn.jsdelivr.net/npm/mermaid@11>
## Deploying Mermaid
@@ -314,7 +314,7 @@ To Deploy Mermaid:
```html
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true });
</script>
```

View File

@@ -90,7 +90,7 @@ Mermaid syntax for ER diagrams is compatible with PlantUML, with an extension to
Where:
- `first-entity` is the name of an entity. Names must begin with an alphabetic character or an underscore (from v\<MERMAID_RELEASE_VERSION>+), and may also contain digits and hyphens.
- `first-entity` is the name of an entity. Names must begin with an alphabetic character and may also contain digits, hyphens, and underscores.
- `relationship` describes the way that both entities inter-relate. See below.
- `second-entity` is the name of the other entity.
- `relationship-label` describes the relationship from the perspective of the first entity.

View File

@@ -1051,9 +1051,9 @@ flowchart LR
classDef foobar stroke:#00f
```
### CSS classes
### Css classes
It is also possible to predefine classes in CSS styles that can be applied from the graph definition as in the example
It is also possible to predefine classes in css styles that can be applied from the graph definition as in the example
below:
**Example style**

View File

@@ -58,7 +58,7 @@ mindmap
The syntax for creating Mindmaps is simple and relies on indentation for setting the levels in the hierarchy.
In the following example you can see how there are 3 different levels. One with starting at the left of the text and another level with two rows starting at the same column, defining the node A. At the end there is one more level where the text is indented further than the previous lines defining the nodes B and C.
In the following example you can see how there are 3 different levels. One with starting at the left of the text and another level with two rows starting at the same column, defining the node A. At the end there is one more level where the text is indented further then the previous lines defining the nodes B and C.
mindmap
Root
@@ -66,7 +66,7 @@ In the following example you can see how there are 3 different levels. One with
B
C
In summary is a simple text outline where there is one node at the root level called `Root` which has one child `A`. `A` in turn has two children `B`and `C`. In the diagram below we can see this rendered as a mindmap.
In summary is a simple text outline where there are one node at the root level called `Root` which has one child `A`. `A` in turn has two children `B`and `C`. In the diagram below we can see this rendered as a mindmap.
```mermaid-example
mindmap
@@ -228,7 +228,7 @@ _These classes need to be supplied by the site administrator._
## Unclear indentation
The actual indentation does not really matter only compared with the previous rows. If we take the previous example and disrupt it a little we can see how the calculations are performed. Let us start with placing C with a smaller indentation than `B` but larger then `A`.
The actual indentation does not really matter only compared with the previous rows. If we take the previous example and disrupt it a little we can se how the calculations are performed. Let us start with placing C with a smaller indentation than `B`but larger then `A`.
mindmap
Root
@@ -300,7 +300,7 @@ From version 9.4.0 you can simplify this code to:
```html
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
</script>
```

View File

@@ -47,8 +47,8 @@ quadrantChart
## Syntax
> **Note**
> If there are no points available in the chart both **axis** text and **quadrant** will be rendered in the center of the respective quadrant.
> If there are points **x-axis** labels will rendered from the left of the respective quadrant also they will be displayed at the bottom of the chart, and **y-axis** labels will be rendered at the bottom of the respective quadrant, the quadrant text will render at the top of the respective quadrant.
> If there is no points available in the chart both **axis** text and **quadrant** will be rendered in the center of the respective quadrant.
> If there are points **x-axis** labels will rendered from left of the respective quadrant also they will be displayed in bottom of the chart, and **y-axis** lables will be rendered in bottom of the respective quadrant, the quadrant text will render at top of the respective quadrant.
> **Note**
> For points x and y value min value is 0 and max value is 1.
@@ -64,7 +64,7 @@ The title is a short description of the chart and it will always render on top o
### x-axis
The x-axis determines what text would be displayed in the x-axis. In x-axis there is two part **left** and **right** you can pass **both** or you can pass only **left**. The statement should start with `x-axis` then the `left axis text` followed by the delimiter `-->` then `right axis text`.
The x-axis determine what text would be displayed in the x-axis. In x-axis there is two part **left** and **right** you can pass **both** or you can pass only **left**. The statement should start with `x-axis` then the `left axis text` followed by the delimiter `-->` then `right axis text`.
#### Example
@@ -73,7 +73,7 @@ The x-axis determines what text would be displayed in the x-axis. In x-axis ther
### y-axis
The y-axis determines what text would be displayed in the y-axis. In y-axis there is two part **top** and **bottom** you can pass **both** or you can pass only **bottom**. The statement should start with `y-axis` then the `bottom axis text` followed by the delimiter `-->` then `top axis text`.
The y-axis determine what text would be displayed in the y-axis. In y-axis there is two part **top** and **bottom** you can pass **both** or you can pass only **bottom**. The statement should start with `y-axis` then the `bottom axis text` followed by the delimiter `-->` then `top axis text`.
#### Example

View File

@@ -197,7 +197,7 @@ Electricity grid,H2 conversion,27.14
### Empty Lines
CSV does not support empty lines without comma delimiters by default. But you can add them if needed:
CSV does not support empty lines without comma delimeters by default. But you can add them if needed:
```mermaid-example
sankey-beta

View File

@@ -164,7 +164,7 @@ The actor(s) can be grouped in vertical boxes. You can define a color (if not, i
end
A->>J: Hello John, how are you?
J->>A: Great!
A->>B: Hello Bob, how is Charly?
A->>B: Hello Bob, how is Charly ?
B->>C: Hello Charly, how are you?
```
@@ -180,7 +180,7 @@ The actor(s) can be grouped in vertical boxes. You can define a color (if not, i
end
A->>J: Hello John, how are you?
J->>A: Great!
A->>B: Hello Bob, how is Charly?
A->>B: Hello Bob, how is Charly ?
B->>C: Hello Charly, how are you?
```

View File

@@ -469,7 +469,7 @@ You can use this method to add mermaid including the timeline diagram to a web p
```html
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
</script>
```

View File

@@ -4,7 +4,7 @@
"version": "10.2.4",
"description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"type": "module",
"packageManager": "pnpm@8.7.1",
"packageManager": "pnpm@8.6.12",
"keywords": [
"diagram",
"markdown",
@@ -15,14 +15,14 @@
"git graph"
],
"scripts": {
"build:vite": "ts-node-esm --transpileOnly .vite/build.ts",
"build:mermaid": "pnpm build:vite --mermaid",
"build:viz": "pnpm build:mermaid --visualize",
"build:types": "tsc -p ./packages/mermaid/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-zenuml/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-example-diagram/tsconfig.json --emitDeclarationOnly",
"build:watch": "pnpm build:vite --watch",
"build": "pnpm run -r clean && pnpm build:types && pnpm build:vite",
"dev": "concurrently \"pnpm build:vite --watch\" \"ts-node-esm .vite/server.ts\"",
"dev:coverage": "pnpm coverage:cypress:clean && VITE_COVERAGE=true pnpm dev",
"build": "pnpm build:esbuild && pnpm build:types",
"build:esbuild": "pnpm run -r clean && ts-node-esm --transpileOnly .esbuild/build.ts",
"build:mermaid": "pnpm build:esbuild --mermaid",
"build:viz": "pnpm build:esbuild --visualize",
"build:types": "tsc -p ./packages/parser/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-zenuml/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-example-diagram/tsconfig.json --emitDeclarationOnly",
"dev": "ts-node-esm --transpileOnly .esbuild/server.ts",
"dev:vite": "ts-node-esm --transpileOnly .vite/server.ts",
"dev:coverage": "pnpm coverage:cypress:clean && VITE_COVERAGE=true pnpm dev:vite",
"release": "pnpm build",
"lint": "eslint --cache --cache-strategy content --ignore-path .gitignore . && pnpm lint:jison && prettier --cache --check .",
"lint:fix": "eslint --cache --cache-strategy content --fix --ignore-path .gitignore . && prettier --write . && ts-node-esm scripts/fixCSpell.ts",
@@ -31,8 +31,8 @@
"cypress": "cypress run",
"cypress:open": "cypress open",
"e2e": "start-server-and-test dev http://localhost:9000/ cypress",
"e2e:coverage": "start-server-and-test dev:coverage http://localhost:9000/ cypress",
"coverage:cypress:clean": "rimraf .nyc_output coverage/cypress",
"e2e:coverage": "pnpm coverage:cypress:clean && VITE_COVERAGE=true pnpm e2e",
"coverage:merge": "ts-node-esm scripts/coverage.ts",
"coverage": "pnpm test:coverage --run && pnpm e2e:coverage && pnpm coverage:merge",
"ci": "vitest run",
@@ -82,6 +82,7 @@
"@vitest/spy": "^0.34.0",
"@vitest/ui": "^0.34.0",
"ajv": "^8.12.0",
"chokidar": "^3.5.3",
"concurrently": "^8.0.1",
"cors": "^2.8.5",
"cypress": "^12.10.0",
@@ -106,6 +107,7 @@
"jison": "^0.4.18",
"js-yaml": "^4.1.0",
"jsdom": "^22.0.0",
"langium-cli": "2.0.1",
"lint-staged": "^13.2.1",
"nyc": "^15.1.0",
"path-browserify": "^1.0.1",

View File

@@ -43,8 +43,7 @@
"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"
"khroma": "^2.0.0"
},
"devDependencies": {
"@types/cytoscape": "^3.19.9",

View File

@@ -1 +1 @@
../mermaid/src/docs/syntax/zenuml.md
../mermaid/src/docs/syntax/zenuml.md

View File

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

View File

@@ -5,7 +5,6 @@
* This is a dummy parser that satisfies the mermaid API logic.
*/
export default {
parser: { yy: {} },
parse: () => {
// no op
},

View File

@@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "10.4.0",
"version": "11.0.0-alpha.2",
"description": "Markdown-ish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"type": "module",
"module": "./dist/mermaid.core.mjs",
@@ -60,8 +60,6 @@
},
"dependencies": {
"@braintree/sanitize-url": "^6.0.1",
"@types/d3-scale": "^4.0.3",
"@types/d3-scale-chromatic": "^3.0.0",
"cytoscape": "^3.23.0",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.1.0",
@@ -74,11 +72,10 @@
"khroma": "^2.0.0",
"lodash-es": "^4.17.21",
"mdast-util-from-markdown": "^1.3.0",
"non-layered-tidy-tree-layout": "^2.0.2",
"mermaid-parser": "workspace:^",
"stylis": "^4.1.3",
"ts-dedent": "^2.2.0",
"uuid": "^9.0.0",
"web-worker": "^1.2.0"
"uuid": "^9.0.0"
},
"devDependencies": {
"@adobe/jsonschema2md": "^7.1.4",
@@ -86,6 +83,7 @@
"@types/d3": "^7.4.0",
"@types/d3-sankey": "^0.12.1",
"@types/d3-scale": "^4.0.3",
"@types/d3-scale-chromatic": "^3.0.0",
"@types/d3-selection": "^3.0.5",
"@types/d3-shape": "^3.1.1",
"@types/dompurify": "^3.0.2",
@@ -117,7 +115,7 @@
"rimraf": "^5.0.0",
"start-server-and-test": "^2.0.0",
"type-fest": "^4.1.0",
"typedoc": "^0.25.0",
"typedoc": "^0.24.5",
"typedoc-plugin-markdown": "^3.15.2",
"typescript": "^5.0.4",
"unist-util-flatmap": "^1.0.0",

View File

@@ -50,7 +50,10 @@ export class Diagram {
this.parser.parse = (text: string) =>
originalParse(cleanupComments(extractFrontMatter(text, this.db, configApi.addDirective)));
this.parser.parser.yy = this.db;
if (this.parser.parser !== undefined) {
// The parser.parser.yy is only present in JISON parsers. So, we'll only set if required.
this.parser.parser.yy = this.db;
}
this.init = diagram.init;
this.parse();
}

View File

@@ -368,7 +368,20 @@ const cutPathAtIntersect = (_points, boundryNode) => {
return points;
};
//(edgePaths, e, edge, clusterDb, diagramtype, graph)
/**
* Calculate the deltas and angle between two points
* @param {{x: number, y:number}} point1
* @param {{x: number, y:number}} point2
* @returns {{angle: number, deltaX: number, deltaY: number}}
*/
function calculateDeltaAndAngle(point1, point2) {
const [x1, y1] = [point1.x, point1.y];
const [x2, y2] = [point2.x, point2.y];
const deltaX = x2 - x1;
const deltaY = y2 - y1;
return { angle: Math.atan(deltaY / deltaX), deltaX, deltaY };
}
export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph) {
let points = edge.points;
let pointsHasChanged = false;
@@ -435,22 +448,52 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph
const lineData = points.filter((p) => !Number.isNaN(p.y));
// This is the accessor function we talked about above
let curve;
let curve = curveBasis;
// Currently only flowcharts get the curve from the settings, perhaps this should
// be expanded to a common setting? Restricting it for now in order not to cause side-effects that
// have not been thought through
if (diagramType === 'graph' || diagramType === 'flowchart') {
curve = edge.curve || curveBasis;
} else {
curve = curveBasis;
if (edge.curve && (diagramType === 'graph' || diagramType === 'flowchart')) {
curve = edge.curve;
}
// curve = curveLinear;
const markerOffsets = {
aggregation: 18,
extension: 18,
composition: 18,
dependency: 6,
lollipop: 13.5,
};
const lineFunction = line()
.x(function (d) {
return d.x;
.x(function (d, i, data) {
let offset = 0;
if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) {
const { angle, deltaX } = calculateDeltaAndAngle(data[0], data[1]);
offset = markerOffsets[edge.arrowTypeStart] * Math.cos(angle) * (deltaX >= 0 ? 1 : -1) || 0;
} else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) {
const { angle, deltaX } = calculateDeltaAndAngle(
data[data.length - 1],
data[data.length - 2]
);
offset = markerOffsets[edge.arrowTypeEnd] * Math.cos(angle) * (deltaX >= 0 ? 1 : -1) || 0;
}
return d.x + offset;
})
.y(function (d) {
return d.y;
.y(function (d, i, data) {
let offset = 0;
if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) {
const { angle, deltaY } = calculateDeltaAndAngle(data[0], data[1]);
offset =
markerOffsets[edge.arrowTypeStart] * Math.abs(Math.sin(angle)) * (deltaY >= 0 ? 1 : -1);
} else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) {
const { angle, deltaY } = calculateDeltaAndAngle(
data[data.length - 1],
data[data.length - 2]
);
offset =
markerOffsets[edge.arrowTypeEnd] * Math.abs(Math.sin(angle)) * (deltaY >= 0 ? 1 : -1);
}
return d.y + offset;
})
.curve(curve);
@@ -489,15 +532,15 @@ 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) => {
// elem
// .append('circle')
// .style('stroke', 'red')
// .style('fill', 'red')
// .attr('r', 1)
// .attr('cx', point.x)
// .attr('cy', point.y);
// });
edge.points.forEach((point) => {
elem
.append('circle')
.style('stroke', 'red')
.style('fill', 'red')
.attr('r', 1)
.attr('cx', point.x)
.attr('cy', point.y);
});
let url = '';
// // TODO: Can we load this config only from the rendered graph type?

View File

@@ -155,7 +155,7 @@ export const render = async (elem, graph, markers, diagramtype, id) => {
clearClusters();
clearGraphlib();
log.warn('Graph at first:', graphlibJson.write(graph));
log.warn('Graph at first:', JSON.stringify(graphlibJson.write(graph)));
adjustClustersAndEdges(graph);
log.warn('Graph after:', graphlibJson.write(graph));
// log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph));

View File

@@ -16,7 +16,7 @@ const extension = (elem, type, id) => {
.append('marker')
.attr('id', type + '-extensionStart')
.attr('class', 'marker extension ' + type)
.attr('refX', 0)
.attr('refX', 18)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
@@ -29,7 +29,7 @@ const extension = (elem, type, id) => {
.append('marker')
.attr('id', type + '-extensionEnd')
.attr('class', 'marker extension ' + type)
.attr('refX', 19)
.attr('refX', 1)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
@@ -44,7 +44,7 @@ const composition = (elem, type) => {
.append('marker')
.attr('id', type + '-compositionStart')
.attr('class', 'marker composition ' + type)
.attr('refX', 0)
.attr('refX', 18)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
@@ -57,7 +57,7 @@ const composition = (elem, type) => {
.append('marker')
.attr('id', type + '-compositionEnd')
.attr('class', 'marker composition ' + type)
.attr('refX', 19)
.attr('refX', 1)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
@@ -71,7 +71,7 @@ const aggregation = (elem, type) => {
.append('marker')
.attr('id', type + '-aggregationStart')
.attr('class', 'marker aggregation ' + type)
.attr('refX', 0)
.attr('refX', 18)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
@@ -84,7 +84,7 @@ const aggregation = (elem, type) => {
.append('marker')
.attr('id', type + '-aggregationEnd')
.attr('class', 'marker aggregation ' + type)
.attr('refX', 19)
.attr('refX', 1)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
@@ -98,7 +98,7 @@ const dependency = (elem, type) => {
.append('marker')
.attr('id', type + '-dependencyStart')
.attr('class', 'marker dependency ' + type)
.attr('refX', 0)
.attr('refX', 6)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
@@ -111,7 +111,7 @@ const dependency = (elem, type) => {
.append('marker')
.attr('id', type + '-dependencyEnd')
.attr('class', 'marker dependency ' + type)
.attr('refX', 19)
.attr('refX', 13)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
@@ -125,15 +125,31 @@ const lollipop = (elem, type) => {
.append('marker')
.attr('id', type + '-lollipopStart')
.attr('class', 'marker lollipop ' + type)
.attr('refX', 0)
.attr('refX', 13)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('circle')
.attr('stroke', 'black')
.attr('fill', 'white')
.attr('cx', 6)
.attr('fill', 'transparent')
.attr('cx', 7)
.attr('cy', 7)
.attr('r', 6);
elem
.append('defs')
.append('marker')
.attr('id', type + '-lollipopEnd')
.attr('class', 'marker lollipop ' + type)
.attr('refX', 1)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('circle')
.attr('stroke', 'black')
.attr('fill', 'transparent')
.attr('cx', 7)
.attr('cy', 7)
.attr('r', 6);
};

View File

@@ -291,8 +291,8 @@ export const adjustClustersAndEdges = (graph, depth) => {
shape: 'labelRect',
style: '',
});
const edge1 = JSON.parse(JSON.stringify(edge));
const edge2 = JSON.parse(JSON.stringify(edge));
const edge1 = structuredClone(edge);
const edge2 = structuredClone(edge);
edge1.label = '';
edge1.arrowTypeEnd = 'none';
edge2.label = '';

View File

@@ -5,6 +5,7 @@ import { getConfig } from '../config.js';
import intersect from './intersect/index.js';
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 formatClass = (str) => {
@@ -879,8 +880,8 @@ const class_box = (parent, node) => {
maxWidth = classTitleBBox.width;
}
const classAttributes = [];
node.classData.members.forEach((member) => {
const parsedInfo = member.getDisplayDetails();
node.classData.members.forEach((str) => {
const parsedInfo = parseMember(str);
let parsedText = parsedInfo.displayText;
if (getConfig().flowchart.htmlLabels) {
parsedText = parsedText.replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -913,8 +914,8 @@ const class_box = (parent, node) => {
maxHeight += lineHeight;
const classMethods = [];
node.classData.methods.forEach((member) => {
const parsedInfo = member.getDisplayDetails();
node.classData.methods.forEach((str) => {
const parsedInfo = parseMember(str);
let displayText = parsedInfo.displayText;
if (getConfig().flowchart.htmlLabels) {
displayText = displayText.replace(/</g, '&lt;').replace(/>/g, '&gt;');

View File

@@ -45,7 +45,6 @@ export const addDiagrams = () => {
styles: {}, // should never be used
renderer: {}, // should never be used
parser: {
parser: { yy: {} },
parse: () => {
throw new Error(
'Diagrams beginning with --- are not valid. ' +

View File

@@ -39,7 +39,6 @@ describe('DiagramAPI', () => {
parse: (_text) => {
return;
},
parser: { yy: {} },
},
renderer: {},
styles: {},

View File

@@ -80,7 +80,7 @@ export type DrawDefinition = (
export interface ParserDefinition {
parse: (text: string) => void;
parser: { yy: DiagramDB };
parser?: { yy: DiagramDB };
}
/**

View File

@@ -30,9 +30,6 @@ describe('diagram detection', () => {
parse: () => {
// no-op
},
parser: {
yy: {},
},
},
renderer: {},
styles: {},

View File

@@ -15,7 +15,6 @@ import {
setDiagramTitle,
getDiagramTitle,
} from '../../commonDb.js';
import { ClassMember } from './classTypes.js';
import type {
ClassRelation,
ClassNode,
@@ -116,11 +115,11 @@ export const clear = function () {
commonClear();
};
export const getClass = function (id: string): ClassNode {
export const getClass = function (id: string) {
return classes[id];
};
export const getClasses = function (): ClassMap {
export const getClasses = function () {
return classes;
};
@@ -188,9 +187,9 @@ export const addMember = function (className: string, member: string) {
theClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2)));
} else if (memberString.indexOf(')') > 0) {
//its a method
theClass.methods.push(new ClassMember(memberString, 'method'));
theClass.methods.push(sanitizeText(memberString));
} else if (memberString) {
theClass.members.push(new ClassMember(memberString, 'attribute'));
theClass.members.push(sanitizeText(memberString));
}
}
};
@@ -257,7 +256,6 @@ export const getTooltip = function (id: string, namespace?: string) {
return classes[id].tooltip;
};
/**
* Called by parser when a link is found. Adds the URL to the vertex data.
*

View File

@@ -4,9 +4,6 @@ import classDb from './classDb.js';
import { vi, describe, it, expect } from 'vitest';
const spyOn = vi.spyOn;
const staticCssStyle = 'text-decoration:underline;';
const abstractCssStyle = 'font-style:italic;';
describe('given a basic class diagram, ', function () {
describe('when parsing class definition', function () {
beforeEach(function () {
@@ -130,7 +127,7 @@ describe('given a basic class diagram, ', function () {
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.members.length).toBe(1);
expect(c1.members[0].getDisplayDetails().displayText).toBe('member1');
expect(c1.members[0]).toBe('member1');
});
it('should parse a class with a text label, member and annotation', () => {
@@ -145,7 +142,7 @@ describe('given a basic class diagram, ', function () {
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.members.length).toBe(1);
expect(c1.members[0].getDisplayDetails().displayText).toBe('int member1');
expect(c1.members[0]).toBe('int member1');
expect(c1.annotations.length).toBe(1);
expect(c1.annotations[0]).toBe('interface');
});
@@ -171,7 +168,7 @@ describe('given a basic class diagram, ', function () {
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.members[0].getDisplayDetails().displayText).toBe('int member1');
expect(c1.members[0]).toBe('int member1');
expect(c1.cssClasses[0]).toBe('styleClass');
});
@@ -374,74 +371,6 @@ class C13["With Città foreign language"]
note ${keyword}`;
expect(() => parser.parse(str)).toThrowError(/(Expecting\s'STR'|Unrecognized\stext)/);
});
it('should parse diagram with direction', () => {
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-134",
"id": "Student",
"label": "Student",
"members": [
ClassMember {
"classifier": "",
"id": "idCard : IdCard",
"memberType": "attribute",
"visibility": "-",
},
],
"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",
},
]
`);
});
});
describe('when parsing class defined in brackets', function () {
@@ -493,8 +422,8 @@ class C13["With Città foreign language"]
'classDiagram\n' +
'class Class1 {\n' +
'int testMember\n' +
'test()\n' +
'string fooMember\n' +
'test()\n' +
'foo()\n' +
'}';
parser.parse(str);
@@ -502,10 +431,10 @@ class C13["With Città foreign language"]
const actual = parser.yy.getClass('Class1');
expect(actual.members.length).toBe(2);
expect(actual.methods.length).toBe(2);
expect(actual.members[0].getDisplayDetails().displayText).toBe('int testMember');
expect(actual.members[1].getDisplayDetails().displayText).toBe('string fooMember');
expect(actual.methods[0].getDisplayDetails().displayText).toBe('test()');
expect(actual.methods[1].getDisplayDetails().displayText).toBe('foo()');
expect(actual.members[0]).toBe('int testMember');
expect(actual.members[1]).toBe('string fooMember');
expect(actual.methods[0]).toBe('test()');
expect(actual.methods[1]).toBe('foo()');
});
it('should parse a class with a text label and members', () => {
@@ -515,7 +444,7 @@ class C13["With Città foreign language"]
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.members.length).toBe(1);
expect(c1.members[0].getDisplayDetails().displayText).toBe('+member1');
expect(c1.members[0]).toBe('+member1');
});
it('should parse a class with a text label, members and annotation', () => {
@@ -530,7 +459,7 @@ class C13["With Città foreign language"]
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.members.length).toBe(1);
expect(c1.members[0].getDisplayDetails().displayText).toBe('+member1');
expect(c1.members[0]).toBe('+member1');
expect(c1.annotations.length).toBe(1);
expect(c1.annotations[0]).toBe('interface');
});
@@ -833,10 +762,10 @@ describe('given a class diagram with members and methods ', function () {
const actual = parser.yy.getClass('actual');
expect(actual.members.length).toBe(4);
expect(actual.methods.length).toBe(0);
expect(actual.members[0].getDisplayDetails().displayText).toBe('-int privateMember');
expect(actual.members[1].getDisplayDetails().displayText).toBe('+int publicMember');
expect(actual.members[2].getDisplayDetails().displayText).toBe('#int protectedMember');
expect(actual.members[3].getDisplayDetails().displayText).toBe('~int privatePackage');
expect(actual.members[0]).toBe('-int privateMember');
expect(actual.members[1]).toBe('+int publicMember');
expect(actual.members[2]).toBe('#int protectedMember');
expect(actual.members[3]).toBe('~int privatePackage');
});
it('should handle generic types', function () {
@@ -889,9 +818,7 @@ describe('given a class diagram with members and methods ', function () {
expect(actual.annotations.length).toBe(0);
expect(actual.members.length).toBe(0);
expect(actual.methods.length).toBe(1);
const method = actual.methods[0];
expect(method.getDisplayDetails().displayText).toBe('someMethod()');
expect(method.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
expect(actual.methods[0]).toBe('someMethod()*');
});
it('should handle static methods', function () {
@@ -902,9 +829,7 @@ describe('given a class diagram with members and methods ', function () {
expect(actual.annotations.length).toBe(0);
expect(actual.members.length).toBe(0);
expect(actual.methods.length).toBe(1);
const method = actual.methods[0];
expect(method.getDisplayDetails().displayText).toBe('someMethod()');
expect(method.getDisplayDetails().cssStyle).toBe(staticCssStyle);
expect(actual.methods[0]).toBe('someMethod()$');
});
it('should handle generic types in arguments', function () {
@@ -1030,6 +955,53 @@ foo()
parser.parse(str);
});
});
describe('when parsing invalid generic classes', function () {
beforeEach(function () {
classDb.clear();
parser.yy = classDb;
});
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);
});
});
});
describe('given a class diagram with relationships, ', function () {
@@ -1302,10 +1274,10 @@ describe('given a class diagram with relationships, ', function () {
const testClass = parser.yy.getClass('Class1');
expect(testClass.members.length).toBe(2);
expect(testClass.methods.length).toBe(2);
expect(testClass.members[0].getDisplayDetails().displayText).toBe('int : test');
expect(testClass.members[1].getDisplayDetails().displayText).toBe('string : foo');
expect(testClass.methods[0].getDisplayDetails().displayText).toBe('test()');
expect(testClass.methods[1].getDisplayDetails().displayText).toBe('foo()');
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 () {
@@ -1316,9 +1288,7 @@ describe('given a class diagram with relationships, ', function () {
expect(testClass.annotations.length).toBe(0);
expect(testClass.members.length).toBe(0);
expect(testClass.methods.length).toBe(1);
const method = testClass.methods[0];
expect(method.getDisplayDetails().displayText).toBe('someMethod()');
expect(method.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
expect(testClass.methods[0]).toBe('someMethod()*');
});
it('should handle static methods', function () {
@@ -1329,9 +1299,7 @@ describe('given a class diagram with relationships, ', function () {
expect(testClass.annotations.length).toBe(0);
expect(testClass.members.length).toBe(0);
expect(testClass.methods.length).toBe(1);
const method = testClass.methods[0];
expect(method.getDisplayDetails().displayText).toBe('someMethod()');
expect(method.getDisplayDetails().cssStyle).toBe(staticCssStyle);
expect(testClass.methods[0]).toBe('someMethod()$');
});
it('should associate link and css appropriately', function () {
@@ -1546,19 +1514,11 @@ class Class2
const testClasses = parser.yy.getClasses();
const testRelations = parser.yy.getRelations();
expect(Object.keys(testNamespaceA.classes).length).toBe(2);
expect(testNamespaceA.classes['A1'].members[0].getDisplayDetails().displayText).toBe(
'+foo : string'
);
expect(testNamespaceA.classes['A2'].members[0].getDisplayDetails().displayText).toBe(
'+bar : int'
);
expect(testNamespaceA.classes['A1'].members[0]).toBe('+foo : string');
expect(testNamespaceA.classes['A2'].members[0]).toBe('+bar : int');
expect(Object.keys(testNamespaceB.classes).length).toBe(2);
expect(testNamespaceB.classes['B1'].members[0].getDisplayDetails().displayText).toBe(
'+foo : bool'
);
expect(testNamespaceB.classes['B2'].members[0].getDisplayDetails().displayText).toBe(
'+bar : float'
);
expect(testNamespaceB.classes['B1'].members[0]).toBe('+foo : bool');
expect(testNamespaceB.classes['B2'].members[0]).toBe('+bar : float');
expect(Object.keys(testClasses).length).toBe(4);
expect(testClasses['A1'].parent).toBe('A');
expect(testClasses['A2'].parent).toBe('A');
@@ -1610,8 +1570,8 @@ class Class2
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.members.length).toBe(1);
const member = c1.members[0];
expect(member.getDisplayDetails().displayText).toBe('+member1');
expect(c1.members[0]).toBe('+member1');
const c2 = classDb.getClass('C2');
expect(c2.label).toBe('C2');
});
@@ -1627,10 +1587,9 @@ class Class2
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.members.length).toBe(1);
expect(c1.members[0]).toBe('+member1');
expect(c1.annotations.length).toBe(1);
expect(c1.annotations[0]).toBe('interface');
const member = c1.members[0];
expect(member.getDisplayDetails().displayText).toBe('+member1');
const c2 = classDb.getClass('C2');
expect(c2.label).toBe('C2');
@@ -1647,9 +1606,8 @@ C1 --> C2
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.cssClasses.length).toBe(1);
expect(c1.members[0]).toBe('+member1');
expect(c1.cssClasses[0]).toBe('styleClass');
const member = c1.members[0];
expect(member.getDisplayDetails().displayText).toBe('+member1');
});
it('should parse a class with text label and css class', () => {
@@ -1664,9 +1622,8 @@ cssClass "C1" styleClass
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.cssClasses.length).toBe(1);
expect(c1.members[0]).toBe('+member1');
expect(c1.cssClasses[0]).toBe('styleClass');
const member = c1.members[0];
expect(member.getDisplayDetails().displayText).toBe('+member1');
});
it('should parse two classes with text labels and css classes', () => {

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

@@ -156,17 +156,24 @@ export const addNotes = function (
) {
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,
@@ -294,7 +301,7 @@ export const setConf = function (cnf: any) {
};
/**
* Draws a class diagram in the tag with id: id based on the definition in text.
* Draws a flowchart in the tag with id: id based on the graph definition in text.
*
* @param text -
* @param id -

View File

@@ -1,683 +0,0 @@
import { ClassMember } from './classTypes.js';
import { vi, describe, it, expect } from 'vitest';
const spyOn = vi.spyOn;
const staticCssStyle = 'text-decoration:underline;';
const abstractCssStyle = 'font-style:italic;';
describe('given text representing a method, ', function () {
describe('when method has no parameters', function () {
it('should parse correctly', function () {
const str = `getTime()`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle public visibility', function () {
const str = `+getTime()`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTime()');
});
it('should handle private visibility', function () {
const str = `-getTime()`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTime()');
});
it('should handle protected visibility', function () {
const str = `#getTime()`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTime()');
});
it('should handle internal visibility', function () {
const str = `~getTime()`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTime()');
});
it('should return correct css for static classifier', function () {
const str = `getTime()$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime()');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTime()*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime()');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method has single parameter value', function () {
it('should parse correctly', function () {
const str = `getTime(int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle public visibility', function () {
const str = `+getTime(int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTime(int)');
});
it('should handle private visibility', function () {
const str = `-getTime(int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTime(int)');
});
it('should handle protected visibility', function () {
const str = `#getTime(int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTime(int)');
});
it('should handle internal visibility', function () {
const str = `~getTime(int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTime(int)');
});
it('should return correct css for static classifier', function () {
const str = `getTime(int)$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(int)');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTime(int)*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(int)');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method has single parameter type and name (type first)', function () {
it('should parse correctly', function () {
const str = `getTime(int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle public visibility', function () {
const str = `+getTime(int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTime(int count)');
});
it('should handle private visibility', function () {
const str = `-getTime(int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTime(int count)');
});
it('should handle protected visibility', function () {
const str = `#getTime(int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTime(int count)');
});
it('should handle internal visibility', function () {
const str = `~getTime(int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTime(int count)');
});
it('should return correct css for static classifier', function () {
const str = `getTime(int count)$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(int count)');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTime(int count)*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(int count)');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method has single parameter type and name (name first)', function () {
it('should parse correctly', function () {
const str = `getTime(count int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle public visibility', function () {
const str = `+getTime(count int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTime(count int)');
});
it('should handle private visibility', function () {
const str = `-getTime(count int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTime(count int)');
});
it('should handle protected visibility', function () {
const str = `#getTime(count int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTime(count int)');
});
it('should handle internal visibility', function () {
const str = `~getTime(count int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTime(count int)');
});
it('should return correct css for static classifier', function () {
const str = `getTime(count int)$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(count int)');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTime(count int)*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(count int)');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method has multiple parameters', function () {
it('should parse correctly', function () {
const str = `getTime(string text, int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle public visibility', function () {
const str = `+getTime(string text, int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle private visibility', function () {
const str = `-getTime(string text, int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle protected visibility', function () {
const str = `#getTime(string text, int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle internal visibility', function () {
const str = `~getTime(string text, int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should return correct css for static classifier', function () {
const str = `getTime(string text, int count)$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(string text, int count)');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTime(string text, int count)*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(string text, int count)');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method has return type', function () {
it('should parse correctly', function () {
const str = `getTime() DateTime`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime() : DateTime');
});
it('should handle public visibility', function () {
const str = `+getTime() DateTime`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTime() : DateTime');
});
it('should handle private visibility', function () {
const str = `-getTime() DateTime`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTime() : DateTime');
});
it('should handle protected visibility', function () {
const str = `#getTime() DateTime`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTime() : DateTime');
});
it('should handle internal visibility', function () {
const str = `~getTime() DateTime`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTime() : DateTime');
});
it('should return correct css for static classifier', function () {
const str = `getTime() DateTime$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime() : DateTime');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTime() DateTime*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime() : DateTime');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method parameter is generic', function () {
it('should parse correctly', function () {
const str = `getTimes(List~T~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List<T>)');
});
it('should handle public visibility', function () {
const str = `+getTimes(List~T~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTimes(List<T>)');
});
it('should handle private visibility', function () {
const str = `-getTimes(List~T~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTimes(List<T>)');
});
it('should handle protected visibility', function () {
const str = `#getTimes(List~T~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTimes(List<T>)');
});
it('should handle internal visibility', function () {
const str = `~getTimes(List~T~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTimes(List<T>)');
});
it('should return correct css for static classifier', function () {
const str = `getTimes(List~T~)$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List<T>)');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTimes(List~T~)*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List<T>)');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method parameter contains two generic', function () {
it('should parse correctly', function () {
const str = `getTimes(List~T~, List~OT~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List<T>, List<OT>)');
});
it('should handle public visibility', function () {
const str = `+getTimes(List~T~, List~OT~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTimes(List<T>, List<OT>)');
});
it('should handle private visibility', function () {
const str = `-getTimes(List~T~, List~OT~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTimes(List<T>, List<OT>)');
});
it('should handle protected visibility', function () {
const str = `#getTimes(List~T~, List~OT~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTimes(List<T>, List<OT>)');
});
it('should handle internal visibility', function () {
const str = `~getTimes(List~T~, List~OT~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTimes(List<T>, List<OT>)');
});
it('should return correct css for static classifier', function () {
const str = `getTimes(List~T~, List~OT~)$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List<T>, List<OT>)');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTimes(List~T~, List~OT~)*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List<T>, List<OT>)');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method parameter is a nested generic', function () {
it('should parse correctly', function () {
const str = `getTimetableList(List~List~T~~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimetableList(List<List<T>>)');
});
it('should handle public visibility', function () {
const str = `+getTimetableList(List~List~T~~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTimetableList(List<List<T>>)');
});
it('should handle private visibility', function () {
const str = `-getTimetableList(List~List~T~~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTimetableList(List<List<T>>)');
});
it('should handle protected visibility', function () {
const str = `#getTimetableList(List~List~T~~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTimetableList(List<List<T>>)');
});
it('should handle internal visibility', function () {
const str = `~getTimetableList(List~List~T~~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTimetableList(List<List<T>>)');
});
it('should return correct css for static classifier', function () {
const str = `getTimetableList(List~List~T~~)$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimetableList(List<List<T>>)');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTimetableList(List~List~T~~)*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimetableList(List<List<T>>)');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method parameter is a composite generic', function () {
const methodNameAndParameters = 'getTimes(List~K, V~)';
const expectedMethodNameAndParameters = 'getTimes(List<K, V>)';
it('should parse correctly', function () {
const str = methodNameAndParameters;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(expectedMethodNameAndParameters);
});
it('should handle public visibility', function () {
const str = '+' + methodNameAndParameters;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'+' + expectedMethodNameAndParameters
);
});
it('should handle private visibility', function () {
const str = '-' + methodNameAndParameters;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'-' + expectedMethodNameAndParameters
);
});
it('should handle protected visibility', function () {
const str = '#' + methodNameAndParameters;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'#' + expectedMethodNameAndParameters
);
});
it('should handle internal visibility', function () {
const str = '~' + methodNameAndParameters;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'~' + expectedMethodNameAndParameters
);
});
it('should return correct css for static classifier', function () {
const str = methodNameAndParameters + '$';
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(expectedMethodNameAndParameters);
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = methodNameAndParameters + '*';
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(expectedMethodNameAndParameters);
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method return type is generic', function () {
it('should parse correctly', function () {
const str = `getTimes() List~T~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes() : List<T>');
});
it('should handle public visibility', function () {
const str = `+getTimes() List~T~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTimes() : List<T>');
});
it('should handle private visibility', function () {
const str = `-getTimes() List~T~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTimes() : List<T>');
});
it('should handle protected visibility', function () {
const str = `#getTimes() List~T~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTimes() : List<T>');
});
it('should handle internal visibility', function () {
const str = `~getTimes() List~T~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTimes() : List<T>');
});
it('should return correct css for static classifier', function () {
const str = `getTimes() List~T~$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes() : List<T>');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTimes() List~T~*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes() : List<T>');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method return type is a nested generic', function () {
it('should parse correctly', function () {
const str = `getTimetableList() List~List~T~~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'getTimetableList() : List<List<T>>'
);
});
it('should handle public visibility', function () {
const str = `+getTimetableList() List~List~T~~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'+getTimetableList() : List<List<T>>'
);
});
it('should handle private visibility', function () {
const str = `-getTimetableList() List~List~T~~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'-getTimetableList() : List<List<T>>'
);
});
it('should handle protected visibility', function () {
const str = `#getTimetableList() List~List~T~~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'#getTimetableList() : List<List<T>>'
);
});
it('should handle internal visibility', function () {
const str = `~getTimetableList() List~List~T~~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'~getTimetableList() : List<List<T>>'
);
});
it('should return correct css for static classifier', function () {
const str = `getTimetableList() List~List~T~~$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'getTimetableList() : List<List<T>>'
);
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTimetableList() List~List~T~~*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'getTimetableList() : List<List<T>>'
);
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('--uncategorized tests--', function () {
it('member name should handle double colons', function () {
const str = `std::map ~int,string~ pMap;`;
const classMember = new ClassMember(str, 'attribute');
expect(classMember.getDisplayDetails().displayText).toBe('std::map <int,string> pMap;');
});
it('member name should handle generic type', function () {
const str = `getTime~T~(this T, int seconds)$ DateTime`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'getTime<T>(this T, int seconds) : DateTime'
);
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
});
});

View File

@@ -1,13 +1,10 @@
import { getConfig } from '../../config.js';
import { parseGenericTypes, sanitizeText } from '../common/common.js';
export interface ClassNode {
id: string;
type: string;
label: string;
cssClasses: string[];
methods: ClassMember[];
members: ClassMember[];
methods: string[];
members: string[];
annotations: string[];
domId: string;
parent?: string;
@@ -17,120 +14,6 @@ export interface ClassNode {
tooltip?: string;
}
export type Visibility = '#' | '+' | '~' | '-' | '';
export const visibilityValues = ['#', '+', '~', '-', ''];
/**
* Parses and stores class diagram member variables/methods.
*
*/
export class ClassMember {
id!: string;
cssStyle!: string;
memberType!: 'method' | 'attribute';
visibility!: Visibility;
/**
* denote if static or to determine which css class to apply to the node
* @defaultValue ''
*/
classifier!: string;
/**
* parameters for method
* @defaultValue ''
*/
parameters!: string;
/**
* return type for method
* @defaultValue ''
*/
returnType!: string;
constructor(input: string, memberType: 'method' | 'attribute') {
this.memberType = memberType;
this.visibility = '';
this.classifier = '';
const sanitizedInput = sanitizeText(input, getConfig());
this.parseMember(sanitizedInput);
}
getDisplayDetails() {
let displayText = this.visibility + parseGenericTypes(this.id);
if (this.memberType === 'method') {
displayText += `(${parseGenericTypes(this.parameters.trim())})`;
if (this.returnType) {
displayText += ' : ' + parseGenericTypes(this.returnType);
}
}
displayText = displayText.trim();
const cssStyle = this.parseClassifier();
return {
displayText,
cssStyle,
};
}
parseMember(input: string) {
let potentialClassifier = '';
if (this.memberType === 'method') {
const methodRegEx = /([#+~-])?(.+)\((.*)\)([\s$*])?(.*)([$*])?/;
const match = input.match(methodRegEx);
if (match) {
const detectedVisibility = match[1] ? match[1].trim() : '';
if (visibilityValues.includes(detectedVisibility)) {
this.visibility = detectedVisibility as Visibility;
}
this.id = match[2].trim();
this.parameters = match[3] ? match[3].trim() : '';
potentialClassifier = match[4] ? match[4].trim() : '';
this.returnType = match[5] ? match[5].trim() : '';
if (potentialClassifier === '') {
const lastChar = this.returnType.substring(this.returnType.length - 1);
if (lastChar.match(/[$*]/)) {
potentialClassifier = lastChar;
this.returnType = this.returnType.substring(0, this.returnType.length - 1);
}
}
}
} else {
const length = input.length;
const firstChar = input.substring(0, 1);
const lastChar = input.substring(length - 1);
if (visibilityValues.includes(firstChar)) {
this.visibility = firstChar as Visibility;
}
if (lastChar.match(/[*?]/)) {
potentialClassifier = lastChar;
}
this.id = input.substring(
this.visibility === '' ? 0 : 1,
potentialClassifier === '' ? length : length - 1
);
}
this.classifier = potentialClassifier;
}
parseClassifier() {
switch (this.classifier) {
case '*':
return 'font-style:italic;';
case '$':
return 'text-decoration:underline;';
default:
return '';
}
}
}
export interface ClassNote {
id: string;
class: string;

View File

@@ -224,12 +224,19 @@ start
| statements
;
mermaidDoc
: graphConfig
direction
: direction_tb
{ yy.setDirection('TB');}
| direction_bt
{ yy.setDirection('BT');}
| direction_rl
{ yy.setDirection('RL');}
| direction_lr
{ yy.setDirection('LR');}
;
graphConfig
: CLASS_DIAGRAM NEWLINE statements EOF
mermaidDoc
: graphConfig
;
directive
@@ -253,6 +260,10 @@ closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'class'); }
;
graphConfig
: CLASS_DIAGRAM NEWLINE statements EOF
;
statements
: statement
| statement NEWLINE
@@ -281,7 +292,7 @@ statement
| relationStatement LABEL { $1.title = yy.cleanupLabel($2); yy.addRelation($1); }
| namespaceStatement
| classStatement
| memberStatement
| methodStatement
| annotationStatement
| clickStatement
| cssClassStatement
@@ -328,7 +339,7 @@ members
| MEMBER members { $2.push($1);$$=$2;}
;
memberStatement
methodStatement
: className {/*console.log('Rel found',$1);*/}
| className LABEL {yy.addMember($1,yy.cleanupLabel($2));}
| MEMBER {/*console.warn('Member',$1);*/}
@@ -347,17 +358,6 @@ noteStatement
| NOTE noteText { yy.addNote($2); }
;
direction
: direction_tb
{ yy.setDirection('TB');}
| direction_bt
{ yy.setDirection('BT');}
| direction_rl
{ yy.setDirection('RL');}
| direction_lr
{ yy.setDirection('LR');}
;
relation
: relationType lineType relationType { $$={type1:$1,type2:$3,lineType:$2}; }
| lineType relationType { $$={type1:'none',type2:$2,lineType:$1}; }

View File

@@ -91,7 +91,7 @@ g.classGroup line {
}
#compositionEnd, .composition {
fill: ${options.lineColor} !important;
fill: transparent !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}

View File

@@ -172,6 +172,7 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
// add class group
const g = elem.append('g').attr('id', diagObj.db.lookUpDomId(id)).attr('class', 'classGroup');
// add title
let title;
if (classDef.link) {
title = g
@@ -208,56 +209,47 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
}
const titleHeight = title.node().getBBox().height;
let membersLine;
let membersBox;
let methodsLine;
// don't draw box if no members
if (classDef.members.length > 0) {
membersLine = g
.append('line') // text label for the x axis
.attr('x1', 0)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2);
const membersLine = g
.append('line') // text label for the x axis
.attr('x1', 0)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2);
const members = g
.append('text') // text label for the x axis
.attr('x', conf.padding)
.attr('y', titleHeight + conf.dividerMargin + conf.textHeight)
.attr('fill', 'white')
.attr('class', 'classText');
const members = g
.append('text') // text label for the x axis
.attr('x', conf.padding)
.attr('y', titleHeight + conf.dividerMargin + conf.textHeight)
.attr('fill', 'white')
.attr('class', 'classText');
isFirst = true;
classDef.members.forEach(function (member) {
addTspan(members, member, isFirst, conf);
isFirst = false;
});
isFirst = true;
classDef.members.forEach(function (member) {
addTspan(members, member, isFirst, conf);
isFirst = false;
});
membersBox = members.node().getBBox();
}
const membersBox = members.node().getBBox();
// don't draw box if no methods
if (classDef.methods.length > 0) {
methodsLine = g
.append('line') // text label for the x axis
.attr('x1', 0)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height);
const methodsLine = g
.append('line') // text label for the x axis
.attr('x1', 0)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height);
const methods = g
.append('text') // text label for the x axis
.attr('x', conf.padding)
.attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight)
.attr('fill', 'white')
.attr('class', 'classText');
const methods = g
.append('text') // text label for the x axis
.attr('x', conf.padding)
.attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight)
.attr('fill', 'white')
.attr('class', 'classText');
isFirst = true;
isFirst = true;
classDef.methods.forEach(function (method) {
addTspan(methods, method, isFirst, conf);
isFirst = false;
});
}
classDef.methods.forEach(function (method) {
addTspan(methods, method, isFirst, conf);
isFirst = false;
});
const classBox = g.node().getBBox();
var cssClassStr = ' ';
@@ -286,12 +278,8 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
title.insert('title').text(classDef.tooltip);
}
if (membersLine) {
membersLine.attr('x2', rectWidth);
}
if (methodsLine) {
methodsLine.attr('x2', rectWidth);
}
membersLine.attr('x2', rectWidth);
methodsLine.attr('x2', rectWidth);
classInfo.width = rectWidth;
classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin;
@@ -303,7 +291,7 @@ export const getClassTitleString = function (classDef) {
let classTitleString = classDef.id;
if (classDef.type) {
classTitleString += '<' + parseGenericTypes(classDef.type) + '>';
classTitleString += '<' + classDef.type + '>';
}
return classTitleString;
@@ -372,19 +360,82 @@ export const drawNote = function (elem, note, conf, diagObj) {
return noteInfo;
};
export const parseMember = 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);
}
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);
displayText = visibility + methodName + '(' + parseGenericTypes(parameters.trim()) + ')';
if (methodEnd < text.length) {
// 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 = visibility + parseGenericTypes(text);
}
return {
displayText,
cssStyle,
};
};
/**
* Adds a <tspan> for a member in a diagram
*
* @param {SVGElement} textEl The element to append to
* @param {string} member The member
* @param {string} txt The member
* @param {boolean} isFirst
* @param {{ padding: string; textHeight: string }} conf The configuration for the member
*/
const addTspan = function (textEl, member, isFirst, conf) {
const { displayText, cssStyle } = member.getDisplayDetails();
const tSpan = textEl.append('tspan').attr('x', conf.padding).text(displayText);
const addTspan = function (textEl, txt, isFirst, conf) {
let member = parseMember(txt);
if (cssStyle !== '') {
const tSpan = textEl.append('tspan').attr('x', conf.padding).text(member.displayText);
if (member.cssStyle !== '') {
tSpan.attr('style', member.cssStyle);
}
@@ -393,9 +444,27 @@ const addTspan = function (textEl, member, isFirst, conf) {
}
};
/**
* Gives the styles for a classifier
*
* @param {'+' | '-' | '#' | '~' | '*' | '$'} classifier The classifier string
* @returns {string} Styling for the classifier
*/
const parseClassifier = function (classifier) {
switch (classifier) {
case '*':
return 'font-style:italic;';
case '$':
return 'text-decoration:underline;';
default:
return '';
}
};
export default {
getClassTitleString,
drawClass,
drawEdge,
drawNote,
parseMember,
};

View File

@@ -1,27 +1,330 @@
import svgDraw from './svgDraw.js';
import { JSDOM } from 'jsdom';
describe('given a string representing a class, ', function () {
describe('when class name includes generic, ', function () {
it('should return correct text for generic', function () {
const classDef = {
id: 'Car',
type: 'T',
label: 'Car',
};
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>');
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);
expect(actual.displayText).toBe('foo()');
expect(actual.cssStyle).toBe('');
});
it('should return correct text for nested generics', function () {
const classDef = {
id: 'Car',
type: 'T~T~',
label: 'Car',
};
let actual = svgDraw.getClassTitleString(classDef);
expect(actual).toBe('Car<T<T>>');
it('should handle declaration with parameters', function () {
const str = 'foo(int id)';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(int id)');
expect(actual.cssStyle).toBe('');
});
it('should handle declaration with multiple parameters', function () {
const str = 'foo(int id, object thing)';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(int id, object thing)');
expect(actual.cssStyle).toBe('');
});
it('should handle declaration with single item in parameters', function () {
const str = 'foo(id)';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(id)');
expect(actual.cssStyle).toBe('');
});
it('should handle declaration with single item in parameters with extra spaces', function () {
const str = ' foo ( id) ';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(id)');
expect(actual.cssStyle).toBe('');
});
it('should handle method declaration with generic parameter', function () {
const str = 'foo(List~int~)';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(List<int>)');
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);
expect(actual.displayText).toBe('+foo(List<int> ids) : List<Item>');
expect(actual.cssStyle).toBe('font-style:italic;');
});
it('should handle method declaration with nested generics', function () {
const str = '+foo ( List~List~int~~ ids )* List~List~Item~~';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('+foo(List<List<int>> ids) : List<List<Item>>');
expect(actual.cssStyle).toBe('font-style:italic;');
});
});
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);
expect(actual.displayText).toBe('int[] ids');
expect(actual.cssStyle).toBe('');
});
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);
expect(actual.displayText).toBe('List<int> ids');
expect(actual.cssStyle).toBe('');
});
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

@@ -69,5 +69,6 @@ describe('generic parser', () => {
'test <Array<Array<string[]>>>'
);
expect(parseGenericTypes('~test')).toEqual('~test');
expect(parseGenericTypes('~test Array~string~')).toEqual('~test Array<string>');
});
});

View File

@@ -178,63 +178,23 @@ export const getMin = function (...values: number[]): number {
* @param text - The text to convert
* @returns The converted string
*/
export const parseGenericTypes = function (input: string): string {
const inputSets = input.split(/(,)/);
const output = [];
export const parseGenericTypes = function (text: string): string {
let cleanedText = text;
for (let i = 0; i < inputSets.length; i++) {
let thisSet = inputSets[i];
if (text.split('~').length - 1 >= 2) {
let newCleanedText = cleanedText;
// if the original input included a value such as "~K, V~"", these will be split into
// an array of ["~K",","," V~"].
// This means that on each call of processSet, there will only be 1 ~ present
// To account for this, if we encounter a ",", we are checking the previous and next sets in the array
// to see if they contain matching ~'s
// in which case we are assuming that they should be rejoined and sent to be processed
if (thisSet === ',' && i > 0 && i + 1 < inputSets.length) {
const previousSet = inputSets[i - 1];
const nextSet = inputSets[i + 1];
// 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);
if (shouldCombineSets(previousSet, nextSet)) {
thisSet = previousSet + ',' + nextSet;
i++; // Move the index forward to skip the next iteration since we're combining sets
output.pop();
}
}
output.push(processSet(thisSet));
return parseGenericTypes(newCleanedText);
} else {
return cleanedText;
}
return output.join('');
};
const shouldCombineSets = (previousSet: string, nextSet: string): boolean => {
const prevCount = [...previousSet].reduce((count, char) => (char === '~' ? count + 1 : count), 0);
const nextCount = [...nextSet].reduce((count, char) => (char === '~' ? count + 1 : count), 0);
return prevCount === 1 && nextCount === 1;
};
const processSet = (input: string): string => {
const chars = [...input];
const tildeCount = chars.reduce((count, char) => (char === '~' ? count + 1 : count), 0);
if (tildeCount <= 1) {
return input;
}
let first = chars.indexOf('~');
let last = chars.lastIndexOf('~');
while (first !== -1 && last !== -1 && first !== last) {
chars[first] = '<';
chars[last] = '>';
first = chars.indexOf('~');
last = chars.lastIndexOf('~');
}
return chars.join('');
};
export default {

View File

@@ -0,0 +1,15 @@
import type { DiagramAST } from 'mermaid-parser';
import type { DiagramDB } from '../../diagram-api/types.js';
export function populateCommonDb(ast: DiagramAST, db: DiagramDB) {
if (ast.accDescr) {
db.setAccDescription?.(ast.accDescr);
}
if (ast.accTitle) {
db.setAccTitle?.(ast.accTitle);
}
if (ast.title) {
db.setDiagramTitle?.(ast.title);
}
}

View File

@@ -66,7 +66,7 @@ o\{ return 'ZERO_OR_MORE';
"optionally to" return 'NON_IDENTIFYING';
\.\- return 'NON_IDENTIFYING';
\-\. return 'NON_IDENTIFYING';
[A-Za-z_][A-Za-z0-9\-_]* return 'ALPHANUM';
[A-Za-z][A-Za-z0-9\-_]* return 'ALPHANUM';
. return yytext[0];
<<EOF>> return 'EOF';

View File

@@ -33,7 +33,7 @@ describe('when parsing ER diagram it...', function () {
describe('has non A-Za-z0-9_- chars', function () {
// these were entered using the Mac keyboard utility.
const chars =
"~ ` ! @ # $ ^ & * ( ) - = + [ ] { } | / ; : ' . ? ¡ ™ € £ ¢ ∞ fi § ‡ • ° ª · º ≠ ± œ Œ ∑ „ ® † ˇ ¥ Á ¨ ˆ ˆ Ø π ∏ “ « » å Å ß Í ∂ Î ƒ Ï © ˙ Ó ∆ Ô ˚  ¬ Ò … Ú æ Æ Ω ¸ ≈ π ˛ ç Ç √ ◊ ∫ ı ˜ µ  ≤ ¯ ≥ ˘ ÷ ¿";
"~ ` ! @ # $ ^ & * ( ) - _ = + [ ] { } | / ; : ' . ? ¡ ™ € £ ¢ ∞ fi § ‡ • ° ª · º ≠ ± œ Œ ∑ „ ® † ˇ ¥ Á ¨ ˆ ˆ Ø π ∏ “ « » å Å ß Í ∂ Î ƒ Ï © ˙ Ó ∆ Ô ˚  ¬ Ò … Ú æ Æ Ω ¸ ≈ π ˛ ç Ç √ ◊ ∫ ı ˜ µ  ≤ ¯ ≥ ˘ ÷ ¿";
const allowed = chars.split(' ');
allowed.forEach((allowedChar) => {
@@ -170,13 +170,6 @@ describe('when parsing ER diagram it...', function () {
expect(entities[firstEntity].alias).toBe(alias);
expect(entities[secondEntity].alias).toBeUndefined();
});
it('can start with an underscore', function () {
const entity = '_foo';
erDiagram.parser.parse(`erDiagram\n${entity}\n`);
const entities = erDb.getEntities();
expect(entities.hasOwnProperty(entity)).toBe(true);
});
});
describe('attribute name', () => {

View File

@@ -5,7 +5,6 @@ const diagram: DiagramDefinition = {
db: {},
renderer,
parser: {
parser: { yy: {} },
parse: (): void => {
return;
},

View File

@@ -1,24 +1,31 @@
// @ts-ignore - jison doesn't export types
import { parser } from './parser/info.jison';
import { db } from './infoDb.js';
describe('info diagram', () => {
beforeEach(() => {
parser.yy = db;
parser.yy.clear();
});
import { parser } from './infoParser.js';
describe('info', () => {
it('should handle an info definition', () => {
const str = `info`;
parser.parse(str);
expect(db.getInfo()).toBeFalsy();
expect(() => {
parser.parse(str);
}).not.toThrow();
});
it('should handle an info definition with showInfo', () => {
const str = `info showInfo`;
parser.parse(str);
expect(() => {
parser.parse(str);
}).not.toThrow();
});
expect(db.getInfo()).toBeTruthy();
it('should throw because of unsupported info grammar', () => {
const str = `info unsupported`;
expect(() => {
parser.parse(str);
}).toThrow('Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.');
});
it('should throw because of unsupported info grammar', () => {
const str = `info unsupported`;
expect(() => {
parser.parse(str);
}).toThrow('Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.');
});
});

View File

@@ -1,23 +1,10 @@
import type { InfoFields, InfoDB } from './infoTypes.js';
import { version } from '../../../package.json';
export const DEFAULT_INFO_DB: InfoFields = {
info: false,
} as const;
export const DEFAULT_INFO_DB: InfoFields = { version } as const;
let info: boolean = DEFAULT_INFO_DB.info;
export const setInfo = (toggle: boolean): void => {
info = toggle;
};
export const getInfo = (): boolean => info;
const clear = (): void => {
info = DEFAULT_INFO_DB.info;
};
export const getVersion = (): string => DEFAULT_INFO_DB.version;
export const db: InfoDB = {
clear,
setInfo,
getInfo,
getVersion,
};

View File

@@ -1,6 +1,5 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore - jison doesn't export types
import parser from './parser/info.jison';
import { parser } from './infoParser.js';
import { db } from './infoDb.js';
import { renderer } from './infoRenderer.js';

View File

@@ -0,0 +1,12 @@
import type { Info } from 'mermaid-parser';
import { parse } from 'mermaid-parser';
import { log } from '../../logger.js';
import type { ParserDefinition } from '../../diagram-api/types.js';
export const parser: ParserDefinition = {
parse: (input: string): void => {
const ast: Info = parse('info', input);
log.debug(ast);
},
};

View File

@@ -1,11 +1,9 @@
import type { DiagramDB } from '../../diagram-api/types.js';
export interface InfoFields {
info: boolean;
version: string;
}
export interface InfoDB extends DiagramDB {
clear: () => void;
setInfo: (info: boolean) => void;
getInfo: () => boolean;
getVersion: () => string;
}

View File

@@ -1,48 +0,0 @@
/** mermaid
* https://knsv.github.io/mermaid
* (c) 2015 Knut Sveidqvist
* MIT license.
*/
%lex
%options case-insensitive
%{
// Pre-lexer code can go here
%}
%%
"info" return 'info' ;
[\s\n\r]+ return 'NL' ;
[\s]+ return 'space';
"showInfo" return 'showInfo';
<<EOF>> return 'EOF' ;
. return 'TXT' ;
/lex
%start start
%% /* language grammar */
start
// %{ : info document 'EOF' { return yy; } }
: info document 'EOF' { return yy; }
;
document
: /* empty */
| document line
;
line
: statement { }
| 'NL'
;
statement
: showInfo { yy.setInfo(true); }
;
%%

View File

@@ -31,7 +31,7 @@ Once the release happens we add a tag to the `release` branch and merge it with
2. Check out the `develop` branch
3. Create a new branch for your work. Please name the branch following our naming convention below.
We use the following naming convention for branches:
We use the follow naming convention for branches:
```txt
[feature | bug | chore | docs]/[issue number]_[short description using dashes ('-') or underscores ('_') instead of spaces]

View File

@@ -16,7 +16,7 @@ In GitHub, you first **fork** a repository when you are going to make changes an
Then you **clone** a copy to your local development machine (e.g. where you code) to make a copy with all the files to work with.
[Fork mermaid](https://github.com/mermaid-js/mermaid/fork) to start contributing to the main project and its documentation, or [search for other repositories](https://github.com/orgs/mermaid-js/repositories).
[Fork mermaid](https://github.com/mermaid-js/mermaid/fork) to start contributing to the main project and its documentaion, or [search for other repositories](https://github.com/orgs/mermaid-js/repositories).
[Here is a GitHub document that gives an overview of the process.](https://docs.github.com/en/get-started/quickstart/fork-a-repo)

View File

@@ -16,7 +16,7 @@ In GitHub, you first **fork** a repository when you are going to make changes an
Then you **clone** a copy to your local development machine (e.g. where you code) to make a copy with all the files to work with.
[Fork mermaid](https://github.com/mermaid-js/mermaid/fork) to start contributing to the main project and its documentation, or [search for other repositories](https://github.com/orgs/mermaid-js/repositories).
[Fork mermaid](https://github.com/mermaid-js/mermaid/fork) to start contributing to the main project and its documentaion, or [search for other repositories](https://github.com/orgs/mermaid-js/repositories).
[Here is a GitHub document that gives an overview of the process.](https://docs.github.com/en/get-started/quickstart/fork-a-repo)

View File

@@ -13,7 +13,7 @@ For instance:
#### Store data found during parsing
There are some jison specific sub steps here where the parser stores the data encountered when parsing the diagram, this data is later used by the renderer. You can during the parsing call an object provided to the parser by the user of the parser. This object can be called during parsing for storing data.
There are some jison specific sub steps here where the parser stores the data encountered when parsing the diagram, this data is later used by the renderer. You can during the parsing call a object provided to the parser by the user of the parser. This object can be called during parsing for storing data.
```jison
statement
@@ -30,7 +30,7 @@ In the extract of the grammar above, it is defined that a call to the setTitle m
Make sure that the `parseError` function for the parser is defined and calling `mermaid.parseError`. This way a common way of detecting parse errors is provided for the end-user.
```
For more info look at the example diagram type:
For more info look in the example diagram type:
The `yy` object has the following function:
@@ -49,7 +49,7 @@ parser.yy = db;
### Step 2: Rendering
Write a renderer that given the data found during parsing renders the diagram. To look at an example look at sequenceRenderer.js rather than the flowchart renderer as this is a more generic example.
Write a renderer that given the data found during parsing renders the diagram. To look at an example look at sequenceRenderer.js rather then the flowchart renderer as this is a more generic example.
Place the renderer in the diagram folder.
@@ -57,7 +57,7 @@ Place the renderer in the diagram folder.
The second thing to do is to add the capability to detect the new diagram to type to the detectType in `diagram-api/detectType.ts`. The detection should return a key for the new diagram type.
[This key will be used to as the aria roledescription](#aria-roledescription), so it should be a word that clearly describes the diagram type.
For example, if your new diagram uses a UML deployment diagram, a good key would be "UMLDeploymentDiagram" because assistive technologies such as a screen reader
For example, if your new diagram use a UML deployment diagram, a good key would be "UMLDeploymentDiagram" because assistive technologies such as a screen reader
would voice that as "U-M-L Deployment diagram." Another good key would be "deploymentDiagram" because that would be voiced as "Deployment Diagram." A bad key would be "deployment" because that would not sufficiently describe the diagram.
Note that the diagram type key does not have to be the same as the diagram keyword chosen for the [grammar](#grammar), but it is helpful if they are the same.
@@ -117,7 +117,7 @@ There are a few features that are common between the different types of diagrams
- Themes, there is a common way to modify the styling of diagrams in Mermaid.
- Comments should follow mermaid standards
Here are some pointers on how to handle these different areas.
Here some pointers on how to handle these different areas.
## Accessibility
@@ -135,7 +135,7 @@ See [the definition of aria-roledescription](https://www.w3.org/TR/wai-aria-1.1/
### accessible title and description
The syntax for accessible titles and descriptions is described in [the Accessibility documentation section.](../config/accessibility.md)
The syntax for accessible titles and descriptions is described in [the Accessibility documenation section.](../config/accessibility.md)
As a design goal, the jison syntax should be similar between the diagrams.

View File

@@ -116,7 +116,7 @@ The following code snippet changes `theme` to `forest`:
`%%{init: { "theme": "forest" } }%%`
Possible theme values are: `default`, `base`, `dark`, `forest` and `neutral`.
Possible theme values are: `default`,`base`, `dark`, `forest` and `neutral`.
Default Value is `default`.
Example:
@@ -235,7 +235,7 @@ Let us see an example:
sequenceDiagram
Alice->Bob: Hello Bob, how are you?
Bob->Alice: Fine, how did your mother like the book I suggested? And did you catch the new book about alien invasion?
Bob->Alice: Fine, how did you mother like the book I suggested? And did you catch the new book about alien invasion?
Alice->Bob: Good.
Bob->Alice: Cool
```
@@ -252,7 +252,7 @@ By applying that snippet to the diagram above, `wrap` will be enabled:
%%{init: { "sequence": { "wrap": true, "width":300 } } }%%
sequenceDiagram
Alice->Bob: Hello Bob, how are you?
Bob->Alice: Fine, how did your mother like the book I suggested? And did you catch the new book about alien invasion?
Bob->Alice: Fine, how did you mother like the book I suggested? And did you catch the new book about alien invasion?
Alice->Bob: Good.
Bob->Alice: Cool
```

View File

@@ -35,7 +35,7 @@ pnpm add mermaid
**Hosting mermaid on a web page:**
> Note: This topic is explored in greater depth in the [User Guide for Beginners](../intro/getting-started.md)
> Note:This topic explored in greater depth in the [User Guide for Beginners](../intro/getting-started.md)
The easiest way to integrate mermaid on a web page requires two elements:
@@ -94,7 +94,7 @@ Mermaid can load multiple diagrams, in the same page.
## Enabling Click Event and Tags in Nodes
A `securityLevel` configuration has to first be cleared. `securityLevel` sets the level of trust for the parsed diagrams and limits click functionality. This was introduced in version 8.2 as a security improvement, aimed at preventing malicious use.
A `securityLevel` configuration has to first be cleared. `securityLevel` sets the level of trust for the parsed diagrams and limits click functionality. This was introduce in version 8.2 as a security improvement, aimed at preventing malicious use.
**It is the site owner's responsibility to discriminate between trustworthy and untrustworthy user-bases and we encourage the use of discretion.**
@@ -109,14 +109,14 @@ Values:
- **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 prevents any JavaScript from running in the context. This may hinder interactive functionality of the diagram, like scripts, popups in the sequence diagram, links to other tabs or targets, etc.
- **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 the sequence diagram, links to other tabs or targets, etc.
```note
This changes the default behaviour of mermaid so that after upgrade to 8.2, unless the `securityLevel` is not changed, tags in flowcharts are encoded as tags and clicking is disabled.
**sandbox** security level is still in the beta version.
```
**If you are taking responsibility for the diagram source security you can set the `securityLevel` to a value of your choosing. This allows clicks and tags are allowed.**
**If you are taking responsibility for the diagram source security you can set the `securityLevel` to a value of your choosing . This allows clicks and tags are allowed.**
**To change `securityLevel`, you have to call `mermaid.initialize`:**

View File

@@ -43,7 +43,6 @@ They also serve as proof of concept, for the variety of things that can be built
- [Mermaid plugin for GitBook](https://github.com/wwformat/gitbook-plugin-mermaid-pdf)
- [LiveBook](https://livebook.dev) (**Native support**)
- [Atlassian Products](https://www.atlassian.com)
- [Mermaid Live Editor for Confluence Cloud](https://marketplace.atlassian.com/apps/1231571/mermaid-live-editor-for-confluence?hosting=cloud&tab=overview)
- [Mermaid Plugin for Confluence](https://marketplace.atlassian.com/apps/1214124/mermaid-plugin-for-confluence?hosting=server&tab=overview)
- [CloudScript.io Addon](https://marketplace.atlassian.com/apps/1219878/cloudscript-io-mermaid-addon?hosting=cloud&tab=overview)
- [Auto convert diagrams in Jira](https://github.com/coddingtonbear/jirafs-mermaid)

View File

@@ -86,7 +86,7 @@ When writing the .html file, we give two instructions inside the html code to th
a. The mermaid code for the diagram we want to create.
b. The importing of mermaid library through the `mermaid.esm.mjs` or `mermaid.esm.min.mjs` and the `mermaid.initialize()` call, which dictates the appearance of diagrams and also starts the rendering process.
b. The importing of mermaid library through the `mermaid.esm.mjs` or `mermaid.esm.min.mjs` and the `mermaid.initialize()` call, which dictates the appearance of diagrams and also starts the rendering process .
**a. The embedded mermaid diagram definition inside a `<pre class="mermaid">`:**
@@ -204,4 +204,4 @@ In this example mermaid.js is referenced in `src` as a separate JavaScript file,
**Comments from Knut Sveidqvist, creator of mermaid:**
- In early versions of mermaid, the `<script>` tag was invoked in the `<head>` part of the web page. Nowadays we can place it in the `<body>` as seen above. Older parts of the documentation frequently reflect the previous way which still works.
- In early versions of mermaid, the `<script>` tag was invoked in the `<head>` part of the web page. Nowadays we can place it in the `<body>` as seen above. Older parts of the documentation frequently reflects the previous way which still works.

View File

@@ -32,7 +32,7 @@
"unplugin-vue-components": "^0.25.0",
"vite": "^4.3.9",
"vite-plugin-pwa": "^0.16.0",
"vitepress": "1.0.0-rc.10",
"vitepress": "1.0.0-rc.4",
"workbox-window": "^7.0.0"
}
}

View File

@@ -56,7 +56,7 @@ Mermaid syntax for ER diagrams is compatible with PlantUML, with an extension to
Where:
- `first-entity` is the name of an entity. Names must begin with an alphabetic character or an underscore (from v<MERMAID_RELEASE_VERSION>+), and may also contain digits and hyphens.
- `first-entity` is the name of an entity. Names must begin with an alphabetic character and may also contain digits, hyphens, and underscores.
- `relationship` describes the way that both entities inter-relate. See below.
- `second-entity` is the name of the other entity.
- `relationship-label` describes the relationship from the perspective of the first entity.

View File

@@ -709,9 +709,9 @@ flowchart LR
classDef foobar stroke:#00f
```
### CSS classes
### Css classes
It is also possible to predefine classes in CSS styles that can be applied from the graph definition as in the example
It is also possible to predefine classes in css styles that can be applied from the graph definition as in the example
below:
**Example style**

View File

@@ -31,7 +31,7 @@ mindmap
The syntax for creating Mindmaps is simple and relies on indentation for setting the levels in the hierarchy.
In the following example you can see how there are 3 different levels. One with starting at the left of the text and another level with two rows starting at the same column, defining the node A. At the end there is one more level where the text is indented further than the previous lines defining the nodes B and C.
In the following example you can see how there are 3 different levels. One with starting at the left of the text and another level with two rows starting at the same column, defining the node A. At the end there is one more level where the text is indented further then the previous lines defining the nodes B and C.
```
mindmap
@@ -41,7 +41,7 @@ mindmap
C
```
In summary is a simple text outline where there is one node at the root level called `Root` which has one child `A`. `A` in turn has two children `B`and `C`. In the diagram below we can see this rendered as a mindmap.
In summary is a simple text outline where there are one node at the root level called `Root` which has one child `A`. `A` in turn has two children `B`and `C`. In the diagram below we can see this rendered as a mindmap.
```mermaid
mindmap
@@ -142,7 +142,7 @@ _These classes need to be supplied by the site administrator._
## Unclear indentation
The actual indentation does not really matter only compared with the previous rows. If we take the previous example and disrupt it a little we can see how the calculations are performed. Let us start with placing C with a smaller indentation than `B` but larger then `A`.
The actual indentation does not really matter only compared with the previous rows. If we take the previous example and disrupt it a little we can se how the calculations are performed. Let us start with placing C with a smaller indentation than `B`but larger then `A`.
```
mindmap

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