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
84 changed files with 4908 additions and 3477 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

@@ -57,6 +57,7 @@
"gzipped",
"huynh",
"huynhicode",
"iife",
"inkdrop",
"jaoude",
"jgreywolf",
@@ -70,6 +71,7 @@
"knut",
"knutsveidqvist",
"laganeckas",
"langium",
"linetype",
"lintstagedrc",
"logmsg",
@@ -81,6 +83,7 @@
"mdbook",
"mermaidjs",
"mermerd",
"metafile",
"mindaugas",
"mindmap",
"mindmaps",
@@ -94,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

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

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

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

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

View File

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

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

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

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

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

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

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

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

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

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

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

@@ -102,7 +102,6 @@ describe('when using mermaid and ', () => {
parse: (_text) => {
return;
},
parser: { yy: {} },
},
styles: () => {
// do nothing

View File

@@ -7,7 +7,7 @@
# - `scripts/create-types-from-json-schema.mjs`
# Used to generate the `src/config.type.ts` TypeScript types for MermaidConfig
# with the `json-schema-to-typescript` NPM package.
# - `.vite/jsonSchemaPlugin.ts`
# - `.build/jsonSchema.ts`
# Used to generate the default values from the `default` keys in this
# JSON Schema using the `ajv` NPM package.
# Non-JSON values, like functions or `undefined`, still need to be manually
@@ -21,7 +21,7 @@
# - Use `meta:enum` to document enum values (from jsonschema2md)
# - Use `tsType` to override the TypeScript type (from json-schema-to-typescript)
# - If adding a new object to `MermaidConfig` (e.g. a new diagram type),
# you may need to add it to `.vite/jsonSchemaPlugin.ts` and `src/docs.mts`
# you may need to add it to `.build/jsonSchema.ts` and `src/docs.mts`
# to get the docs/default values to generate properly.
$id: https://mermaid-js.github.io/schemas/config.schema.json
$schema: https://json-schema.org/draft/2019-09/schema

21
packages/parser/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2023 Yokozuna59
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

63
packages/parser/README.md Normal file
View File

@@ -0,0 +1,63 @@
<p align="center">
<img src="https://raw.githubusercontent.com/mermaid-js/mermaid/develop/docs/public/favicon.svg" height="150">
</p>
<h1 align="center">
Mermaid Parser
</h1>
<p align="center">
Mermaid parser package
<p>
[![NPM](https://img.shields.io/npm/v/mermaid-parser)](https://www.npmjs.com/package/mermaid-parser)
## How the package works
The package exports a `parse` function that has two parameters:
```ts
declare function parse<T extends DiagramAST>(
diagramType: keyof typeof initializers,
text: string
): T;
```
## How does a Langium-based parser work?
```mermaid
sequenceDiagram
actor Package
participant Module
participant TokenBuilder
participant Lexer
participant Parser
participant ValueConverter
Package ->> Module: Create services
Module ->> TokenBuilder: Override or/and<br>reorder rules
TokenBuilder ->> Lexer: Read the string and transform<br>it into a token stream
Lexer ->> Parser: Parse token<br>stream into AST
Parser ->> ValueConverter: Clean/modify tokenized<br>rules returned value
ValueConverter -->> Package: Return AST
```
- When to override `TokenBuilder`?
- To override keyword rules.
- To override terminal rules that need a custom function.
- To manually reorder the list of rules.
- When to override `Lexer`?
- To modify input before tokenizing.
- To insert/modify tokens that cannot or have not been parsed.
- When to override `LangiumParser`?
- To insert or modify attributes that can't be parsed.
- When to override `ValueConverter`?
- To modify the returned value from the parser.

View File

@@ -0,0 +1,13 @@
{
"projectName": "Mermaid",
"languages": [
{
"id": "info",
"grammar": "src/language/info/info.langium",
"fileExtensions": [".mmd", ".mermaid"]
}
],
"mode": "production",
"importExtension": ".js",
"out": "src/language/generated"
}

View File

@@ -0,0 +1,48 @@
{
"name": "mermaid-parser",
"version": "0.2.0",
"description": "MermaidJS parser",
"author": "Yokozuna59",
"contributors": [
"Yokozuna59",
"Sidharth Vinod (https://sidharth.dev)"
],
"homepage": "https://github.com/mermaid-js/mermaid/tree/develop/packages/mermaid/parser/#readme",
"types": "dist/src/index.d.ts",
"type": "module",
"exports": {
".": {
"import": "./dist/mermaid-parser.esm.mjs",
"types": "./dist/src/index.d.ts"
}
},
"scripts": {
"clean": "rimraf dist src/language/generated",
"langium:generate": "langium generate",
"langium:watch": "langium generate --watch",
"prepublishOnly": "pnpm -w run build"
},
"repository": {
"type": "git",
"url": "https://github.com/mermaid-js/mermaid.git",
"directory": "packages/parser"
},
"license": "MIT",
"keywords": [
"mermaid",
"parser",
"ast"
],
"dependencies": {
"langium": "2.0.1"
},
"devDependencies": {
"langium-cli": "2.0.1"
},
"files": [
"dist/"
],
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,3 @@
export type { Info } from './language/index.js';
export type { DiagramAST } from './parse.js';
export { parse, MermaidParseError } from './parse.js';

View File

@@ -0,0 +1,51 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
type CustomPatternMatcherReturn = [string] & { payload?: any };
export type CustomPatternMatcherFunc = (
text: string,
offset: number,
tokens: IToken[],
groups: {
[groupName: string]: IToken[];
}
) => CustomPatternMatcherReturn | RegExpExecArray | null;
interface ICustomPattern {
exec: CustomPatternMatcherFunc;
}
type TokenPattern = RegExp | string | CustomPatternMatcherFunc | ICustomPattern;
export interface IToken {
image: string;
startOffset: number;
startLine?: number;
startColumn?: number;
endOffset?: number;
endLine?: number;
endColumn?: number;
isInsertedInRecovery?: boolean;
tokenTypeIdx: number;
tokenType: TokenType;
payload?: any;
}
export interface TokenType {
name: string;
GROUP?: string;
PATTERN?: TokenPattern;
LABEL?: string;
LONGER_ALT?: TokenType | TokenType[];
POP_MODE?: boolean;
PUSH_MODE?: string;
LINE_BREAKS?: boolean;
CATEGORIES?: TokenType[];
tokenTypeIdx?: number;
categoryMatches?: number[];
categoryMatchesMap?: {
[tokType: number]: boolean;
};
isParent?: boolean;
START_CHARS_HINT?: (string | number)[];
}

View File

@@ -0,0 +1,14 @@
fragment TitleAndAccessibilities:
((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE) NEWLINE+)+
;
terminal NEWLINE: /\r?\n/;
terminal ACC_DESCR: /accDescr(?:[\t ]*:[\t ]*[^\n\r]*?(?=%%)|\s*{[^}]*})|accDescr(?:[\t ]*:[\t ]*[^\n\r]*|\s*{[^}]*})/;
terminal ACC_TITLE: /accTitle[\t ]*:[\t ]*[^\n\r]*?(?=%%)|accTitle[\t ]*:[\t ]*[^\n\r]*/;
terminal TITLE: /title(?:[\t ]+[^\n\r]*?|)(?=%%)|title(?:[\t ]+[^\n\r]*|)/;
hidden terminal WHITESPACE: /[\t ]+/;
// TODO: add YAML_COMMENT hidden rule without interfere actual grammar
hidden terminal YAML: /---[\t ]*\r?\n[\S\s]*?---[\t ]*(?!.)/;
hidden terminal DIRECTIVE: /[\t ]*%%{[\S\s]*?}%%\s*/;
hidden terminal SINGLE_LINE_COMMENT: /[\t ]*%%[^\n\r]*/;

View File

@@ -0,0 +1,8 @@
import type { LexerResult } from 'langium';
import { DefaultLexer } from 'langium';
export class CommonLexer extends DefaultLexer {
public override tokenize(text: string): LexerResult {
return super.tokenize(text + '\n');
}
}

View File

@@ -0,0 +1,14 @@
/**
* Matches single and multiline accessible description
*/
export const accessibilityDescrRegex = /accDescr(?:[\t ]*:[\t ]*([^\n\r]*)|\s*{([^}]*)})/;
/**
* Matches single line accessible title
*/
export const accessibilityTitleRegex = /accTitle[\t ]*:[\t ]*([^\n\r]*)/;
/**
* Matches a single line title
*/
export const titleRegex = /title([\t ]+([^\n\r]*)|)/;

View File

@@ -0,0 +1,74 @@
import type { CstNode, GrammarAST, ValueType } from 'langium';
import { DefaultValueConverter } from 'langium';
import { accessibilityDescrRegex, accessibilityTitleRegex, titleRegex } from './commonMatcher.js';
export class CommonValueConverter extends DefaultValueConverter {
protected override runConverter(
rule: GrammarAST.AbstractRule,
input: string,
cstNode: CstNode
): ValueType {
const value: ValueType | undefined = CommonValueConverter.customRunConverter(
rule,
input,
cstNode
);
if (value === undefined) {
return super.runConverter(rule, input, cstNode);
} else {
return value;
}
}
/**
* A method contains convert logic to be used by class itself or `MermaidValueConverter`.
*
* @param rule - Parsed rule.
* @param input - Matched string.
* @param _cstNode - Node in the Concrete Syntax Tree (CST).
* @returns converted the value if it's common rule or `undefined` if it's not.
*/
public static customRunConverter(
rule: GrammarAST.AbstractRule,
input: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_cstNode: CstNode
): ValueType | undefined {
let regex: RegExp | undefined;
switch (rule.name) {
case 'ACC_DESCR': {
regex = new RegExp(accessibilityDescrRegex.source);
break;
}
case 'ACC_TITLE': {
regex = new RegExp(accessibilityTitleRegex.source);
break;
}
case 'TITLE': {
regex = new RegExp(titleRegex.source);
break;
}
}
if (regex === undefined) {
return undefined;
}
const match = regex.exec(input);
if (match === null) {
return undefined;
}
// single line title, accTitle, accDescr
if (match[1] !== undefined) {
return match[1].trim().replaceAll(/[\t ]{2,}/gm, ' ');
}
// multi line accDescr
if (match[2] !== undefined) {
return match[2]
.replaceAll(/^\s*/gm, '')
.replaceAll(/\s+$/gm, '')
.replaceAll(/[\t ]{2,}/gm, ' ')
.replaceAll(/[\n\r]{2,}/gm, '\n');
}
return undefined;
}
}

View File

@@ -0,0 +1,2 @@
export * from './commonLexer.js';
export * from './commonValueConverters.js';

View File

@@ -0,0 +1,6 @@
export * from './generated/ast.js';
export * from './generated/grammar.js';
export * from './generated/module.js';
export * from './common/index.js';
export * from './info/index.js';

View File

@@ -0,0 +1 @@
export * from './infoModule.js';

View File

@@ -0,0 +1,9 @@
grammar Info
import "../common/common";
entry Info:
NEWLINE*
"info" NEWLINE*
("showInfo" NEWLINE*)?
TitleAndAccessibilities?
;

View File

@@ -0,0 +1,72 @@
import type {
DefaultSharedModuleContext,
LangiumServices,
LangiumSharedServices,
Module,
PartialLangiumServices,
} from 'langium';
import { EmptyFileSystem, createDefaultModule, createDefaultSharedModule, inject } from 'langium';
import { MermaidGeneratedSharedModule, InfoGeneratedModule } from '../generated/module.js';
import { CommonLexer } from '../common/commonLexer.js';
import { CommonValueConverter } from '../common/commonValueConverters.js';
import { InfoTokenBuilder } from './infoTokenBuilder.js';
/**
* Declaration of `Info` services.
*/
type InfoAddedServices = {
parser: {
Lexer: CommonLexer;
TokenBuilder: InfoTokenBuilder;
ValueConverter: CommonValueConverter;
};
};
/**
* Union of Langium default services and `Info` services.
*/
export type InfoServices = LangiumServices & InfoAddedServices;
/**
* Dependency injection module that overrides Langium default services and
* contributes the declared `Info` services.
*/
export const InfoModule: Module<InfoServices, PartialLangiumServices & InfoAddedServices> = {
parser: {
Lexer: (services) => new CommonLexer(services),
TokenBuilder: () => new InfoTokenBuilder(),
ValueConverter: () => new CommonValueConverter(),
},
};
/**
* Create the full set of services required by Langium.
*
* First inject the shared services by merging two modules:
* - Langium default shared services
* - Services generated by langium-cli
*
* Then inject the language-specific services by merging three modules:
* - Langium default language-specific services
* - Services generated by langium-cli
* - Services specified in this file
* @param context - Optional module context with the LSP connection
* @returns An object wrapping the shared services and the language-specific services
*/
export function createInfoServices(context: DefaultSharedModuleContext = EmptyFileSystem): {
shared: LangiumSharedServices;
Info: InfoServices;
} {
const shared: LangiumSharedServices = inject(
createDefaultSharedModule(context),
MermaidGeneratedSharedModule
);
const Info: InfoServices = inject(
createDefaultModule({ shared }),
InfoGeneratedModule,
InfoModule
);
shared.ServiceRegistry.register(Info);
return { shared, Info };
}

View File

@@ -0,0 +1,24 @@
import type { GrammarAST, Stream, TokenBuilderOptions } from 'langium';
import { DefaultTokenBuilder } from 'langium';
import type { TokenType } from '../chevrotainWrapper.js';
export class InfoTokenBuilder extends DefaultTokenBuilder {
protected override buildKeywordTokens(
rules: Stream<GrammarAST.AbstractRule>,
terminalTokens: TokenType[],
options?: TokenBuilderOptions
): TokenType[] {
const tokenTypes: TokenType[] = super.buildKeywordTokens(rules, terminalTokens, options);
// to restrict users, they mustn't have any non-whitespace characters after the keyword.
tokenTypes.forEach((tokenType: TokenType): void => {
if (
(tokenType.name === 'info' || tokenType.name === 'showInfo') &&
tokenType.PATTERN !== undefined
) {
tokenType.PATTERN = new RegExp(tokenType.PATTERN.toString() + '(?!\\S)');
}
});
return tokenTypes;
}
}

View File

@@ -0,0 +1,43 @@
import type { LangiumParser, ParseResult } from 'langium';
import type { Info } from './index.js';
import { createInfoServices } from './language/index.js';
export type DiagramAST = Info;
const parsers: Record<string, LangiumParser> = {};
const initializers = {
info: () => {
// Will have to make parse async to use this. Can try later...
// const { createInfoServices } = await import('./language/info/index.js');
const parser = createInfoServices().Info.parser.LangiumParser;
parsers['info'] = parser;
},
} as const;
export function parse(diagramType: 'info', text: string): Info;
export function parse<T extends DiagramAST>(
diagramType: keyof typeof initializers,
text: string
): T {
const initializer = initializers[diagramType];
if (!initializer) {
throw new Error(`Unknown diagram type: ${diagramType}`);
}
if (!parsers[diagramType]) {
initializer();
}
const parser: LangiumParser = parsers[diagramType];
const result: ParseResult<T> = parser.parse<T>(text);
if (result.lexerErrors.length > 0 || result.parserErrors.length > 0) {
throw new MermaidParseError(result);
}
return result.value;
}
export class MermaidParseError extends Error {
constructor(public result: ParseResult<DiagramAST>) {
const lexerErrors: string = result.lexerErrors.map((err) => err.message).join('\n');
const parserErrors: string = result.parserErrors.map((err) => err.message).join('\n');
super(`Parsing failed: ${lexerErrors} ${parserErrors}`);
}
}

View File

@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest';
import type { LangiumParser, ParseResult } from 'langium';
import type { InfoServices } from '../src/language/index.js';
import { Info, createInfoServices } from '../src/language/index.js';
import { noErrorsOrAlternatives } from './test-util.js';
const services: InfoServices = createInfoServices().Info;
const parser: LangiumParser = services.parser.LangiumParser;
function createInfoTestServices(): {
services: InfoServices;
parse: (input: string) => ParseResult<Info>;
} {
const parse = (input: string) => {
return parser.parse<Info>(input);
};
return { services, parse };
}
describe('info', () => {
const { parse } = createInfoTestServices();
it.each([
`info`,
`
info`,
`info
`,
`
info
`,
])('should handle empty info', (context: string) => {
const result = parse(context);
noErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Info);
});
it.each([
`info showInfo`,
`
info showInfo`,
`info
showInfo
`,
`
info
showInfo
`,
])('should handle showInfo', (context: string) => {
const result = parse(context);
noErrorsOrAlternatives(result);
expect(result.value.$type).toBe(Info);
});
});

View File

@@ -0,0 +1,18 @@
import { expect, vi } from 'vitest';
import type { ParseResult } from 'langium';
const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined);
/**
* A helper test function that validate that the result doesn't have errors
* or any ambiguous alternatives from chevrotain.
*
* @param result - the result `parse` function.
*/
export function noErrorsOrAlternatives(result: ParseResult) {
expect(result.lexerErrors).toHaveLength(0);
expect(result.parserErrors).toHaveLength(0);
expect(consoleMock).not.toHaveBeenCalled();
consoleMock.mockReset();
}

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist",
"allowJs": false,
"preserveSymlinks": false,
"strictPropertyInitialization": false
},
"include": ["./src/**/*.ts", "./tests/**/*.ts"],
"typeRoots": ["./src/types"]
}

6595
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,12 @@
#!/usr/bin/env bash
# Fail on errors
set -euxo pipefail
# Increase heap size
export NODE_OPTIONS="--max_old_space_size=4096"
# Build mermaid
pnpm build
# Clone the Mermaid Live Editor repository

View File

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

View File

@@ -1,7 +1,7 @@
import jison from './.vite/jisonPlugin.js';
import jsonSchemaPlugin from './.vite/jsonSchemaPlugin.js';
import typescript from '@rollup/plugin-typescript';
import { defineConfig } from 'vitest/config';
import { defaultExclude, defineConfig } from 'vitest/config';
export default defineConfig({
resolve: {
@@ -22,7 +22,7 @@ export default defineConfig({
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
reportsDirectory: './coverage/vitest',
exclude: ['**/node_modules/**', '**/tests/**', '**/__mocks__/**'],
exclude: [...defaultExclude, './tests/**', '**/__mocks__/**', '**/generated/'],
},
},
build: {